Skip to content

Commit 6aafa77

Browse files
authored
Merge pull request #628 from PyAutoLabs/feature/potential-correction-uv-campaign
feat: per-iteration evidence re-optimization for the interferometer LM engine
2 parents 231a7bc + 91e49e5 commit 6aafa77

1 file changed

Lines changed: 118 additions & 3 deletions

File tree

autolens/potential_correction/iterative_interferometer.py

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)