diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml index e378911d8..07bc812ff 100644 --- a/.github/workflows/release_pypi.yml +++ b/.github/workflows/release_pypi.yml @@ -27,27 +27,26 @@ jobs: needs: [build] runs-on: ubuntu-latest if: github.event_name == 'workflow_dispatch' + permissions: + id-token: write steps: - uses: actions/download-artifact@v4 with: name: artifact path: dist - - uses: pypa/gh-action-pypi-publish@v1.4.2 + - uses: pypa/gh-action-pypi-publish@release/v1 with: - user: __token__ - password: ${{ secrets.TEST_PYPI_TOKEN_ALIVE }} - repository_url: https://test.pypi.org/legacy/ + repository-url: https://test.pypi.org/legacy/ upload_pypi: needs: [build] runs-on: ubuntu-latest if: github.event_name == 'release' && github.event.action == 'published' + permissions: + id-token: write steps: - uses: actions/download-artifact@v4 with: name: artifact path: dist - - uses: pypa/gh-action-pypi-publish@v1.4.2 - with: - user: __token__ - password: ${{ secrets.PYPI_TOKEN_ALIVE }} + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/eitprocessing/filters/butterworth_filters.py b/eitprocessing/filters/butterworth_filters.py index 459e2b5a9..f7a74609a 100644 --- a/eitprocessing/filters/butterworth_filters.py +++ b/eitprocessing/filters/butterworth_filters.py @@ -52,7 +52,7 @@ class ButterworthFilter(TimeDomainFilter): """ filter_type: Literal["lowpass", "highpass", "bandpass", "bandstop"] - cutoff_frequency: float | tuple[float] + cutoff_frequency: float | tuple[float, float] order: int sample_frequency: float ignore_max_order: InitVar[bool] = False diff --git a/eitprocessing/roi/__init__.py b/eitprocessing/roi/__init__.py index 592dafda6..3abb0cfcf 100644 --- a/eitprocessing/roi/__init__.py +++ b/eitprocessing/roi/__init__.py @@ -4,21 +4,8 @@ module is `PixelMask`. Any type of region of interest selection results in a `PixelMask` object. A mask can be applied to any pixel dataset (EITData, PixelMap) with the same shape. -Several default masks have been predefined. NB: the right side of the patient is to the left side of the EIT image and -vice versa. - -- `VENTRAL_MASK` includes only the first 16 rows; -- `DORSAL_MASK` includes only the last 16 rows; -- `ANATOMICAL_RIGHT_MASK` includes only the first 16 columns; -- `ANATOMICAL_LEFT_MASK` includes only the last 16 columns; -- `QUADRANT_1_MASK` includes the top right quadrant; -- `QUADRANT_2_MASK` includes the top left quadrant; -- `QUADRANT_3_MASK` includes the bottom right quadrant; -- `QUADRANT_4_MASK` includes the bottom left quadrant; -- `LAYER_1_MASK` includes only the first 8 rows; -- `LAYER_2_MASK` includes only the second set of 8 rows; -- `LAYER_3_MASK` includes only the third set of 8 rows; -- `LAYER_4_MASK` includes only the last 8 rows. +Predefined default masks can be created using `get_geometric_mask()`. These masks are based on common anatomical regions +of interest. """ from __future__ import annotations @@ -296,7 +283,7 @@ def __eq__(self, other: object) -> bool: return self.mask.shape == other.mask.shape and np.array_equal(self.mask, other.mask, equal_nan=True) -def get_geometric_mask(mask: str, shape: tuple[int, int] = (32, 32)) -> PixelMask: +def get_geometric_mask(mask: str, shape: tuple[int, int] = (32, 32)) -> PixelMask: # noqa: PLR0911 """Get a geometric mask by name. Masks can be generated for appropriates shapes, provided by the shape` argument, a tuple of two integers @@ -306,6 +293,7 @@ def get_geometric_mask(mask: str, shape: tuple[int, int] = (32, 32)) -> PixelMas The function accepts both full names (e.g., "layer 1") and abbreviations (e.g., "L1"). The following masks are available: + - "global" or "G": all pixels in the EIT image. - "ventral" or "V": the first half rows of the EIT image. - "dorsal" or "D": the last half rows of the EIT image. - "anatomical right" or "R": the first half columns of the EIT image. @@ -332,23 +320,6 @@ def get_geometric_mask(mask: str, shape: tuple[int, int] = (32, 32)) -> PixelMas ValueError: If an unknown mask name is provided. ValueError: If the shape is not compatible with the requested mask. """ - - def _check_dimensions( - name: str, shape: tuple[int, int], *, height_divisor: int | None = None, width_divisor: int | None = None - ) -> None: - total_height, total_width = shape - if isinstance(height_divisor, int) and total_height % height_divisor != 0: - msg = ( - f"Shape {shape} is not compatible with a {name} mask. " - "The height must be a multiple of {height_divisor}." - ) - raise ValueError(msg) - if isinstance(width_divisor, int) and total_width % width_divisor != 0: - msg = ( - f"Shape {shape} is not compatible with a {name} mask. The width must be a multiple of {width_divisor}." - ) - raise ValueError(msg) - total_height, total_width = shape n_layers_quadrants = 4 @@ -357,6 +328,8 @@ def _check_dimensions( mask = re.sub(r"^L([1-4]{1})$", r"layer \1", mask) match mask.split(" "): + case ["global"] | ["G"]: + return PixelMask(np.ones(shape), label="global") case ["ventral"] | ["V"]: _check_dimensions("ventral", shape, height_divisor=2) height = total_height // 2 @@ -387,7 +360,7 @@ def _check_dimensions( case ["layer", num_str] if num_str.isdigit() and 1 <= int(num_str) <= n_layers_quadrants: num = int(num_str) - _check_dimensions("layer", shape, height_divisor=4) + _check_dimensions("layer", shape, height_divisor=n_layers_quadrants) height = total_height // n_layers_quadrants return PixelMask( np.concatenate( @@ -411,3 +384,17 @@ def _check_dimensions( case _: msg = f"Unknown mask name: {mask}." raise ValueError(msg) + + +def _check_dimensions( + name: str, shape: tuple[int, int], *, height_divisor: int | None = None, width_divisor: int | None = None +) -> None: + total_height, total_width = shape + if isinstance(height_divisor, int) and total_height % height_divisor != 0: + msg = ( + f"Shape {shape} is not compatible with a {name} mask. The height must be a multiple of {{height_divisor}}." + ) + raise ValueError(msg) + if isinstance(width_divisor, int) and total_width % width_divisor != 0: + msg = f"Shape {shape} is not compatible with a {name} mask. The width must be a multiple of {width_divisor}." + raise ValueError(msg) diff --git a/pyproject.toml b/pyproject.toml index b8c989308..73aaeec30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,6 +110,7 @@ eitprocessing = ["config/*.yaml", "py.typed"] output-format = "concise" line-length = 120 extend-include = ["*.ipynb"] +target-version = "py310" [tool.ruff.lint] select = ["ALL"] diff --git a/tests/test_pixelmask.py b/tests/test_pixelmask.py index a69e16698..eb2d5e5a2 100644 --- a/tests/test_pixelmask.py +++ b/tests/test_pixelmask.py @@ -220,6 +220,14 @@ def test_pixelmask_subtract(): assert np.array_equal(pm3.mask, np.array([[np.nan, np.nan, 1, np.nan], [0.1, np.nan, np.nan, 0.5]]), equal_nan=True) +@pytest.mark.parametrize("shape", [(32, 1), (64, 1), (16, 1), (100, 1), (4, 1)]) +def test_predefined_global_mask(shape: tuple[int, int]): + global_mask = get_geometric_mask("global", shape) + assert global_mask.label == "global" + assert global_mask.shape == shape + assert np.all(global_mask.mask == 1.0) + + @pytest.mark.parametrize("shape", [(32, 1), (64, 1), (16, 1), (100, 1), (4, 1)]) def test_predefined_layer_masks(shape: tuple[int, int]): quarter_height = shape[0] // 4