fitscube 2.1.0__tar.gz → 2.3.0__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.1.0 → fitscube-2.3.0}/.github/workflows/cd.yml +2 -2
- {fitscube-2.1.0 → fitscube-2.3.0}/.github/workflows/ci.yml +3 -3
- {fitscube-2.1.0 → fitscube-2.3.0}/.pre-commit-config.yaml +5 -5
- {fitscube-2.1.0 → fitscube-2.3.0}/PKG-INFO +1 -1
- {fitscube-2.1.0 → fitscube-2.3.0}/fitscube/_version.py +16 -3
- fitscube-2.3.0/fitscube/bounding_box.py +149 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/fitscube/combine_fits.py +73 -3
- {fitscube-2.1.0 → fitscube-2.3.0}/.github/CONTRIBUTING.md +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/.github/dependabot.yml +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/.github/release.yml +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/.gitignore +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/CHANGELOG.md +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/LICENSE +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/README.md +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/fitscube/__init__.py +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/fitscube/asyncio.py +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/fitscube/cli.py +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/fitscube/exceptions.py +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/fitscube/extract.py +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/fitscube/logging.py +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/fitscube/version.pyi +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/noxfile.py +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/pyproject.toml +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/tests/__init__.py +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/tests/conftest.py +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/tests/data/cube.zip +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/tests/data/images.zip +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/tests/data/time_images.zip +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/tests/data/timecube.zip +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/tests/test_extract.py +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/tests/test_frequencies.py +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/tests/test_package.py +0 -0
- {fitscube-2.1.0 → fitscube-2.3.0}/tests/test_times.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,14 +33,14 @@ 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"]
|
|
40
40
|
- id: ruff-format
|
|
41
41
|
|
|
42
42
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
43
|
-
rev: "v1.17.
|
|
43
|
+
rev: "v1.17.1"
|
|
44
44
|
hooks:
|
|
45
45
|
- id: mypy
|
|
46
46
|
files: src|tests
|
|
@@ -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,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '2.
|
|
21
|
-
__version_tuple__ = version_tuple = (2,
|
|
31
|
+
__version__ = version = '2.3.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 3, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,149 @@
|
|
|
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"""
|
|
19
|
+
|
|
20
|
+
xmin: int
|
|
21
|
+
"""Minimum x pixel"""
|
|
22
|
+
xmax: int
|
|
23
|
+
"""Maximum x pixel"""
|
|
24
|
+
ymin: int
|
|
25
|
+
"""Minimum y pixel"""
|
|
26
|
+
ymax: int
|
|
27
|
+
"""Maximum y pixel"""
|
|
28
|
+
original_shape: tuple[int, int]
|
|
29
|
+
"""The original shape of the image. If constructed against a cube this is the shape of a single plane."""
|
|
30
|
+
y_span: int
|
|
31
|
+
"""The span between ymax and ymin"""
|
|
32
|
+
x_span: int
|
|
33
|
+
"""The span between xmax and xmin"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_bound_box_plane(image_data: np.ndarray) -> BoundingBox | None:
|
|
37
|
+
"""Create a bounding box around pixels in a 2D image. If all
|
|
38
|
+
pixels are not valid, then ``None`` is returned.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
image_data (np.ndarray): The 2D image to construct a bounding box around
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Optional[BoundingBox]: None if no valid pixels, a bounding box with the (xmin,xmax,ymin,ymax) of valid pixels
|
|
45
|
+
"""
|
|
46
|
+
assert len(image_data.shape) == 2, (
|
|
47
|
+
f"Only two-dimensional arrays supported, received {image_data.shape}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# First convert to a boolean array
|
|
51
|
+
image_valid = np.isfinite(image_data)
|
|
52
|
+
|
|
53
|
+
if not any(image_valid.reshape(-1)):
|
|
54
|
+
logger.info("No pixels to creating bounding box for")
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
# Then make them 1D arrays
|
|
58
|
+
x_valid = np.any(image_valid, axis=1)
|
|
59
|
+
y_valid = np.any(image_valid, axis=0)
|
|
60
|
+
|
|
61
|
+
# Now get the first and last index
|
|
62
|
+
xmin, xmax = np.where(x_valid)[0][[0, -1]]
|
|
63
|
+
ymin, ymax = np.where(y_valid)[0][[0, -1]]
|
|
64
|
+
|
|
65
|
+
y_span = ymax - ymin
|
|
66
|
+
x_span = xmax - xmin
|
|
67
|
+
|
|
68
|
+
return BoundingBox(
|
|
69
|
+
xmin=xmin,
|
|
70
|
+
xmax=xmax,
|
|
71
|
+
ymin=ymin,
|
|
72
|
+
ymax=ymax,
|
|
73
|
+
y_span=y_span,
|
|
74
|
+
x_span=x_span,
|
|
75
|
+
original_shape=image_data.shape[-2:],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def extract_common_bounding_box(
|
|
80
|
+
bounding_boxes: list[BoundingBox | None],
|
|
81
|
+
) -> BoundingBox:
|
|
82
|
+
"""Get the smallest bounding box that encompasses all bounding boxes
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
bounding_boxes (list[BoundingBox | None]): A list of bounding boxes. If None (returned for invalid images) skip it.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ValueError: If all input bounding boxes are invalid
|
|
89
|
+
ValueError: If there is an `original_shape` mismatch
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
BoundingBox: The smallest bounding box
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# Step 1: filter out all Nones
|
|
96
|
+
valid_boxes: list[BoundingBox] = [bb for bb in bounding_boxes if bb is not None]
|
|
97
|
+
|
|
98
|
+
if len(valid_boxes) == 0:
|
|
99
|
+
msg = "No valid input boxes to consider"
|
|
100
|
+
raise ValueError(msg)
|
|
101
|
+
|
|
102
|
+
if not all(
|
|
103
|
+
valid_boxes[0].original_shape == bb.original_shape for bb in valid_boxes
|
|
104
|
+
):
|
|
105
|
+
msg = "Different shapes, and not sure this is really supported or meaningful"
|
|
106
|
+
raise ValueError(msg)
|
|
107
|
+
|
|
108
|
+
xmin = int(np.min([bb.xmin for bb in valid_boxes]))
|
|
109
|
+
xmax = int(np.max([bb.xmax for bb in valid_boxes]))
|
|
110
|
+
ymin = int(np.min([bb.ymin for bb in valid_boxes]))
|
|
111
|
+
ymax = int(np.max([bb.ymax for bb in valid_boxes]))
|
|
112
|
+
|
|
113
|
+
y_span = ymax - ymin
|
|
114
|
+
x_span = xmax - xmin
|
|
115
|
+
|
|
116
|
+
return BoundingBox(
|
|
117
|
+
xmin=xmin,
|
|
118
|
+
xmax=xmax,
|
|
119
|
+
ymin=ymin,
|
|
120
|
+
ymax=ymax,
|
|
121
|
+
y_span=y_span,
|
|
122
|
+
x_span=x_span,
|
|
123
|
+
original_shape=valid_boxes[0].original_shape,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def get_bounding_box_for_fits_coro(
|
|
128
|
+
fits_path: Path, invalidate_zeros: bool = False
|
|
129
|
+
) -> BoundingBox | None:
|
|
130
|
+
"""Create a bounding box for an image contained in a FITS file.
|
|
131
|
+
|
|
132
|
+
The assumption is that the FITS file contains an image, not a cube.
|
|
133
|
+
If the cube can bot be reshapped to an image without losing data
|
|
134
|
+
the underlying bounding box creation will fail.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
fits_path (Path): The fits image to call
|
|
138
|
+
invalidate_zeros (bool, optional): Mark pixels that are exactly 0.0 as invalid (NaN them). Defaults to False.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
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.
|
|
142
|
+
"""
|
|
143
|
+
data = await asyncio.to_thread(fits.getdata, fits_path, memmap=False)
|
|
144
|
+
data = np.squeeze(data)
|
|
145
|
+
|
|
146
|
+
if invalidate_zeros:
|
|
147
|
+
data[data == 0.0] = np.nan
|
|
148
|
+
|
|
149
|
+
return await asyncio.to_thread(create_bound_box_plane, image_data=data)
|
|
@@ -34,6 +34,11 @@ from radio_beam.beam import NoBeamException
|
|
|
34
34
|
from tqdm.asyncio import tqdm
|
|
35
35
|
|
|
36
36
|
from fitscube.asyncio import gather_with_limit, sync_wrapper
|
|
37
|
+
from fitscube.bounding_box import (
|
|
38
|
+
BoundingBox,
|
|
39
|
+
extract_common_bounding_box,
|
|
40
|
+
get_bounding_box_for_fits_coro,
|
|
41
|
+
)
|
|
37
42
|
from fitscube.logging import TQDM_OUT, logger, set_verbosity
|
|
38
43
|
|
|
39
44
|
T = TypeVar("T")
|
|
@@ -160,10 +165,17 @@ async def create_cube_from_scratch_coro(
|
|
|
160
165
|
if output_file.exists() and overwrite:
|
|
161
166
|
output_file.unlink()
|
|
162
167
|
|
|
163
|
-
|
|
168
|
+
try:
|
|
169
|
+
output_wcs = WCS(output_header)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error("Error creating new header")
|
|
172
|
+
for k in output_header:
|
|
173
|
+
logger.error(f"{k} = {output_header[k]}")
|
|
174
|
+
raise e
|
|
164
175
|
output_shape = output_wcs.array_shape
|
|
165
176
|
msg = f"Creating a new FITS file with shape {output_shape}"
|
|
166
177
|
logger.info(msg)
|
|
178
|
+
|
|
167
179
|
# If the output shape is less than 1801, we can create a blank array
|
|
168
180
|
# in memory and write it to disk
|
|
169
181
|
if np.prod(output_shape) < 1801:
|
|
@@ -234,6 +246,7 @@ async def create_output_cube_coro(
|
|
|
234
246
|
single_beam: bool = False,
|
|
235
247
|
overwrite: bool = False,
|
|
236
248
|
time_domain_mode: bool = False,
|
|
249
|
+
bounding_box: BoundingBox | None = None,
|
|
237
250
|
) -> InitResult:
|
|
238
251
|
"""Initialize the data cube.
|
|
239
252
|
|
|
@@ -324,6 +337,13 @@ async def create_output_cube_coro(
|
|
|
324
337
|
key = f"{k}{fits_idx}"
|
|
325
338
|
logger.debug(f"{key}={new_header[key]}")
|
|
326
339
|
|
|
340
|
+
# Add extra transform fields for consistency
|
|
341
|
+
if ("CD1_1" in new_header or "PC1_1" in new_header) and fits_idx != 1:
|
|
342
|
+
transform_type = "CD" if "CD1_1" in new_header else "PC"
|
|
343
|
+
pv1 = f"{transform_type}{fits_idx}_{fits_idx}"
|
|
344
|
+
logger.info(f"Adding {pv1} to header")
|
|
345
|
+
new_header[pv1] = 1.0
|
|
346
|
+
|
|
327
347
|
if ignore_spec or not even_spec:
|
|
328
348
|
logger.info(
|
|
329
349
|
f"Ignore the specrency information, {ignore_spec=} or {not even_spec=}"
|
|
@@ -345,8 +365,13 @@ async def create_output_cube_coro(
|
|
|
345
365
|
)
|
|
346
366
|
del new_header["BMAJ"], new_header["BMIN"], new_header["BPA"]
|
|
347
367
|
|
|
348
|
-
if
|
|
349
|
-
|
|
368
|
+
if bounding_box:
|
|
369
|
+
logger.info("Updating CRPIX1 and CRPIX2 header values to reflect bounding box")
|
|
370
|
+
new_header["CRPIX1"] -= bounding_box.ymin
|
|
371
|
+
new_header["CRPIX2"] -= bounding_box.xmin
|
|
372
|
+
logger.info("Updating NAXIS1 and NAXIS2 ro reflect bounding box")
|
|
373
|
+
new_header["NAXIS1"] = bounding_box.y_span
|
|
374
|
+
new_header["NAXIS2"] = bounding_box.x_span
|
|
350
375
|
|
|
351
376
|
plane_shape = list(old_data.shape)
|
|
352
377
|
cube_shape = plane_shape.copy()
|
|
@@ -609,6 +634,8 @@ async def process_channel(
|
|
|
609
634
|
old_channel: int,
|
|
610
635
|
is_missing: bool,
|
|
611
636
|
file_list: list[Path],
|
|
637
|
+
bounding_box: BoundingBox | None = None,
|
|
638
|
+
invalidate_zeros: bool = False,
|
|
612
639
|
) -> None:
|
|
613
640
|
msg = f"Processing channel {new_channel}"
|
|
614
641
|
logger.info(msg)
|
|
@@ -621,6 +648,15 @@ async def process_channel(
|
|
|
621
648
|
fits.getdata, file_list[old_channel], memmap=False
|
|
622
649
|
)
|
|
623
650
|
|
|
651
|
+
if bounding_box is not None:
|
|
652
|
+
plane = plane[
|
|
653
|
+
...,
|
|
654
|
+
bounding_box.xmin : bounding_box.xmax,
|
|
655
|
+
bounding_box.ymin : bounding_box.ymax,
|
|
656
|
+
]
|
|
657
|
+
if invalidate_zeros:
|
|
658
|
+
plane[plane == 0.0] = np.nan
|
|
659
|
+
|
|
624
660
|
await write_channel_to_cube_coro(
|
|
625
661
|
file_handle=file_handle,
|
|
626
662
|
plane=plane,
|
|
@@ -640,6 +676,8 @@ async def combine_fits_coro(
|
|
|
640
676
|
overwrite: bool = False,
|
|
641
677
|
max_workers: int | None = None,
|
|
642
678
|
time_domain_mode: bool = False,
|
|
679
|
+
bounding_box: bool = False,
|
|
680
|
+
invalidate_zeros: bool = False,
|
|
643
681
|
) -> u.Quantity:
|
|
644
682
|
"""Combine FITS files into a cube.
|
|
645
683
|
Can handle either frequency or time dimensions agnostically
|
|
@@ -650,6 +688,8 @@ async def combine_fits_coro(
|
|
|
650
688
|
ignore_spec (bool, optional): Ignore frequency/time information. Defaults to False.
|
|
651
689
|
create_blanks (bool, optional): Attempt to create even frequency spacing. Defaults to False.
|
|
652
690
|
time_domain_mode (bool, optional): Work in time domain mode - make a time-cube. Default = False.
|
|
691
|
+
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
|
+
invalidate_zeros (bool, optionals): Set pixels whose values are exactly zero to NaNs. Defaults to False.
|
|
653
693
|
|
|
654
694
|
Returns:
|
|
655
695
|
tuple[fits.HDUList, u.Quantity]: The combined FITS cube and frequencies
|
|
@@ -694,6 +734,21 @@ async def combine_fits_coro(
|
|
|
694
734
|
specs = specs[new_sort_idx]
|
|
695
735
|
missing_chan_idx = missing_chan_idx[new_sort_idx]
|
|
696
736
|
|
|
737
|
+
# Get the bounding box, if requested
|
|
738
|
+
final_bounding_box = None
|
|
739
|
+
if bounding_box:
|
|
740
|
+
boxes_futures = [
|
|
741
|
+
get_bounding_box_for_fits_coro(
|
|
742
|
+
fits_path=fits_path, invalidate_zeros=invalidate_zeros
|
|
743
|
+
)
|
|
744
|
+
for fits_path in file_list
|
|
745
|
+
]
|
|
746
|
+
boxes = await gather_with_limit(
|
|
747
|
+
max_workers, *boxes_futures, desc="Bounding boxes"
|
|
748
|
+
)
|
|
749
|
+
final_bounding_box = extract_common_bounding_box(bounding_boxes=boxes)
|
|
750
|
+
logger.info(f"The final bounding box is: {final_bounding_box=}")
|
|
751
|
+
|
|
697
752
|
# Initialize the data cube
|
|
698
753
|
new_header, _, _, _ = await create_output_cube_coro(
|
|
699
754
|
old_name=file_list[0],
|
|
@@ -704,6 +759,7 @@ async def combine_fits_coro(
|
|
|
704
759
|
single_beam=single_beam,
|
|
705
760
|
overwrite=overwrite,
|
|
706
761
|
time_domain_mode=time_domain_mode,
|
|
762
|
+
bounding_box=final_bounding_box,
|
|
707
763
|
)
|
|
708
764
|
|
|
709
765
|
new_channels = np.arange(len(specs))
|
|
@@ -731,6 +787,8 @@ async def combine_fits_coro(
|
|
|
731
787
|
old_channel=old_channel,
|
|
732
788
|
is_missing=is_missing,
|
|
733
789
|
file_list=file_list,
|
|
790
|
+
bounding_box=final_bounding_box,
|
|
791
|
+
invalidate_zeros=invalidate_zeros,
|
|
734
792
|
)
|
|
735
793
|
coros.append(coro)
|
|
736
794
|
|
|
@@ -813,6 +871,16 @@ def get_parser(
|
|
|
813
871
|
default=None,
|
|
814
872
|
help="Maximum number of workers to use for concurrent processing",
|
|
815
873
|
)
|
|
874
|
+
parser.add_argument(
|
|
875
|
+
"--bounding-box",
|
|
876
|
+
action="store_true",
|
|
877
|
+
help="Attempt to consider padded images when creating the cube. Requires an extract read of the input data.",
|
|
878
|
+
)
|
|
879
|
+
parser.add_argument(
|
|
880
|
+
"--invalidate-zeros",
|
|
881
|
+
action="store_true",
|
|
882
|
+
help="Set pixels whose values are exactly zero to NaNs",
|
|
883
|
+
)
|
|
816
884
|
|
|
817
885
|
return parser
|
|
818
886
|
|
|
@@ -853,6 +921,8 @@ def cli(args: argparse.Namespace | None = None) -> None:
|
|
|
853
921
|
overwrite=overwrite,
|
|
854
922
|
max_workers=args.max_workers,
|
|
855
923
|
time_domain_mode=time_domain_mode,
|
|
924
|
+
bounding_box=args.bounding_box,
|
|
925
|
+
invalidate_zeros=args.invalidate_zeros,
|
|
856
926
|
)
|
|
857
927
|
|
|
858
928
|
spequency = "times" if time_domain_mode else "frequencies"
|
|
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
|
|
File without changes
|