Skip to content

Commit

Permalink
Merge pull request #325 from EIT-ALIVE/320_eeli_sparse_data
Browse files Browse the repository at this point in the history
Return SparseData object from EELI
  • Loading branch information
JulietteFrancovich authored Dec 2, 2024
2 parents ed54c5b + d25fc26 commit 5f543e1
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 121 deletions.
4 changes: 4 additions & 0 deletions eitprocessing/datahandling/continuousdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ def __post_init__(self) -> None:
)
warnings.warn(msg, DeprecationWarning)

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})."
raise ValueError(msg)

def __setattr__(self, attr: str, value: Any): # noqa: ANN401
try:
old_value = getattr(self, attr)
Expand Down
4 changes: 4 additions & 0 deletions eitprocessing/datahandling/eitdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ def __post_init__(self):

self.name = self.name or self.label

if (lv := len(self.pixel_impedance)) != (lt := len(self.time)):
msg = f"The number of time points ({lt}) does not match the number of pixel impedance values ({lv})."
raise ValueError(msg)

@property
def framerate(self) -> float:
"""Deprecated alias to `sample_frequency`."""
Expand Down
4 changes: 4 additions & 0 deletions eitprocessing/datahandling/intervaldata.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ class IntervalData(DataContainer, SelectByIndex, HasTimeIndexer):
def __post_init__(self) -> None:
self.intervals = [Interval._make(interval) for interval in self.intervals]

if self.has_values and (lv := len(self.values)) != (lt := len(self.intervals)):
msg = f"The number of time points ({lt}) does not match the number of values ({lv})."
raise ValueError(msg)

def __len__(self) -> int:
return len(self.intervals)

Expand Down
5 changes: 5 additions & 0 deletions eitprocessing/datahandling/sparsedata.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ def __repr__(self) -> str:
def __len__(self) -> int:
return len(self.time)

def __post_init__(self):
if self.has_values and (lv := len(self.values)) != (lt := len(self.time)):
msg = f"The number of time points ({lt}) does not match the number of values ({lv})."
raise ValueError(msg)

@property
def has_values(self) -> bool:
"""True if the SparseData has values, False otherwise."""
Expand Down
56 changes: 50 additions & 6 deletions eitprocessing/parameters/eeli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from eitprocessing.categories import check_category
from eitprocessing.datahandling.continuousdata import ContinuousData
from eitprocessing.datahandling.sequence import Sequence
from eitprocessing.datahandling.sparsedata import SparseData
from eitprocessing.features.breath_detection import BreathDetection
from eitprocessing.parameters import ParameterCalculation

Expand All @@ -22,7 +24,13 @@ def __post_init__(self):
msg = f"Method {self.method} is not valid. Use any of {', '.join(_methods)}"
raise ValueError(msg)

def compute_parameter(self, continuous_data: ContinuousData) -> np.ndarray:
def compute_parameter(
self,
continuous_data: ContinuousData,
sequence: Sequence | None = None,
store: bool | None = None,
result_label: str = "continuous_eelis",
) -> SparseData:
"""Compute the EELI for each breath in the impedance data.
Example:
Expand All @@ -33,20 +41,56 @@ def compute_parameter(self, continuous_data: ContinuousData) -> np.ndarray:
Args:
continuous_data: a ContinuousData object containing impedance data.
sequence: optional, Sequence to store the result in.
store: whether to store the result in the sequence, defaults to `True` if a Sequence if provided.
result_label: label of the returned SparseData object, defaults to `'continuous_eelis'`.
Returns:
np.ndarray: the end-expiratory values of all breaths in the impedance data.
A SparseData object with the end-expiratory values of all breaths in the impedance data.
Raises:
RuntimeError: If store is set to true but no sequence is provided.
ValueError: If the provided sequence is not an instance of the Sequence dataclass.
ValueError: If tiv_method is not one of 'inspiratory', 'expiratory', or 'mean'.
"""
if store is None and isinstance(sequence, Sequence):
store = True

if store and sequence is None:
msg = "Can't store the result if no Sequence is provided."
raise RuntimeError(msg)

if store and not isinstance(sequence, Sequence):
msg = "To store the result a Sequence dataclass must be provided."
raise ValueError(msg)

check_category(continuous_data, "impedance", raise_=True)

bd_kwargs = self.breath_detection_kwargs.copy()
breath_detection = BreathDetection(**bd_kwargs)
breaths = breath_detection.find_breaths(continuous_data)

if not len(breaths):
return np.array([], dtype=float)
time = np.array([], dtype=float)
values = np.array([], dtype=float)
else:
_, _, end_expiratory_times = zip(*breaths.values, strict=True)
end_expiratory_indices = np.flatnonzero(np.isin(continuous_data.time, end_expiratory_times))
time = [breath.end_time for breath in breaths.values if breath is not None]
values = continuous_data.values[end_expiratory_indices]

_, _, end_expiratory_times = zip(*breaths.values, strict=True)
end_expiratory_indices = np.flatnonzero(np.isin(continuous_data.time, end_expiratory_times))
eeli_container = SparseData(
label=result_label,
name="End-expiratory lung impedance (EELI)",
unit=None,
category="impedance",
time=time,
description="End-expiratory lung impedance (EELI) determined on continuous data",
parameters=self.breath_detection_kwargs,
derived_from=[continuous_data],
values=values,
)
if store:
sequence.sparse_data.add(eeli_container)

return continuous_data.values[end_expiratory_indices]
return eeli_container
16 changes: 12 additions & 4 deletions eitprocessing/parameters/tidal_impedance_variation.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def compute_continuous_parameter(
result_label: label of the returned SparseData object, defaults to `'continuous_tivs'`.
Returns:
A list with the computed TIV values.
A SparseData object with the computed TIV values.
Raises:
RuntimeError: If store is set to true but no sequence is provided.
Expand Down Expand Up @@ -99,7 +99,7 @@ def compute_continuous_parameter(
name="Continuous tidal impedance variation",
unit=None,
category="impedance difference",
time=continuous_data.time,
time=[breath.middle_time for breath in breaths.values if breath is not None],
description="Tidal impedance variation determined on continuous data",
parameters=self.breath_detection_kwargs,
derived_from=[continuous_data],
Expand Down Expand Up @@ -136,7 +136,7 @@ def compute_pixel_parameter(
store: whether to store the result in the sequence, defaults to `True` if a Sequence if provided.
Returns:
An np.ndarray with the computed TIV values.
A SparseData object with the computed TIV values.
Raises:
RuntimeError: If store is set to true but no sequence is provided.
Expand Down Expand Up @@ -186,6 +186,7 @@ def compute_pixel_parameter(

number_of_breaths = len(breath_data)
all_pixels_tiv_values = np.full((number_of_breaths, n_rows, n_cols), None, dtype=object)
all_pixels_breath_timings = np.full((number_of_breaths, n_rows, n_cols), None, dtype=object)

for row, col in itertools.product(range(n_rows), range(n_cols)):
time_series = data[:, row, col]
Expand All @@ -197,19 +198,26 @@ def compute_pixel_parameter(
tiv_method,
tiv_timing,
)
# Get the middle times of each breath where breath is not None
pixel_breath_timings = [breath.middle_time for breath in breaths if breath is not None]

# Store these in all_pixels_breath_timings, ensuring they match the expected shape
all_pixels_breath_timings[: len(pixel_breath_timings), row, col] = pixel_breath_timings

all_pixels_tiv_values[:, row, col] = pixel_tiv_values

tiv_container = SparseData(
label=result_label,
name="Pixel tidal impedance variation",
unit=None,
category="impedance difference",
time=eit_data.time,
time=list(all_pixels_breath_timings),
description="Tidal impedance variation determined on pixel impedance",
parameters=self.breath_detection_kwargs,
derived_from=[eit_data],
values=list(all_pixels_tiv_values.astype(float)),
)

if store:
sequence.sparse_data.add(tiv_container)

Expand Down
47 changes: 22 additions & 25 deletions notebooks/test_parameter_eeli_.ipynb

Large diffs are not rendered by default.

147 changes: 94 additions & 53 deletions notebooks/test_parameter_tiv.ipynb

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions tests/test_eeli.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def test_compute_parameter(
expected_number_breaths = duration * frequency - 1
cd = create_continuous_data_object(sample_frequency, duration * sample_frequency, frequency)
eeli = EELI()
eeli_values = eeli.compute_parameter(cd)
eeli_values = eeli.compute_parameter(cd).values
assert len(eeli_values) == expected_number_breaths
if len(eeli_values) > 0:
assert set(eeli_values.tolist()) == {-1.0}
Expand Down Expand Up @@ -87,7 +87,7 @@ def test_eeli_values(repeat_n: int): # noqa: ARG001
sample_frequency=sample_frequency,
)
eeli = EELI(breath_detection_kwargs={"minimum_duration": 0})
eeli_values = eeli.compute_parameter(cd)
eeli_values = eeli.compute_parameter(cd).values

assert len(eeli_values) == expected_n_breaths
assert np.array_equal(eeli_values, valley_values[1:])
Expand All @@ -98,7 +98,7 @@ def test_with_data(draeger1: Sequence, pytestconfig: pytest.Config):
pytest.skip("Skip with option '--cov' so other tests can cover 100%.")

cd = draeger1.continuous_data["global_impedance_(raw)"]
eeli_values = EELI().compute_parameter(cd)
eeli_values = EELI().compute_parameter(cd).values

breaths = BreathDetection().find_breaths(cd)

Expand Down
28 changes: 2 additions & 26 deletions tests/test_pixel_breath.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,40 +168,16 @@ def none_sequence():

def mock_compute_pixel_parameter(mean: int):
def _mock(*_args, **_kwargs) -> np.ndarray:
if mean > 0:
return SparseData(
label="mock_sparse_data",
name="Tidal impedance variation",
unit=None,
category="impedance difference",
time=np.linspace(0, 100),
description="Mock tidal impedance variation",
parameters={},
derived_from=[],
values=np.full(100, 1),
)
if mean < 0:
return SparseData(
label="mock_sparse_data",
name="Tidal impedance variation",
unit=None,
category="impedance difference",
time=np.linspace(0, 100),
description="Mock tidal impedance variation",
parameters={},
derived_from=[],
values=np.full(100, -1),
)
return SparseData(
label="mock_sparse_data",
name="Tidal impedance variation",
unit=None,
category="impedance difference",
time=np.linspace(0, 100),
time=np.linspace(0, 100, 100),
description="Mock tidal impedance variation",
parameters={},
derived_from=[],
values=np.full(100, 0),
values=np.full(100, np.sign(mean)),
)

return _mock
Expand Down
12 changes: 8 additions & 4 deletions tests/test_sparsedata.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,31 @@ def sparsedata_novalues():
def sparsedata_valueslist():
"""SparseData object with random values as list at random time points."""
n = random.randint(50, 150)
time = np.array(sorted({random.randint(0, 1000) for _ in range(n)}))
values = [random.random() for _ in range(len(time))]
return SparseData(
label="sparsedata_valueslist",
name="SparseData with values as list",
unit=None,
category="dummy",
time=np.array(sorted({random.randint(0, 1000) for _ in range(n)})),
values=[random.random() for _ in range(n)],
time=time,
values=values,
)


@pytest.fixture
def sparsedata_valuesarray():
"""SparseData object with random values as array at random time points."""
n = random.randint(50, 150)
time = np.array(sorted({random.randint(0, 1000) for _ in range(n)}))
values = np.array([random.random() for _ in range(len(time))])
return SparseData(
label="sparsedata_valuesarray",
name="SparseData with values as array",
unit=None,
category="dummy",
time=np.array(sorted({random.randint(0, 1000) for _ in range(n)})),
values=np.array([random.random() for _ in range(n)]),
time=time,
values=values,
)


Expand Down

0 comments on commit 5f543e1

Please sign in to comment.