holobench 1.40.1__py3-none-any.whl → 1.42.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 (93) hide show
  1. CHANGELOG.md +10 -0
  2. bencher/__init__.py +20 -2
  3. bencher/bench_cfg.py +265 -61
  4. bencher/bench_report.py +2 -2
  5. bencher/bench_runner.py +96 -10
  6. bencher/bencher.py +421 -89
  7. bencher/caching.py +1 -4
  8. bencher/class_enum.py +70 -7
  9. bencher/example/example_composable_container_image.py +60 -0
  10. bencher/example/example_composable_container_video.py +49 -0
  11. bencher/example/example_dataframe.py +2 -2
  12. bencher/example/example_image.py +17 -21
  13. bencher/example/example_image1.py +16 -20
  14. bencher/example/example_levels.py +17 -173
  15. bencher/example/example_pareto.py +107 -31
  16. bencher/example/example_rerun2.py +1 -1
  17. bencher/example/example_simple_bool.py +2 -2
  18. bencher/example/example_simple_float2d.py +6 -1
  19. bencher/example/example_video.py +35 -17
  20. bencher/example/experimental/example_hvplot_explorer.py +3 -4
  21. bencher/example/inputs_0D/example_0_in_1_out.py +25 -15
  22. bencher/example/inputs_0D/example_0_in_2_out.py +12 -3
  23. bencher/example/inputs_0_float/example_0_cat_in_2_out.py +88 -0
  24. bencher/example/inputs_0_float/example_1_cat_in_2_out.py +98 -0
  25. bencher/example/inputs_0_float/example_2_cat_in_2_out.py +107 -0
  26. bencher/example/inputs_0_float/example_3_cat_in_2_out.py +111 -0
  27. bencher/example/inputs_1D/example1d_common.py +48 -12
  28. bencher/example/inputs_1D/example_0_float_1_cat.py +33 -0
  29. bencher/example/inputs_1D/example_1_cat_in_2_out_repeats.py +68 -0
  30. bencher/example/inputs_1D/example_1_float_2_cat_repeats.py +15 -0
  31. bencher/example/inputs_1D/example_1_int_in_1_out.py +98 -0
  32. bencher/example/inputs_1D/example_1_int_in_2_out.py +101 -0
  33. bencher/example/inputs_1D/example_1_int_in_2_out_repeats.py +99 -0
  34. bencher/example/inputs_1_float/example_1_float_0_cat_in_2_out.py +117 -0
  35. bencher/example/inputs_1_float/example_1_float_1_cat_in_2_out.py +124 -0
  36. bencher/example/inputs_1_float/example_1_float_2_cat_in_2_out.py +132 -0
  37. bencher/example/inputs_1_float/example_1_float_3_cat_in_2_out.py +140 -0
  38. bencher/example/inputs_2D/example_2_cat_in_4_out_repeats.py +104 -0
  39. bencher/example/inputs_2_float/example_2_float_0_cat_in_2_out.py +98 -0
  40. bencher/example/inputs_2_float/example_2_float_1_cat_in_2_out.py +112 -0
  41. bencher/example/inputs_2_float/example_2_float_2_cat_in_2_out.py +122 -0
  42. bencher/example/inputs_2_float/example_2_float_3_cat_in_2_out.py +138 -0
  43. bencher/example/inputs_3_float/example_3_float_0_cat_in_2_out.py +111 -0
  44. bencher/example/inputs_3_float/example_3_float_1_cat_in_2_out.py +117 -0
  45. bencher/example/inputs_3_float/example_3_float_2_cat_in_2_out.py +124 -0
  46. bencher/example/inputs_3_float/example_3_float_3_cat_in_2_out.py +129 -0
  47. bencher/example/meta/generate_examples.py +124 -7
  48. bencher/example/meta/generate_meta.py +88 -40
  49. bencher/job.py +175 -12
  50. bencher/plotting/plot_filter.py +52 -17
  51. bencher/results/bench_result.py +119 -26
  52. bencher/results/bench_result_base.py +119 -10
  53. bencher/results/composable_container/composable_container_video.py +39 -12
  54. bencher/results/dataset_result.py +6 -200
  55. bencher/results/explorer_result.py +23 -0
  56. bencher/results/{hvplot_result.py → histogram_result.py} +3 -18
  57. bencher/results/holoview_results/__init__.py +0 -0
  58. bencher/results/holoview_results/bar_result.py +79 -0
  59. bencher/results/holoview_results/curve_result.py +110 -0
  60. bencher/results/holoview_results/distribution_result/__init__.py +0 -0
  61. bencher/results/holoview_results/distribution_result/box_whisker_result.py +73 -0
  62. bencher/results/holoview_results/distribution_result/distribution_result.py +109 -0
  63. bencher/results/holoview_results/distribution_result/scatter_jitter_result.py +92 -0
  64. bencher/results/holoview_results/distribution_result/violin_result.py +70 -0
  65. bencher/results/holoview_results/heatmap_result.py +319 -0
  66. bencher/results/holoview_results/holoview_result.py +346 -0
  67. bencher/results/holoview_results/line_result.py +240 -0
  68. bencher/results/holoview_results/scatter_result.py +107 -0
  69. bencher/results/holoview_results/surface_result.py +158 -0
  70. bencher/results/holoview_results/table_result.py +14 -0
  71. bencher/results/holoview_results/tabulator_result.py +20 -0
  72. bencher/results/laxtex_result.py +42 -35
  73. bencher/results/optuna_result.py +30 -115
  74. bencher/results/video_controls.py +38 -0
  75. bencher/results/video_result.py +39 -36
  76. bencher/results/video_summary.py +2 -2
  77. bencher/results/{plotly_result.py → volume_result.py} +29 -8
  78. bencher/utils.py +176 -30
  79. bencher/variables/inputs.py +122 -15
  80. bencher/video_writer.py +38 -2
  81. bencher/worker_job.py +34 -7
  82. {holobench-1.40.1.dist-info → holobench-1.42.0.dist-info}/METADATA +21 -25
  83. holobench-1.42.0.dist-info/RECORD +147 -0
  84. bencher/example/example_composable_container.py +0 -106
  85. bencher/example/example_levels2.py +0 -37
  86. bencher/example/inputs_1D/example_1_in_1_out.py +0 -62
  87. bencher/example/inputs_1D/example_1_in_2_out.py +0 -63
  88. bencher/example/inputs_1D/example_1_in_2_out_repeats.py +0 -61
  89. bencher/results/holoview_result.py +0 -787
  90. bencher/results/panel_result.py +0 -41
  91. holobench-1.40.1.dist-info/RECORD +0 -111
  92. {holobench-1.40.1.dist-info → holobench-1.42.0.dist-info}/WHEEL +0 -0
  93. {holobench-1.40.1.dist-info → holobench-1.42.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
  from typing import Callable
3
- from sortedcontainers import SortedDict
4
3
  import logging
5
4
  from diskcache import Cache
6
5
  from concurrent.futures import Future, ProcessPoolExecutor
@@ -11,27 +10,72 @@ from enum import auto
11
10
  try:
12
11
  from scoop import futures as scoop_future_executor
13
12
  except ImportError as e:
14
- logging.warning(e.msg)
15
13
  scoop_future_executor = None
16
14
 
17
15
 
18
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
+
19
30
  def __init__(
20
- 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 = ""
21
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
+ """
22
43
  self.job_id = job_id
23
44
  self.function = function
24
45
  self.job_args = job_args
25
46
  if job_key is None:
26
- self.job_key = hash_sha1(tuple(SortedDict(self.job_args).items()))
47
+ self.job_key = hash_sha1(tuple(sorted(self.job_args.items())))
27
48
  else:
28
49
  self.job_key = job_key
29
50
  self.tag = tag
30
51
 
31
52
 
32
- # @dataclass
33
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
+
34
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
+ """
35
79
  self.job = job
36
80
  self.res = res
37
81
  self.future = future
@@ -42,7 +86,16 @@ class JobFuture:
42
86
 
43
87
  self.cache = cache
44
88
 
45
- 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
+ """
46
99
  if self.future is not None:
47
100
  self.res = self.future.result()
48
101
  if self.cache is not None and self.res is not None:
@@ -51,18 +104,42 @@ class JobFuture:
51
104
 
52
105
 
53
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
+ """
54
117
  result = job.function(**job.job_args)
55
118
  return result
56
119
 
57
120
 
58
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
+
59
128
  SERIAL = auto() # slow but reliable
60
129
  MULTIPROCESSING = auto() # breaks for large number of futures
61
130
  SCOOP = auto() # requires running with python -m scoop your_file.py
62
131
  # THREADS=auto() #not that useful as most bench code is cpu bound
63
132
 
64
133
  @staticmethod
65
- 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
+ """
66
143
  providers = {
67
144
  Executors.SERIAL: None,
68
145
  Executors.MULTIPROCESSING: ProcessPoolExecutor(),
@@ -72,7 +149,23 @@ class Executors(StrEnum):
72
149
 
73
150
 
74
151
  class FutureCache:
75
- """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
+ """
76
169
 
77
170
  def __init__(
78
171
  self,
@@ -81,8 +174,18 @@ class FutureCache:
81
174
  cache_name: str = "fcache",
82
175
  tag_index: bool = True,
83
176
  size_limit: int = int(20e9), # 20 GB
84
- cache_results=True,
177
+ cache_results: bool = True,
85
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
+ """
86
189
  self.executor_type = executor
87
190
  self.executor = None
88
191
  if cache_results:
@@ -100,6 +203,18 @@ class FutureCache:
100
203
  self.worker_cache_call_count = 0
101
204
 
102
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
+ """
103
218
  self.worker_wrapper_call_count += 1
104
219
 
105
220
  if self.cache is not None:
@@ -132,25 +247,38 @@ class FutureCache:
132
247
  )
133
248
 
134
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
+ """
135
256
  msg = "OVERWRITING" if self.overwrite else "NOT in"
136
257
  logging.info(f"{job.job_id} {msg} cache{suffix}")
137
258
 
138
259
  def clear_call_counts(self) -> None:
139
- """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."""
140
261
  self.worker_wrapper_call_count = 0
141
262
  self.worker_fn_call_count = 0
142
263
  self.worker_cache_call_count = 0
143
264
 
144
265
  def clear_cache(self) -> None:
266
+ """Clear all entries from the cache."""
145
267
  if self.cache:
146
268
  self.cache.clear()
147
269
 
148
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
+ """
149
276
  logging.info(f"clearing the sample cache for tag: {tag}")
150
277
  removed_vals = self.cache.evict(tag)
151
278
  logging.info(f"removed: {removed_vals} items from the cache")
152
279
 
153
280
  def close(self) -> None:
281
+ """Close the cache and shutdown the executor if they exist."""
154
282
  if self.cache:
155
283
  self.cache.close()
156
284
  if self.executor:
@@ -158,6 +286,11 @@ class FutureCache:
158
286
  self.executor = None
159
287
 
160
288
  def stats(self) -> str:
289
+ """Get statistics about cache usage.
290
+
291
+ Returns:
292
+ str: A string with cache size information
293
+ """
161
294
  logging.info(f"job calls: {self.worker_wrapper_call_count}")
162
295
  logging.info(f"cache calls: {self.worker_cache_call_count}")
163
296
  logging.info(f"worker calls: {self.worker_fn_call_count}")
@@ -167,15 +300,35 @@ class FutureCache:
167
300
 
168
301
 
169
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
+
170
313
  def __init__(
171
314
  self,
172
315
  function: Callable,
173
- overwrite=False,
174
- executor: bool = False,
316
+ overwrite: bool = False,
317
+ executor: Executors = Executors.SERIAL,
175
318
  cache_name: str = "fcache",
176
319
  tag_index: bool = True,
177
320
  size_limit: int = int(100e8),
178
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
+ """
179
332
  super().__init__(
180
333
  executor=executor,
181
334
  cache_name=cache_name,
@@ -186,4 +339,14 @@ class JobFunctionCache(FutureCache):
186
339
  self.function = function
187
340
 
188
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
+ """
189
352
  return self.submit(Job(self.call_count, self.function, kwargs))
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
  from typing import Optional
3
3
  from dataclasses import dataclass
4
4
  from bencher.plotting.plt_cnt_cfg import PltCntCfg
5
+ import logging
5
6
  import panel as pn
6
7
 
7
8
 
@@ -43,9 +44,20 @@ class VarRange:
43
44
 
44
45
  return lower_match and upper_match
45
46
 
46
- def matches_info(self, val, name):
47
+ def matches_info(self, val: int, name: str) -> tuple[bool, str]:
48
+ """Get matching info for a value with a descriptive name.
49
+
50
+ Args:
51
+ val (int): A positive integer to check against the range
52
+ name (str): A descriptive name for the value being checked, used in the output string
53
+
54
+ Returns:
55
+ tuple[bool, str]: A tuple containing:
56
+ - bool: True if the value matches the range, False otherwise
57
+ - str: A formatted string describing the match result
58
+ """
47
59
  match = self.matches(val)
48
- info = f"{name}\t{match}\t{self.lower_bound}>= {val} <={self.upper_bound}"
60
+ info = f"{name}\t{self.lower_bound}>= {val} <={self.upper_bound} is {match}"
49
61
  return match, info
50
62
 
51
63
  def __str__(self) -> str:
@@ -65,13 +77,21 @@ class PlotFilter:
65
77
  input_range: VarRange = VarRange(1, None)
66
78
 
67
79
  def matches_result(
68
- self, plt_cnt_cfg: PltCntCfg, plot_name: str, override: bool = False
80
+ self, plt_cnt_cfg: PltCntCfg, plot_name: str, override: bool
69
81
  ) -> PlotMatchesResult:
70
- """Checks if the result data signature matches the type of data the plot is able to display."""
82
+ """Checks if the result data signature matches the type of data the plot is able to display.
83
+
84
+ Args:
85
+ plt_cnt_cfg (PltCntCfg): Configuration containing counts of different plot elements
86
+ plot_name (str): Name of the plot being checked
87
+ override (bool): Whether to override filter matching rules
88
+
89
+ Returns:
90
+ PlotMatchesResult: Object containing match results and information
91
+ """
71
92
  return PlotMatchesResult(self, plt_cnt_cfg, plot_name, override)
72
93
 
73
94
 
74
- # @dataclass
75
95
  class PlotMatchesResult:
76
96
  """Stores information about which properties match the requirements of a particular plotter"""
77
97
 
@@ -80,12 +100,20 @@ class PlotMatchesResult:
80
100
  plot_filter: PlotFilter,
81
101
  plt_cnt_cfg: PltCntCfg,
82
102
  plot_name: str,
83
- override: bool = False,
84
- ):
85
- match_info = []
86
- matches = []
103
+ override: bool,
104
+ ) -> None:
105
+ """Initialize a PlotMatchesResult with filter matching information.
87
106
 
88
- match_candidates = [
107
+ Args:
108
+ plot_filter (PlotFilter): The filter defining acceptable ranges for plot properties
109
+ plt_cnt_cfg (PltCntCfg): Configuration containing counts of different plot elements
110
+ plot_name (str): Name of the plot being checked
111
+ override (bool): Whether to override filter matching rules
112
+ """
113
+ match_info: list[str] = []
114
+ matches: list[bool] = []
115
+
116
+ match_candidates: list[tuple[VarRange, int, str]] = [
89
117
  (plot_filter.float_range, plt_cnt_cfg.float_cnt, "float"),
90
118
  (plot_filter.cat_range, plt_cnt_cfg.cat_cnt, "cat"),
91
119
  (plot_filter.vector_len, plt_cnt_cfg.vector_len, "vec"),
@@ -99,7 +127,7 @@ class PlotMatchesResult:
99
127
  match, info = m.matches_info(cnt, name)
100
128
  matches.append(match)
101
129
  if not match:
102
- match_info.append(info)
130
+ match_info.append(f"\t{info}")
103
131
  if override:
104
132
  match_info.append(f"override: {override}")
105
133
  self.overall = True
@@ -107,15 +135,22 @@ class PlotMatchesResult:
107
135
  self.overall = all(matches)
108
136
 
109
137
  match_info.insert(0, f"plot {plot_name} matches: {self.overall}")
110
- self.matches_info = "\n".join(match_info).strip()
111
- self.plt_cnt_cfg = plt_cnt_cfg
138
+ self.matches_info: str = "\n".join(match_info).strip()
139
+ self.plt_cnt_cfg: PltCntCfg = plt_cnt_cfg
112
140
 
113
- if self.plt_cnt_cfg.print_debug:
114
- print(f"checking {plot_name} result: {self.overall}")
115
- if not self.overall:
116
- print(self.matches_info)
141
+ # if self.plt_cnt_cfg.print_debug:
142
+ logging.info(self.matches_info)
117
143
 
118
144
  def to_panel(self, **kwargs) -> Optional[pn.pane.Markdown]:
145
+ """Convert match information to a Panel Markdown pane if debug mode is enabled.
146
+
147
+ Args:
148
+ **kwargs: Additional keyword arguments to pass to the Panel Markdown constructor
149
+
150
+ Returns:
151
+ Optional[pn.pane.Markdown]: A Markdown pane containing match information if in debug mode,
152
+ None otherwise
153
+ """
119
154
  if self.plt_cnt_cfg.print_debug:
120
155
  return pn.pane.Markdown(self.matches_info, **kwargs)
121
156
  return None