@@ -57,6 +57,8 @@ def __init__(
5757 preloads : Optional [dict ] = None ,
5858 n_iter : int = 20 ,
5959 tol : float = 1e-6 ,
60+ damping : str = "identity" ,
61+ max_consecutive_rejections : int = 10 ,
6062 verbose : bool = False ,
6163 visualize_output_dir : Optional [str ] = None ,
6264 visualize_every_n : int = 1000000 ,
@@ -98,7 +100,22 @@ def __init__(
98100 n_iter
99101 The maximum number of outer LM iterations.
100102 tol
101- The step-norm convergence tolerance.
103+ The step-norm convergence tolerance. Also applied to rejected
104+ steps: once a proposed step is smaller than ``tol``, growing the
105+ damping can only shrink it further, so the solve returns rather
106+ than rejecting its way to the mu ceiling.
107+ damping
108+ The LM damping matrix (``dense_util.solve_lm_step_from``):
109+ ``"identity"`` (default) is the reference implementation's
110+ ``H + mu I`` — near Gauss-Newton early steps, converging the
111+ imaging problem in a few iterations from a cold start;
112+ ``"marquardt"`` is the scale-invariant ``H + mu diag(H)``, whose
113+ conservative steps need a much larger iteration budget.
114+ max_consecutive_rejections
115+ Stop after this many consecutive rejected trial steps (each costs
116+ a full Jacobian rebuild); at a cost minimum no decreasing step
117+ exists and unbounded rejection wastes the runtime driving mu to
118+ its ceiling.
102119 verbose
103120 Whether to log per-iteration costs.
104121 visualize_output_dir
@@ -115,6 +132,8 @@ def __init__(
115132 self .src_image_mesh = src_image_mesh
116133 self .n_iter = int (n_iter )
117134 self .tol = float (tol )
135+ self .damping = str (damping )
136+ self .max_consecutive_rejections = int (max_consecutive_rejections )
118137 self .verbose = bool (verbose )
119138 self .visualize_output_dir = visualize_output_dir
120139 self .visualize_every_n = int (visualize_every_n )
@@ -486,12 +505,14 @@ def solve_joint_optimization(self, xp=np, x0=None, gauge_project_x0=False):
486505 )
487506
488507 step_accepted = False
508+ consecutive_rejections = 0
489509 while not step_accepted :
490510 delta_x = None
491511 try :
492512 delta_x = dense_util .solve_lm_step_from (
493513 H , minus_gradient , mu ,
494514 constraint_matrix = constraint_matrix , x = x , xp = xp ,
515+ damping = self .damping ,
495516 )
496517 if np .any (np .isnan (np .asarray (delta_x ))):
497518 delta_x = None
@@ -530,6 +551,30 @@ def solve_joint_optimization(self, xp=np, x0=None, gauge_project_x0=False):
530551 self .dpsi_opt = np .asarray (x [n_s :])
531552 return self .s_opt , self .dpsi_opt
532553 else :
554+ # rejected step below the step tolerance: growing mu
555+ # only shrinks it further — the state is converged
556+ # (at a cost minimum no decreasing step exists), so
557+ # return instead of rejecting to the mu ceiling.
558+ if float (xp .linalg .norm (delta_x )) < self .tol :
559+ if self .verbose :
560+ logger .info (
561+ "Converged at iteration %d (rejected step "
562+ "below tolerance)." ,
563+ i ,
564+ )
565+ self .s_opt = np .asarray (x [:n_s ])
566+ self .dpsi_opt = np .asarray (x [n_s :])
567+ return self .s_opt , self .dpsi_opt
568+ consecutive_rejections += 1
569+ if consecutive_rejections >= self .max_consecutive_rejections :
570+ logger .warning (
571+ "%d consecutive rejected LM steps (each a full "
572+ "Jacobian rebuild); stopping at the current state." ,
573+ consecutive_rejections ,
574+ )
575+ self .s_opt = np .asarray (x [:n_s ])
576+ self .dpsi_opt = np .asarray (x [n_s :])
577+ return self .s_opt , self .dpsi_opt
533578 mu *= 5.0
534579 if mu > 1e15 :
535580 logger .warning (
@@ -539,6 +584,16 @@ def solve_joint_optimization(self, xp=np, x0=None, gauge_project_x0=False):
539584 self .dpsi_opt = np .asarray (x [n_s :])
540585 return self .s_opt , self .dpsi_opt
541586 else :
587+ consecutive_rejections += 1
588+ if consecutive_rejections >= self .max_consecutive_rejections :
589+ logger .warning (
590+ "%d consecutive failed LM solves; stopping at the "
591+ "current state." ,
592+ consecutive_rejections ,
593+ )
594+ self .s_opt = np .asarray (x [:n_s ])
595+ self .dpsi_opt = np .asarray (x [n_s :])
596+ return self .s_opt , self .dpsi_opt
542597 mu *= 5.0
543598 if mu > 1e15 :
544599 logger .warning ("LM solver failed repeatedly; stopping." )
0 commit comments