Skip to content

Commit 1637bc5

Browse files
authored
Merge pull request #418 from EIT-ALIVE/feature/389-watershed
Adds a WatershedLungspace class, as well as a lot of general improvements to make this possible. ## PixelMap An AmplitudeMap and IntegerMap (subclasses of PixelMap) have been added, including plotting configurations. For IntegerMaps to be possible, the data type of PixelMap values has been made variable (set per class), and casting to that type has been made safe (i.e., a change in actual value when converting from e.g. float to int will throw an error). Creating a mask from a PixelMap has been improved. PixelMap now has three methods to get the values array as boolean, integer or non-nan-float array. PixelMap plotting now has contour and surface methods. ## PixelMask PixelMasks can now be plotted using the same imshow method as PixelMap. PixelMasks gains a subtraction method. The standard masks are now generated using get_geomtric_mask, which allows for different shapes. ## TIV and AmplitudeLungspace TIVLungspace and AmplitudeLungspace provide easy determination of functional lung space. ## Warnings The stack level of warnings has been set to 2 everywhere. Also, warnings in tests have been explicitly silenced.
2 parents e8c9372 + f45fc39 commit 1637bc5

36 files changed

Lines changed: 75570 additions & 208 deletions

docs/api/datacontainers/pixelmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
- PixelMap
55
- DifferenceMap
66
- TIVMap
7+
- AmplitudeMap
78
- ODCLMap
89
- PerfusionMap
910
- PendelluftMap
1011
- SignedPendelluftMap
12+
- IntegerMap
1113
- PlotParameters

docs/api/roi/tivlungspace.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
::: eitprocessing.roi.tiv.TIVLungspace
2+
3+
::: eitprocessing.roi.amplitude.AmplitudeLungspace

docs/api/roi/watershed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
::: eitprocessing.roi.watershed.WatershedLungspace

eitprocessing/datahandling/continuousdata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def __post_init__(self) -> None:
5858
"`sample_frequency` is set to `None`. This will not be supported in future versions. "
5959
"Provide a sample frequency when creating a ContinuousData object."
6060
)
61-
warnings.warn(msg, DeprecationWarning)
61+
warnings.warn(msg, DeprecationWarning, stacklevel=2)
6262

6363
if (lv := len(self.values)) != (lt := len(self.time)):
6464
msg = f"The number of time points ({lt}) does not match the number of values ({lv})."

eitprocessing/datahandling/eitdata.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
from __future__ import annotations
22

33
import warnings
4-
from dataclasses import dataclass, field
4+
from dataclasses import InitVar, dataclass, field
55
from enum import auto
66
from pathlib import Path
7-
from typing import TYPE_CHECKING, TypeVar
7+
from typing import TYPE_CHECKING, Any, TypeVar
88

99
import numpy as np
10-
from strenum import LowercaseStrEnum
10+
from strenum import LowercaseStrEnum # TODO: EOL 3.10: replace with native StrEnum
1111

1212
from eitprocessing.datahandling import DataContainer
13+
from eitprocessing.datahandling.continuousdata import ContinuousData
1314
from eitprocessing.datahandling.mixins.slicing import SelectByTime
1415

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

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

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

69+
if not suppress_simulated_warning and self.vendor == Vendor.SIMULATED:
70+
warnings.warn(
71+
"The simulated vendor is used for testing purposes. "
72+
"It is not a real vendor and should not be used in production code.",
73+
UserWarning,
74+
stacklevel=2,
75+
)
76+
6777
@property
6878
def framerate(self) -> float:
6979
"""Deprecated alias to `sample_frequency`."""
7080
warnings.warn(
7181
"The `framerate` attribute has been deprecated. Use `sample_frequency` instead.",
7282
DeprecationWarning,
83+
stacklevel=2,
7384
)
7485
return self.sample_frequency
7586

@@ -135,6 +146,28 @@ def _sliced_copy(
135146
def __len__(self):
136147
return self.pixel_impedance.shape[0]
137148

149+
def get_summed_impedance(self, *, return_label: str | None = None, **return_kwargs) -> ContinuousData:
150+
"""Return a ContinuousData-object with the same time axis and summed pixel values over time.
151+
152+
Args:
153+
return_label: The label of the returned object; defaults to 'summed <label>' where '<label>' is the label of
154+
the current object.
155+
**return_kwargs: Keyword arguments for the creation of the returned object.
156+
"""
157+
summed_impedance = np.nansum(self.pixel_impedance, axis=(1, 2))
158+
159+
if return_label is None:
160+
return_label = f"summed {self.label}"
161+
162+
return_kwargs_: dict[str, Any] = {
163+
"name": return_label,
164+
"unit": "AU",
165+
"category": "impedance",
166+
"sample_frequency": self.sample_frequency,
167+
} | return_kwargs
168+
169+
return ContinuousData(label=return_label, time=np.copy(self.time), values=summed_impedance, **return_kwargs_)
170+
138171
def calculate_global_impedance(self) -> np.ndarray:
139172
"""Return the global impedance, i.e. the sum of all included pixels at each frame."""
140173
return np.nansum(self.pixel_impedance, axis=(1, 2))
@@ -148,3 +181,4 @@ class Vendor(LowercaseStrEnum):
148181
SENTEC = auto()
149182
DRAGER = DRAEGER
150183
DRÄGER = DRAEGER # noqa: PLC2401
184+
SIMULATED = auto()

eitprocessing/datahandling/loading/draeger.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def load_from_single_path(
6868
f"the first frame selected ({first_frame}, total frames: "
6969
f"{total_frames}).\n {n_frames} frames will be loaded."
7070
)
71-
warnings.warn(msg)
71+
warnings.warn(msg, RuntimeWarning, stacklevel=2)
7272

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

199199
return sample_frequency
200200

@@ -274,7 +274,7 @@ def _read_frame(
274274
if ((previous_marker is not None) and (event_marker > previous_marker)) or (index == 0 and event_text):
275275
events.append((frame_time, Event(event_marker, event_text)))
276276
if timing_error:
277-
warnings.warn("A timing error was encountered during loading.")
277+
warnings.warn("A timing error was encountered during loading.", RuntimeWarning, stacklevel=2)
278278
# TODO: expand on what timing errors are in some documentation.
279279
if min_max_flag in (1, -1):
280280
phases.append((frame_time, min_max_flag))

eitprocessing/datahandling/loading/sentec.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def load_from_single_path( # noqa: C901, PLR0912
9090
"Sample frequency value found in file. "
9191
f"The sample frequency value will be set to {sample_frequency:.2f}"
9292
)
93-
warnings.warn(msg)
93+
warnings.warn(msg, RuntimeWarning, stacklevel=2)
9494

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

113113
if not sample_frequency:
114114
sample_frequency = SENTEC_SAMPLE_FREQUENCY

eitprocessing/datahandling/loading/timpel.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ def load_from_single_path(
7474
f"than the available number ({data.shape[0]}) of frames after "
7575
f"the first frame selected ({first_frame}).\n"
7676
f"{data.shape[0]} frames have been loaded.",
77+
RuntimeWarning,
78+
stacklevel=2,
7779
)
7880
nframes = data.shape[0]
7981

eitprocessing/datahandling/mixins/slicing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def select_by_index(
5252
object is attached.
5353
"""
5454
if start is None and end is None:
55-
warnings.warn("No starting or end timepoint was selected.")
55+
warnings.warn("No starting or end timepoint was selected.", UserWarning, stacklevel=2)
5656
return self
5757

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

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

150150
if not np.all(np.sort(self.time) == self.time):

0 commit comments

Comments
 (0)