|
12 | 12 |
|
13 | 13 | logger = logging.getLogger(__name__) |
14 | 14 |
|
| 15 | +_IMAGING_PYTREES_REGISTERED = False |
| 16 | + |
| 17 | + |
| 18 | +def _register_imaging_pytrees() -> None: |
| 19 | + """Register ``Imaging`` so ``jax.jit(via_image_from)`` can flatten its return. |
| 20 | +
|
| 21 | + Counterpart of ``AnalysisImaging._register_fit_imaging_pytrees``, which does |
| 22 | + the same job for ``FitImaging``. Without it a jitted simulator call raises |
| 23 | + ``TypeError: ... returned a value of type Imaging, which is not a valid JAX |
| 24 | + type``. |
| 25 | +
|
| 26 | + ``data`` and ``noise_map`` are the only per-simulation dynamic values, so |
| 27 | + everything else rides as aux: ``psf`` and ``grids`` are constants for a given |
| 28 | + simulation, the over-sample sizes are static integer geometry, and the |
| 29 | + remaining slots are ``None``. |
| 30 | +
|
| 31 | + Unlike pytree registration for a jitted function's *arguments* — which must |
| 32 | + happen before the first call, because JAX flattens arguments at trace time — |
| 33 | + registering here is in time, because the return value is flattened only after |
| 34 | + the body has run. Idempotent via the module-level flag. |
| 35 | + """ |
| 36 | + global _IMAGING_PYTREES_REGISTERED |
| 37 | + if _IMAGING_PYTREES_REGISTERED: |
| 38 | + return |
| 39 | + |
| 40 | + from autoarray.abstract_ndarray import register_instance_pytree |
| 41 | + |
| 42 | + # ``Array2D.instance_flatten`` emits every ``__dict__`` entry not opted out, |
| 43 | + # so a flattened ``data`` exposes its ``mask`` as a child. ``Mask2D`` must |
| 44 | + # therefore be a pytree itself, or it surfaces as a bare leaf and JAX |
| 45 | + # rejects the return value. Registering it here rather than adding ``mask`` |
| 46 | + # to ``Array2D.__no_flatten__`` keeps ``Array2D``'s flatten semantics |
| 47 | + # unchanged for every other jitted path in the stack. |
| 48 | + register_instance_pytree(Mask2D) |
| 49 | + |
| 50 | + register_instance_pytree( |
| 51 | + Imaging, |
| 52 | + no_flatten=( |
| 53 | + "psf", |
| 54 | + "grids", |
| 55 | + "over_sample_size_lp", |
| 56 | + "over_sample_size_pixelization", |
| 57 | + "convolve_over_sample_size_lp", |
| 58 | + "convolve_over_sample_size_pixelization", |
| 59 | + "noise_covariance_matrix", |
| 60 | + "sparse_operator", |
| 61 | + ), |
| 62 | + ) |
| 63 | + |
| 64 | + _IMAGING_PYTREES_REGISTERED = True |
| 65 | + |
15 | 66 |
|
16 | 67 | class SimulatorImaging: |
17 | 68 | def __init__( |
@@ -162,6 +213,9 @@ def via_image_from( |
162 | 213 | if xp is None: |
163 | 214 | xp = self._xp |
164 | 215 |
|
| 216 | + if xp is not np: |
| 217 | + _register_imaging_pytrees() |
| 218 | + |
165 | 219 | exposure_time_map = Array2D.full( |
166 | 220 | fill_value=self.exposure_time, |
167 | 221 | shape_native=image.shape_native, |
@@ -201,7 +255,9 @@ def via_image_from( |
201 | 255 |
|
202 | 256 | if self.include_poisson_noise_in_noise_map: |
203 | 257 | noise_map = preprocess.noise_map_via_data_eps_and_exposure_time_map_from( |
204 | | - data_eps=image_with_poisson_noise, exposure_time_map=exposure_time_map |
| 258 | + data_eps=image_with_poisson_noise, |
| 259 | + exposure_time_map=exposure_time_map, |
| 260 | + xp=xp, |
205 | 261 | ) |
206 | 262 |
|
207 | 263 | else: |
|
0 commit comments