Skip to content

Commit 74bb13f

Browse files
authored
Merge pull request #632 from PyAutoLabs/fix/potential-correction-warm-start-imaging
Add opt-in gauge_project_x0 to the imaging iterative warm start
2 parents ff5f072 + 3fca005 commit 74bb13f

2 files changed

Lines changed: 92 additions & 4 deletions

File tree

autolens/potential_correction/iterative.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,33 @@ def _init_joint_optimization(self):
354354
# The Levenberg-Marquardt loop
355355
# -----------------------------
356356

357-
def solve_joint_optimization(self, xp=np, x0=None):
357+
def _gauge_project_dpsi(self, dpsi_vec: np.ndarray) -> np.ndarray:
358+
"""
359+
Project a dpsi vector onto the gauge-constrained subspace
360+
<dpsi, 1> = <dpsi, x> = <dpsi, y> = 0 by removing its constant and
361+
linear (deflection-drift) modes.
362+
363+
These modes are the null space of the Hamiltonian dpsi -> dkappa
364+
map, so the projection leaves the physical convergence correction
365+
unchanged. It is needed when warm-starting a ``gauge_constraints``
366+
solve from an un-gauged ``x0`` (e.g. the one-shot
367+
``FitDpsiSrcImaging`` solution, which is not gauge-fixed). Each
368+
accepted LM step re-imposes the gauge on the updated state
369+
(C (x + dx) = 0), but from a near-optimal un-gauged start the
370+
gauge-fixing move slides along the model-degenerate constant /
371+
deflection-drift modes and so is not cost-decreasing: it is rejected
372+
and the solve stalls off the constraint surface. Projecting ``x0``
373+
up front places the warm start on the surface, where a cold start
374+
from zero already begins.
375+
"""
376+
dpsi_vec = np.asarray(dpsi_vec, dtype=float)
377+
x = self.dpsi_points[:, 1]
378+
y = self.dpsi_points[:, 0]
379+
basis = np.column_stack([np.ones_like(x), x, y])
380+
coeffs, *_ = np.linalg.lstsq(basis, dpsi_vec, rcond=None)
381+
return dpsi_vec - basis @ coeffs
382+
383+
def solve_joint_optimization(self, xp=np, x0=None, gauge_project_x0=False):
358384
"""
359385
Runs the Levenberg-Marquardt loop on the combined state
360386
x = [s | dpsi], accepting steps that decrease the penalized cost and
@@ -373,17 +399,32 @@ def solve_joint_optimization(self, xp=np, x0=None):
373399
the one-shot ``FitDpsiSrcImaging``): the LM then refines inside
374400
that basin rather than searching from zero — the recipe
375401
certified by the interferometer uv campaign (PyAutoLens#627).
402+
gauge_project_x0
403+
When warm-starting a ``gauge_constraints`` solve, project the
404+
dpsi component of ``x0`` onto the gauge subspace
405+
<dpsi, 1> = <dpsi, x> = <dpsi, y> = 0 before iterating (see
406+
:meth:`_gauge_project_dpsi`). Defaults to ``False`` (``x0`` used
407+
verbatim); set ``True`` when ``x0`` is not itself gauge-fixed —
408+
a near-optimal un-gauged start cannot reach the constraint
409+
surface without a non-cost-decreasing move, which the LM
410+
rejects, so it would otherwise stall with the gauge violated.
411+
The dkappa recovery is unaffected by the projection.
376412
"""
377413
n_s, n_dpsi = self._init_joint_optimization()
378414

379415
if x0 is None:
380416
x = xp.zeros(n_s + n_dpsi)
381417
else:
382-
x = xp.asarray(np.asarray(x0, dtype=float))
383-
if x.shape != (n_s + n_dpsi,):
418+
x0 = np.asarray(x0, dtype=float)
419+
if x0.shape != (n_s + n_dpsi,):
384420
raise ValueError(
385-
f"x0 has shape {x.shape}; expected ({n_s + n_dpsi},)"
421+
f"x0 has shape {x0.shape}; expected ({n_s + n_dpsi},)"
422+
)
423+
if gauge_project_x0:
424+
x0 = np.concatenate(
425+
[x0[:n_s], self._gauge_project_dpsi(x0[n_s:])]
386426
)
427+
x = xp.asarray(x0)
387428

388429
data_slim = xp.asarray(np.asarray(self.masked_imaging.data.slim))
389430
inv_var = xp.asarray(self.inverse_noise_variance)

test_autolens/potential_correction/test_iterative.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,53 @@ def test__solve_joint_optimization__gauge_constraints_are_satisfied(
7171
assert G @ dpsi_opt == pytest.approx(np.zeros(3), abs=1.0e-6)
7272

7373

74+
def test__gauge_project_dpsi__zeroes_the_three_functionals(masked_imaging_7x7):
75+
fit = iter_fit_from(masked_imaging_7x7, gauge_constraints=True)
76+
fit._init_joint_optimization()
77+
78+
x = fit.dpsi_points[:, 1]
79+
y = fit.dpsi_points[:, 0]
80+
n_dpsi = fit.dpsi_points.shape[0]
81+
82+
# an arbitrary field carrying a deliberate constant + linear (gauge) part
83+
rng = np.random.default_rng(0)
84+
dpsi = rng.standard_normal(n_dpsi) + 4.0 + 2.0 * x - 3.0 * y
85+
86+
projected = fit._gauge_project_dpsi(dpsi)
87+
88+
functionals = np.array(
89+
[np.sum(projected), np.sum(x * projected), np.sum(y * projected)]
90+
)
91+
assert functionals == pytest.approx(np.zeros(3), abs=1.0e-8)
92+
# removing only constant + linear modes: a already-gauged field is unchanged
93+
assert fit._gauge_project_dpsi(projected) == pytest.approx(projected, abs=1.0e-8)
94+
95+
96+
def test__solve_joint_optimization__gauge_project_x0__gauges_an_ungauged_warm_start(
97+
masked_imaging_7x7,
98+
):
99+
# a cold solve gives a gauge-satisfied optimum; offset its dpsi by a pure
100+
# constant to build a warm start that sits on the optimum but violates the
101+
# <dpsi,1> constraint (an un-gauged x0, as the one-shot solution is).
102+
fit0 = iter_fit_from(masked_imaging_7x7, gauge_constraints=True)
103+
s_opt, dpsi_opt = fit0.solve_joint_optimization()
104+
x0 = np.concatenate([s_opt, dpsi_opt + 0.7])
105+
106+
def gauge_of(fit, dpsi):
107+
n_dpsi = dpsi.shape[0]
108+
G = np.zeros((3, n_dpsi))
109+
G[0, :] = 1.0 / n_dpsi
110+
G[1, :] = fit.dpsi_points[:, 1] / n_dpsi
111+
G[2, :] = fit.dpsi_points[:, 0] / n_dpsi
112+
return G @ dpsi
113+
114+
assert np.abs(gauge_of(fit0, dpsi_opt + 0.7)[0]) > 1.0e-2 # x0 is un-gauged
115+
116+
fit_on = iter_fit_from(masked_imaging_7x7, gauge_constraints=True)
117+
_, dpsi_on = fit_on.solve_joint_optimization(x0=x0, gauge_project_x0=True)
118+
assert gauge_of(fit_on, dpsi_on) == pytest.approx(np.zeros(3), abs=1.0e-6)
119+
120+
74121
def test__log_evidence__finite_at_optimum(masked_imaging_7x7):
75122
fit = iter_fit_from(masked_imaging_7x7)
76123

0 commit comments

Comments
 (0)