Skip to content

Commit 61917e2

Browse files
authored
Merge pull request #688 from mfinean/refactor/split-bench-cfg-into-focused-modules
Split bench_cfg.py into focused configuration modules
2 parents 77c452a + d57f57d commit 61917e2

12 files changed

Lines changed: 1100 additions & 680 deletions

bencher/bench_cfg.py

Lines changed: 0 additions & 680 deletions
This file was deleted.

bencher/bench_cfg/__init__.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Benchmark configuration package.
2+
3+
Configuration classes for benchmarking, organized by concern:
4+
5+
- CacheCfg: Caching (cache_results, cache_samples, only_plot, ...)
6+
- ExecutionCfg: Execution (repeats, level, executor, nightly, headless)
7+
- DisplayCfg: Output (print_bench_inputs, serve_pandas, ...)
8+
- VisualizationCfg: Plotting (auto_plot, plot_size, use_holoview, ...)
9+
- TimeCfg: History (over_time, run_tag, run_date, ...)
10+
- BenchPlotSrvCfg: Server (port, show, allow_ws_origin)
11+
- BenchRunCfg: Composes all above with backward-compatible flat access
12+
- BenchCfg: Full benchmark config with variables and metadata
13+
- DimsCfg: Dimension info extraction
14+
15+
Usage:
16+
# Flat access (backward compatible)
17+
cfg = BenchRunCfg(cache_results=True, repeats=5)
18+
cfg.auto_plot = False
19+
20+
# Grouped access
21+
cfg.cache.cache_samples = True
22+
cfg.execution.level = 3
23+
"""
24+
25+
from bencher.bench_cfg.server_cfg import BenchPlotSrvCfg
26+
from bencher.bench_cfg.cache_cfg import CacheCfg
27+
from bencher.bench_cfg.execution_cfg import ExecutionCfg
28+
from bencher.bench_cfg.display_cfg import DisplayCfg
29+
from bencher.bench_cfg.visualization_cfg import VisualizationCfg
30+
from bencher.bench_cfg.time_cfg import TimeCfg
31+
from bencher.bench_cfg.run_cfg import BenchRunCfg
32+
from bencher.bench_cfg.bench_cfg_class import BenchCfg
33+
from bencher.bench_cfg.dims_cfg import DimsCfg
34+
35+
__all__ = [
36+
"BenchPlotSrvCfg",
37+
"CacheCfg",
38+
"ExecutionCfg",
39+
"DisplayCfg",
40+
"VisualizationCfg",
41+
"TimeCfg",
42+
"BenchRunCfg",
43+
"BenchCfg",
44+
"DimsCfg",
45+
]
Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
"""Complete configuration for a benchmark protocol."""
2+
3+
from __future__ import annotations
4+
5+
from typing import List, Optional, Any, Union
6+
7+
import param
8+
import panel as pn
9+
10+
from bencher.bench_cfg.run_cfg import BenchRunCfg
11+
from bencher.variables.sweep_base import hash_sha1, describe_variable
12+
from bencher.variables.time import TimeSnapshot, TimeEvent
13+
from bencher.variables.results import OptDir
14+
from bencher.results.laxtex_result import to_latex
15+
16+
17+
class BenchCfg(BenchRunCfg):
18+
"""Full benchmark config: input/result variables, metadata, and sweep parameters."""
19+
20+
input_vars = param.List(
21+
default=None,
22+
doc="A list of ParameterizedSweep variables to perform a parameter sweep over",
23+
)
24+
result_vars = param.List(
25+
default=None,
26+
doc="A list of ParameterizedSweep results collect and plot.",
27+
)
28+
29+
const_vars = param.List(
30+
default=None,
31+
doc="Variables to keep constant but are different from the default value",
32+
)
33+
34+
result_hmaps = param.List(default=None, doc="a list of holomap results")
35+
36+
meta_vars = param.List(
37+
default=None,
38+
doc="Meta variables such as recording time and repeat id",
39+
)
40+
all_vars = param.List(
41+
default=None,
42+
doc="Stores a list of both the input_vars and meta_vars that are used to define a unique "
43+
"hash for the input",
44+
)
45+
iv_time = param.List(
46+
default=[],
47+
item_type=Union[TimeSnapshot, TimeEvent],
48+
doc="A parameter to represent the sampling the same inputs over time as a scalar type",
49+
)
50+
51+
iv_time_event = param.List(
52+
default=[],
53+
item_type=TimeEvent,
54+
doc="A parameter to represent the sampling the same inputs over time as a discrete type",
55+
)
56+
57+
# Note: over_time is inherited from BenchRunCfg via TimeCfg delegation
58+
name: Optional[str] = param.String(None, doc="The name of the benchmarkCfg")
59+
title: Optional[str] = param.String(None, doc="The title of the benchmark")
60+
raise_duplicate_exception: bool = param.Boolean(
61+
False, doc="Use this while debugging if filename generation is unique"
62+
)
63+
bench_name: Optional[str] = param.String(
64+
None, doc="The name of the benchmark and the name of the save folder"
65+
)
66+
description: Optional[str] = param.String(
67+
None,
68+
doc="A place to store a longer description of the function of the benchmark",
69+
)
70+
post_description: Optional[str] = param.String(
71+
None, doc="A place to comment on the output of the graphs"
72+
)
73+
74+
has_results: bool = param.Boolean(
75+
False,
76+
doc="If this config has results, true, otherwise used to store titles and other bench "
77+
"metadata",
78+
)
79+
80+
pass_repeat: bool = param.Boolean(
81+
False,
82+
doc="By default do not pass the kwarg 'repeat' to the benchmark function. Set to true "
83+
"if you want the benchmark function to be passed the repeat number",
84+
)
85+
86+
tag: str = param.String(
87+
"",
88+
doc="Use tags to group different benchmarks together. By default benchmarks are "
89+
"considered distinct from each other and are identified by the hash of their name and "
90+
"inputs, constants and results and tag, but you can optionally change the hash value "
91+
"to only depend on the tag. This way you can have multiple unrelated benchmarks share "
92+
"values with each other based only on the tag value.",
93+
)
94+
95+
hash_value: str = param.String(
96+
"",
97+
doc="store the hash value of the config to avoid having to hash multiple times",
98+
)
99+
100+
plot_callbacks = param.List(
101+
None,
102+
doc="A callable that takes a BenchResult and returns panel representation of the results",
103+
)
104+
105+
def __init__(self, **params: Any) -> None:
106+
"""Initialize a BenchCfg with the given parameters.
107+
108+
Args:
109+
**params (Any): Parameters to set on the BenchCfg
110+
"""
111+
super().__init__(**params)
112+
self.plot_lib = None
113+
self.hmap_kdims = None
114+
self.iv_repeat = None
115+
116+
def hash_persistent(self, include_repeats: bool) -> str:
117+
"""Generate a persistent hash for the benchmark configuration.
118+
119+
Overrides the default hash function because the default hash function does not
120+
return the same value for the same inputs. This method references only stable
121+
variables that are consistent across instances of BenchCfg with the same
122+
configuration.
123+
124+
Args:
125+
include_repeats (bool): Whether to include repeats as part of the hash
126+
(True by default except when using the sample cache)
127+
128+
Returns:
129+
str: A persistent hash value for the benchmark configuration
130+
"""
131+
132+
if include_repeats:
133+
# needed so that the historical xarray arrays are the same size
134+
repeats_hash = hash_sha1(self.repeats)
135+
else:
136+
repeats_hash = 0
137+
138+
hash_val = hash_sha1(
139+
(
140+
hash_sha1(str(self.bench_name)),
141+
hash_sha1(str(self.title)),
142+
hash_sha1(self.over_time),
143+
repeats_hash,
144+
hash_sha1(self.tag),
145+
)
146+
)
147+
all_vars = self.input_vars + self.result_vars
148+
for v in all_vars:
149+
hash_val = hash_sha1((hash_val, v.hash_persistent()))
150+
151+
for v in self.const_vars:
152+
hash_val = hash_sha1((v[0].hash_persistent(), hash_sha1(v[1])))
153+
154+
return hash_val
155+
156+
def inputs_as_str(self) -> List[str]:
157+
"""Get a list of input variable names.
158+
159+
Returns:
160+
List[str]: List of the names of input variables
161+
"""
162+
return [i.name for i in self.input_vars]
163+
164+
def to_latex(self) -> Optional[pn.pane.LaTeX]:
165+
"""Convert benchmark configuration to LaTeX representation.
166+
167+
Returns:
168+
Optional[pn.pane.LaTeX]: LaTeX representation of the benchmark configuration
169+
"""
170+
return to_latex(self)
171+
172+
def describe_sweep(
173+
self, width: int = 800, accordion: bool = True
174+
) -> Union[pn.pane.Markdown, pn.Column]:
175+
"""Produce a markdown summary of the sweep settings.
176+
177+
Args:
178+
width (int): Width of the markdown panel in pixels. Defaults to 800.
179+
accordion (bool): Whether to wrap the description in an accordion. Defaults to True.
180+
181+
Returns:
182+
Union[pn.pane.Markdown, pn.Column]: Panel containing the sweep description
183+
"""
184+
185+
latex = self.to_latex()
186+
desc = pn.pane.Markdown(self.describe_benchmark(), width=width)
187+
if accordion:
188+
desc = pn.Accordion(("Expand Full Data Collection Parameters", desc))
189+
190+
sentence = self.sweep_sentence()
191+
if latex is not None:
192+
return pn.Column(sentence, latex, desc)
193+
return pn.Column(sentence, desc)
194+
195+
def sweep_sentence(self) -> pn.pane.Markdown:
196+
"""Generate a concise summary sentence of the sweep configuration.
197+
198+
Returns:
199+
pn.pane.Markdown: A panel containing a markdown summary sentence
200+
"""
201+
inputs = " by ".join([iv.name for iv in self.all_vars])
202+
203+
all_vars_lens = [len(iv.values()) for iv in reversed(self.all_vars)]
204+
if len(all_vars_lens) == 1:
205+
all_vars_lens.append(1)
206+
result_sizes = "x".join([str(iv) for iv in all_vars_lens])
207+
results = ", ".join([rv.name for rv in self.result_vars])
208+
209+
return pn.pane.Markdown(
210+
f"Sweeping {inputs} to generate a {result_sizes} result dataframe containing "
211+
f"{results}. "
212+
)
213+
214+
def describe_benchmark(self) -> str:
215+
"""Generate a detailed string summary of the inputs and results from a BenchCfg.
216+
217+
Returns:
218+
str: Comprehensive summary of BenchCfg
219+
"""
220+
benchmark_sampling_str = ["```text"]
221+
benchmark_sampling_str.append("")
222+
223+
benchmark_sampling_str.append("Input Variables:")
224+
for iv in self.input_vars:
225+
benchmark_sampling_str.extend(describe_variable(iv, True))
226+
227+
if self.const_vars and (self.summarise_constant_inputs):
228+
benchmark_sampling_str.append("\nConstants:")
229+
for cv in self.const_vars:
230+
benchmark_sampling_str.extend(describe_variable(cv[0], False, cv[1]))
231+
232+
benchmark_sampling_str.append("\nResult Variables:")
233+
for rv in self.result_vars:
234+
benchmark_sampling_str.extend(describe_variable(rv, False))
235+
236+
benchmark_sampling_str.append("\nMeta Variables:")
237+
benchmark_sampling_str.append(f" run date: {self.run_date}")
238+
if self.run_tag:
239+
benchmark_sampling_str.append(f" run tag: {self.run_tag}")
240+
if self.level is not None:
241+
benchmark_sampling_str.append(f" bench level: {self.level}")
242+
benchmark_sampling_str.append(f" cache_results: {self.cache_results}")
243+
benchmark_sampling_str.append(f" cache_samples {self.cache_samples}")
244+
benchmark_sampling_str.append(f" only_hash_tag: {self.only_hash_tag}")
245+
benchmark_sampling_str.append(f" executor: {self.executor}")
246+
247+
for mv in self.meta_vars:
248+
benchmark_sampling_str.extend(describe_variable(mv, True))
249+
250+
benchmark_sampling_str.append("```")
251+
252+
benchmark_sampling_str = "\n".join(benchmark_sampling_str)
253+
return benchmark_sampling_str
254+
255+
def to_title(self, panel_name: Optional[str] = None) -> pn.pane.Markdown:
256+
"""Create a markdown panel with the benchmark title.
257+
258+
Args:
259+
panel_name (Optional[str]): The name for the panel. Defaults to the benchmark title.
260+
261+
Returns:
262+
pn.pane.Markdown: A panel with the benchmark title as a heading
263+
"""
264+
if panel_name is None:
265+
panel_name = self.title
266+
return pn.pane.Markdown(f"# {self.title}", name=panel_name)
267+
268+
def to_description(self, width: int = 800) -> pn.pane.Markdown:
269+
"""Create a markdown panel with the benchmark description.
270+
271+
Args:
272+
width (int): Width of the markdown panel in pixels. Defaults to 800.
273+
274+
Returns:
275+
pn.pane.Markdown: A panel with the benchmark description
276+
"""
277+
return pn.pane.Markdown(f"{self.description}", width=width)
278+
279+
def to_post_description(self, width: int = 800) -> pn.pane.Markdown:
280+
"""Create a markdown panel with the benchmark post-description.
281+
282+
Args:
283+
width (int): Width of the markdown panel in pixels. Defaults to 800.
284+
285+
Returns:
286+
pn.pane.Markdown: A panel with the benchmark post-description
287+
"""
288+
return pn.pane.Markdown(f"{self.post_description}", width=width)
289+
290+
def to_sweep_summary(
291+
self,
292+
name: Optional[str] = None,
293+
description: bool = True,
294+
describe_sweep: bool = True,
295+
results_suffix: bool = True,
296+
title: bool = True,
297+
) -> pn.Column:
298+
"""Produce panel output summarising the title, description and sweep setting.
299+
300+
Args:
301+
name (Optional[str]): Name for the panel. Defaults to benchmark title or
302+
"Data Collection Parameters" if title is False.
303+
description (bool): Whether to include the benchmark description. Defaults to True.
304+
describe_sweep (bool): Whether to include the sweep description. Defaults to True.
305+
results_suffix (bool): Whether to add a "Results:" heading. Defaults to True.
306+
title (bool): Whether to include the benchmark title. Defaults to True.
307+
308+
Returns:
309+
pn.Column: A panel with the benchmark summary
310+
"""
311+
if name is None:
312+
if title:
313+
name = self.title
314+
else:
315+
name = "Data Collection Parameters"
316+
col = pn.Column(name=name)
317+
if title:
318+
col.append(self.to_title())
319+
if self.description is not None and description:
320+
col.append(self.to_description())
321+
if describe_sweep:
322+
col.append(self.describe_sweep())
323+
if results_suffix:
324+
col.append(pn.pane.Markdown("## Results:"))
325+
return col
326+
327+
def optuna_targets(self, as_var: bool = False) -> List[Any]:
328+
"""Get the list of result variables that are optimization targets.
329+
330+
Args:
331+
as_var (bool): If True, return the variable objects rather than their names.
332+
Defaults to False.
333+
334+
Returns:
335+
List[Any]: List of result variable names or objects that are optimization targets
336+
"""
337+
targets = []
338+
for rv in self.result_vars:
339+
if hasattr(rv, "direction") and rv.direction != OptDir.none:
340+
if as_var:
341+
targets.append(rv)
342+
else:
343+
targets.append(rv.name)
344+
return targets

0 commit comments

Comments
 (0)