|
| 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