Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
with:
fetch-depth: 0

- uses: hynek/build-and-inspect-python-package@v2
- uses: hynek/build-and-inspect-python-package@v3.0.1

publish:
needs: [dist]
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ jobs:
fetch-depth: 0
- uses: actions/setup-python@v6
with:
python-version: "3.x"
python-version: |
3.11
3.x

- uses: astral-sh/setup-uv@v7

Expand All @@ -42,7 +44,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.12", "3.13", "3.14"]
python-version: ["3.11", "3.12", "3.13", "3.14"]
runs-on: [ubuntu-latest]

steps:
Expand Down
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ repos:
- id: mypy
files: eye_patch|tests
args: ["--ignore-missing-imports"]
language_version: python3.11
additional_dependencies:
- numpy
- capn-crunch
Expand Down
8 changes: 5 additions & 3 deletions eye_patch/masking.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,9 @@ def beam_shape_erode(
scales (list[int] | tuple[int, ...] | None, optional): Defines the scales that are being used during multi-scale clean. Perform the beam erosion at each of these scales. Defaults to None.

Returns:
MaskLike: The eroded beam shape. If a no/single scale provide it is a bool return, otherwise int32.
MaskLike: The eroded beam shape. For no/single scale the dtype of ``mask`` is
preserved; for multiple scales a float64 bitmask is returned, where the
n'th scale is stored as the n'th bit.
"""

if not all(key in fits_header for key in ["BMAJ", "BMIN", "BPA"]):
Expand Down Expand Up @@ -899,14 +901,14 @@ def create_snr_mask_from_fits(
mask_data = reverse_negative_flood_fill(
base_image=np.squeeze(signal_data),
masking_options=masking_options,
)
).astype(np.float32)
mask_data = mask_data.reshape(signal_data.shape)
else:
logger.info(f"Clipping using a {masking_options.base_snr_clip=}")
mask_data = (signal_data > masking_options.base_snr_clip).astype(float)

if masking_options.beam_shape_erode:
mask_data = beam_shape_erode( # type: ignore[assignment]
mask_data = beam_shape_erode(
mask=mask_data,
fits_header=fits_header,
minimum_response=masking_options.beam_shape_erode_minimum_response,
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
Expand Down Expand Up @@ -98,7 +97,7 @@ report.exclude_also = ['\.\.\.', 'if typing.TYPE_CHECKING:']

[tool.mypy]
files = ["eye_patch", "tests"]
python_version = "3.10"
python_version = "3.11"
warn_unused_configs = true
strict = true
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
Expand Down
52 changes: 52 additions & 0 deletions tests/test_masking.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,58 @@ def test_fits_masking(fits_dir):
assert valid == np.prod(SHAPE)


@pytest.fixture
def beam_fits_dir(tmp_path: Path, beam_fits_header: fits.Header) -> Path:
"""As ``fits_dir``, but the image carries the beam keywords that
``beam_shape_erode`` needs to build its kernel."""
beam_fits_dir = tmp_path / "beam_fits"
beam_fits_dir.mkdir()

fits.writeto(beam_fits_dir / "image.fits", np.ones(SHAPE), header=beam_fits_header)
fits.writeto(beam_fits_dir / "rms.fits", np.ones(SHAPE) * 0.1)
fits.writeto(beam_fits_dir / "bkg.fits", np.ones(SHAPE) * 0.5)

return beam_fits_dir


def test_fits_masking_flood_fill_with_beam_erode(beam_fits_dir: Path) -> None:
"""The flood filled mask reaches ``beam_shape_erode``, which types its input
as floats. This is the path the bool mask was travelling down."""
masking_options = MaskingOptions(flood_fill=True, beam_shape_erode=True)
names = create_snr_mask_from_fits(
fits_image_path=beam_fits_dir / "image.fits",
fits_rms_path=beam_fits_dir / "rms.fits",
fits_bkg_path=beam_fits_dir / "bkg.fits",
masking_options=masking_options,
)

assert isinstance(names, FITSMaskNames)
assert names.mask_fits.exists()

mask_data = fits.getdata(names.mask_fits)
assert mask_data.dtype.kind == "f", "A bool mask cannot be written to FITS"


def test_fits_masking_flood_fill_writes_float_mask(fits_dir: Path) -> None:
"""The flood fill path builds its mask as a bool array, which ``fits.writeto``
rejects. Reported by Beth via #14, and not caught earlier because every other
mask test runs with ``flood_fill=False``."""
masking_options = MaskingOptions(flood_fill=True)
names = create_snr_mask_from_fits(
fits_image_path=fits_dir / "image.fits",
fits_rms_path=fits_dir / "rms.fits",
fits_bkg_path=fits_dir / "bkg.fits",
masking_options=masking_options,
)

assert isinstance(names, FITSMaskNames)
assert names.mask_fits.exists()

mask_data = fits.getdata(names.mask_fits)
assert mask_data.dtype.kind == "f", "A bool mask cannot be written to FITS"
assert np.sum(mask_data) == np.prod(SHAPE)


def test_fits_masking_with_signal(fits_dir):
masking_options = MaskingOptions(flood_fill=False)
names = create_snr_mask_from_fits(
Expand Down
Loading