@@ -70,6 +70,8 @@ def __init__(
7070 preloads : Optional [dict ] = None ,
7171 n_iter : int = 20 ,
7272 tol : float = 1e-6 ,
73+ reg_optimize_every : Optional [int ] = None ,
74+ reg_optimize_grid : int = 5 ,
7375 verbose : bool = False ,
7476 ):
7577 """
@@ -118,6 +120,18 @@ def __init__(
118120 The maximum number of outer LM iterations.
119121 tol
120122 The step-norm convergence tolerance.
123+ reg_optimize_every
124+ When set, every N accepted outer iterations (and once at the
125+ start) the regularization strength multipliers (a_src, a_dpsi)
126+ are re-optimized by maximising the Laplace evidence at the
127+ current normal equations — the per-iteration objective control
128+ of Koopmans 2005 / Vegetti & Koopmans 2009 that prevents the
129+ source block absorbing potential perturbations. Each candidate
130+ costs one Cholesky solve at fixed F, D.
131+ reg_optimize_grid
132+ The number of log-spaced multipliers per axis of the
133+ re-optimization grid (spanning 1e-2..1e2 around the current
134+ scales).
121135 verbose
122136 Whether to log per-iteration costs.
123137 """
@@ -130,6 +144,9 @@ def __init__(
130144 self .dpsi_mask = dpsi_mask
131145 self .n_iter = int (n_iter )
132146 self .tol = float (tol )
147+ self .reg_optimize_every = reg_optimize_every
148+ self .reg_optimize_grid = int (reg_optimize_grid )
149+ self .reg_scales = (1.0 , 1.0 )
133150 self .verbose = bool (verbose )
134151
135152 if settings_inversion is None :
@@ -402,6 +419,67 @@ def _cost_from(self, x, F, D, R):
402419 reg = 0.5 * float (x @ (R @ x ))
403420 return chi2 + reg , chi2 , reg
404421
422+ def _effective_regularization (self ):
423+ """The block-diagonal regularization at the current strength scales."""
424+ a_s , a_d = self .reg_scales
425+ return dense_util .dense_block_diag_from (
426+ a_s * dense_util .as_dense (self .src_reg_mat ),
427+ a_d * np .asarray (self .dpsi_regularization_matrix ),
428+ )
429+
430+ def _optimize_regularization (self , F , D ):
431+ """
432+ Re-optimizes the regularization strength multipliers (a_src, a_dpsi)
433+ by maximising the Laplace evidence at the current (fixed) normal
434+ equations F, D — the objective control of Koopmans 2005 / Vegetti &
435+ Koopmans 2009. Returns the new scales and the solution under them.
436+ """
437+ n_s = self .src_reg_mat .shape [0 ]
438+ R_s = dense_util .as_dense (self .src_reg_mat )
439+ R_d = np .asarray (self .dpsi_regularization_matrix )
440+ n_d = R_d .shape [0 ]
441+ logdet_Rs = pc_util .log_det_mat (R_s , sparse = False )
442+ try :
443+ logdet_Rd = pc_util .log_det_mat (R_d , sparse = False )
444+ except np .linalg .LinAlgError :
445+ logdet_Rd = pc_util .log_det_mat (R_d , sparse = True )
446+
447+ candidates = np .logspace (- 2.0 , 2.0 , self .reg_optimize_grid )
448+ best = None
449+ for a_s in candidates * self .reg_scales [0 ]:
450+ for a_d in candidates * self .reg_scales [1 ]:
451+ R = dense_util .dense_block_diag_from (a_s * R_s , a_d * R_d )
452+ curve = F + R
453+ try :
454+ L_chol = np .linalg .cholesky (curve )
455+ except np .linalg .LinAlgError :
456+ continue
457+ from scipy .linalg import cho_solve
458+
459+ x = cho_solve ((L_chol , True ), D )
460+ logdet_curve = 2.0 * float (np .sum (np .log (np .diag (L_chol ))))
461+ chi2 = (
462+ self .data_weighted_norm
463+ - 2.0 * float (x @ D )
464+ + float (x @ (F @ x ))
465+ )
466+ reg_pen = float (x @ (R @ x ))
467+ evidence = 0.5 * (
468+ (n_s * np .log (a_s ) + logdet_Rs )
469+ + (n_d * np .log (a_d ) + logdet_Rd )
470+ - logdet_curve
471+ - chi2
472+ - reg_pen
473+ )
474+ if best is None or evidence > best [0 ]:
475+ best = (evidence , a_s , a_d , x )
476+ if best is None :
477+ raise exc .InversionException (
478+ "Regularization re-optimization found no positive-definite "
479+ "candidate."
480+ )
481+ return best
482+
405483 # -----------------------------
406484 # The Levenberg-Marquardt loop
407485 # -----------------------------
@@ -419,7 +497,7 @@ def solve_joint_optimization(self):
419497 n_s = self .src_reg_mat .shape [0 ]
420498
421499 x = np .zeros (n_s + n_dpsi )
422- R = np .asarray (self ._regularization_matrix ())
500+ R = np .asarray (self ._effective_regularization ())
423501
424502 constraint_matrix = None
425503 if self .gauge_constraints :
@@ -430,6 +508,19 @@ def solve_joint_optimization(self):
430508 constraint_matrix = np .hstack ([np .zeros ((3 , n_s )), G_c ])
431509
432510 F , D , A = self ._normal_equations_from (x [:n_s ], x [n_s :])
511+
512+ if self .reg_optimize_every is not None :
513+ ev , a_s , a_d , x_star = self ._optimize_regularization (F , D )
514+ self .reg_scales = (a_s , a_d )
515+ R = np .asarray (self ._effective_regularization ())
516+ x = np .asarray (x_star )
517+ F , D , A = self ._normal_equations_from (x [:n_s ], x [n_s :])
518+ if self .verbose :
519+ logger .info (
520+ "reg re-optimization: a_src=%.3e a_dpsi=%.3e evidence=%.4e" ,
521+ a_s , a_d , ev ,
522+ )
523+
433524 current_cost , chi2 , reg = self ._cost_from (x , F , D , R )
434525
435526 mu = 1.0
@@ -480,6 +571,23 @@ def solve_joint_optimization(self):
480571 mu = max (1e-15 , mu / 3.0 )
481572 step_accepted = True
482573
574+ if (
575+ self .reg_optimize_every is not None
576+ and (i + 1 ) % self .reg_optimize_every == 0
577+ ):
578+ ev , a_s , a_d , x_star = self ._optimize_regularization (F , D )
579+ self .reg_scales = (a_s , a_d )
580+ R = np .asarray (self ._effective_regularization ())
581+ x = np .asarray (x_star )
582+ F , D , A = self ._normal_equations_from (x [:n_s ], x [n_s :])
583+ current_cost , chi2 , reg = self ._cost_from (x , F , D , R )
584+ if self .verbose :
585+ logger .info (
586+ "reg re-optimization @iter %d: a_src=%.3e "
587+ "a_dpsi=%.3e evidence=%.4e cost=%.4e" ,
588+ i , a_s , a_d , ev , current_cost ,
589+ )
590+
483591 if float (np .linalg .norm (delta_x )) < self .tol :
484592 if self .verbose :
485593 logger .info (
@@ -534,12 +642,18 @@ def log_evidence(
534642
535643 x = np .concatenate ([np .asarray (s ), np .asarray (dpsi )])
536644 F , D , A = self ._normal_equations_from (s , dpsi )
537- R = np .asarray (self ._regularization_matrix ())
645+ R = np .asarray (self ._effective_regularization ())
538646
539647 chi2 = self .data_weighted_norm - 2.0 * float (x @ D ) + float (x @ (F @ x ))
540648 reg_val = float (x @ (R @ x ))
541649
542- logdet_Rs = pc_util .log_det_mat (self .src_reg_mat , sparse = True )
650+ a_s , a_d = self .reg_scales
651+ n_src = self .src_reg_mat .shape [0 ]
652+ n_dpsi = np .asarray (self .dpsi_regularization_matrix ).shape [0 ]
653+ logdet_Rs = (
654+ pc_util .log_det_mat (self .src_reg_mat , sparse = True )
655+ + n_src * np .log (a_s )
656+ )
543657 try :
544658 sign , logdet_Rd = np .linalg .slogdet (
545659 np .asarray (self .dpsi_regularization_matrix )
@@ -552,6 +666,7 @@ def log_evidence(
552666 logdet_Rd = pc_util .log_det_mat (
553667 self .dpsi_regularization_matrix , sparse = True
554668 )
669+ logdet_Rd = logdet_Rd + n_dpsi * np .log (a_d )
555670
556671 sign , logdet_H = np .linalg .slogdet (F + R )
557672 if sign != 1 :
0 commit comments