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.
- {geometrykit-0.1.0/python/geometrykit.egg-info → geometrykit-0.5.0}/PKG-INFO +10 -5
- {geometrykit-0.1.0 → geometrykit-0.5.0}/README.md +9 -4
- {geometrykit-0.1.0 → geometrykit-0.5.0}/pyproject.toml +9 -1
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/__init__.py +1 -1
- geometrykit-0.5.0/python/geometrykit/cage.py +232 -0
- geometrykit-0.5.0/python/geometrykit/export.py +483 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/curve.py +135 -138
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/field.py +26 -22
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/intersection.py +1 -1
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/patterns.py +1 -4
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/surface.py +401 -113
- geometrykit-0.5.0/python/geometrykit/runtimes/__init__.py +2 -0
- geometrykit-0.5.0/python/geometrykit/runtimes/_npu/__init__.py +0 -0
- geometrykit-0.5.0/python/geometrykit/runtimes/_npu/curve_np.py +351 -0
- geometrykit-0.5.0/python/geometrykit/runtimes/_npu/fit_np.py +328 -0
- geometrykit-0.5.0/python/geometrykit/runtimes/_npu/linalg_np.py +581 -0
- geometrykit-0.5.0/python/geometrykit/runtimes/_npu/patch_g1.py +1448 -0
- geometrykit-0.5.0/python/geometrykit/runtimes/_npu/patch_np.py +732 -0
- geometrykit-0.5.0/python/geometrykit/runtimes/_npu/section_np.py +518 -0
- geometrykit-0.5.0/python/geometrykit/runtimes/_npu/surface_np.py +410 -0
- geometrykit-0.5.0/python/geometrykit/runtimes/dispatch.py +230 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0/python/geometrykit.egg-info}/PKG-INFO +10 -5
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit.egg-info/SOURCES.txt +13 -0
- geometrykit-0.5.0/tests/test_cage.py +88 -0
- geometrykit-0.5.0/tests/test_engine_parity.py +605 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/tests/test_engine_probe.py +18 -6
- geometrykit-0.1.0/tests/test_engine_parity.py +0 -23
- {geometrykit-0.1.0 → geometrykit-0.5.0}/COPYING +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/COPYING.LESSER +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/contract.py +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/engine.py +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/__init__.py +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/_bridge.py +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/biarc.py +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/frames.py +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/path.py +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/primitives.py +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/spiral.py +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/subdivision.py +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit/geometry/transform.py +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit.egg-info/dependency_links.txt +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit.egg-info/requires.txt +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/python/geometrykit.egg-info/top_level.txt +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/setup.cfg +0 -0
- {geometrykit-0.1.0 → geometrykit-0.5.0}/tests/test_contract.py +0 -0
- {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.
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|
|
@@ -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())
|