44``FitWeak`` compares a model shear field (derived from a ``Tracer``'s mass profiles via
55``LensCalc.shear_yx_2d_via_hessian_from`` — the same primitive ``SimulatorShearYX`` uses) against an observed
66``WeakDataset`` and reports per-galaxy residuals, chi-squared and the log-likelihood. It is the weak-lensing
7- analogue of :class:`autolens.imaging.fit_imaging.FitImaging` and the input to a future ``AnalysisWeak``.
7+ analogue of :class:`autolens.imaging.fit_imaging.FitImaging` and the input to ``AnalysisWeak``.
88
99Each background source galaxy contributes **two** independent measurements (:math:`\\ gamma_1` and
1010:math:`\\ gamma_2` carry the same per-galaxy noise but are independent Gaussian draws), so the chi-squared sum
1111and ``noise_normalization`` count :math:`N \\ times 2` elements rather than just :math:`N`.
1212
13+ The model quantity adapts to what the dataset declares:
14+
15+ - ``dataset.is_reduced`` — the model is the *reduced* shear :math:`g = \\ gamma / (1 - \\ kappa)`, the quantity
16+ real surveys measure from galaxy ellipticities.
17+ - ``dataset.redshifts`` — the model signal is scaled per galaxy by the lensing-efficiency ratio
18+ :math:`\\ beta_i / \\ beta_{\\ rm ref}` (``LensingCosmology.scaling_factor_between_redshifts_from``), so a
19+ catalogue spanning a range of source redshifts is fitted self-consistently. The reference plane is the
20+ tracer's outermost (source) plane; galaxies at or below the lens redshift carry zero signal. Without
21+ redshifts the fit assumes the tracer's single effective source plane — exactly the pre-scaling behaviour.
22+ The scale factors are computed eagerly from concrete plane redshifts (galaxy redshifts are fixed
23+ constants, not sampled parameters), which keeps them outside any JAX trace.
24+
1325The class is deliberately standalone — it does not inherit from ``autoarray.fit.fit_dataset.AbstractFit``,
1426which is shaped for "data + noise_map + mask" pixel-grid fits. ``FitPoint`` (in ``autolens.point``) follows
1527the same standalone pattern.
28+
29+ JAX support follows the ``LensCalc`` guard pattern: with ``xp=jnp`` the fit statistics are traceable and
30+ ``model_shear`` returns a raw ``(N, 2)`` array (``ShearYX2DIrregular`` is not a registered pytree);
31+ ``AnalysisWeak`` registers the ``FitWeak`` pytree so ``jax.jit(fit_from)`` round-trips a real fit object.
1632"""
1733import math
1834from functools import cached_property
2642
2743
2844class FitWeak :
29- def __init__ (self , dataset : WeakDataset , tracer ):
45+ def __init__ (self , dataset : WeakDataset , tracer , xp = np ):
3046 """
3147 Fit a ``Tracer`` lens model to a ``WeakDataset`` shear catalogue.
3248
@@ -36,71 +52,140 @@ def __init__(self, dataset: WeakDataset, tracer):
3652 The observed weak-lensing shear catalogue.
3753 tracer
3854 The PyAutoLens ``Tracer`` whose mass profiles generate the model shear field.
55+ xp
56+ The array module (``numpy`` or ``jax.numpy``). With ``jax.numpy`` the fit statistics are
57+ traceable and ``model_shear`` returns a raw array rather than a ``ShearYX2DIrregular``.
3958 """
4059 self .dataset = dataset
4160 self .tracer = tracer
61+ self ._xp = xp
4262
4363 @cached_property
44- def model_shear (self ) -> ShearYX2DIrregular :
64+ def _redshift_scale_factors (self ):
65+ """
66+ Per-galaxy lensing-efficiency ratios ``beta_i / beta_ref``, or ``None`` when the dataset carries no
67+ redshifts.
68+
69+ Unity for galaxies at the tracer's source-plane redshift, zero at or below the lens redshift (such
70+ galaxies are not lensed), and ``LensingCosmology.scaling_factor_between_redshifts_from`` in between —
71+ the same factor multi-plane ray-tracing applies to deflections. Always a concrete NumPy array:
72+ plane and catalogue redshifts are fixed constants, so this never participates in a JAX trace.
4573 """
46- The model shear field evaluated at the galaxy positions, via ``LensCalc``.
74+ redshifts = getattr (self .dataset , "redshifts" , None )
75+ if redshifts is None :
76+ return None
77+
78+ plane_redshifts = sorted (
79+ float (galaxy .redshift ) for galaxy in self .tracer .galaxies
80+ )
81+ redshift_lens = plane_redshifts [0 ]
82+ redshift_ref = plane_redshifts [- 1 ]
83+
84+ cosmology = self .tracer .cosmology
85+
86+ factors = [
87+ 0.0
88+ if float (redshift_i ) <= redshift_lens
89+ else float (
90+ cosmology .scaling_factor_between_redshifts_from (
91+ redshift_0 = redshift_lens ,
92+ redshift_1 = float (redshift_i ),
93+ redshift_final = redshift_ref ,
94+ )
95+ )
96+ for redshift_i in np .asarray (redshifts )
97+ ]
98+ return np .asarray (factors )
4799
48- When the dataset is marked ``is_reduced`` (real catalogues measure galaxy ellipticities,
49- i.e. the reduced shear) the model quantity is ``g = gamma / (1 - kappa)``, with the
50- convergence from the same Hessian primitive — so data and model always live in the same
51- space.
100+ @cached_property
101+ def model_shear (self ):
52102 """
103+ The model signal evaluated at the galaxy positions, via ``LensCalc``.
104+
105+ This is the (optionally per-galaxy-scaled) shear ``gamma``, or the reduced shear
106+ ``g = gamma / (1 - kappa)`` when the dataset is marked ``is_reduced`` — data and model always live
107+ in the same space. On the NumPy path the return is a ``ShearYX2DIrregular``; with ``xp=jax.numpy``
108+ it is a raw ``(N, 2)`` array (the ``LensCalc`` guard pattern).
109+ """
110+ xp = self ._xp
111+
53112 lens_calc = LensCalc .from_tracer (self .tracer )
54113
55- shear = lens_calc .shear_yx_2d_via_hessian_from (grid = self .dataset .positions )
114+ shear = lens_calc .shear_yx_2d_via_hessian_from (
115+ grid = self .dataset .positions , xp = xp
116+ )
56117
57- if not getattr (self .dataset , "is_reduced" , False ):
118+ scale = self ._redshift_scale_factors
119+ is_reduced = getattr (self .dataset , "is_reduced" , False )
120+
121+ if scale is None and not is_reduced :
58122 return shear
59123
60- convergence = lens_calc .convergence_2d_via_hessian_from (
61- grid = self .dataset .positions
62- )
63- return ShearYX2DIrregular (
64- values = np .asarray (shear ) / (1.0 - np .asarray (convergence ))[:, None ],
65- grid = self .dataset .positions ,
66- )
124+ values = xp .asarray (shear )
125+
126+ if is_reduced :
127+ convergence = xp .asarray (
128+ lens_calc .convergence_2d_via_hessian_from (
129+ grid = self .dataset .positions , xp = xp
130+ )
131+ )
132+ if scale is not None :
133+ values = (scale [:, None ] * values ) / (
134+ 1.0 - scale * convergence
135+ )[:, None ]
136+ else :
137+ values = values / (1.0 - convergence )[:, None ]
138+ else :
139+ values = scale [:, None ] * values
140+
141+ if xp is np :
142+ return ShearYX2DIrregular (
143+ values = np .asarray (values ), grid = self .dataset .positions
144+ )
145+ return values
67146
68147 @property
69- def residual_map (self ) -> np . ndarray :
148+ def residual_map (self ):
70149 """``(N, 2)`` residuals ``data - model`` for each galaxy's ``(gamma_2, gamma_1)`` components."""
71- return np .asarray (self .dataset .shear_yx ) - np .asarray (self .model_shear )
150+ xp = self ._xp
151+ data = xp .asarray (np .asarray (self .dataset .shear_yx ))
152+ return data - xp .asarray (self .model_shear )
72153
73154 @property
74- def normalized_residual_map (self ) -> np . ndarray :
155+ def normalized_residual_map (self ):
75156 """``(N, 2)`` residuals divided by the per-galaxy noise broadcast across both shear components."""
76- noise = np .asarray (self .dataset .noise_map )[:, None ]
157+ xp = self ._xp
158+ noise = xp .asarray (np .asarray (self .dataset .noise_map ))[:, None ]
77159 return self .residual_map / noise
78160
79161 @property
80- def chi_squared_map (self ) -> np . ndarray :
162+ def chi_squared_map (self ):
81163 """``(N, 2)`` per-component chi-squared contributions."""
82164 return self .normalized_residual_map ** 2
83165
84166 @property
85- def chi_squared (self ) -> float :
167+ def chi_squared (self ):
86168 """Scalar chi-squared summed over all ``N x 2`` shear measurements."""
87- return float (np .sum (self .chi_squared_map ))
169+ xp = self ._xp
170+ chi_squared = xp .sum (self .chi_squared_map )
171+ return float (chi_squared ) if xp is np else chi_squared
88172
89173 @property
90174 def noise_normalization (self ) -> float :
91175 r"""
92176 Gaussian likelihood normalisation :math:`\sum \log(2 \pi \sigma^2)` summed over all ``N x 2`` shear
93177 measurements — the factor of 2 reflects that each galaxy contributes two independent components.
178+ Always concrete (it depends only on the dataset).
94179 """
95180 noise = np .asarray (self .dataset .noise_map )
96181 return float (2.0 * np .sum (np .log (2.0 * math .pi * noise ** 2 )))
97182
98183 @property
99- def log_likelihood (self ) -> float :
184+ def log_likelihood (self ):
100185 r"""Standard Gaussian log-likelihood :math:`-\tfrac{1}{2}(\chi^2 + \text{noise normalization})`."""
101186 return - 0.5 * (self .chi_squared + self .noise_normalization )
102187
103188 @property
104- def figure_of_merit (self ) -> float :
189+ def figure_of_merit (self ):
105190 """Quantity returned to non-linear searches; same as ``log_likelihood`` (no inversion / evidence)."""
106191 return self .log_likelihood
0 commit comments