Skip to content

Commit 10bfea5

Browse files
authored
Merge pull request #523 from PyAutoLabs/feature/weak-visualization
feat(weak): aplt plotters for WeakDataset shear catalogues (#496)
2 parents d522aed + 3700bc2 commit 10bfea5

5 files changed

Lines changed: 333 additions & 0 deletions

File tree

autolens/plot/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@
6565
from autolens.point.plot.fit_point_plots import subplot_fit as subplot_fit_point
6666
from autolens.point.plot.point_dataset_plots import subplot_dataset as subplot_point_dataset
6767

68+
from autolens.weak.plot.weak_dataset_plots import (
69+
plot_shear_yx_2d,
70+
plot_ellipticities,
71+
plot_phis,
72+
plot_noise_map,
73+
subplot_weak_dataset,
74+
)
75+
6876
from autolens.lens.plot.subhalo_plots import (
6977
subplot_detection_imaging,
7078
subplot_detection_fits,

autolens/weak/plot/__init__.py

Whitespace-only changes.
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
"""
2+
Module-level matplotlib helpers for visualising a ``WeakDataset``.
3+
4+
A shear catalogue is a set of complex shear measurements ``(gamma_2, gamma_1)``
5+
at the ``(y, x)`` positions of background source galaxies. The natural way to
6+
draw it is matplotlib's ``quiver`` with **headless line segments**, because
7+
shear is a spin-2 quantity — a 180-degree rotation maps the shear back to
8+
itself, so an arrowhead would suggest a directionality the data does not
9+
have. This is the same convention used in weak-lensing science papers
10+
(e.g. KiDS, DES).
11+
12+
The plotters access the shear field exclusively through the derived properties
13+
``.ellipticities`` (``|gamma|``) and ``.phis`` (position angle, in **degrees**)
14+
defined on ``AbstractShearField``. Indexing the underlying ``[:, 0]`` /
15+
``[:, 1]`` storage directly is deliberately avoided so the plotters keep
16+
working if the ``[gamma_2, gamma_1]`` convention pinned by PyAutoGalaxy PR
17+
#366 ever changes.
18+
"""
19+
from typing import Optional
20+
21+
import numpy as np
22+
23+
from autoarray.plot.grid import plot_grid
24+
from autoarray.plot.utils import (
25+
subplots,
26+
save_figure,
27+
conf_subplot_figsize,
28+
tight_layout,
29+
)
30+
31+
32+
def _positions_yx(shear_yx) -> np.ndarray:
33+
"""Return the ``(N, 2)`` ``[y, x]`` position array for a shear field."""
34+
grid = shear_yx.grid
35+
return np.array(grid.array if hasattr(grid, "array") else grid)
36+
37+
38+
def plot_shear_yx_2d(
39+
shear_yx,
40+
ax=None,
41+
title: str = "Shear Field",
42+
output_path: Optional[str] = None,
43+
output_filename: str = "shear_yx",
44+
output_format: Optional[str] = None,
45+
):
46+
"""
47+
Plot a shear field as a quiver of headless line segments at galaxy positions.
48+
49+
Each segment is centred on the galaxy position (``pivot="middle"``), has a
50+
length proportional to the shear magnitude ``|gamma|`` and is oriented at
51+
the shear position angle ``phi``. Segments are colour-coded by ``|gamma|``.
52+
53+
Parameters
54+
----------
55+
shear_yx
56+
A ``ShearYX2D`` / ``ShearYX2DIrregular`` carrying the shear vectors and
57+
the ``(y, x)`` galaxy grid.
58+
ax
59+
Existing ``Axes`` to draw onto; ``None`` creates a new figure.
60+
title
61+
Figure title.
62+
output_path, output_filename, output_format
63+
Standard workspace output controls. When ``ax`` is supplied the saving
64+
is the caller's responsibility (typically ``subplot_weak_dataset``).
65+
"""
66+
positions = _positions_yx(shear_yx)
67+
y, x = positions[:, 0], positions[:, 1]
68+
69+
mag = np.asarray(shear_yx.ellipticities)
70+
phi_rad = np.deg2rad(np.asarray(shear_yx.phis))
71+
72+
u = mag * np.cos(phi_rad)
73+
v = mag * np.sin(phi_rad)
74+
75+
standalone = ax is None
76+
if standalone:
77+
fig, ax = subplots(1, 1)
78+
79+
ax.quiver(
80+
x,
81+
y,
82+
u,
83+
v,
84+
mag,
85+
pivot="middle",
86+
headwidth=0,
87+
headlength=0,
88+
headaxislength=0,
89+
cmap="viridis",
90+
)
91+
ax.set_xlabel('x (")')
92+
ax.set_ylabel('y (")')
93+
ax.set_title(title)
94+
ax.set_aspect("equal")
95+
96+
if standalone:
97+
tight_layout()
98+
save_figure(
99+
fig,
100+
path=output_path,
101+
filename=output_filename,
102+
format=output_format,
103+
)
104+
105+
106+
def plot_ellipticities(
107+
shear_yx,
108+
ax=None,
109+
title: str = r"Shear Magnitude $|\gamma|$",
110+
output_path: Optional[str] = None,
111+
output_filename: str = "shear_ellipticities",
112+
output_format: Optional[str] = None,
113+
):
114+
"""
115+
Plot a colour-coded scatter of the shear magnitude ``|gamma|`` at each galaxy.
116+
117+
Delegates to ``autoarray.plot.grid.plot_grid`` with ``color_array`` set to
118+
the per-galaxy ellipticities.
119+
"""
120+
plot_grid(
121+
grid=_positions_yx(shear_yx),
122+
ax=ax,
123+
color_array=np.asarray(shear_yx.ellipticities),
124+
colormap="viridis",
125+
title=title,
126+
output_path=output_path if ax is None else None,
127+
output_filename=output_filename,
128+
output_format=output_format,
129+
)
130+
131+
132+
def plot_phis(
133+
shear_yx,
134+
ax=None,
135+
title: str = r"Shear Position Angle $\phi$",
136+
output_path: Optional[str] = None,
137+
output_filename: str = "shear_phis",
138+
output_format: Optional[str] = None,
139+
):
140+
"""
141+
Plot a colour-coded scatter of the shear position angle ``phi`` at each galaxy.
142+
143+
Position angles are cyclic, so a cyclic colormap (``twilight``) is used.
144+
"""
145+
plot_grid(
146+
grid=_positions_yx(shear_yx),
147+
ax=ax,
148+
color_array=np.asarray(shear_yx.phis),
149+
colormap="twilight",
150+
title=title,
151+
output_path=output_path if ax is None else None,
152+
output_filename=output_filename,
153+
output_format=output_format,
154+
)
155+
156+
157+
def plot_noise_map(
158+
dataset,
159+
ax=None,
160+
title: str = "Noise Map",
161+
output_path: Optional[str] = None,
162+
output_filename: str = "noise_map",
163+
output_format: Optional[str] = None,
164+
):
165+
"""
166+
Plot a colour-coded scatter of the per-galaxy shear noise at each position.
167+
168+
Takes the full ``WeakDataset`` (not just the shear field) because the noise
169+
map lives on the dataset.
170+
"""
171+
plot_grid(
172+
grid=_positions_yx(dataset.shear_yx),
173+
ax=ax,
174+
color_array=np.asarray(dataset.noise_map),
175+
colormap="magma",
176+
title=title,
177+
output_path=output_path if ax is None else None,
178+
output_filename=output_filename,
179+
output_format=output_format,
180+
)
181+
182+
183+
def subplot_weak_dataset(
184+
dataset,
185+
output_path: Optional[str] = None,
186+
output_filename: str = "subplot_weak_dataset",
187+
output_format: Optional[str] = None,
188+
title_prefix: Optional[str] = None,
189+
):
190+
"""
191+
Produce a 2x2 subplot mosaic visualising a ``WeakDataset``.
192+
193+
Panels: shear field, noise map, shear magnitude, shear position angle.
194+
"""
195+
fig, axes = subplots(2, 2, figsize=conf_subplot_figsize(2, 2))
196+
ax_quiver, ax_noise, ax_mag, ax_phi = (
197+
axes[0, 0],
198+
axes[0, 1],
199+
axes[1, 0],
200+
axes[1, 1],
201+
)
202+
203+
_prefix = f"{title_prefix.rstrip()} " if title_prefix else ""
204+
name_part = f" — {dataset.name}" if dataset.name else ""
205+
206+
plot_shear_yx_2d(
207+
shear_yx=dataset.shear_yx,
208+
ax=ax_quiver,
209+
title=f"{_prefix}Shear Field{name_part}",
210+
)
211+
plot_noise_map(
212+
dataset=dataset,
213+
ax=ax_noise,
214+
title=f"{_prefix}Noise Map{name_part}",
215+
)
216+
plot_ellipticities(
217+
shear_yx=dataset.shear_yx,
218+
ax=ax_mag,
219+
title=f"{_prefix}Shear Magnitude{name_part}",
220+
)
221+
plot_phis(
222+
shear_yx=dataset.shear_yx,
223+
ax=ax_phi,
224+
title=f"{_prefix}Position Angle{name_part}",
225+
)
226+
227+
tight_layout()
228+
save_figure(
229+
fig,
230+
path=output_path,
231+
filename=output_filename,
232+
format=output_format,
233+
)

test_autolens/weak/plot/__init__.py

Whitespace-only changes.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from pathlib import Path
2+
3+
import autoarray as aa
4+
import autolens as al
5+
6+
import pytest
7+
8+
from autolens.weak.plot.weak_dataset_plots import (
9+
plot_shear_yx_2d,
10+
plot_ellipticities,
11+
plot_phis,
12+
plot_noise_map,
13+
subplot_weak_dataset,
14+
)
15+
16+
directory = Path(__file__).resolve().parent
17+
18+
19+
def _isothermal_tracer():
20+
lens = al.Galaxy(
21+
redshift=0.5,
22+
mass=al.mp.Isothermal(centre=(0.0, 0.0), einstein_radius=1.6),
23+
)
24+
source = al.Galaxy(redshift=1.0)
25+
return al.Tracer(galaxies=[lens, source])
26+
27+
28+
@pytest.fixture(name="weak_dataset")
29+
def make_weak_dataset():
30+
"""Deterministic 4-galaxy WeakDataset built from an Isothermal lens."""
31+
grid = aa.Grid2DIrregular(
32+
values=[(0.7, 0.5), (1.0, 1.0), (-0.3, 0.6), (-1.1, -0.8)]
33+
)
34+
simulator = al.SimulatorShearYX(noise_sigma=0.0, seed=0)
35+
return simulator.via_tracer_from(
36+
tracer=_isothermal_tracer(), grid=grid, name="test"
37+
)
38+
39+
40+
@pytest.fixture(name="plot_path")
41+
def make_plot_path():
42+
return directory / "files" / "plots" / "weak_dataset"
43+
44+
45+
def test__plot_shear_yx_2d(weak_dataset, plot_path, plot_patch):
46+
plot_shear_yx_2d(
47+
shear_yx=weak_dataset.shear_yx,
48+
output_path=plot_path,
49+
output_filename="shear_yx",
50+
output_format="png",
51+
)
52+
assert str(plot_path / "shear_yx.png") in plot_patch.paths
53+
54+
55+
def test__plot_ellipticities(weak_dataset, plot_path, plot_patch):
56+
plot_ellipticities(
57+
shear_yx=weak_dataset.shear_yx,
58+
output_path=plot_path,
59+
output_filename="shear_ellipticities",
60+
output_format="png",
61+
)
62+
assert str(plot_path / "shear_ellipticities.png") in plot_patch.paths
63+
64+
65+
def test__plot_phis(weak_dataset, plot_path, plot_patch):
66+
plot_phis(
67+
shear_yx=weak_dataset.shear_yx,
68+
output_path=plot_path,
69+
output_filename="shear_phis",
70+
output_format="png",
71+
)
72+
assert str(plot_path / "shear_phis.png") in plot_patch.paths
73+
74+
75+
def test__plot_noise_map(weak_dataset, plot_path, plot_patch):
76+
plot_noise_map(
77+
dataset=weak_dataset,
78+
output_path=plot_path,
79+
output_filename="noise_map",
80+
output_format="png",
81+
)
82+
assert str(plot_path / "noise_map.png") in plot_patch.paths
83+
84+
85+
def test__subplot_weak_dataset(weak_dataset, plot_path, plot_patch):
86+
subplot_weak_dataset(
87+
dataset=weak_dataset,
88+
output_path=plot_path,
89+
output_filename="subplot_weak_dataset",
90+
output_format="png",
91+
)
92+
assert str(plot_path / "subplot_weak_dataset.png") in plot_patch.paths

0 commit comments

Comments
 (0)