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