|
29 | 29 |
|
30 | 30 |
|
31 | 31 | class FitPositionsSource(AbstractFitPositions): |
| 32 | + #: How each back-traced position's residual from the source-plane centre is weighted: |
| 33 | + #: `"magnification"` — the traditional scalar `µᵢ²/σᵢ²` weighting with the magnified-noise |
| 34 | + #: normalization (the long-standing behaviour of this class, and the Lenstool convention); |
| 35 | + #: `"jacobian"` — the per-image precision tensor `Wᵢ = Aᵢ⁻ᵀΘᵢAᵢ⁻¹` with the observed-plane |
| 36 | + #: normalization, matching `FitPositionsSourceSolved` but with the centre a free parameter. |
| 37 | + weighting = "magnification" |
| 38 | + |
32 | 39 | def __init__( |
33 | 40 | self, |
34 | 41 | name: str, |
@@ -66,6 +73,13 @@ def __init__( |
66 | 73 |
|
67 | 74 | 7) Sum the chi-squared values to compute the overall log likelihood of the fit. |
68 | 75 |
|
| 76 | + Steps 4-6 describe the default `weighting = "magnification"` scalar convention. Setting the |
| 77 | + `weighting` class attribute to `"jacobian"` instead weights each vector residual `β̂ᵢ − c` (with `c` |
| 78 | + the profile's free `centre`) by the per-image precision tensor `Wᵢ = Aᵢ⁻ᵀΘᵢAᵢ⁻¹` (see |
| 79 | + `autolens.point.fit.solved.precision_tensor_components_from`), with the observed-plane noise |
| 80 | + normalization matching `FitPositionsSourceSolved` — the same tensor likelihood as that class, but |
| 81 | + with the centre sampled as a free parameter rather than solved and marginalized. |
| 82 | +
|
69 | 83 | Point source fitting uses name pairing, whereby the `name` of the `Point` object is paired to the name of the |
70 | 84 | point source dataset to ensure that point source datasets are fitted to the correct point source. |
71 | 85 |
|
@@ -136,32 +150,68 @@ def residual_map(self) -> aa.ArrayIrregular: |
136 | 150 | coordinate=self.source_plane_coordinate |
137 | 151 | ) |
138 | 152 |
|
| 153 | + @property |
| 154 | + def residual_vectors(self) -> np.ndarray: |
| 155 | + """ |
| 156 | + The (n_positions, 2) array of vector residuals `β̂ᵢ − c`: the back-traced source-plane positions |
| 157 | + minus the source-plane centre `c` (here the profile's free `centre`; `FitPositionsSourceSolved` |
| 158 | + overrides this to use the solved `β*` via `_beta_hat`, tolerating plain-ndarray test inputs). |
| 159 | + """ |
| 160 | + beta_hat = self.model_data.array |
| 161 | + centre_y, centre_x = self.source_plane_coordinate |
| 162 | + centre = self._xp.array([centre_y, centre_x]) |
| 163 | + return beta_hat - centre |
| 164 | + |
139 | 165 | @property |
140 | 166 | def chi_squared_map(self) -> float: |
141 | 167 | """ |
142 | | - Returns the chi-squared of the point-source source-plane fit, which is the sum of the squared residuals |
143 | | - multiplied by the magnifications squared, divided by the noise-map values squared. |
| 168 | + Returns the chi-squared of the point-source source-plane fit. |
| 169 | +
|
| 170 | + For `weighting = "magnification"` this is the squared residuals multiplied by the magnifications |
| 171 | + squared, divided by the noise-map values squared. For `weighting = "jacobian"` it is the per-image |
| 172 | + quadratic form `(β̂ᵢ−c)ᵀ Wᵢ (β̂ᵢ−c)` with the precision tensor `Wᵢ = Aᵢ⁻ᵀΘᵢAᵢ⁻¹`. |
144 | 173 | """ |
| 174 | + if self.weighting == "magnification": |
| 175 | + return self.residual_map**2.0 / ( |
| 176 | + self.magnifications_at_positions.array**-2.0 |
| 177 | + * self.noise_map.array**2.0 |
| 178 | + ) |
145 | 179 |
|
146 | | - return self.residual_map**2.0 / ( |
147 | | - self.magnifications_at_positions.array**-2.0 * self.noise_map.array**2.0 |
148 | | - ) |
| 180 | + w11, w12, w21, w22 = precision_tensor_components_from(self, self.weighting) |
| 181 | + |
| 182 | + delta = self.residual_vectors |
| 183 | + dy = delta[:, 0] |
| 184 | + dx = delta[:, 1] |
| 185 | + |
| 186 | + terms = dy * (w11 * dy + w12 * dx) + dx * (w21 * dy + w22 * dx) |
| 187 | + |
| 188 | + return aa.ArrayIrregular(values=terms) |
149 | 189 |
|
150 | 190 | @property |
151 | 191 | def noise_normalization(self) -> float: |
152 | 192 | """ |
153 | | - Returns the normalization of the noise-map, which is the sum of the noise-map values squared. |
| 193 | + Returns the noise normalization of the fit's Gaussian likelihood. |
| 194 | +
|
| 195 | + For `weighting = "magnification"` this is the long-standing magnified-noise source-plane-data |
| 196 | + convention `Σᵢ log(2π µᵢ⁻²σᵢ²)`. For `weighting = "jacobian"` it is the observed-plane |
| 197 | + (model-independent) convention `Σᵢ log((2π)² σᵢ⁴)` matching `FitPositionsSourceSolved` (see that |
| 198 | + class's docstring for why a model-dependent normalization would spuriously favour |
| 199 | + high-magnification models). |
154 | 200 | """ |
155 | | - return self._xp.sum( |
156 | | - self._xp.log( |
157 | | - 2 |
158 | | - * np.pi |
159 | | - * ( |
160 | | - self.magnifications_at_positions.array**-2.0 |
161 | | - * self.noise_map.array**2.0 |
| 201 | + if self.weighting == "magnification": |
| 202 | + return self._xp.sum( |
| 203 | + self._xp.log( |
| 204 | + 2 |
| 205 | + * np.pi |
| 206 | + * ( |
| 207 | + self.magnifications_at_positions.array**-2.0 |
| 208 | + * self.noise_map.array**2.0 |
| 209 | + ) |
162 | 210 | ) |
163 | 211 | ) |
164 | | - ) |
| 212 | + |
| 213 | + sigma_sq = self.noise_map.array**2.0 |
| 214 | + return self._xp.sum(self._xp.log((2.0 * np.pi) ** 2.0 * sigma_sq**2.0)) |
165 | 215 |
|
166 | 216 | @property |
167 | 217 | def log_likelihood(self) -> float: |
|
0 commit comments