geometrykit 0.1.0__tar.gz → 0.5.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 (46) hide show
  1. {geometrykit-0.1.0/python/geometrykit.egg-info → geometrykit-0.5.0}/PKG-INFO +10 -5
  2. {geometrykit-0.1.0 → geometrykit-0.5.0}/README.md +9 -4
  3. {geometrykit-0.1.0 → geometrykit-0.5.0}/pyproject.toml +9 -1
  4. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/__init__.py +1 -1
  5. geometrykit-0.5.0/python/geometrykit/cage.py +232 -0
  6. geometrykit-0.5.0/python/geometrykit/export.py +483 -0
  7. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/curve.py +135 -138
  8. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/field.py +26 -22
  9. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/intersection.py +1 -1
  10. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/patterns.py +1 -4
  11. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/surface.py +401 -113
  12. geometrykit-0.5.0/python/geometrykit/runtimes/__init__.py +2 -0
  13. geometrykit-0.5.0/python/geometrykit/runtimes/_npu/__init__.py +0 -0
  14. geometrykit-0.5.0/python/geometrykit/runtimes/_npu/curve_np.py +351 -0
  15. geometrykit-0.5.0/python/geometrykit/runtimes/_npu/fit_np.py +328 -0
  16. geometrykit-0.5.0/python/geometrykit/runtimes/_npu/linalg_np.py +581 -0
  17. geometrykit-0.5.0/python/geometrykit/runtimes/_npu/patch_g1.py +1448 -0
  18. geometrykit-0.5.0/python/geometrykit/runtimes/_npu/patch_np.py +732 -0
  19. geometrykit-0.5.0/python/geometrykit/runtimes/_npu/section_np.py +518 -0
  20. geometrykit-0.5.0/python/geometrykit/runtimes/_npu/surface_np.py +410 -0
  21. geometrykit-0.5.0/python/geometrykit/runtimes/dispatch.py +230 -0
  22. {geometrykit-0.1.0 → geometrykit-0.5.0/python/geometrykit.egg-info}/PKG-INFO +10 -5
  23. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit.egg-info/SOURCES.txt +13 -0
  24. geometrykit-0.5.0/tests/test_cage.py +88 -0
  25. geometrykit-0.5.0/tests/test_engine_parity.py +605 -0
  26. {geometrykit-0.1.0 → geometrykit-0.5.0}/tests/test_engine_probe.py +18 -6
  27. geometrykit-0.1.0/tests/test_engine_parity.py +0 -23
  28. {geometrykit-0.1.0 → geometrykit-0.5.0}/COPYING +0 -0
  29. {geometrykit-0.1.0 → geometrykit-0.5.0}/COPYING.LESSER +0 -0
  30. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/contract.py +0 -0
  31. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/engine.py +0 -0
  32. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/__init__.py +0 -0
  33. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/_bridge.py +0 -0
  34. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/biarc.py +0 -0
  35. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/frames.py +0 -0
  36. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/path.py +0 -0
  37. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/primitives.py +0 -0
  38. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/spiral.py +0 -0
  39. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/subdivision.py +0 -0
  40. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/transform.py +0 -0
  41. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit.egg-info/dependency_links.txt +0 -0
  42. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit.egg-info/requires.txt +0 -0
  43. {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit.egg-info/top_level.txt +0 -0
  44. {geometrykit-0.1.0 → geometrykit-0.5.0}/setup.cfg +0 -0
  45. {geometrykit-0.1.0 → geometrykit-0.5.0}/tests/test_contract.py +0 -0
  46. {geometrykit-0.1.0 → geometrykit-0.5.0}/tests/test_repo_boundary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geometrykit
3
- Version: 0.1.0
3
+ Version: 0.5.0
4
4
  Summary: Geometry primitives and NURBS data model — the geometry substrate for the ifckit family
5
5
  Author: Sander Boer
6
6
  License-Expression: LGPL-3.0-or-later
@@ -51,12 +51,17 @@ spec).
51
51
  ```bash
52
52
  python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
53
53
  .venv/bin/pytest tests/ -q # full suite (plain env)
54
- # oracle-inclusive run + staged coverage ladder (needs pythonocc-core):
55
- /opt/miniconda3/envs/a320-ifc-configurator/bin/python -m pytest tests/ --cov -q
56
- /opt/miniconda3/envs/a320-ifc-configurator/bin/python -m coverage json -o /tmp/geometrykit-cov.json
54
+
55
+ # oracle tier: the tests that check the kernel against OCCT
56
+ conda env create -f environment.yml # once
57
+ conda run -n geometrykit-oracle python -m pytest tests/ -q
58
+ # with coverage + the staged ladder:
59
+ conda run -n geometrykit-oracle python -m pytest tests/ --cov -q
60
+ conda run -n geometrykit-oracle python -m coverage json -o /tmp/geometrykit-cov.json
57
61
  python scripts/coverage_ladder.py /tmp/geometrykit-cov.json
58
62
  # corpus refresh (A320 demos → tests/fixtures/corpus/):
59
- /opt/miniconda3/envs/a320-ifc-configurator/bin/python scripts/capture_corpus.py
63
+ # where ifckit is importable (its own environment, not the oracle one):
64
+ python scripts/capture_corpus.py
60
65
  ```
61
66
 
62
67
  Layers: `tests/geometry/` (ported ifckit geometry suite), `tests/test_contract.py`
@@ -22,12 +22,17 @@ spec).
22
22
  ```bash
23
23
  python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
24
24
  .venv/bin/pytest tests/ -q # full suite (plain env)
25
- # oracle-inclusive run + staged coverage ladder (needs pythonocc-core):
26
- /opt/miniconda3/envs/a320-ifc-configurator/bin/python -m pytest tests/ --cov -q
27
- /opt/miniconda3/envs/a320-ifc-configurator/bin/python -m coverage json -o /tmp/geometrykit-cov.json
25
+
26
+ # oracle tier: the tests that check the kernel against OCCT
27
+ conda env create -f environment.yml # once
28
+ conda run -n geometrykit-oracle python -m pytest tests/ -q
29
+ # with coverage + the staged ladder:
30
+ conda run -n geometrykit-oracle python -m pytest tests/ --cov -q
31
+ conda run -n geometrykit-oracle python -m coverage json -o /tmp/geometrykit-cov.json
28
32
  python scripts/coverage_ladder.py /tmp/geometrykit-cov.json
29
33
  # corpus refresh (A320 demos → tests/fixtures/corpus/):
30
- /opt/miniconda3/envs/a320-ifc-configurator/bin/python scripts/capture_corpus.py
34
+ # where ifckit is importable (its own environment, not the oracle one):
35
+ python scripts/capture_corpus.py
31
36
  ```
32
37
 
33
38
  Layers: `tests/geometry/` (ported ifckit geometry suite), `tests/test_contract.py`
@@ -2,9 +2,17 @@
2
2
  requires = ["setuptools>=68"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
+ # The pure wheel (py3-none-any) is built by setuptools above: the
6
+ # always-works fallback (full mirror, no accelerator). The binary
7
+ # wheels carrying the compiled `_rust.abi3.so` are built by maturin per
8
+ # platform from crates/geometrykit-py, whose own pyproject.toml config
9
+ # points at this python/ tree (maturin reads the pyproject next to the
10
+ # cargo manifest, not the build-system above). One release publishes
11
+ # both routes together.
12
+
5
13
  [project]
6
14
  name = "geometrykit"
7
- version = "0.1.0"
15
+ version = "0.5.0"
8
16
  description = "Geometry primitives and NURBS data model — the geometry substrate for the ifckit family"
9
17
  readme = "README.md"
10
18
  requires-python = ">=3.9"
@@ -21,7 +21,7 @@ from geometrykit.geometry import (
21
21
  Vec,
22
22
  )
23
23
 
24
- __version__ = "0.1.0"
24
+ __version__ = "0.5.0"
25
25
 
26
26
  __all__ = [
27
27
  "__version__",
@@ -0,0 +1,232 @@
1
+ # This file was generated with the assistance of an AI coding tool.
2
+ """Spline-cage data records: coordinate systems + corners.
3
+
4
+ Contract v0.1 additions (plan: OCC-exit; cage data-model sessions):
5
+
6
+ - ``CoordinateSystem`` — a placed Cartesian frame in model space
7
+ (position + quaternion). Generic; carries no cage semantics itself.
8
+ - ``Corner`` — the cage composition: binds a ``CoordinateSystem`` to
9
+ curve terminals via a dict of angles (radians, XY polar around the
10
+ frame's +z). The **XY plane is the planarity plane** for cage
11
+ tangents (in-plane tangents have polar z = 0).
12
+
13
+ Conventions (pinned, see also ``contract.py``):
14
+
15
+ - quaternion order ``(qw, qx, qy, qz)``, Hamilton, right-handed,
16
+ normalized, **canonical sign ``qw ≥ 0``** (q and −q are the same
17
+ rotation; signature stability requires a single representation);
18
+ - ``tangent_for`` returns the tangent in **3D model space** (the frame's
19
+ local XY polar direction at θ, mapped through the quaternion);
20
+ - serialization is contract-canonical (hex-float bits) — cross-language
21
+ stable, same hashing rules as everything else.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import math
27
+ from typing import Optional
28
+
29
+ from geometrykit import contract as _contract
30
+
31
+
32
+ def _normalize_quaternion(qw: float, qx: float, qy: float, qz: float) -> tuple:
33
+ """Normalize and canonically sign-fix a quaternion (qw ≥ 0)."""
34
+ n = math.sqrt(qw * qw + qx * qx + qy * qy + qz * qz)
35
+ if n == 0.0:
36
+ raise ValueError("zero quaternion")
37
+ qw, qx, qy, qz = qw / n, qx / n, qy / n, qz / n
38
+ if qw < 0.0 or (qw == 0.0 and (qx, qy, qz) < (0.0, 0.0, 0.0)):
39
+ qw, qx, qy, qz = -qw, -qx, -qy, -qz
40
+ return qw, qx, qy, qz
41
+
42
+
43
+ def _quat_to_matrix(qw: float, qx: float, qy: float, qz: float) -> tuple:
44
+ """3×3 rotation matrix (row-major) from a normalized quaternion."""
45
+ return (
46
+ 1 - 2 * (qy * qy + qz * qz), 2 * (qx * qy - qz * qw), 2 * (qx * qz + qy * qw),
47
+ 2 * (qx * qy + qz * qw), 1 - 2 * (qx * qx + qz * qz), 2 * (qy * qz - qx * qw),
48
+ 2 * (qx * qz - qy * qw), 2 * (qy * qz + qx * qw), 1 - 2 * (qx * qx + qy * qy),
49
+ )
50
+
51
+
52
+ def _basis_from_normal_xref(nx, ny, nz, xref):
53
+ """Orthonormal frame with +z = normal and +x = projected xref (auto when None)."""
54
+ z = (nx, ny, nz)
55
+ n = math.sqrt(sum(c * c for c in z))
56
+ if n == 0.0:
57
+ raise ValueError("zero plane normal")
58
+ z = (z[0] / n, z[1] / n, z[2] / n)
59
+ if xref is None:
60
+ # auto: project world +X (or +Y when nearly parallel)
61
+ xref = (1.0, 0.0, 0.0) if abs(z[0]) < 0.9 else (0.0, 1.0, 0.0)
62
+ x = (xref[0], xref[1], xref[2])
63
+ d = x[0] * z[0] + x[1] * z[1] + x[2] * z[2]
64
+ x = (x[0] - d * z[0], x[1] - d * z[1], x[2] - d * z[2])
65
+ nx = math.sqrt(sum(c * c for c in x))
66
+ if nx < 1e-12:
67
+ raise ValueError("x reference is parallel to the plane normal")
68
+ x = (x[0] / nx, x[1] / nx, x[2] / nx)
69
+ y = (z[1] * x[2] - z[2] * x[1], z[2] * x[0] - z[0] * x[2], z[0] * x[1] - z[1] * x[0])
70
+ return x, y, z
71
+
72
+
73
+ def _matrix_to_quaternion(m) -> tuple:
74
+ """Quaternion from a 3×3 rotation matrix (robust branch on largest pivot)."""
75
+ tr = m[0] + m[4] + m[8]
76
+ if tr > 0.0:
77
+ s = math.sqrt(tr + 1.0) * 2.0
78
+ qw = 0.25 * s
79
+ qx = (m[7] - m[5]) / s
80
+ qy = (m[2] - m[6]) / s
81
+ qz = (m[3] - m[1]) / s
82
+ elif m[0] > m[4] and m[0] > m[8]:
83
+ s = math.sqrt(1.0 + m[0] - m[4] - m[8]) * 2.0
84
+ qw = (m[7] - m[5]) / s
85
+ qx = 0.25 * s
86
+ qy = (m[1] + m[3]) / s
87
+ qz = (m[2] + m[6]) / s
88
+ elif m[4] > m[8]:
89
+ s = math.sqrt(1.0 + m[4] - m[0] - m[8]) * 2.0
90
+ qw = (m[2] - m[6]) / s
91
+ qx = (m[1] + m[3]) / s
92
+ qy = 0.25 * s
93
+ qz = (m[5] + m[7]) / s
94
+ else:
95
+ s = math.sqrt(1.0 + m[8] - m[0] - m[4]) * 2.0
96
+ qw = (m[3] - m[1]) / s
97
+ qx = (m[2] + m[6]) / s
98
+ qy = (m[5] + m[7]) / s
99
+ qz = 0.25 * s
100
+ return _normalize_quaternion(qw, qx, qy, qz)
101
+
102
+
103
+ class CoordinateSystem:
104
+ """A placed Cartesian frame in 3D model space.
105
+
106
+ The XY plane is the designated planarity plane for cage tangents
107
+ (z = plane normal; angles in XY polar around +z, from +x).
108
+ Generic and reusable — cage semantics live in :class:`Corner`.
109
+ """
110
+
111
+ def __init__(
112
+ self,
113
+ position,
114
+ orientation=None,
115
+ *,
116
+ id: Optional[str] = None,
117
+ ) -> None:
118
+ self.id = id if id is not None else f"cs-{contract_signature_seed()}"
119
+ pos = tuple(float(c) for c in position)
120
+ if len(pos) != 3:
121
+ raise ValueError("position must have 3 components")
122
+ self.position = pos
123
+ if orientation is None:
124
+ self.orientation = (1.0, 0.0, 0.0, 0.0) # identity, canonical
125
+ else:
126
+ qw, qx, qy, qz = (float(c) for c in orientation)
127
+ self.orientation = _normalize_quaternion(qw, qx, qy, qz)
128
+
129
+ @classmethod
130
+ def from_plane(
131
+ cls, position, normal, x_ref=None, *, id: Optional[str] = None
132
+ ) -> "CoordinateSystem":
133
+ """Place a frame with +z = *normal* and +x = projected *x_ref* (auto when None)."""
134
+ x, y, z = _basis_from_normal_xref(normal[0], normal[1], normal[2], x_ref)
135
+ m = (x[0], x[1], x[2], y[0], y[1], y[2], z[0], z[1], z[2])
136
+ # row-major matrix maps local axes onto these world columns:
137
+ m = (x[0], y[0], z[0], x[1], y[1], z[1], x[2], y[2], z[2])
138
+ quat = _matrix_to_quaternion(m)
139
+ return cls(position, quat, id=id)
140
+
141
+ def matrix(self) -> tuple:
142
+ qw, qx, qy, qz = self.orientation
143
+ return _quat_to_matrix(qw, qx, qy, qz)
144
+
145
+ def apply_direction(self, v) -> tuple:
146
+ m = self.matrix()
147
+ return (
148
+ m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
149
+ m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
150
+ m[6] * v[0] + m[7] * v[1] + m[8] * v[2],
151
+ )
152
+
153
+ def apply_point(self, p) -> tuple:
154
+ d = self.apply_direction(p)
155
+ p0 = self.position
156
+ return (d[0] + p0[0], d[1] + p0[1], d[2] + p0[2])
157
+
158
+ def to_record(self) -> dict:
159
+ rec = {
160
+ "kind": "coordinate_system",
161
+ "id": self.id,
162
+ "position": {"x": self.position[0], "y": self.position[1], "z": self.position[2]},
163
+ "orientation": {
164
+ "qw": self.orientation[0], "qx": self.orientation[1],
165
+ "qy": self.orientation[2], "qz": self.orientation[3],
166
+ },
167
+ }
168
+ return rec
169
+
170
+ def signature(self) -> str:
171
+ return _contract.signature(self.to_record())
172
+
173
+
174
+ def contract_signature_seed() -> str:
175
+ """Short id seed for auto-named frames (stable within a process)."""
176
+ import uuid
177
+
178
+ return uuid.uuid4().hex[:8]
179
+
180
+
181
+ class Corner:
182
+ """Cage corner: binds a ``CoordinateSystem`` to curve terminals.
183
+
184
+ ``angles`` maps ``curve_id`` to the tangent angle θ (radians) in the
185
+ frame's XY polar coordinates — the associative record: an entry is
186
+ the end-tangent direction for the curve departing this corner.
187
+
188
+ v0.1 limitation: one terminal per curve per corner (loop curves with
189
+ both ends on one corner need terminal-keyed records; documented
190
+ extension).
191
+ """
192
+
193
+ def __init__(
194
+ self, coordinate_system: CoordinateSystem, angles: dict, *, id: Optional[str] = None
195
+ ) -> None:
196
+ self.coordinate_system = coordinate_system
197
+ self.angles = {str(k): float(v) for k, v in angles.items()}
198
+ self.id = id if id is not None else f"corner-{contract_signature_seed()}"
199
+
200
+ def tangent_for(self, curve_id) -> tuple:
201
+ """End-tangent direction of *curve_id* at this corner, in 3D model space.
202
+
203
+ Composition: the frame's XY polar direction at the recorded angle θ,
204
+ mapped through the coordinate system's orientation. Unit length by
205
+ construction.
206
+ """
207
+ key = str(curve_id)
208
+ if key not in self.angles:
209
+ raise KeyError(f"curve {curve_id!r} has no terminal at corner {self.id!r}")
210
+ theta = self.angles[key]
211
+ return self.coordinate_system.apply_direction((math.cos(theta), math.sin(theta), 0.0))
212
+
213
+ def validate_terminals(self, min_spacing: float = 1e-6) -> None:
214
+ """Raise when two terminals share (nearly) the same angle (degenerate cage)."""
215
+ values = sorted(v % (2.0 * math.pi) for v in self.angles.values())
216
+ for a, b in zip(values, values[1:] + [values[0] + 2.0 * math.pi]):
217
+ if b - a < min_spacing:
218
+ raise ValueError(
219
+ f"corner {self.id!r}: terminal angles degenerate (spacing {b - a:.3e})"
220
+ )
221
+
222
+ def to_record(self) -> dict:
223
+ rec = {
224
+ "kind": "corner",
225
+ "id": self.id,
226
+ "coordinate_system_id": self.coordinate_system.id,
227
+ "angles": {str(k): v for k, v in self.angles.items()},
228
+ }
229
+ return rec
230
+
231
+ def signature(self) -> str:
232
+ return _contract.signature(self.to_record())