holobench 1.22.2__tar.gz → 1.23.0__tar.gz

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 (49) hide show
  1. {holobench-1.22.2/holobench.egg-info → holobench-1.23.0}/PKG-INFO +7 -5
  2. {holobench-1.22.2 → holobench-1.23.0}/bencher/__init__.py +9 -0
  3. {holobench-1.22.2 → holobench-1.23.0}/bencher/bench_runner.py +1 -1
  4. {holobench-1.22.2 → holobench-1.23.0}/bencher/bencher.py +3 -1
  5. {holobench-1.22.2 → holobench-1.23.0}/bencher/optuna_conversions.py +3 -2
  6. {holobench-1.22.2 → holobench-1.23.0}/bencher/utils.py +15 -2
  7. holobench-1.23.0/bencher/video_writer.py +67 -0
  8. {holobench-1.22.2 → holobench-1.23.0/holobench.egg-info}/PKG-INFO +7 -5
  9. {holobench-1.22.2 → holobench-1.23.0}/holobench.egg-info/SOURCES.txt +2 -0
  10. {holobench-1.22.2 → holobench-1.23.0}/holobench.egg-info/requires.txt +4 -4
  11. {holobench-1.22.2 → holobench-1.23.0}/pyproject.toml +15 -9
  12. {holobench-1.22.2 → holobench-1.23.0}/setup.py +2 -5
  13. holobench-1.23.0/test/test_composable_container_base.py +77 -0
  14. holobench-1.23.0/test/test_composable_container_video.py +255 -0
  15. {holobench-1.22.2 → holobench-1.23.0}/test/test_utils.py +4 -0
  16. holobench-1.22.2/bencher/video_writer.py +0 -121
  17. {holobench-1.22.2 → holobench-1.23.0}/LICENSE +0 -0
  18. {holobench-1.22.2 → holobench-1.23.0}/README.md +0 -0
  19. {holobench-1.22.2 → holobench-1.23.0}/bencher/bench_cfg.py +0 -0
  20. {holobench-1.22.2 → holobench-1.23.0}/bencher/bench_plot_server.py +0 -0
  21. {holobench-1.22.2 → holobench-1.23.0}/bencher/bench_report.py +0 -0
  22. {holobench-1.22.2 → holobench-1.23.0}/bencher/caching.py +0 -0
  23. {holobench-1.22.2 → holobench-1.23.0}/bencher/class_enum.py +0 -0
  24. {holobench-1.22.2 → holobench-1.23.0}/bencher/job.py +0 -0
  25. {holobench-1.22.2 → holobench-1.23.0}/bencher/worker_job.py +0 -0
  26. {holobench-1.22.2 → holobench-1.23.0}/holobench.egg-info/dependency_links.txt +0 -0
  27. {holobench-1.22.2 → holobench-1.23.0}/holobench.egg-info/not-zip-safe +0 -0
  28. {holobench-1.22.2 → holobench-1.23.0}/holobench.egg-info/top_level.txt +0 -0
  29. {holobench-1.22.2 → holobench-1.23.0}/package.xml +0 -0
  30. {holobench-1.22.2 → holobench-1.23.0}/resource/bencher +0 -0
  31. {holobench-1.22.2 → holobench-1.23.0}/setup.cfg +0 -0
  32. {holobench-1.22.2 → holobench-1.23.0}/test/test_bench_examples.py +0 -0
  33. {holobench-1.22.2 → holobench-1.23.0}/test/test_bench_report.py +0 -0
  34. {holobench-1.22.2 → holobench-1.23.0}/test/test_bench_result_base.py +0 -0
  35. {holobench-1.22.2 → holobench-1.23.0}/test/test_bench_runner.py +0 -0
  36. {holobench-1.22.2 → holobench-1.23.0}/test/test_bencher.py +0 -0
  37. {holobench-1.22.2 → holobench-1.23.0}/test/test_cache.py +0 -0
  38. {holobench-1.22.2 → holobench-1.23.0}/test/test_class_enum.py +0 -0
  39. {holobench-1.22.2 → holobench-1.23.0}/test/test_combinations.py +0 -0
  40. {holobench-1.22.2 → holobench-1.23.0}/test/test_float_formatter.py +0 -0
  41. {holobench-1.22.2 → holobench-1.23.0}/test/test_job.py +0 -0
  42. {holobench-1.22.2 → holobench-1.23.0}/test/test_meta_tests.py +0 -0
  43. {holobench-1.22.2 → holobench-1.23.0}/test/test_parametrized_sweep.py +0 -0
  44. {holobench-1.22.2 → holobench-1.23.0}/test/test_plot_filter.py +0 -0
  45. {holobench-1.22.2 → holobench-1.23.0}/test/test_plot_server.py +0 -0
  46. {holobench-1.22.2 → holobench-1.23.0}/test/test_sample_cache.py +0 -0
  47. {holobench-1.22.2 → holobench-1.23.0}/test/test_sweep_base.py +0 -0
  48. {holobench-1.22.2 → holobench-1.23.0}/test/test_sweep_vars.py +0 -0
  49. {holobench-1.22.2 → holobench-1.23.0}/test/test_vars.py +0 -0
@@ -1,8 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: holobench
3
- Version: 1.22.2
3
+ Version: 1.23.0
4
4
  Summary: A package for benchmarking the performance of arbitrary functions
5
5
  Author-email: Austin Gregg-Smith <blooop@gmail.com>
6
+ Maintainer: austin.gregg-smith
7
+ Maintainer-email: austin.gregg-smith@dyson.com
6
8
  Project-URL: Repository, https://github.com/dyson-ai/bencher
7
9
  Project-URL: Home, https://github.com/dyson-ai/bencher
8
10
  Project-URL: Documentation, https://bencher.readthedocs.io/en/latest/
@@ -13,7 +15,7 @@ Requires-Dist: numpy<=1.26.4,>=1.0
13
15
  Requires-Dist: param<=2.1.0,>=1.13.0
14
16
  Requires-Dist: hvplot<=0.10.0,>=0.8
15
17
  Requires-Dist: matplotlib<=3.9.0,>=3.6.3
16
- Requires-Dist: panel<=1.4.3,>=1.3.6
18
+ Requires-Dist: panel<=1.4.4,>=1.3.6
17
19
  Requires-Dist: diskcache<=5.6.3,>=5.6
18
20
  Requires-Dist: optuna<=3.6.1,>=3.2
19
21
  Requires-Dist: xarray<=2024.5.0,>=2023.7
@@ -30,9 +32,9 @@ Requires-Dist: black<=24.4.2,>=23; extra == "test"
30
32
  Requires-Dist: pylint<=3.2.2,>=2.17.7; extra == "test"
31
33
  Requires-Dist: pytest-cov<=5.0.0,>=4.1; extra == "test"
32
34
  Requires-Dist: pytest<=8.2.1,>=7.4; extra == "test"
33
- Requires-Dist: hypothesis<=6.102.6,>=6.82; extra == "test"
34
- Requires-Dist: ruff<=0.4.5,>=0.3; extra == "test"
35
- Requires-Dist: coverage<=7.5.2,>=7.2.7; extra == "test"
35
+ Requires-Dist: hypothesis<=6.103.0,>=6.82; extra == "test"
36
+ Requires-Dist: ruff<=0.4.7,>=0.3; extra == "test"
37
+ Requires-Dist: coverage<=7.5.3,>=7.2.7; extra == "test"
36
38
 
37
39
  # Bencher
38
40
 
@@ -22,6 +22,15 @@ from .variables.results import (
22
22
  curve,
23
23
  )
24
24
 
25
+ from .results.composable_container.composable_container_base import (
26
+ ComposeType,
27
+ ComposableContainerBase,
28
+ )
29
+ from .results.composable_container.composable_container_video import (
30
+ ComposableContainerVideo,
31
+ RenderCfg,
32
+ )
33
+
25
34
  from .plotting.plot_filter import VarRange, PlotFilter
26
35
  from .utils import (
27
36
  hmap_canonical_input,
@@ -121,7 +121,7 @@ class BenchRunner:
121
121
  self.show_publish(report_level, show, publish, save, debug)
122
122
  return self.results
123
123
 
124
- def show_publish(self, report, show, publish, save, debug):
124
+ def show_publish(self, report: BenchReport, show: bool, publish: bool, save: bool, debug: bool):
125
125
  if save:
126
126
  report.save_index()
127
127
  if publish and self.publisher is not None:
@@ -220,9 +220,11 @@ class Bench(BenchPlotServer):
220
220
  relationship_cb=None,
221
221
  plot_callbacks: List | bool = None,
222
222
  ) -> List[BenchResult]:
223
- results = []
224
223
  if relationship_cb is None:
225
224
  relationship_cb = combinations
225
+ if input_vars is None:
226
+ input_vars = self.worker_class_instance.get_inputs_only()
227
+ results = []
226
228
  for it in range(iterations):
227
229
  for input_group in relationship_cb(input_vars, group_size):
228
230
  title_gen = title + "Sweeping " + " vs ".join(params_to_str(input_group))
@@ -1,4 +1,5 @@
1
1
  from typing import List
2
+ import logging
2
3
 
3
4
  import optuna
4
5
  import panel as pn
@@ -158,8 +159,8 @@ def summarise_optuna_study(study: optuna.study.Study) -> pn.pane.panel:
158
159
  row.append(plot_param_importances(study))
159
160
  try:
160
161
  row.append(plot_pareto_front(study))
161
- except Exception:
162
- pass
162
+ except Exception as e: # pylint: disable=broad-except
163
+ logging.exception(e)
163
164
 
164
165
  row.append(
165
166
  pn.pane.Markdown(f"```\nBest value: {study.best_value}\nParams: {study.best_params}```")
@@ -8,8 +8,9 @@ from colorsys import hsv_to_rgb
8
8
  from pathlib import Path
9
9
  from uuid import uuid4
10
10
  from functools import partial
11
- from typing import Callable, Any, List
11
+ from typing import Callable, Any, List, Tuple
12
12
  import param
13
+ import numpy as np
13
14
 
14
15
 
15
16
  def hmap_canonical_input(dic: dict) -> tuple:
@@ -98,6 +99,10 @@ def un_camel(camel: str) -> str:
98
99
  return capitalise_words(re.sub("([a-z])([A-Z])", r"\g<1> \g<2>", camel.replace("_", " ")))
99
100
 
100
101
 
102
+ def mult_tuple(inp: Tuple[float], val: float) -> Tuple[float]:
103
+ return tuple(np.array(inp) * val)
104
+
105
+
101
106
  def tabs_in_markdown(regular_str: str, spaces: int = 2) -> str:
102
107
  """Given a string with tabs in the form \t convert the to &ensp; which is a double space in markdown
103
108
 
@@ -142,13 +147,21 @@ def color_tuple_to_css(color: tuple[float, float, float]) -> str:
142
147
  return f"rgb{(color[0] * 255, color[1] * 255, color[2] * 255)}"
143
148
 
144
149
 
150
+ def color_tuple_to_255(color: tuple[float, float, float]) -> tuple[float, float, float]:
151
+ return (
152
+ min(int(color[0] * 255), 255),
153
+ min(int(color[1] * 255), 255),
154
+ min(int(color[2] * 255), 255),
155
+ )
156
+
157
+
145
158
  def gen_path(filename, folder="generic", suffix=".dat"):
146
159
  path = Path(f"cachedir/{folder}/{filename}/")
147
160
  path.mkdir(parents=True, exist_ok=True)
148
161
  return f"{path.absolute().as_posix()}/{filename}_{uuid4()}{suffix}"
149
162
 
150
163
 
151
- def gen_video_path(video_name: str = "vid", extension: str = ".webm") -> str:
164
+ def gen_video_path(video_name: str = "vid", extension: str = ".mp4") -> str:
152
165
  return gen_path(video_name, "vid", extension)
153
166
 
154
167
 
@@ -0,0 +1,67 @@
1
+ import numpy as np
2
+ import moviepy.video.io.ImageSequenceClip
3
+ from pathlib import Path
4
+ from .utils import gen_video_path, gen_image_path
5
+
6
+ import moviepy
7
+ from PIL import Image, ImageDraw
8
+
9
+
10
+ class VideoWriter:
11
+ def __init__(self, filename: str = "vid") -> None:
12
+ self.images = []
13
+ self.image_files = []
14
+ self.video_files = []
15
+ self.filename = gen_video_path(filename)
16
+
17
+ def append(self, img):
18
+ self.images.append(img)
19
+
20
+ def write(self) -> str:
21
+ if len(self.images) > 0:
22
+ clip = moviepy.video.io.ImageSequenceClip.ImageSequenceClip(
23
+ self.images, fps=30, with_mask=False, load_images=True
24
+ )
25
+ self.write_video_raw(clip)
26
+ return self.filename
27
+
28
+ @staticmethod
29
+ def create_label(label, width=None, height=16, color=(255, 255, 255)):
30
+ if width is None:
31
+ width = len(label) * 10
32
+ new_img = Image.new("RGB", (width, height), color=color)
33
+ # ImageDraw.Draw(new_img).text((width/2, 0), label, (0, 0, 0),align="center",achor="ms")
34
+ ImageDraw.Draw(new_img).text(
35
+ (width / 2.0, 0), label, (0, 0, 0), anchor="mt", font_size=height
36
+ )
37
+
38
+ return new_img
39
+
40
+ @staticmethod
41
+ def label_image(path: Path, label, padding=20, color=(255, 255, 255)) -> Path:
42
+ image = Image.open(path)
43
+ new_img = VideoWriter.create_label(
44
+ label, image.size[0], image.size[1] + padding, color=color
45
+ )
46
+ new_img.paste(image, (0, padding))
47
+ return new_img
48
+
49
+ def write_video_raw(self, video_clip: moviepy.video.VideoClip, fps: int = 30) -> str:
50
+ video_clip.write_videofile(
51
+ self.filename,
52
+ codec="libx264",
53
+ audio=False,
54
+ bitrate="0",
55
+ fps=fps,
56
+ ffmpeg_params=["-crf", "23"],
57
+ threads=8,
58
+ )
59
+ video_clip.close()
60
+ return self.filename
61
+
62
+
63
+ def add_image(np_array: np.ndarray, name: str = "img") -> str:
64
+ """Creates a file on disk from a numpy array and returns the created image path"""
65
+ filename = gen_image_path(name)
66
+ Image.fromarray(np_array).save(filename)
67
+ return filename
@@ -1,8 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: holobench
3
- Version: 1.22.2
3
+ Version: 1.23.0
4
4
  Summary: A package for benchmarking the performance of arbitrary functions
5
5
  Author-email: Austin Gregg-Smith <blooop@gmail.com>
6
+ Maintainer: austin.gregg-smith
7
+ Maintainer-email: austin.gregg-smith@dyson.com
6
8
  Project-URL: Repository, https://github.com/dyson-ai/bencher
7
9
  Project-URL: Home, https://github.com/dyson-ai/bencher
8
10
  Project-URL: Documentation, https://bencher.readthedocs.io/en/latest/
@@ -13,7 +15,7 @@ Requires-Dist: numpy<=1.26.4,>=1.0
13
15
  Requires-Dist: param<=2.1.0,>=1.13.0
14
16
  Requires-Dist: hvplot<=0.10.0,>=0.8
15
17
  Requires-Dist: matplotlib<=3.9.0,>=3.6.3
16
- Requires-Dist: panel<=1.4.3,>=1.3.6
18
+ Requires-Dist: panel<=1.4.4,>=1.3.6
17
19
  Requires-Dist: diskcache<=5.6.3,>=5.6
18
20
  Requires-Dist: optuna<=3.6.1,>=3.2
19
21
  Requires-Dist: xarray<=2024.5.0,>=2023.7
@@ -30,9 +32,9 @@ Requires-Dist: black<=24.4.2,>=23; extra == "test"
30
32
  Requires-Dist: pylint<=3.2.2,>=2.17.7; extra == "test"
31
33
  Requires-Dist: pytest-cov<=5.0.0,>=4.1; extra == "test"
32
34
  Requires-Dist: pytest<=8.2.1,>=7.4; extra == "test"
33
- Requires-Dist: hypothesis<=6.102.6,>=6.82; extra == "test"
34
- Requires-Dist: ruff<=0.4.5,>=0.3; extra == "test"
35
- Requires-Dist: coverage<=7.5.2,>=7.2.7; extra == "test"
35
+ Requires-Dist: hypothesis<=6.103.0,>=6.82; extra == "test"
36
+ Requires-Dist: ruff<=0.4.7,>=0.3; extra == "test"
37
+ Requires-Dist: coverage<=7.5.3,>=7.2.7; extra == "test"
36
38
 
37
39
  # Bencher
38
40
 
@@ -32,6 +32,8 @@ test/test_bencher.py
32
32
  test/test_cache.py
33
33
  test/test_class_enum.py
34
34
  test/test_combinations.py
35
+ test/test_composable_container_base.py
36
+ test/test_composable_container_video.py
35
37
  test/test_float_formatter.py
36
38
  test/test_job.py
37
39
  test/test_meta_tests.py
@@ -3,7 +3,7 @@ numpy<=1.26.4,>=1.0
3
3
  param<=2.1.0,>=1.13.0
4
4
  hvplot<=0.10.0,>=0.8
5
5
  matplotlib<=3.9.0,>=3.6.3
6
- panel<=1.4.3,>=1.3.6
6
+ panel<=1.4.4,>=1.3.6
7
7
  diskcache<=5.6.3,>=5.6
8
8
  optuna<=3.6.1,>=3.2
9
9
  xarray<=2024.5.0,>=2023.7
@@ -21,6 +21,6 @@ black<=24.4.2,>=23
21
21
  pylint<=3.2.2,>=2.17.7
22
22
  pytest-cov<=5.0.0,>=4.1
23
23
  pytest<=8.2.1,>=7.4
24
- hypothesis<=6.102.6,>=6.82
25
- ruff<=0.4.5,>=0.3
26
- coverage<=7.5.2,>=7.2.7
24
+ hypothesis<=6.103.0,>=6.82
25
+ ruff<=0.4.7,>=0.3
26
+ coverage<=7.5.3,>=7.2.7
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "holobench"
3
- version = "1.22.2"
3
+ version = "1.23.0"
4
4
 
5
5
  authors = [{ name = "Austin Gregg-Smith", email = "blooop@gmail.com" }]
6
6
  description = "A package for benchmarking the performance of arbitrary functions"
@@ -12,7 +12,7 @@ dependencies = [
12
12
  "param>=1.13.0,<=2.1.0",
13
13
  "hvplot>=0.8,<=0.10.0",
14
14
  "matplotlib>=3.6.3,<=3.9.0",
15
- "panel>=1.3.6,<=1.4.3",
15
+ "panel>=1.3.6,<=1.4.4",
16
16
  "diskcache>=5.6,<=5.6.3",
17
17
  "optuna>=3.2,<=3.6.1",
18
18
  "xarray>=2023.7,<=2024.5.0",
@@ -48,9 +48,9 @@ test = [
48
48
  "pylint>=2.17.7,<=3.2.2",
49
49
  "pytest-cov>=4.1,<=5.0.0",
50
50
  "pytest>=7.4,<=8.2.1",
51
- "hypothesis>=6.82,<=6.102.6",
52
- "ruff>=0.3,<=0.4.5",
53
- "coverage>=7.2.7,<=7.5.2",
51
+ "hypothesis>=6.82,<=6.103.0",
52
+ "ruff>=0.3,<=0.4.7",
53
+ "coverage>=7.2.7,<=7.5.3",
54
54
  ]
55
55
 
56
56
  [project.urls]
@@ -72,20 +72,26 @@ py311 = ["py311", "test"]
72
72
  format = "black ."
73
73
  check-clean-workspace = "git diff --exit-code"
74
74
  ruff-lint = "ruff check . --fix"
75
- pylint = "pylint --version && echo 'running pylint...'&& pylint $(git ls-files '*.py')"
75
+ pylint = "pylint --version && echo 'running pylint...' && pylint $(git ls-files '*.py')"
76
76
  lint = { depends_on = ["ruff-lint", "pylint"] }
77
77
  style = { depends_on = ["format", "lint"] }
78
+ commit-format = "git commit -a -m'autoformat code'"
78
79
  test = "pytest"
79
80
  coverage = "coverage run -m pytest && coverage xml -o coverage.xml"
80
81
  coverage-report = "coverage report -m"
82
+ update-lock = "pixi update && git commit -a -m'update pixi.lock'"
83
+ push = "git push"
84
+ update-lock-push = { depends_on = ["update-lock", "push"] }
81
85
  ci-no-cover = { depends_on = ["style", "test"] }
82
86
  ci = { depends_on = [
83
- "style",
87
+ "format",
88
+ "ruff-lint",
84
89
  "check-clean-workspace",
90
+ "pylint",
85
91
  "coverage",
86
92
  "coverage-report",
87
93
  ] }
88
- update-lock-push = "pixi update; git commit -a -m'update pixi.lock';git push"
94
+ ci-push = { depends_on = ["format", "ruff-lint", "update-lock", "ci", "push"] }
89
95
  clear-pixi = "rm -rf .pixi pixi.lock"
90
96
 
91
97
 
@@ -97,7 +103,7 @@ extension-pkg-whitelist = ["numpy", "scipy"]
97
103
  jobs = 16 #detect number of cores
98
104
 
99
105
  [tool.pylint.'MESSAGES CONTROL']
100
- disable = "C,logging-fstring-interpolation,line-too-long,fixme,broad-exception-caught,missing-module-docstring,too-many-instance-attributes,too-few-public-methods,too-many-arguments,too-many-locals,too-many-branches,too-many-statements,use-dict-literal,cyclic-import,duplicate-code,too-many-public-methods,too-many-nested-blocks"
106
+ disable = "C,logging-fstring-interpolation,line-too-long,fixme,missing-module-docstring,too-many-instance-attributes,too-few-public-methods,too-many-arguments,too-many-locals,too-many-branches,too-many-statements,use-dict-literal,duplicate-code,too-many-public-methods,too-many-nested-blocks"
101
107
  enable = "no-else-return,consider-using-in"
102
108
 
103
109
  [tool.black]
@@ -4,17 +4,14 @@ package_name = "bencher"
4
4
 
5
5
  setup(
6
6
  name=package_name,
7
- # version="0.0.1",
8
7
  description="A library for benchmarking code and generating reports for analysis",
9
- # maintainer="austin.gregg-smith",
10
- # maintainer_email="austin.gregg-smith@dyson.com",
8
+ maintainer="austin.gregg-smith",
9
+ maintainer_email="austin.gregg-smith@dyson.com",
11
10
  packages=find_packages(exclude=["test.*", "test"]),
12
11
  data_files=[
13
12
  ("share/ament_index/resource_index/packages", ["resource/" + package_name]),
14
13
  ("share/" + package_name, ["package.xml"]),
15
14
  ],
16
- include_package_data=False,
17
- # install_requires=["setuptools"],
18
15
  zip_safe=False,
19
16
  test_suite="test",
20
17
  tests_require=["pytest"],
@@ -0,0 +1,77 @@
1
+ # Generated by CodiumAI
2
+ from bencher.results.composable_container.composable_container_base import (
3
+ ComposeType,
4
+ ComposableContainerBase,
5
+ )
6
+
7
+
8
+ import pytest
9
+
10
+
11
+ class TestFlip:
12
+
13
+ # flipping ComposeType.right returns ComposeType.down
14
+ def test_flip_right_returns_down(self):
15
+ assert ComposeType.right.flip() == ComposeType.down
16
+
17
+ # flipping ComposeType.down returns ComposeType.right
18
+ def test_flip_down_returns_right(self):
19
+ assert ComposeType.down.flip() == ComposeType.right
20
+
21
+ # flipping an invalid ComposeType raises RuntimeError
22
+ def test_flip_invalid_raises_runtime_error(self):
23
+ with pytest.raises(RuntimeError):
24
+ invalid_type = ComposeType.sequence
25
+ invalid_type.flip()
26
+
27
+ # method should handle large number of flip calls without performance degradation
28
+ def test_flip_large_number_of_calls(self):
29
+ for _ in range(1000000):
30
+ assert ComposeType.right.flip() == ComposeType.down
31
+ assert ComposeType.down.flip() == ComposeType.right
32
+
33
+ # method should maintain immutability of the original ComposeType instance
34
+ def test_flip_maintains_immutability(self):
35
+ original = ComposeType.right
36
+ flipped = original.flip()
37
+ assert original == ComposeType.right
38
+ assert flipped == ComposeType.down
39
+
40
+
41
+ # Generated by CodiumAI
42
+ class TestComposableContainerBase:
43
+
44
+ # append method adds an object to the container
45
+ def test_append_adds_object(self):
46
+ container = ComposableContainerBase()
47
+ obj = "test_object"
48
+ container.append(obj)
49
+ assert obj in container.container
50
+
51
+ # render method returns the current state of the container
52
+ def test_render_returns_container_state(self):
53
+ container = ComposableContainerBase()
54
+ obj = "test_object"
55
+ container.append(obj)
56
+ assert container.render() == [obj]
57
+
58
+ # Ensure label_formatter formats int values correctly with spaces included
59
+ def test_label_formatter_formats_int_with_spaces(self):
60
+ formatted_label = ComposableContainerBase.label_formatter("var", 12345)
61
+ assert formatted_label == "var= 12345."
62
+
63
+ # label_formatter handles None values for var_name and var_value
64
+ def test_label_formatter_handles_none(self):
65
+ formatted_label = ComposableContainerBase.label_formatter(None, None)
66
+ assert formatted_label is None
67
+
68
+ # append method handles appending None
69
+ def test_append_handles_none(self):
70
+ container = ComposableContainerBase()
71
+ container.append(None)
72
+ assert None in container.container
73
+
74
+ # render method handles an empty container
75
+ def test_render_empty_container(self):
76
+ container = ComposableContainerBase()
77
+ assert container.render() == []
@@ -0,0 +1,255 @@
1
+ import unittest
2
+ import bencher as bch
3
+ import numpy as np
4
+ from hypothesis import given, strategies as st
5
+
6
+
7
+ class TestComposableContainerVideo(unittest.TestCase):
8
+ def small_img(self, size=None):
9
+ if size is None:
10
+ size = (2, 1)
11
+ return np.ones((size[0], size[1], 3))
12
+ # return np.array(
13
+ # [[[1, 1, 1]], [[0, 0, 0]]]
14
+ # ) # represents an image of 1x2 (keep it small for speed)
15
+
16
+ def small_video(self, num_frames: int = 2, render_cfg=None, size=None):
17
+ img = self.small_img(size=size)
18
+ vid = bch.ComposableContainerVideo()
19
+ for _ in range(num_frames):
20
+ vid.append(img)
21
+ return vid.render(render_cfg)
22
+
23
+ # @given(frames=st.sampled_from([1, 2, 10, 100]))
24
+ # def test_duration_default(self, frames):
25
+ # ccv = bch.ComposableContainerVideo()
26
+ # duration, frame_duration = ccv.calculate_duration(frames, bch.RenderCfg())
27
+ # self.assertEqual(duration, 10)
28
+ # self.assertEqual(frame_duration, 10 / frames)
29
+
30
+ @given(frames=st.sampled_from([1, 2, 10, 100]), duration=st.sampled_from([0.1, 1, 10, 100]))
31
+ def test_set_duration(self, frames, duration):
32
+ ccv = bch.ComposableContainerVideo()
33
+ duration, frame_duration = ccv.calculate_duration(frames, bch.RenderCfg(duration=duration))
34
+ self.assertEqual(duration, duration)
35
+ self.assertEqual(frame_duration, duration / frames)
36
+
37
+ @given(frames=st.sampled_from([1, 2, 10, 100]), duration=st.sampled_from([0.1, 1, 10, 100]))
38
+ def test_set_duration1(self, frames, duration):
39
+ ccv = bch.ComposableContainerVideo()
40
+ min_frame_duration = 1 / 30
41
+ max_frame_duration = 2.0
42
+ duration, frame_duration = ccv.calculate_duration(
43
+ frames,
44
+ bch.RenderCfg(
45
+ duration=duration,
46
+ min_frame_duration=1 / 30,
47
+ max_frame_duration=2,
48
+ duration_target=True,
49
+ ),
50
+ )
51
+ self.assertEqual(duration, duration)
52
+ self.assertEqual(frame_duration, duration / frames)
53
+ self.assertLessEqual(frame_duration, max_frame_duration)
54
+ self.assertGreaterEqual(frame_duration, min_frame_duration)
55
+
56
+ def test_img_right(self):
57
+ res = self.small_video(1, bch.RenderCfg(duration=0.1, compose_method=bch.ComposeType.right))
58
+ self.assertEqual(res.size, (1, 2))
59
+ self.assertEqual(res.duration, 0.1)
60
+
61
+ def test_img_down(self):
62
+ res = self.small_video(1, bch.RenderCfg(duration=0.1, compose_method=bch.ComposeType.down))
63
+ self.assertEqual(res.size, (1, 2))
64
+ self.assertEqual(res.duration, 0.1)
65
+
66
+ def test_img_sequence(self):
67
+ res = self.small_video(
68
+ 1, bch.RenderCfg(duration=0.1, compose_method=bch.ComposeType.sequence)
69
+ )
70
+ self.assertEqual(res.size, (1, 2))
71
+ self.assertEqual(res.duration, 0.1)
72
+
73
+ def test_img_right_x2(self):
74
+ res = self.small_video(2, bch.RenderCfg(compose_method=bch.ComposeType.right))
75
+ self.assertEqual(res.size, (2, 2))
76
+ self.assertEqual(res.duration, 2)
77
+
78
+ def test_img_down_x2(self):
79
+ res = self.small_video(2, bch.RenderCfg(compose_method=bch.ComposeType.down))
80
+ self.assertEqual(res.size, (1, 4))
81
+ self.assertEqual(res.duration, 2)
82
+
83
+ def test_img_seq_x2(self):
84
+ res = self.small_video(2, bch.RenderCfg(compose_method=bch.ComposeType.sequence))
85
+ self.assertEqual(res.size, (1, 2))
86
+ self.assertEqual(res.duration, 4)
87
+
88
+ def test_video_seq(self):
89
+ img = self.small_img()
90
+ vid1 = bch.ComposableContainerVideo()
91
+ vid1.append(img)
92
+ vid1.append(img)
93
+
94
+ res = vid1.deep().render(
95
+ bch.RenderCfg(duration=0.1, compose_method=bch.ComposeType.sequence)
96
+ )
97
+
98
+ self.assertEqual(res.size, (1, 2))
99
+ self.assertEqual(res.duration, 0.1) # two frames
100
+
101
+ render_cfg = bch.RenderCfg(
102
+ duration=10.0,
103
+ duration_target=True,
104
+ min_frame_duration=1.0 / 30,
105
+ max_frame_duration=1.0,
106
+ )
107
+
108
+ res = vid1.deep().render(render_cfg)
109
+
110
+ self.assertEqual(res.size, (1, 2))
111
+ self.assertEqual(res.duration, 2.0) # 2 frames of minimum frame time 1.0
112
+
113
+ vid1.append(img)
114
+
115
+ res = vid1.render(render_cfg)
116
+ self.assertEqual(res.size, (1, 2))
117
+ self.assertEqual(res.duration, 3.0) # 3 frames of minimum frame time 1.0
118
+
119
+ def test_concat_equal_video_len(self):
120
+ render_cfg = bch.RenderCfg(duration_target=True)
121
+ res1 = self.small_video(num_frames=2, render_cfg=render_cfg)
122
+ res2 = self.small_video(num_frames=2, render_cfg=render_cfg)
123
+
124
+ self.assertEqual(res1.size, (1, 2))
125
+ self.assertEqual(res1.duration, 4.0) # 2 frames of minimum frame time 2.0
126
+
127
+ self.assertEqual(res2.size, (1, 2))
128
+ self.assertEqual(res2.duration, 4.0) # 1 frames of minimum frame time 2.0
129
+
130
+ vid = bch.ComposableContainerVideo()
131
+
132
+ vid.append(res1)
133
+ vid.append(res2)
134
+
135
+ res = vid.deep().render(bch.RenderCfg(bch.ComposeType.right))
136
+ self.assertEqual(res.size, (2, 2))
137
+ self.assertEqual(res.duration, 4.0) # the duration of the longer clip
138
+
139
+ res = vid.deep().render(bch.RenderCfg(bch.ComposeType.down))
140
+ self.assertEqual(res.size, (1, 4))
141
+ self.assertEqual(res.duration, 4.0) # the duration of the longer clip
142
+
143
+ res = vid.deep().render(bch.RenderCfg(bch.ComposeType.sequence))
144
+ self.assertEqual(res.size, (1, 2))
145
+ self.assertEqual(res.duration, 8.0) # the duration of both clips
146
+
147
+ def test_concat_unequal_video_len(self):
148
+ render_cfg = bch.RenderCfg(duration_target=True)
149
+ res1 = self.small_video(num_frames=2, render_cfg=render_cfg)
150
+ self.assertEqual(res1.size, (1, 2))
151
+ self.assertEqual(res1.duration, 4.0) # 3 frames of minimum frame time 1.0
152
+
153
+ res2 = self.small_video(num_frames=3, render_cfg=render_cfg)
154
+ self.assertEqual(res2.size, (1, 2))
155
+ self.assertEqual(res2.duration, 6.0) # 3 frames of minimum frame time 1.0
156
+
157
+ vid = bch.ComposableContainerVideo()
158
+
159
+ vid.append(res1)
160
+ vid.append(res2)
161
+
162
+ res = vid.deep().render(bch.RenderCfg(bch.ComposeType.right))
163
+ self.assertEqual(res.size, (2, 2))
164
+ self.assertEqual(res.duration, 6.0) # the duration of the longer clip
165
+
166
+ res = vid.deep().render(bch.RenderCfg(bch.ComposeType.down))
167
+ self.assertEqual(res.size, (1, 4))
168
+ self.assertEqual(res.duration, 6.0) # the duration of the longer clip
169
+
170
+ res = vid.deep().render(bch.RenderCfg(bch.ComposeType.sequence))
171
+ self.assertEqual(res.size, (1, 2))
172
+ self.assertEqual(res.duration, 10.0) # the duration of both clips
173
+
174
+ res_label = vid.deep().render(bch.RenderCfg(bch.ComposeType.sequence))
175
+ self.assertEqual(res.duration, res_label.duration) # the duration of both clips
176
+
177
+ def test_bad_filetype(self):
178
+ vid = bch.ComposableContainerVideo()
179
+ with self.assertRaises(RuntimeWarning):
180
+ vid.append("bad.badextension")
181
+
182
+ def test_simple_image_length(self):
183
+ ccv = bch.ComposableContainerVideo()
184
+ ccv.append(self.small_img())
185
+ ccv.append(self.small_img())
186
+
187
+ res = ccv.render()
188
+ self.assertEqual(res.duration, 4.0) # limited by maximum frame time
189
+
190
+ ccv = bch.ComposableContainerVideo()
191
+ for _ in range(20):
192
+ ccv.append(self.small_img())
193
+
194
+ self.assertEqual(ccv.render().duration, 10.0) # limited by target video duration
195
+
196
+ ccv = bch.ComposableContainerVideo()
197
+ for _ in range(200):
198
+ ccv.append(self.small_img())
199
+
200
+ # still limited by target video duration
201
+ self.assertAlmostEqual(ccv.render().duration, 10.0)
202
+
203
+ def test_composite_image_length(self):
204
+ ccv = bch.ComposableContainerVideo()
205
+
206
+ for _ in range(2):
207
+ sub_img = bch.ComposableContainerVideo()
208
+ sub_img.append(self.small_img())
209
+ sub_img.append(self.small_img())
210
+ sub_img.append(self.small_img())
211
+ ccv.append(sub_img.render(compose_method=bch.ComposeType.right))
212
+
213
+ # should be 4 because there are two sequential frames
214
+ self.assertEqual(ccv.render().duration, 4.0)
215
+
216
+ ccv = bch.ComposableContainerVideo()
217
+
218
+ for _ in range(5):
219
+ sub_img = bch.ComposableContainerVideo()
220
+ sub_img.append(self.small_img())
221
+ sub_img.append(self.small_img())
222
+ sub_img.append(self.small_img())
223
+ ccv.append(sub_img.render(compose_method=bch.ComposeType.right))
224
+
225
+ # should be 10 because of the default target length
226
+ self.assertEqual(ccv.render().duration, 10.0)
227
+
228
+ def test_video_lengths(self):
229
+ ccv = bch.ComposableContainerVideo()
230
+ ccv.append(self.small_video())
231
+ ccv.append(self.small_video())
232
+
233
+ res = ccv.render()
234
+ self.assertEqual(res.duration, 8.0) # no frame limits, just concat videos
235
+
236
+ ccv = bch.ComposableContainerVideo()
237
+ for _ in range(20):
238
+ ccv.append(self.small_video())
239
+
240
+ self.assertEqual(ccv.render().duration, 80.0) # concatted vid time
241
+
242
+ ccv = bch.ComposableContainerVideo()
243
+ for _ in range(200):
244
+ ccv.append(self.small_video())
245
+
246
+ # still concatted vid time
247
+ self.assertAlmostEqual(ccv.render().duration, 800.0)
248
+
249
+
250
+ if __name__ == "__main__":
251
+ tst = TestComposableContainerVideo()
252
+
253
+ tst.test_img_right()
254
+
255
+ unittest.main()
@@ -9,6 +9,7 @@ from bencher.utils import (
9
9
  lerp,
10
10
  listify,
11
11
  tabs_in_markdown,
12
+ mult_tuple,
12
13
  )
13
14
  from functools import partial
14
15
  import xarray as xr
@@ -78,6 +79,9 @@ class TestBencherUtils(unittest.TestCase):
78
79
  bch.hmap_canonical_input(dic2),
79
80
  )
80
81
 
82
+ def test_mult_tuple(self) -> None:
83
+ self.assertTupleEqual(mult_tuple((1, 2, 3), 2), (2, 4, 6))
84
+
81
85
  # Tests that the function returns the nearest coordinate name value pair for a dataset containing multiple coordinates
82
86
  def test_multiple_coordinates(self):
83
87
  ds = xr.Dataset({"x": [1, 2, 3], "y": [4, 5, 6]})
@@ -1,121 +0,0 @@
1
- import numpy as np
2
- import moviepy.video.io.ImageSequenceClip
3
- from pathlib import Path
4
- from .utils import gen_video_path, gen_image_path
5
-
6
- import moviepy
7
- from moviepy.editor import (
8
- VideoFileClip,
9
- ImageClip,
10
- ImageSequenceClip,
11
- clips_array,
12
- concatenate_videoclips,
13
- )
14
- from PIL import Image, ImageDraw
15
-
16
-
17
- class VideoWriter:
18
- def __init__(self, filename: str = "vid") -> None:
19
- self.images = []
20
- self.image_files = []
21
- self.video_files = []
22
- self.filename = gen_video_path(filename)
23
-
24
- def append(self, img):
25
- self.images.append(img)
26
-
27
- def write(self) -> str:
28
- clip = moviepy.video.io.ImageSequenceClip.ImageSequenceClip(
29
- self.images, fps=30, with_mask=False, load_images=True
30
- )
31
- self.write_video_raw(clip)
32
- return self.filename
33
-
34
- @staticmethod
35
- def create_label(label, width=None, height=14):
36
- if width is None:
37
- width = len(label) * 8
38
- new_img = Image.new("RGB", (width, height), (255, 255, 255))
39
- # ImageDraw.Draw(new_img).text((width/2, 0), label, (0, 0, 0),align="center",achor="ms")
40
- ImageDraw.Draw(new_img).text((width / 2.0, 0), label, (0, 0, 0), anchor="mt", font_size=12)
41
-
42
- return new_img
43
-
44
- @staticmethod
45
- def label_image(path: Path, label, padding=20) -> Path:
46
- image = Image.open(path)
47
- new_img = VideoWriter.create_label(label, image.size[0], image.size[1] + padding)
48
- new_img.paste(image, (0, padding))
49
- return new_img
50
-
51
- def append_file(self, filepath, label=None):
52
- if label is not None:
53
- path = Path(filepath)
54
- new_path = path.with_name(path.stem + "_labelled" + path.suffix).as_posix()
55
- padding = 20
56
- match path.suffix:
57
- case ".png" | ".jpg":
58
- image = Image.open(filepath)
59
- new_img = self.create_label(label, image.size[0], image.size[1] + padding)
60
- new_img.paste(image, (0, padding))
61
- new_img.save(new_path)
62
- self.image_files.append(new_path)
63
- case ".webm":
64
- import warnings
65
-
66
- video_clip = VideoFileClip(filepath)
67
- new_img = self.create_label(label, video_clip.w, padding)
68
-
69
- # Convert PIL image to MoviePy clip
70
- label_clip = ImageClip(np.array(new_img), duration=video_clip.duration)
71
-
72
- labeled_video_clip = clips_array([[label_clip], [video_clip]])
73
-
74
- # otherwise ffmpeg complains that the file is not getting read. We don't need the file just the size
75
- with warnings.catch_warnings():
76
- warnings.simplefilter(action="ignore")
77
- labeled_video_clip.write_videofile(new_path, remove_temp=True, logger=None)
78
- self.video_files.append(new_path)
79
- else:
80
- self.image_files.append(filepath)
81
-
82
- def to_images_sequence(self, images, target_duration: float = 10.0, frame_time=None, **kwargs):
83
- target_duration = kwargs.pop("target_duration", target_duration)
84
- if isinstance(images, list) and len(images) > 0:
85
- if frame_time is None:
86
- fps = len(images) / target_duration
87
- fps = max(fps, 1) # never slower that 1 seconds per frame
88
- fps = min(fps, 30)
89
- else:
90
- fps = 1.0 / frame_time
91
- return ImageSequenceClip(images, fps=fps, with_mask=False)
92
- return None
93
-
94
- def write_png(self, **kwargs):
95
- clip = None
96
- if len(self.image_files) > 0:
97
- clip = self.to_images_sequence(self.image_files, **kwargs)
98
- if len(self.video_files) > 0:
99
- clip = concatenate_videoclips([VideoFileClip(f) for f in self.video_files])
100
- if clip is not None:
101
- clip.write_videofile(self.filename)
102
- return self.filename
103
- return None
104
-
105
- def write_video_raw(self, video_clip: moviepy.video.VideoClip, fps: int = 30) -> str:
106
- video_clip.write_videofile(
107
- self.filename,
108
- codec="libvpx",
109
- audio=False,
110
- bitrate="0",
111
- fps=fps,
112
- ffmpeg_params=["-crf", "34"],
113
- )
114
- video_clip.close()
115
- return self.filename
116
-
117
-
118
- def add_image(np_array: np.ndarray, name: str = "img"):
119
- filename = gen_image_path(name)
120
- Image.fromarray(np_array).save(filename)
121
- return filename
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes