Skip to content

Commit f2f3bb2

Browse files
authored
Merge pull request #619 from PyAutoLabs/feature/slam-resume-fastpath
feat: cache solved multiple-image positions for SLaM resume fast-path
2 parents b9c36cd + 8028336 commit f2f3bb2

2 files changed

Lines changed: 105 additions & 5 deletions

File tree

autolens/analysis/result.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
1616
These results feed directly into downstream pipeline stages and post-processing scripts.
1717
"""
18+
import json
1819
import logging
1920
import os
2021
import numpy as np
@@ -267,6 +268,59 @@ def positions_threshold_from(
267268

268269
return threshold
269270

271+
def _cached_multiple_image_positions_from(
272+
self, plane_redshift: Optional[float] = None
273+
) -> aa.Grid2DIrregular:
274+
"""
275+
The multiple image positions of the maximum log likelihood lens model, loaded
276+
from this result's own ``files/`` folder when previously solved, else solved
277+
via the point solver and persisted there.
278+
279+
Solving the multiple image positions runs a point-solver grid search over the
280+
maximum log likelihood tracer, which on a resumed pipeline (e.g. SLaM) pays a
281+
fresh JIT compile and dominates resume overhead (autolens_profiling#70) — the
282+
solved positions themselves are a pure product of the completed fit, so they
283+
are cached as ``files/multiple_image_positions[_plane_<z>].json``. Staleness
284+
is structurally guarded: a changed model or search produces a new search
285+
identifier and a fresh output directory with no cache file. Results with no
286+
on-disk output (e.g. ``NullPaths``) always solve.
287+
"""
288+
from pathlib import Path
289+
290+
name = "multiple_image_positions"
291+
if plane_redshift is not None:
292+
name += f"_plane_{str(plane_redshift).replace('.', '_')}"
293+
294+
files_path = getattr(getattr(self, "paths", None), "_files_path", None)
295+
cache_path = (
296+
Path(files_path) / f"{name}.json"
297+
if files_path is not None and Path(files_path).is_dir()
298+
else None
299+
)
300+
301+
if cache_path is not None and cache_path.exists():
302+
with open(cache_path) as f:
303+
return aa.Grid2DIrregular(values=[tuple(p) for p in json.load(f)])
304+
305+
positions = self.image_plane_multiple_image_positions(
306+
plane_redshift=plane_redshift
307+
)
308+
309+
if cache_path is not None:
310+
with open(cache_path, "w") as f:
311+
json.dump(np.asarray(positions.array).tolist(), f)
312+
313+
# Preserve the cache in the search's zip — a resumed search's
314+
# paths.restore() wipes the output dir and re-extracts the zip,
315+
# destroying any file written only to files/ after completion.
316+
from autogalaxy.analysis.adapt_images.adapt_images import (
317+
_append_to_search_zip,
318+
)
319+
320+
_append_to_search_zip(self.paths, cache_path)
321+
322+
return positions
323+
270324
def positions_likelihood_from(
271325
self,
272326
factor=1.0,
@@ -355,11 +409,10 @@ def positions_likelihood_from(
355409
)
356410
return
357411

358-
positions = (
359-
self.image_plane_multiple_image_positions(plane_redshift=plane_redshift)
360-
if positions is None
361-
else positions
362-
)
412+
if positions is None:
413+
positions = self._cached_multiple_image_positions_from(
414+
plane_redshift=plane_redshift
415+
)
363416

364417
if mass_centre_radial_distance_min is not None:
365418
mass_centre = self.max_log_likelihood_tracer.extract_attribute(

test_autolens/analysis/test_result.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,50 @@ def test___image_dict(analysis_imaging_7x7):
380380

381381
assert (image_dict[str(("galaxies", "lens"))].native == np.zeros((7, 7))).all()
382382
assert isinstance(image_dict[str(("galaxies", "source"))], Array2D)
383+
384+
385+
def test__positions_likelihood_from__loads_cached_positions_on_second_call(
386+
tmp_path, monkeypatch, analysis_imaging_7x7
387+
):
388+
class _StubPaths:
389+
def __init__(self, files_path):
390+
self._files_path = files_path
391+
392+
tracer = al.Tracer(
393+
galaxies=[
394+
al.Galaxy(
395+
redshift=0.5,
396+
mass=al.mp.Isothermal(
397+
centre=(0.1, 0.0), einstein_radius=1.0, ell_comps=(0.0, 0.0)
398+
),
399+
),
400+
al.Galaxy(redshift=1.0, bulge=al.lp.SersicSph(centre=(0.0, 0.0))),
401+
]
402+
)
403+
404+
samples_summary = al.m.MockSamplesSummary(max_log_likelihood_instance=tracer)
405+
406+
result = res.Result(samples_summary=samples_summary, analysis=analysis_imaging_7x7)
407+
result.paths = _StubPaths(files_path=tmp_path)
408+
409+
first = result.positions_likelihood_from(factor=0.1, minimum_threshold=0.2)
410+
411+
assert (tmp_path / "multiple_image_positions.json").exists()
412+
413+
# The second call must load the cached positions — solving again raises.
414+
def _poison(*args, **kwargs):
415+
raise AssertionError("point solver re-ran — cached positions not used")
416+
417+
result_cached = res.Result(
418+
samples_summary=samples_summary, analysis=analysis_imaging_7x7
419+
)
420+
result_cached.paths = _StubPaths(files_path=tmp_path)
421+
monkeypatch.setattr(
422+
result_cached, "image_plane_multiple_image_positions", _poison
423+
)
424+
425+
second = result_cached.positions_likelihood_from(factor=0.1, minimum_threshold=0.2)
426+
427+
assert isinstance(second, al.PositionsLH)
428+
assert second.positions.array == pytest.approx(first.positions.array, 1.0e-8)
429+
assert second.threshold == pytest.approx(first.threshold, 1.0e-8)

0 commit comments

Comments
 (0)