Skip to content

Commit c38841a

Browse files
committed
Add bez_ssx6_baseline.py and _ssx_v6.py to introduce baseline harness for intersection testing and new SSX implementation with enhanced handling for boundary conditions, Gauss map-based separability, and Newton refinement.
1 parent aa7f7a1 commit c38841a

3 files changed

Lines changed: 3920 additions & 0 deletions

File tree

examples/ssx/bez_ssx6_baseline.py

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""Baseline harness for bez_ssx v5.
2+
3+
Runs the 5 representative test cases, reports per-case:
4+
- number of branches, total points, open/closed tag
5+
- max residual |S1(s,t) - S2(u,v)| over all branch points
6+
- number of standalone SSXPoint results
7+
- min timing over N_RUNS
8+
9+
Used as the starting point and as the regression/perf check after each design iteration.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import time
15+
import warnings
16+
from dataclasses import dataclass
17+
18+
import numpy as np
19+
20+
from mmcore.numeric.intersection._bezier_common import eval_surface
21+
from mmcore.numeric.intersection.ssx._bez_ssx6 import bez_ssx, BoundaryOverlap
22+
23+
warnings.filterwarnings("ignore")
24+
25+
N_RUNS = 3
26+
27+
28+
@dataclass
29+
class Case:
30+
name: str
31+
S1: np.ndarray
32+
S2: np.ndarray
33+
expected_branches: int
34+
notes: str
35+
36+
37+
def make_cases() -> list[Case]:
38+
cases: list[Case] = []
39+
40+
s1 = np.array([[[0., 0., 5.], [0., 10., 5.]],
41+
[[10., 0., 5.], [10., 10., 5.]]])
42+
s2 = np.array([[[0., 0., 0.], [0., 10., 0.]],
43+
[[10., 0., 10.], [10., 10., 10.]]])
44+
cases.append(Case("planes", s1, s2, 1, "two planes, line intersection x=z=5"))
45+
46+
s1 = np.array([[[0., 0., 0.], [0., 10., 0.]],
47+
[[10., 0., 0.], [10., 10., 10.]]])
48+
s2 = np.array([[[0., 0., 3.], [0., 10., 3.]],
49+
[[10., 0., 3.], [10., 10., 3.]]])
50+
cases.append(Case("transversal", s1, s2, 1, "bilinear raised corner vs flat z=3"))
51+
52+
s1 = np.array([[[0., 0., 10.], [5., 5., 10.], [5., 10., 10.], [0., 15., 10.]],
53+
[[5., 0., 0.], [10., 5., 0.], [10., 10., 0.], [5., 15., 0.]],
54+
[[10., 0., 10.], [15., 5., 10.], [15., 10., 10.], [10., 15., 10.]]])
55+
s2 = np.array([[[0., 0., 0.], [5., 5., 0.], [5., 10., 0.], [0., 15., 0.]],
56+
[[5., 0., 10.], [10., 5., 10.], [10., 10., 10.], [5., 15., 10.]],
57+
[[10., 0., 0.], [15., 5., 0.], [15., 10., 0.], [10., 15., 0.]]])
58+
cases.append(Case("tangential", s1, s2, 1, "crossed bicubic saddles, tangent curve via Φ-tracer"))
59+
60+
s1 = np.array([[[-128.25, -129.86, 67.44], [-128.25, 129.86, 0.]],
61+
[[128.25, -46.98, 0.], [128.25, 129.86, 0.]]])
62+
s2 = np.array([[[-128.25, -129.86, 0.], [-128.25, 129.86, 0.]],
63+
[[128.25, -129.86, 0.], [128.25, 129.86, 0.]]])
64+
cases.append(Case("overlaps", s1, s2, 2, "bilinear with 2 edges on plane, 2 overlap branches"))
65+
66+
s1 = np.array([[[-19., -51., 3.], [-19., -46., 8.], [-19., -41., 3.], [-19., -36., 8.]],
67+
[[-14., -51., 8.], [-14., -46., 3.], [-14., -41., 8.], [-14., -36., 3.]],
68+
[[-9., -51., 3.], [-9., -46., 8.], [-9., -41., 3.], [-9., -36., 8.]],
69+
[[-4., -51., 8.], [-4., -46., 3.], [-4., -41., 8.], [-4., -36., 3.]]])
70+
s2 = np.array([[[-20.35213885, -55.05885716, 0.], [-19., -46., 0.],
71+
[-18.09739608, -42.8559574, 0.], [-19.39149131, -33.02632827, 0.]],
72+
[[-15.52250972, -56.35295239, 7.], [-14., -46., 7.],
73+
[-14., -41., 13.], [-14.56186218, -31.73223305, 7.]],
74+
[[-10.69288059, -57.64704761, 7.], [-9., -46., 13.],
75+
[-9., -41., 13.], [-9.73223305, -30.43813782, 7.]],
76+
[[-5.86325146, -58.94114284, 0.], [-4., -46., 6.],
77+
[-4., -41., 0.], [-4.90260392, -29.1440426, 0.]]])
78+
cases.append(Case("case5", s1, s2, 2, "wavy bicubic surfaces, two open branches"))
79+
80+
s1 = np.array([[[7.4968198, -34.44808135, 6.627417],
81+
[3.96128589, -40.81204238, -8.372583],
82+
[-3.8168887, -48.59021697, 6.627417]],
83+
[[14.73328128, -36.02768858, 6.627417],
84+
[6.95510669, -43.80586318, -9.372583],
85+
[3.41957278, -47.34139708, 6.627417]],
86+
[[17.72710208, -39.02150938, 6.627417],
87+
[9.94892749, -46.79968397, -7.372583],
88+
[6.41339358, -50.33521788, 6.627417]],
89+
[[17.18538897, -45.55086408, 6.627417],
90+
[15.94274828, -49.79350477, -2.372583],
91+
[13.16457369, -49.57167936, -7.372583]]])
92+
s2 = np.array([[[0., -51., 6.29241333], [0., -46., 6.09945352],
93+
[-0., -46., -4.68504969], [0., -36., -4.70758667]],
94+
[[5., -51., 6.09945352], [5., -46., -4.68504969],
95+
[5., -41., 6.09945352], [10., -36., -4.68504969]],
96+
[[6., -51., -4.68504969], [10., -46., 6.09945352],
97+
[10., -41., -4.68504969], [10., -36., 6.09945352]],
98+
[[15., -51., -4.70758667], [15., -42., -4.68504969],
99+
[15., -41., 6.09945352], [15., -36., 6.29241333]]])
100+
cases.append(Case("case6", s1, s2, 2, "loop + open branch (loop currently MISSING)"))
101+
102+
s1_swept = np.array([[[33.05079627, -57.09987394, 0.],
103+
[29.5295466, -63.44484237, 6.7646494],
104+
[21.73708777, -71.24200956, 0.]],
105+
[[40.28725776, -58.67948118, 0.],
106+
[32.51384961, -66.4481508, 9.37051336],
107+
[28.97354926, -69.99318967, 0.]],
108+
[[43.28107855, -61.67330197, 0.],
109+
[35.49815262, -69.45145922, 9.37051336],
110+
[28.73859119, -79.68670826, 0.]],
111+
[[45.10433572, -68.20265667, 0.],
112+
[41.48244052, -72.46428996, 4.71678541],
113+
[38.71855016, -76.6579268, 0.]]])
114+
115+
s2 = np.array([[[40.25282656, -76.40733562, 3.35169568],
116+
[45.30378577, -65.64948729, 4.77804379]],
117+
[[23.11248642, -70.28329548, 4.87380266],
118+
[30.39942473, -58.97443598, 3.35169568]]])
119+
cases.append(Case("case7", s1_swept, s2, 1, "one internal loop (currently MISSED)"))
120+
121+
s2 = np.array([[[40.25282656, -76.40733562, 2.23739797],
122+
[45.30378577, -65.64948729, 3.66374609]],
123+
[[23.11248642, -70.28329548, 3.75950495],
124+
[30.39942473, -58.97443598, 2.23739797]]])
125+
cases.append(Case("case8", s1_swept, s2, 1, "one branch (segments dropping out)"))
126+
127+
s2 = np.array([[[40.25282656, -76.40733562, -0.05990905],
128+
[45.30378577, -65.64948729, 1.36643906]],
129+
[[23.11248642, -70.28329548, 1.46219793],
130+
[30.39942473, -58.97443598, -0.05990905]]])
131+
cases.append(Case("case9", s1_swept, s2, 2, "two branches (one currently MISSED)"))
132+
133+
s2 = np.array([[[29.63685574, -70.79194487, 4.04308391],
134+
[33.99717923, -70.79194487, 7.50248027],
135+
[39.66180486, -70.79194487, 4.18744742]],
136+
[[29.63685574, -66.43162138, 0.58368755],
137+
[33.99717923, -66.43162138, 4.04308391],
138+
[39.66180486, -66.43162138, 0.72805106]],
139+
[[29.63685574, -60.76699576, 3.89872039],
140+
[33.99717923, -60.76699576, 7.35811675],
141+
[39.66180486, -60.76699576, 4.04308391]]])
142+
cases.append(Case("case10", s1_swept, s2, 2, "two branches (both partial)"))
143+
144+
s2 = np.array([[[39.589021714123604, -77.29117490559284, 3.5489239217672024],
145+
[44.639980924198085, -66.5333265772515, 4.9752720370625365]],
146+
[[22.4486815782254, -71.16713476157344, 5.071030903961587],
147+
[29.735619889641292, -59.85827526536872, 3.5489239217672024]]])
148+
cases.append(Case("case11", s1_swept, s2, 1, "single closed loop, interior — currently fragmented"))
149+
150+
return cases
151+
152+
153+
def max_residual(branches: list, S1: np.ndarray, S2: np.ndarray, rational: bool) -> float:
154+
err = 0.0
155+
for b in branches:
156+
stuv, xyz = b.stuv, b.xyz
157+
158+
for i in range(len(stuv)):
159+
s, t, u, v = stuv[i]
160+
p1 = eval_surface(S1, s, t, rational=rational)
161+
p2 = eval_surface(S2, u, v, rational=rational)
162+
d = float(np.linalg.norm(p1 - p2))
163+
if d > err:
164+
err = d
165+
return err
166+
167+
168+
def run_case(case: Case, atol: float = 1e-3) -> dict:
169+
S1h = np.concatenate([case.S1, np.ones(case.S1.shape[:-1] + (1,))], axis=-1)
170+
S2h = np.concatenate([case.S2, np.ones(case.S2.shape[:-1] + (1,))], axis=-1)
171+
172+
best_t = float("inf")
173+
result = None
174+
for _ in range(N_RUNS):
175+
t0 = time.perf_counter()
176+
result = bez_ssx(case.S1, case.S2, atol=atol, rational=False)
177+
t1 = time.perf_counter()
178+
best_t = min(best_t, t1 - t0)
179+
180+
branches = result["branches"]
181+
points = result["points"]
182+
total_pts = sum(len(b.xyz) if not isinstance(b,BoundaryOverlap) else 0 for b in branches)
183+
err = max_residual(branches, S1h, S2h, rational=True)
184+
185+
return dict(
186+
name=case.name,
187+
expected=case.expected_branches,
188+
actual=len(branches),
189+
total_pts=total_pts,
190+
n_points=len(points),
191+
max_err=err,
192+
time_s=best_t,
193+
notes=case.notes,
194+
)
195+
196+
197+
def main() -> None:
198+
rows = []
199+
for case in make_cases():
200+
r = run_case(case)
201+
rows.append(r)
202+
status = "OK" if r["actual"] == r["expected"] else "MISMATCH"
203+
print(f"[{status:8}] {r['name']:12} | exp={r['expected']} act={r['actual']:>2} "
204+
f"pts={r['total_pts']:>4} +{r['n_points']:>2}pt err={r['max_err']:.2e} "
205+
f"t={r['time_s']*1000:7.1f}ms // {r['notes']}")
206+
207+
print()
208+
print("| case | expected | actual | branches pts | standalone pts | max |S1-S2| | time (ms) | status |")
209+
print("|------|---------:|-------:|------------:|---------------:|-----------:|----------:|:------:|")
210+
for r in rows:
211+
status = "OK" if r["actual"] == r["expected"] else "MISMATCH"
212+
print(f"| {r['name']} | {r['expected']} | {r['actual']} | {r['total_pts']} | "
213+
f"{r['n_points']} | {r['max_err']:.2e} | {r['time_s']*1000:.1f} | {status} |")
214+
215+
216+
if __name__ == "__main__":
217+
main()

0 commit comments

Comments
 (0)