Skip to content

Commit 220d03a

Browse files
authored
Merge pull request #247 from Jammy2211/feature/visualization_final
Visualization final: config origin, fits API, output mode
2 parents 772294f + 248e7ff commit 220d03a

File tree

14 files changed

+253
-222
lines changed

14 files changed

+253
-222
lines changed

CLAUDE.md

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,44 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
99
pip install -e ".[dev]"
1010
```
1111

12-
### Run Tests
13-
```bash
14-
# All tests
15-
python -m pytest test_autoarray/
12+
### Run Tests
13+
```bash
14+
# All tests
15+
python -m pytest test_autoarray/
1616

1717
# Single test file
1818
python -m pytest test_autoarray/structures/test_arrays.py
1919

20-
# With output
21-
python -m pytest test_autoarray/structures/test_arrays.py -s
22-
```
23-
24-
### Codex / sandboxed runs
25-
26-
When running Python from Codex or any restricted environment, set writable cache directories so `numba` and `matplotlib` do not fail on unwritable home or source-tree paths:
27-
28-
```bash
29-
NUMBA_CACHE_DIR=/tmp/numba_cache MPLCONFIGDIR=/tmp/matplotlib python -m pytest test_autoarray/
30-
```
31-
32-
This workspace is often imported from `/mnt/c/...` and Codex may not be able to write to module `__pycache__` directories or `/home/jammy/.cache`, which can cause import-time `numba` caching failures without this override.
33-
34-
### Formatting
35-
```bash
36-
black autoarray/
20+
# With output
21+
python -m pytest test_autoarray/structures/test_arrays.py -s
22+
```
23+
24+
### Codex / sandboxed runs
25+
26+
When running Python from Codex or any restricted environment, set writable cache directories so `numba` and `matplotlib` do not fail on unwritable home or source-tree paths:
27+
28+
```bash
29+
NUMBA_CACHE_DIR=/tmp/numba_cache MPLCONFIGDIR=/tmp/matplotlib python -m pytest test_autoarray/
30+
```
31+
32+
This workspace is often imported from `/mnt/c/...` and Codex may not be able to write to module `__pycache__` directories or `/home/jammy/.cache`, which can cause import-time `numba` caching failures without this override.
33+
34+
### Formatting
35+
```bash
36+
black autoarray/
37+
```
38+
39+
### Plot Output Mode
40+
41+
Set `PYAUTOARRAY_OUTPUT_MODE=1` to capture every figure produced by a script into numbered PNG files in `./output_mode/<script_name>/`. This is useful for visually inspecting all plots from an integration test without needing a display.
42+
43+
```bash
44+
PYAUTOARRAY_OUTPUT_MODE=1 python scripts/my_script.py
45+
# -> ./output_mode/my_script/0_fit.png, 1_tracer.png, ...
3746
```
3847

48+
When this env var is set, all `save_figure`, `subplot_save`, and `_save_subplot` calls are intercepted — the normal output path is bypassed and figures are written sequentially to the output_mode directory instead.
49+
3950
## Architecture
4051

4152
**PyAutoArray** is the low-level data structures and numerical utilities package for the PyAuto ecosystem. It provides:

autoarray/abstract_ndarray.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import numpy as np
99

10-
from autoconf.fitsable import output_to_fits
10+
1111

1212
from typing import TYPE_CHECKING
1313

@@ -135,11 +135,6 @@ def with_new_array(self, array: np.ndarray) -> "AbstractNDArray":
135135
new_array._array = array
136136
return new_array
137137

138-
def flip_hdu_for_ds9(self, values):
139-
if conf.instance["general"]["fits"]["flip_for_ds9"]:
140-
return self._xp.flipud(values)
141-
return values
142-
143138
def copy(self):
144139
new = copy(self)
145140
return new
@@ -272,24 +267,6 @@ def native(self) -> Structure:
272267
Returns the data structure in its `native` format which contains all unmaksed values to the native dimensions.
273268
"""
274269

275-
def output_to_fits(self, file_path: str, overwrite: bool = False):
276-
"""
277-
Output the grid to a .fits file.
278-
279-
Parameters
280-
----------
281-
file_path
282-
The path the file is output to, including the filename and the .fits extension, e.g. '/path/to/filename.fits'
283-
overwrite
284-
If a file already exists at the path, if overwrite=True it is overwritten else an error is raised.
285-
"""
286-
output_to_fits(
287-
values=self.native.array.astype("float"),
288-
file_path=file_path,
289-
overwrite=overwrite,
290-
header_dict=self.mask.header_dict,
291-
)
292-
293270
@property
294271
def shape(self):
295272
try:

autoarray/config/general.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
fits:
2-
flip_for_ds9: false # If True, the image is flipped before output to a .fits file, which is useful for viewing in DS9.
31
psf:
42
use_fft_default: true # If True, PSFs are convolved using FFTs by default, which is faster and uses less memory in all cases except for very small PSFs, False uses direct convolution.
53
inversion:

autoarray/dataset/imaging/dataset.py

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -542,38 +542,3 @@ def apply_sparse_operator_cpu(
542542
sparse_operator=sparse_operator,
543543
)
544544

545-
def output_to_fits(
546-
self,
547-
data_path: Union[Path, str],
548-
psf_path: Optional[Union[Path, str]] = None,
549-
noise_map_path: Optional[Union[Path, str]] = None,
550-
overwrite: bool = False,
551-
):
552-
"""
553-
Output an imaging dataset to multiple .fits file.
554-
555-
For each attribute of the imaging data (e.g. `data`, `noise_map`) the path to
556-
the .fits can be specified, with `hdu=0` assumed automatically.
557-
558-
If the `data` has been masked, the masked data is output to .fits files. A mask can be separately output to
559-
a file `mask.fits` via the `Mask` objects `output_to_fits` method.
560-
561-
Parameters
562-
----------
563-
data_path
564-
The path to the data .fits file where the image data is output (e.g. '/path/to/data.fits').
565-
psf_path
566-
The path to the psf .fits file where the psf is output (e.g. '/path/to/psf.fits').
567-
noise_map_path
568-
The path to the noise_map .fits where the noise_map is output (e.g. '/path/to/noise_map.fits').
569-
overwrite
570-
If `True`, the .fits files are overwritten if they already exist, if `False` they are not and an
571-
exception is raised.
572-
"""
573-
self.data.output_to_fits(file_path=data_path, overwrite=overwrite)
574-
575-
if self.psf is not None and psf_path is not None:
576-
self.psf.kernel.output_to_fits(file_path=psf_path, overwrite=overwrite)
577-
578-
if self.noise_map is not None and noise_map_path is not None:
579-
self.noise_map.output_to_fits(file_path=noise_map_path, overwrite=overwrite)

autoarray/dataset/interferometer/dataset.py

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import numpy as np
33
from typing import Optional
44

5-
from autoconf.fitsable import ndarray_via_fits_from, output_to_fits
5+
from autoconf.fitsable import ndarray_via_fits_from
66
from autoconf import cached_property
77

88
from autoarray.dataset.abstract.dataset import AbstractDataset
@@ -424,47 +424,6 @@ def signal_to_noise_map(self):
424424
signal_to_noise_map_real + 1j * signal_to_noise_map_imag
425425
)
426426

427-
def output_to_fits(
428-
self,
429-
data_path=None,
430-
noise_map_path=None,
431-
uv_wavelengths_path=None,
432-
overwrite=False,
433-
):
434-
"""
435-
Output the interferometer dataset to multiple .fits files.
436-
437-
Each component (visibilities, noise map, uv_wavelengths) is saved to its own .fits file.
438-
Any path set to `None` means that component is not saved.
439-
440-
Parameters
441-
----------
442-
data_path
443-
The path where the visibility data is saved (e.g. '/path/to/visibilities.fits').
444-
If `None`, the visibilities are not saved.
445-
noise_map_path
446-
The path where the noise map is saved (e.g. '/path/to/noise_map.fits').
447-
If `None`, the noise map is not saved.
448-
uv_wavelengths_path
449-
The path where the uv_wavelengths array is saved (e.g. '/path/to/uv_wavelengths.fits').
450-
If `None`, the uv_wavelengths are not saved.
451-
overwrite
452-
If `True`, existing .fits files are overwritten. If `False`, an exception is raised
453-
if a file already exists at the given path.
454-
"""
455-
if data_path is not None:
456-
self.data.output_to_fits(file_path=data_path, overwrite=overwrite)
457-
458-
if self.noise_map is not None and noise_map_path is not None:
459-
self.noise_map.output_to_fits(file_path=noise_map_path, overwrite=overwrite)
460-
461-
if self.uv_wavelengths is not None and uv_wavelengths_path is not None:
462-
output_to_fits(
463-
values=self.uv_wavelengths,
464-
file_path=uv_wavelengths_path,
465-
overwrite=overwrite,
466-
)
467-
468427
@property
469428
def psf(self):
470429
"""

autoarray/dataset/plot/imaging_plots.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,75 @@ def subplot_imaging_dataset_list(
181181
plot_array(dataset.signal_to_noise_map, ax=axes[i][2], title="Signal-To-Noise Map")
182182
plt.tight_layout()
183183
subplot_save(fig, output_path, output_filename, output_format)
184+
185+
186+
def fits_imaging(
187+
dataset,
188+
file_path=None,
189+
data_path=None,
190+
psf_path=None,
191+
noise_map_path=None,
192+
overwrite=False,
193+
):
194+
"""Write an ``Imaging`` dataset to FITS.
195+
196+
Supports two modes:
197+
198+
* **Separate files** — pass ``data_path``, ``psf_path``, ``noise_map_path``
199+
to write each component to its own single-HDU FITS file.
200+
* **Single multi-HDU file** — pass ``file_path`` to write all components
201+
into one FITS file with named extensions (``mask``, ``data``, ``psf``,
202+
``noise_map``).
203+
204+
Parameters
205+
----------
206+
dataset
207+
The ``Imaging`` dataset to write.
208+
file_path : str or Path, optional
209+
Path for a single multi-HDU FITS file.
210+
data_path, psf_path, noise_map_path : str or Path, optional
211+
Paths for individual component files.
212+
overwrite : bool
213+
If ``True`` existing files are replaced.
214+
"""
215+
from autoconf.fitsable import output_to_fits, hdu_list_for_output_from, write_hdu_list
216+
217+
header_dict = dataset.data.mask.header_dict if hasattr(dataset.data.mask, "header_dict") else None
218+
219+
if file_path is not None:
220+
values_list = [dataset.data.mask.astype("float")]
221+
ext_name_list = ["mask"]
222+
223+
values_list.append(dataset.data.native.array.astype("float"))
224+
ext_name_list.append("data")
225+
226+
if dataset.psf is not None:
227+
values_list.append(dataset.psf.kernel.native.array.astype("float"))
228+
ext_name_list.append("psf")
229+
230+
if dataset.noise_map is not None:
231+
values_list.append(dataset.noise_map.native.array.astype("float"))
232+
ext_name_list.append("noise_map")
233+
234+
hdu_list = hdu_list_for_output_from(
235+
values_list=values_list,
236+
ext_name_list=ext_name_list,
237+
header_dict=header_dict,
238+
)
239+
write_hdu_list(hdu_list, file_path=file_path, overwrite=overwrite)
240+
else:
241+
if data_path is not None:
242+
output_to_fits(
243+
values=dataset.data.native.array.astype("float"),
244+
file_path=data_path, overwrite=overwrite, header_dict=header_dict,
245+
)
246+
if dataset.psf is not None and psf_path is not None:
247+
output_to_fits(
248+
values=dataset.psf.kernel.native.array.astype("float"),
249+
file_path=psf_path, overwrite=overwrite,
250+
)
251+
if dataset.noise_map is not None and noise_map_path is not None:
252+
output_to_fits(
253+
values=dataset.noise_map.native.array.astype("float"),
254+
file_path=noise_map_path, overwrite=overwrite, header_dict=header_dict,
255+
)

autoarray/dataset/plot/interferometer_plots.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def subplot_interferometer_dataset(
1919
use_log10: bool = False,
2020
):
2121
"""
22-
2×3 subplot of interferometer dataset components.
22+
2x3 subplot of interferometer dataset components.
2323
2424
Panels: Visibilities | UV-Wavelengths | Amplitudes vs UV-distances |
2525
Phases vs UV-distances | Dirty Image | Dirty S/N Map
@@ -100,7 +100,7 @@ def subplot_interferometer_dirty_images(
100100
use_log10: bool = False,
101101
):
102102
"""
103-
1×3 subplot of dirty image, dirty noise map, and dirty S/N map.
103+
1x3 subplot of dirty image, dirty noise map, and dirty S/N map.
104104
105105
Parameters
106106
----------
@@ -144,3 +144,72 @@ def subplot_interferometer_dirty_images(
144144
hide_unused_axes(axes)
145145
plt.tight_layout()
146146
subplot_save(fig, output_path, output_filename, output_format)
147+
148+
149+
def fits_interferometer(
150+
dataset,
151+
file_path=None,
152+
data_path=None,
153+
noise_map_path=None,
154+
uv_wavelengths_path=None,
155+
overwrite=False,
156+
):
157+
"""Write an ``Interferometer`` dataset to FITS.
158+
159+
Supports two modes:
160+
161+
* **Separate files** -- pass ``data_path``, ``noise_map_path``,
162+
``uv_wavelengths_path`` to write each component to its own FITS file.
163+
* **Single multi-HDU file** -- pass ``file_path`` to write all components
164+
into one FITS file with named extensions (``data``, ``noise_map``,
165+
``uv_wavelengths``).
166+
167+
Parameters
168+
----------
169+
dataset
170+
The ``Interferometer`` dataset to write.
171+
file_path : str or Path, optional
172+
Path for a single multi-HDU FITS file.
173+
data_path, noise_map_path, uv_wavelengths_path : str or Path, optional
174+
Paths for individual component files.
175+
overwrite : bool
176+
If ``True`` existing files are replaced.
177+
"""
178+
from autoconf.fitsable import output_to_fits, hdu_list_for_output_from, write_hdu_list
179+
180+
if file_path is not None:
181+
values_list = []
182+
ext_name_list = []
183+
184+
values_list.append(np.asarray(dataset.data.in_array))
185+
ext_name_list.append("data")
186+
187+
if dataset.noise_map is not None:
188+
values_list.append(np.asarray(dataset.noise_map.in_array))
189+
ext_name_list.append("noise_map")
190+
191+
if dataset.uv_wavelengths is not None:
192+
values_list.append(np.asarray(dataset.uv_wavelengths))
193+
ext_name_list.append("uv_wavelengths")
194+
195+
hdu_list = hdu_list_for_output_from(
196+
values_list=values_list,
197+
ext_name_list=ext_name_list,
198+
)
199+
write_hdu_list(hdu_list, file_path=file_path, overwrite=overwrite)
200+
else:
201+
if data_path is not None:
202+
output_to_fits(
203+
values=np.asarray(dataset.data.in_array),
204+
file_path=data_path, overwrite=overwrite,
205+
)
206+
if dataset.noise_map is not None and noise_map_path is not None:
207+
output_to_fits(
208+
values=np.asarray(dataset.noise_map.in_array),
209+
file_path=noise_map_path, overwrite=overwrite,
210+
)
211+
if dataset.uv_wavelengths is not None and uv_wavelengths_path is not None:
212+
output_to_fits(
213+
values=dataset.uv_wavelengths,
214+
file_path=uv_wavelengths_path, overwrite=overwrite,
215+
)

0 commit comments

Comments
 (0)