sheafrelgeom 0.1.0a1__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.
- sheafrelgeom-0.1.0a1/.gitignore +5 -0
- sheafrelgeom-0.1.0a1/PKG-INFO +39 -0
- sheafrelgeom-0.1.0a1/README.md +20 -0
- sheafrelgeom-0.1.0a1/pyproject.toml +29 -0
- sheafrelgeom-0.1.0a1/src/sheafrelgeom/__init__.py +4 -0
- sheafrelgeom-0.1.0a1/src/sheafrelgeom/frames.py +75 -0
- sheafrelgeom-0.1.0a1/tests/test_frames.py +28 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sheafrelgeom
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Small reference-frame transformations for relative geometric measurements.
|
|
5
|
+
Project-URL: Repository, https://github.com/SheafLab/sheafrelgeom-python
|
|
6
|
+
Author: SheafLab
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: coordinates,geometry,reference-frames,relative,sheaflab
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# sheafrelgeom
|
|
21
|
+
|
|
22
|
+
`sheafrelgeom` provides a tiny typed core for measurements expressed relative
|
|
23
|
+
to named reference frames. It deliberately handles translation only, keeping
|
|
24
|
+
the semantics explicit while leaving rotations, projections, and units to
|
|
25
|
+
specialized packages.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from sheafrelgeom import Frame, Point, reframe
|
|
29
|
+
|
|
30
|
+
world = Frame("world")
|
|
31
|
+
camera = Frame("camera", parent=world, origin=(2.0, 1.0))
|
|
32
|
+
pixel_origin = Point("camera", (0.0, 0.0))
|
|
33
|
+
|
|
34
|
+
assert reframe(pixel_origin, camera, world) == Point("world", (2.0, 1.0))
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Frames form an acyclic parent chain. `reframe` resolves a point through that
|
|
38
|
+
chain and returns it in any related target frame. The package has no runtime
|
|
39
|
+
dependencies and supports coordinates of any matching dimension.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# sheafrelgeom
|
|
2
|
+
|
|
3
|
+
`sheafrelgeom` provides a tiny typed core for measurements expressed relative
|
|
4
|
+
to named reference frames. It deliberately handles translation only, keeping
|
|
5
|
+
the semantics explicit while leaving rotations, projections, and units to
|
|
6
|
+
specialized packages.
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from sheafrelgeom import Frame, Point, reframe
|
|
10
|
+
|
|
11
|
+
world = Frame("world")
|
|
12
|
+
camera = Frame("camera", parent=world, origin=(2.0, 1.0))
|
|
13
|
+
pixel_origin = Point("camera", (0.0, 0.0))
|
|
14
|
+
|
|
15
|
+
assert reframe(pixel_origin, camera, world) == Point("world", (2.0, 1.0))
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Frames form an acyclic parent chain. `reframe` resolves a point through that
|
|
19
|
+
chain and returns it in any related target frame. The package has no runtime
|
|
20
|
+
dependencies and supports coordinates of any matching dimension.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sheafrelgeom"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "Small reference-frame transformations for relative geometric measurements."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "SheafLab" }]
|
|
13
|
+
keywords = ["geometry", "reference-frames", "relative", "coordinates", "sheaflab"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Repository = "https://github.com/SheafLab/sheafrelgeom-python"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/sheafrelgeom"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Translation-only relationships between named coordinate frames."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class Frame:
|
|
10
|
+
name: str
|
|
11
|
+
parent: Frame | None = None
|
|
12
|
+
origin: tuple[float, ...] = ()
|
|
13
|
+
|
|
14
|
+
def __post_init__(self) -> None:
|
|
15
|
+
if not self.name.strip():
|
|
16
|
+
raise ValueError("frames require a non-empty name")
|
|
17
|
+
if self.parent is None and self.origin:
|
|
18
|
+
raise ValueError("root frames must have an empty origin")
|
|
19
|
+
if self.parent is not None and not self.origin:
|
|
20
|
+
raise ValueError("child frames require an origin in their parent")
|
|
21
|
+
cursor = self.parent
|
|
22
|
+
while cursor is not None:
|
|
23
|
+
if cursor.name == self.name:
|
|
24
|
+
raise ValueError("frame parent chain contains a repeated name")
|
|
25
|
+
cursor = cursor.parent
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class Point:
|
|
30
|
+
frame: str
|
|
31
|
+
coordinates: tuple[float, ...]
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
if not self.frame.strip() or not self.coordinates:
|
|
35
|
+
raise ValueError("points require a frame and at least one coordinate")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _world_coordinates(point: Point, frame: Frame) -> tuple[float, ...]:
|
|
39
|
+
if point.frame != frame.name:
|
|
40
|
+
raise ValueError("point does not belong to the supplied frame")
|
|
41
|
+
values = point.coordinates
|
|
42
|
+
cursor = frame
|
|
43
|
+
while cursor.parent is not None:
|
|
44
|
+
if len(values) != len(cursor.origin):
|
|
45
|
+
raise ValueError("point and frame dimensions must match")
|
|
46
|
+
values = tuple(value + offset for value, offset in zip(values, cursor.origin))
|
|
47
|
+
cursor = cursor.parent
|
|
48
|
+
return values
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _root(frame: Frame) -> Frame:
|
|
52
|
+
cursor = frame
|
|
53
|
+
while cursor.parent is not None:
|
|
54
|
+
cursor = cursor.parent
|
|
55
|
+
return cursor
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def reframe(point: Point, source: Frame, target: Frame) -> Point:
|
|
59
|
+
"""Express ``point`` from ``source`` in a related ``target`` frame."""
|
|
60
|
+
|
|
61
|
+
if _root(source).name != _root(target).name:
|
|
62
|
+
raise ValueError("source and target frames do not share a root")
|
|
63
|
+
world = _world_coordinates(point, source)
|
|
64
|
+
target_origin = _world_coordinates(Point(target.name, (0.0,) * len(world)), target)
|
|
65
|
+
if len(world) != len(target_origin):
|
|
66
|
+
raise ValueError("source and target dimensions must match")
|
|
67
|
+
return Point(target.name, tuple(value - offset for value, offset in zip(world, target_origin)))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def displacement(start: Point, end: Point) -> tuple[float, ...]:
|
|
71
|
+
"""Return the vector from ``start`` to ``end`` in their shared frame."""
|
|
72
|
+
|
|
73
|
+
if start.frame != end.frame or len(start.coordinates) != len(end.coordinates):
|
|
74
|
+
raise ValueError("displacement requires points in the same dimension and frame")
|
|
75
|
+
return tuple(right - left for left, right in zip(start.coordinates, end.coordinates))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from sheafrelgeom import Frame, Point, displacement, reframe
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FrameTests(unittest.TestCase):
|
|
7
|
+
def setUp(self) -> None:
|
|
8
|
+
self.world = Frame("world")
|
|
9
|
+
self.camera = Frame("camera", self.world, (2.0, 1.0))
|
|
10
|
+
self.sensor = Frame("sensor", self.camera, (0.5, -1.0))
|
|
11
|
+
|
|
12
|
+
def test_reframe_walks_parent_chain(self) -> None:
|
|
13
|
+
point = reframe(Point("sensor", (1.0, 2.0)), self.sensor, self.world)
|
|
14
|
+
self.assertEqual(point, Point("world", (3.5, 2.0)))
|
|
15
|
+
|
|
16
|
+
def test_reframe_can_target_an_ancestor(self) -> None:
|
|
17
|
+
point = reframe(Point("sensor", (1.0, 2.0)), self.sensor, self.camera)
|
|
18
|
+
self.assertEqual(point, Point("camera", (1.5, 1.0)))
|
|
19
|
+
|
|
20
|
+
def test_displacement_requires_shared_frame(self) -> None:
|
|
21
|
+
self.assertEqual(displacement(Point("world", (1.0, 2.0)), Point("world", (3.0, 1.0))), (2.0, -1.0))
|
|
22
|
+
with self.assertRaises(ValueError):
|
|
23
|
+
displacement(Point("world", (1.0,)), Point("camera", (1.0,)))
|
|
24
|
+
|
|
25
|
+
def test_reframe_rejects_unrelated_roots(self) -> None:
|
|
26
|
+
other = Frame("other")
|
|
27
|
+
with self.assertRaises(ValueError):
|
|
28
|
+
reframe(Point("camera", (0.0, 0.0)), self.camera, other)
|