fitscube 2.3.0__tar.gz → 2.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fitscube-2.3.0 → fitscube-2.3.2}/PKG-INFO +2 -1
- fitscube-2.3.2/fitscube/_version.py +24 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/fitscube/bounding_box.py +7 -3
- {fitscube-2.3.0 → fitscube-2.3.2}/fitscube/combine_fits.py +89 -15
- {fitscube-2.3.0 → fitscube-2.3.2}/pyproject.toml +2 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/tests/conftest.py +29 -0
- fitscube-2.3.2/tests/test_bb.py +97 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/tests/test_frequencies.py +14 -1
- {fitscube-2.3.0 → fitscube-2.3.2}/tests/test_times.py +118 -0
- fitscube-2.3.0/fitscube/_version.py +0 -34
- {fitscube-2.3.0 → fitscube-2.3.2}/.github/CONTRIBUTING.md +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/.github/dependabot.yml +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/.github/release.yml +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/.github/workflows/cd.yml +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/.github/workflows/ci.yml +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/.gitignore +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/.pre-commit-config.yaml +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/CHANGELOG.md +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/LICENSE +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/README.md +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/fitscube/__init__.py +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/fitscube/asyncio.py +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/fitscube/cli.py +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/fitscube/exceptions.py +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/fitscube/extract.py +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/fitscube/logging.py +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/fitscube/version.pyi +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/noxfile.py +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/tests/__init__.py +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/tests/data/cube.zip +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/tests/data/images.zip +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/tests/data/time_images.zip +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/tests/data/timecube.zip +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/tests/test_extract.py +0 -0
- {fitscube-2.3.0 → fitscube-2.3.2}/tests/test_package.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fitscube
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: A package to produce produce FITS cubes.
|
|
5
5
|
Project-URL: Homepage, https://github.com/AlecThomson/fitscube
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/AlecThomson/fitscube/issues
|
|
@@ -58,6 +58,7 @@ Requires-Dist: sphinx-autodoc-typehints; extra == 'docs'
|
|
|
58
58
|
Requires-Dist: sphinx-copybutton; extra == 'docs'
|
|
59
59
|
Requires-Dist: sphinx>=7.0; extra == 'docs'
|
|
60
60
|
Provides-Extra: test
|
|
61
|
+
Requires-Dist: pytest-asyncio; extra == 'test'
|
|
61
62
|
Requires-Dist: pytest-cov>=3; extra == 'test'
|
|
62
63
|
Requires-Dist: pytest>=6; extra == 'test'
|
|
63
64
|
Provides-Extra: uvloop
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '2.3.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 3, 2)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -15,16 +15,17 @@ from fitscube.logging import logger
|
|
|
15
15
|
|
|
16
16
|
@dataclass(frozen=True)
|
|
17
17
|
class BoundingBox:
|
|
18
|
-
"""Simple container to represent a bounding box
|
|
18
|
+
"""Simple container to represent a bounding box. Maximum values can be
|
|
19
|
+
used as is when slicing."""
|
|
19
20
|
|
|
20
21
|
xmin: int
|
|
21
22
|
"""Minimum x pixel"""
|
|
22
23
|
xmax: int
|
|
23
24
|
"""Maximum x pixel"""
|
|
24
25
|
ymin: int
|
|
25
|
-
"""Minimum y pixel"""
|
|
26
|
+
"""Minimum y pixel. Can be used as is in slice (e.g. is exclusive). """
|
|
26
27
|
ymax: int
|
|
27
|
-
"""Maximum y pixel"""
|
|
28
|
+
"""Maximum y pixel Can be used as is in slice (e.g. is exclusive)."""
|
|
28
29
|
original_shape: tuple[int, int]
|
|
29
30
|
"""The original shape of the image. If constructed against a cube this is the shape of a single plane."""
|
|
30
31
|
y_span: int
|
|
@@ -62,6 +63,9 @@ def create_bound_box_plane(image_data: np.ndarray) -> BoundingBox | None:
|
|
|
62
63
|
xmin, xmax = np.where(x_valid)[0][[0, -1]]
|
|
63
64
|
ymin, ymax = np.where(y_valid)[0][[0, -1]]
|
|
64
65
|
|
|
66
|
+
xmax += 1
|
|
67
|
+
ymax += 1
|
|
68
|
+
|
|
65
69
|
y_span = ymax - ymin
|
|
66
70
|
x_span = xmax - xmin
|
|
67
71
|
|
|
@@ -16,10 +16,7 @@ import asyncio
|
|
|
16
16
|
import warnings
|
|
17
17
|
from io import BufferedRandom
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import
|
|
20
|
-
NamedTuple,
|
|
21
|
-
TypeVar,
|
|
22
|
-
)
|
|
19
|
+
from typing import Literal, NamedTuple, TypeVar
|
|
23
20
|
|
|
24
21
|
import astropy.units as u
|
|
25
22
|
import numpy as np
|
|
@@ -50,6 +47,8 @@ BIT_DICT = {
|
|
|
50
47
|
16: 2,
|
|
51
48
|
8: 1,
|
|
52
49
|
}
|
|
50
|
+
FLOAT_LENGTH = Literal[16, 32, 64]
|
|
51
|
+
FLOAT_TYPE = {64: ">f8", 32: ">f4"}
|
|
53
52
|
|
|
54
53
|
warnings.filterwarnings("ignore", category=UserWarning, module="astropy.io.fits")
|
|
55
54
|
warnings.filterwarnings("ignore", category=VerifyWarning)
|
|
@@ -133,6 +132,41 @@ def isin_close(
|
|
|
133
132
|
return np.isclose(element[:, None], test_element, atol, rtol).any(1)
|
|
134
133
|
|
|
135
134
|
|
|
135
|
+
def grid_step(diffs: ArrayLike) -> float:
|
|
136
|
+
"""Recover the fundamental channel step from the observed diffs.
|
|
137
|
+
|
|
138
|
+
On a regular grid with missing channels every diff is an integer multiple
|
|
139
|
+
of the channel width, so the gcd of the diffs recovers that width even when
|
|
140
|
+
no two surviving channels are adjacent -- the case where ``min(diffs)``
|
|
141
|
+
overestimates the step (e.g. present channels 0, 3, 5 give ``min == 2`` but
|
|
142
|
+
the true step is 1). Falls back to ``min(diffs)`` if the gcd is numerically
|
|
143
|
+
unstable or does not divide every diff, so the common case is unchanged (any
|
|
144
|
+
surviving adjacent pair makes the gcd equal the minimum diff exactly).
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
diffs (ArrayLike): Differences between consecutive spec values.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
float: Estimated regular grid step.
|
|
151
|
+
"""
|
|
152
|
+
diffs = np.abs(np.asarray(diffs, dtype=np.longdouble))
|
|
153
|
+
min_diff = np.min(diffs)
|
|
154
|
+
tol = np.max(diffs) * 1e-6
|
|
155
|
+
|
|
156
|
+
g = diffs[0]
|
|
157
|
+
for d in diffs[1:]:
|
|
158
|
+
a, b = (g, d) if g >= d else (d, g)
|
|
159
|
+
# Tolerant Euclid: stop once the remainder is within the noise floor.
|
|
160
|
+
while b > tol:
|
|
161
|
+
a, b = b, a % b
|
|
162
|
+
g = a
|
|
163
|
+
|
|
164
|
+
divides_all = np.all(np.abs(np.round(diffs / g) - diffs / g) <= 1e-3)
|
|
165
|
+
if g <= tol or not divides_all:
|
|
166
|
+
return float(min_diff)
|
|
167
|
+
return float(g)
|
|
168
|
+
|
|
169
|
+
|
|
136
170
|
def even_spacing(specs: u.Quantity, time_domain_mode: bool = False) -> SpequencyInfo:
|
|
137
171
|
"""Make the frequencies or times evenly spaced.
|
|
138
172
|
|
|
@@ -144,9 +178,9 @@ def even_spacing(specs: u.Quantity, time_domain_mode: bool = False) -> Spequency
|
|
|
144
178
|
"""
|
|
145
179
|
specs_arr = specs.value.astype(np.longdouble)
|
|
146
180
|
diffs = np.diff(specs_arr)
|
|
147
|
-
|
|
148
|
-
# Create a new array with the
|
|
149
|
-
new_specs = np_arange_fix(specs_arr[0], specs_arr[-1],
|
|
181
|
+
step = grid_step(diffs)
|
|
182
|
+
# Create a new array with the fundamental grid step
|
|
183
|
+
new_specs = np_arange_fix(specs_arr[0], specs_arr[-1], step)
|
|
150
184
|
missing_chan_idx = np.logical_not(
|
|
151
185
|
isin_close(new_specs, specs_arr, time_domain_mode)
|
|
152
186
|
)
|
|
@@ -247,19 +281,26 @@ async def create_output_cube_coro(
|
|
|
247
281
|
overwrite: bool = False,
|
|
248
282
|
time_domain_mode: bool = False,
|
|
249
283
|
bounding_box: BoundingBox | None = None,
|
|
284
|
+
float_length: FLOAT_LENGTH | None = None,
|
|
250
285
|
) -> InitResult:
|
|
251
|
-
"""
|
|
286
|
+
"""Generate the output header and write a dummy cube to disk based on properties of the
|
|
287
|
+
inpute data. The output cube written here has a correctly formed header and a pre-zerod
|
|
288
|
+
data cube written as output.
|
|
252
289
|
|
|
253
290
|
Args:
|
|
254
|
-
old_name (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
291
|
+
old_name (Path): The path to a representative image to draw the base fits header from
|
|
292
|
+
out_cube (Path): Path of the output cube to create
|
|
293
|
+
specs (u.Quantity): Specification of the unit that denotes the 'cube' axis
|
|
294
|
+
ignore_spec (bool, optional): Whether the provided `specs` axis should be ignored. If True dummy placeholder fields added. Defaults to False.
|
|
295
|
+
has_beams (bool, optional): Indicates whether a CASA Beam table will also be generated and added to the output cube. Defaults to False.
|
|
296
|
+
single_beam (bool, optional): Indicates whether a constant restoring beam has been used among all input images. If so only the beam fields are need. Defaults to False.
|
|
297
|
+
overwrite (bool, optional): If True the out the output cube will overwrite any existing file. Defaults to False.
|
|
298
|
+
time_domain_mode (bool, optional): Whether to join images via the DATE-OBS (e.g. time) axis. If False cube joined along frequency axis. Defaults to False.
|
|
299
|
+
bounding_box (BoundingBox | None, optional): Whether a trimming operation should be applied to input images. If a BoundingBox is supplied this will be used to update the reference pixel position and data shape indicators. Defaults to None.
|
|
300
|
+
float_length (Literal[16, 32, 64] | None, optional): The precision of the output data. If None drawn from input data. Otherwise values accepted are 16, 32 and 64. Defaults to None.
|
|
260
301
|
|
|
261
302
|
Returns:
|
|
262
|
-
InitResult:
|
|
303
|
+
InitResult: Details of the output cube, including the output header
|
|
263
304
|
"""
|
|
264
305
|
|
|
265
306
|
# define units if in time or freq domain
|
|
@@ -380,6 +421,23 @@ async def create_output_cube_coro(
|
|
|
380
421
|
else:
|
|
381
422
|
cube_shape[idx] = n_chan
|
|
382
423
|
|
|
424
|
+
logger.critical(f"{float_length=} {new_header['BITPIX']=}")
|
|
425
|
+
if float_length is not None:
|
|
426
|
+
# Per astropy docs
|
|
427
|
+
# bITPIX numpy data type
|
|
428
|
+
# 8 numpy.uint8 (note it is UNsigned integer)
|
|
429
|
+
# 16 numpy.int16
|
|
430
|
+
# 32 numpy.int32
|
|
431
|
+
# 64 numpy.int64
|
|
432
|
+
# -32 numpy.float32
|
|
433
|
+
# -64 numpy.float64
|
|
434
|
+
assert float_length in list(BIT_DICT.keys()), (
|
|
435
|
+
f"{float_length=} not in {BIT_DICT=}"
|
|
436
|
+
)
|
|
437
|
+
bit_pix = int(float_length)
|
|
438
|
+
logger.info(f"Specified {float_length=}, corresponding to {bit_pix=}")
|
|
439
|
+
new_header["BITPIX"] = -abs(bit_pix)
|
|
440
|
+
|
|
383
441
|
output_header = await create_cube_from_scratch_coro(
|
|
384
442
|
output_file=out_cube, output_header=new_header, overwrite=overwrite
|
|
385
443
|
)
|
|
@@ -657,6 +715,11 @@ async def process_channel(
|
|
|
657
715
|
if invalidate_zeros:
|
|
658
716
|
plane[plane == 0.0] = np.nan
|
|
659
717
|
|
|
718
|
+
if "BITPIX" in new_header:
|
|
719
|
+
bit_pix = abs(new_header["BITPIX"])
|
|
720
|
+
float_type = FLOAT_TYPE[bit_pix]
|
|
721
|
+
plane = plane.astype(float_type)
|
|
722
|
+
|
|
660
723
|
await write_channel_to_cube_coro(
|
|
661
724
|
file_handle=file_handle,
|
|
662
725
|
plane=plane,
|
|
@@ -678,6 +741,7 @@ async def combine_fits_coro(
|
|
|
678
741
|
time_domain_mode: bool = False,
|
|
679
742
|
bounding_box: bool = False,
|
|
680
743
|
invalidate_zeros: bool = False,
|
|
744
|
+
float_length: FLOAT_LENGTH | None = None,
|
|
681
745
|
) -> u.Quantity:
|
|
682
746
|
"""Combine FITS files into a cube.
|
|
683
747
|
Can handle either frequency or time dimensions agnostically
|
|
@@ -690,6 +754,7 @@ async def combine_fits_coro(
|
|
|
690
754
|
time_domain_mode (bool, optional): Work in time domain mode - make a time-cube. Default = False.
|
|
691
755
|
bounding_box (bool, optional): Clip invalid/padded pixels when crafting the fits cube. When True an extra read of the input daata is needed, but output cube is smaller. Defaults to False.
|
|
692
756
|
invalidate_zeros (bool, optionals): Set pixels whose values are exactly zero to NaNs. Defaults to False.
|
|
757
|
+
float_length (Literal[16, 32, 64] | None, optional): The floating point precision in bits to use when creating the output cube. If None the size of the input data are used. Defaults to None.
|
|
693
758
|
|
|
694
759
|
Returns:
|
|
695
760
|
tuple[fits.HDUList, u.Quantity]: The combined FITS cube and frequencies
|
|
@@ -760,6 +825,7 @@ async def combine_fits_coro(
|
|
|
760
825
|
overwrite=overwrite,
|
|
761
826
|
time_domain_mode=time_domain_mode,
|
|
762
827
|
bounding_box=final_bounding_box,
|
|
828
|
+
float_length=float_length,
|
|
763
829
|
)
|
|
764
830
|
|
|
765
831
|
new_channels = np.arange(len(specs))
|
|
@@ -881,6 +947,13 @@ def get_parser(
|
|
|
881
947
|
action="store_true",
|
|
882
948
|
help="Set pixels whose values are exactly zero to NaNs",
|
|
883
949
|
)
|
|
950
|
+
parser.add_argument(
|
|
951
|
+
"--floating",
|
|
952
|
+
type=int,
|
|
953
|
+
choices=(8, 16, 32, 64),
|
|
954
|
+
default=None,
|
|
955
|
+
help="The number of floating point bits to use in the out cube. If None the input data precision is used.",
|
|
956
|
+
)
|
|
884
957
|
|
|
885
958
|
return parser
|
|
886
959
|
|
|
@@ -923,6 +996,7 @@ def cli(args: argparse.Namespace | None = None) -> None:
|
|
|
923
996
|
time_domain_mode=time_domain_mode,
|
|
924
997
|
bounding_box=args.bounding_box,
|
|
925
998
|
invalidate_zeros=args.invalidate_zeros,
|
|
999
|
+
float_length=args.floating,
|
|
926
1000
|
)
|
|
927
1001
|
|
|
928
1002
|
spequency = "times" if time_domain_mode else "frequencies"
|
|
@@ -38,6 +38,7 @@ dependencies = [
|
|
|
38
38
|
[project.optional-dependencies]
|
|
39
39
|
test = [
|
|
40
40
|
"pytest >=6",
|
|
41
|
+
"pytest-asyncio",
|
|
41
42
|
"pytest-cov >=3",
|
|
42
43
|
]
|
|
43
44
|
dev = [
|
|
@@ -82,6 +83,7 @@ addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"]
|
|
|
82
83
|
xfail_strict = true
|
|
83
84
|
filterwarnings = [
|
|
84
85
|
"error",
|
|
86
|
+
"ignore:The TestRunner",
|
|
85
87
|
]
|
|
86
88
|
log_cli_level = "INFO"
|
|
87
89
|
testpaths = [
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from shutil import unpack_archive
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
|
+
from astropy.io import fits
|
|
7
8
|
|
|
8
9
|
EXAMPLE_HEADER = "SIMPLE = T / conforms to FITS standard BITPIX = -32 / array data type NAXIS = 4 / number of array dimensions NAXIS1 = 6192 NAXIS2 = 6192 NAXIS3 = 1 NAXIS4 = 72 EXTEND = T BSCALE = 1.0 BZERO = 0.0 BUNIT = 'JY/BEAM ' BMAJ = 0.00398184964207042 BMIN = 0.00332268385509243 BPA = 77.3858939868987 EQUINOX = 2000.0 LONPOLE = 180.0 BTYPE = 'Intensity' TELESCOP= 'ASKAP ' OBJECT = 'EMU_1141-55' ORIGIN = 'WSClean ' CTYPE1 = 'RA---SIN' CRPIX1 = 3097.0 CRVAL1 = 173.522555019647 CDELT1 = -0.000625 CUNIT1 = 'deg ' CTYPE2 = 'DEC--SIN' CRPIX2 = 3097.0 CRVAL2 = -55.3190947400628 CDELT2 = 0.000625 CUNIT2 = 'deg ' CTYPE3 = 'STOKES ' CRPIX3 = 1.0 CRVAL3 = 1.0 CDELT3 = 1.0 CUNIT3 = '' CTYPE4 = 'FREQ ' CRPIX4 = 1 CRVAL4 = 801490740.740741 CDELT4 = 4000000.0 CUNIT4 = 'Hz ' SPECSYS = 'TOPOCENT' DATE-OBS= '2023-01-08T15:36:40.9' WSCDATAC= 'DATA ' WSCVDATE= '2022-10-21' WSCVERSI= '3.2 ' WSCWEIGH= 'Briggs''(-0)' WSCENVIS= 1026544.24656423 WSCFIELD= 0.0 WSCGAIN = 0.1 WSCGKRNL= 7.0 WSCIMGWG= 4052355.35920529 WSCMAJOR= 9.0 WSCMGAIN= 0.9 WSCMINOR= 62860.0 WSCNEGCM= 1.0 WSCNEGST= 0.0 WSCNITER= 5000000.0 WSCNORMF= 4052355.35920529 WSCNVIS = 7648518.0 WSCNWLAY= 1.0 WSCTHRES= 0.0 WSCVWSUM= 30594072.0 COMMENT FITS (Flexible Image Transport System) format is defined in 'AstronomyCOMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H HISTORY wsclean -abs-mem 200 -local-rms-window 20 -size 6192 6192 -local-rms -foHISTORY rce-mask-rounds 4 -auto-mask 8.0 -auto-threshold 3.0 -channels-out 72 -mHISTORY gain 0.9 -nmiter 10 -niter 5000000 -multiscale-scale-bias 0.8 -multiscalHISTORY e-scales 0,4,8,16,24,32,48,64,92,128,196,512,796,1025 -fit-spectral-pol HISTORY 4 -weight briggs -0 -data-column DATA -scale 2.25asec -gridder wgridder HISTORY -wgridder-accuracy 0.0001 -join-channels -minuv-l 50.0 -beam-fitting-sizHISTORY e 1.25 -deconvolution-channels 8 -parallel-gridding 18 -temp-dir /dev/shHISTORY m/gal16b.8974844 -pol i -save-source-list -name /dev/shm/gal16b.8974844/HISTORY SB47138.EMU_1141-55.beam19.i /scratch3/gal16b/emu_download/flint_jollytrHISTORY actor/47138/SB47138.EMU_1141-55.beam19.ms END "
|
|
9
10
|
|
|
@@ -65,3 +66,31 @@ def time_image_paths(tmpdir) -> list[Path]:
|
|
|
65
66
|
image_paths.sort()
|
|
66
67
|
|
|
67
68
|
return image_paths
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.fixture
|
|
72
|
+
def time_image_paths_withzeroborder(tmpdir) -> list[Path]:
|
|
73
|
+
"""Same as above but zero out border pixels in the data"""
|
|
74
|
+
tmp_dir = Path(tmpdir) / "time_images"
|
|
75
|
+
tmp_dir.mkdir(exist_ok=True, parents=True)
|
|
76
|
+
images_zip = Path(__file__).parent / "data" / "time_images.zip"
|
|
77
|
+
|
|
78
|
+
unpack_archive(images_zip, tmp_dir)
|
|
79
|
+
image_paths = list(tmp_dir.glob("*fits"))
|
|
80
|
+
image_paths.sort()
|
|
81
|
+
|
|
82
|
+
output_paths = []
|
|
83
|
+
for image in image_paths:
|
|
84
|
+
with fits.open(image, memmap=False, lazy_load_hdus=False) as a:
|
|
85
|
+
data = a[0].data
|
|
86
|
+
data[..., :5, :] = 0
|
|
87
|
+
data[..., -5:, :] = 0
|
|
88
|
+
data[..., :, :5] = 0
|
|
89
|
+
data[..., :, -5:] = 0
|
|
90
|
+
|
|
91
|
+
a[0].data = data
|
|
92
|
+
image_output = image.with_suffix(".zero.fits")
|
|
93
|
+
fits.writeto(image_output, data=data, header=a[0].header)
|
|
94
|
+
output_paths.append(image_output)
|
|
95
|
+
|
|
96
|
+
return output_paths
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Tests to ensure the bounding box functionality is working
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pytest
|
|
6
|
+
from fitscube.bounding_box import (
|
|
7
|
+
BoundingBox,
|
|
8
|
+
create_bound_box_plane,
|
|
9
|
+
extract_common_bounding_box,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_bound_box_plane() -> None:
|
|
14
|
+
"""See if we can make the bounding box correctly"""
|
|
15
|
+
|
|
16
|
+
image = np.zeros((100, 100))
|
|
17
|
+
|
|
18
|
+
bb = create_bound_box_plane(image_data=image)
|
|
19
|
+
assert isinstance(bb, BoundingBox)
|
|
20
|
+
assert bb.xmin == 0
|
|
21
|
+
assert bb.ymin == 0
|
|
22
|
+
assert bb.xmax == 100
|
|
23
|
+
assert bb.ymax == 100
|
|
24
|
+
assert bb.x_span == 100
|
|
25
|
+
assert bb.y_span == 100
|
|
26
|
+
|
|
27
|
+
image[:, :4] = np.nan
|
|
28
|
+
image[96:, :] = np.nan
|
|
29
|
+
|
|
30
|
+
bb = create_bound_box_plane(image_data=image)
|
|
31
|
+
assert isinstance(bb, BoundingBox)
|
|
32
|
+
assert bb.xmin == 0
|
|
33
|
+
assert bb.ymin == 4
|
|
34
|
+
assert bb.xmax == 96
|
|
35
|
+
assert bb.ymax == 100
|
|
36
|
+
assert bb.x_span == 96
|
|
37
|
+
assert bb.y_span == 96
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_extract_common_bounding_box() -> None:
|
|
41
|
+
"""Construct a set of bounding boxes to ensure a largest fits all
|
|
42
|
+
one can be created"""
|
|
43
|
+
|
|
44
|
+
bbs = []
|
|
45
|
+
for i in range(2, 8, 1):
|
|
46
|
+
image = np.zeros((100, 100))
|
|
47
|
+
|
|
48
|
+
print(i)
|
|
49
|
+
|
|
50
|
+
image[:i, :] = np.nan
|
|
51
|
+
image[:, -i:] = np.nan
|
|
52
|
+
assert np.sum(np.isnan(image)) > 1
|
|
53
|
+
bbs.append(create_bound_box_plane(image_data=image))
|
|
54
|
+
|
|
55
|
+
assert len(bbs) == 6
|
|
56
|
+
|
|
57
|
+
bb = extract_common_bounding_box(bounding_boxes=bbs)
|
|
58
|
+
assert bb.xmin == 2
|
|
59
|
+
assert bb.ymin == 0
|
|
60
|
+
assert bb.xmax == 100
|
|
61
|
+
assert bb.ymax == 98
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_extract_common_bounding_box_error() -> None:
|
|
65
|
+
"""See if the correct errors are raised"""
|
|
66
|
+
|
|
67
|
+
with pytest.raises(ValueError, match="No valid"):
|
|
68
|
+
extract_common_bounding_box(bounding_boxes=[None, None, None])
|
|
69
|
+
|
|
70
|
+
bbs = []
|
|
71
|
+
for i in range(2, 8, 1):
|
|
72
|
+
image = np.zeros((100 + i, 100))
|
|
73
|
+
bbs.append(create_bound_box_plane(image_data=image))
|
|
74
|
+
|
|
75
|
+
with pytest.raises(ValueError, match="Different shapes"):
|
|
76
|
+
extract_common_bounding_box(bounding_boxes=bbs)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# @pytest.mark.asyncio
|
|
80
|
+
# async def test_get_bounding_box_from_fits(time_image_paths) -> None:
|
|
81
|
+
# """!!! NOTE: Sometimes this test can raise the following error:
|
|
82
|
+
|
|
83
|
+
# > Exception ignored in: <socket.socket fd=-1, family=1, type=1, proto=0>
|
|
84
|
+
|
|
85
|
+
# Not sure why it is capable of such a thing.
|
|
86
|
+
# """
|
|
87
|
+
# futures = [
|
|
88
|
+
# await get_bounding_box_for_fits_coro(fits_path=fits_path)
|
|
89
|
+
# for fits_path in time_image_paths
|
|
90
|
+
# ]
|
|
91
|
+
# assert all(isinstance(bb, BoundingBox) for bb in futures)
|
|
92
|
+
|
|
93
|
+
# common_bb = extract_common_bounding_box(bounding_boxes=futures)
|
|
94
|
+
# assert common_bb.xmin == 0
|
|
95
|
+
# assert common_bb.ymin == 0
|
|
96
|
+
# assert common_bb.xmax == 100
|
|
97
|
+
# assert common_bb.ymax == 100
|
|
@@ -6,7 +6,7 @@ import astropy.units as u
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pytest
|
|
8
8
|
from astropy.io import fits
|
|
9
|
-
from fitscube.combine_fits import combine_fits, parse_specs
|
|
9
|
+
from fitscube.combine_fits import combine_fits, even_spacing, parse_specs
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@pytest.fixture
|
|
@@ -108,3 +108,16 @@ def test_uneven_combine(
|
|
|
108
108
|
assert chan in (1, 2)
|
|
109
109
|
continue
|
|
110
110
|
assert np.allclose(plane, image)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_even_spacing_non_adjacent_gaps():
|
|
114
|
+
# Present channels 0, 3, 5 GHz on a 1 GHz grid (1, 2, 4 missing). No two
|
|
115
|
+
# surviving channels are adjacent, so min(diffs) == 2 GHz would mis-grid the
|
|
116
|
+
# axis to [0, 2, 4] and drop the real channels. The gcd of the diffs
|
|
117
|
+
# [3, 2] GHz recovers the true 1 GHz step.
|
|
118
|
+
specs = np.array([0.0, 3.0, 5.0]) * u.GHz
|
|
119
|
+
new_specs, missing_chan_idx = even_spacing(specs)
|
|
120
|
+
assert len(new_specs) == 6, "should rebuild the full 0..5 GHz grid"
|
|
121
|
+
assert missing_chan_idx.sum() == 3
|
|
122
|
+
expected_missing = np.array([False, True, True, False, True, False])
|
|
123
|
+
assert np.array_equal(missing_chan_idx, expected_missing)
|
|
@@ -92,6 +92,14 @@ def test_even_combine(file_list: list[Path], even_specs: u.Quantity, output_file
|
|
|
92
92
|
def test_uneven_combine(
|
|
93
93
|
file_list: list[Path], even_specs: u.Quantity, output_file: Path
|
|
94
94
|
):
|
|
95
|
+
"""!!!! NOTE: For some unknown reason thios test *SOMETIMES* raises the
|
|
96
|
+
following error:
|
|
97
|
+
|
|
98
|
+
> FAILED tests/test_times.py::test_uneven - ValueError: operands could not be broadcast together with shapes (5,) (6,)
|
|
99
|
+
|
|
100
|
+
Completely unclear to me why
|
|
101
|
+
"""
|
|
102
|
+
|
|
95
103
|
# uneven_specs = np.concatenate([even_specs[0:1], even_specs[3:]])
|
|
96
104
|
file_array = np.array(file_list)
|
|
97
105
|
uneven_files = np.concatenate([file_array[0:1], file_array[3:]]).tolist()
|
|
@@ -149,3 +157,113 @@ def test_wsclean_images_create_axis(time_image_paths, tmpdir) -> None:
|
|
|
149
157
|
# The TIME axis will be appended as a new dimension
|
|
150
158
|
cube_image_data = cube_data[i]
|
|
151
159
|
assert np.allclose(image_data.squeeze(), cube_image_data.squeeze())
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.filterwarnings("ignore:'datfix' made the change")
|
|
163
|
+
def test_wsclean_images_create_axis_floating(time_image_paths, tmpdir) -> None:
|
|
164
|
+
"""Ensure that the combined cube conforms to the input data"""
|
|
165
|
+
|
|
166
|
+
tmpdir = Path(tmpdir) / "time_cube_combine"
|
|
167
|
+
tmpdir.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
out_cube = tmpdir / "time_cube_mate.fits"
|
|
169
|
+
|
|
170
|
+
for float_length, float_rep in ((32, ">f4"), (64, ">f8")):
|
|
171
|
+
combine_fits(
|
|
172
|
+
file_list=time_image_paths,
|
|
173
|
+
out_cube=out_cube,
|
|
174
|
+
overwrite=True,
|
|
175
|
+
time_domain_mode=True,
|
|
176
|
+
float_length=float_length, # type: ignore [arg-type]
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
cube_data = fits.getdata(out_cube)
|
|
180
|
+
assert cube_data.dtype == float_rep
|
|
181
|
+
for i, time_image_path in enumerate(time_image_paths):
|
|
182
|
+
image_data = fits.getdata(time_image_path)
|
|
183
|
+
# The TIME axis will be appended as a new dimension
|
|
184
|
+
cube_image_data = cube_data[i]
|
|
185
|
+
assert np.allclose(image_data.squeeze(), cube_image_data.squeeze())
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.mark.filterwarnings("ignore:'datfix' made the change")
|
|
189
|
+
def test_wsclean_images_create_axis_withbb(time_image_paths, tmpdir) -> None:
|
|
190
|
+
"""Ensure that the combined cube conforms to the input data. This will use the
|
|
191
|
+
bounding box option. This should led to no change."""
|
|
192
|
+
|
|
193
|
+
tmpdir = Path(tmpdir) / "time_cube_combine"
|
|
194
|
+
tmpdir.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
out_cube = tmpdir / "time_cube_mate.fits"
|
|
196
|
+
|
|
197
|
+
combine_fits(
|
|
198
|
+
file_list=time_image_paths,
|
|
199
|
+
out_cube=out_cube,
|
|
200
|
+
overwrite=True,
|
|
201
|
+
time_domain_mode=True,
|
|
202
|
+
bounding_box=True,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
cube_data = fits.getdata(out_cube)
|
|
206
|
+
for i, time_image_path in enumerate(time_image_paths):
|
|
207
|
+
image_data = fits.getdata(time_image_path)
|
|
208
|
+
# The TIME axis will be appended as a new dimension
|
|
209
|
+
cube_image_data = cube_data[i]
|
|
210
|
+
assert np.allclose(image_data.squeeze(), cube_image_data.squeeze())
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@pytest.mark.filterwarnings("ignore:'datfix' made the change")
|
|
214
|
+
def test_wsclean_images_create_axis_withbb_noinvalidate0(
|
|
215
|
+
time_image_paths_withzeroborder, tmpdir
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Ensure that the combined cube conforms to the input data. This will use the
|
|
218
|
+
bounding box option. This should led to no change."""
|
|
219
|
+
|
|
220
|
+
tmpdir = Path(tmpdir) / "time_cube_combine_zeros"
|
|
221
|
+
tmpdir.mkdir(parents=True, exist_ok=True)
|
|
222
|
+
out_cube = tmpdir / "time_cube_mate.fits"
|
|
223
|
+
|
|
224
|
+
combine_fits(
|
|
225
|
+
file_list=time_image_paths_withzeroborder,
|
|
226
|
+
out_cube=out_cube,
|
|
227
|
+
overwrite=True,
|
|
228
|
+
time_domain_mode=True,
|
|
229
|
+
bounding_box=True,
|
|
230
|
+
invalidate_zeros=False,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
cube_data = fits.getdata(out_cube)
|
|
234
|
+
assert cube_data.squeeze().shape[-2:] == (100, 100)
|
|
235
|
+
for i, time_image_path in enumerate(time_image_paths_withzeroborder):
|
|
236
|
+
image_data = fits.getdata(time_image_path)
|
|
237
|
+
# The TIME axis will be appended as a new dimension
|
|
238
|
+
cube_image_data = cube_data[i]
|
|
239
|
+
assert np.allclose(image_data.squeeze(), cube_image_data.squeeze())
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@pytest.mark.filterwarnings("ignore:'datfix' made the change")
|
|
243
|
+
def test_wsclean_images_create_axis_withbb_invalidatezeros(
|
|
244
|
+
time_image_paths_withzeroborder, tmpdir
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Ensure that the combined cube conforms to the input data. This will use the
|
|
247
|
+
bounding box option. This should led to no change. Here we also get fitscube to
|
|
248
|
+
mark pixels that are exactly 0 to be nans"""
|
|
249
|
+
|
|
250
|
+
tmpdir = Path(tmpdir) / "time_cube_combine_zeros"
|
|
251
|
+
tmpdir.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
out_cube = tmpdir / "time_cube_mate.fits"
|
|
253
|
+
|
|
254
|
+
combine_fits(
|
|
255
|
+
file_list=time_image_paths_withzeroborder,
|
|
256
|
+
out_cube=out_cube,
|
|
257
|
+
overwrite=True,
|
|
258
|
+
time_domain_mode=True,
|
|
259
|
+
bounding_box=True,
|
|
260
|
+
invalidate_zeros=True,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
cube_data = fits.getdata(out_cube)
|
|
264
|
+
assert cube_data.squeeze().shape[-2:] == (90, 90)
|
|
265
|
+
for i, time_image_path in enumerate(time_image_paths_withzeroborder):
|
|
266
|
+
image_data = fits.getdata(time_image_path)
|
|
267
|
+
# The TIME axis will be appended as a new dimension
|
|
268
|
+
cube_image_data = cube_data[i]
|
|
269
|
+
assert np.allclose(image_data.squeeze()[5:-5, 5:-5], cube_image_data.squeeze())
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# file generated by setuptools-scm
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
__all__ = [
|
|
5
|
-
"__version__",
|
|
6
|
-
"__version_tuple__",
|
|
7
|
-
"version",
|
|
8
|
-
"version_tuple",
|
|
9
|
-
"__commit_id__",
|
|
10
|
-
"commit_id",
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
TYPE_CHECKING = False
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from typing import Tuple
|
|
16
|
-
from typing import Union
|
|
17
|
-
|
|
18
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
-
COMMIT_ID = Union[str, None]
|
|
20
|
-
else:
|
|
21
|
-
VERSION_TUPLE = object
|
|
22
|
-
COMMIT_ID = object
|
|
23
|
-
|
|
24
|
-
version: str
|
|
25
|
-
__version__: str
|
|
26
|
-
__version_tuple__: VERSION_TUPLE
|
|
27
|
-
version_tuple: VERSION_TUPLE
|
|
28
|
-
commit_id: COMMIT_ID
|
|
29
|
-
__commit_id__: COMMIT_ID
|
|
30
|
-
|
|
31
|
-
__version__ = version = '2.3.0'
|
|
32
|
-
__version_tuple__ = version_tuple = (2, 3, 0)
|
|
33
|
-
|
|
34
|
-
__commit_id__ = commit_id = None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|