1414"""
1515import logging
1616
17+ import autoarray as aa
1718import autofit as af
1819import autogalaxy as ag
1920
@@ -39,7 +40,48 @@ class AnalysisImaging(AnalysisDataset):
3940 Visualizer = VisualizerImaging
4041 Latent = LatentLens
4142
42- def log_likelihood_function (self , instance : af .ModelInstance ) -> float :
43+ def __init__ (
44+ self ,
45+ dataset ,
46+ positions_likelihood_list = None ,
47+ adapt_images : ag .AdaptImages = None ,
48+ cosmology : ag .cosmo .LensingCosmology = None ,
49+ settings = None ,
50+ raise_inversion_positions_likelihood_exception : bool = True ,
51+ title_prefix : str = None ,
52+ use_jax : bool = True ,
53+ shared_preloads : bool = False ,
54+ ** kwargs ,
55+ ):
56+ """
57+ Fits a lens model to an imaging dataset via a non-linear search (see `AnalysisDataset` for
58+ the full docstring of the shared parameters).
59+
60+ Parameters
61+ ----------
62+ shared_preloads
63+ Opts this analysis into the cross-factor shared-state mechanism of a `FactorGraphModel`
64+ (see `shared_state_from`). Set this to `True` only when this analysis is one of many
65+ exposures of the same lens (e.g. multi-exposure imaging with per-exposure pixel offsets)
66+ sharing an identical lens model, so the exposure-invariant source-plane mesh geometry
67+ can be computed once and reused by every exposure. `False` by default, leaving the
68+ standard per-analysis behaviour unchanged.
69+ """
70+ super ().__init__ (
71+ dataset = dataset ,
72+ positions_likelihood_list = positions_likelihood_list ,
73+ adapt_images = adapt_images ,
74+ cosmology = cosmology ,
75+ settings = settings ,
76+ raise_inversion_positions_likelihood_exception = raise_inversion_positions_likelihood_exception ,
77+ title_prefix = title_prefix ,
78+ use_jax = use_jax ,
79+ ** kwargs ,
80+ )
81+
82+ self .shared_preloads = shared_preloads
83+
84+ def log_likelihood_function (self , instance : af .ModelInstance , shared = None ) -> float :
4385 """
4486 Given an instance of the model, where the model parameters are set via a non-linear search, fit the model
4587 instance to the imaging dataset.
@@ -71,6 +113,11 @@ def log_likelihood_function(self, instance: af.ModelInstance) -> float:
71113 instance
72114 An instance of the model that is being fitted to the data by this analysis (whose parameters have been set
73115 via a non-linear search).
116+ shared
117+ The cross-factor shared state of a `FactorGraphModel`, computed once per evaluation by the lead
118+ factor's `shared_state_from` (see that method). For this analysis it is a `PreloadsImaging`
119+ carrying the exposure-invariant source-plane mesh geometry; when provided it is reused by the fit
120+ instead of being recomputed. `None` (the default, e.g. a standalone fit) leaves behaviour unchanged.
74121
75122 Returns
76123 -------
@@ -83,16 +130,63 @@ def log_likelihood_function(self, instance: af.ModelInstance) -> float:
83130 )
84131
85132 if self ._use_jax :
86- return self .fit_from (instance = instance ).figure_of_merit - log_likelihood_penalty
133+ return (
134+ self .fit_from (instance = instance , preloads = shared ).figure_of_merit
135+ - log_likelihood_penalty
136+ )
87137
88138 try :
89- return self .fit_from (instance = instance ).figure_of_merit - log_likelihood_penalty
139+ return (
140+ self .fit_from (instance = instance , preloads = shared ).figure_of_merit
141+ - log_likelihood_penalty
142+ )
90143 except Exception as e :
91144 raise af .exc .FitException
92145
146+ def shared_state_from (self , instance : af .ModelInstance ):
147+ """
148+ Compute the exposure-invariant source-plane mesh geometry once so it can be shared across the factors
149+ of a multi-exposure `FactorGraphModel` (see `autofit.Analysis.shared_state_from`).
150+
151+ When `shared_preloads` is set, every factor of the graph is an exposure of the same lens sharing an
152+ identical lens model, so the source-plane mesh (the image-mesh centres of this lead exposure,
153+ ray-traced through the shared lens model) is built once here and returned inside a `PreloadsImaging`,
154+ which `FactorGraphModel` forwards as the `shared` argument to every factor's
155+ `log_likelihood_function`. Each exposure then maps its own (offset) data grid onto the shared mesh
156+ instead of computing its own image-mesh and mesh ray-trace, so every exposure reconstructs on an
157+ identical source-pixel grid.
158+
159+ Unlike the interferometer datacube case, the mapper, mapping matrix, curvature matrix and
160+ regularization matrix are NOT shared — per-exposure PSFs and pixel offsets make the first three
161+ per-dataset, and regularization may adapt to per-exposure data.
162+
163+ Returns `None` when the analysis has not opted in (`shared_preloads=False`) or when the model performs
164+ no inversion, in which case no state is shared and every factor fits as normal.
165+
166+ The caller is responsible for the invariance contract: only enable `shared_preloads` when the factors
167+ genuinely share the lens model, so the source-plane mesh really is exposure-invariant. The lead
168+ factor's own `DatasetModel` offset (if any) is applied when the mesh is traced, so the mesh is defined
169+ in the lead exposure's frame.
170+ """
171+ if not self .shared_preloads :
172+ return None
173+
174+ fit = self .fit_from (instance = instance )
175+
176+ if not fit .perform_inversion :
177+ return None
178+
179+ tracer_to_inversion = fit .tracer_to_inversion
180+
181+ return aa .PreloadsImaging (
182+ source_plane_mesh_grid = tracer_to_inversion .traced_mesh_grid_pg_list ,
183+ image_plane_mesh_grid = tracer_to_inversion .image_plane_mesh_grid_pg_list ,
184+ )
185+
93186 def fit_from (
94187 self ,
95188 instance : af .ModelInstance ,
189+ preloads = None ,
96190 ) -> FitImaging :
97191 """
98192 Given a model instance create a `FitImaging` object.
@@ -105,9 +199,10 @@ def fit_from(
105199 instance
106200 An instance of the model that is being fitted to the data by this analysis (whose parameters have been set
107201 via a non-linear search).
108- check_positions
109- Whether the multiple image positions of the lensed source should be checked, i.e. whether they trace
110- within the position threshold of one another in the source plane.
202+ preloads
203+ An optional `PreloadsImaging` carrying the exposure-invariant source-plane mesh geometry,
204+ computed once and reused by the fit instead of being rebuilt. Supplied by the multi-exposure
205+ shared-state path (see `shared_state_from`); `None` (the default) fits as normal.
111206
112207 Returns
113208 -------
@@ -137,7 +232,8 @@ def fit_from(
137232 dataset_model = dataset_model ,
138233 adapt_images = adapt_images ,
139234 settings = self .settings ,
140- xp = self ._xp
235+ xp = self ._xp ,
236+ preloads = preloads ,
141237 )
142238
143239 def save_attributes (self , paths : af .DirectoryPaths ):
@@ -221,7 +317,7 @@ def _register_fit_imaging_pytrees() -> None:
221317
222318 register_instance_pytree (
223319 FitImaging ,
224- no_flatten = ("dataset" , "adapt_images" , "settings" ),
320+ no_flatten = ("dataset" , "adapt_images" , "settings" , "preloads" ),
225321 )
226322 register_instance_pytree (DatasetModel )
227323 # ``cosmology`` is a fixed physical constant per fit; ride as aux.
0 commit comments