|
| 1 | +""" |
| 2 | +SimulatorImaging use_jax parity test |
| 3 | +===================================== |
| 4 | +
|
| 5 | +Runs ``al.SimulatorImaging(use_jax=False)`` and ``al.SimulatorImaging(use_jax=True)`` |
| 6 | +against the same tracer + grid and asserts the noise-free simulated image agrees |
| 7 | +to machine precision. Cross-xp numerical validation for ``SimulatorImaging.use_jax=True`` |
| 8 | +— library unit tests stay NumPy-only. |
| 9 | +
|
| 10 | +Noise is disabled on both paths because JAX and NumPy use different RNG algorithms; |
| 11 | +the same seed produces different draws. The simulator's xp-threaded noise path is |
| 12 | +tested separately (``test_autoarray/dataset/imaging/test_simulator_use_jax.py`` |
| 13 | +covers the constructor wiring; this script covers numerical agreement of the |
| 14 | +deterministic image-generation path). |
| 15 | +
|
| 16 | +Also tests the @jax.jit roundtrip: the simulator under jit produces a dataset |
| 17 | +whose data array agrees with the eager path. This exercises the |
| 18 | +``register_tracer_classes(tracer)`` registration walker shipped in PR 1 |
| 19 | +(PyAutoLens#538) — without prior registration the @jax.jit decoration would fail |
| 20 | +to flatten the Tracer at the JIT boundary. |
| 21 | +""" |
| 22 | + |
| 23 | +from autoconf import jax_wrapper # Sets JAX float64 before other imports |
| 24 | + |
| 25 | +import jax |
| 26 | +import numpy as np |
| 27 | + |
| 28 | +import autolens as al |
| 29 | + |
| 30 | + |
| 31 | +grid = al.Grid2D.uniform(shape_native=(100, 100), pixel_scales=0.1) |
| 32 | + |
| 33 | +lens_galaxy = al.Galaxy( |
| 34 | + redshift=0.5, |
| 35 | + light=al.lp.Sersic( |
| 36 | + centre=(0.0, 0.0), |
| 37 | + ell_comps=(0.01, 0.01), |
| 38 | + intensity=1.0, |
| 39 | + effective_radius=0.5, |
| 40 | + sersic_index=4.0, |
| 41 | + ), |
| 42 | + mass=al.mp.Isothermal( |
| 43 | + centre=(0.0, 0.0), |
| 44 | + ell_comps=(0.01, 0.01), |
| 45 | + einstein_radius=1.6, |
| 46 | + ), |
| 47 | +) |
| 48 | +source_galaxy = al.Galaxy( |
| 49 | + redshift=1.0, |
| 50 | + light=al.lp.Sersic( |
| 51 | + centre=(0.1, 0.1), |
| 52 | + ell_comps=(0.01, 0.01), |
| 53 | + intensity=0.5, |
| 54 | + effective_radius=0.2, |
| 55 | + sersic_index=1.0, |
| 56 | + ), |
| 57 | +) |
| 58 | +tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) |
| 59 | + |
| 60 | +psf = al.Convolver.from_gaussian( |
| 61 | + shape_native=(11, 11), sigma=0.05, pixel_scales=grid.pixel_scales |
| 62 | +) |
| 63 | + |
| 64 | +# Noise-free configuration: both backends must produce identical deterministic images. |
| 65 | +common_kwargs = dict( |
| 66 | + exposure_time=300.0, |
| 67 | + psf=psf, |
| 68 | + background_sky_level=0.1, |
| 69 | + add_poisson_noise_to_data=False, |
| 70 | + include_poisson_noise_in_noise_map=False, |
| 71 | +) |
| 72 | + |
| 73 | +# NumPy path. |
| 74 | +simulator_np = al.SimulatorImaging(**common_kwargs) |
| 75 | +dataset_np = simulator_np.via_tracer_from(tracer=tracer, grid=grid) |
| 76 | +data_np = np.asarray(dataset_np.data.array) |
| 77 | + |
| 78 | +# JAX path (eager, not yet under @jax.jit). |
| 79 | +simulator_jax = al.SimulatorImaging(use_jax=True, **common_kwargs) |
| 80 | +dataset_jax = simulator_jax.via_tracer_from(tracer=tracer, grid=grid) |
| 81 | +data_jax = np.asarray(dataset_jax.data.array) |
| 82 | + |
| 83 | +assert data_np.shape == data_jax.shape, ( |
| 84 | + f"Shape mismatch: numpy={data_np.shape} vs jax={data_jax.shape}" |
| 85 | +) |
| 86 | +np.testing.assert_allclose( |
| 87 | + data_np, |
| 88 | + data_jax, |
| 89 | + atol=1e-8, |
| 90 | + err_msg="SimulatorImaging(use_jax=True) data differs from use_jax=False", |
| 91 | +) |
| 92 | +print( |
| 93 | + f"PASS: SimulatorImaging(use_jax=True) data matches use_jax=False " |
| 94 | + f"to atol=1e-8 (shape {data_np.shape})." |
| 95 | +) |
| 96 | + |
| 97 | +# @jax.jit roundtrip: register tracer classes so JAX can flatten/unflatten |
| 98 | +# the Tracer pytree at the JIT boundary, then run the simulator under jit |
| 99 | +# and verify the output matches the eager JAX path. |
| 100 | +al.util.register_tracer_classes(tracer) |
| 101 | + |
| 102 | + |
| 103 | +@jax.jit |
| 104 | +def simulate_jit(tracer): |
| 105 | + dataset = simulator_jax.via_tracer_from(tracer=tracer, grid=grid) |
| 106 | + return dataset.data._array |
| 107 | + |
| 108 | + |
| 109 | +data_jit = np.asarray(simulate_jit(tracer)) |
| 110 | + |
| 111 | +np.testing.assert_allclose( |
| 112 | + data_jax, |
| 113 | + data_jit, |
| 114 | + atol=1e-8, |
| 115 | + err_msg="SimulatorImaging @jax.jit data differs from eager JAX path", |
| 116 | +) |
| 117 | +print( |
| 118 | + f"PASS: SimulatorImaging @jax.jit roundtrip matches eager JAX " |
| 119 | + f"to atol=1e-8 (shape {data_jit.shape})." |
| 120 | +) |
0 commit comments