forgekernel 0.1.0__py3-none-any.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.
forgekernel/nurbs.py ADDED
@@ -0,0 +1,481 @@
1
+ """K3.1 — NURBS / B-spline curve evaluation via de Boor (ADR-0018/0019).
2
+
3
+ The groundwork for free-form geometry: STEP AP214 curves, general
4
+ sweeps/lofts, and — the crown jewel — surface–surface intersection.
5
+
6
+ The exactness charter reaches further here than one might expect. A
7
+ B-spline with **rational** control points, knots, and weights, evaluated
8
+ at a **rational** parameter, comes out *exactly rational*: de Boor's
9
+ recurrence is nothing but convex combinations — ``+``, ``×``, and ``÷``
10
+ by rationals — so no irrationality enters. Such a curve is ``exact``,
11
+ not merely certified, and ``eval`` returns a point in ℚ³ that OCCT can
12
+ only approximate.
13
+
14
+ The certified-interval path (ADR-0019) is reserved for the genuinely
15
+ irrational cases: an irrational weight (the √2/2 of a true circular
16
+ NURBS arc) or an irrational parameter. There ``eval_ci`` carries each
17
+ homogeneous coordinate as a ``CInterval`` and the division by the
18
+ weight stays enclosure-preserving.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from fractions import Fraction
24
+
25
+ from forgekernel.interval import CInterval
26
+
27
+ F = Fraction
28
+
29
+
30
+ class BSplineCurve:
31
+ """A NURBS curve: degree ``p``, control points, clamped/open knot
32
+ vector, optional weights (default 1 → a polynomial B-spline).
33
+
34
+ Control points are 3-tuples; knots and weights are scalars. Anything
35
+ rational stays exact through evaluation."""
36
+
37
+ def __init__(self, degree: int, control_points, knots, weights=None) -> None:
38
+ self.p = int(degree)
39
+ self.cp = [tuple(F(c) for c in pt) for pt in control_points]
40
+ self.U = [F(u) for u in knots]
41
+ n = len(self.cp)
42
+ if weights is None:
43
+ self.w = [F(1)] * n
44
+ self.rational = False
45
+ self.exact_weights = True
46
+ else:
47
+ # weights may be rational (exact eval available) or CInterval
48
+ # (a genuinely irrational weight, e.g. √2/2 for a true circle —
49
+ # only the certified path eval_ci applies)
50
+ self.w = [x if isinstance(x, CInterval) else F(x) for x in weights]
51
+ self.exact_weights = all(not isinstance(x, CInterval) for x in self.w)
52
+ self.rational = not self.exact_weights or \
53
+ any(x != self.w[0] for x in self.w)
54
+ if len(self.w) != n:
55
+ raise ValueError("weights and control points differ in count")
56
+ if len(self.U) != n + self.p + 1:
57
+ raise ValueError(
58
+ f"knot vector must have n+p+1={n + self.p + 1} entries, "
59
+ f"got {len(self.U)}")
60
+
61
+ # -- span location --------------------------------------------------------
62
+
63
+ def _span(self, u: F) -> int:
64
+ """Knot span index k with U[k] <= u < U[k+1] (clamped to the last
65
+ non-empty span at the right end). Exact rational comparisons."""
66
+ n = len(self.cp) - 1
67
+ if u >= self.U[n + 1]:
68
+ return n
69
+ if u <= self.U[self.p]:
70
+ return self.p
71
+ lo, hi = self.p, n + 1
72
+ while hi - lo > 1: # binary search, exact
73
+ mid = (lo + hi) // 2
74
+ if u < self.U[mid]:
75
+ hi = mid
76
+ else:
77
+ lo = mid
78
+ return lo
79
+
80
+ # -- exact rational evaluation (de Boor in homogeneous coords) ------------
81
+
82
+ def eval(self, t):
83
+ """Exact point in ℚ³ at parameter ``t`` (rational in, rational out).
84
+ Raises if any weight is irrational — use :meth:`eval_ci` for that."""
85
+ if not self.exact_weights:
86
+ raise ValueError(
87
+ "curve has irrational weights — use eval_ci (certified)")
88
+ u = F(t)
89
+ k = self._span(u)
90
+ p = self.p
91
+ # homogeneous control points (w·x, w·y, w·z, w) for the active span
92
+ d = []
93
+ for j in range(p + 1):
94
+ i = k - p + j
95
+ wi = self.w[i]
96
+ x, y, z = self.cp[i]
97
+ d.append([wi * x, wi * y, wi * z, wi])
98
+ for r in range(1, p + 1):
99
+ for j in range(p, r - 1, -1):
100
+ i = k - p + j
101
+ denom = self.U[i + p - r + 1] - self.U[i]
102
+ a = F(0) if denom == 0 else (u - self.U[i]) / denom
103
+ b = 1 - a
104
+ d[j] = [b * d[j - 1][c] + a * d[j][c] for c in range(4)]
105
+ hx, hy, hz, hw = d[p]
106
+ return (hx / hw, hy / hw, hz / hw)
107
+
108
+ # -- certified evaluation (for irrational weights/parameters) -------------
109
+
110
+ def eval_ci(self, t):
111
+ """Certified point (three ``CInterval``s) — the enclosure-preserving
112
+ path for irrational weights or parameters. Accepts rational or
113
+ ``CInterval`` inputs and never loses the bracket."""
114
+ u = t if isinstance(t, CInterval) else CInterval.exact(F(t))
115
+ # span from the interval midpoint (a location choice, not a decision
116
+ # that affects the certified value — the recurrence is continuous)
117
+ k = self._span(u.mid)
118
+ p = self.p
119
+ d = []
120
+ for j in range(p + 1):
121
+ i = k - p + j
122
+ wi = _ci(self.w[i])
123
+ x, y, z = (_ci(v) for v in self.cp[i])
124
+ d.append([wi * x, wi * y, wi * z, wi])
125
+ for r in range(1, p + 1):
126
+ for j in range(p, r - 1, -1):
127
+ i = k - p + j
128
+ denom = self.U[i + p - r + 1] - self.U[i]
129
+ if denom == 0:
130
+ continue
131
+ a = (u - _ci(self.U[i])) * _ci(F(1) / denom)
132
+ b = _ci(1) - a
133
+ d[j] = [b * d[j - 1][c] + a * d[j][c] for c in range(4)]
134
+ hx, hy, hz, hw = d[p]
135
+ inv = _ci_reciprocal(hw)
136
+ return (hx * inv, hy * inv, hz * inv)
137
+
138
+ # -- float evaluation (tessellation) --------------------------------------
139
+
140
+ def eval_f(self, t: float):
141
+ x, y, z = self.eval(F(t).limit_denominator(10 ** 9))
142
+ return (float(x), float(y), float(z))
143
+
144
+ def domain(self):
145
+ return (float(self.U[self.p]), float(self.U[len(self.cp)]))
146
+
147
+
148
+ # -- constructors -------------------------------------------------------------
149
+
150
+ def bezier(control_points, weights=None) -> BSplineCurve:
151
+ """A Bézier curve as a clamped B-spline on [0, 1]."""
152
+ n = len(control_points)
153
+ p = n - 1
154
+ knots = [F(0)] * (p + 1) + [F(1)] * (p + 1)
155
+ return BSplineCurve(p, control_points, knots, weights)
156
+
157
+
158
+ def _ci(x) -> CInterval:
159
+ return x if isinstance(x, CInterval) else CInterval.exact(F(x))
160
+
161
+
162
+ def _ci_reciprocal(x: CInterval) -> CInterval:
163
+ """1/x for an interval that strictly excludes zero (certified)."""
164
+ s = x.sign() # raises if straddles 0
165
+ lo, hi = (F(1) / x.hi, F(1) / x.lo) if s > 0 else (F(1) / x.hi, F(1) / x.lo)
166
+ return CInterval(min(lo, hi), max(lo, hi))
167
+
168
+
169
+ # -- K3.2: tensor-product NURBS surfaces --------------------------------------
170
+
171
+ def _deboor4(p: int, U, pts, u):
172
+ """De Boor on homogeneous 4-vectors (exact rational). ``pts`` spans one
173
+ curve; returns the 4-vector at parameter ``u``."""
174
+ n = len(pts) - 1
175
+ # span
176
+ if u >= U[n + 1]:
177
+ k = n
178
+ elif u <= U[p]:
179
+ k = p
180
+ else:
181
+ lo, hi = p, n + 1
182
+ while hi - lo > 1:
183
+ mid = (lo + hi) // 2
184
+ if u < U[mid]:
185
+ hi = mid
186
+ else:
187
+ lo = mid
188
+ k = lo
189
+ d = [list(pts[k - p + j]) for j in range(p + 1)]
190
+ dim = len(d[0])
191
+ for r in range(1, p + 1):
192
+ for j in range(p, r - 1, -1):
193
+ i = k - p + j
194
+ denom = U[i + p - r + 1] - U[i]
195
+ a = F(0) if denom == 0 else (u - U[i]) / denom
196
+ b = 1 - a
197
+ d[j] = [b * d[j - 1][c] + a * d[j][c] for c in range(dim)]
198
+ return tuple(d[p])
199
+
200
+
201
+ def _hodograph4(p: int, U, pts):
202
+ """Derivative curve of a (homogeneous) B-spline: degree p-1, control
203
+ points p·(P[i+1]−P[i])/(U[i+p+1]−U[i+1]), knots U[1:-1]. Exact."""
204
+ D = []
205
+ for i in range(len(pts) - 1):
206
+ denom = U[i + p + 1] - U[i + 1]
207
+ s = F(0) if denom == 0 else F(p) / denom
208
+ D.append(tuple(s * (pts[i + 1][c] - pts[i][c]) for c in range(4)))
209
+ return p - 1, U[1:-1], D
210
+
211
+
212
+ class BSplineSurface:
213
+ """A tensor-product NURBS surface: degrees (p, q), an nu×nv control
214
+ net, clamped knot vectors U (nu+p+1) and V (nv+q+1), optional
215
+ weights. Rational data at rational parameters evaluates exactly."""
216
+
217
+ def __init__(self, degree_u: int, degree_v: int, control_net,
218
+ knots_u, knots_v, weights=None) -> None:
219
+ self.p = int(degree_u)
220
+ self.q = int(degree_v)
221
+ self.cp = [[tuple(F(c) for c in pt) for pt in row] for row in control_net]
222
+ self.nu = len(self.cp)
223
+ self.nv = len(self.cp[0])
224
+ if any(len(r) != self.nv for r in self.cp):
225
+ raise ValueError("ragged control net")
226
+ self.U = [F(u) for u in knots_u]
227
+ self.V = [F(v) for v in knots_v]
228
+ if len(self.U) != self.nu + self.p + 1:
229
+ raise ValueError(f"knots_u wants {self.nu + self.p + 1} entries")
230
+ if len(self.V) != self.nv + self.q + 1:
231
+ raise ValueError(f"knots_v wants {self.nv + self.q + 1} entries")
232
+ if weights is None:
233
+ self.w = [[F(1)] * self.nv for _ in range(self.nu)]
234
+ else:
235
+ self.w = [[F(x) for x in row] for row in weights]
236
+ # homogeneous net
237
+ self.H = [[(self.w[i][j] * self.cp[i][j][0],
238
+ self.w[i][j] * self.cp[i][j][1],
239
+ self.w[i][j] * self.cp[i][j][2],
240
+ self.w[i][j]) for j in range(self.nv)]
241
+ for i in range(self.nu)]
242
+
243
+ # -- exact evaluation ------------------------------------------------------
244
+
245
+ def _eval_h(self, u, v):
246
+ """Homogeneous 4-vector at (u, v): de Boor down v then across u."""
247
+ u, v = F(u), F(v)
248
+ col = [_deboor4(self.q, self.V, row, v) for row in self.H]
249
+ return _deboor4(self.p, self.U, col, u)
250
+
251
+ def eval(self, u, v):
252
+ """Exact surface point in ℚ³ (rational in → rational out)."""
253
+ hx, hy, hz, hw = self._eval_h(u, v)
254
+ return (hx / hw, hy / hw, hz / hw)
255
+
256
+ def eval_f(self, u: float, v: float):
257
+ x, y, z = self.eval(F(u).limit_denominator(10 ** 9),
258
+ F(v).limit_denominator(10 ** 9))
259
+ return (float(x), float(y), float(z))
260
+
261
+ # -- exact partial derivatives (quotient rule in homogeneous space) -------
262
+
263
+ def _partials_h(self, u, v):
264
+ """(H, H_u, H_v) homogeneous 4-vectors, all exact."""
265
+ u, v = F(u), F(v)
266
+ col = [_deboor4(self.q, self.V, row, v) for row in self.H]
267
+ H = _deboor4(self.p, self.U, col, u)
268
+ pu, Uu, Du = _hodograph4(self.p, self.U, col)
269
+ H_u = _deboor4(pu, Uu, Du, u) if pu >= 0 and Du else (F(0),) * 4
270
+ # v-partial: hodograph each row in v, evaluate, then de Boor in u
271
+ rows_dv = []
272
+ for row in self.H:
273
+ qv, Vv, Dv = _hodograph4(self.q, self.V, row)
274
+ rows_dv.append(_deboor4(qv, Vv, Dv, v) if qv >= 0 and Dv
275
+ else (F(0),) * 4)
276
+ H_v = _deboor4(self.p, self.U, rows_dv, u)
277
+ return H, H_u, H_v
278
+
279
+ def partials(self, u, v):
280
+ """(S, S_u, S_v) in ℚ³ — the exact tangent plane data SSI needs.
281
+ Quotient rule: S_u = (A_u·w − A·w_u)/w² with H = (A, w)."""
282
+ H, H_u, H_v = self._partials_h(u, v)
283
+ w, wu, wv = H[3], H_u[3], H_v[3]
284
+ S = tuple(H[c] / w for c in range(3))
285
+ S_u = tuple((H_u[c] * w - H[c] * wu) / (w * w) for c in range(3))
286
+ S_v = tuple((H_v[c] * w - H[c] * wv) / (w * w) for c in range(3))
287
+ return S, S_u, S_v
288
+
289
+ def normal(self, u, v):
290
+ """Exact (unnormalized) normal S_u × S_v in ℚ³."""
291
+ _, su, sv = self.partials(u, v)
292
+ return (su[1] * sv[2] - su[2] * sv[1],
293
+ su[2] * sv[0] - su[0] * sv[2],
294
+ su[0] * sv[1] - su[1] * sv[0])
295
+
296
+ def domain(self):
297
+ return ((float(self.U[self.p]), float(self.U[self.nu])),
298
+ (float(self.V[self.q]), float(self.V[self.nv])))
299
+
300
+
301
+ def bezier_surface(control_net, weights=None) -> BSplineSurface:
302
+ """A Bézier patch as a clamped B-spline surface on [0,1]²."""
303
+ nu, nv = len(control_net), len(control_net[0])
304
+ p, q = nu - 1, nv - 1
305
+ ku = [F(0)] * (p + 1) + [F(1)] * (p + 1)
306
+ kv = [F(0)] * (q + 1) + [F(1)] * (q + 1)
307
+ return BSplineSurface(p, q, control_net, ku, kv, weights)
308
+
309
+
310
+ # -- K3.5: exact Bézier extraction (knot insertion to full multiplicity) ------
311
+
312
+ def _insert_knot_once(p: int, U, pts, u):
313
+ """Boehm insertion of knot ``u`` into a (homogeneous or cartesian)
314
+ control sequence. Exact: convex combinations only. Returns (U', pts')."""
315
+ n = len(pts) - 1
316
+ # span k: U[k] <= u < U[k+1]
317
+ k = p
318
+ while k < n + 1 and not (U[k] <= u < U[k + 1]):
319
+ k += 1
320
+ if k == n + 1:
321
+ k = n
322
+ dim = len(pts[0])
323
+ out = [pts[0]]
324
+ for i in range(1, len(pts) + 1):
325
+ if i <= k - p:
326
+ out.append(pts[i] if i < len(pts) else pts[-1])
327
+ elif i <= k:
328
+ denom = U[i + p] - U[i]
329
+ a = F(0) if denom == 0 else (u - U[i]) / denom
330
+ out.append(tuple((1 - a) * pts[i - 1][c] + a * pts[i][c]
331
+ for c in range(dim)))
332
+ else:
333
+ out.append(pts[i - 1])
334
+ newU = sorted(list(U) + [u])
335
+ return newU, out[:n + 2]
336
+
337
+
338
+ def bezier_segments(curve: "BSplineCurve"):
339
+ """Split a (polynomial) B-spline curve into exact Bézier segments:
340
+ [(u0, u1, [P0..Pp]), ...]. Knot insertion to full multiplicity."""
341
+ if not curve.exact_weights or curve.rational:
342
+ raise ValueError("bezier extraction: polynomial curves only (K3.6)")
343
+ p = curve.p
344
+ U = list(curve.U)
345
+ pts = [tuple(pt) for pt in curve.cp]
346
+ # insert every interior knot up to multiplicity p
347
+ lo, hi = U[p], U[len(pts)]
348
+ interior = sorted({u for u in U if lo < u < hi})
349
+ for u in interior:
350
+ while U.count(u) < p:
351
+ U, pts = _insert_knot_once(p, U, pts, u)
352
+ breaks = [lo] + interior + [hi]
353
+ segs = []
354
+ for j in range(len(breaks) - 1):
355
+ segs.append((breaks[j], breaks[j + 1], pts[j * p: j * p + p + 1]))
356
+ return segs
357
+
358
+
359
+ def bezier_patches(surface: "BSplineSurface"):
360
+ """Split a B-spline surface into exact Bézier patches:
361
+ [(u0, u1, v0, v1, net), ...] — insertion along u then along v.
362
+
363
+ Polynomial surfaces yield 3-tuple cartesian nets; rational surfaces
364
+ yield homogeneous 4-tuple nets (wx, wy, wz, w) — knot insertion is
365
+ the same convex-combination recurrence in either space."""
366
+ rational = any(w != F(1) for row in surface.w for w in row)
367
+ p, q = surface.p, surface.q
368
+ # --- u direction: treat each v-column of the net as a u-curve
369
+ U = list(surface.U)
370
+ src = surface.H if rational else surface.cp
371
+ cols = [[src[i][j] for i in range(surface.nu)]
372
+ for j in range(surface.nv)]
373
+ lo_u, hi_u = U[p], U[surface.nu]
374
+ int_u = sorted({u for u in U if lo_u < u < hi_u})
375
+ for u in int_u:
376
+ while U.count(u) < p:
377
+ newU = None
378
+ for j in range(len(cols)):
379
+ nu2, cols[j] = _insert_knot_once(p, U, cols[j], u)
380
+ newU = nu2
381
+ U = newU
382
+ # --- v direction on the refined net
383
+ V = list(surface.V)
384
+ rows = [[cols[j][i] for j in range(len(cols))]
385
+ for i in range(len(cols[0]))]
386
+ lo_v, hi_v = V[q], V[surface.nv]
387
+ int_v = sorted({v for v in V if lo_v < v < hi_v})
388
+ for v in int_v:
389
+ while V.count(v) < q:
390
+ newV = None
391
+ for i in range(len(rows)):
392
+ nv2, rows[i] = _insert_knot_once(q, V, rows[i], v)
393
+ newV = nv2
394
+ V = newV
395
+ ub = [lo_u] + int_u + [hi_u]
396
+ vb = [lo_v] + int_v + [hi_v]
397
+ patches = []
398
+ for a in range(len(ub) - 1):
399
+ for b in range(len(vb) - 1):
400
+ net = [[rows[a * p + i][b * q + j] for j in range(q + 1)]
401
+ for i in range(p + 1)]
402
+ patches.append((ub[a], ub[a + 1], vb[b], vb[b + 1], net))
403
+ return patches
404
+
405
+
406
+ # -- K6.0: second partials (polynomial surfaces; rational → K6.1) -------------
407
+
408
+ def _hodo_list(p, U, pts):
409
+ """Hodograph as plain lists (degree p-1, knots U[1:-1])."""
410
+ D = []
411
+ for i in range(len(pts) - 1):
412
+ denom = U[i + p + 1] - U[i + 1]
413
+ s = F(0) if denom == 0 else F(p) / denom
414
+ D.append(tuple(s * (pts[i + 1][c] - pts[i][c])
415
+ for c in range(len(pts[0]))))
416
+ return p - 1, U[1:-1], D
417
+
418
+
419
+ def _h_partials2(surface: "BSplineSurface", u, v):
420
+ """Homogeneous 4-vector derivatives (H, H_u, H_v, H_uu, H_uv, H_vv)
421
+ of the weighted net — exact via double hodographs."""
422
+ u, v = F(u), F(v)
423
+ p, q, U, V = surface.p, surface.q, surface.U, surface.V
424
+ zero4 = (F(0),) * 4
425
+
426
+ def eval_curve(pp, UU, pts, t):
427
+ return _deboor4(pp, UU, pts, t) if pp >= 0 and pts else zero4
428
+
429
+ rows_v = [tuple(_deboor4(q, V, [tuple(pt) for pt in row], v))
430
+ for row in surface.H]
431
+ H = _deboor4(p, U, rows_v, u)
432
+ pu, Uu, Du = _hodo_list(p, U, rows_v)
433
+ H_u = eval_curve(pu, Uu, Du, u)
434
+ puu, Uuu, Duu = _hodo_list(pu, Uu, Du) if pu >= 1 else (-1, [], [])
435
+ H_uu = eval_curve(puu, Uuu, Duu, u)
436
+ rows_dv, rows_dvv = [], []
437
+ for r in surface.H:
438
+ qv, Vv, Dv = _hodo_list(q, V, [tuple(pt) for pt in r])
439
+ rows_dv.append(eval_curve(qv, Vv, Dv, v))
440
+ if qv >= 1:
441
+ qvv, Vvv, Dvv = _hodo_list(qv, Vv, Dv)
442
+ rows_dvv.append(eval_curve(qvv, Vvv, Dvv, v))
443
+ else:
444
+ rows_dvv.append(zero4)
445
+ H_v = _deboor4(p, U, rows_dv, u)
446
+ H_vv = _deboor4(p, U, rows_dvv, u)
447
+ puv, Uuv, Duv = _hodo_list(p, U, rows_dv)
448
+ H_uv = eval_curve(puv, Uuv, Duv, u)
449
+ return H, H_u, H_v, H_uu, H_uv, H_vv
450
+
451
+
452
+ def surface_partials2(surface: "BSplineSurface", u, v):
453
+ """(S, S_u, S_v, S_uu, S_uv, S_vv) — all exact ℚ³, polynomial OR
454
+ rational (K6.1). Recursive quotient rule in homogeneous space:
455
+
456
+ S = A/w
457
+ S_x = (A_x − S·w_x)/w
458
+ S_xy = (A_xy − S_x·w_y − S_y·w_x − S·w_xy)/w
459
+
460
+ — only +, ×, ÷ by rationals, so the result is exact whenever the
461
+ weights are."""
462
+ H, H_u, H_v, H_uu, H_uv, H_vv = _h_partials2(surface, u, v)
463
+ w = H[3]
464
+
465
+ def sub3(A, *terms):
466
+ """(A[0:3] − Σ coeff·vec)/w componentwise."""
467
+ out = []
468
+ for c in range(3):
469
+ acc = A[c]
470
+ for coeff, vec in terms:
471
+ acc -= coeff * vec[c]
472
+ out.append(acc / w)
473
+ return tuple(out)
474
+
475
+ S = (H[0] / w, H[1] / w, H[2] / w)
476
+ S_u = sub3(H_u, (H_u[3], S))
477
+ S_v = sub3(H_v, (H_v[3], S))
478
+ S_uu = sub3(H_uu, (2 * H_u[3], S_u), (H_uu[3], S))
479
+ S_vv = sub3(H_vv, (2 * H_v[3], S_v), (H_vv[3], S))
480
+ S_uv = sub3(H_uv, (H_u[3], S_v), (H_v[3], S_u), (H_uv[3], S))
481
+ return S, S_u, S_v, S_uu, S_uv, S_vv