pyframetree 0.3.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 (30) hide show
  1. pyframetree-0.3.0/LICENSE +21 -0
  2. pyframetree-0.3.0/MANIFEST.in +3 -0
  3. pyframetree-0.3.0/PKG-INFO +72 -0
  4. pyframetree-0.3.0/README.md +55 -0
  5. pyframetree-0.3.0/examples/geometry.py +150 -0
  6. pyframetree-0.3.0/examples/geometry_viewer.py +187 -0
  7. pyframetree-0.3.0/examples/simple_transform.py +38 -0
  8. pyframetree-0.3.0/performance/__init__.py +1 -0
  9. pyframetree-0.3.0/performance/benchmark_frames.py +58 -0
  10. pyframetree-0.3.0/performance/benchmark_operations.py +22 -0
  11. pyframetree-0.3.0/performance/benchmark_transformations.py +131 -0
  12. pyframetree-0.3.0/performance/run_benchmarks.py +48 -0
  13. pyframetree-0.3.0/pyframetree/__init__.py +15 -0
  14. pyframetree-0.3.0/pyframetree/backend.py +38 -0
  15. pyframetree-0.3.0/pyframetree/coordinate_systems.py +346 -0
  16. pyframetree-0.3.0/pyframetree/rotation.py +85 -0
  17. pyframetree-0.3.0/pyframetree/transformations.py +125 -0
  18. pyframetree-0.3.0/pyframetree/utils.py +89 -0
  19. pyframetree-0.3.0/pyframetree.egg-info/PKG-INFO +72 -0
  20. pyframetree-0.3.0/pyframetree.egg-info/SOURCES.txt +28 -0
  21. pyframetree-0.3.0/pyframetree.egg-info/dependency_links.txt +1 -0
  22. pyframetree-0.3.0/pyframetree.egg-info/requires.txt +6 -0
  23. pyframetree-0.3.0/pyframetree.egg-info/top_level.txt +1 -0
  24. pyframetree-0.3.0/pyproject.toml +45 -0
  25. pyframetree-0.3.0/setup.cfg +4 -0
  26. pyframetree-0.3.0/setup.py +4 -0
  27. pyframetree-0.3.0/tests/test_coordinate_systems.py +349 -0
  28. pyframetree-0.3.0/tests/test_examples_benchmarks.py +103 -0
  29. pyframetree-0.3.0/tests/test_geometry_viewer.py +116 -0
  30. pyframetree-0.3.0/tests/test_transformations.py +238 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ recursive-include examples *.py
2
+ recursive-include performance *.py
3
+ recursive-include tests *.py
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyframetree
3
+ Version: 0.3.0
4
+ Summary: Nested 3D coordinate frames with physical units and vectorized transformations
5
+ Author-email: Gabriel Lefloch <gabriel.lefloch@kit.edu>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/gabs1234/QuaternionFrame
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: numpy>=1.24
12
+ Requires-Dist: quantities>=0.16
13
+ Provides-Extra: visualization
14
+ Requires-Dist: pyvista>=0.43; extra == "visualization"
15
+ Requires-Dist: matplotlib>=3.6; extra == "visualization"
16
+ Dynamic: license-file
17
+
18
+ # PyFrameTree
19
+
20
+ Nested 3D coordinate frames and point-cloud transformations, with physical units.
21
+
22
+ ## Installation
23
+
24
+ From the repository:
25
+
26
+ ```bash
27
+ uv sync --extra visualization
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```bash
33
+ uv run --extra visualization python examples/simple_transform.py --show
34
+ ```
35
+
36
+ ```python
37
+ from pyframetree import CoordinateSystem
38
+ import quantities as pq
39
+ import numpy as np
40
+ import pyvista as pv
41
+ ```
42
+
43
+ ```python
44
+ plotter = pv.Plotter()
45
+
46
+ # Create a random point cloud
47
+ N = 100
48
+ points = np.random.rand(N, 3)
49
+
50
+ # Create a reference coordinate system
51
+ Reference = CoordinateSystem()
52
+
53
+ # Create a main coordinate system
54
+ Main = CoordinateSystem()
55
+
56
+ # Create a child coordinate system
57
+ Child = CoordinateSystem(parent=Main, name='Child')
58
+ Child.add_points(points, 'points')
59
+ Child.translate([2, 2, 0]*pq.m)
60
+ Child.rotate([0, 1, 1], 30*pq.deg)
61
+
62
+ Main.scale(3, inherit=False) # Scale the main coordinate system, but not its children
63
+ Main.rotate([1, 0, 0], 30*pq.deg, inherit=True)
64
+
65
+ Reference.visualize(plotter=plotter, children=False, cmap='gray')
66
+ Main.visualize(plotter=plotter, children=False, cmap='plasma')
67
+ Child.visualize(plotter=plotter, children=False, cmap='viridis')
68
+
69
+ plotter.save_graphic('assets/frames.svg')
70
+ ```
71
+
72
+ ![example](assets/frames.svg)
@@ -0,0 +1,55 @@
1
+ # PyFrameTree
2
+
3
+ Nested 3D coordinate frames and point-cloud transformations, with physical units.
4
+
5
+ ## Installation
6
+
7
+ From the repository:
8
+
9
+ ```bash
10
+ uv sync --extra visualization
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ uv run --extra visualization python examples/simple_transform.py --show
17
+ ```
18
+
19
+ ```python
20
+ from pyframetree import CoordinateSystem
21
+ import quantities as pq
22
+ import numpy as np
23
+ import pyvista as pv
24
+ ```
25
+
26
+ ```python
27
+ plotter = pv.Plotter()
28
+
29
+ # Create a random point cloud
30
+ N = 100
31
+ points = np.random.rand(N, 3)
32
+
33
+ # Create a reference coordinate system
34
+ Reference = CoordinateSystem()
35
+
36
+ # Create a main coordinate system
37
+ Main = CoordinateSystem()
38
+
39
+ # Create a child coordinate system
40
+ Child = CoordinateSystem(parent=Main, name='Child')
41
+ Child.add_points(points, 'points')
42
+ Child.translate([2, 2, 0]*pq.m)
43
+ Child.rotate([0, 1, 1], 30*pq.deg)
44
+
45
+ Main.scale(3, inherit=False) # Scale the main coordinate system, but not its children
46
+ Main.rotate([1, 0, 0], 30*pq.deg, inherit=True)
47
+
48
+ Reference.visualize(plotter=plotter, children=False, cmap='gray')
49
+ Main.visualize(plotter=plotter, children=False, cmap='plasma')
50
+ Child.visualize(plotter=plotter, children=False, cmap='viridis')
51
+
52
+ plotter.save_graphic('assets/frames.svg')
53
+ ```
54
+
55
+ ![example](assets/frames.svg)
@@ -0,0 +1,150 @@
1
+ import marimo
2
+
3
+ __generated_with = '0.25.2'
4
+ app = marimo.App(width='medium')
5
+
6
+
7
+ @app.cell
8
+ def _():
9
+ import marimo as mo
10
+ import numpy as np
11
+ import quantities as pq
12
+ from geometry_viewer import GeometryViewer
13
+
14
+ from pyframetree import CoordinateSystem
15
+
16
+ mo.output.clear() # Hide fastplotlib's optional Jupyter logo/device banner.
17
+ return CoordinateSystem, GeometryViewer, mo, np, pq
18
+
19
+
20
+ @app.cell(hide_code=True)
21
+ def _(mo):
22
+ mo.md(r"""
23
+ # See how a parent frame moves its child
24
+
25
+ The **Reference** frame stays fixed. **Main** is the parent of **Child**,
26
+ which carries a point cloud. Axis labels identify each frame and its local
27
+ **u**, **v**, and **w** directions. Labels stay upright as the camera moves.
28
+ All positions are in metres.
29
+
30
+ Drag the 3D view to rotate it; scroll to zoom. Change the angles below to
31
+ update the geometry without resetting your camera. The defaults reproduce
32
+ the original Jupyter example.
33
+
34
+ Open this notebook with `uv run --group notebook marimo edit examples/geometry.py`.
35
+ """)
36
+ return
37
+
38
+
39
+ @app.cell(hide_code=True)
40
+ def _(mo):
41
+ parent_angle = mo.ui.slider(
42
+ -180,
43
+ 180,
44
+ value=30,
45
+ step=5,
46
+ show_value=True,
47
+ label='Parent rotation (deg)',
48
+ )
49
+ child_angle = mo.ui.slider(
50
+ -180,
51
+ 180,
52
+ value=30,
53
+ step=5,
54
+ show_value=True,
55
+ label='Child rotation (deg)',
56
+ )
57
+ parent_scale = mo.ui.slider(
58
+ 1,
59
+ 4,
60
+ value=3,
61
+ step=0.25,
62
+ show_value=True,
63
+ label='Parent axis scale',
64
+ )
65
+ inherit_scale = mo.ui.checkbox(value=False, label='Scale the child with its parent')
66
+ mo.vstack([parent_angle, child_angle, parent_scale, inherit_scale])
67
+ return child_angle, inherit_scale, parent_angle, parent_scale
68
+
69
+
70
+ @app.cell
71
+ def _(np):
72
+ # Keep the same cloud when a control changes, so only the geometry moves.
73
+ points = np.random.default_rng(0).random((100, 3))
74
+ return (points,)
75
+
76
+
77
+ @app.cell
78
+ def _(CoordinateSystem, child_angle, inherit_scale, parent_angle, parent_scale, points, pq):
79
+ reference = CoordinateSystem(name='Reference')
80
+ main = CoordinateSystem(name='Main')
81
+ child = CoordinateSystem(parent=main, name='Child')
82
+ child.add_points(points, 'cloud')
83
+ child.translate([2, 2, 0] * pq.m)
84
+ child.rotate([0, 1, 1], child_angle.value * pq.deg)
85
+
86
+ main.scale(parent_scale.value, inherit=inherit_scale.value)
87
+ _ = main.rotate([1, 0, 0], parent_angle.value * pq.deg)
88
+ return child, main, reference
89
+
90
+
91
+ @app.cell
92
+ def _(GeometryViewer, points):
93
+ viewer = GeometryViewer(points)
94
+ viewer.canvas
95
+ return (viewer,)
96
+
97
+
98
+ @app.cell
99
+ def _(child, main, reference, viewer):
100
+ viewer.update(reference, main, child)
101
+ return
102
+
103
+
104
+ @app.cell(hide_code=True)
105
+ def _(mo):
106
+ reset_view = mo.ui.run_button(label='Reset view')
107
+ reset_view
108
+ return (reset_view,)
109
+
110
+
111
+ @app.cell
112
+ def _(reset_view, viewer):
113
+ if reset_view.value:
114
+ viewer.reset_view()
115
+ return
116
+
117
+
118
+ @app.cell(hide_code=True)
119
+ def _(child, main, mo, pq, reference):
120
+ _rows = []
121
+ for _frame in (reference, main, child):
122
+ _position = _frame.origin.rescale(pq.m).magnitude
123
+ _rows.append(
124
+ {
125
+ 'Frame': _frame.name,
126
+ 'Parent': _frame.parent.name if _frame.parent else '—',
127
+ 'x (m)': round(float(_position[0]), 4),
128
+ 'y (m)': round(float(_position[1]), 4),
129
+ 'z (m)': round(float(_position[2]), 4),
130
+ }
131
+ )
132
+ mo.vstack(
133
+ [
134
+ mo.md('### World positions'),
135
+ mo.ui.table(_rows, selection=None, pagination=False, show_column_summaries=False),
136
+ mo.md("""
137
+ The parent rotates about the world x-axis. The child rotates about its
138
+ own origin, around its parent's axis `(0, 1, 1)`.
139
+
140
+ With **Scale the child with its parent** unchecked, `inherit=False`
141
+ changes the parent's axes while holding the child and its points fixed.
142
+ Rotation keeps the default `inherit=True`, so the child follows its parent.
143
+ """),
144
+ ]
145
+ )
146
+ return
147
+
148
+
149
+ if __name__ == '__main__':
150
+ app.run()
@@ -0,0 +1,187 @@
1
+ """Fastplotlib rendering for the coordinate-frame notebook."""
2
+
3
+ import weakref
4
+
5
+ import fastplotlib as fpl
6
+ import numpy as np
7
+ import pygfx
8
+ import quantities as pq
9
+ from rendercanvas.anywidget import RenderCanvas
10
+
11
+
12
+ def _unit_arrow_geometry():
13
+ """A unit arrow along +z, with a short head joined to a slender shaft."""
14
+ # Fastplotlib 0.7 misplaces the shaft when its height differs from the cone's.
15
+ # Assemble the two standard primitives explicitly, with tail=0 and tip=1.
16
+ shaft = pygfx.cylinder_geometry(
17
+ radius_bottom=0.012,
18
+ radius_top=0.012,
19
+ height=0.82,
20
+ radial_segments=16,
21
+ )
22
+ head = pygfx.cone_geometry(radius=0.045, height=0.18, radial_segments=16)
23
+ shaft.positions.data[:, 2] += 0.41
24
+ head.positions.data[:, 2] += 0.91
25
+ return pygfx.Geometry(
26
+ positions=np.concatenate([shaft.positions.data, head.positions.data]),
27
+ normals=np.concatenate([shaft.normals.data, head.normals.data]),
28
+ indices=np.concatenate(
29
+ [
30
+ shaft.indices.data,
31
+ head.indices.data + len(shaft.positions.data),
32
+ ]
33
+ ),
34
+ )
35
+
36
+
37
+ def _arrow_transform(origin, axis):
38
+ """Place a unit arrow along ``axis``, keeping its proportions for any axis length."""
39
+ length = np.linalg.norm(axis)
40
+ direction = axis / length
41
+ helper = np.eye(3)[np.argmin(np.abs(direction))]
42
+ side = np.cross(helper, direction)
43
+ side /= np.linalg.norm(side)
44
+ matrix = np.eye(4)
45
+ matrix[:3, :3] = np.column_stack([side, np.cross(direction, side), direction]) * length
46
+ matrix[:3, 3] = origin
47
+ return matrix
48
+
49
+
50
+ class NotebookCanvas(RenderCanvas):
51
+ """Guard rendercanvas 2.7's recursive close notification, without patching it globally."""
52
+
53
+ def close(self):
54
+ if getattr(self, '_closing', False) or getattr(self, '_is_closed', False):
55
+ return
56
+ self._closing = True
57
+ try:
58
+ self._rfb_cancel_lossless_draw()
59
+ super().close()
60
+ finally:
61
+ self._closing = False
62
+
63
+
64
+ class GeometryViewer:
65
+ """Keep the notebook's three frames and child point cloud in one live scene.
66
+
67
+ Render positions are metres. Only the display buffers use float32;
68
+ CoordinateSystem continues to calculate its transforms at its own precision.
69
+ """
70
+
71
+ def __init__(self, points, *, size=(900, 600), canvas=None):
72
+ self.canvas = NotebookCanvas(size=size, max_fps=60) if canvas is None else canvas
73
+ camera = pygfx.OrthographicCamera(8, 6)
74
+ camera.local.position = (6, 6, 6)
75
+ camera.look_at((0, 0, 0))
76
+
77
+ # Scope the frame styling to this figure, then restore the caller's defaults.
78
+ config = fpl.layouts.Subplot.config.init
79
+ previous = config.frame_kwargs
80
+ config.frame_kwargs = {
81
+ 'spacing': dict(x0=0, sides=0, title_flanks=0, resize_handle_space=0, bottom=0),
82
+ 'title_kwargs': dict(font_size=0),
83
+ }
84
+ try:
85
+ self.figure = fpl.Figure(
86
+ cameras=camera,
87
+ controller_types='orbit',
88
+ canvas=self.canvas,
89
+ size=size,
90
+ )
91
+ finally:
92
+ config.frame_kwargs = previous
93
+
94
+ self.plot = self.figure[0, 0]
95
+ self.plot.background_color = ('white',)
96
+ self.arrows, self.labels = [], []
97
+ arrow_geometry = _unit_arrow_geometry()
98
+ for name, color in [
99
+ ('Reference', '#666666'),
100
+ ('Main', '#4e79a7'),
101
+ ('Child', '#b86b16'),
102
+ ]:
103
+ arrows = self.plot.add_vectors(
104
+ np.zeros((3, 3), dtype=np.float32),
105
+ np.eye(3, dtype=np.float32),
106
+ color='white',
107
+ size=1,
108
+ )
109
+ arrows.world_object.geometry = arrow_geometry
110
+ arrows.world_object.material.color = color
111
+ self.arrows.append(arrows)
112
+ labels = [
113
+ self.plot.add_text(
114
+ f'{name}.{axis}',
115
+ font_size=14,
116
+ face_color='#222222',
117
+ screen_space=True,
118
+ outline_color='white',
119
+ outline_thickness=0.15,
120
+ )
121
+ for axis in ('u', 'v', 'w')
122
+ ]
123
+ for label in labels:
124
+ label.world_object.material.depth_test = False
125
+ self.labels.append(labels)
126
+
127
+ self.cloud = self.plot.add_scatter(
128
+ np.asarray(points, dtype=np.float32),
129
+ colors='#b86b16',
130
+ sizes=7,
131
+ edge_width=0,
132
+ )
133
+ self.link = self.plot.add_line(
134
+ np.zeros((2, 3), dtype=np.float32),
135
+ colors='#666666',
136
+ thickness=1,
137
+ )
138
+ self.figure.show(autoscale=False, axes_visible=False)
139
+ self._bounds = None
140
+ self._cleanup = weakref.finalize(self, self.canvas.close)
141
+
142
+ def update(self, reference, main, child):
143
+ """Update poses and local geometry while preserving the user's camera."""
144
+ bounds = []
145
+ for frame, arrows, labels in zip((reference, main, child), self.arrows, self.labels):
146
+ matrix = frame.world_transform.as_unit(pq.m).matrix
147
+ origin, basis = matrix[:3, 3], matrix[:3, :3].T
148
+ bounds.extend([origin, *(origin + 1.12 * basis)])
149
+ for i, (label, axis) in enumerate(zip(labels, basis)):
150
+ arrows.world_object.set_matrix_at(i, _arrow_transform(origin, axis))
151
+ label.offset = origin + 1.12 * axis
152
+
153
+ local = (child.get_points('cloud') * child.unit).rescale(pq.m).magnitude
154
+ local = np.asarray(local, dtype=np.float32).reshape(-1, 3)
155
+ if not np.array_equal(local, self.cloud.data[:]):
156
+ self.cloud.data = local
157
+ matrix = child.world_transform.as_unit(pq.m).matrix
158
+ self.cloud.world_object.local.matrix = matrix
159
+ self.link.data[:] = np.array(
160
+ [main.origin.rescale(pq.m).magnitude, child.origin.rescale(pq.m).magnitude],
161
+ dtype=np.float32,
162
+ )
163
+ if local.size:
164
+ bounds.extend(local @ matrix[:3, :3].T + matrix[:3, 3])
165
+ first_update = self._bounds is None
166
+ self._bounds = np.array([np.min(bounds, axis=0), np.max(bounds, axis=0)])
167
+ if first_update:
168
+ self.reset_view()
169
+ self.canvas.request_draw()
170
+
171
+ def reset_view(self):
172
+ """Fit the current geometry and restore the initial viewing angle."""
173
+ if self._bounds is None:
174
+ return
175
+ center = self._bounds.mean(axis=0)
176
+ radius = max(float(np.linalg.norm(self._bounds[1] - center)), 1e-6)
177
+ self.plot.camera.show_object(
178
+ (*center, radius),
179
+ view_dir=(-1, 1, -0.8),
180
+ up=(0, 0, 1),
181
+ scale=1.15,
182
+ )
183
+ self.plot.camera.zoom = 1
184
+ self.canvas.request_draw()
185
+
186
+ def close(self):
187
+ self._cleanup()
@@ -0,0 +1,38 @@
1
+ """A CPU example; pass --show to visualize with the optional plotting extra."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ if __package__ in (None, ''):
8
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
9
+
10
+ import numpy as np
11
+ import quantities as q
12
+
13
+ from pyframetree import CoordinateSystem
14
+
15
+
16
+ def make_scene():
17
+ root = CoordinateSystem(name='root')
18
+ child = CoordinateSystem(parent=root, name='cloud')
19
+ child.add_points(np.random.default_rng(0).random((1000, 3)), 'points')
20
+ child.translate([2, 2, 0] * q.m)
21
+ child.rotate([0, 1, 1], 30 * q.deg)
22
+ root.rotate([1, 0, 0], 30 * q.deg)
23
+ return root, child
24
+
25
+
26
+ def main(argv=None):
27
+ parser = argparse.ArgumentParser(description=__doc__)
28
+ parser.add_argument('--show', action='store_true')
29
+ args = parser.parse_args(argv)
30
+ root, child = make_scene()
31
+ print(child)
32
+ print(f'World point array: {child.get_points("points", space="world").shape}')
33
+ if args.show:
34
+ root.visualize().show()
35
+
36
+
37
+ if __name__ == '__main__':
38
+ main()
@@ -0,0 +1 @@
1
+ # Tests package initialization
@@ -0,0 +1,58 @@
1
+ """Per-call cost of frame operations, in microseconds."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from timeit import Timer
6
+
7
+ if __package__ in (None, ''):
8
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
9
+
10
+ import numpy as np
11
+ import quantities as q
12
+
13
+ from pyframetree import CoordinateSystem
14
+
15
+
16
+ def make_chain(depth):
17
+ root = leaf = CoordinateSystem()
18
+ for _ in range(depth):
19
+ leaf = CoordinateSystem([1, 0, 0], parent=leaf)
20
+ leaf.rotate([0, 0, 1], 0.1)
21
+ return root, leaf
22
+
23
+
24
+ def microseconds(operation, min_time=0.05):
25
+ timer = Timer(operation)
26
+ count, _ = timer.autorange()
27
+ count = max(1, int(count * min_time / 0.2))
28
+ return min(timer.repeat(repeat=3, number=count)) / count * 1e6
29
+
30
+
31
+ def run_benchmarks(depths=(1, 10, 100), min_time=0.05):
32
+ """Return rows of (operation, depth, microseconds per call)."""
33
+ point = np.array([1.0, 2.0, 3.0])
34
+ rows = [('create a frame', 0, microseconds(CoordinateSystem, min_time))]
35
+ for depth in depths:
36
+ root, leaf = make_chain(depth)
37
+ operations = {
38
+ 'origin (cached)': lambda: leaf.origin,
39
+ 'origin after root moved': lambda: (root.translate([0, 0, 0]), leaf.origin),
40
+ 'translate': lambda: leaf.translate([0, 0, 0]),
41
+ 'translate (quantity)': lambda: leaf.translate([0, 0, 0] * q.cm),
42
+ 'translate (world)': lambda: leaf.translate([0, 0, 0], relative_to='world'),
43
+ 'rotate': lambda: leaf.rotate([0, 0, 1], 0.0),
44
+ 'to_world (one point)': lambda: leaf.to_world(point),
45
+ }
46
+ for name, operation in operations.items():
47
+ rows.append((name, depth, microseconds(operation, min_time)))
48
+ return rows
49
+
50
+
51
+ def print_results(rows):
52
+ print(f'{"operation":<26} {"depth":>5} {"µs/call":>9}')
53
+ for name, depth, time in rows:
54
+ print(f'{name:<26} {depth:>5} {time:9.2f}')
55
+
56
+
57
+ if __name__ == '__main__':
58
+ print_results(run_benchmarks())
@@ -0,0 +1,22 @@
1
+ """Translation, rotation and scaling, using the shared benchmark methodology."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ if __package__ in (None, ''):
7
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
8
+
9
+ from performance.benchmark_transformations import print_results, run_benchmarks
10
+
11
+
12
+ def benchmark_operations(nmin=100, nmax=1000000, num_runs=5, *, cpu_only=False):
13
+ import numpy as np
14
+
15
+ counts = np.unique(np.geomspace(nmin, nmax, num=6, dtype=int))
16
+ return run_benchmarks(
17
+ counts, num_runs, cpu_only=cpu_only, operations=('Translation', 'Rotation', 'Scaling')
18
+ )
19
+
20
+
21
+ if __name__ == '__main__':
22
+ print_results(benchmark_operations())