holobench 1.41.0__py3-none-any.whl → 1.43.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/__init__.py +20 -2
- bencher/bench_cfg.py +262 -54
- bencher/bench_report.py +2 -2
- bencher/bench_runner.py +96 -10
- bencher/bencher.py +421 -89
- bencher/class_enum.py +70 -7
- bencher/example/example_dataframe.py +2 -2
- bencher/example/example_levels.py +17 -173
- bencher/example/example_pareto.py +107 -31
- bencher/example/example_rerun2.py +1 -1
- bencher/example/example_simple_bool.py +2 -2
- bencher/example/example_simple_float2d.py +6 -1
- bencher/example/example_video.py +2 -0
- bencher/example/experimental/example_hvplot_explorer.py +2 -2
- bencher/example/inputs_0D/example_0_in_1_out.py +25 -15
- bencher/example/inputs_0D/example_0_in_2_out.py +12 -3
- bencher/example/inputs_0_float/example_0_cat_in_2_out.py +88 -0
- bencher/example/inputs_0_float/example_1_cat_in_2_out.py +98 -0
- bencher/example/inputs_0_float/example_2_cat_in_2_out.py +107 -0
- bencher/example/inputs_0_float/example_3_cat_in_2_out.py +111 -0
- bencher/example/inputs_1D/example1d_common.py +48 -12
- bencher/example/inputs_1D/example_0_float_1_cat.py +33 -0
- bencher/example/inputs_1D/example_1_cat_in_2_out_repeats.py +68 -0
- bencher/example/inputs_1D/example_1_float_2_cat_repeats.py +3 -0
- bencher/example/inputs_1D/example_1_int_in_1_out.py +98 -0
- bencher/example/inputs_1D/example_1_int_in_2_out.py +101 -0
- bencher/example/inputs_1D/example_1_int_in_2_out_repeats.py +99 -0
- bencher/example/inputs_1_float/example_1_float_0_cat_in_2_out.py +117 -0
- bencher/example/inputs_1_float/example_1_float_1_cat_in_2_out.py +124 -0
- bencher/example/inputs_1_float/example_1_float_2_cat_in_2_out.py +132 -0
- bencher/example/inputs_1_float/example_1_float_3_cat_in_2_out.py +140 -0
- bencher/example/inputs_2D/example_2_cat_in_4_out_repeats.py +104 -0
- bencher/example/inputs_2_float/example_2_float_0_cat_in_2_out.py +98 -0
- bencher/example/inputs_2_float/example_2_float_1_cat_in_2_out.py +112 -0
- bencher/example/inputs_2_float/example_2_float_2_cat_in_2_out.py +122 -0
- bencher/example/inputs_2_float/example_2_float_3_cat_in_2_out.py +138 -0
- bencher/example/inputs_3_float/example_3_float_0_cat_in_2_out.py +111 -0
- bencher/example/inputs_3_float/example_3_float_1_cat_in_2_out.py +117 -0
- bencher/example/inputs_3_float/example_3_float_2_cat_in_2_out.py +124 -0
- bencher/example/inputs_3_float/example_3_float_3_cat_in_2_out.py +129 -0
- bencher/example/meta/generate_examples.py +118 -7
- bencher/example/meta/generate_meta.py +88 -40
- bencher/job.py +174 -9
- bencher/plotting/plot_filter.py +52 -17
- bencher/results/bench_result.py +117 -25
- bencher/results/bench_result_base.py +117 -8
- bencher/results/dataset_result.py +6 -200
- bencher/results/explorer_result.py +23 -0
- bencher/results/{hvplot_result.py → histogram_result.py} +3 -18
- bencher/results/holoview_results/__init__.py +0 -0
- bencher/results/holoview_results/bar_result.py +79 -0
- bencher/results/holoview_results/curve_result.py +110 -0
- bencher/results/holoview_results/distribution_result/__init__.py +0 -0
- bencher/results/holoview_results/distribution_result/box_whisker_result.py +73 -0
- bencher/results/holoview_results/distribution_result/distribution_result.py +109 -0
- bencher/results/holoview_results/distribution_result/scatter_jitter_result.py +92 -0
- bencher/results/holoview_results/distribution_result/violin_result.py +70 -0
- bencher/results/holoview_results/heatmap_result.py +319 -0
- bencher/results/holoview_results/holoview_result.py +346 -0
- bencher/results/holoview_results/line_result.py +240 -0
- bencher/results/holoview_results/scatter_result.py +107 -0
- bencher/results/holoview_results/surface_result.py +158 -0
- bencher/results/holoview_results/table_result.py +14 -0
- bencher/results/holoview_results/tabulator_result.py +20 -0
- bencher/results/optuna_result.py +30 -115
- bencher/results/video_controls.py +38 -0
- bencher/results/video_result.py +39 -36
- bencher/results/video_summary.py +2 -2
- bencher/results/{plotly_result.py → volume_result.py} +29 -8
- bencher/utils.py +175 -26
- bencher/variables/inputs.py +122 -15
- bencher/video_writer.py +2 -1
- bencher/worker_job.py +31 -3
- {holobench-1.41.0.dist-info → holobench-1.43.0.dist-info}/METADATA +24 -24
- holobench-1.43.0.dist-info/RECORD +147 -0
- bencher/example/example_levels2.py +0 -37
- bencher/example/inputs_1D/example_1_in_1_out.py +0 -62
- bencher/example/inputs_1D/example_1_in_2_out.py +0 -63
- bencher/example/inputs_1D/example_1_in_2_out_repeats.py +0 -61
- bencher/results/holoview_result.py +0 -796
- bencher/results/panel_result.py +0 -41
- holobench-1.41.0.dist-info/RECORD +0 -114
- {holobench-1.41.0.dist-info → holobench-1.43.0.dist-info}/WHEEL +0 -0
- {holobench-1.41.0.dist-info → holobench-1.43.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,13 +2,15 @@ import nbformat as nbf
|
|
2
2
|
from pathlib import Path
|
3
3
|
|
4
4
|
|
5
|
-
def convert_example_to_jupyter_notebook(
|
6
|
-
|
5
|
+
def convert_example_to_jupyter_notebook(
|
6
|
+
filename: str, output_path: str, repeats: int = None
|
7
|
+
) -> None:
|
7
8
|
source_path = Path(filename)
|
8
9
|
|
9
10
|
nb = nbf.v4.new_notebook()
|
10
11
|
title = source_path.stem
|
11
|
-
|
12
|
+
repeat_exr = f"bch.BenchRunCfg(repeats={repeats})" if repeats else ""
|
13
|
+
function_name = f"{source_path.stem}({repeat_exr})"
|
12
14
|
text = f"""# {title}"""
|
13
15
|
|
14
16
|
code = "%%capture\n"
|
@@ -34,19 +36,128 @@ bench.get_result().to_auto_plots()
|
|
34
36
|
]
|
35
37
|
output_path = Path(f"docs/reference/{output_path}/ex_{title}.ipynb")
|
36
38
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
37
|
-
|
39
|
+
# Add a newline character at the end to ensure proper end-of-file
|
40
|
+
notebook_content = nbf.writes(nb) + "\n"
|
41
|
+
output_path.write_text(notebook_content, encoding="utf-8")
|
38
42
|
|
39
43
|
|
40
44
|
if __name__ == "__main__":
|
45
|
+
# Examples with different numbers of categorical variables in increasing order
|
41
46
|
convert_example_to_jupyter_notebook(
|
42
|
-
"/workspaces/bencher/bencher/example/
|
47
|
+
"/workspaces/bencher/bencher/example/inputs_0_float/example_0_cat_in_2_out.py",
|
48
|
+
"inputs_0_float",
|
49
|
+
repeats=100,
|
43
50
|
)
|
51
|
+
|
52
|
+
convert_example_to_jupyter_notebook(
|
53
|
+
"/workspaces/bencher/bencher/example/inputs_0_float/example_1_cat_in_2_out.py",
|
54
|
+
"inputs_0_float",
|
55
|
+
)
|
56
|
+
|
57
|
+
convert_example_to_jupyter_notebook(
|
58
|
+
"/workspaces/bencher/bencher/example/inputs_0_float/example_2_cat_in_2_out.py",
|
59
|
+
"inputs_0_float",
|
60
|
+
)
|
61
|
+
|
62
|
+
convert_example_to_jupyter_notebook(
|
63
|
+
"/workspaces/bencher/bencher/example/inputs_0_float/example_3_cat_in_2_out.py",
|
64
|
+
"inputs_0_float",
|
65
|
+
)
|
66
|
+
|
67
|
+
# Examples with 1 float input plus varying categorical inputs
|
68
|
+
convert_example_to_jupyter_notebook(
|
69
|
+
"/workspaces/bencher/bencher/example/inputs_1_float/example_1_float_0_cat_in_2_out.py",
|
70
|
+
"inputs_1_float",
|
71
|
+
)
|
72
|
+
|
73
|
+
convert_example_to_jupyter_notebook(
|
74
|
+
"/workspaces/bencher/bencher/example/inputs_1_float/example_1_float_1_cat_in_2_out.py",
|
75
|
+
"inputs_1_float",
|
76
|
+
)
|
77
|
+
|
78
|
+
convert_example_to_jupyter_notebook(
|
79
|
+
"/workspaces/bencher/bencher/example/inputs_1_float/example_1_float_2_cat_in_2_out.py",
|
80
|
+
"inputs_1_float",
|
81
|
+
)
|
82
|
+
|
83
|
+
convert_example_to_jupyter_notebook(
|
84
|
+
"/workspaces/bencher/bencher/example/inputs_1_float/example_1_float_3_cat_in_2_out.py",
|
85
|
+
"inputs_1_float",
|
86
|
+
)
|
87
|
+
|
88
|
+
# Example with 2 float inputs plus categorical inputs
|
89
|
+
convert_example_to_jupyter_notebook(
|
90
|
+
"/workspaces/bencher/bencher/example/inputs_2_float/example_2_float_3_cat_in_2_out.py",
|
91
|
+
"inputs_2_float",
|
92
|
+
)
|
93
|
+
|
94
|
+
convert_example_to_jupyter_notebook(
|
95
|
+
"/workspaces/bencher/bencher/example/inputs_2_float/example_2_float_2_cat_in_2_out.py",
|
96
|
+
"inputs_2_float",
|
97
|
+
)
|
98
|
+
|
99
|
+
convert_example_to_jupyter_notebook(
|
100
|
+
"/workspaces/bencher/bencher/example/inputs_2_float/example_2_float_1_cat_in_2_out.py",
|
101
|
+
"inputs_2_float",
|
102
|
+
)
|
103
|
+
|
104
|
+
convert_example_to_jupyter_notebook(
|
105
|
+
"/workspaces/bencher/bencher/example/inputs_2_float/example_2_float_0_cat_in_2_out.py",
|
106
|
+
"inputs_2_float",
|
107
|
+
)
|
108
|
+
|
109
|
+
# Examples with 3 float inputs plus categorical inputs
|
110
|
+
convert_example_to_jupyter_notebook(
|
111
|
+
"/workspaces/bencher/bencher/example/inputs_3_float/example_3_float_3_cat_in_2_out.py",
|
112
|
+
"inputs_3_float",
|
113
|
+
)
|
114
|
+
|
115
|
+
convert_example_to_jupyter_notebook(
|
116
|
+
"/workspaces/bencher/bencher/example/inputs_3_float/example_3_float_2_cat_in_2_out.py",
|
117
|
+
"inputs_3_float",
|
118
|
+
)
|
119
|
+
|
120
|
+
convert_example_to_jupyter_notebook(
|
121
|
+
"/workspaces/bencher/bencher/example/inputs_3_float/example_3_float_1_cat_in_2_out.py",
|
122
|
+
"inputs_3_float",
|
123
|
+
)
|
124
|
+
|
125
|
+
convert_example_to_jupyter_notebook(
|
126
|
+
"/workspaces/bencher/bencher/example/inputs_3_float/example_3_float_0_cat_in_2_out.py",
|
127
|
+
"inputs_3_float",
|
128
|
+
)
|
129
|
+
|
130
|
+
convert_example_to_jupyter_notebook(
|
131
|
+
"/workspaces/bencher/bencher/example/inputs_0D/example_0_in_1_out.py", "0D", repeats=100
|
132
|
+
)
|
133
|
+
|
134
|
+
convert_example_to_jupyter_notebook(
|
135
|
+
"/workspaces/bencher/bencher/example/inputs_0D/example_0_in_2_out.py", "0D", repeats=100
|
136
|
+
)
|
137
|
+
|
138
|
+
# Other 1D examples
|
139
|
+
convert_example_to_jupyter_notebook(
|
140
|
+
"/workspaces/bencher/bencher/example/inputs_1D/example_1_int_in_1_out.py", "1D"
|
141
|
+
)
|
142
|
+
|
143
|
+
convert_example_to_jupyter_notebook(
|
144
|
+
"/workspaces/bencher/bencher/example/inputs_1D/example_1_int_in_2_out.py", "1D"
|
145
|
+
)
|
146
|
+
|
147
|
+
convert_example_to_jupyter_notebook(
|
148
|
+
"/workspaces/bencher/bencher/example/inputs_1D/example_1_int_in_2_out_repeats.py", "1D"
|
149
|
+
)
|
150
|
+
|
151
|
+
convert_example_to_jupyter_notebook(
|
152
|
+
"/workspaces/bencher/bencher/example/inputs_1D/example_1_cat_in_2_out_repeats.py", "1D"
|
153
|
+
)
|
154
|
+
|
44
155
|
convert_example_to_jupyter_notebook(
|
45
|
-
"/workspaces/bencher/bencher/example/
|
156
|
+
"/workspaces/bencher/bencher/example/inputs_2D/example_2_cat_in_4_out_repeats.py", "1D"
|
46
157
|
)
|
47
158
|
|
48
159
|
convert_example_to_jupyter_notebook(
|
49
|
-
"/workspaces/bencher/bencher/example/
|
160
|
+
"/workspaces/bencher/bencher/example/example_levels.py", "Levels"
|
50
161
|
)
|
51
162
|
|
52
163
|
# todo, enable
|
@@ -9,22 +9,68 @@ from bencher.example.meta.example_meta import BenchableObject
|
|
9
9
|
class BenchMetaGen(bch.ParametrizedSweep):
|
10
10
|
"""This class uses bencher to display the multidimensional types bencher can represent"""
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
# Benchable object to use
|
13
|
+
benchable_obj = None # Will store the benchable object instance
|
14
|
+
|
15
|
+
# Variables for controlling the sweep
|
16
|
+
float_vars_count = bch.IntSweep(
|
17
|
+
default=0, bounds=(0, 3), doc="The number of floating point variables that are swept"
|
14
18
|
)
|
15
|
-
|
16
|
-
default=0, bounds=(0,
|
19
|
+
categorical_vars_count = bch.IntSweep(
|
20
|
+
default=0, bounds=(0, 2), doc="The number of categorical variables that are swept"
|
17
21
|
)
|
18
|
-
sample_with_repeats = bch.IntSweep(default=1, bounds=(1, 100))
|
19
22
|
|
20
|
-
|
23
|
+
# Lists to store the actual variable names
|
24
|
+
float_var_names = [] # Will store float variable names
|
25
|
+
categorical_var_names = [] # Will store categorical variable names
|
26
|
+
result_var_names = [] # Will store result variable names
|
21
27
|
|
28
|
+
# Configuration options
|
29
|
+
sample_with_repeats = bch.IntSweep(default=1, bounds=(1, 100))
|
30
|
+
sample_over_time = bch.BoolSweep(default=False)
|
22
31
|
level = bch.IntSweep(default=2, units="level", bounds=(2, 5))
|
23
|
-
|
24
32
|
run_bench = False
|
25
33
|
|
26
34
|
plots = bch.ResultReference(units="int")
|
27
35
|
|
36
|
+
def __init__(
|
37
|
+
self, benchable_obj=None, float_vars=None, categorical_vars=None, result_vars=None, **params
|
38
|
+
):
|
39
|
+
"""Initialize with a benchable object and variable specifications
|
40
|
+
|
41
|
+
Args:
|
42
|
+
benchable_obj: The benchable object to use
|
43
|
+
float_vars: List of float variable names to sweep
|
44
|
+
categorical_vars: List of categorical variable names to sweep
|
45
|
+
result_vars: List of result variable names to plot
|
46
|
+
**params: Additional parameters
|
47
|
+
"""
|
48
|
+
super().__init__(**params)
|
49
|
+
|
50
|
+
# Set the benchable object
|
51
|
+
self.benchable_obj = benchable_obj if benchable_obj is not None else BenchableObject()
|
52
|
+
|
53
|
+
# Dynamically discover parameters from the benchable object
|
54
|
+
float_param_names = []
|
55
|
+
categorical_param_names = []
|
56
|
+
result_param_names = []
|
57
|
+
|
58
|
+
# Check all parameters of the benchable object
|
59
|
+
for name, param in self.benchable_obj.__class__.param.objects().items():
|
60
|
+
if hasattr(param, "bounds") and isinstance(param, bch.FloatSweep):
|
61
|
+
float_param_names.append(name)
|
62
|
+
elif isinstance(param, (bch.BoolSweep, bch.EnumSweep, bch.StringSweep)):
|
63
|
+
categorical_param_names.append(name)
|
64
|
+
elif isinstance(param, bch.ResultVar):
|
65
|
+
result_param_names.append(name)
|
66
|
+
|
67
|
+
# Use provided parameter lists or discovered ones
|
68
|
+
self.float_var_names = float_vars if float_vars is not None else float_param_names
|
69
|
+
self.categorical_var_names = (
|
70
|
+
categorical_vars if categorical_vars is not None else categorical_param_names
|
71
|
+
)
|
72
|
+
self.result_var_names = result_vars if result_vars is not None else result_param_names
|
73
|
+
|
28
74
|
def __call__(self, **kwargs: Any) -> Any:
|
29
75
|
self.update_params_from_kwargs(**kwargs)
|
30
76
|
|
@@ -34,50 +80,52 @@ class BenchMetaGen(bch.ParametrizedSweep):
|
|
34
80
|
run_cfg.over_time = self.sample_over_time
|
35
81
|
run_cfg.plot_size = 500
|
36
82
|
|
37
|
-
#
|
83
|
+
# Limit the variables to the specified counts
|
84
|
+
float_vars = []
|
85
|
+
# Apply the max_level=3 limit to the 3rd and subsequent float variables
|
86
|
+
for i, var_name in enumerate(self.float_var_names[: self.float_vars_count]):
|
87
|
+
if i >= 2: # Third variable (index 2) and beyond
|
88
|
+
float_vars.append(bch.p(var_name, max_level=3))
|
89
|
+
else:
|
90
|
+
float_vars.append(var_name)
|
38
91
|
|
39
|
-
|
40
|
-
|
41
|
-
"float2",
|
42
|
-
bch.p("float3", max_level=3),
|
43
|
-
"sigma",
|
44
|
-
]
|
92
|
+
categorical_vars = self.categorical_var_names[: self.categorical_vars_count]
|
93
|
+
input_vars = float_vars + categorical_vars
|
45
94
|
|
46
|
-
|
47
|
-
|
48
|
-
"noise_distribution",
|
49
|
-
"negate_output",
|
50
|
-
]
|
51
|
-
|
52
|
-
input_vars = inputs_vars_float[: self.float_vars] + inputs_vars_cat[: self.categorical_vars]
|
53
|
-
|
54
|
-
if self.run_bench:
|
55
|
-
bench = BenchableObject().to_bench(run_cfg)
|
95
|
+
if self.run_bench and input_vars and self.result_var_names:
|
96
|
+
bench = self.benchable_obj.to_bench(run_cfg)
|
56
97
|
res = bench.plot_sweep(
|
57
98
|
"test",
|
58
99
|
input_vars=input_vars,
|
59
|
-
result_vars=
|
100
|
+
result_vars=self.result_var_names,
|
60
101
|
plot_callbacks=False,
|
61
102
|
)
|
62
103
|
self.plots = bch.ResultReference()
|
63
104
|
self.plots.obj = res.to_auto()
|
64
105
|
|
65
|
-
title = f"{self.
|
106
|
+
title = f"{self.float_vars_count}_float_{self.categorical_vars_count}_cat"
|
66
107
|
|
67
108
|
nb = nbf.v4.new_notebook()
|
68
109
|
text = f"""# {title}"""
|
69
110
|
|
111
|
+
# Customize notebook generation to use the actual benchmark object and variables
|
112
|
+
module_import = "import bencher as bch\n"
|
113
|
+
if self.benchable_obj.__class__.__module__ != "__main__":
|
114
|
+
benchmark_import = f"from {self.benchable_obj.__class__.__module__} import {self.benchable_obj.__class__.__name__}\n"
|
115
|
+
else:
|
116
|
+
# If it's from main, use BenchableObject as fallback
|
117
|
+
benchmark_import = "from bencher.example.meta.example_meta import BenchableObject\n"
|
118
|
+
|
70
119
|
code_gen = f"""
|
71
120
|
%%capture
|
72
|
-
|
73
|
-
|
74
|
-
|
121
|
+
{module_import}
|
122
|
+
{benchmark_import}
|
75
123
|
run_cfg = bch.BenchRunCfg()
|
76
124
|
run_cfg.repeats = {self.sample_with_repeats}
|
77
125
|
run_cfg.level = 4
|
78
|
-
bench =
|
126
|
+
bench = {self.benchable_obj.__class__.__name__}().to_bench(run_cfg)
|
79
127
|
res=bench.plot_sweep(input_vars={input_vars},
|
80
|
-
result_vars=
|
128
|
+
result_vars={self.result_var_names})
|
81
129
|
"""
|
82
130
|
code_results = """
|
83
131
|
from bokeh.io import output_notebook
|
@@ -107,14 +155,14 @@ def example_meta(run_cfg: bch.BenchRunCfg = None, report: bch.BenchReport = None
|
|
107
155
|
description="""## All Combinations of Variable Sweeps and Resulting Plots
|
108
156
|
This uses bencher to display all the combinations of plots bencher is able to produce""",
|
109
157
|
input_vars=[
|
110
|
-
bch.p("
|
111
|
-
"
|
158
|
+
bch.p("float_vars_count", [0, 1]),
|
159
|
+
"categorical_vars_count",
|
112
160
|
bch.p("sample_with_repeats", [1, 20]),
|
113
161
|
],
|
114
162
|
const_vars=[
|
115
|
-
# BenchMeta.param.
|
163
|
+
# BenchMeta.param.float_vars_count.with_const(1),
|
116
164
|
# BenchMeta.param.sample_with_repeats.with_const(2),
|
117
|
-
# BenchMeta.param.
|
165
|
+
# BenchMeta.param.categorical_vars_count.with_const(2),
|
118
166
|
# BenchMeta.param.sample_over_time.with_const(True),
|
119
167
|
],
|
120
168
|
)
|
@@ -123,8 +171,8 @@ This uses bencher to display all the combinations of plots bencher is able to pr
|
|
123
171
|
description="""## All Combinations of Variable Sweeps and Resulting Plots
|
124
172
|
This uses bencher to display all the combinations of plots bencher is able to produce""",
|
125
173
|
input_vars=[
|
126
|
-
bch.p("
|
127
|
-
"
|
174
|
+
bch.p("float_vars_count", [2, 3]),
|
175
|
+
"categorical_vars_count",
|
128
176
|
],
|
129
177
|
)
|
130
178
|
|
@@ -133,13 +181,13 @@ This uses bencher to display all the combinations of plots bencher is able to pr
|
|
133
181
|
# description="""## All Combinations of Variable Sweeps and Resulting Plots
|
134
182
|
# This uses bencher to display all the combinations of plots bencher is able to produce""",
|
135
183
|
# input_vars=[
|
136
|
-
# bch.p("
|
137
|
-
# "
|
184
|
+
# bch.p("float_vars_count", [2, 3]),
|
185
|
+
# "categorical_vars_count",
|
138
186
|
# ],
|
139
187
|
# const_vars=[
|
140
188
|
# dict(level=3)
|
141
189
|
# # BenchMeta.param.sample_with_repeats.with_const(2),
|
142
|
-
# # BenchMeta.param.
|
190
|
+
# # BenchMeta.param.categorical_vars_count.with_const(2),
|
143
191
|
# # BenchMeta.param.sample_over_time.with_const(True),
|
144
192
|
# ],
|
145
193
|
# )
|
bencher/job.py
CHANGED
@@ -14,9 +14,32 @@ except ImportError as e:
|
|
14
14
|
|
15
15
|
|
16
16
|
class Job:
|
17
|
+
"""Represents a benchmarking job to be executed or retrieved from cache.
|
18
|
+
|
19
|
+
A Job encapsulates a function, its arguments, and metadata for caching
|
20
|
+
and tracking purposes.
|
21
|
+
|
22
|
+
Attributes:
|
23
|
+
job_id (str): A unique identifier for the job, used for logging
|
24
|
+
function (Callable): The function to be executed
|
25
|
+
job_args (dict): Arguments to pass to the function
|
26
|
+
job_key (str): A hash key for caching, derived from job_args if not provided
|
27
|
+
tag (str): Optional tag for grouping related jobs
|
28
|
+
"""
|
29
|
+
|
17
30
|
def __init__(
|
18
|
-
self, job_id: str, function: Callable, job_args: dict, job_key=None, tag=""
|
31
|
+
self, job_id: str, function: Callable, job_args: dict, job_key: str = None, tag: str = ""
|
19
32
|
) -> None:
|
33
|
+
"""Initialize a Job with function and arguments.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
job_id (str): A unique identifier for this job
|
37
|
+
function (Callable): The function to execute
|
38
|
+
job_args (dict): Arguments to pass to the function
|
39
|
+
job_key (str, optional): Cache key for this job. If None, will be generated
|
40
|
+
from job_args. Defaults to None.
|
41
|
+
tag (str, optional): Tag for grouping related jobs. Defaults to "".
|
42
|
+
"""
|
20
43
|
self.job_id = job_id
|
21
44
|
self.function = function
|
22
45
|
self.job_args = job_args
|
@@ -27,9 +50,32 @@ class Job:
|
|
27
50
|
self.tag = tag
|
28
51
|
|
29
52
|
|
30
|
-
# @dataclass
|
31
53
|
class JobFuture:
|
54
|
+
"""A wrapper for a job result or future that handles caching.
|
55
|
+
|
56
|
+
This class provides a unified interface for handling both immediate results
|
57
|
+
and futures (for asynchronous execution). It also handles caching results
|
58
|
+
when they become available.
|
59
|
+
|
60
|
+
Attributes:
|
61
|
+
job (Job): The job this future corresponds to
|
62
|
+
res (dict): The result, if available immediately
|
63
|
+
future (Future): The future representing the pending job, if executed asynchronously
|
64
|
+
cache: The cache to store results in when they become available
|
65
|
+
"""
|
66
|
+
|
32
67
|
def __init__(self, job: Job, res: dict = None, future: Future = None, cache=None) -> None:
|
68
|
+
"""Initialize a JobFuture with either an immediate result or a future.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
job (Job): The job this future corresponds to
|
72
|
+
res (dict, optional): The immediate result, if available. Defaults to None.
|
73
|
+
future (Future, optional): The future representing the pending result. Defaults to None.
|
74
|
+
cache (Cache, optional): The cache to store results in. Defaults to None.
|
75
|
+
|
76
|
+
Raises:
|
77
|
+
AssertionError: If neither res nor future is provided
|
78
|
+
"""
|
33
79
|
self.job = job
|
34
80
|
self.res = res
|
35
81
|
self.future = future
|
@@ -40,7 +86,16 @@ class JobFuture:
|
|
40
86
|
|
41
87
|
self.cache = cache
|
42
88
|
|
43
|
-
def result(self):
|
89
|
+
def result(self) -> dict:
|
90
|
+
"""Get the job result, waiting for completion if necessary.
|
91
|
+
|
92
|
+
If the result is not immediately available (i.e., it's a future),
|
93
|
+
this method will wait for the future to complete. Once the result
|
94
|
+
is available, it will be cached if a cache is provided.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
dict: The job result
|
98
|
+
"""
|
44
99
|
if self.future is not None:
|
45
100
|
self.res = self.future.result()
|
46
101
|
if self.cache is not None and self.res is not None:
|
@@ -49,18 +104,42 @@ class JobFuture:
|
|
49
104
|
|
50
105
|
|
51
106
|
def run_job(job: Job) -> dict:
|
107
|
+
"""Execute a job by calling its function with the provided arguments.
|
108
|
+
|
109
|
+
This is a helper function used primarily by executors to run jobs.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
job (Job): The job to execute
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
dict: The result of the job execution
|
116
|
+
"""
|
52
117
|
result = job.function(**job.job_args)
|
53
118
|
return result
|
54
119
|
|
55
120
|
|
56
121
|
class Executors(StrEnum):
|
122
|
+
"""Enumeration of available execution strategies for benchmark jobs.
|
123
|
+
|
124
|
+
This enum defines the execution modes for running benchmark jobs
|
125
|
+
and provides a factory method to create appropriate executors.
|
126
|
+
"""
|
127
|
+
|
57
128
|
SERIAL = auto() # slow but reliable
|
58
129
|
MULTIPROCESSING = auto() # breaks for large number of futures
|
59
130
|
SCOOP = auto() # requires running with python -m scoop your_file.py
|
60
131
|
# THREADS=auto() #not that useful as most bench code is cpu bound
|
61
132
|
|
62
133
|
@staticmethod
|
63
|
-
def factory(provider: Executors) -> Future:
|
134
|
+
def factory(provider: "Executors") -> Future | None:
|
135
|
+
"""Create an executor instance based on the specified execution strategy.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
provider (Executors): The type of executor to create
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
Future | None: The executor instance, or None for serial execution
|
142
|
+
"""
|
64
143
|
providers = {
|
65
144
|
Executors.SERIAL: None,
|
66
145
|
Executors.MULTIPROCESSING: ProcessPoolExecutor(),
|
@@ -70,7 +149,23 @@ class Executors(StrEnum):
|
|
70
149
|
|
71
150
|
|
72
151
|
class FutureCache:
|
73
|
-
"""
|
152
|
+
"""A cache system for benchmark job results with executor support.
|
153
|
+
|
154
|
+
This class provides a unified interface for running benchmark jobs either serially
|
155
|
+
or in parallel, with optional caching of results. It manages the execution strategy,
|
156
|
+
caching policy, and tracks statistics about job execution.
|
157
|
+
|
158
|
+
Attributes:
|
159
|
+
executor_type (Executors): The execution strategy to use
|
160
|
+
executor: The executor instance, created on demand
|
161
|
+
cache (Cache): Cache for storing job results
|
162
|
+
overwrite (bool): Whether to overwrite existing cached results
|
163
|
+
call_count (int): Counter for job calls
|
164
|
+
size_limit (int): Maximum size of the cache in bytes
|
165
|
+
worker_wrapper_call_count (int): Number of job submissions
|
166
|
+
worker_fn_call_count (int): Number of actual function executions
|
167
|
+
worker_cache_call_count (int): Number of cache hits
|
168
|
+
"""
|
74
169
|
|
75
170
|
def __init__(
|
76
171
|
self,
|
@@ -79,8 +174,18 @@ class FutureCache:
|
|
79
174
|
cache_name: str = "fcache",
|
80
175
|
tag_index: bool = True,
|
81
176
|
size_limit: int = int(20e9), # 20 GB
|
82
|
-
cache_results=True,
|
177
|
+
cache_results: bool = True,
|
83
178
|
):
|
179
|
+
"""Initialize a FutureCache with optional caching and execution settings.
|
180
|
+
|
181
|
+
Args:
|
182
|
+
executor (Executors, optional): The execution strategy to use. Defaults to Executors.SERIAL.
|
183
|
+
overwrite (bool, optional): Whether to overwrite existing cached results. Defaults to True.
|
184
|
+
cache_name (str, optional): Base name for the cache directory. Defaults to "fcache".
|
185
|
+
tag_index (bool, optional): Whether to enable tag-based indexing in the cache. Defaults to True.
|
186
|
+
size_limit (int, optional): Maximum size of the cache in bytes. Defaults to 20GB.
|
187
|
+
cache_results (bool, optional): Whether to cache results at all. Defaults to True.
|
188
|
+
"""
|
84
189
|
self.executor_type = executor
|
85
190
|
self.executor = None
|
86
191
|
if cache_results:
|
@@ -98,6 +203,18 @@ class FutureCache:
|
|
98
203
|
self.worker_cache_call_count = 0
|
99
204
|
|
100
205
|
def submit(self, job: Job) -> JobFuture:
|
206
|
+
"""Submit a job for execution, with caching if enabled.
|
207
|
+
|
208
|
+
This method first checks if the job result is already in the cache (if caching is enabled
|
209
|
+
and overwrite is False). If not found in the cache, it executes the job either serially
|
210
|
+
or using the configured executor.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
job (Job): The job to submit
|
214
|
+
|
215
|
+
Returns:
|
216
|
+
JobFuture: A future representing the job execution
|
217
|
+
"""
|
101
218
|
self.worker_wrapper_call_count += 1
|
102
219
|
|
103
220
|
if self.cache is not None:
|
@@ -130,25 +247,38 @@ class FutureCache:
|
|
130
247
|
)
|
131
248
|
|
132
249
|
def overwrite_msg(self, job: Job, suffix: str) -> None:
|
250
|
+
"""Log a message about overwriting or using cache.
|
251
|
+
|
252
|
+
Args:
|
253
|
+
job (Job): The job being executed
|
254
|
+
suffix (str): Additional text to add to the log message
|
255
|
+
"""
|
133
256
|
msg = "OVERWRITING" if self.overwrite else "NOT in"
|
134
257
|
logging.info(f"{job.job_id} {msg} cache{suffix}")
|
135
258
|
|
136
259
|
def clear_call_counts(self) -> None:
|
137
|
-
"""Clear the worker and cache call counts, to help debug and assert caching is happening properly"""
|
260
|
+
"""Clear the worker and cache call counts, to help debug and assert caching is happening properly."""
|
138
261
|
self.worker_wrapper_call_count = 0
|
139
262
|
self.worker_fn_call_count = 0
|
140
263
|
self.worker_cache_call_count = 0
|
141
264
|
|
142
265
|
def clear_cache(self) -> None:
|
266
|
+
"""Clear all entries from the cache."""
|
143
267
|
if self.cache:
|
144
268
|
self.cache.clear()
|
145
269
|
|
146
270
|
def clear_tag(self, tag: str) -> None:
|
271
|
+
"""Remove all cache entries with the specified tag.
|
272
|
+
|
273
|
+
Args:
|
274
|
+
tag (str): The tag identifying entries to remove from the cache
|
275
|
+
"""
|
147
276
|
logging.info(f"clearing the sample cache for tag: {tag}")
|
148
277
|
removed_vals = self.cache.evict(tag)
|
149
278
|
logging.info(f"removed: {removed_vals} items from the cache")
|
150
279
|
|
151
280
|
def close(self) -> None:
|
281
|
+
"""Close the cache and shutdown the executor if they exist."""
|
152
282
|
if self.cache:
|
153
283
|
self.cache.close()
|
154
284
|
if self.executor:
|
@@ -156,6 +286,11 @@ class FutureCache:
|
|
156
286
|
self.executor = None
|
157
287
|
|
158
288
|
def stats(self) -> str:
|
289
|
+
"""Get statistics about cache usage.
|
290
|
+
|
291
|
+
Returns:
|
292
|
+
str: A string with cache size information
|
293
|
+
"""
|
159
294
|
logging.info(f"job calls: {self.worker_wrapper_call_count}")
|
160
295
|
logging.info(f"cache calls: {self.worker_cache_call_count}")
|
161
296
|
logging.info(f"worker calls: {self.worker_fn_call_count}")
|
@@ -165,15 +300,35 @@ class FutureCache:
|
|
165
300
|
|
166
301
|
|
167
302
|
class JobFunctionCache(FutureCache):
|
303
|
+
"""A specialized cache for a specific function with various input parameters.
|
304
|
+
|
305
|
+
This class simplifies caching results for a specific function called with
|
306
|
+
different sets of parameters. It wraps the general FutureCache with a focus
|
307
|
+
on a single function.
|
308
|
+
|
309
|
+
Attributes:
|
310
|
+
function (Callable): The function to cache results for
|
311
|
+
"""
|
312
|
+
|
168
313
|
def __init__(
|
169
314
|
self,
|
170
315
|
function: Callable,
|
171
|
-
overwrite=False,
|
172
|
-
executor:
|
316
|
+
overwrite: bool = False,
|
317
|
+
executor: Executors = Executors.SERIAL,
|
173
318
|
cache_name: str = "fcache",
|
174
319
|
tag_index: bool = True,
|
175
320
|
size_limit: int = int(100e8),
|
176
321
|
):
|
322
|
+
"""Initialize a JobFunctionCache for a specific function.
|
323
|
+
|
324
|
+
Args:
|
325
|
+
function (Callable): The function to cache results for
|
326
|
+
overwrite (bool, optional): Whether to overwrite existing cached results. Defaults to False.
|
327
|
+
executor (Executors, optional): The execution strategy to use. Defaults to Executors.SERIAL.
|
328
|
+
cache_name (str, optional): Base name for the cache directory. Defaults to "fcache".
|
329
|
+
tag_index (bool, optional): Whether to enable tag-based indexing in the cache. Defaults to True.
|
330
|
+
size_limit (int, optional): Maximum size of the cache in bytes. Defaults to 10GB.
|
331
|
+
"""
|
177
332
|
super().__init__(
|
178
333
|
executor=executor,
|
179
334
|
cache_name=cache_name,
|
@@ -184,4 +339,14 @@ class JobFunctionCache(FutureCache):
|
|
184
339
|
self.function = function
|
185
340
|
|
186
341
|
def call(self, **kwargs) -> JobFuture:
|
342
|
+
"""Call the wrapped function with the provided arguments.
|
343
|
+
|
344
|
+
This method creates a Job for the function call and submits it through the cache.
|
345
|
+
|
346
|
+
Args:
|
347
|
+
**kwargs: Arguments to pass to the function
|
348
|
+
|
349
|
+
Returns:
|
350
|
+
JobFuture: A future representing the function call
|
351
|
+
"""
|
187
352
|
return self.submit(Job(self.call_count, self.function, kwargs))
|