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.
Files changed (36) hide show
  1. geometrykit/__init__.py +43 -0
  2. geometrykit/_rust.pyd +0 -0
  3. geometrykit/cage.py +232 -0
  4. geometrykit/contract.py +106 -0
  5. geometrykit/engine.py +51 -0
  6. geometrykit/export.py +483 -0
  7. geometrykit/geometry/__init__.py +136 -0
  8. geometrykit/geometry/_bridge.py +48 -0
  9. geometrykit/geometry/biarc.py +425 -0
  10. geometrykit/geometry/curve.py +1007 -0
  11. geometrykit/geometry/field.py +531 -0
  12. geometrykit/geometry/frames.py +447 -0
  13. geometrykit/geometry/intersection.py +270 -0
  14. geometrykit/geometry/path.py +2701 -0
  15. geometrykit/geometry/patterns.py +402 -0
  16. geometrykit/geometry/primitives.py +1301 -0
  17. geometrykit/geometry/spiral.py +884 -0
  18. geometrykit/geometry/subdivision.py +324 -0
  19. geometrykit/geometry/surface.py +1576 -0
  20. geometrykit/geometry/transform.py +270 -0
  21. geometrykit/runtimes/__init__.py +2 -0
  22. geometrykit/runtimes/_npu/__init__.py +0 -0
  23. geometrykit/runtimes/_npu/curve_np.py +351 -0
  24. geometrykit/runtimes/_npu/fit_np.py +328 -0
  25. geometrykit/runtimes/_npu/linalg_np.py +581 -0
  26. geometrykit/runtimes/_npu/patch_g1.py +1448 -0
  27. geometrykit/runtimes/_npu/patch_np.py +732 -0
  28. geometrykit/runtimes/_npu/section_np.py +518 -0
  29. geometrykit/runtimes/_npu/surface_np.py +410 -0
  30. geometrykit/runtimes/dispatch.py +230 -0
  31. geometrykit-0.5.0.dist-info/METADATA +94 -0
  32. geometrykit-0.5.0.dist-info/RECORD +36 -0
  33. geometrykit-0.5.0.dist-info/WHEEL +5 -0
  34. geometrykit-0.5.0.dist-info/licenses/COPYING +621 -0
  35. geometrykit-0.5.0.dist-info/licenses/COPYING.LESSER +165 -0
  36. geometrykit-0.5.0.dist-info/top_level.txt +1 -0
@@ -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())
@@ -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()