holobench 1.25.2__py3-none-any.whl → 1.27.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bencher/bench_report.py +6 -109
- bencher/example/__init__.py +0 -0
- bencher/example/benchmark_data.py +196 -0
- bencher/example/example_all.py +45 -0
- bencher/example/example_categorical.py +99 -0
- bencher/example/example_composable_container.py +106 -0
- bencher/example/example_composable_container2.py +160 -0
- bencher/example/example_consts.py +39 -0
- bencher/example/example_custom_sweep.py +59 -0
- bencher/example/example_custom_sweep2.py +42 -0
- bencher/example/example_docs.py +34 -0
- bencher/example/example_filepath.py +27 -0
- bencher/example/example_float3D.py +101 -0
- bencher/example/example_float_cat.py +99 -0
- bencher/example/example_floats.py +89 -0
- bencher/example/example_floats2D.py +93 -0
- bencher/example/example_holosweep.py +98 -0
- bencher/example/example_holosweep_objects.py +111 -0
- bencher/example/example_holosweep_tap.py +144 -0
- bencher/example/example_image.py +155 -0
- bencher/example/example_levels.py +181 -0
- bencher/example/example_levels2.py +37 -0
- bencher/example/example_pareto.py +53 -0
- bencher/example/example_sample_cache.py +85 -0
- bencher/example/example_sample_cache_context.py +116 -0
- bencher/example/example_simple.py +134 -0
- bencher/example/example_simple_bool.py +35 -0
- bencher/example/example_simple_cat.py +48 -0
- bencher/example/example_simple_float.py +28 -0
- bencher/example/example_simple_float2d.py +29 -0
- bencher/example/example_strings.py +47 -0
- bencher/example/example_time_event.py +63 -0
- bencher/example/example_video.py +118 -0
- bencher/example/example_workflow.py +189 -0
- bencher/example/experimental/example_bokeh_plotly.py +38 -0
- bencher/example/experimental/example_hover_ex.py +45 -0
- bencher/example/experimental/example_hvplot_explorer.py +39 -0
- bencher/example/experimental/example_interactive.py +75 -0
- bencher/example/experimental/example_streamnd.py +49 -0
- bencher/example/experimental/example_streams.py +36 -0
- bencher/example/experimental/example_template.py +40 -0
- bencher/example/experimental/example_updates.py +84 -0
- bencher/example/experimental/example_vector.py +84 -0
- bencher/example/meta/example_meta.py +171 -0
- bencher/example/meta/example_meta_cat.py +25 -0
- bencher/example/meta/example_meta_float.py +23 -0
- bencher/example/meta/example_meta_levels.py +26 -0
- bencher/example/optuna/example_optuna.py +78 -0
- bencher/example/shelved/example_float2D_scatter.py +109 -0
- bencher/example/shelved/example_float3D_cone.py +96 -0
- bencher/example/shelved/example_kwargs.py +63 -0
- bencher/plotting/__init__.py +0 -0
- bencher/plotting/plot_filter.py +110 -0
- bencher/plotting/plt_cnt_cfg.py +75 -0
- bencher/results/__init__.py +0 -0
- bencher/results/bench_result.py +94 -0
- bencher/results/bench_result_base.py +476 -0
- bencher/results/composable_container/__init__.py +0 -0
- bencher/results/composable_container/composable_container_base.py +73 -0
- bencher/results/composable_container/composable_container_panel.py +39 -0
- bencher/results/composable_container/composable_container_video.py +184 -0
- bencher/results/float_formatter.py +44 -0
- bencher/results/holoview_result.py +753 -0
- bencher/results/optuna_result.py +354 -0
- bencher/results/panel_result.py +41 -0
- bencher/results/plotly_result.py +65 -0
- bencher/results/video_result.py +38 -0
- bencher/results/video_summary.py +222 -0
- bencher/variables/__init__.py +0 -0
- bencher/variables/inputs.py +202 -0
- bencher/variables/parametrised_sweep.py +208 -0
- bencher/variables/results.py +214 -0
- bencher/variables/sweep_base.py +162 -0
- bencher/variables/time.py +92 -0
- holobench-1.27.0.data/data/share/ament_index/resource_index/packages/bencher +0 -0
- holobench-1.27.0.data/data/share/bencher/package.xml +33 -0
- {holobench-1.25.2.dist-info → holobench-1.27.0.dist-info}/METADATA +5 -5
- holobench-1.27.0.dist-info/RECORD +93 -0
- holobench-1.25.2.dist-info/RECORD +0 -18
- {holobench-1.25.2.dist-info → holobench-1.27.0.dist-info}/LICENSE +0 -0
- {holobench-1.25.2.dist-info → holobench-1.27.0.dist-info}/WHEEL +0 -0
- {holobench-1.25.2.dist-info → holobench-1.27.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,181 @@
|
|
1
|
+
import bencher as bch
|
2
|
+
from bencher.utils import int_to_col
|
3
|
+
|
4
|
+
import math
|
5
|
+
import holoviews as hv
|
6
|
+
from typing import Any, List
|
7
|
+
import panel as pn
|
8
|
+
from holoviews import opts
|
9
|
+
|
10
|
+
|
11
|
+
class LevelsExample(bch.ParametrizedSweep):
|
12
|
+
xval = bch.FloatSweep(bounds=[0, 3.14])
|
13
|
+
yval = bch.FloatSweep(bounds=[0, 3.14])
|
14
|
+
level = bch.IntSweep(default=1, bounds=[1, 6])
|
15
|
+
|
16
|
+
output = bch.ResultVar(units="v")
|
17
|
+
hmap = bch.ResultHmap()
|
18
|
+
|
19
|
+
def __call__(self, **kwargs: Any) -> Any:
|
20
|
+
self.update_params_from_kwargs(**kwargs)
|
21
|
+
self.output = math.sin(self.xval) + math.cos(self.yval)
|
22
|
+
self.hmap = hv.Points((self.xval, self.yval)).opts(
|
23
|
+
marker="o", size=110 - self.level * 20, color=int_to_col(self.level - 1)
|
24
|
+
)
|
25
|
+
|
26
|
+
return self.get_results_values_as_dict()
|
27
|
+
|
28
|
+
|
29
|
+
class RunWithLevel(bch.ParametrizedSweep):
|
30
|
+
level = bch.IntSweep(default=1, bounds=[1, 8])
|
31
|
+
dimensions = bch.IntSweep(default=1, bounds=[1, 2])
|
32
|
+
|
33
|
+
level_samples = bch.ResultVar()
|
34
|
+
|
35
|
+
def __call__(self, **kwargs) -> dict():
|
36
|
+
self.update_params_from_kwargs(**kwargs)
|
37
|
+
|
38
|
+
self.level_samples = int(
|
39
|
+
pow(
|
40
|
+
len(bch.FloatSweep(bounds=[0, 1]).with_level(self.level).values()),
|
41
|
+
self.dimensions,
|
42
|
+
)
|
43
|
+
)
|
44
|
+
return self.get_results_values_as_dict()
|
45
|
+
|
46
|
+
|
47
|
+
def run_with_dim(bench: bch.Bench, dims: List[bch.SweepBase]) -> List[bch.BenchResult]:
|
48
|
+
results = []
|
49
|
+
for level in range(1, 6):
|
50
|
+
print(level)
|
51
|
+
res = bench.plot_sweep(
|
52
|
+
f"Level:{level}",
|
53
|
+
input_vars=dims,
|
54
|
+
const_vars=LevelsExample.get_input_defaults(
|
55
|
+
[LevelsExample.param.level.with_const(level)]
|
56
|
+
),
|
57
|
+
result_vars=[LevelsExample.param.output, LevelsExample.param.hmap],
|
58
|
+
run_cfg=bch.BenchRunCfg(level=level, auto_plot=False),
|
59
|
+
)
|
60
|
+
|
61
|
+
results.append(res)
|
62
|
+
return results
|
63
|
+
|
64
|
+
|
65
|
+
def run_levels_1D(bench: bch.Bench) -> bch.Bench:
|
66
|
+
results = run_with_dim(bench, [LevelsExample.param.xval])
|
67
|
+
bench.report.append_title("Using Levels to define sample density")
|
68
|
+
|
69
|
+
bench1 = bch.Bench("levels", RunWithLevel(), run_cfg=bch.BenchRunCfg(auto_plot=False))
|
70
|
+
res1 = bench1.plot_sweep("Levels", input_vars=[RunWithLevel.param.level])
|
71
|
+
|
72
|
+
bench.report.append_markdown(
|
73
|
+
"Sample levels let you perform parameter sweeps without having to decide how many samples to take when defining the class. If you perform a sweep at level 2, then all the points are reused when sampling at level 3. The higher levels reuse the points from lower levels to avoid having to recompute potentially expensive samples. The other advantage is that it enables a workflow where you can quickly see the results of the sweep at a low resolution to sense check the code, and then run it at a high level to get the fidelity you want. When calling a sweep at a high level, you can publish the intermediate lower level results as the computiation continues so that you can track the progress of the computation and end the sweep early when you have sufficient resolution",
|
74
|
+
width=600,
|
75
|
+
)
|
76
|
+
row = pn.Row()
|
77
|
+
row.append(res1.to_table())
|
78
|
+
# row.append(res1.to_curve().opts(shared_axes=False))
|
79
|
+
row.append(res1.to_curve())
|
80
|
+
|
81
|
+
bench.report.append(row)
|
82
|
+
|
83
|
+
bench.report.append_markdown(
|
84
|
+
"Level 1 returns a single point at the lower bound of the parameter. Level 2 uses the uppper and lower bounds of the parameter. All subsequent levels are created by adding a sample between each previously calculated sample to ensure that all previous values can be reused while retaining an equal sample spacing. The following plots show the sample points as circles and the corresponding plot of a sin function sampled at that level.",
|
85
|
+
width=600,
|
86
|
+
)
|
87
|
+
|
88
|
+
combined_pts = hv.Overlay()
|
89
|
+
combined_curve = hv.Overlay()
|
90
|
+
for it, r in enumerate(results):
|
91
|
+
lvl = it + 1
|
92
|
+
row = pn.Row()
|
93
|
+
pts = r.to_holomap().overlay().opts(title=f"Sample Points for level: {lvl}", height=300)
|
94
|
+
ds = r.to_hv_dataset()
|
95
|
+
crv = r.to_curve_ds(ds.data).opts(shared_axes=False, height=300) * r.to_hv_dataset(
|
96
|
+
bch.ReduceType.NONE
|
97
|
+
).to(hv.Scatter).opts(
|
98
|
+
title=f"Function Values for level: {lvl}", size=5, height=300, shared_axes=False
|
99
|
+
)
|
100
|
+
|
101
|
+
combined_pts *= pts
|
102
|
+
combined_curve *= crv
|
103
|
+
row.append(pts)
|
104
|
+
row.append(crv)
|
105
|
+
bench.report.append_markdown(f"## {r.bench_cfg.title}")
|
106
|
+
bench.report.append(row)
|
107
|
+
|
108
|
+
bench.report.append_markdown(
|
109
|
+
"This plot overlays the previous plots into a single image. It shows how each level overlaps the previous level"
|
110
|
+
)
|
111
|
+
|
112
|
+
bench.report.append(pn.Row(combined_pts, combined_curve))
|
113
|
+
return bench
|
114
|
+
|
115
|
+
|
116
|
+
def run_levels_2D(bench: bch.Bench) -> bch.Bench:
|
117
|
+
results = run_with_dim(bench, [LevelsExample.param.xval, LevelsExample.param.yval])
|
118
|
+
bench.report.append_markdown("# Using Levels to define 2D sample density", "Levels 2D")
|
119
|
+
|
120
|
+
bench1 = bch.Bench("lol", RunWithLevel(), run_cfg=bch.BenchRunCfg(auto_plot=False))
|
121
|
+
res1 = bench1.plot_sweep(
|
122
|
+
"Levels",
|
123
|
+
input_vars=[RunWithLevel.param.level],
|
124
|
+
const_vars=[RunWithLevel.param.dimensions.with_const(2)],
|
125
|
+
)
|
126
|
+
row = pn.Row()
|
127
|
+
row.append(res1.to_table())
|
128
|
+
# row.append(res1.to_curve().opts(shared_axes=False))
|
129
|
+
row.append(res1.to_curve())
|
130
|
+
|
131
|
+
bench.report.append(row)
|
132
|
+
|
133
|
+
for it, r in enumerate(results):
|
134
|
+
lvl = it + 1
|
135
|
+
row = pn.Row()
|
136
|
+
bench.report.append_markdown(f"## {r.bench_cfg.title}")
|
137
|
+
row.append(
|
138
|
+
r.to_holomap()
|
139
|
+
.overlay()
|
140
|
+
.opts(title=f"Sample Points for level: {lvl}", shared_axes=False)
|
141
|
+
)
|
142
|
+
row.append(
|
143
|
+
r.to_heatmap_single(r.bench_cfg.result_vars[0], bch.ReduceType.NONE).opts(
|
144
|
+
title=f"Function Value Heatmap for level: {lvl}", shared_axes=False
|
145
|
+
)
|
146
|
+
)
|
147
|
+
bench.report.append(row)
|
148
|
+
|
149
|
+
bench.report.append_markdown(
|
150
|
+
"This plot overlays the previous plots into a single image. It shows how each level overlaps the previous level"
|
151
|
+
)
|
152
|
+
overlay = hv.Overlay()
|
153
|
+
for lvl, r in enumerate(results):
|
154
|
+
overlay *= (
|
155
|
+
r.to_holomap()
|
156
|
+
.overlay()
|
157
|
+
.opts(width=1000, height=1000, show_legend=False, shared_axes=False)
|
158
|
+
)
|
159
|
+
|
160
|
+
bench.report.append(overlay)
|
161
|
+
return bench
|
162
|
+
|
163
|
+
|
164
|
+
def run_levels(
|
165
|
+
run_cfg: bch.BenchRunCfg = bch.BenchRunCfg(), report: bch.BenchReport = bch.BenchReport()
|
166
|
+
) -> bch.Bench:
|
167
|
+
hv.extension("bokeh")
|
168
|
+
opts.defaults(
|
169
|
+
opts.Curve(show_legend=False),
|
170
|
+
opts.Points(show_legend=False),
|
171
|
+
)
|
172
|
+
|
173
|
+
bench = bch.Bench("Levels", LevelsExample(), run_cfg=run_cfg, report=report)
|
174
|
+
bench = run_levels_1D(bench)
|
175
|
+
bench = run_levels_2D(bench)
|
176
|
+
|
177
|
+
return bench
|
178
|
+
|
179
|
+
|
180
|
+
if __name__ == "__main__":
|
181
|
+
run_levels().report.show()
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import bencher as bch
|
2
|
+
|
3
|
+
|
4
|
+
class Square(bch.ParametrizedSweep):
|
5
|
+
"""An example of a datatype with an integer and float parameter"""
|
6
|
+
|
7
|
+
x = bch.FloatSweep(default=0, bounds=[0, 6])
|
8
|
+
|
9
|
+
result = bch.ResultVar("ul", doc="Square of x")
|
10
|
+
|
11
|
+
def __call__(self, **kwargs) -> dict:
|
12
|
+
self.update_params_from_kwargs(**kwargs)
|
13
|
+
self.result = self.x * self.x
|
14
|
+
return self.get_results_values_as_dict()
|
15
|
+
|
16
|
+
|
17
|
+
def example_levels2(run_cfg: bch.BenchRunCfg = None, report: bch.BenchReport = None) -> bch.Bench:
|
18
|
+
"""This example shows how to define a custom set of value to sample from intead of a uniform sweep
|
19
|
+
|
20
|
+
Args:
|
21
|
+
run_cfg (BenchRunCfg): configuration of how to perform the param sweep
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
Bench: results of the parameter sweep
|
25
|
+
"""
|
26
|
+
|
27
|
+
bench = Square().to_bench(run_cfg=run_cfg, report=report)
|
28
|
+
|
29
|
+
# These are all equivalent
|
30
|
+
bench.plot_sweep(input_vars=[Square.param.x.with_level(run_cfg.level, 3)])
|
31
|
+
bench.plot_sweep(input_vars=[bch.p("x", max_level=3)])
|
32
|
+
|
33
|
+
return bench
|
34
|
+
|
35
|
+
|
36
|
+
if __name__ == "__main__":
|
37
|
+
example_levels2(bch.BenchRunCfg(level=4)).report.show()
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# pylint: disable=duplicate-code
|
2
|
+
|
3
|
+
import bencher as bch
|
4
|
+
|
5
|
+
# All the examples will be using the data structures and benchmark function defined in this file
|
6
|
+
from bencher.example.benchmark_data import ExampleBenchCfgIn, ExampleBenchCfgOut, bench_function
|
7
|
+
|
8
|
+
|
9
|
+
def example_pareto(
|
10
|
+
run_cfg: bch.BenchRunCfg = bch.BenchRunCfg(), report: bch.BenchReport = bch.BenchReport()
|
11
|
+
) -> bch.Bench:
|
12
|
+
"""Example of how to calculate the pareto front of a parameter sweep
|
13
|
+
|
14
|
+
Args:
|
15
|
+
run_cfg (BenchRunCfg): configuration of how to perform the param sweep
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
Bench: results of the parameter sweep
|
19
|
+
"""
|
20
|
+
run_cfg.use_optuna = True
|
21
|
+
|
22
|
+
bench = bch.Bench(
|
23
|
+
"Multi-objective optimisation",
|
24
|
+
bench_function,
|
25
|
+
ExampleBenchCfgIn,
|
26
|
+
run_cfg=run_cfg,
|
27
|
+
report=report,
|
28
|
+
)
|
29
|
+
|
30
|
+
res = bench.plot_sweep(
|
31
|
+
title="Pareto Optimisation with Optuna",
|
32
|
+
description="This example shows how to plot the pareto front of the tradeoff between multiple criteria. When multiple result variable are defined, and use_optuna=True a pareto plot and the relative importance of each input variable on the output criteria is plotted. A summary of the points on the pareto front is printed as well. You can use the pareto plot to decide the how to trade off one objective for another. Pareto plots are suppored for 2D and 3D. If you have more than 3 result variables the first 3 are selected for the pareto plot. Plotting 4D surfaces is left as an exercise to the reader",
|
33
|
+
input_vars=[
|
34
|
+
ExampleBenchCfgIn.param.theta,
|
35
|
+
ExampleBenchCfgIn.param.offset,
|
36
|
+
],
|
37
|
+
result_vars=[ExampleBenchCfgOut.param.out_sin, ExampleBenchCfgOut.param.out_cos],
|
38
|
+
const_vars=ExampleBenchCfgIn.get_input_defaults(
|
39
|
+
[ExampleBenchCfgIn.param.noisy.with_const(True)]
|
40
|
+
),
|
41
|
+
post_description="""# Post Description
|
42
|
+
This is a slightly unusual way of doing pareto optimisation as we are not using a typical multi-objective optimisation algorithm [TODO, add example]. Instead we are performing a grid search and looking at the resulting pareto plot. The reason for doing a grid search instead of standard pareto optimisation is that we can produce more isolated plots of how an input affects an output which can help understanding of the parameter space. Future examples will show how to use grid search to bootstrap further optimisation with a multi objective optimiser""",
|
43
|
+
)
|
44
|
+
|
45
|
+
bench.report.append(res.to_optuna_plots())
|
46
|
+
return bench
|
47
|
+
|
48
|
+
|
49
|
+
if __name__ == "__main__":
|
50
|
+
run_cfg_ex = bch.BenchRunCfg()
|
51
|
+
run_cfg_ex.repeats = 2
|
52
|
+
run_cfg_ex.level = 2
|
53
|
+
example_pareto(run_cfg_ex).report.show()
|
@@ -0,0 +1,85 @@
|
|
1
|
+
import bencher as bch
|
2
|
+
|
3
|
+
|
4
|
+
class UnreliableClass(bch.ParametrizedSweep):
|
5
|
+
"""This class helps demonstrate benchmarking a function that sometimes crashes during sampling. By using BenchRunCfg.use_sample_cache you can store the results of every call to the benchmark function so data is not lost in the event of a crash. However, because cache invalidation is hard (https://martinfowler.com/bliki/TwoHardThings.html) you need to be mindful of how you could get bad results due to incorrect cache data. For example if you change your benchmark function and use the sample cache you will not get correct values; you will need to use BenchRunCfg.clear_sample_cache to purge any out of date results."""
|
6
|
+
|
7
|
+
input_val = bch.IntSweep(
|
8
|
+
default=0,
|
9
|
+
bounds=[0, 3],
|
10
|
+
doc="If check limit=True the crashy_fn will crash if this value is >1",
|
11
|
+
)
|
12
|
+
return_value = bch.ResultVar(
|
13
|
+
units="ul",
|
14
|
+
doc="This is a dummy result variable. In this example, it is the same as the value passed in.",
|
15
|
+
)
|
16
|
+
trigger_crash = bch.ResultVar(
|
17
|
+
units="True/False",
|
18
|
+
doc="if true crashy_fn will crash when input_val >1",
|
19
|
+
)
|
20
|
+
|
21
|
+
def crashy_fn(self, input_val: int = 0, **kwargs) -> float: # pylint: disable=unused-argument
|
22
|
+
if self.trigger_crash:
|
23
|
+
if input_val > 1:
|
24
|
+
raise RuntimeError("I crashed for no good reason ;P")
|
25
|
+
|
26
|
+
return {"return_value": input_val, "trigger_crash": self.trigger_crash}
|
27
|
+
|
28
|
+
|
29
|
+
def example_sample_cache(
|
30
|
+
run_cfg: bch.BenchRunCfg = bch.BenchRunCfg(),
|
31
|
+
report: bch.BenchReport = bch.BenchReport(),
|
32
|
+
trigger_crash: bool = False,
|
33
|
+
) -> bch.Bench:
|
34
|
+
"""This example shows how to use the use_sample_cache option to deal with unreliable functions and to continue benchmarking using previously calculated results even if the code crashed during the run
|
35
|
+
|
36
|
+
Args:
|
37
|
+
run_cfg (BenchRunCfg): configuration of how to perform the param sweep
|
38
|
+
trigger_crash: (bool): Turn on/off code to artificially trigger a crash
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
Bench: results of the parameter sweep
|
42
|
+
"""
|
43
|
+
|
44
|
+
instance = UnreliableClass()
|
45
|
+
instance.trigger_crash = trigger_crash
|
46
|
+
|
47
|
+
bencher = bch.Bench("example_sample_cache", instance.crashy_fn, run_cfg=run_cfg, report=report)
|
48
|
+
|
49
|
+
bencher.plot_sweep(
|
50
|
+
title="Example Crashy Function with the sample_cache",
|
51
|
+
input_vars=[UnreliableClass.param.input_val],
|
52
|
+
result_vars=[UnreliableClass.param.return_value, UnreliableClass.param.trigger_crash],
|
53
|
+
description="""This example shows how to use the use_sample_cache option to deal with unreliable functions and to continue benchmarking using previously calculated results even if the code crashed during the run""",
|
54
|
+
run_cfg=run_cfg,
|
55
|
+
post_description="The input_val vs return value graph is a straight line as expected and there is no record of the fact the benchmark crashed halfway through. The second graph shows that for values >1 the trigger_crash value had to be 0 in order to proceed",
|
56
|
+
)
|
57
|
+
return bencher
|
58
|
+
|
59
|
+
|
60
|
+
if __name__ == "__main__":
|
61
|
+
ex_run_cfg = bch.BenchRunCfg()
|
62
|
+
ex_run_cfg.repeats = 1
|
63
|
+
ex_run_cfg.executor = bch.Executors.SCOOP
|
64
|
+
|
65
|
+
# this will store the result of of every call to crashy_fn
|
66
|
+
ex_run_cfg.use_sample_cache = True
|
67
|
+
ex_run_cfg.clear_sample_cache = True
|
68
|
+
|
69
|
+
try:
|
70
|
+
# this will crash after iteration 2 because we are checking the crash_threshold >1. We don't want to lose those (potentially expensive to calculate) datapoints so they are stored in the sample_cache
|
71
|
+
example_sample_cache(ex_run_cfg, trigger_crash=True)
|
72
|
+
except RuntimeError as e:
|
73
|
+
print(f"caught the exception {e}")
|
74
|
+
|
75
|
+
print(
|
76
|
+
"Running the same benchmark but without checking the limit. The benchmarking should load the previously calculated values and continue to finish calculating the values that were missed due to the crash"
|
77
|
+
)
|
78
|
+
ex_run_cfg.clear_sample_cache = False
|
79
|
+
example_sample_cache(ex_run_cfg, trigger_crash=False)
|
80
|
+
|
81
|
+
ex_run_cfg.repeats = 2
|
82
|
+
|
83
|
+
example_sample_cache(ex_run_cfg, trigger_crash=False).report.show()
|
84
|
+
|
85
|
+
# see the test_sample_cache for a more detailed explanation of the mechanisms of the cache
|
@@ -0,0 +1,116 @@
|
|
1
|
+
from enum import auto
|
2
|
+
|
3
|
+
from strenum import StrEnum
|
4
|
+
|
5
|
+
import bencher as bch
|
6
|
+
|
7
|
+
|
8
|
+
class ExampleEnum(StrEnum):
|
9
|
+
value_1 = auto()
|
10
|
+
value_2 = auto()
|
11
|
+
# value3 = auto()
|
12
|
+
# value4 = auto()
|
13
|
+
|
14
|
+
|
15
|
+
class Cfg(bch.ParametrizedSweep):
|
16
|
+
enum1 = bch.EnumSweep(ExampleEnum)
|
17
|
+
result = bch.ResultVar()
|
18
|
+
|
19
|
+
# def __call__(self,**kwargs) -> Any:
|
20
|
+
# self.update_params_from_kwargs(**kwargs)
|
21
|
+
# self.result = float(str(self.enum1)[-1])
|
22
|
+
# return self.get_results_values_as_dict()
|
23
|
+
|
24
|
+
|
25
|
+
def bench_function(cfg: Cfg):
|
26
|
+
return {"result": float(str(cfg.enum1)[-1])}
|
27
|
+
|
28
|
+
|
29
|
+
def print_assert_equal(msg, first, second):
|
30
|
+
print(f"{msg} {first}=={second}")
|
31
|
+
assert first == second
|
32
|
+
|
33
|
+
|
34
|
+
def assert_call_counts(bencher, run_cfg, wrapper_calls=-1, fn_calls=-1, cache_calls=-1):
|
35
|
+
print_assert_equal(
|
36
|
+
"worker wrapper call count",
|
37
|
+
bencher.sample_cache.worker_wrapper_call_count,
|
38
|
+
wrapper_calls * run_cfg.repeats,
|
39
|
+
)
|
40
|
+
print_assert_equal(
|
41
|
+
"worker fn call count",
|
42
|
+
bencher.sample_cache.worker_fn_call_count,
|
43
|
+
fn_calls * run_cfg.repeats,
|
44
|
+
)
|
45
|
+
print_assert_equal(
|
46
|
+
"worker cache call count",
|
47
|
+
bencher.sample_cache.worker_cache_call_count,
|
48
|
+
cache_calls * run_cfg.repeats,
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
def example_cache_context() -> bch.Bench:
|
53
|
+
run_cfg = bch.BenchRunCfg()
|
54
|
+
run_cfg.use_sample_cache = True
|
55
|
+
run_cfg.only_hash_tag = True
|
56
|
+
run_cfg.repeats = 2
|
57
|
+
run_cfg.parallel = False
|
58
|
+
|
59
|
+
bencher = bch.Bench("bench_context", bench_function, Cfg, run_cfg=run_cfg)
|
60
|
+
|
61
|
+
# clear all tags from the cache at the beginning so that the example works the same not matter how many times the example is run. When using this for you own code you probably don't want to clear the cache at the beginning because you will lose all the data you collected.
|
62
|
+
bencher.clear_tag_from_sample_cache("example_tag1", run_cfg)
|
63
|
+
bencher.clear_tag_from_sample_cache("example_tag2", run_cfg)
|
64
|
+
|
65
|
+
# run a benchmark with a constant value and save results with example_tag1
|
66
|
+
bencher.plot_sweep(
|
67
|
+
title="Benchmark enum=value_1",
|
68
|
+
const_vars=[Cfg.param.enum1.with_const(ExampleEnum.value_1)],
|
69
|
+
result_vars=[Cfg.param.result],
|
70
|
+
tag="example_tag1",
|
71
|
+
)
|
72
|
+
|
73
|
+
# there are not values in the cache, so we expect 1 fn call and 0 cache calls
|
74
|
+
assert_call_counts(bencher, run_cfg, wrapper_calls=1, fn_calls=1, cache_calls=0)
|
75
|
+
|
76
|
+
# now run another benchmark with the same tag but a different value
|
77
|
+
bencher.clear_call_counts()
|
78
|
+
bencher.plot_sweep(
|
79
|
+
title="Benchmark enum=value_2",
|
80
|
+
const_vars=[Cfg.param.enum1.with_const(ExampleEnum.value_2)],
|
81
|
+
result_vars=[Cfg.param.result],
|
82
|
+
tag="example_tag1",
|
83
|
+
)
|
84
|
+
|
85
|
+
# these values have not been calcuated before so there should be 1 fn call
|
86
|
+
assert_call_counts(bencher, run_cfg, wrapper_calls=1, fn_calls=1, cache_calls=0)
|
87
|
+
|
88
|
+
# now create a new benchmark that calculates the values of the previous two benchmarks. The tag is the same so those values will be loaded from the cache instead of getting calculated again
|
89
|
+
bencher.clear_call_counts()
|
90
|
+
bencher.plot_sweep(
|
91
|
+
title="Benchmark enum=[value_1,value_2] combined",
|
92
|
+
input_vars=[Cfg.param.enum1],
|
93
|
+
result_vars=[Cfg.param.result],
|
94
|
+
tag="example_tag1",
|
95
|
+
)
|
96
|
+
|
97
|
+
# both calls hit the cache.
|
98
|
+
assert_call_counts(bencher, run_cfg, wrapper_calls=2, fn_calls=0, cache_calls=2)
|
99
|
+
|
100
|
+
# run the same benchmark as before but use a different tag. The previously cached values will not be used and fresh values will be calculated instead
|
101
|
+
bencher.clear_call_counts()
|
102
|
+
bencher.plot_sweep(
|
103
|
+
title="Benchmark enum=[value_1,value_2] with different tag",
|
104
|
+
input_vars=[Cfg.param.enum1],
|
105
|
+
result_vars=[Cfg.param.result],
|
106
|
+
tag="example_tag2",
|
107
|
+
)
|
108
|
+
|
109
|
+
# Both calls are calcuated becuase the tag is different so they don't hit the cache
|
110
|
+
assert_call_counts(bencher, run_cfg, wrapper_calls=2, fn_calls=2, cache_calls=0)
|
111
|
+
|
112
|
+
return bencher
|
113
|
+
|
114
|
+
|
115
|
+
if __name__ == "__main__":
|
116
|
+
example_cache_context().report.show()
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# you need this import to be able to reference a class from a static method in that class
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import math
|
5
|
+
import random
|
6
|
+
import time
|
7
|
+
from datetime import datetime
|
8
|
+
from enum import auto
|
9
|
+
|
10
|
+
from strenum import StrEnum
|
11
|
+
|
12
|
+
import bencher as bch
|
13
|
+
|
14
|
+
|
15
|
+
# define a class with the output variables you want to benchmark. It must inherit from ParametrizedSweep (which inherits from param.Parametrized). Param is a python library that allows you to track metadata about parameters. I would recommend reading at least the intro: https://param.holoviz.org/. I have extended param with some extra metadata such is the units of the variable so that it can automaticaly be plotted.
|
16
|
+
class OutputCfg(bch.ParametrizedSweep):
|
17
|
+
"""A class for defining what variables the benchmark function returns and metadata on those variables"""
|
18
|
+
|
19
|
+
# Documenting the variable here with enables automatic summaries of what has been benchmarked.
|
20
|
+
# This made up example uses accuracy as an example, but the variable defined here can be any metric that is important for the performance of a system. You can also define the direction of the optimisation i.e. to minimise or maximise the metric.
|
21
|
+
accuracy = bch.ResultVar(
|
22
|
+
units="%", direction=bch.OptDir.maximize, doc="The accuracy of the algorithm."
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
# Define categorical variables with enums that inherit from StrEnum. In this example, its just an arbitrary set of categories that have an unknown influence on the metric you want to understand. In a real world case these would be a set of conditions or settings you are benchmarking
|
27
|
+
class AlgoSetting(StrEnum):
|
28
|
+
"""Use enums to describe categorical input. In this example they are given names that describe how they affect the function output, but in a real world example these will be some settings to an algorithm that you want to understand how they affect the metric you are trying to optimise."""
|
29
|
+
|
30
|
+
# add some random noise to the output. When your algorithm has noisy output it often is an indication that something is not quite right. The graphs should show that you want to avoid the "noisy" setting in your algorithm
|
31
|
+
noisy = auto()
|
32
|
+
|
33
|
+
# This is the setting with the best performance, and characterising that that is is the goal of the benchmarking
|
34
|
+
optimum = auto()
|
35
|
+
|
36
|
+
poor = auto() # this setting results in poor performance
|
37
|
+
|
38
|
+
|
39
|
+
# define a class with the input variables you want to benchmark. It must inherit from ParametrizeSweep. This class defines a struct that is passed to the benchmark function. The function must be pure and so we define it as a staticmethod that takes an InputCfg class and returns an OutputCfg class. By accepting and returning parametrized classes the metadata about what the relationship between the input and output are easy to track.
|
40
|
+
class InputCfg(bch.ParametrizedSweep):
|
41
|
+
# The variables must be defined as one of the Sweep types, i.e, FloatSweep, IntSweep, EnumSweep from bencher.bench_vars
|
42
|
+
# theta = FloatSweep(default=0, bounds=[0, math.pi], doc="Input angle", units="rad", samples=30)
|
43
|
+
|
44
|
+
# Define sweep variables by passing in an enum class name. The first element of the enum is the default by convention, but you can overrride the default in the constructor
|
45
|
+
algo_setting_enum = bch.EnumSweep(AlgoSetting, default=AlgoSetting.poor)
|
46
|
+
|
47
|
+
# In this case there are no units so its marked as unitless or ul. You can define how many evenly distributed samples to sample the parameter with
|
48
|
+
algo_setting_float = bch.FloatSweep(
|
49
|
+
default=0.0,
|
50
|
+
bounds=[0.0, 6.0],
|
51
|
+
doc="This represents a continuous input value to your function that affects the desired output in a way you want to characterise.",
|
52
|
+
units="ul",
|
53
|
+
samples=10,
|
54
|
+
)
|
55
|
+
|
56
|
+
# define the objective function you want to benchmark. It must be static and have no side effects. It should accept 1 input of type InputCfg (or whatever your input config class is called) and return the OutputCfg class you have defined
|
57
|
+
@staticmethod
|
58
|
+
def bench_function(cfg: InputCfg) -> OutputCfg:
|
59
|
+
"""Takes an ExampleBenchCfgIn and returns a ExampleBenchCfgOut output. This is just a dummy example so the behavior of the function is rather transparent, but in a real use case the function would be a black box you want to characterise."""
|
60
|
+
output = OutputCfg()
|
61
|
+
|
62
|
+
output.accuracy = 50 + math.sin(cfg.algo_setting_float) * 5
|
63
|
+
|
64
|
+
# this simulates random long term change in the function
|
65
|
+
output.accuracy += time.localtime(datetime.now().second).tm_sec / 30
|
66
|
+
|
67
|
+
match cfg.algo_setting_enum:
|
68
|
+
case AlgoSetting.noisy:
|
69
|
+
# add some random noise to the output. When your algorith has noisy output it often is an indication that something is not quite right. The graphs should show that you want to avoid the "noisy" setting in your algorithm
|
70
|
+
output.accuracy += random.uniform(-10, 10)
|
71
|
+
case AlgoSetting.optimum:
|
72
|
+
output.accuracy += 30 # This is the setting with the best performance, and characterising that is is the goal of the benchmarking
|
73
|
+
case AlgoSetting.poor:
|
74
|
+
output.accuracy -= 20 # this setting results in poor performance
|
75
|
+
return output
|
76
|
+
|
77
|
+
|
78
|
+
if __name__ == "__main__":
|
79
|
+
# pass the objective function you have defined to bencher. This benchmark function can be reused for multiple sweeps. You also need to pass the inputCfg to the bencher so that it can process the metadata about the input configuration.
|
80
|
+
bench = bch.Bench("Bencher_Example_Categorical", InputCfg.bench_function, InputCfg)
|
81
|
+
|
82
|
+
# Bencher needs to know the metadata of the variable in order to automatically sweep and plot it, so it is passed by using param's metadata syntax. InputCfg.param.* is how to access the metadata defined in the class description. Unfortunately vscode autocomplete doesn't work with params metaclass machinery so you will need to look at the class definition to get a list of possible settings. Define what parameter you want to sweep over and the result variable you want to plot. If you pass 1 input, it will perform a 1D sweep over that dimension and plot a line or a bar graph of the result (depending on if that variable on continuous or discrete). In this example we are going to sweep the enum variable and record the accuracy.
|
83
|
+
bench.plot_sweep(
|
84
|
+
input_vars=[InputCfg.param.algo_setting_enum],
|
85
|
+
result_vars=[OutputCfg.param.accuracy],
|
86
|
+
title="Simple example 1D enum sweep",
|
87
|
+
description="""Sample all the values in enum setting and record the resulting accuracy. The algo_setting_float is not mentioned in the inputs and so it takes the default value that was set in the InputCfg class. Repeats=10 so the benchmark function is called 10 times serially. This is why the function must be pure, if a past call to the function affects the future call to the function (through global side effects) any statistics you calculate will not be correct.
|
88
|
+
""",
|
89
|
+
post_description="Here you can see the affect of each setting on the output and the optimum is clearly the best.",
|
90
|
+
run_cfg=bch.BenchRunCfg(repeats=10),
|
91
|
+
)
|
92
|
+
|
93
|
+
# There is also a floating point input setting that affects the performance of the algorithm. By passing only the float setting, the InputCfg class will use the default setting of the categorical value so you can understand the float setting in isolation
|
94
|
+
bench.plot_sweep(
|
95
|
+
input_vars=[InputCfg.param.algo_setting_float],
|
96
|
+
result_vars=[OutputCfg.param.accuracy],
|
97
|
+
title="Simple example 1D float sweep",
|
98
|
+
description="""Perform a 1D sweep over the continuous variable algo_setting_float taking sweep the bounds and number of samples from the InputCfg class definition. The algo_setting_enum is not mentioned in the inputs and so it takes the default value that was set in the InputCfg class. Repeats=10 so the benchmark function is called 10 times serially.
|
99
|
+
""",
|
100
|
+
post_description="The plot shows the output is affected by the float input in a continuous way with a peak around 1.5",
|
101
|
+
run_cfg=bch.BenchRunCfg(repeats=10),
|
102
|
+
)
|
103
|
+
|
104
|
+
# This sweep is a combination of the previous two sweeps
|
105
|
+
bench.plot_sweep(
|
106
|
+
input_vars=[
|
107
|
+
InputCfg.param.algo_setting_float,
|
108
|
+
InputCfg.param.algo_setting_enum,
|
109
|
+
],
|
110
|
+
result_vars=[OutputCfg.param.accuracy],
|
111
|
+
title="Simple example 2D sweep",
|
112
|
+
description="""Perform a 2D sweep over the enum and continuous variable to see how they act together. Here the setting use_optuna=True so additional graphs a plotted at the end.
|
113
|
+
""",
|
114
|
+
post_description="In this example function the two input settings combine in a linear and predictable way, so the best combination of settings is enum = AlgoSetting.optimum and float = 1.33. Setting use_optuna=True adds a plot of how much each input parameter affects the metric and a printout of the best parameter values found during the sweep. If the value for repeat is high it is an indication there is something wrong with your benchmark function. The repeat should have no affect on the value of the function if calls to the function are independent. This can be useful to detect undesired side effects in your code",
|
115
|
+
run_cfg=bch.BenchRunCfg(repeats=10, use_optuna=True, serve_xarray=True, serve_pandas=True),
|
116
|
+
)
|
117
|
+
|
118
|
+
# In the last example we track the value of the categorical values over time.
|
119
|
+
# run this code in a loop twice to simulate calling the benchmarking function at different times. The most common use case for tracking over time would be run once a day during nightly benchmarking
|
120
|
+
bench.plot_sweep(
|
121
|
+
input_vars=[InputCfg.param.algo_setting_enum],
|
122
|
+
result_vars=[OutputCfg.param.accuracy],
|
123
|
+
const_vars=[(InputCfg.param.algo_setting_float, 1.33)],
|
124
|
+
title="Simple example 1D sweep over time",
|
125
|
+
description="""Once you have found the optimal settings for your algorithm you want to make sure that the performance is not lost over time. You can set variables to a constant value and in this case the float value is set to its optimum value. The first time this function is run only the results from sweeping the categorical value is plotted (the same as example 1), but the second time it is run a graph the values over time is shown. [Run the code again if you don't see a graph over time]. If the graphs over time shows long term changes (not just noise), it indicate there is another external factor that is affecting your performace over time, i.e. dependencies changing, physical degradation of equipment, an unnoticed bug from a pull request etc...
|
126
|
+
|
127
|
+
This shows the basic features of bencher. These examples are purposefully simplified to demonstrate its features in isolation and don't reeally show the real advantages of bencher. If you only have a few inputs and outputs its not that complicated to throw together some plots of performance. The power of bencher is that when you have a system with many moving parts that all interact with eachother, teasing apart those influences becomes much harder because the parameter spaces combine quite quickly into a high dimensional mess. Bencher makes it easier to experiment with different combination of inputs to gain an intuition of the system performance. Bencher can plot up to 6D input natively and you can add custom plots if you have exotic data types or state spaces [WIP].
|
128
|
+
""",
|
129
|
+
post_description="",
|
130
|
+
run_cfg=bch.BenchRunCfg(repeats=10, over_time=True, clear_history=False),
|
131
|
+
)
|
132
|
+
|
133
|
+
# launch web server and view
|
134
|
+
bench.report.show()
|
@@ -0,0 +1,35 @@
|
|
1
|
+
"""This file has some examples for how to perform basic benchmarking parameter sweeps"""
|
2
|
+
|
3
|
+
import bencher as bch
|
4
|
+
|
5
|
+
# All the examples will be using the data structures and benchmark function defined in this file
|
6
|
+
from bencher.example.benchmark_data import ExampleBenchCfgIn, ExampleBenchCfgOut, bench_function
|
7
|
+
|
8
|
+
|
9
|
+
def example_1D_bool(run_cfg: bch.BenchRunCfg) -> bch.Bench:
|
10
|
+
"""This example shows how to sample a 1 dimensional categorical variable and plot the result of passing that parameter sweep to the benchmarking function"""
|
11
|
+
|
12
|
+
bench = bch.Bench(
|
13
|
+
"benchmarking_example_categorical1D",
|
14
|
+
bench_function,
|
15
|
+
ExampleBenchCfgIn,
|
16
|
+
)
|
17
|
+
|
18
|
+
# here we sample the input variable theta and plot the value of output1. The (noisy) function is sampled 20 times so you can see the distribution
|
19
|
+
res = bench.plot_sweep(
|
20
|
+
title="Example 1D Bool",
|
21
|
+
input_vars=[ExampleBenchCfgIn.param.noisy],
|
22
|
+
result_vars=[ExampleBenchCfgOut.param.out_sin],
|
23
|
+
description=example_1D_bool.__doc__,
|
24
|
+
run_cfg=run_cfg,
|
25
|
+
)
|
26
|
+
bench.report.append(res.to_bar())
|
27
|
+
|
28
|
+
return bench
|
29
|
+
|
30
|
+
|
31
|
+
if __name__ == "__main__":
|
32
|
+
ex_run_cfg = bch.BenchRunCfg()
|
33
|
+
ex_run_cfg.repeats = 20
|
34
|
+
|
35
|
+
example_1D_bool(ex_run_cfg).report.show()
|