|
| 1 | +""" |
| 2 | +Simulate weak-lensing shear catalogues from a ``Tracer``. |
| 3 | +
|
| 4 | +The shear field of a strong lens system is computed by ``Tracer.shear_yx_2d_via_hessian_from``, which differentiates |
| 5 | +the deflection-angle field to derive the lensing Hessian and from there the (gamma_2, gamma_1) shear at any (y, x). |
| 6 | +That gives a *noise-free* shear field; this module adds two extras needed for a realistic weak-lensing simulation: |
| 7 | +
|
| 8 | +1. **Shape noise.** Each background source galaxy has a random unlensed ellipticity, drawn here as iid Gaussian |
| 9 | + noise per shear component with standard deviation ``noise_sigma``. Realistic values are around 0.2 - 0.4. |
| 10 | +
|
| 11 | +2. **Source-galaxy positions.** Either provide an explicit grid of (y, x) source positions, or let the simulator |
| 12 | + draw a uniform-random distribution of ``n_galaxies`` positions inside a square arc-second extent — sufficient |
| 13 | + for development work; future iterations may swap in number-density / luminosity-function-driven distributions. |
| 14 | +
|
| 15 | +The output is a :class:`autolens.weak.dataset.WeakDataset`. |
| 16 | +""" |
| 17 | +from typing import Optional |
| 18 | + |
| 19 | +import numpy as np |
| 20 | + |
| 21 | +import autoarray as aa |
| 22 | + |
| 23 | +from autogalaxy.operate.lens_calc import LensCalc |
| 24 | +from autogalaxy.util.shear_field import ShearYX2DIrregular |
| 25 | + |
| 26 | +from autolens.weak.dataset import WeakDataset |
| 27 | + |
| 28 | + |
| 29 | +class SimulatorShearYX: |
| 30 | + def __init__(self, noise_sigma: float = 0.3, seed: Optional[int] = None): |
| 31 | + """ |
| 32 | + Simulator for weak-lensing shear catalogues. |
| 33 | +
|
| 34 | + Parameters |
| 35 | + ---------- |
| 36 | + noise_sigma |
| 37 | + Standard deviation of the per-component Gaussian shape noise added to each measured shear vector. |
| 38 | + Values around 0.2 - 0.4 are realistic for ground-based weak-lensing surveys; ``0.0`` produces a |
| 39 | + noise-free shear field, useful for unit tests. |
| 40 | + seed |
| 41 | + Optional seed for the random number generator. When set, both the noise and the random-position |
| 42 | + draws (for ``via_tracer_random_positions_from``) are reproducible. |
| 43 | + """ |
| 44 | + self.noise_sigma = float(noise_sigma) |
| 45 | + self.seed = seed |
| 46 | + self._rng = np.random.default_rng(seed) |
| 47 | + |
| 48 | + def via_tracer_from( |
| 49 | + self, |
| 50 | + tracer, |
| 51 | + grid: aa.Grid2DIrregular, |
| 52 | + name: str = "", |
| 53 | + ) -> WeakDataset: |
| 54 | + """ |
| 55 | + Simulate a weak-lensing shear catalogue from a ``Tracer`` evaluated at the given (y, x) source positions. |
| 56 | +
|
| 57 | + Computes ``tracer.shear_yx_2d_via_hessian_from(grid=grid)`` — which goes through ``LensCalc`` under the |
| 58 | + hood — and adds iid Gaussian shape noise per component with std ``self.noise_sigma``. |
| 59 | +
|
| 60 | + Parameters |
| 61 | + ---------- |
| 62 | + tracer |
| 63 | + A PyAutoLens ``Tracer`` object exposing ``shear_yx_2d_via_hessian_from``. |
| 64 | + grid |
| 65 | + The (y, x) source-galaxy positions where the shear is measured. Must be an ``aa.Grid2DIrregular`` |
| 66 | + (or convertible to one). |
| 67 | + name |
| 68 | + Optional label passed through to ``WeakDataset.name``. |
| 69 | + """ |
| 70 | + if not isinstance(grid, aa.Grid2DIrregular): |
| 71 | + grid = aa.Grid2DIrregular(values=grid) |
| 72 | + |
| 73 | + true_shear = self._true_shear_yx_from(tracer=tracer, grid=grid) |
| 74 | + |
| 75 | + if self.noise_sigma > 0.0: |
| 76 | + noise = self._rng.normal( |
| 77 | + loc=0.0, scale=self.noise_sigma, size=true_shear.shape |
| 78 | + ) |
| 79 | + shear_array = np.asarray(true_shear) + noise |
| 80 | + else: |
| 81 | + shear_array = np.asarray(true_shear) |
| 82 | + |
| 83 | + shear_yx = ShearYX2DIrregular(values=shear_array, grid=grid) |
| 84 | + |
| 85 | + noise_map = aa.ArrayIrregular( |
| 86 | + values=[self.noise_sigma] * len(grid) |
| 87 | + ) |
| 88 | + |
| 89 | + return WeakDataset(shear_yx=shear_yx, noise_map=noise_map, name=name) |
| 90 | + |
| 91 | + def via_tracer_random_positions_from( |
| 92 | + self, |
| 93 | + tracer, |
| 94 | + n_galaxies: int, |
| 95 | + grid_extent: float = 3.0, |
| 96 | + name: str = "", |
| 97 | + ) -> WeakDataset: |
| 98 | + """ |
| 99 | + Simulate a weak-lensing shear catalogue at ``n_galaxies`` uniform-random source positions. |
| 100 | +
|
| 101 | + Galaxy positions are drawn uniformly inside a square ``[-grid_extent, +grid_extent]`` arc-second box. |
| 102 | + This is the simplest sensible distribution for development purposes; production weak-lensing simulations |
| 103 | + typically use a survey-specific number density / redshift distribution. |
| 104 | +
|
| 105 | + Parameters |
| 106 | + ---------- |
| 107 | + tracer |
| 108 | + A PyAutoLens ``Tracer`` object. |
| 109 | + n_galaxies |
| 110 | + Number of background source galaxies to simulate. |
| 111 | + grid_extent |
| 112 | + Half-width of the square (in arc-seconds) inside which positions are drawn. |
| 113 | + name |
| 114 | + Optional label passed through to ``WeakDataset.name``. |
| 115 | + """ |
| 116 | + positions = self._rng.uniform( |
| 117 | + low=-grid_extent, high=grid_extent, size=(n_galaxies, 2) |
| 118 | + ) |
| 119 | + grid = aa.Grid2DIrregular(values=positions) |
| 120 | + return self.via_tracer_from(tracer=tracer, grid=grid, name=name) |
| 121 | + |
| 122 | + @staticmethod |
| 123 | + def _true_shear_yx_from(tracer, grid: aa.Grid2DIrregular): |
| 124 | + """ |
| 125 | + Evaluate the noise-free shear field of ``tracer`` on ``grid``. |
| 126 | +
|
| 127 | + - If the input exposes ``shear_yx_2d_via_hessian_from`` directly, use it. |
| 128 | + - Else, if it exposes ``deflections_between_planes_from`` (it duck-types as a ``Tracer``), build a |
| 129 | + multi-plane ``LensCalc`` via ``from_tracer``. |
| 130 | + - Otherwise treat the input as a mass-like object (single ``MassProfile``, ``Galaxy``, ``Galaxies``) |
| 131 | + and build a single-plane ``LensCalc`` via ``from_mass_obj``. This fallback keeps the simulator |
| 132 | + usable in unit tests that don't want to spin up a full ``Tracer``. |
| 133 | + """ |
| 134 | + method = getattr(tracer, "shear_yx_2d_via_hessian_from", None) |
| 135 | + if method is not None: |
| 136 | + return method(grid=grid) |
| 137 | + if hasattr(tracer, "deflections_between_planes_from"): |
| 138 | + return LensCalc.from_tracer(tracer).shear_yx_2d_via_hessian_from(grid=grid) |
| 139 | + return LensCalc.from_mass_obj(tracer).shear_yx_2d_via_hessian_from(grid=grid) |
0 commit comments