Skip to content

Commit 1d9a511

Browse files
committed
Refactor _bez_csx4.py, _bez_ccx4.py, _ssx_v6.py, and related modules to enhance intersection routines with candidate pre-filtering, improved residual tolerance handling, and optimized subdivision logic. Update brep data classes to use unique UUID identifiers.
1 parent e10dc29 commit 1d9a511

10 files changed

Lines changed: 181 additions & 165 deletions

File tree

examples/ccx/tangential_int_2d.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from mmcore.extras.renderer.renderer3d import OrbitCamera, Viewer
44
from mmcore.geom._nurbs_eval import NURBSCurveTuple
5-
from mmcore.numeric.intersection.ccx import nurbs_ccx
5+
from mmcore.numeric.intersection.ccx._nccx4 import nurbs_ccx
66

77
import numpy as np
88
from mmcore.geom._nurbs_eval import NURBSCurveTuple
@@ -11,10 +11,10 @@
1111
curve1 = NURBSCurveTuple(
1212
order=4,
1313
knot=np.array([0., 0., 0., 0., 1., 1., 1., 1.]),
14-
control_points=np.array([[-0.92475126, 1.46166592, 0. ],
15-
[-0.25992915, 0.7062367 , 0. ],
16-
[-1.41598441, 1.14500509, 0. ],
17-
[-0.1356103 , 1.30775672, 0. ]]),
14+
control_points=np.array([[-0.92475126, 1.46166592, ],
15+
[-0.25992915, 0.7062367 ],
16+
[-1.41598441, 1.14500509 ],
17+
[-0.1356103 , 1.30775672 ]]),
1818
weights=np.array([1., 1., 1., 1.])
1919
)
2020
import numpy as np
@@ -24,16 +24,16 @@
2424
curve2 = NURBSCurveTuple(
2525
order=3,
2626
knot=np.array([0., 0., 0., 1., 1., 1.]),
27-
control_points=np.array([[-0.77783268, 1.2703046 , 0. ],
28-
[-0.17966058, 1.13222589, 0. ],
29-
[-0.25365612, 1.65359905, 0. ]]),
27+
control_points=np.array([[-0.77783268, 1.2703046 ],
28+
[-0.17966058, 1.13222589 ],
29+
[-0.25365612, 1.65359905 ]]),
3030
weights=np.array([1., 1., 1.])
3131
)
3232

3333

3434
tol=0.001
35-
translate_d=tol/10
36-
translate_vec=np.array([-0.09709519, 0.9952751,0.])*translate_d
35+
translate_d=tol*2
36+
translate_vec=np.array([-0.09709519, 0.9952751])*translate_d
3737

3838
curve3=curve2._replace(control_points=curve2.control_points-(translate_vec[np.newaxis,:]))
3939

mmcore/numeric/closest_point.py

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,41 +1020,26 @@ def bez_curve_closest_point(curve:NDArray, point:NDArray,atol=1e-3,rational=Fals
10201020
return best_cand.best_t,best_cand.best_d
10211021

10221022

1023+
import itertools
10231024

10241025

1025-
1026-
1027-
1028-
1029-
1030-
1031-
1032-
1033-
1034-
1035-
1036-
def nurbs_curve_closest_point(self: NURBSCurveTuple, point: NDArray[float], atol: float = 0.001, spt=None,angle_tol: float = None):
1026+
def nurbs_curve_closest_point(self: NURBSCurveTuple, point: NDArray[float], spt: float = 0.001,
1027+
angle_tol: float = None):
10371028
if isinstance(self, NURBSCurve):
1038-
self=_nurbs_to_tuple(self)
1029+
self = _nurbs_to_tuple(self)
10391030
candidates = decompose_curve(self)
1040-
if spt is not None:
1041-
atol=spt
10421031

1043-
best_f = float("inf")
1032+
best_f = [float("inf"), {}, (None, None)]
10441033
best_x = None
10451034
for candidate in candidates:
1046-
rational = not np.allclose(candidate.weights, 1)
1047-
bez=sbern.nurbs_bezier_to_bern(candidate,rational=rational)
10481035

1049-
min_t ,min_val= bez_curve_closest_point(bez, point, atol=atol,rational=rational)
1036+
min_val, min_t = _nurbs_curve_closest_point_divide_and_conquer(candidate, point, spt=spt, angle_tol=angle_tol)
10501037

1051-
if best_f > min_val:
1038+
if best_f[0] > min_val[0]:
10521039
best_f = min_val
10531040
best_x = min_t
1054-
1055-
1056-
return best_x, best_f
10571041

1042+
return best_x, (best_f[0], *best_f[1:])
10581043

10591044

10601045
def nurbs_surface_closest_point(self:NURBSSurfaceTuple, point:NDArray[float],spt:float=0.001, angle_tol:float=None):

mmcore/numeric/intersection/_bezier_common.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,30 @@ def _clamp(x, lo, hi):
368368

369369
G = eval_curve(C, t, rational=rational) - eval_surface(S, u, v, rational=rational)
370370
return t, u, v, G, (last_dt, last_du, last_dv)
371+
372+
373+
def _compute_remaining_intervals(excludes, lo, hi):
374+
"""Compute [lo, hi] minus the union of exclude intervals."""
375+
if not excludes:
376+
return [(lo, hi)]
377+
378+
excludes = sorted(excludes, key=lambda x: x[0])
379+
merged = [excludes[0]]
380+
for a, b in excludes[1:]:
381+
if a <= merged[-1][1]:
382+
merged[-1] = (merged[-1][0], max(merged[-1][1], b))
383+
else:
384+
merged.append((a, b))
385+
386+
result = []
387+
cursor = lo
388+
for ex_lo, ex_hi in merged:
389+
ex_lo = max(ex_lo, lo)
390+
ex_hi = min(ex_hi, hi)
391+
if cursor < ex_lo:
392+
result.append((cursor, ex_lo))
393+
cursor = max(cursor, ex_hi)
394+
if cursor < hi:
395+
result.append((cursor, hi))
396+
397+
return result

mmcore/numeric/intersection/_sq_dist_classify.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def _squeeze_value_dim(arr: NDArray) -> NDArray:
6464
def _check_min_of_net(F: NDArray, atol: float, w_scale: float) -> bool:
6565
"""Return True if net proves NO_INTERSECTION."""
6666
lb = float(np.min(F)) / (w_scale ** 2)
67+
#print(f"lb = {lb}")
6768
return lb > atol * atol
6869

6970

mmcore/numeric/intersection/ccx/_bez_ccx4.py

Lines changed: 78 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ def _subdivide_sq_dist_net(F, axis, t=0.5):
6565
left_v, right_v = de_casteljau_split_nd(Fv, axis=axis, t=t)
6666
return left_v[..., 0], right_v[..., 0]
6767

68+
def _subdivide_sq_dist_net_2d(F, u=0.5,v=0.5):
69+
"""Subdivide the scalar sq-dist Bernstein net along *axis*.
70+
71+
``de_casteljau_split_nd`` requires a trailing value dimension, so we
72+
temporarily add one and squeeze it back off.
73+
"""
74+
Fv = F[..., np.newaxis]
75+
76+
left_v, right_v = de_casteljau_split_nd(Fv, axis=1, t=u)
77+
left_u_left_v,right_u_left_v=de_casteljau_split_nd(left_v, axis=0, t=v)
78+
left_u_right_v,right_u_right_v =de_casteljau_split_nd(right_v, axis=0, t=v)
79+
return left_u_left_v[..., 0], left_u_right_v[..., 0],right_u_right_v[..., 0],right_u_left_v[..., 0]
6880

6981
def _boundary_zero_to_uv(bz: BoundaryZero, u0: float, u1: float, v0: float, v1: float) -> tuple[float, float]:
7082
"""Convert a BoundaryZero from the classifier into global (u, v) parameters.
@@ -216,7 +228,7 @@ def _phase2_ccx(F, C1, C2, C1_orig, C2_orig,
216228
cells += 1
217229

218230
seg1, seg2, F_cell, pw, qw, u0, u1, v0, v1, depth = stack.pop()
219-
231+
#print(f"CCX: {cells} cells: {( ( u0, u1), (v0, v1), depth)}")
220232
# AABB prune: cheapest possible check — control point bounding boxes
221233
from mmcore.numeric._aabb import aabb, aabb_intersect
222234
if rational:
@@ -246,7 +258,7 @@ def _phase2_ccx(F, C1, C2, C1_orig, C2_orig,
246258
for ax in range(2):
247259
dF = bernstein_partial_derivative_coeffs(Fv, axis=ax)
248260
coeffs = dF[..., 0]
249-
if np.min(coeffs) >= 0 or np.max(coeffs) <= 0:
261+
if np.min(coeffs) > 0 or np.max(coeffs) <= 0:
250262
can_have_stationary = False
251263
break
252264
if not can_have_stationary:
@@ -257,69 +269,87 @@ def _phase2_ccx(F, C1, C2, C1_orig, C2_orig,
257269
u_mid = 0.5 * (u0 + u1)
258270
v_mid = 0.5 * (v0 + v1)
259271
pt = eval_curve(C1_orig, u_mid, rational=rational)
272+
260273
if not _is_duplicate(isolated, pt, atol):
261274
isolated.append({"u": float(u_mid), "v": float(v_mid), "point": pt, "_micro": True})
262275
continue
263276

264277
# Newton from cell center
265278
u_mid = 0.5 * (u0 + u1)
266279
v_mid = 0.5 * (v0 + v1)
267-
u_sol, v_sol, G, last_step = newton_ccx(
268-
C1_orig, C2_orig, u_mid, v_mid, rational=rational,
269-
)
270-
step_norm = abs(last_step[0]) + abs(last_step[1])
271-
residual_ok = float(np.linalg.norm(G)) < atol
272-
converged = ((step_norm > 0 or residual_ok)
273-
and abs(last_step[0]) <= ptol_u
274-
and abs(last_step[1]) <= ptol_v)
275-
276-
if converged and u0 - ptol_u <= u_sol <= u1 + ptol_u and v0 - ptol_v <= v_sol <= v1 + ptol_v:
277-
pt = eval_curve(C1_orig, u_sol, rational=rational)
278-
is_new = not _is_duplicate(isolated, pt, atol)
279-
if is_new:
280-
isolated.append({"u": float(u_sol), "v": float(v_sol), "point": pt})
281-
sub_cells = _cutout_2d(
282-
F_cell, seg1, seg2, pw, qw, u0, u1, v0, v1, depth,
283-
float(u_sol), float(v_sol), ptol_u, ptol_v, rational,
284-
)
285-
stack.extend(sub_cells)
286-
continue
287-
288-
if converged:
280+
uv_candidates=[(u0 , v0),(u0,v1),(u1,v1),(u1,v0)]
281+
root_found=False
282+
is_converged=False
283+
for u_mid,v_mid in uv_candidates:
284+
if root_found:
285+
break
286+
u_sol, v_sol, G, last_step = newton_ccx(
287+
C1_orig,C2_orig, u_mid, v_mid, rational=rational,
288+
)
289+
step_norm = abs(last_step[0]) + abs(last_step[1])
290+
residual_ok = float(np.linalg.norm(G)) < atol
291+
converged = ((step_norm > 0 or residual_ok)
292+
and abs(last_step[0]) < ptol_u
293+
and abs(last_step[1]) < ptol_v)
294+
#print(f"CCX: {cells} cells NEWTON: {( u_sol, v_sol, G, last_step, ( u0, u1), (v0, v1), depth)}: {converged}")
295+
if converged:
296+
is_converged = True
297+
if converged and u0 < u_sol < u1 and v0 < v_sol < v1 :
298+
299+
pt = eval_curve(C1_orig, u_sol, rational=rational)
300+
is_new = not _is_duplicate(isolated, pt, atol)
301+
#print(f"CCX: is_new: {is_new}")
302+
if is_new:
303+
isolated.append({"u": float(u_sol), "v": float(v_sol), "point": pt})
304+
sub_cells = _cutout_2d(
305+
F_cell, seg1, seg2, pw, qw, u0, u1, v0, v1, depth,
306+
float(u_sol), float(v_sol), ptol_u, ptol_v, rational,
307+
)
308+
stack.extend(sub_cells)
309+
root_found=True
310+
break
311+
312+
if root_found:continue
313+
314+
315+
316+
if is_converged:
289317
# Converged outside cell → prune
290318
continue
291319

320+
292321
if depth >= max_depth:
293322
continue
294323

295324
# Subdivide
296-
u_span = u1 - u0
297-
v_span = v1 - v0
298-
axis = 0 if u_span >= v_span else 1
299-
300-
if axis == 0:
301-
u_mid_split = 0.5 * (u0 + u1)
302-
seg1_L, seg1_R = _subdivide_curve(seg1)
303-
F_L, F_R = _subdivide_sq_dist_net(F_cell, axis=0)
304-
pw_L = seg1_L[:, -1].copy() if rational else np.ones(seg1_L.shape[0])
305-
pw_R = seg1_R[:, -1].copy() if rational else np.ones(seg1_R.shape[0])
306-
stack.append((seg1_L, seg2.copy(), F_L, pw_L, qw.copy(), u0, u_mid_split, v0, v1, depth+1))
307-
stack.append((seg1_R, seg2.copy(), F_R, pw_R, qw.copy(), u_mid_split, u1, v0, v1, depth+1))
308-
else:
309-
v_mid_split = 0.5 * (v0 + v1)
310-
seg2_L, seg2_R = _subdivide_curve(seg2)
311-
F_L, F_R = _subdivide_sq_dist_net(F_cell, axis=1)
312-
qw_L = seg2_L[:, -1].copy() if rational else np.ones(seg2_L.shape[0])
313-
qw_R = seg2_R[:, -1].copy() if rational else np.ones(seg2_R.shape[0])
314-
stack.append((seg1.copy(), seg2_L, F_L, pw.copy(), qw_L, u0, u1, v0, v_mid_split, depth+1))
315-
stack.append((seg1.copy(), seg2_R, F_R, pw.copy(), qw_R, u0, u1, v_mid_split, v1, depth+1))
325+
326+
327+
328+
329+
u_mid_split = 0.5 * (u0 + u1)
330+
v_mid_split = 0.5 * (v0 + v1)
331+
seg1_L, seg1_R = _subdivide_curve(seg1)
332+
seg2_L, seg2_R = _subdivide_curve(seg2)
333+
F_LL, F_LR, F_RR,F_RL, =_subdivide_sq_dist_net_2d(F_cell,0.5 ,0.5)
334+
335+
336+
pw_L = seg1_L[:, -1].copy() if rational else np.ones(seg1_L.shape[0])
337+
pw_R = seg1_R[:, -1].copy() if rational else np.ones(seg1_R.shape[0])
338+
qw_L = seg2_L[:, -1].copy() if rational else np.ones(seg2_L.shape[0])
339+
qw_R = seg2_R[:, -1].copy() if rational else np.ones(seg2_R.shape[0])
340+
stack.append((seg1_L, seg2_L, F_LL, pw_L, qw_L, u0, u_mid_split, v0, v_mid_split, depth+1))
341+
stack.append((seg1_L, seg2_R, F_LR, pw_L, qw_R,u0, u_mid_split, v_mid_split, v1, depth+1))
342+
stack.append((seg1_R, seg2_R, F_RR, pw_R, qw_R,u_mid_split, u1, v_mid_split, v1, depth+1))
343+
stack.append((seg1_R, seg2_L, F_RL, pw_R, qw_L,u_mid_split, u1, v0, v_mid_split, depth+1))
316344

317345
return isolated[n_known:]
318346

319347

320348
# ---------------------------------------------------------------------------
321349
# Main algorithm: two-phase architecture
322350
# ---------------------------------------------------------------------------
351+
from .._bezier_common import _compute_remaining_intervals
352+
323353

324354
def bez_ccx(
325355
C1,
@@ -338,14 +368,7 @@ def bez_ccx(
338368
"""
339369
C1 = np.asarray(C1, dtype=np.float64)
340370
C2 = np.asarray(C2, dtype=np.float64)
341-
if rational:
342-
bb1=aabb_offset(aabb(C1[...,:-1] /C1[...,None,-1]),atol/2)
343-
bb2=aabb_offset(aabb(C2[...,:-1] / C2[..., None, -1]),atol/2)
344-
else:
345-
bb1 = aabb_offset(aabb(C1 ),atol/2)
346-
bb2 = aabb_offset(aabb(C2),atol/2)
347-
if not aabb_intersect(bb1, bb2):
348-
return {"isolated": [], "overlaps": []}
371+
349372
F = curve_curve_squared_net_homog(C1, C2, rational=rational)
350373

351374
_, Pw = extract_weights(C1, rational=rational)
@@ -363,7 +386,7 @@ def bez_ccx(
363386
# PHASE 1: Boundary analysis + overlap (initial patch only)
364387
# ===================================================================
365388
cls = classify_sq_dist_net(F, atol, Pw, Qw)
366-
389+
#print(f"CCX: {cls} (phase 1)")
367390
if cls.kind == NO_INTERSECTION:
368391
return {"isolated": [], "overlaps": []}
369392

@@ -417,7 +440,7 @@ def bez_ccx(
417440
u_lo_ovl = min(ovl["u_range"])
418441
u_hi_ovl = max(ovl["u_range"])
419442
for u_bz, v_bz, pt in boundary_hits:
420-
if not (u_lo_ovl - atol <= u_bz <= u_hi_ovl + atol):
443+
if not (u_lo_ovl-ptol_u <= u_bz <= u_hi_ovl + ptol_u):
421444
if not _is_duplicate(isolated, pt, atol):
422445
isolated.append({"u": u_bz, "v": v_bz, "point": pt})
423446
else:
@@ -442,7 +465,7 @@ def bez_ccx(
442465
for iso in isolated:
443466
u_exclude.append((iso["u"] - ptol_u, iso["u"] + ptol_u))
444467

445-
from mmcore.numeric.intersection.csx._bez_csx4 import _compute_remaining_intervals
468+
446469
u_intervals = _compute_remaining_intervals(u_exclude, 0.0, 1.0)
447470

448471
for u_lo, u_hi in u_intervals:

mmcore/numeric/intersection/csx/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from mmcore.numeric.intersection.csx._ncsx import nurbs_csx
33
from mmcore.numeric.intersection.csx._ncsx2 import nurbs_csx_v2
44
from mmcore.numeric.intersection.csx._ncsx4 import nurbs_csx as nurbs_csx_v4
5-
from mmcore.numeric.intersection.csx._bez_csx3 import bez_csx
5+
from mmcore.numeric.intersection.csx._bez_csx4 import bez_csx
66

77

88

0 commit comments

Comments
 (0)