geometrykit 0.5.0__cp39-abi3-win_amd64.whl
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/__init__.py +43 -0
- geometrykit/_rust.pyd +0 -0
- geometrykit/cage.py +232 -0
- geometrykit/contract.py +106 -0
- geometrykit/engine.py +51 -0
- geometrykit/export.py +483 -0
- geometrykit/geometry/__init__.py +136 -0
- geometrykit/geometry/_bridge.py +48 -0
- geometrykit/geometry/biarc.py +425 -0
- geometrykit/geometry/curve.py +1007 -0
- geometrykit/geometry/field.py +531 -0
- geometrykit/geometry/frames.py +447 -0
- geometrykit/geometry/intersection.py +270 -0
- geometrykit/geometry/path.py +2701 -0
- geometrykit/geometry/patterns.py +402 -0
- geometrykit/geometry/primitives.py +1301 -0
- geometrykit/geometry/spiral.py +884 -0
- geometrykit/geometry/subdivision.py +324 -0
- geometrykit/geometry/surface.py +1576 -0
- geometrykit/geometry/transform.py +270 -0
- geometrykit/runtimes/__init__.py +2 -0
- geometrykit/runtimes/_npu/__init__.py +0 -0
- geometrykit/runtimes/_npu/curve_np.py +351 -0
- geometrykit/runtimes/_npu/fit_np.py +328 -0
- geometrykit/runtimes/_npu/linalg_np.py +581 -0
- geometrykit/runtimes/_npu/patch_g1.py +1448 -0
- geometrykit/runtimes/_npu/patch_np.py +732 -0
- geometrykit/runtimes/_npu/section_np.py +518 -0
- geometrykit/runtimes/_npu/surface_np.py +410 -0
- geometrykit/runtimes/dispatch.py +230 -0
- geometrykit-0.5.0.dist-info/METADATA +94 -0
- geometrykit-0.5.0.dist-info/RECORD +36 -0
- geometrykit-0.5.0.dist-info/WHEEL +5 -0
- geometrykit-0.5.0.dist-info/licenses/COPYING +621 -0
- geometrykit-0.5.0.dist-info/licenses/COPYING.LESSER +165 -0
- geometrykit-0.5.0.dist-info/top_level.txt +1 -0
geometrykit/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# This file was generated with the assistance of an AI coding tool.
|
|
2
|
+
"""geometrykit — geometry primitives and NURBS data model.
|
|
3
|
+
|
|
4
|
+
v0.1: first API release (contract v0, Tier-1 promotion of the geometry
|
|
5
|
+
primitives from ifckit, engine probe, and the M-phase test corpus).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from geometrykit import contract, engine
|
|
9
|
+
from geometrykit.geometry import (
|
|
10
|
+
Arc,
|
|
11
|
+
Curve,
|
|
12
|
+
Field,
|
|
13
|
+
HexPattern,
|
|
14
|
+
Line,
|
|
15
|
+
Path,
|
|
16
|
+
Plane,
|
|
17
|
+
Polyline,
|
|
18
|
+
Spiral,
|
|
19
|
+
Surface,
|
|
20
|
+
Transform,
|
|
21
|
+
Vec,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__version__ = "0.5.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"__version__",
|
|
28
|
+
"contract",
|
|
29
|
+
"engine",
|
|
30
|
+
# geometry (value types — guide tier)
|
|
31
|
+
"Vec",
|
|
32
|
+
"Plane",
|
|
33
|
+
"Line",
|
|
34
|
+
"Arc",
|
|
35
|
+
"Polyline",
|
|
36
|
+
"Path",
|
|
37
|
+
"Curve",
|
|
38
|
+
"Spiral",
|
|
39
|
+
"Surface",
|
|
40
|
+
"Transform",
|
|
41
|
+
"Field",
|
|
42
|
+
"HexPattern",
|
|
43
|
+
]
|
geometrykit/_rust.pyd
ADDED
|
Binary file
|
geometrykit/cage.py
ADDED
|
@@ -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())
|
geometrykit/contract.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# This file was generated with the assistance of an AI coding tool.
|
|
2
|
+
"""geometrykit contract v0 — canonical representation + signatures.
|
|
3
|
+
|
|
4
|
+
The kernel speaks one wire format across languages and engines: **plain
|
|
5
|
+
arrays + stable ids**, canonically serialized for hashing. Every engine
|
|
6
|
+
(`runtimes/_npu` numpy mirror, the `geometrykit._rust` accelerator) and the
|
|
7
|
+
planned `crates/geometrykit-core` must agree byte-for-byte on these
|
|
8
|
+
functions; cross-engine hash equality is a CI property test
|
|
9
|
+
(``tests/test_contract.py``).
|
|
10
|
+
|
|
11
|
+
Canonicalization rules (v0):
|
|
12
|
+
|
|
13
|
+
- dicts: keys sorted, all keys strings;
|
|
14
|
+
- sequences: lists (tuples/list equivalence);
|
|
15
|
+
- floats: replaced by ``"f64:<16 hex digits of the IEEE-754 bit pattern>"``
|
|
16
|
+
— stable across Python repr, JSON round trips, and Rust ``f64::to_bits``
|
|
17
|
+
(Python: ``struct.pack(">d", v).hex()``; Rust: ``format!("{:016x}", v.to_bits())``).
|
|
18
|
+
NaN/Inf make their bit patterns deterministic instead of breaking JSON.
|
|
19
|
+
- volatile keys (``id``, ``meta`` on nested geometry objects) are dropped —
|
|
20
|
+
ifckit's ``preview_session._VOLATILE_KEYS`` convention, generalized here;
|
|
21
|
+
- the canonical form is JSON (``sort_keys``), UTF-8 encoded; the signature
|
|
22
|
+
is MD5 truncated to 16 hex chars (matches ifckit's ``_element_sig`` grain).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import hashlib
|
|
28
|
+
import json
|
|
29
|
+
import math
|
|
30
|
+
import struct
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
#: Nested keys that carry no geometric meaning (per-run uuids, render meta).
|
|
34
|
+
VOLATILE_KEYS = frozenset({"id", "meta"})
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"VOLATILE_KEYS",
|
|
38
|
+
"canonicalize",
|
|
39
|
+
"contract_bytes",
|
|
40
|
+
"signature",
|
|
41
|
+
"element_record",
|
|
42
|
+
"float_to_hex",
|
|
43
|
+
"hex_to_float",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def float_to_hex(v: float) -> str:
|
|
48
|
+
"""IEEE-754 bit pattern of *v* as ``f64:<16 hex digits>`` (deterministic for NaN/Inf)."""
|
|
49
|
+
bits = struct.unpack(">Q", struct.pack(">d", float(v)))[0]
|
|
50
|
+
return f"f64:{bits:016x}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def hex_to_float(s: str) -> float:
|
|
54
|
+
"""Inverse of :func:`float_to_hex` (Rust ``f64::from_bits(u64::from_str_radix)``)."""
|
|
55
|
+
if not s.startswith("f64:"):
|
|
56
|
+
raise ValueError(f"not a canonical f64 payload: {s!r}")
|
|
57
|
+
return struct.unpack(">d", struct.pack(">Q", int(s[4:], 16)))[0]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def canonicalize(value: Any) -> Any:
|
|
61
|
+
"""Return the canonical in-memory form: sorted string keys, hex floats, no volatiles."""
|
|
62
|
+
if isinstance(value, dict):
|
|
63
|
+
return {
|
|
64
|
+
str(k): canonicalize(v)
|
|
65
|
+
for k, v in sorted(value.items(), key=lambda kv: str(kv[0]))
|
|
66
|
+
if k not in VOLATILE_KEYS
|
|
67
|
+
}
|
|
68
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
69
|
+
return [canonicalize(v) for v in value]
|
|
70
|
+
if isinstance(value, bool) or value is None or isinstance(value, str):
|
|
71
|
+
return value
|
|
72
|
+
if isinstance(value, int):
|
|
73
|
+
return value
|
|
74
|
+
if isinstance(value, float):
|
|
75
|
+
if math.isnan(value) or math.isinf(value):
|
|
76
|
+
return float_to_hex(value)
|
|
77
|
+
return float_to_hex(value) # every float is bit-canonicalized
|
|
78
|
+
if hasattr(value, "to_dict"):
|
|
79
|
+
return canonicalize(value.to_dict())
|
|
80
|
+
raise TypeError(f"cannot canonicalize {type(value).__name__}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def contract_bytes(value: Any) -> bytes:
|
|
84
|
+
"""Exact canonical bytes of *value* (UTF-8 JSON, sorted keys, hex floats)."""
|
|
85
|
+
|
|
86
|
+
text = json.dumps(canonicalize(value), sort_keys=True, separators=(",", ":"))
|
|
87
|
+
return text.encode("utf-8")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def signature(value: Any) -> str:
|
|
91
|
+
"""MD5 of the canonical bytes, truncated to 16 hex chars (ifckit sig grain)."""
|
|
92
|
+
return hashlib.md5(contract_bytes(value)).hexdigest()[:16]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def element_record(kind: str, body: Any, elem_id: str | None = None) -> dict:
|
|
96
|
+
"""Wrap geometry *body* in a contract record: ``{id, kind, body, sig}``.
|
|
97
|
+
|
|
98
|
+
The signature covers kind + body + ``elem_id`` (the record's own ``id``
|
|
99
|
+
key is excluded from hashing by the volatile-key rule, so the id rides
|
|
100
|
+
under a non-volatile alias inside the signed payload).
|
|
101
|
+
"""
|
|
102
|
+
record = {"kind": str(kind), "body": body, "id": elem_id}
|
|
103
|
+
record["sig"] = signature(
|
|
104
|
+
{"kind": record["kind"], "body": record["body"], "elem_id": record["id"]}
|
|
105
|
+
)
|
|
106
|
+
return record
|
geometrykit/engine.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# This file was generated with the assistance of an AI coding tool.
|
|
2
|
+
"""geometrykit engine probe.
|
|
3
|
+
|
|
4
|
+
Two engines implement one API against the contract:
|
|
5
|
+
|
|
6
|
+
- ``npu`` — the numpy/pure-Python mirror in ``runtimes/_npu/``; always
|
|
7
|
+
available, the distribution's floor;
|
|
8
|
+
- ``rust`` — the ``geometrykit._rust`` extension module (maturin,
|
|
9
|
+
``crates/geometrykit-py``), an accelerator that never adds API.
|
|
10
|
+
|
|
11
|
+
Selection: explicit override wins, otherwise probe the extension.
|
|
12
|
+
``GEOMETRYKIT_ENGINE`` accepts ``npu``/``rust``/``auto``
|
|
13
|
+
(``auto`` = probe, the default).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
from typing import Literal
|
|
20
|
+
|
|
21
|
+
Engine = Literal["npu", "rust"]
|
|
22
|
+
_OVERRIDE_KEYS = ("GEOMETRYKIT_ENGINE",)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _probe_rust() -> bool:
|
|
26
|
+
try:
|
|
27
|
+
import geometrykit._rust # noqa: F401
|
|
28
|
+
except ImportError:
|
|
29
|
+
return False
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def engine() -> Engine:
|
|
34
|
+
"""Return the engine this process will use for kernel functions."""
|
|
35
|
+
override = os.environ.get(_OVERRIDE_KEYS[0], "auto").strip().lower()
|
|
36
|
+
if override in ("npu", "python", "mirror"):
|
|
37
|
+
return "npu"
|
|
38
|
+
has_rust = _probe_rust()
|
|
39
|
+
if override == "rust":
|
|
40
|
+
if not has_rust:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
"GEOMETRYKIT_ENGINE=rust but the geometrykit._rust extension "
|
|
43
|
+
"is not importable in this environment"
|
|
44
|
+
)
|
|
45
|
+
return "rust"
|
|
46
|
+
return "rust" if has_rust else "npu"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def has_rust() -> bool:
|
|
50
|
+
"""True when the accelerator extension is importable."""
|
|
51
|
+
return _probe_rust()
|