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.
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
@@ -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,4 @@
1
+ from .frames import Frame, Point, displacement, reframe
2
+
3
+ __all__ = ["Frame", "Point", "displacement", "reframe"]
4
+ __version__ = "0.1.0a1"
@@ -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)