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
@@ -1,29 +1,50 @@
1
- import panel as pn
2
- import plotly.graph_objs as go
3
- from typing import Optional
4
- import xarray as xr
1
+ from typing import Optional, Any
5
2
 
3
+ import xarray as xr
6
4
  from param import Parameter
5
+ import panel as pn
6
+ import plotly.graph_objs as go
7
7
 
8
8
  from bencher.plotting.plot_filter import VarRange
9
9
  from bencher.results.bench_result_base import BenchResultBase, ReduceType
10
10
  from bencher.variables.results import ResultVar
11
11
 
12
12
 
13
- class PlotlyResult(BenchResultBase):
14
- def to_volume(self, result_var: Parameter = None, **kwargs):
13
+ class VolumeResult(BenchResultBase):
14
+ def to_plot(
15
+ self, result_var: Optional[Parameter] = None, override: bool = True, **kwargs: Any
16
+ ) -> Optional[pn.panel]:
17
+ """Generates a 3d volume plot from benchmark data.
18
+
19
+ Args:
20
+ result_var (Optional[Parameter]): The result variable to plot. If None, uses the default.
21
+ override (bool): Whether to override filter restrictions. Defaults to True.
22
+ **kwargs (Any): Additional keyword arguments passed to the plot rendering.
23
+
24
+ Returns:
25
+ Optional[pn.panel]: A panel containing the volume plot if data is appropriate,
26
+ otherwise returns filter match results.
27
+ """
28
+ return self.to_volume(
29
+ result_var=result_var,
30
+ override=override,
31
+ **kwargs,
32
+ )
33
+
34
+ def to_volume(self, result_var: Parameter = None, override: bool = True, **kwargs):
15
35
  return self.filter(
16
- self.to_volume_da,
36
+ self.to_volume_ds,
17
37
  float_range=VarRange(3, 3),
18
38
  cat_range=VarRange(-1, 0),
19
39
  reduce=ReduceType.REDUCE,
20
40
  target_dimension=3,
21
41
  result_var=result_var,
22
42
  result_types=(ResultVar),
43
+ override=override,
23
44
  **kwargs,
24
45
  )
25
46
 
26
- def to_volume_da(
47
+ def to_volume_ds(
27
48
  self, dataset: xr.Dataset, result_var: Parameter, width=600, height=600
28
49
  ) -> Optional[pn.pane.Plotly]:
29
50
  """Given a benchCfg generate a 3D surface plot
bencher/utils.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from collections import namedtuple
2
2
  import xarray as xr
3
- from sortedcontainers import SortedDict
4
3
  import hashlib
5
4
  import re
6
5
  import math
@@ -27,9 +26,7 @@ def hmap_canonical_input(dic: dict) -> tuple:
27
26
  Returns:
28
27
  tuple: values of the dictionary always in the same order and hashable
29
28
  """
30
-
31
- function_input = SortedDict(dic)
32
- return tuple(function_input.values())
29
+ return tuple(value for _, value in sorted(dic.items()))
33
30
 
34
31
 
35
32
  def make_namedtuple(class_name: str, **fields) -> namedtuple:
@@ -44,16 +41,22 @@ def make_namedtuple(class_name: str, **fields) -> namedtuple:
44
41
  return namedtuple(class_name, fields)(*fields.values())
45
42
 
46
43
 
47
- def get_nearest_coords(dataset: xr.Dataset, collapse_list=False, **kwargs) -> dict:
48
- """Given an xarray dataset and kwargs of key value pairs of coordinate values, return a dictionary of the nearest coordinate name value pair that was found in the dataset
44
+ def get_nearest_coords(dataset: xr.Dataset, collapse_list: bool = False, **kwargs) -> dict:
45
+ """Find the nearest coordinates in an xarray dataset based on provided coordinate values.
46
+
47
+ Given an xarray dataset and kwargs of key-value pairs of coordinate values, return a dictionary
48
+ of the nearest coordinate name-value pair that was found in the dataset.
49
49
 
50
50
  Args:
51
- ds (xr.Dataset): dataset
51
+ dataset (xr.Dataset): The xarray dataset to search in
52
+ collapse_list (bool, optional): If True, when a coordinate value is a list, only the first
53
+ item is returned. Defaults to False.
54
+ **kwargs: Key-value pairs where keys are coordinate names and values are points to find
55
+ the nearest match for
52
56
 
53
57
  Returns:
54
- dict: nearest coordinate name value pair that matches the input coordinate name value pairs.
58
+ dict: Dictionary of coordinate name-value pairs with the nearest values found in the dataset
55
59
  """
56
-
57
60
  selection = dataset.sel(method="nearest", **kwargs)
58
61
  cd = selection.coords.to_dataset().to_dict()["coords"]
59
62
  cd2 = {}
@@ -64,7 +67,19 @@ def get_nearest_coords(dataset: xr.Dataset, collapse_list=False, **kwargs) -> di
64
67
  return cd2
65
68
 
66
69
 
67
- def get_nearest_coords1D(val: Any, coords) -> Any:
70
+ def get_nearest_coords1D(val: Any, coords: List[Any]) -> Any:
71
+ """Find the closest coordinate to a given value in a list of coordinates.
72
+
73
+ For numeric values, finds the value in coords that is closest to val.
74
+ For non-numeric values, returns the exact match if found, otherwise returns val.
75
+
76
+ Args:
77
+ val (Any): The value to find the closest coordinate for
78
+ coords (List[Any]): The list of coordinates to search in
79
+
80
+ Returns:
81
+ Any: The closest coordinate value from the list
82
+ """
68
83
  if isinstance(val, (int, float)):
69
84
  return min(coords, key=lambda x_: abs(x_ - val))
70
85
  for i in coords:
@@ -73,19 +88,28 @@ def get_nearest_coords1D(val: Any, coords) -> Any:
73
88
  return val
74
89
 
75
90
 
76
- def hash_sha1(var: any) -> str:
77
- """A hash function that avoids the PYTHONHASHSEED 'feature' which returns a different hash value each time the program is run"""
91
+ def hash_sha1(var: Any) -> str:
92
+ """A hash function that avoids the PYTHONHASHSEED 'feature' which returns a different hash value each time the program is run.
93
+
94
+ Converts input to a consistent SHA1 hash string.
95
+
96
+ Args:
97
+ var (Any): The variable to hash
98
+
99
+ Returns:
100
+ str: A hexadecimal SHA1 hash of the string representation of the variable
101
+ """
78
102
  return hashlib.sha1(str(var).encode("ASCII")).hexdigest()
79
103
 
80
104
 
81
- def capitalise_words(message: str):
82
- """Given a string of lowercase words, capitalise them
105
+ def capitalise_words(message: str) -> str:
106
+ """Given a string of lowercase words, capitalise them.
83
107
 
84
108
  Args:
85
109
  message (str): lower case string
86
110
 
87
111
  Returns:
88
- _type_: capitalised string
112
+ str: capitalised string where each word starts with an uppercase letter
89
113
  """
90
114
  capitalized_message = " ".join([word.capitalize() for word in message.split(" ")])
91
115
  return capitalized_message
@@ -104,7 +128,16 @@ def un_camel(camel: str) -> str:
104
128
  return capitalise_words(re.sub("([a-z])([A-Z])", r"\g<1> \g<2>", camel.replace("_", " ")))
105
129
 
106
130
 
107
- def mult_tuple(inp: Tuple[float], val: float) -> Tuple[float]:
131
+ def mult_tuple(inp: Tuple[float, ...], val: float) -> Tuple[float, ...]:
132
+ """Multiply each element in a tuple by a scalar value.
133
+
134
+ Args:
135
+ inp (Tuple[float, ...]): The input tuple of floats to multiply
136
+ val (float): The scalar value to multiply each element by
137
+
138
+ Returns:
139
+ Tuple[float, ...]: A new tuple with each element multiplied by val
140
+ """
108
141
  return tuple(np.array(inp) * val)
109
142
 
110
143
 
@@ -121,15 +154,17 @@ def tabs_in_markdown(regular_str: str, spaces: int = 2) -> str:
121
154
  return regular_str.replace("\t", "".join(["&nbsp;"] * spaces))
122
155
 
123
156
 
124
- def int_to_col(int_val, sat=0.5, val=0.95, alpha=-1) -> tuple[float, float, float]:
157
+ def int_to_col(
158
+ int_val: int, sat: float = 0.5, val: float = 0.95, alpha: float = -1
159
+ ) -> tuple[float, float, float] | tuple[float, float, float, float]:
125
160
  """Uses the golden angle to generate colors programmatically with minimum overlap between colors.
126
161
  https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
127
162
 
128
163
  Args:
129
- int_val (_type_): index of an object you want to color, this is mapped to hue in HSV
164
+ int_val (int): index of an object you want to color, this is mapped to hue in HSV
130
165
  sat (float, optional): saturation in HSV. Defaults to 0.5.
131
166
  val (float, optional): value in HSV. Defaults to 0.95.
132
- alpha (int, optional): transparency. If -1 then only RGB is returned, if 0 or greater, RGBA is returned. Defaults to -1.
167
+ alpha (float, optional): transparency. If -1 then only RGB is returned, if 0 or greater, RGBA is returned. Defaults to -1.
133
168
 
134
169
  Returns:
135
170
  tuple[float, float, float] | tuple[float, float, float, float]: either RGB or RGBA vector
@@ -141,7 +176,23 @@ def int_to_col(int_val, sat=0.5, val=0.95, alpha=-1) -> tuple[float, float, floa
141
176
  return rgb
142
177
 
143
178
 
144
- def lerp(value, input_low: float, input_high: float, output_low: float, output_high: float):
179
+ def lerp(
180
+ value: float, input_low: float, input_high: float, output_low: float, output_high: float
181
+ ) -> float:
182
+ """Linear interpolation between two ranges.
183
+
184
+ Maps a value from one range [input_low, input_high] to another range [output_low, output_high].
185
+
186
+ Args:
187
+ value (float): The input value to interpolate
188
+ input_low (float): The lower bound of the input range
189
+ input_high (float): The upper bound of the input range
190
+ output_low (float): The lower bound of the output range
191
+ output_high (float): The upper bound of the output range
192
+
193
+ Returns:
194
+ float: The interpolated value in the output range
195
+ """
145
196
  input_low = float(input_low)
146
197
  return output_low + ((float(value) - input_low) / (float(input_high) - input_low)) * (
147
198
  float(output_high) - output_low
@@ -149,10 +200,26 @@ def lerp(value, input_low: float, input_high: float, output_low: float, output_h
149
200
 
150
201
 
151
202
  def color_tuple_to_css(color: tuple[float, float, float]) -> str:
203
+ """Convert a RGB color tuple to CSS rgb format string.
204
+
205
+ Args:
206
+ color (tuple[float, float, float]): RGB color tuple with values in range [0.0, 1.0]
207
+
208
+ Returns:
209
+ str: CSS color string in format 'rgb(r, g, b)' with values in range [0, 255]
210
+ """
152
211
  return f"rgb{(color[0] * 255, color[1] * 255, color[2] * 255)}"
153
212
 
154
213
 
155
- def color_tuple_to_255(color: tuple[float, float, float]) -> tuple[float, float, float]:
214
+ def color_tuple_to_255(color: tuple[float, float, float]) -> tuple[int, int, int]:
215
+ """Convert a RGB color tuple with values in range [0.0, 1.0] to values in range [0, 255].
216
+
217
+ Args:
218
+ color (tuple[float, float, float]): RGB color tuple with values in range [0.0, 1.0]
219
+
220
+ Returns:
221
+ tuple[int, int, int]: RGB color tuple with values clamped to range [0, 255]
222
+ """
156
223
  return (
157
224
  min(int(color[0] * 255), 255),
158
225
  min(int(color[1] * 255), 255),
@@ -160,25 +227,76 @@ def color_tuple_to_255(color: tuple[float, float, float]) -> tuple[float, float,
160
227
  )
161
228
 
162
229
 
163
- def gen_path(filename, folder="generic", suffix=".dat"):
230
+ def gen_path(filename: str, folder: str = "generic", suffix: str = ".dat") -> str:
231
+ """Generate a unique path for a file in the cache directory.
232
+
233
+ Creates a directory structure in the 'cachedir' folder and returns a path
234
+ with a UUID to ensure uniqueness.
235
+
236
+ Args:
237
+ filename (str): Base name for the file
238
+ folder (str, optional): Subfolder within cachedir. Defaults to "generic".
239
+ suffix (str, optional): File extension. Defaults to ".dat".
240
+
241
+ Returns:
242
+ str: Absolute path to a unique file location
243
+ """
164
244
  path = Path(f"cachedir/{folder}/{filename}/")
165
245
  path.mkdir(parents=True, exist_ok=True)
166
246
  return f"{path.absolute().as_posix()}/{filename}_{uuid4()}{suffix}"
167
247
 
168
248
 
169
249
  def gen_video_path(video_name: str = "vid", extension: str = ".mp4") -> str:
250
+ """Generate a unique path for a video file in the cache directory.
251
+
252
+ Args:
253
+ video_name (str, optional): Base name for the video file. Defaults to "vid".
254
+ extension (str, optional): Video file extension. Defaults to ".mp4".
255
+
256
+ Returns:
257
+ str: Absolute path to a unique video file location
258
+ """
170
259
  return gen_path(video_name, "vid", extension)
171
260
 
172
261
 
173
- def gen_image_path(image_name: str = "img", filetype=".png") -> str:
262
+ def gen_image_path(image_name: str = "img", filetype: str = ".png") -> str:
263
+ """Generate a unique path for an image file in the cache directory.
264
+
265
+ Args:
266
+ image_name (str, optional): Base name for the image file. Defaults to "img".
267
+ filetype (str, optional): Image file extension. Defaults to ".png".
268
+
269
+ Returns:
270
+ str: Absolute path to a unique image file location
271
+ """
174
272
  return gen_path(image_name, "img", filetype)
175
273
 
176
274
 
177
- def gen_rerun_data_path(rrd_name: str = "rrd", filetype=".rrd") -> str:
275
+ def gen_rerun_data_path(rrd_name: str = "rrd", filetype: str = ".rrd") -> str:
276
+ """Generate a unique path for a rerun data file in the cache directory.
277
+
278
+ Args:
279
+ rrd_name (str, optional): Base name for the rerun data file. Defaults to "rrd".
280
+ filetype (str, optional): File extension. Defaults to ".rrd".
281
+
282
+ Returns:
283
+ str: Absolute path to a unique rerun data file location
284
+ """
178
285
  return gen_path(rrd_name, "rrd", filetype)
179
286
 
180
287
 
181
288
  def callable_name(any_callable: Callable[..., Any]) -> str:
289
+ """Extract the name of a callable object, handling various callable types.
290
+
291
+ This function attempts to extract the name of a callable object, including
292
+ regular functions, partial functions, and other callables.
293
+
294
+ Args:
295
+ any_callable (Callable[..., Any]): The callable object to get the name from
296
+
297
+ Returns:
298
+ str: The name of the callable
299
+ """
182
300
  if isinstance(any_callable, partial):
183
301
  return any_callable.func.__name__
184
302
  try:
@@ -187,8 +305,20 @@ def callable_name(any_callable: Callable[..., Any]) -> str:
187
305
  return str(any_callable)
188
306
 
189
307
 
190
- def listify(obj) -> list:
191
- """Take an object and turn it into a list if its not already a list. However if the object is none, don't turn it into a list"""
308
+ def listify(obj: Any) -> List[Any] | None:
309
+ """Convert an object to a list if it's not already a list.
310
+
311
+ This function handles conversion of various object types to lists, with special
312
+ handling for None values and existing list/tuple types.
313
+
314
+ Args:
315
+ obj (Any): The object to convert to a list
316
+
317
+ Returns:
318
+ List[Any] | None: A list containing the object, the object itself if it was
319
+ already a list, a list from the tuple if it was a tuple, or None if the
320
+ input was None
321
+ """
192
322
  if obj is None:
193
323
  return None
194
324
  if isinstance(obj, list):
@@ -198,13 +328,29 @@ def listify(obj) -> list:
198
328
  return [obj]
199
329
 
200
330
 
201
- def get_name(var):
331
+ def get_name(var: Any) -> str:
332
+ """Extract the name from a variable, handling param.Parameter objects.
333
+
334
+ Args:
335
+ var (Any): The variable to extract the name from
336
+
337
+ Returns:
338
+ str: The name of the variable
339
+ """
202
340
  if isinstance(var, param.Parameter):
203
341
  return var.name
204
342
  return var
205
343
 
206
344
 
207
- def params_to_str(param_list: List[param.Parameter]):
345
+ def params_to_str(param_list: List[param.Parameter]) -> List[str]:
346
+ """Convert a list of param.Parameter objects to a list of their names.
347
+
348
+ Args:
349
+ param_list (List[param.Parameter]): List of parameter objects
350
+
351
+ Returns:
352
+ List[str]: List of parameter names
353
+ """
208
354
  return [get_name(i) for i in param_list]
209
355
 
210
356
 
@@ -215,8 +361,8 @@ def publish_file(filepath: str, remote: str, branch_name: str) -> str: # pragma
215
361
 
216
362
  def publish_args(branch_name) -> Tuple[str, str]:
217
363
  return (
218
- "https://github.com/dyson-ai/bencher.git",
219
- f"https://github.com/dyson-ai/bencher/blob/{branch_name}")
364
+ "https://github.com/blooop/bencher.git",
365
+ f"https://github.com/blooop/bencher/blob/{branch_name}")
220
366
 
221
367
 
222
368
  Args:
@@ -7,7 +7,15 @@ from bencher.variables.sweep_base import SweepBase, shared_slots
7
7
 
8
8
 
9
9
  class SweepSelector(Selector, SweepBase):
10
- """A class to represent a parameter sweep of bools"""
10
+ """A class representing a parameter sweep for selectable options.
11
+
12
+ This class extends both Selector and SweepBase to provide parameter sweeping
13
+ capabilities for categorical variables that have a predefined set of options.
14
+
15
+ Attributes:
16
+ units (str): The units of measurement for the parameter
17
+ samples (int): The number of samples to take from the available options
18
+ """
11
19
 
12
20
  __slots__ = shared_slots
13
21
 
@@ -22,14 +30,26 @@ class SweepSelector(Selector, SweepBase):
22
30
  self.samples = samples
23
31
 
24
32
  def values(self) -> List[Any]:
25
- """return all the values for a parameter sweep. If debug is true return a reduced list"""
33
+ """Return all the values for the parameter sweep.
34
+
35
+ Returns:
36
+ List[Any]: A list of parameter values to sweep through
37
+ """
26
38
  return self.indices_to_samples(self.samples, self.objects)
27
39
 
28
40
 
29
41
  class BoolSweep(SweepSelector):
30
- """A class to represent a parameter sweep of bools"""
42
+ """A class representing a parameter sweep for boolean values.
43
+
44
+ This class extends SweepSelector to provide parameter sweeping capabilities
45
+ specifically for boolean values (True and False).
31
46
 
32
- def __init__(self, units: str = "ul", samples: int = None, default=True, **params):
47
+ Attributes:
48
+ units (str): The units of measurement for the parameter
49
+ samples (int): The number of samples to take (typically 2 for booleans)
50
+ """
51
+
52
+ def __init__(self, units: str = "ul", samples: int = None, default: bool = True, **params):
33
53
  SweepSelector.__init__(
34
54
  self,
35
55
  units=units,
@@ -41,7 +61,15 @@ class BoolSweep(SweepSelector):
41
61
 
42
62
 
43
63
  class StringSweep(SweepSelector):
44
- """A class to represent a parameter sweep of strings"""
64
+ """A class representing a parameter sweep for string values.
65
+
66
+ This class extends SweepSelector to provide parameter sweeping capabilities
67
+ specifically for a list of string values.
68
+
69
+ Attributes:
70
+ units (str): The units of measurement for the parameter
71
+ samples (int): The number of samples to take from the available strings
72
+ """
45
73
 
46
74
  def __init__(
47
75
  self,
@@ -61,11 +89,21 @@ class StringSweep(SweepSelector):
61
89
 
62
90
 
63
91
  class EnumSweep(SweepSelector):
64
- """A class to represent a parameter sweep of enums"""
92
+ """A class representing a parameter sweep for enum values.
93
+
94
+ This class extends SweepSelector to provide parameter sweeping capabilities
95
+ specifically for enumeration types, supporting both enum types and lists of enum values.
96
+
97
+ Attributes:
98
+ units (str): The units of measurement for the parameter
99
+ samples (int): The number of samples to take from the available enum values
100
+ """
65
101
 
66
102
  __slots__ = shared_slots
67
103
 
68
- def __init__(self, enum_type: Enum | List[Enum], units="ul", samples=None, **params):
104
+ def __init__(
105
+ self, enum_type: Enum | List[Enum], units: str = "ul", samples: int = None, **params
106
+ ):
69
107
  # The enum can either be an Enum type or a list of enums
70
108
  list_of_enums = isinstance(enum_type, list)
71
109
  selector_list = enum_type if list_of_enums else list(enum_type)
@@ -82,11 +120,23 @@ class EnumSweep(SweepSelector):
82
120
 
83
121
 
84
122
  class IntSweep(Integer, SweepBase):
85
- """A class to represent a parameter sweep of ints"""
123
+ """A class representing a parameter sweep for integer values.
124
+
125
+ This class extends both Integer and SweepBase to provide parameter sweeping capabilities
126
+ specifically for integer values within specified bounds or with custom sample values.
127
+
128
+ Attributes:
129
+ units (str): The units of measurement for the parameter
130
+ samples (int): The number of samples to take from the range
131
+ sample_values (List[int], optional): Specific integer values to use as samples instead of
132
+ generating them from bounds. If provided, overrides the samples parameter.
133
+ """
86
134
 
87
135
  __slots__ = shared_slots + ["sample_values"]
88
136
 
89
- def __init__(self, units="ul", samples=None, sample_values=None, **params):
137
+ def __init__(
138
+ self, units: str = "ul", samples: int = None, sample_values: List[int] = None, **params
139
+ ):
90
140
  SweepBase.__init__(self)
91
141
  Integer.__init__(self, **params)
92
142
 
@@ -107,7 +157,14 @@ class IntSweep(Integer, SweepBase):
107
157
  self.default = sample_values[0]
108
158
 
109
159
  def values(self) -> List[int]:
110
- """return all the values for a parameter sweep. If debug is true return the list"""
160
+ """Return all the values for the parameter sweep.
161
+
162
+ If sample_values is provided, returns those values. Otherwise generates values
163
+ within the specified bounds.
164
+
165
+ Returns:
166
+ List[int]: A list of integer values to sweep through
167
+ """
111
168
  sample_values = (
112
169
  self.sample_values
113
170
  if self.sample_values is not None
@@ -136,11 +193,29 @@ class IntSweep(Integer, SweepBase):
136
193
 
137
194
 
138
195
  class FloatSweep(Number, SweepBase):
139
- """A class to represent a parameter sweep of floats"""
196
+ """A class representing a parameter sweep for floating point values.
197
+
198
+ This class extends both Number and SweepBase to provide parameter sweeping capabilities
199
+ specifically for floating point values within specified bounds or with custom sample values.
200
+
201
+ Attributes:
202
+ units (str): The units of measurement for the parameter
203
+ samples (int): The number of samples to take from the range
204
+ sample_values (List[float], optional): Specific float values to use as samples instead of
205
+ generating them from bounds. If provided, overrides the samples parameter.
206
+ step (float, optional): Step size between samples when generating values from bounds
207
+ """
140
208
 
141
209
  __slots__ = shared_slots + ["sample_values"]
142
210
 
143
- def __init__(self, units="ul", samples=10, sample_values=None, step=None, **params):
211
+ def __init__(
212
+ self,
213
+ units: str = "ul",
214
+ samples: int = 10,
215
+ sample_values: List[float] = None,
216
+ step: float = None,
217
+ **params,
218
+ ):
144
219
  SweepBase.__init__(self)
145
220
  Number.__init__(self, step=step, **params)
146
221
 
@@ -156,7 +231,14 @@ class FloatSweep(Number, SweepBase):
156
231
  self.default = sample_values[0]
157
232
 
158
233
  def values(self) -> List[float]:
159
- """return all the values for a parameter sweep. If debug is true return a reduced list"""
234
+ """Return all the values for the parameter sweep.
235
+
236
+ If sample_values is provided, returns those values. Otherwise generates values
237
+ within the specified bounds, either using linspace (when step is None) or arange.
238
+
239
+ Returns:
240
+ List[float]: A list of float values to sweep through
241
+ """
160
242
  samps = self.samples
161
243
  if self.sample_values is None:
162
244
  if self.step is None:
@@ -166,7 +248,20 @@ class FloatSweep(Number, SweepBase):
166
248
  return self.sample_values
167
249
 
168
250
 
169
- def box(name, center, width):
251
+ def box(name: str, center: float, width: float) -> FloatSweep:
252
+ """Create a FloatSweep parameter centered around a value with a given width.
253
+
254
+ This is a convenience function to create a bounded FloatSweep parameter with
255
+ bounds centered on a specific value, extending by the width in both directions.
256
+
257
+ Args:
258
+ name (str): The name of the parameter
259
+ center (float): The center value of the parameter
260
+ width (float): The distance from the center to the bounds in both directions
261
+
262
+ Returns:
263
+ FloatSweep: A FloatSweep parameter with the specified name, default, and bounds
264
+ """
170
265
  var = FloatSweep(default=center, bounds=(center - width, center + width))
171
266
  var.name = name
172
267
  return var
@@ -195,6 +290,18 @@ def p(
195
290
  return {"name": name, "values": values, "max_level": max_level, "samples": samples}
196
291
 
197
292
 
198
- def with_level(arr: list, level) -> list:
293
+ def with_level(arr: list, level: int) -> list:
294
+ """Apply level-based sampling to a list of values.
295
+
296
+ This function uses an IntSweep with the provided values and applies level-based
297
+ sampling to it, returning the resulting values.
298
+
299
+ Args:
300
+ arr (list): List of values to sample from
301
+ level (int): The sampling level to apply (higher levels provide more samples)
302
+
303
+ Returns:
304
+ list: The level-sampled values
305
+ """
199
306
  return IntSweep(sample_values=arr).with_level(level).values()
200
307
  # return tmp.with_sample_values(arr).with_level(level).values()
bencher/video_writer.py CHANGED
@@ -1,9 +1,8 @@
1
1
  import numpy as np
2
2
  import moviepy.video.io.ImageSequenceClip
3
+ import moviepy.video.io.VideoFileClip
3
4
  from pathlib import Path
4
5
  from .utils import gen_video_path, gen_image_path
5
-
6
- import moviepy
7
6
  from PIL import Image, ImageDraw
8
7
 
9
8
 
@@ -46,6 +45,16 @@ class VideoWriter:
46
45
  new_img.paste(image, (0, padding))
47
46
  return new_img
48
47
 
48
+ @staticmethod
49
+ def convert_to_compatible_format(video_path: str) -> str:
50
+ new_path = Path(video_path)
51
+ new_path = new_path.with_name(f"{new_path.stem}_fixed{new_path.suffix}").as_posix()
52
+ vw = VideoWriter()
53
+ vw.filename = new_path
54
+ with moviepy.video.io.VideoFileClip.VideoFileClip(video_path) as vid:
55
+ vw.write_video_raw(vid)
56
+ return new_path
57
+
49
58
  def write_video_raw(self, video_clip: moviepy.video.VideoClip, fps: int = 30) -> str:
50
59
  video_clip.write_videofile(
51
60
  self.filename,
@@ -59,6 +68,33 @@ class VideoWriter:
59
68
  video_clip.close()
60
69
  return self.filename
61
70
 
71
+ @staticmethod
72
+ def extract_frame(video_path: str, time: float = None, output_path: str = None) -> str:
73
+ """Extract a frame from a video at a specific time.
74
+
75
+ Args:
76
+ video_path: Path to the video file
77
+ time: Time in seconds to extract frame. If None, uses last frame
78
+ output_path: Optional path where to save the image. If None, uses video name with _frame.png
79
+
80
+ Returns:
81
+ str: Path to the saved PNG image
82
+ """
83
+ if output_path is None:
84
+ output_path = (
85
+ Path(video_path).with_stem(f"{Path(video_path).stem}_frame").with_suffix(".png")
86
+ )
87
+ else:
88
+ output_path = Path(output_path)
89
+
90
+ with moviepy.video.io.VideoFileClip.VideoFileClip(video_path) as video:
91
+ frame_time = time if time is not None else video.duration - 2.0 / video.fps
92
+ frame_time = max(frame_time, 0)
93
+ frame = video.get_frame(frame_time)
94
+ Image.fromarray(frame).save(output_path)
95
+
96
+ return output_path.as_posix()
97
+
62
98
 
63
99
  def add_image(np_array: np.ndarray, name: str = "img") -> str:
64
100
  """Creates a file on disk from a numpy array and returns the created image path"""