Skip to content
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
c5bca1f
Limit version of pytest
psomhorst Aug 18, 2025
e4ff11d
Set stack level and type of warnings
psomhorst Aug 18, 2025
ddffe25
Add subtraction to PixelMask class
psomhorst Aug 13, 2025
287a1ad
Add support for PixelMask plotting in PixelMap plotting
psomhorst Aug 18, 2025
9e33fb0
Add function to get summed impedance to EITData
psomhorst Aug 18, 2025
3248b61
Rename absolute argument to use_magnitude and add fraction_of_max arg…
psomhorst Aug 18, 2025
27905b3
Add captures to PixelMap.apply
psomhorst Aug 18, 2025
5aa5e0c
Add IntegerMap including PlotConfig
psomhorst Aug 18, 2025
dfc27bf
Add to_..._array convenience methods to PixelMap
psomhorst Aug 18, 2025
7291afd
Add default PixelMapPlotConfig to registry
psomhorst Aug 18, 2025
5734478
Update deprecated function name
psomhorst Aug 18, 2025
cac298c
Add PixelMask.values alias to .mask
psomhorst Aug 18, 2025
d43f8ff
Add PixelMask.shape property
psomhorst Aug 18, 2025
ca952fb
Add __eq__ method to check equivalence of PixelMasks
psomhorst Aug 18, 2025
1fc168b
Replace fixed default masks with generated masks, based on shape
psomhorst Aug 18, 2025
f24510b
Add contour and surface plots to PixelMap
psomhorst Aug 18, 2025
bdb9b1c
Update tests for PixelMasks
psomhorst Aug 18, 2025
513f28b
Update tests for PixelMap
psomhorst Aug 18, 2025
23cb2bb
Add option for simulated data to Vendor
psomhorst Aug 18, 2025
69f5c2b
Add WatershedLungspace class and accompanying tests
psomhorst Aug 18, 2025
c69dea0
Add notebook with Watershed demonstration
psomhorst Aug 18, 2025
5bac9b0
Add watershed and notebook to documentation
psomhorst Aug 18, 2025
a67d7fe
Update PixelMaskCollection tests to use real EITData
psomhorst Aug 18, 2025
4e0ff70
Improve IntegerMap with dtype class variable and lossless conversion …
psomhorst Aug 22, 2025
dff9811
Add test value conversion at init
psomhorst Aug 22, 2025
207321f
Update IntegerMap plot config
psomhorst Aug 22, 2025
251e48d
Update notebook to show integer map plotting
psomhorst Aug 22, 2025
48b3dc9
Update to_boolean_array to optionally keep zero values as True
psomhorst Aug 22, 2025
d6e6cb4
Update pixelmap tests
psomhorst Aug 22, 2025
17a8530
Add check for no markers inside TIV mask
psomhorst Aug 22, 2025
90a8b34
Fix issue where BreathDetection fails if no peaks are found
psomhorst Aug 22, 2025
672e47d
Raise error if no breaths are found in breath detection
psomhorst Aug 22, 2025
7e82552
Add test for watershed with no amplitude
psomhorst Aug 22, 2025
95af7b4
Add test for captures
psomhorst Aug 22, 2025
b3aa395
Fix langauge issues
psomhorst Aug 22, 2025
c85340e
Add TIVLungspace class
psomhorst Aug 25, 2025
f2fb3a6
Add tests for TIVLungspace class
psomhorst Aug 25, 2025
0ae5e52
Add TIV lungspace to docs
psomhorst Aug 25, 2025
6eacd1b
Use TIVLungspace in WatershedLungspace
psomhorst Aug 25, 2025
fedebb7
Add AmplitudeMap, AmplitudeMapPlotConfig
psomhorst Aug 25, 2025
8a1812a
Update notebook to show AmplitudeMap
psomhorst Aug 25, 2025
9d69897
Create separate AmplitudeLungspace class, and remove modes from TIVLu…
psomhorst Aug 25, 2025
a9042fe
Update documentation for AmplitudeLungspace
psomhorst Aug 25, 2025
1f0aaa6
Update Watershed to use AmplitudeLungspace
psomhorst Aug 25, 2025
dd072d5
Re-add warning for AmplitudeLungspace
psomhorst Aug 25, 2025
1c56417
Update tests for AmplitudeLungspace
psomhorst Aug 25, 2025
8abfd6c
Add comment about test coverage
psomhorst Aug 25, 2025
1b434c3
Add AmplitudeMap and IntegerMap to pixelmap docs
psomhorst Aug 25, 2025
93c9a7e
Fix typo
psomhorst Aug 25, 2025
51b42ef
Only consider pixels that are not all-NaN when aggregating
psomhorst Aug 25, 2025
3daeba8
Suppress zero warning when subtracting masks
psomhorst Aug 25, 2025
b909af1
Add exception when trying to normalize a all-NaN PixelMap
psomhorst Aug 25, 2025
650c5f2
Add explicit check for all-NaN values
psomhorst Aug 25, 2025
9c33751
Catch or prevent warnings in tests
psomhorst Aug 25, 2025
f45fc39
Test butterworth filters with apply instead of deprecated apply_filter
psomhorst Aug 25, 2025
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
1 change: 1 addition & 0 deletions docs/api/roi/tivlungspace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: eitprocessing.roi.tiv.TIVLungspace
1 change: 1 addition & 0 deletions docs/api/roi/watershed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: eitprocessing.roi.watershed.WatershedLungspace
2 changes: 1 addition & 1 deletion eitprocessing/datahandling/continuousdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def __post_init__(self) -> None:
"`sample_frequency` is set to `None`. This will not be supported in future versions. "
"Provide a sample frequency when creating a ContinuousData object."
)
warnings.warn(msg, DeprecationWarning)
warnings.warn(msg, DeprecationWarning, stacklevel=2)

if (lv := len(self.values)) != (lt := len(self.time)):
msg = f"The number of time points ({lt}) does not match the number of values ({lv})."
Expand Down
42 changes: 38 additions & 4 deletions eitprocessing/datahandling/eitdata.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from __future__ import annotations

import warnings
from dataclasses import dataclass, field
from dataclasses import InitVar, dataclass, field
from enum import auto
from pathlib import Path
from typing import TYPE_CHECKING, TypeVar
from typing import TYPE_CHECKING, Any, TypeVar

import numpy as np
from strenum import LowercaseStrEnum
from strenum import LowercaseStrEnum # TODO: EOL 3.10: replace with native StrEnum

from eitprocessing.datahandling import DataContainer
from eitprocessing.datahandling.continuousdata import ContinuousData
from eitprocessing.datahandling.mixins.slicing import SelectByTime

if TYPE_CHECKING:
Expand Down Expand Up @@ -49,8 +50,9 @@ class is meant to hold data from (part of) a singular continuous measurement.
description: str = field(default="", compare=False, repr=False)
name: str | None = field(default=None, compare=False, repr=False)
pixel_impedance: np.ndarray = field(repr=False, kw_only=True)
suppress_simulated_warning: InitVar[bool] = False

def __post_init__(self):
def __post_init__(self, suppress_simulated_warning: bool) -> None:
if not self.label:
self.label = f"{self.__class__.__name__}_{id(self)}"

Expand All @@ -64,12 +66,21 @@ def __post_init__(self):
msg = f"The number of time points ({lt}) does not match the number of pixel impedance values ({lv})."
raise ValueError(msg)

if not suppress_simulated_warning and self.vendor == Vendor.SIMULATED:
warnings.warn(
"The simulated vendor is used for testing purposes. "
"It is not a real vendor and should not be used in production code.",
UserWarning,
stacklevel=2,
)

@property
def framerate(self) -> float:
"""Deprecated alias to `sample_frequency`."""
warnings.warn(
"The `framerate` attribute has been deprecated. Use `sample_frequency` instead.",
DeprecationWarning,
stacklevel=2,
)
return self.sample_frequency

Expand Down Expand Up @@ -135,6 +146,28 @@ def _sliced_copy(
def __len__(self):
return self.pixel_impedance.shape[0]

def get_summed_impedance(self, *, return_label: str | None = None, **return_kwargs) -> ContinuousData:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this for summing impedance within a ROI? Is it necessary to have a separate calculate_global_impedance then? Or I would remove the word "included" in the docstring for calculate_global_impedance since that suggests that there is some sort of selection as well.

@psomhorst psomhorst Aug 22, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that get_summed_impedance will replace the function of calculate_global_impedance. However, for now, calculate_global_impedance is used to calculate the global impedance of the raw signal. The idea is to mark calculate_global_impedance as deprecated in a future version, and remove it altogether in a later version.

Do you think we should already mark it deprecated in this PR? It is not a good idea to make it deprecated in this PR, because it wil result in a DeprecationWarning every time you load EIT data.

"""Return a ContinuousData-object with the same time axis and summed pixel values over time.

Args:
return_label: The label of the returned object; defaults to 'summed <label>' where '<label>' is the label of
the current object.
**return_kwargs: Keyword arguments for the creation of the returned object.
"""
summed_impedance = np.nansum(self.pixel_impedance, axis=(1, 2))

if return_label is None:
return_label = f"summed {self.label}"

return_kwargs_: dict[str, Any] = {
"name": return_label,
"unit": "AU",
"category": "impedance",
"sample_frequency": self.sample_frequency,
} | return_kwargs

return ContinuousData(label=return_label, time=np.copy(self.time), values=summed_impedance, **return_kwargs_)

def calculate_global_impedance(self) -> np.ndarray:
"""Return the global impedance, i.e. the sum of all included pixels at each frame."""
return np.nansum(self.pixel_impedance, axis=(1, 2))
Expand All @@ -148,3 +181,4 @@ class Vendor(LowercaseStrEnum):
SENTEC = auto()
DRAGER = DRAEGER
DRÄGER = DRAEGER # noqa: PLC2401
SIMULATED = auto()
6 changes: 3 additions & 3 deletions eitprocessing/datahandling/loading/draeger.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def load_from_single_path(
f"the first frame selected ({first_frame}, total frames: "
f"{total_frames}).\n {n_frames} frames will be loaded."
)
warnings.warn(msg)
warnings.warn(msg, RuntimeWarning, stacklevel=2)

# We need to load 1 frame before first actual frame to check if there is an event marker. Data for the pre-first
# (dummy) frame will be removed from self at the end of this function.
Expand Down Expand Up @@ -194,7 +194,7 @@ def _estimate_sample_frequency(time: np.ndarray, sample_frequency: float | None)
f"Provided sample frequency ({sample_frequency}) does not match "
f"the estimated sample frequency ({estimated_sample_frequency})."
)
warnings.warn(msg, RuntimeWarning)
warnings.warn(msg, RuntimeWarning, stacklevel=2)

return sample_frequency

Expand Down Expand Up @@ -274,7 +274,7 @@ def _read_frame(
if ((previous_marker is not None) and (event_marker > previous_marker)) or (index == 0 and event_text):
events.append((frame_time, Event(event_marker, event_text)))
if timing_error:
warnings.warn("A timing error was encountered during loading.")
warnings.warn("A timing error was encountered during loading.", RuntimeWarning, stacklevel=2)
# TODO: expand on what timing errors are in some documentation.
if min_max_flag in (1, -1):
phases.append((frame_time, min_max_flag))
Expand Down
4 changes: 2 additions & 2 deletions eitprocessing/datahandling/loading/sentec.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def load_from_single_path( # noqa: C901, PLR0912
"Sample frequency value found in file. "
f"The sample frequency value will be set to {sample_frequency:.2f}"
)
warnings.warn(msg)
warnings.warn(msg, RuntimeWarning, stacklevel=2)

else:
fh.seek(payload_size, os.SEEK_CUR)
Expand All @@ -108,7 +108,7 @@ def load_from_single_path( # noqa: C901, PLR0912
f"the first frame selected ({first_frame}, total frames: "
f"{index}).\n {n_frames} frames will be loaded."
)
warnings.warn(msg)
warnings.warn(msg, RuntimeWarning, stacklevel=2)

if not sample_frequency:
sample_frequency = SENTEC_SAMPLE_FREQUENCY
Expand Down
2 changes: 2 additions & 0 deletions eitprocessing/datahandling/loading/timpel.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ def load_from_single_path(
f"than the available number ({data.shape[0]}) of frames after "
f"the first frame selected ({first_frame}).\n"
f"{data.shape[0]} frames have been loaded.",
RuntimeWarning,
stacklevel=2,
)
nframes = data.shape[0]

Expand Down
4 changes: 2 additions & 2 deletions eitprocessing/datahandling/mixins/slicing.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def select_by_index(
object is attached.
"""
if start is None and end is None:
warnings.warn("No starting or end timepoint was selected.")
warnings.warn("No starting or end timepoint was selected.", UserWarning, stacklevel=2)
return self

start = start if start is not None else 0
Expand Down Expand Up @@ -144,7 +144,7 @@ def select_by_time( # noqa: D417
return copy.deepcopy(self)

if start_time is None and end_time is None:
warnings.warn("No starting or end timepoint was selected.")
warnings.warn("No starting or end timepoint was selected.", UserWarning, stacklevel=2)
return self

if not np.all(np.sort(self.time) == self.time):
Expand Down
Loading