• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

EIT-ALIVE / eitprocessing / 12400367746

18 Dec 2024 07:58PM UTC coverage: 81.598% (+0.05%) from 81.545%
12400367746

push

github

psomhorst
Bump version: 1.4.8 → 1.5.0

347 of 470 branches covered (73.83%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

7 existing lines in 3 files now uncovered.

1369 of 1633 relevant lines covered (83.83%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.44
/eitprocessing/datahandling/loading/draeger.py
1
from __future__ import annotations
1✔
2

3
import mmap
1✔
4
import sys
1✔
5
import warnings
1✔
6
from functools import partial
1✔
7
from typing import TYPE_CHECKING, NamedTuple
1✔
8

9
import numpy as np
1✔
10

11
from eitprocessing.datahandling.continuousdata import ContinuousData
1✔
12
from eitprocessing.datahandling.datacollection import DataCollection
1✔
13
from eitprocessing.datahandling.eitdata import EITData, Vendor
1✔
14
from eitprocessing.datahandling.event import Event
1✔
15
from eitprocessing.datahandling.intervaldata import IntervalData
1✔
16
from eitprocessing.datahandling.loading import load_eit_data
1✔
17
from eitprocessing.datahandling.loading.binreader import BinReader
1✔
18
from eitprocessing.datahandling.sparsedata import SparseData
1✔
19

20
if TYPE_CHECKING:
21
    from pathlib import Path
22

23
    from numpy.typing import NDArray
24

25
_FRAME_SIZE_BYTES = 4358
1✔
26
load_draeger_data = partial(load_eit_data, vendor=Vendor.DRAEGER)
1✔
27

28

29
def load_from_single_path(
1✔
30
    path: Path,
31
    sample_frequency: float | None = None,
32
    first_frame: int = 0,
33
    max_frames: int | None = None,
34
) -> dict[str, DataCollection]:
35
    """Load Dräger EIT data from path."""
36
    file_size = path.stat().st_size
1✔
37
    if file_size % _FRAME_SIZE_BYTES:
1✔
38
        msg = (
1✔
39
            f"File size {file_size} of file {path!s} not divisible by {_FRAME_SIZE_BYTES}.\n"
40
            "Currently this package does not support loading files containing "
41
            "esophageal pressure or other non-standard data. "
42
            "Make sure this is a valid and uncorrupted Dräger data file."
43
        )
44
        raise OSError(msg)
1✔
45
    total_frames = file_size // _FRAME_SIZE_BYTES
1✔
46

47
    if (f0 := first_frame) > (fn := total_frames):
1✔
48
        msg = f"Invalid input: `first_frame` ({f0}) is larger than the total number of frames in the file ({fn})."
1✔
49
        raise ValueError(msg)
1✔
50

51
    n_frames = min(total_frames - first_frame, max_frames or sys.maxsize)
1✔
52

53
    if max_frames and max_frames != n_frames:
1✔
54
        msg = (
1✔
55
            f"The number of frames requested ({max_frames}) is larger "
56
            f"than the available number ({n_frames}) of frames after "
57
            f"the first frame selected ({first_frame}, total frames: "
58
            f"{total_frames}).\n {n_frames} frames will be loaded."
59
        )
60
        warnings.warn(msg)
1✔
61

62
    # We need to load 1 frame before first actual frame to check if there is an event marker. Data for the pre-first
63
    # (dummy) frame will be removed from self at the end of this function.
64
    load_dummy_frame = first_frame > 0
1✔
65
    first_frame_to_load = first_frame - 1 if load_dummy_frame else 0
1✔
66

67
    pixel_impedance = np.zeros((n_frames, 32, 32))
1✔
68
    time = np.zeros((n_frames,))
1✔
69
    events: list[tuple[float, Event]] = []
1✔
70
    phases: list[tuple[float, int]] = []
1✔
71
    medibus_data = np.zeros((52, n_frames))
1✔
72

73
    with path.open("br") as fo, mmap.mmap(fo.fileno(), length=0, access=mmap.ACCESS_READ) as fh:
1✔
74
        fh.seek(first_frame_to_load * _FRAME_SIZE_BYTES)
1✔
75
        reader = BinReader(fh)
1✔
76
        previous_marker = None
1✔
77

78
        first_index = -1 if load_dummy_frame else 0
1✔
79
        for index in range(first_index, n_frames):
1✔
80
            previous_marker = _read_frame(
1✔
81
                reader,
82
                index,
83
                time,
84
                pixel_impedance,
85
                medibus_data,
86
                events,
87
                phases,
88
                previous_marker,
89
            )
90

91
    estimated_sample_frequency = round((len(time) - 1) / (time[-1] - time[0]), 4)
1✔
92

93
    if not sample_frequency:
1✔
94
        sample_frequency = estimated_sample_frequency
1✔
95

96
    elif sample_frequency != estimated_sample_frequency:
1✔
97
        msg = (
1✔
98
            f"Provided sample frequency ({sample_frequency}) does not match "
99
            f"the estimated sample frequency ({estimated_sample_frequency})."
100
        )
101
        warnings.warn(msg, RuntimeWarning)
1✔
102

103
    # time wraps around the number of seconds in a day
104
    time = np.unwrap(time, period=24 * 60 * 60)
1✔
105

106
    eit_data = EITData(
1✔
107
        vendor=Vendor.DRAEGER,
108
        path=path,
109
        sample_frequency=sample_frequency,
110
        nframes=n_frames,
111
        time=time,
112
        label="raw",
113
        pixel_impedance=pixel_impedance,
114
    )
115
    eitdata_collection = DataCollection(EITData, raw=eit_data)
1✔
116

117
    (
1✔
118
        continuousdata_collection,
119
        sparsedata_collection,
120
    ) = _convert_medibus_data(medibus_data, time, sample_frequency)
121
    intervaldata_collection = DataCollection(IntervalData)
1✔
122
    # TODO: move some medibus data to sparse / interval
123
    # TODO: move phases and events to sparse / interval
124

125
    continuousdata_collection.add(
1✔
126
        ContinuousData(
127
            label="global_impedance_(raw)",
128
            name="Global impedance (raw)",
129
            unit="a.u.",
130
            category="impedance",
131
            derived_from=[eit_data],
132
            time=eit_data.time,
133
            values=eit_data.calculate_global_impedance(),
134
            sample_frequency=sample_frequency,
135
        ),
136
    )
137
    sparsedata_collection.add(
1✔
138
        SparseData(
139
            label="minvalues_(draeger)",
140
            name="Minimum values detected by Draeger device.",
141
            unit=None,
142
            category="minvalue",
143
            derived_from=[eit_data],
144
            time=np.array([t for t, d in phases if d == -1]),
145
        ),
146
    )
147
    sparsedata_collection.add(
1✔
148
        SparseData(
149
            label="maxvalues_(draeger)",
150
            name="Maximum values detected by Draeger device.",
151
            unit=None,
152
            category="maxvalue",
153
            derived_from=[eit_data],
154
            time=np.array([t for t, d in phases if d == 1]),
155
        ),
156
    )
157
    if len(events):
1✔
158
        time_, events_ = zip(*events, strict=True)
1✔
159
        time = np.array(time_)
1✔
160
        events = list(events_)
1✔
161
    else:
162
        time, events = np.array([]), []
1✔
163
    sparsedata_collection.add(
1✔
164
        SparseData(
165
            label="events_(draeger)",
166
            name="Events loaded from Draeger data",
167
            unit=None,
168
            category="event",
169
            derived_from=[eit_data],
170
            time=time,
171
            values=events,
172
        ),
173
    )
174

175
    return {
1✔
176
        "eitdata_collection": eitdata_collection,
177
        "continuousdata_collection": continuousdata_collection,
178
        "sparsedata_collection": sparsedata_collection,
179
        "intervaldata_collection": intervaldata_collection,
180
    }
181

182

183
def _convert_medibus_data(
1✔
184
    medibus_data: NDArray,
185
    time: NDArray,
186
    sample_frequency: float,
187
) -> tuple[DataCollection, DataCollection]:
188
    continuousdata_collection = DataCollection(ContinuousData)
1✔
189
    sparsedata_collection = DataCollection(SparseData)
1✔
190

191
    for field_info, data in zip(_medibus_fields, medibus_data, strict=True):
1✔
192
        if field_info.continuous:
1✔
193
            continuous_data = ContinuousData(
1✔
194
                label=field_info.signal_name,
195
                name=field_info.signal_name,
196
                description=f"Continuous {field_info.signal_name} data loaded from file",
197
                unit=field_info.unit,
198
                time=time,
199
                values=data,
200
                category=field_info.signal_name,
201
                sample_frequency=sample_frequency,
202
            )
203
            continuous_data.lock()
1✔
204
            continuousdata_collection.add(continuous_data)
1✔
205

206
        else:
207
            # TODO parse sparse data
208
            ...
1✔
209

210
    return continuousdata_collection, sparsedata_collection
1✔
211

212

213
def _read_frame(
1✔
214
    reader: BinReader,
215
    index: int,
216
    time: NDArray,
217
    pixel_impedance: NDArray,
218
    medibus_data: NDArray,
219
    events: list,
220
    phases: list,
221
    previous_marker: int | None,
222
) -> int:
223
    """Read frame by frame data from DRAEGER files.
224

225
    This method adds the loaded data to the provided arrays `time` and
226
    `pixel_impedance` and the provided lists `events` and `phases` when the
227
    index is non-negative. When the index is negative, no data is saved. In
228
    any case, the event marker is returned.
229
    """
230
    frame_time = round(reader.float64() * 24 * 60 * 60, 3)
1✔
231
    _ = reader.float32()
1✔
232
    frame_pixel_impedance = reader.npfloat32(length=1024)
1✔
233
    frame_pixel_impedance = np.reshape(frame_pixel_impedance, (32, 32), "C")
1✔
234
    min_max_flag = reader.int32()
1✔
235
    event_marker = reader.int32()
1✔
236
    event_text = reader.string(length=30)
1✔
237
    timing_error = reader.int32()
1✔
238

239
    frame_medibus_data = reader.npfloat32(length=52)
1✔
240

241
    if index < 0:
1✔
242
        # do not keep any loaded data, just return the event marker
243
        return event_marker
1✔
244

245
    time[index] = frame_time
1✔
246
    pixel_impedance[index, :, :] = frame_pixel_impedance
1✔
247
    medibus_data[:, index] = frame_medibus_data
1✔
248

249
    # The event marker stays the same until the next event occurs.
250
    # Therefore, check whether the event marker has changed with
251
    # respect to the most recent event. If so, create a new event.
252
    if ((previous_marker is not None) and (event_marker > previous_marker)) or (index == 0 and event_text):
1✔
253
        events.append((frame_time, Event(event_marker, event_text)))
1✔
254
    if timing_error:
1!
UNCOV
255
        warnings.warn("A timing error was encountered during loading.")
×
256
        # TODO: expand on what timing errors are in some documentation.
257
    if min_max_flag in (1, -1):
1✔
258
        phases.append((frame_time, min_max_flag))
1✔
259

260
    return event_marker
1✔
261

262

263
class _MedibusField(NamedTuple):
1✔
264
    signal_name: str
1✔
265
    unit: str
1✔
266
    continuous: bool
1✔
267

268

269
_medibus_fields = [
1✔
270
    _MedibusField("airway pressure", "mbar", True),
271
    _MedibusField("flow", "L/min", True),
272
    _MedibusField("volume", "mL", True),
273
    _MedibusField("CO2 (%)", "%", True),
274
    _MedibusField("CO2 (kPa)", "kPa", True),
275
    _MedibusField("CO2 (mmHg)", "mmHg", True),
276
    _MedibusField("dynamic compliance", "mL/mbar", False),
277
    _MedibusField("resistance", "mbar/L/s", False),
278
    _MedibusField("r^2", "", False),
279
    _MedibusField("spontaneous inspiratory time", "s", False),
280
    _MedibusField("minimal pressure", "mbar", False),
281
    _MedibusField("P0.1", "mbar", False),
282
    _MedibusField("mean pressure", "mbar", False),
283
    _MedibusField("plateau pressure", "mbar", False),
284
    _MedibusField("PEEP", "mbar", False),
285
    _MedibusField("intrinsic PEEP", "mbar", False),
286
    _MedibusField("mandatory respiratory rate", "/min", False),
287
    _MedibusField("mandatory minute volume", "L/min", False),
288
    _MedibusField("peak inspiratory pressure", "mbar", False),
289
    _MedibusField("mandatory tidal volume", "L", False),
290
    _MedibusField("spontaneous tidal volume", "L", False),
291
    _MedibusField("trapped volume", "mL", False),
292
    _MedibusField("mandatory expiratory tidal volume", "mL", False),
293
    _MedibusField("spontaneous expiratory tidal volume", "mL", False),
294
    _MedibusField("mandatory inspiratory tidal volume", "mL", False),
295
    _MedibusField("tidal volume", "mL", False),
296
    _MedibusField("spontaneous inspiratory tidal volume", "mL", False),
297
    _MedibusField("negative inspiratory force", "mbar", False),
298
    _MedibusField("leak minute volume", "L/min", False),
299
    _MedibusField("leak percentage", "%", False),
300
    _MedibusField("spontaneous respiratory rate", "/min", False),
301
    _MedibusField("percentage of spontaneous minute volume", "%", False),
302
    _MedibusField("spontaneous minute volume", "L/min", False),
303
    _MedibusField("minute volume", "L/min", False),
304
    _MedibusField("airway temperature", "degrees C", False),
305
    _MedibusField("rapid shallow breating index", "1/min/L", False),
306
    _MedibusField("respiratory rate", "/min", False),
307
    _MedibusField("inspiratory:expiratory ratio", "", False),
308
    _MedibusField("CO2 flow", "mL/min", False),
309
    _MedibusField("dead space volume", "mL", False),
310
    _MedibusField("percentage dead space of expiratory tidal volume", "%", False),
311
    _MedibusField("end-tidal CO2", "%", False),
312
    _MedibusField("end-tidal CO2", "kPa", False),
313
    _MedibusField("end-tidal CO2", "mmHg", False),
314
    _MedibusField("fraction inspired O2", "%", False),
315
    _MedibusField("spontaneous inspiratory:expiratory ratio", "", False),
316
    _MedibusField("elastance", "mbar/L", False),
317
    _MedibusField("time constant", "s", False),
318
    _MedibusField(
319
        "ratio between upper 20% pressure range and total dynamic compliance",
320
        "",
321
        False,
322
    ),
323
    _MedibusField("end-inspiratory pressure", "mbar", False),
324
    _MedibusField("expiratory tidal volume", "mL", False),
325
    _MedibusField("time at low pressure", "s", False),
326
]
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc