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.
Files changed (84) hide show
  1. bencher/__init__.py +20 -2
  2. bencher/bench_cfg.py +262 -54
  3. bencher/bench_report.py +2 -2
  4. bencher/bench_runner.py +96 -10
  5. bencher/bencher.py +421 -89
  6. bencher/class_enum.py +70 -7
  7. bencher/example/example_dataframe.py +2 -2
  8. bencher/example/example_levels.py +17 -173
  9. bencher/example/example_pareto.py +107 -31
  10. bencher/example/example_rerun2.py +1 -1
  11. bencher/example/example_simple_bool.py +2 -2
  12. bencher/example/example_simple_float2d.py +6 -1
  13. bencher/example/example_video.py +2 -0
  14. bencher/example/experimental/example_hvplot_explorer.py +2 -2
  15. bencher/example/inputs_0D/example_0_in_1_out.py +25 -15
  16. bencher/example/inputs_0D/example_0_in_2_out.py +12 -3
  17. bencher/example/inputs_0_float/example_0_cat_in_2_out.py +88 -0
  18. bencher/example/inputs_0_float/example_1_cat_in_2_out.py +98 -0
  19. bencher/example/inputs_0_float/example_2_cat_in_2_out.py +107 -0
  20. bencher/example/inputs_0_float/example_3_cat_in_2_out.py +111 -0
  21. bencher/example/inputs_1D/example1d_common.py +48 -12
  22. bencher/example/inputs_1D/example_0_float_1_cat.py +33 -0
  23. bencher/example/inputs_1D/example_1_cat_in_2_out_repeats.py +68 -0
  24. bencher/example/inputs_1D/example_1_float_2_cat_repeats.py +3 -0
  25. bencher/example/inputs_1D/example_1_int_in_1_out.py +98 -0
  26. bencher/example/inputs_1D/example_1_int_in_2_out.py +101 -0
  27. bencher/example/inputs_1D/example_1_int_in_2_out_repeats.py +99 -0
  28. bencher/example/inputs_1_float/example_1_float_0_cat_in_2_out.py +117 -0
  29. bencher/example/inputs_1_float/example_1_float_1_cat_in_2_out.py +124 -0
  30. bencher/example/inputs_1_float/example_1_float_2_cat_in_2_out.py +132 -0
  31. bencher/example/inputs_1_float/example_1_float_3_cat_in_2_out.py +140 -0
  32. bencher/example/inputs_2D/example_2_cat_in_4_out_repeats.py +104 -0
  33. bencher/example/inputs_2_float/example_2_float_0_cat_in_2_out.py +98 -0
  34. bencher/example/inputs_2_float/example_2_float_1_cat_in_2_out.py +112 -0
  35. bencher/example/inputs_2_float/example_2_float_2_cat_in_2_out.py +122 -0
  36. bencher/example/inputs_2_float/example_2_float_3_cat_in_2_out.py +138 -0
  37. bencher/example/inputs_3_float/example_3_float_0_cat_in_2_out.py +111 -0
  38. bencher/example/inputs_3_float/example_3_float_1_cat_in_2_out.py +117 -0
  39. bencher/example/inputs_3_float/example_3_float_2_cat_in_2_out.py +124 -0
  40. bencher/example/inputs_3_float/example_3_float_3_cat_in_2_out.py +129 -0
  41. bencher/example/meta/generate_examples.py +118 -7
  42. bencher/example/meta/generate_meta.py +88 -40
  43. bencher/job.py +174 -9
  44. bencher/plotting/plot_filter.py +52 -17
  45. bencher/results/bench_result.py +117 -25
  46. bencher/results/bench_result_base.py +117 -8
  47. bencher/results/dataset_result.py +6 -200
  48. bencher/results/explorer_result.py +23 -0
  49. bencher/results/{hvplot_result.py → histogram_result.py} +3 -18
  50. bencher/results/holoview_results/__init__.py +0 -0
  51. bencher/results/holoview_results/bar_result.py +79 -0
  52. bencher/results/holoview_results/curve_result.py +110 -0
  53. bencher/results/holoview_results/distribution_result/__init__.py +0 -0
  54. bencher/results/holoview_results/distribution_result/box_whisker_result.py +73 -0
  55. bencher/results/holoview_results/distribution_result/distribution_result.py +109 -0
  56. bencher/results/holoview_results/distribution_result/scatter_jitter_result.py +92 -0
  57. bencher/results/holoview_results/distribution_result/violin_result.py +70 -0
  58. bencher/results/holoview_results/heatmap_result.py +319 -0
  59. bencher/results/holoview_results/holoview_result.py +346 -0
  60. bencher/results/holoview_results/line_result.py +240 -0
  61. bencher/results/holoview_results/scatter_result.py +107 -0
  62. bencher/results/holoview_results/surface_result.py +158 -0
  63. bencher/results/holoview_results/table_result.py +14 -0
  64. bencher/results/holoview_results/tabulator_result.py +20 -0
  65. bencher/results/optuna_result.py +30 -115
  66. bencher/results/video_controls.py +38 -0
  67. bencher/results/video_result.py +39 -36
  68. bencher/results/video_summary.py +2 -2
  69. bencher/results/{plotly_result.py → volume_result.py} +29 -8
  70. bencher/utils.py +175 -26
  71. bencher/variables/inputs.py +122 -15
  72. bencher/video_writer.py +2 -1
  73. bencher/worker_job.py +31 -3
  74. {holobench-1.41.0.dist-info → holobench-1.43.0.dist-info}/METADATA +24 -24
  75. holobench-1.43.0.dist-info/RECORD +147 -0
  76. bencher/example/example_levels2.py +0 -37
  77. bencher/example/inputs_1D/example_1_in_1_out.py +0 -62
  78. bencher/example/inputs_1D/example_1_in_2_out.py +0 -63
  79. bencher/example/inputs_1D/example_1_in_2_out_repeats.py +0 -61
  80. bencher/results/holoview_result.py +0 -796
  81. bencher/results/panel_result.py +0 -41
  82. holobench-1.41.0.dist-info/RECORD +0 -114
  83. {holobench-1.41.0.dist-info → holobench-1.43.0.dist-info}/WHEEL +0 -0
  84. {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(filename: str, output_path: str):
6
- # print
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
- function_name = f"{source_path.stem}()"
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
- output_path.write_text(nbf.writes(nb), encoding="utf-8")
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/inputs_1D/example_1_in_1_out.py", "1D"
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/inputs_1D/example_1_in_2_out.py", "1D"
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/inputs_1D/example_1_in_2_out_repeats.py", "1D"
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
- float_vars = bch.IntSweep(
13
- default=0, bounds=(0, 4), doc="The number of floating point variables that are swept"
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
- categorical_vars = bch.IntSweep(
16
- default=0, bounds=(0, 3), doc="The number of categorical variables that are swept"
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
- sample_over_time = bch.BoolSweep(default=False)
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
- # bench = bch.Bench("benchable", BenchableObject(), run_cfg=run_cfg)
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
- inputs_vars_float = [
40
- "float1",
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
- inputs_vars_cat = [
47
- "noisy",
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=["distance", "sample_noise"],
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.float_vars}_float_{self.categorical_vars}_cat"
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
- import bencher as bch
73
- from bencher.example.meta.example_meta import BenchableObject
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 = BenchableObject().to_bench(run_cfg)
126
+ bench = {self.benchable_obj.__class__.__name__}().to_bench(run_cfg)
79
127
  res=bench.plot_sweep(input_vars={input_vars},
80
- result_vars=["distance","sample_noise"])
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("float_vars", [0, 1]),
111
- "categorical_vars",
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.float_vars.with_const(1),
163
+ # BenchMeta.param.float_vars_count.with_const(1),
116
164
  # BenchMeta.param.sample_with_repeats.with_const(2),
117
- # BenchMeta.param.categorical_vars.with_const(2),
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("float_vars", [2, 3]),
127
- "categorical_vars",
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("float_vars", [2, 3]),
137
- # "categorical_vars",
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.categorical_vars.with_const(2),
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
- """The aim of this class is to provide a unified interface for running jobs. T"""
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: bool = False,
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))