@@ -316,6 +316,40 @@ def _to_float_scalar(x):
316316def _to_float_vec3 (v ):
317317 return np .array ([_to_float_scalar (v [0 ]), _to_float_scalar (v [1 ]), _to_float_scalar (v [2 ])], dtype = float )
318318
319+ # ------------------------------------------------------------
320+ # Fast point evaluators — avoid numpy moveaxis/asarray overhead
321+ # of the generic de_casteljau_section_nd for scalar point queries.
322+ # ------------------------------------------------------------
323+
324+ def _decasteljau_1axis (cur , t ):
325+ """Apply de Casteljau along axis-0, reducing its size to 1, then squeeze."""
326+ omt = 1.0 - t
327+ for _ in range (cur .shape [0 ] - 1 ):
328+ cur = omt * cur [:- 1 ] + t * cur [1 :]
329+ return cur [0 ]
330+
331+ def _fast_beval_scalar_4d (net , s , t , u , v ):
332+ """Evaluate a 4D scalar Bernstein net at float point (s,t,u,v).
333+ net shape: (A, B, C, D). Returns float."""
334+ cur = np .asarray (net , dtype = np .float64 )
335+ cur = _decasteljau_1axis (cur , s ) # (B, C, D)
336+ cur = _decasteljau_1axis (cur , t ) # (C, D)
337+ cur = _decasteljau_1axis (cur , u ) # (D,)
338+ omt = 1.0 - v
339+ for _ in range (cur .shape [0 ] - 1 ):
340+ cur = omt * cur [:- 1 ] + v * cur [1 :]
341+ return float (cur [0 ])
342+
343+ def _fast_beval_vec3_2d (net , s , t ):
344+ """Evaluate a 2D Bernstein net with 3D vector values at float point (s,t).
345+ net shape: (A, B, 3). Returns ndarray of shape (3,)."""
346+ cur = np .asarray (net , dtype = np .float64 )
347+ cur = _decasteljau_1axis (cur , s ) # (B, 3)
348+ omt = 1.0 - t
349+ for _ in range (cur .shape [0 ] - 1 ):
350+ cur = omt * cur [:- 1 ] + t * cur [1 :]
351+ return cur [0 ] # shape (3,)
352+
319353# ------------------------------------------------------------
320354# Bézier derivatives for surfaces (tensor-product)
321355# P: (m+1,n+1,3)
@@ -375,12 +409,35 @@ def __post_init__(self):
375409 d3 = bernstein_derivative_nd (Ti , axis = 3 )
376410 self .dT .append ((d0 ,d1 ,d2 ,d3 ))
377411
412+ # Precompute float64 copies for fast point evaluation.
413+ # The originals may be interval-dtype; these are midpoint extractions.
414+ def _to_f64 (arr ):
415+ a = np .asarray (arr )
416+ try :
417+ return a .astype (np .float64 )
418+ except (TypeError , ValueError ):
419+ # interval dtype — extract midpoints
420+ from mmcore .numeric .ndinterval import get_lu
421+ lo , hi = get_lu (a )
422+ return 0.5 * (lo + hi )
423+
424+ self ._P1_f = _to_f64 (self .P1 )
425+ self ._P2_f = _to_f64 (self .P2 )
426+ self ._P1s_f = _to_f64 (self .P1_s )
427+ self ._P1t_f = _to_f64 (self .P1_t )
428+ self ._P2u_f = _to_f64 (self .P2_u )
429+ self ._P2v_f = _to_f64 (self .P2_v )
430+ self ._T_f = [_to_f64 (Ti ) for Ti in self .T ]
431+ self ._dT_f = []
432+ for grads in self .dT :
433+ self ._dT_f .append (tuple (_to_f64 (g ) if g is not None else None for g in grads ))
434+
378435 # ---- Ψ evaluation
379436
380437 def psi_point (self , x ): # x float[4]
381438 s ,t ,u ,v = x
382- r1 = _to_float_vec3 ( _beval_vec3 ( self .P1 , ( s , t ), self . bern_eval ) )
383- r2 = _to_float_vec3 ( _beval_vec3 ( self .P2 , ( u , v ), self . bern_eval ) )
439+ r1 = _fast_beval_vec3_2d ( self ._P1_f , s , t )
440+ r2 = _fast_beval_vec3_2d ( self ._P2_f , u , v )
384441 return r1 - r2
385442
386443 def psi_box (self , B ): # B float bounds
@@ -398,11 +455,10 @@ def psi_box(self, B): # B float bounds
398455
399456 def T_point (self , x ): # returns 4 floats
400457 s ,t ,u ,v = x
401- vals = []
402- for Ti in self .T :
403- val = _beval_scalar (Ti , (s ,t ,u ,v ), self .bern_eval )
404- vals .append (_to_float_scalar (val ))
405- return np .array (vals , dtype = float )
458+ vals = np .empty (4 , dtype = float )
459+ for i , Tf in enumerate (self ._T_f ):
460+ vals [i ] = _fast_beval_scalar_4d (Tf , s , t , u , v )
461+ return vals
406462
407463 def T_box (self , B ): # returns 4 intervals
408464 params = _box_to_interval_params (B , self .interval_ctor )
@@ -428,30 +484,25 @@ def delta_box(self, B):
428484 def jac_point (self , x ):
429485 s ,t ,u ,v = x
430486 # Ψ Jacobian columns from surface partials
431- a = _to_float_vec3 ( _beval_vec3 ( self .P1_s , ( s , t ), self . bern_eval ) ) # dR1/ds
432- b = _to_float_vec3 ( _beval_vec3 ( self .P1_t , ( s , t ), self . bern_eval ) ) # dR1/dt
433- c = _to_float_vec3 ( _beval_vec3 ( self .P2_u , ( u , v ), self . bern_eval ) ) # dR2/du
434- d = _to_float_vec3 ( _beval_vec3 ( self .P2_v , ( u , v ), self . bern_eval ) ) # dR2/dv
487+ a = _fast_beval_vec3_2d ( self ._P1s_f , s , t ) # dR1/ds
488+ b = _fast_beval_vec3_2d ( self ._P1t_f , s , t ) # dR1/dt
489+ c = _fast_beval_vec3_2d ( self ._P2u_f , u , v ) # dR2/du
490+ d = _fast_beval_vec3_2d ( self ._P2v_f , u , v ) # dR2/dv
435491
436492 J = np .zeros ((7 ,4 ), dtype = float )
437493 # rows 0..2 correspond to Ψx,Ψy,Ψz
438- for k in range (3 ):
439- J [k ,0 ] = a [k ]
440- J [k ,1 ] = b [k ]
441- J [k ,2 ] = - c [k ]
442- J [k ,3 ] = - d [k ]
494+ J [0 :3 , 0 ] = a
495+ J [0 :3 , 1 ] = b
496+ J [0 :3 , 2 ] = - c
497+ J [0 :3 , 3 ] = - d
443498
444499 # rows 3..6 are gradients of T1..T4
445500 for i in range (4 ):
446- d0 ,d1 ,d2 ,d3 = self .dT [i ]
447- if d0 is None : J [3 + i ,0 ]= 0.0
448- else : J [3 + i ,0 ]= _to_float_scalar (_beval_scalar (d0 , (s ,t ,u ,v ), self .bern_eval ))
449- if d1 is None : J [3 + i ,1 ]= 0.0
450- else : J [3 + i ,1 ]= _to_float_scalar (_beval_scalar (d1 , (s ,t ,u ,v ), self .bern_eval ))
451- if d2 is None : J [3 + i ,2 ]= 0.0
452- else : J [3 + i ,2 ]= _to_float_scalar (_beval_scalar (d2 , (s ,t ,u ,v ), self .bern_eval ))
453- if d3 is None : J [3 + i ,3 ]= 0.0
454- else : J [3 + i ,3 ]= _to_float_scalar (_beval_scalar (d3 , (s ,t ,u ,v ), self .bern_eval ))
501+ d0 ,d1 ,d2 ,d3 = self ._dT_f [i ]
502+ J [3 + i , 0 ] = 0.0 if d0 is None else _fast_beval_scalar_4d (d0 , s , t , u , v )
503+ J [3 + i , 1 ] = 0.0 if d1 is None else _fast_beval_scalar_4d (d1 , s , t , u , v )
504+ J [3 + i , 2 ] = 0.0 if d2 is None else _fast_beval_scalar_4d (d2 , s , t , u , v )
505+ J [3 + i , 3 ] = 0.0 if d3 is None else _fast_beval_scalar_4d (d3 , s , t , u , v )
455506 return J
456507
457508 # ---- Interval Jacobian row for equation idx in {0..6}
@@ -926,65 +977,13 @@ def analyse_deflated_system(
926977
927978 if dim == 1 :
928979 # Infinite solutions of Δ_B in B: singular/tangent curve case.
980+ # The witness point and dimension classification are sufficient for
981+ # trace_gamma to trace the actual curve. The expensive cover
982+ # computation and Krawczyk-based curve sampling are omitted here;
983+ # they can be requested separately if needed.
929984 out ["status" ] = "infinite"
930-
931- # (A) Produce a "cover" of Δ_B by small boxes (subdivide + interval pruning).
932- cover = []
933- stack = [(Bf , 0 )]
934- while stack :
935- Bb , depth = stack .pop ()
936- if depth >= cover_max_depth or _box_max_width (Bb ) <= cover_min_width :
937- # keep if still possible
938- fullI = sys .delta_box (Bb )
939- if all (_iv_contains0 (I ) for I in fullI ):
940- cover .append (Bb )
941- continue
942- fullI = sys .delta_box (Bb )
943- if any (not _iv_contains0 (I ) for I in fullI ):
944- continue
945- B1 ,B2 = _box_split (Bb )
946- stack .append ((B1 , depth + 1 ))
947- stack .append ((B2 , depth + 1 ))
948- out ["cover_boxes" ] = cover
949-
950- # (B) Sample the curve by slicing hyperplanes x_i = const (paper's L trick, but deterministic). :contentReference[oaicite:8]{index=8}
951- axis = _box_widest_axis (Bf )
952- lo , hi = Bf [axis ]
953- if hi > lo :
954- samples = []
955- for k in range (curve_slice_count ):
956- val = lo + (k + 0.5 )/ curve_slice_count * (hi - lo )
957- a = np .zeros (4 ); a [axis ] = 1.0
958- b = - float (val )
959- hyp = {"a" : a , "b" : b }
960-
961- # augmented Jacobian at witness for best subset that includes hyperplane row (index 7)
962- J7 = sys .jac_point (xw )
963- J8 = np .vstack ([J7 , a .reshape (1 ,4 )])
964- candidates = choose_best_subset (J8 , require = 7 , eq_count = 8 , top_k = 3 )
965-
966- # narrow the box in that axis to help isolation
967- Bb = list (Bf )
968- half = 0.5 * (hi - lo )/ curve_slice_count
969- Bb [axis ] = (max (lo , val - half ), min (hi , val + half ))
970- Bb = tuple (Bb )
971-
972- for sub in candidates :
973- sys4 = build_square_from_subset (sys , sub , hyperplane = hyp )
974- boxes , _ = isolate_roots_krawczyk (sys4 , Bb , max_depth = 14 , min_width = max (point_min_width , half * 0.2 ))
975- for rootB in boxes :
976- xm = _box_mid (rootB )
977- # Verify full Δ at xm numerically
978- fn = np .linalg .norm (sys .delta_point (xm ))
979- if fn < 1e-6 :
980- xyz = _to_float_vec3 (_beval_vec3 (np .asarray (P1 ), (xm [0 ],xm [1 ]), bern_eval ))
981- samples .append ({"param" : tuple (map (float ,xm )), "xyz" : tuple (map (float ,xyz ))})
982- if samples :
983- break
984- # sort samples along the slicing axis for a crude polyline
985- samples .sort (key = lambda d : d ["param" ][axis ])
986- out ["curve_samples" ] = samples
987-
985+ out ["cover_boxes" ] = []
986+ out ["curve_samples" ] = []
988987 return out
989988
990989 # dim == 2
0 commit comments