fitscube 2.2.0__tar.gz → 2.3.1__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.2.0 → fitscube-2.3.1}/.github/workflows/cd.yml +2 -2
- {fitscube-2.2.0 → fitscube-2.3.1}/.github/workflows/ci.yml +3 -3
- {fitscube-2.2.0 → fitscube-2.3.1}/.pre-commit-config.yaml +4 -4
- {fitscube-2.2.0 → fitscube-2.3.1}/PKG-INFO +2 -1
- fitscube-2.3.1/fitscube/_version.py +24 -0
- fitscube-2.3.1/fitscube/bounding_box.py +153 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/fitscube/combine_fits.py +111 -12
- {fitscube-2.2.0 → fitscube-2.3.1}/pyproject.toml +1 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/tests/conftest.py +29 -0
- fitscube-2.3.1/tests/test_bb.py +97 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/tests/test_times.py +118 -0
- fitscube-2.2.0/fitscube/_version.py +0 -21
- {fitscube-2.2.0 → fitscube-2.3.1}/.github/CONTRIBUTING.md +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/.github/dependabot.yml +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/.github/release.yml +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/.gitignore +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/CHANGELOG.md +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/LICENSE +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/README.md +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/fitscube/__init__.py +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/fitscube/asyncio.py +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/fitscube/cli.py +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/fitscube/exceptions.py +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/fitscube/extract.py +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/fitscube/logging.py +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/fitscube/version.pyi +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/noxfile.py +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/tests/__init__.py +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/tests/data/cube.zip +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/tests/data/images.zip +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/tests/data/time_images.zip +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/tests/data/timecube.zip +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/tests/test_extract.py +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/tests/test_frequencies.py +0 -0
- {fitscube-2.2.0 → fitscube-2.3.1}/tests/test_package.py +0 -0
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
runs-on: ubuntu-latest
|
|
26
26
|
|
|
27
27
|
steps:
|
|
28
|
-
- uses: actions/checkout@
|
|
28
|
+
- uses: actions/checkout@v5
|
|
29
29
|
with:
|
|
30
30
|
fetch-depth: 0
|
|
31
31
|
|
|
@@ -43,7 +43,7 @@ jobs:
|
|
|
43
43
|
if: github.event_name == 'release' && github.event.action == 'published'
|
|
44
44
|
|
|
45
45
|
steps:
|
|
46
|
-
- uses: actions/download-artifact@
|
|
46
|
+
- uses: actions/download-artifact@v5
|
|
47
47
|
with:
|
|
48
48
|
name: Packages
|
|
49
49
|
path: dist
|
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
name: Format
|
|
22
22
|
runs-on: ubuntu-latest
|
|
23
23
|
steps:
|
|
24
|
-
- uses: actions/checkout@
|
|
24
|
+
- uses: actions/checkout@v5
|
|
25
25
|
with:
|
|
26
26
|
fetch-depth: 0
|
|
27
27
|
- uses: actions/setup-python@v5
|
|
@@ -42,7 +42,7 @@ jobs:
|
|
|
42
42
|
runs-on: [ubuntu-latest]
|
|
43
43
|
|
|
44
44
|
steps:
|
|
45
|
-
- uses: actions/checkout@
|
|
45
|
+
- uses: actions/checkout@v5
|
|
46
46
|
with:
|
|
47
47
|
fetch-depth: 0
|
|
48
48
|
|
|
@@ -62,6 +62,6 @@ jobs:
|
|
|
62
62
|
--durations=20
|
|
63
63
|
|
|
64
64
|
- name: Upload coverage report
|
|
65
|
-
uses: codecov/codecov-action@v5.
|
|
65
|
+
uses: codecov/codecov-action@v5.5.0
|
|
66
66
|
with:
|
|
67
67
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
@@ -10,7 +10,7 @@ repos:
|
|
|
10
10
|
# additional_dependencies: [black==24.*]
|
|
11
11
|
|
|
12
12
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
13
|
-
rev: "
|
|
13
|
+
rev: "v6.0.0"
|
|
14
14
|
hooks:
|
|
15
15
|
- id: check-added-large-files
|
|
16
16
|
- id: check-case-conflict
|
|
@@ -33,7 +33,7 @@ repos:
|
|
|
33
33
|
- id: rst-inline-touching-normal
|
|
34
34
|
|
|
35
35
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
36
|
-
rev: "v0.12.
|
|
36
|
+
rev: "v0.12.11"
|
|
37
37
|
hooks:
|
|
38
38
|
- id: ruff
|
|
39
39
|
args: ["--fix", "--show-fixes"]
|
|
@@ -56,7 +56,7 @@ repos:
|
|
|
56
56
|
- tomli
|
|
57
57
|
|
|
58
58
|
- repo: https://github.com/shellcheck-py/shellcheck-py
|
|
59
|
-
rev: "v0.
|
|
59
|
+
rev: "v0.11.0.1"
|
|
60
60
|
hooks:
|
|
61
61
|
- id: shellcheck
|
|
62
62
|
|
|
@@ -75,7 +75,7 @@ repos:
|
|
|
75
75
|
additional_dependencies: ["validate-pyproject-schema-store[all]"]
|
|
76
76
|
|
|
77
77
|
- repo: https://github.com/python-jsonschema/check-jsonschema
|
|
78
|
-
rev: "0.33.
|
|
78
|
+
rev: "0.33.3"
|
|
79
79
|
hooks:
|
|
80
80
|
- id: check-dependabot
|
|
81
81
|
- id: check-github-workflows
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fitscube
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
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.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 3, 1)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Some basic utilities to help with the creating of
|
|
2
|
+
bounding boxes to use in fitscube"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from astropy.io import fits
|
|
12
|
+
|
|
13
|
+
from fitscube.logging import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class BoundingBox:
|
|
18
|
+
"""Simple container to represent a bounding box. Maximum values can be
|
|
19
|
+
used as is when slicing."""
|
|
20
|
+
|
|
21
|
+
xmin: int
|
|
22
|
+
"""Minimum x pixel"""
|
|
23
|
+
xmax: int
|
|
24
|
+
"""Maximum x pixel"""
|
|
25
|
+
ymin: int
|
|
26
|
+
"""Minimum y pixel. Can be used as is in slice (e.g. is exclusive). """
|
|
27
|
+
ymax: int
|
|
28
|
+
"""Maximum y pixel Can be used as is in slice (e.g. is exclusive)."""
|
|
29
|
+
original_shape: tuple[int, int]
|
|
30
|
+
"""The original shape of the image. If constructed against a cube this is the shape of a single plane."""
|
|
31
|
+
y_span: int
|
|
32
|
+
"""The span between ymax and ymin"""
|
|
33
|
+
x_span: int
|
|
34
|
+
"""The span between xmax and xmin"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_bound_box_plane(image_data: np.ndarray) -> BoundingBox | None:
|
|
38
|
+
"""Create a bounding box around pixels in a 2D image. If all
|
|
39
|
+
pixels are not valid, then ``None`` is returned.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
image_data (np.ndarray): The 2D image to construct a bounding box around
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Optional[BoundingBox]: None if no valid pixels, a bounding box with the (xmin,xmax,ymin,ymax) of valid pixels
|
|
46
|
+
"""
|
|
47
|
+
assert len(image_data.shape) == 2, (
|
|
48
|
+
f"Only two-dimensional arrays supported, received {image_data.shape}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# First convert to a boolean array
|
|
52
|
+
image_valid = np.isfinite(image_data)
|
|
53
|
+
|
|
54
|
+
if not any(image_valid.reshape(-1)):
|
|
55
|
+
logger.info("No pixels to creating bounding box for")
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
# Then make them 1D arrays
|
|
59
|
+
x_valid = np.any(image_valid, axis=1)
|
|
60
|
+
y_valid = np.any(image_valid, axis=0)
|
|
61
|
+
|
|
62
|
+
# Now get the first and last index
|
|
63
|
+
xmin, xmax = np.where(x_valid)[0][[0, -1]]
|
|
64
|
+
ymin, ymax = np.where(y_valid)[0][[0, -1]]
|
|
65
|
+
|
|
66
|
+
xmax += 1
|
|
67
|
+
ymax += 1
|
|
68
|
+
|
|
69
|
+
y_span = ymax - ymin
|
|
70
|
+
x_span = xmax - xmin
|
|
71
|
+
|
|
72
|
+
return BoundingBox(
|
|
73
|
+
xmin=xmin,
|
|
74
|
+
xmax=xmax,
|
|
75
|
+
ymin=ymin,
|
|
76
|
+
ymax=ymax,
|
|
77
|
+
y_span=y_span,
|
|
78
|
+
x_span=x_span,
|
|
79
|
+
original_shape=image_data.shape[-2:],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def extract_common_bounding_box(
|
|
84
|
+
bounding_boxes: list[BoundingBox | None],
|
|
85
|
+
) -> BoundingBox:
|
|
86
|
+
"""Get the smallest bounding box that encompasses all bounding boxes
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
bounding_boxes (list[BoundingBox | None]): A list of bounding boxes. If None (returned for invalid images) skip it.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: If all input bounding boxes are invalid
|
|
93
|
+
ValueError: If there is an `original_shape` mismatch
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
BoundingBox: The smallest bounding box
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
# Step 1: filter out all Nones
|
|
100
|
+
valid_boxes: list[BoundingBox] = [bb for bb in bounding_boxes if bb is not None]
|
|
101
|
+
|
|
102
|
+
if len(valid_boxes) == 0:
|
|
103
|
+
msg = "No valid input boxes to consider"
|
|
104
|
+
raise ValueError(msg)
|
|
105
|
+
|
|
106
|
+
if not all(
|
|
107
|
+
valid_boxes[0].original_shape == bb.original_shape for bb in valid_boxes
|
|
108
|
+
):
|
|
109
|
+
msg = "Different shapes, and not sure this is really supported or meaningful"
|
|
110
|
+
raise ValueError(msg)
|
|
111
|
+
|
|
112
|
+
xmin = int(np.min([bb.xmin for bb in valid_boxes]))
|
|
113
|
+
xmax = int(np.max([bb.xmax for bb in valid_boxes]))
|
|
114
|
+
ymin = int(np.min([bb.ymin for bb in valid_boxes]))
|
|
115
|
+
ymax = int(np.max([bb.ymax for bb in valid_boxes]))
|
|
116
|
+
|
|
117
|
+
y_span = ymax - ymin
|
|
118
|
+
x_span = xmax - xmin
|
|
119
|
+
|
|
120
|
+
return BoundingBox(
|
|
121
|
+
xmin=xmin,
|
|
122
|
+
xmax=xmax,
|
|
123
|
+
ymin=ymin,
|
|
124
|
+
ymax=ymax,
|
|
125
|
+
y_span=y_span,
|
|
126
|
+
x_span=x_span,
|
|
127
|
+
original_shape=valid_boxes[0].original_shape,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def get_bounding_box_for_fits_coro(
|
|
132
|
+
fits_path: Path, invalidate_zeros: bool = False
|
|
133
|
+
) -> BoundingBox | None:
|
|
134
|
+
"""Create a bounding box for an image contained in a FITS file.
|
|
135
|
+
|
|
136
|
+
The assumption is that the FITS file contains an image, not a cube.
|
|
137
|
+
If the cube can bot be reshapped to an image without losing data
|
|
138
|
+
the underlying bounding box creation will fail.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
fits_path (Path): The fits image to call
|
|
142
|
+
invalidate_zeros (bool, optional): Mark pixels that are exactly 0.0 as invalid (NaN them). Defaults to False.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
BoundingBox | None: The bounding box that describes the bounds of valid data. If all data are invalid (and not bounding box possible) None is returned.
|
|
146
|
+
"""
|
|
147
|
+
data = await asyncio.to_thread(fits.getdata, fits_path, memmap=False)
|
|
148
|
+
data = np.squeeze(data)
|
|
149
|
+
|
|
150
|
+
if invalidate_zeros:
|
|
151
|
+
data[data == 0.0] = np.nan
|
|
152
|
+
|
|
153
|
+
return await asyncio.to_thread(create_bound_box_plane, image_data=data)
|
|
@@ -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
|
|
@@ -34,6 +31,11 @@ from radio_beam.beam import NoBeamException
|
|
|
34
31
|
from tqdm.asyncio import tqdm
|
|
35
32
|
|
|
36
33
|
from fitscube.asyncio import gather_with_limit, sync_wrapper
|
|
34
|
+
from fitscube.bounding_box import (
|
|
35
|
+
BoundingBox,
|
|
36
|
+
extract_common_bounding_box,
|
|
37
|
+
get_bounding_box_for_fits_coro,
|
|
38
|
+
)
|
|
37
39
|
from fitscube.logging import TQDM_OUT, logger, set_verbosity
|
|
38
40
|
|
|
39
41
|
T = TypeVar("T")
|
|
@@ -45,6 +47,8 @@ BIT_DICT = {
|
|
|
45
47
|
16: 2,
|
|
46
48
|
8: 1,
|
|
47
49
|
}
|
|
50
|
+
FLOAT_LENGTH = Literal[16, 32, 64]
|
|
51
|
+
FLOAT_TYPE = {64: ">f8", 32: ">f4"}
|
|
48
52
|
|
|
49
53
|
warnings.filterwarnings("ignore", category=UserWarning, module="astropy.io.fits")
|
|
50
54
|
warnings.filterwarnings("ignore", category=VerifyWarning)
|
|
@@ -170,6 +174,7 @@ async def create_cube_from_scratch_coro(
|
|
|
170
174
|
output_shape = output_wcs.array_shape
|
|
171
175
|
msg = f"Creating a new FITS file with shape {output_shape}"
|
|
172
176
|
logger.info(msg)
|
|
177
|
+
|
|
173
178
|
# If the output shape is less than 1801, we can create a blank array
|
|
174
179
|
# in memory and write it to disk
|
|
175
180
|
if np.prod(output_shape) < 1801:
|
|
@@ -240,19 +245,27 @@ async def create_output_cube_coro(
|
|
|
240
245
|
single_beam: bool = False,
|
|
241
246
|
overwrite: bool = False,
|
|
242
247
|
time_domain_mode: bool = False,
|
|
248
|
+
bounding_box: BoundingBox | None = None,
|
|
249
|
+
float_length: FLOAT_LENGTH | None = None,
|
|
243
250
|
) -> InitResult:
|
|
244
|
-
"""
|
|
251
|
+
"""Generate the output header and write a dummy cube to disk based on properties of the
|
|
252
|
+
inpute data. The output cube written here has a correctly formed header and a pre-zerod
|
|
253
|
+
data cube written as output.
|
|
245
254
|
|
|
246
255
|
Args:
|
|
247
|
-
old_name (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
256
|
+
old_name (Path): The path to a representative image to draw the base fits header from
|
|
257
|
+
out_cube (Path): Path of the output cube to create
|
|
258
|
+
specs (u.Quantity): Specification of the unit that denotes the 'cube' axis
|
|
259
|
+
ignore_spec (bool, optional): Whether the provided `specs` axis should be ignored. If True dummy placeholder fields added. Defaults to False.
|
|
260
|
+
has_beams (bool, optional): Indicates whether a CASA Beam table will also be generated and added to the output cube. Defaults to False.
|
|
261
|
+
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.
|
|
262
|
+
overwrite (bool, optional): If True the out the output cube will overwrite any existing file. Defaults to False.
|
|
263
|
+
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.
|
|
264
|
+
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.
|
|
265
|
+
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.
|
|
253
266
|
|
|
254
267
|
Returns:
|
|
255
|
-
InitResult:
|
|
268
|
+
InitResult: Details of the output cube, including the output header
|
|
256
269
|
"""
|
|
257
270
|
|
|
258
271
|
# define units if in time or freq domain
|
|
@@ -358,6 +371,14 @@ async def create_output_cube_coro(
|
|
|
358
371
|
)
|
|
359
372
|
del new_header["BMAJ"], new_header["BMIN"], new_header["BPA"]
|
|
360
373
|
|
|
374
|
+
if bounding_box:
|
|
375
|
+
logger.info("Updating CRPIX1 and CRPIX2 header values to reflect bounding box")
|
|
376
|
+
new_header["CRPIX1"] -= bounding_box.ymin
|
|
377
|
+
new_header["CRPIX2"] -= bounding_box.xmin
|
|
378
|
+
logger.info("Updating NAXIS1 and NAXIS2 ro reflect bounding box")
|
|
379
|
+
new_header["NAXIS1"] = bounding_box.y_span
|
|
380
|
+
new_header["NAXIS2"] = bounding_box.x_span
|
|
381
|
+
|
|
361
382
|
plane_shape = list(old_data.shape)
|
|
362
383
|
cube_shape = plane_shape.copy()
|
|
363
384
|
if is_2d:
|
|
@@ -365,6 +386,23 @@ async def create_output_cube_coro(
|
|
|
365
386
|
else:
|
|
366
387
|
cube_shape[idx] = n_chan
|
|
367
388
|
|
|
389
|
+
logger.critical(f"{float_length=} {new_header['BITPIX']=}")
|
|
390
|
+
if float_length is not None:
|
|
391
|
+
# Per astropy docs
|
|
392
|
+
# bITPIX numpy data type
|
|
393
|
+
# 8 numpy.uint8 (note it is UNsigned integer)
|
|
394
|
+
# 16 numpy.int16
|
|
395
|
+
# 32 numpy.int32
|
|
396
|
+
# 64 numpy.int64
|
|
397
|
+
# -32 numpy.float32
|
|
398
|
+
# -64 numpy.float64
|
|
399
|
+
assert float_length in list(BIT_DICT.keys()), (
|
|
400
|
+
f"{float_length=} not in {BIT_DICT=}"
|
|
401
|
+
)
|
|
402
|
+
bit_pix = int(float_length)
|
|
403
|
+
logger.info(f"Specified {float_length=}, corresponding to {bit_pix=}")
|
|
404
|
+
new_header["BITPIX"] = -abs(bit_pix)
|
|
405
|
+
|
|
368
406
|
output_header = await create_cube_from_scratch_coro(
|
|
369
407
|
output_file=out_cube, output_header=new_header, overwrite=overwrite
|
|
370
408
|
)
|
|
@@ -619,6 +657,8 @@ async def process_channel(
|
|
|
619
657
|
old_channel: int,
|
|
620
658
|
is_missing: bool,
|
|
621
659
|
file_list: list[Path],
|
|
660
|
+
bounding_box: BoundingBox | None = None,
|
|
661
|
+
invalidate_zeros: bool = False,
|
|
622
662
|
) -> None:
|
|
623
663
|
msg = f"Processing channel {new_channel}"
|
|
624
664
|
logger.info(msg)
|
|
@@ -631,6 +671,20 @@ async def process_channel(
|
|
|
631
671
|
fits.getdata, file_list[old_channel], memmap=False
|
|
632
672
|
)
|
|
633
673
|
|
|
674
|
+
if bounding_box is not None:
|
|
675
|
+
plane = plane[
|
|
676
|
+
...,
|
|
677
|
+
bounding_box.xmin : bounding_box.xmax,
|
|
678
|
+
bounding_box.ymin : bounding_box.ymax,
|
|
679
|
+
]
|
|
680
|
+
if invalidate_zeros:
|
|
681
|
+
plane[plane == 0.0] = np.nan
|
|
682
|
+
|
|
683
|
+
if "BITPIX" in new_header:
|
|
684
|
+
bit_pix = abs(new_header["BITPIX"])
|
|
685
|
+
float_type = FLOAT_TYPE[bit_pix]
|
|
686
|
+
plane = plane.astype(float_type)
|
|
687
|
+
|
|
634
688
|
await write_channel_to_cube_coro(
|
|
635
689
|
file_handle=file_handle,
|
|
636
690
|
plane=plane,
|
|
@@ -650,6 +704,9 @@ async def combine_fits_coro(
|
|
|
650
704
|
overwrite: bool = False,
|
|
651
705
|
max_workers: int | None = None,
|
|
652
706
|
time_domain_mode: bool = False,
|
|
707
|
+
bounding_box: bool = False,
|
|
708
|
+
invalidate_zeros: bool = False,
|
|
709
|
+
float_length: FLOAT_LENGTH | None = None,
|
|
653
710
|
) -> u.Quantity:
|
|
654
711
|
"""Combine FITS files into a cube.
|
|
655
712
|
Can handle either frequency or time dimensions agnostically
|
|
@@ -660,6 +717,9 @@ async def combine_fits_coro(
|
|
|
660
717
|
ignore_spec (bool, optional): Ignore frequency/time information. Defaults to False.
|
|
661
718
|
create_blanks (bool, optional): Attempt to create even frequency spacing. Defaults to False.
|
|
662
719
|
time_domain_mode (bool, optional): Work in time domain mode - make a time-cube. Default = False.
|
|
720
|
+
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.
|
|
721
|
+
invalidate_zeros (bool, optionals): Set pixels whose values are exactly zero to NaNs. Defaults to False.
|
|
722
|
+
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.
|
|
663
723
|
|
|
664
724
|
Returns:
|
|
665
725
|
tuple[fits.HDUList, u.Quantity]: The combined FITS cube and frequencies
|
|
@@ -704,6 +764,21 @@ async def combine_fits_coro(
|
|
|
704
764
|
specs = specs[new_sort_idx]
|
|
705
765
|
missing_chan_idx = missing_chan_idx[new_sort_idx]
|
|
706
766
|
|
|
767
|
+
# Get the bounding box, if requested
|
|
768
|
+
final_bounding_box = None
|
|
769
|
+
if bounding_box:
|
|
770
|
+
boxes_futures = [
|
|
771
|
+
get_bounding_box_for_fits_coro(
|
|
772
|
+
fits_path=fits_path, invalidate_zeros=invalidate_zeros
|
|
773
|
+
)
|
|
774
|
+
for fits_path in file_list
|
|
775
|
+
]
|
|
776
|
+
boxes = await gather_with_limit(
|
|
777
|
+
max_workers, *boxes_futures, desc="Bounding boxes"
|
|
778
|
+
)
|
|
779
|
+
final_bounding_box = extract_common_bounding_box(bounding_boxes=boxes)
|
|
780
|
+
logger.info(f"The final bounding box is: {final_bounding_box=}")
|
|
781
|
+
|
|
707
782
|
# Initialize the data cube
|
|
708
783
|
new_header, _, _, _ = await create_output_cube_coro(
|
|
709
784
|
old_name=file_list[0],
|
|
@@ -714,6 +789,8 @@ async def combine_fits_coro(
|
|
|
714
789
|
single_beam=single_beam,
|
|
715
790
|
overwrite=overwrite,
|
|
716
791
|
time_domain_mode=time_domain_mode,
|
|
792
|
+
bounding_box=final_bounding_box,
|
|
793
|
+
float_length=float_length,
|
|
717
794
|
)
|
|
718
795
|
|
|
719
796
|
new_channels = np.arange(len(specs))
|
|
@@ -741,6 +818,8 @@ async def combine_fits_coro(
|
|
|
741
818
|
old_channel=old_channel,
|
|
742
819
|
is_missing=is_missing,
|
|
743
820
|
file_list=file_list,
|
|
821
|
+
bounding_box=final_bounding_box,
|
|
822
|
+
invalidate_zeros=invalidate_zeros,
|
|
744
823
|
)
|
|
745
824
|
coros.append(coro)
|
|
746
825
|
|
|
@@ -823,6 +902,23 @@ def get_parser(
|
|
|
823
902
|
default=None,
|
|
824
903
|
help="Maximum number of workers to use for concurrent processing",
|
|
825
904
|
)
|
|
905
|
+
parser.add_argument(
|
|
906
|
+
"--bounding-box",
|
|
907
|
+
action="store_true",
|
|
908
|
+
help="Attempt to consider padded images when creating the cube. Requires an extract read of the input data.",
|
|
909
|
+
)
|
|
910
|
+
parser.add_argument(
|
|
911
|
+
"--invalidate-zeros",
|
|
912
|
+
action="store_true",
|
|
913
|
+
help="Set pixels whose values are exactly zero to NaNs",
|
|
914
|
+
)
|
|
915
|
+
parser.add_argument(
|
|
916
|
+
"--floating",
|
|
917
|
+
type=int,
|
|
918
|
+
choices=(8, 16, 32, 64),
|
|
919
|
+
default=None,
|
|
920
|
+
help="The number of floating point bits to use in the out cube. If None the input data precision is used.",
|
|
921
|
+
)
|
|
826
922
|
|
|
827
923
|
return parser
|
|
828
924
|
|
|
@@ -863,6 +959,9 @@ def cli(args: argparse.Namespace | None = None) -> None:
|
|
|
863
959
|
overwrite=overwrite,
|
|
864
960
|
max_workers=args.max_workers,
|
|
865
961
|
time_domain_mode=time_domain_mode,
|
|
962
|
+
bounding_box=args.bounding_box,
|
|
963
|
+
invalidate_zeros=args.invalidate_zeros,
|
|
964
|
+
float_length=args.floating,
|
|
866
965
|
)
|
|
867
966
|
|
|
868
967
|
spequency = "times" if time_domain_mode else "frequencies"
|
|
@@ -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
|
|
@@ -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,21 +0,0 @@
|
|
|
1
|
-
# file generated by setuptools-scm
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
-
|
|
6
|
-
TYPE_CHECKING = False
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
from typing import Tuple
|
|
9
|
-
from typing import Union
|
|
10
|
-
|
|
11
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
-
else:
|
|
13
|
-
VERSION_TUPLE = object
|
|
14
|
-
|
|
15
|
-
version: str
|
|
16
|
-
__version__: str
|
|
17
|
-
__version_tuple__: VERSION_TUPLE
|
|
18
|
-
version_tuple: VERSION_TUPLE
|
|
19
|
-
|
|
20
|
-
__version__ = version = '2.2.0'
|
|
21
|
-
__version_tuple__ = version_tuple = (2, 2, 0)
|
|
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
|