diff --git a/.gitignore b/.gitignore index d0ab8ca..2ebb98e 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ coverage.xml .ipynb_checkpoints/ *.ipynb~ + +/docs/reverse_eng/infivision_rec_format.ipynb + # OS .DS_Store Thumbs.db diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 003e97b..e0fb3e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,8 @@ ruff check src/ tests/ ruff format src/ tests/ ``` -Both checks run automatically on every push via GitHub Actions. +GitHub Actions runs `ruff check` and `ruff format --check` on every pull request +to `dev` or `main`, and on every push to those branches. ## Running tests @@ -43,7 +44,8 @@ pytest tests/ ``` Tests that require real patient files are skipped automatically if the files -are not present (they are gitignored). +are not present (they are gitignored). Use `pytest -rs` to see which tests were +skipped and why. ## Adding a new parser @@ -53,11 +55,11 @@ the testing checklist. ## Pull request process -Please ensure to discuss about your idea with an Github issue before opening a PR. +Please open a GitHub issue to discuss your idea before opening a PR. 1. Fork the repository and create a branch from `dev` 2. Make your changes with tests -3. Ensure `ruff check` and `pytest` pass locally +3. Ensure `ruff check`, `ruff format --check` and `pytest` pass locally 4. Open a PR targeting `dev` (not `main`) 5. Describe what changed and why diff --git a/README.md b/README.md index e239871..51f38a2 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,13 @@ PulmoVista 500. Provides structured data containers for research in mechanical v ## Installation **Users:** + ```bash pip install git+https://github.com/Kernel236/fastEIT.git ``` **Developers:** + ```bash git clone https://github.com/Kernel236/fastEIT.git cd fastEIT @@ -40,7 +42,7 @@ print(data.global_signal[:10]) # first 10 frames global EIT signal data = load_data("patient01.eit") print(data.n_frames, data.fs) # e.g. 11500, 50.0 print(data.measurements.shape) # (11500, 208) — 208 = 16 inj × 13 meas -print(list(data.aux_signals.keys())) # timestamp, I_real, V_diff, medibus, ... +print(list(data.aux_signals.keys())) # timestamp, trans_A, trans_B, I_real, V_diff, medibus, ... # Dräger .asc — continuous frame-by-frame signal export data = load_data("patient01.asc") @@ -61,7 +63,7 @@ Pre-alpha. Data model and parsing layer implemented. | Class | Content | Produced by | |-------|---------|-------------| -| `ReconstructedFrameData` | 32×32 pixel matrices + synchronized signals | `.bin`, `.txt` | +| `ReconstructedFrameData` | 32×32 pixel matrices + synchronized signals | Dräger `.bin`, Timpel `.csv`/`.txt`/`.asc` | | `ContinuousSignalData` | Signal table, one row per frame | `.asc` | | `RawImpedanceData` | Calibrated transimpedances + aux signals for pyEIT | `.eit` | @@ -72,14 +74,17 @@ Pre-alpha. Data model and parsing layer implemented. | `DragerBinParser` | Implemented | `.bin` — base frame (4358 b) and PressurePod frame (4382 b); registry-driven, new frame sizes require only a dtype + one entry | | `DragerAscParser` | Implemented | `.asc`, `.txt`, `.csv` — continuous waveform export | | `DragerEitParser` | Implemented | `.eit` — ASCII header + binary frames (5495 b/frame); 208 calibrated transimpedances + Medibus aux signals | -| `TimpelTabularParser` | Implemented | `.csv`, `.txt` — reconstructed frame export | +| `TimpelTabularParser` | Implemented | `.csv`, `.txt`, `.asc` — reconstructed frame export | All parsers are accessible via the single entry point `load_data(path)`, which auto-detects vendor and format. -`RawImpedanceData` from `.eit` files can be reconstructed to 32×32 pixel images via -which is an example of pyEIT wrapping `reconstruct_greit()` (optional dependency: `pip install fasteit[pyeit]`; implements -GREIT — Adler et al., *Physiol. Meas.* 2009, DOI: 10.1088/0967-3334/30/6/S03): +`RawImpedanceData` from `.eit` files can be passed to pyEIT through +`reconstruct_greit()` (optional dependency: `pip install fasteit[pyeit]`; implements +GREIT — Adler et al., *Physiol. Meas.* 2009, DOI: 10.1088/0967-3334/30/6/S03). +This function is far from reliable: it was written only to show one possible +application of the `.eit` parser. It uses a generic circular mesh, so the images do +not match the Dräger reconstruction stored in `.bin` files. ```python from fasteit.parsers.draeger.eit.eit_pyeit_bridge import reconstruct_greit diff --git a/docs/data_model.md b/docs/data_model.md index cbab0a3..ea1697d 100644 --- a/docs/data_model.md +++ b/docs/data_model.md @@ -29,16 +29,17 @@ duration float n_frames / fs (0.0 if fs is None) ``` frames np.ndarray Structured array shape (N,). Named fields: - ts float64 timestamp (seconds) + ts float64 timestamp (unit depends on vendor, see below) pixels float32 32×32 image per frame Dräger .bin only: min_max_flag int32 +1=insp peak, -1=exp trough, 0=none event_marker int32 event counter - event_text S30 null-padded ASCII event string + event_text S30 ASCII event string, space- or null-padded timing_error int32 0 = ok aux_signals dict|None Named arrays, each shape (N,), frame-aligned with frames. - Dräger .bin: 52 or 58 Medibus channels. + Dräger .bin: 52 or 58 Medibus channels, sentinel values + left unchanged (replaced in preprocessing). Timpel: 6 device channels (airway_pressure, flow, volume, min_flag, max_flag, qrs_flag — cols 1024-1029). None if the source file carries no auxiliary signals. @@ -64,7 +65,7 @@ is applied in the preprocessing layer. | Vendor | `ts` field | Unit | |--------|-----------|------| -| Dräger `.bin` | wall-clock fraction-of-day from device | seconds (converted from float64 day fraction) | +| Dräger `.bin` | wall-clock time from device, stored as-is | fraction of day (× 86400 for seconds) | | Timpel `.csv` | synthetic: `(frame_index + first_frame) / 50.0` | seconds from start | --- @@ -89,7 +90,7 @@ table pd.DataFrame One row per EIT frame. Snake_case column names. ## 4. `RawImpedanceData` **File:** `models/raw_impedance_data.py` -**Produced by:** `DragerEitParser` (`.eit`); `TimpelRawParser` (`.x`) — scaffold +**Produced by:** `DragerEitParser` (`.eit`) ``` measurements np.ndarray shape (N_frames, 208) diff --git a/docs/parsers.md b/docs/parsers.md index edab33f..df824e2 100644 --- a/docs/parsers.md +++ b/docs/parsers.md @@ -16,7 +16,6 @@ For **architecture and extension recipes** see [`parsing_layer.md`](parsing_laye | `.asc` / `.txt` / `.csv` | Dräger | `DragerAscParser` | `ContinuousSignalData` | | `.eit` | Dräger | `DragerEitParser` | `RawImpedanceData` | | `.csv` / `.txt` / `.asc` | Timpel | `TimpelTabularParser` | `ReconstructedFrameData` | -| `.x` | Timpel | `TimpelRawParser` *(scaffold)* | `RawImpedanceData` | All parsers inherit from `BaseParser` and are accessible via `load_data(path)`. @@ -34,35 +33,46 @@ Two frame formats are registered; detection is size-based (no header). | Name | Frame size | Medibus channels | Notes | |------|-----------|-----------------|-------| | `Draeger_base_4358` | 4358 bytes | 52 | Standard export | -| `Draeger_ext_4382` | 4382 bytes | 58 | PressurePod: adds esophageal + transpulmonary pressure | +| `Draeger_ext_4382` | 4382 bytes | 58 | PressurePod variant: BiLevel parameters + 4 Pod pressure waveforms | ### Binary frame layout (base format) | Field | Type | Size | Content | |-------|------|------|---------| -| `ts` | float64 | 8 B | Wall-clock timestamp (fraction of day × 86400 → seconds) | +| `ts` | float64 | 8 B | Wall-clock timestamp as fraction of day (kept as-is; × 86400 gives seconds) | | `dummy` | float32 | 4 B | Unknown — ignored | | `pixels` | float32 × 1024 | 4096 B | Reconstructed 32×32 image (C-order) | | `min_max_flag` | int32 | 4 B | +1 = inspiratory peak, −1 = expiratory trough, 0 = none | | `event_marker` | int32 | 4 B | Event counter | -| `event_text` | S30 | 30 B | ASCII event label, space-padded | +| `event_text` | S30 | 30 B | ASCII event label, padded with spaces (or nulls on frames without an event) | | `timing_error` | int32 | 4 B | 0 = frame timing OK | | `medibus_data` | float32 × 52 | 208 B | Ventilator Medibus channels; sentinel −3.4×10³⁸ = disconnected | -PressurePod format (`Draeger_ext_4382`) adds 6 extra Medibus channels (indices 52–57): -esophageal pressure (Pes), airway pressure (Paw), transpulmonary pressure (Ptp), -and three auxiliary pressure waveforms from the Pod hardware. +The two formats share Medibus indices 0–50 and differ from index 51 onward: -### Sentinel handling +| Index | Base (52 channels) | PressurePod (58 channels) | +|-------|--------------------|---------------------------| +| 51 | `time_at_low_pressure` | `high_pressure` (BiLevel PHigh) | +| 52 | — | `low_pressure` (BiLevel Plow) | +| 53 | — | `time_at_low_pressure` (BiLevel Tlow) | +| 54–57 | — | Pod waveforms: `airway_pressure_pod`, `esophageal_pressure_pod`, `transpulmonary_pressure_pod`, `gastric_pressure_pod` | -Medibus channels disconnected from the ventilator carry the sentinel value -`−3.4×10³⁸` (IEEE 754 bit pattern `0xFF7FC99E`). The parser replaces these -with `NaN` so downstream code can use `np.isnan()` to detect disconnected channels. +Full field list with units: `parsers/draeger/bin/draeger_dtypes.py`. + +### Sentinel values + +Disconnected Medibus channels carry the sentinel `−3.4×10³⁸` (IEEE 754 bit +pattern `0xFF7FC99E`); BiLevel fields (51–53 in the PressurePod format) read +`−1000.0` when the ventilator is not in BiLevel mode. The parser returns these +values unchanged. Replacing them with `NaN` is a preprocessing step; +`bin_utils.replace_no_data_sentinels()` is available for it. ### Sampling frequency -Not stored in the binary. Estimated from consecutive timestamp differences: -`fs = 1 / median(diff(ts))`, rounded to the nearest integer Hz. +Not stored in the binary. Estimated from the timestamps: +`fs = (N − 1) / (t_last − t_first)`, with timestamps converted to seconds and +unwrapped across midnight, rounded to the nearest integer Hz. Falls back to +50 Hz (recorded in `metadata["warnings"]`) when the estimate fails. --- @@ -71,7 +81,7 @@ Not stored in the binary. Estimated from consecutive timestamp differences: **File:** `parsers/draeger/eit/eit_parser.py` Reads raw transimpedance measurements from the PulmoVista 500 proprietary -binary format. This is the **only open-source Python parser for this format**. +binary format. We are not aware of another open-source Python parser for it. ### File structure @@ -155,8 +165,9 @@ measurement (step = 1 electrode). Reads the PulmoVista 500 continuous waveform export (tab-separated text). Returns `ContinuousSignalData` with one row per EIT frame. -Detection: looks for `"Dräger"` or `"Draeger"` keyword in the first 40 lines. -Sampling frequency estimated from the `time` column if present. +Detection: looks for the `DraegerEIT Software` header string in the first 40 lines +(read as latin-1). Sampling frequency is estimated from the `time` column +(fraction of day) when present, otherwise `fs` is `None`. Typical columns: `image`, `time`, `global`, `local_1_x_16_y_29` … `local_4_x_16_y_05`, `minmax`, `event`, `eventtext`, `timing_error`, plus Medibus channel columns when @@ -172,16 +183,19 @@ Reads Timpel EIT device tabular exports. Each row is one EIT frame; columns 0–1023 are the 32×32 pixel values (C-order); columns 1024–1029 are auxiliary signals (airway_pressure, flow, volume, min_flag, max_flag, qrs_flag). -Returns `ReconstructedFrameData`. Sampling frequency defaults to 50 Hz if a -`first_frame` column is present but timing cannot be derived otherwise. +Returns `ReconstructedFrameData`. The export has no timestamp column, so `fs` +is fixed at 50 Hz and `ts` is generated as `(frame_index + first_frame) / 50`. -Detection: looks for `"Timpel"` keyword in the first 40 lines. +Detection: the keyword `timpel` in the first 40 lines, or a first data row with +at least 1029 separators (comma, tab or semicolon), i.e. the 1030-column layout. --- ## 6. GREIT reconstruction bridge -This function is far to be reliable. It's just an example how to bridge on recustrustion library such as pyEIT stasting from fastEIT parsing layer output. +This function is far from reliable. It was written only to show one possible +application of the `.eit` parser output: passing it to a reconstruction library +such as pyEIT. **File:** `parsers/draeger/eit/eit_pyeit_bridge.py` **Optional dependency:** `pip install fasteit[pyeit]` @@ -232,7 +246,8 @@ sourced from PulmoVista 500 IFU SW 1.3n: proprietary reconstruction. Both algorithms produce correct relative distributions, but the vendor `.bin` uses a device-specific mesh and matrix. A data-driven reconstruction that learns the Dräger mapping from paired `.eit`/`.bin` recordings -is provided in `fasteit.reconstruction` (see `feat/ml-reconstruction`). +is being developed on the `feat/ml-reconstruction` branch and is not part of this +release. --- @@ -243,7 +258,10 @@ is provided in `fasteit.reconstruction` (see `feat/ml-reconstruction`). ### `load_data(path) → BaseData` Single entry point. Auto-detects vendor and format, selects the right parser, -returns the appropriate container. Raises `ValueError` on unknown format. +returns the appropriate container. Raises `ValueError` for an unsupported +extension or unrecognised content, `NotImplementedError` when no parser is +registered for the detected vendor/extension, and a `ParserError` subclass for +`.bin` files with an unknown or ambiguous frame size. ```python from fasteit.parsers.loader import load_data @@ -266,7 +284,7 @@ recordings = load_many(["patient01.bin", "patient02.bin", "patient01.asc"]) ### `load_folder(folder, pattern="*") → list[BaseData]` Scans a directory, parses every file with a registered parser. -Files that raise any error are silently skipped (warning to stderr). +Files that raise an error are skipped and a one-line message is printed to stderr. ```python from fasteit.parsers.loader import load_folder diff --git a/docs/parsing_layer.md b/docs/parsing_layer.md index e7ab498..e612535 100644 --- a/docs/parsing_layer.md +++ b/docs/parsing_layer.md @@ -4,7 +4,7 @@ This document explains the architecture of the parsing layer and provides step-by-step recipes for the most common extension tasks: 1. [How the parsing layer works](#1-how-the-parsing-layer-works) -2. [Add a new Dräger `.bin` frame size](#2-add-a-new-drger-bin-frame-size) +2. [Add a new Dräger `.bin` frame size](#2-add-a-new-dräger-bin-frame-size) 3. [Add a new parser](#3-add-a-new-parser) 4. [Testing checklist](#4-testing-checklist) 5. [File layout](#5-file-layout) @@ -43,7 +43,7 @@ The strategy depends on the extension: |-----------|----------|--------------| | `.bin` | size-based | `file_size % frame_size == 0` for each registered `FormatSpec` | | `.eit` | header-based | reads first bytes, looks for ASCII magic string for each registered `HeaderFormatSpec` | -| `.asc` `.txt` `.csv` | content-based | reads first 40 lines, looks for vendor keyword | +| `.asc` `.txt` `.csv` | content-based | reads first 40 lines, looks for a vendor keyword; Timpel is also recognised by its 1030-column row | `.bin` is special: there is no header. The only way to identify the format is that the file size must be an exact multiple of the frame size in bytes. The @@ -130,9 +130,9 @@ recordings = load_many([ ``` **`load_folder(folder, pattern="*")`** — scans a directory (alphabetically), -parses every file that has a registered parser, and **silently skips** files -that raise any error (no registered parser, malformed file, wrong extension). -Skipped files print a one-line warning to `stderr` but never abort the scan. +parses every file that has a registered parser, and **skips** files that raise +an error (no registered parser, malformed file, wrong extension). Each skipped +file prints a one-line message to `stderr`; the scan is never aborted. ```python from fasteit.parsers.loader import load_folder @@ -242,7 +242,8 @@ differs by format type. | Format | Return type | Example | |--------|-------------|---------| | Binary frame sequence | `ReconstructedFrameData` | Dräger `.bin`, AcmEIT `.acm` | -| Tabular (CSV / TXT) | `ContinuousSignalData` | Dräger `.asc`, Hamilton export, SERVO-U export, capnograph | +| Tabular (CSV / TXT), signals only | `ContinuousSignalData` | Dräger `.asc`, Hamilton export, SERVO-U export, capnograph | +| Tabular with pixel columns | `ReconstructedFrameData` | Timpel `.csv` | Tabular parsers are common for any device that exports to a plain-text table — including standalone ventilators (Hamilton, Maquet SERVO, Dräger Evita) that @@ -316,7 +317,7 @@ Then add the `HeaderFormatSpec` to `HEADER_FORMAT_SPECS` as above. **Binary without a header (size-based, `.bin`)** -Add a `FormatSpec` to `BIN_FORMAT_SPECS` (see [section 2](#2-add-a-new-drger-bin-frame-size)). +Add a `FormatSpec` to `BIN_FORMAT_SPECS` (see [section 2](#2-add-a-new-dräger-bin-frame-size)). No changes to `detection.py` needed. --- @@ -394,7 +395,8 @@ class AcmEitParser(BaseParser): return True def parse(self, path: Path) -> ReconstructedFrameData: - # np.memmap with ACMEIT_FRAME_DTYPE, sentinel → NaN, fs estimation ... + # np.memmap with ACMEIT_FRAME_DTYPE, fs estimation ... + # (keep sentinel values as-is: NaN replacement belongs to preprocessing) return ReconstructedFrameData( frames=frames, aux_signals=aux_signals, @@ -534,8 +536,8 @@ src/fasteit/ ├── base.py BaseParser (validate / parse / parse_safe) ├── bin_formats.py BIN_FORMAT_SPECS — all known .bin frame sizes ├── detection.py detect_vendor_and_format() — routes any file - ├── errors.py AmbiguousFormatError, UnknownFormatError - ├── header_formats.py EIT_HEADER_FORMATS — known .eit ASCII headers + ├── errors.py ParserError and subclasses (UnsupportedFrameSize, AmbiguousFormat, InvalidSlice) + ├── header_formats.py HEADER_FORMAT_SPECS — known .eit ASCII headers ├── loader.py load_data / load_many / load_folder │ ├── draeger/ @@ -543,16 +545,16 @@ src/fasteit/ │ │ └── asc_parser.py DragerAscParser — .asc continuous export │ ├── bin/ │ │ ├── bin_parser.py DragerBinParser — .bin reconstructed frames - │ │ ├── bin_utils.py Sentinel → NaN, fs estimation helpers + │ │ ├── bin_utils.py Frame slicing, fs estimation, sentinel helpers │ │ └── draeger_dtypes.py FRAME_BASE_DTYPE, FRAME_EXT_DTYPE, MEDIBUS_* fields │ └── eit/ │ ├── eit_parser.py DragerEitParser — .eit raw transimpedances - │ ├── eit_dtypes.py EIT_FRAME_DTYPE (5495-byte frame layout) - │ ├── eit_utils.py Calibration constants FT_A/FT_B/FC_CURRENT/FV_VOLTAGE + │ ├── eit_dtypes.py FRAME_EIT_DTYPE (5495-byte frame layout) + │ ├── eit_utils.py Header parsing, calibration constants FT_A/FT_B/FC_CURRENT/FV_VOLTAGE │ └── eit_pyeit_bridge.py build_greit() / reconstruct_greit() — pyEIT bridge │ └── timpel/ - ├── timpel_parser.py TimpelTabularParser — .csv/.txt reconstructed frames + ├── timpel_parser.py TimpelTabularParser — .csv/.txt/.asc reconstructed frames └── timpel_dtypes.py TIMPEL_AUX_FIELDS — auxiliary signal columns ``` @@ -561,4 +563,3 @@ one `_parser.py`, one `_dtypes.py`, one `__init__.py`. Then follow steps 1–5 in [section 3](#3-add-a-new-parser). --- - diff --git a/docs/reverse_eng/draeger_bin_format.ipynb b/docs/reverse_eng/draeger_bin_format.ipynb index f313509..96ce677 100644 --- a/docs/reverse_eng/draeger_bin_format.ipynb +++ b/docs/reverse_eng/draeger_bin_format.ipynb @@ -8,9 +8,9 @@ "# Reverse Engineering: Draeger PulmoVista500 `.bin` Format\n", "\n", "**Device**: Draeger PulmoVista500 - firmware V1.30 \n", - "**Implemented in**: `src/fasteit/dtypes.py` → `FRAME_EXT_DTYPE`, `MEDIBUS_BASE_FIELDS` / `MEDIBUS_EXT_FIELDS`\n", + "**Implemented in**: `src/fasteit/parsers/draeger/bin/draeger_dtypes.py` → `FRAME_EXT_DTYPE`, `MEDIBUS_BASE_FIELDS` / `MEDIBUS_EXT_FIELDS`\n", "\n", - "**Frame size**: Dräger PulmoVista can writes **4382-byte frames** (`FRAME_EXT_DTYPE`), regardless of PressurePod connection. `FRAME_BASE_DTYPE` (4358 bytes) is documented in eitprocessing repo from EIT-ALIVE (link below) and probably still used from some firmware." + "**Frame size**: Dräger PulmoVista can write **4382-byte frames** (`FRAME_EXT_DTYPE`), regardless of PressurePod connection. `FRAME_BASE_DTYPE` (4358 bytes) is documented in eitprocessing repo from EIT-ALIVE (link below) and probably still used from some firmware." ] }, { @@ -458,7 +458,7 @@ "- bytes 0x000C-0x100B: 1024 float32 pixels — note the zero blocks at row boundaries (outside-chest pixels)\n", "- `9e c9 7f ff` repeating at 0x1036+: sentinel `0xFF7FC99E` = MEDIBUS not connected\n", "- `00 00 7a c4` repeating later: `0xC47A0000` = -1000.0, no breath computed yet / non-BiLevel mode\n", - "- last 24 bytes (6 × float32): PHigh, Plow, Tlow, ~Paw, ~Pes, ~Ptp, ~Pgas (EXT-specific fields)" + "- last 24 bytes (6 × float32, idx 52-57): Plow, Tlow, ~Paw, ~Pes, ~Ptp, ~Pgas (EXT-specific fields); PHigh is idx 51, the 4 bytes just before" ] }, { diff --git a/docs/reverse_eng/eit_format.ipynb b/docs/reverse_eng/eit_format.ipynb index cdbf1ce..e690c5b 100644 --- a/docs/reverse_eng/eit_format.ipynb +++ b/docs/reverse_eng/eit_format.ipynb @@ -14,7 +14,7 @@ "\n", "The frame internal layout and calibration formulas come from studying **EIDORS**\n", "(`eidors_readdata.m`, function `read_draeger_file`, A. Adler et al., GPL-3.0).\n", - "Only the data format description has beenused to understand field offsets and calibration constants.\n", + "Only the data format description has been used to understand field offsets and calibration constants.\n", "All values are independently verified against the real files in this notebook.\n", "The calibration factors `ft = [0.00098242, 0.00019607]` are empirical constants\n", "from EIDORS, not documented in the source." @@ -639,7 +639,7 @@ "id": "md-calibration", "metadata": {}, "source": [ - "## 6. Calibration formula\n", + "## 5. Calibration formula\n", "\n", "### Source and limitations\n", "\n", @@ -715,7 +715,7 @@ "id": "md-medibus", "metadata": {}, "source": [ - "## 7. Medibus (67 float32)\n", + "## 6. Medibus (67 float32)\n", "\n", "The `.eit` frame contains **67 float32 ventilator channels** at offset 5169\n", "(vs 52 or 58 in `.bin` — different channel count, different set).\n", @@ -798,7 +798,7 @@ "id": "md-tail", "metadata": {}, "source": [ - "## 8. Frame tail — event text, frame counter, padding" + "## 7. Frame tail — event text, frame counter, padding" ] }, { @@ -831,7 +831,7 @@ "id": "md-eit-bin-comparison", "metadata": {}, "source": [ - "## 9. `.eit` vs `.bin` — timestamp cross-check\n", + "## 8. `.eit` vs `.bin` — timestamp cross-check\n", "\n", "The `.eit` and `.bin` files for the same patient use the same timestamp encoding.\n", "Frame 0 of both files should have the same (or very close) timestamp." @@ -953,7 +953,7 @@ "\n", "1. **Byte [8:16]** (`unknown`): value ~64 (HealthyLung) vs ~89 (patient01).\n", " Could be `trans_A[0]` (injection-pair self-impedance) or a separate header\n", - " field. Needs verification against EIDORS reconstructed output .\n", + " field. Needs verification against EIDORS reconstructed output.\n", "\n", "2. **`gugus` block [4816:5168]** (44 float64): EIDORS explicitly ignores these.\n", " Physical meaning unknown.\n", diff --git a/notebooks/03_eit_parser_validation.ipynb b/notebooks/03_eit_parser_validation.ipynb index 8a45321..cf7e82f 100644 --- a/notebooks/03_eit_parser_validation.ipynb +++ b/notebooks/03_eit_parser_validation.ipynb @@ -21,11 +21,9 @@ "calibrated with the EIDORS constants, and to show the bridge toward Python EIT reconstruction\n", "libraries such as pyEIT for those who wish to work with raw transimpedance signals.\n", "\n", - "The planned next step is to reproduce with an ML model the deterministic process used by\n", - "Dräger in its internal transformation, using `.eit` frames as input and the corresponding\n", - "`.bin` frames as output. This should teach the model not only the spatial reconstruction\n", - "but also the various filters applied internally. This remains a planned future feature and\n", - "has not yet been implemented." + "A data-driven model that learns the Dräger transformation from `.eit` frames (input) to\n", + "the corresponding `.bin` frames (output) is being explored on the separate\n", + "`feat/ml-reconstruction` branch. It is not part of the parser release." ] }, { @@ -376,18 +374,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 9. Frame alignment: .eit vs .bin\n", + "## 8. Frame alignment: .eit vs .bin\n", "\n", "The PulmoVista 500 records **only the `.eit` file** (raw transimpedances).\n", "The `.bin` is generated afterward by Dräger's software, which applies its\n", "Newton-Raphson FEM reconstruction to each `.eit` frame in sequence.\n", "\n", - "This means frame *k* in `.bin` is the reconstruction of frame *k* in `.eit`\n", - "**by construction**, a temporal offset is architecturally impossible.\n", - "\n", - "A lag of 0 also validates that the paired dataset for the data-driven\n", - "reconstruction model (ML branch) is perfectly aligned with zero noise from\n", - "frame misalignment." + "Frame *k* in `.bin` is therefore expected to be the reconstruction of frame *k*\n", + "in `.eit`, so the two files should have the same frame count (section 5) and no\n", + "temporal offset. This is what makes `.eit`/`.bin` pairs usable as input/output\n", + "examples for a data-driven reconstruction model." ] }, { @@ -405,7 +401,7 @@ "The spatial quality of GREIT images with a generic circular mesh does not match\n", "the vendor-reconstructed `.bin` (Dräger uses a proprietary algorithm and mesh).\n", "A data-driven reconstruction that learns the Dräger mapping directly from paired\n", - "`.eit`/`.bin` recordings will be implemented soon." + "`.eit`/`.bin` recordings is being explored on the `feat/ml-reconstruction` branch." ] } ], diff --git a/src/fasteit/models/__init__.py b/src/fasteit/models/__init__.py index a688448..080f885 100644 --- a/src/fasteit/models/__init__.py +++ b/src/fasteit/models/__init__.py @@ -2,9 +2,9 @@ Core data models for fasteit. - BaseData — base container for raw parsed data -- ReconstructedFrameData — pixel matrices + synchronized signals (.bin, .txt) +- ReconstructedFrameData — pixel matrices + synchronized signals (Dräger .bin, Timpel .csv/.txt/.asc) - ContinuousSignalData — signal-only tables, no matrices (.asc) -- RawImpedanceData — raw transimpedances for pyEIT (.eit, .x) +- RawImpedanceData — raw transimpedances for pyEIT (.eit) """ from .base_data import BaseData diff --git a/src/fasteit/models/base_data.py b/src/fasteit/models/base_data.py index e74b9a6..3dac254 100644 --- a/src/fasteit/models/base_data.py +++ b/src/fasteit/models/base_data.py @@ -11,7 +11,7 @@ class BaseData: Attributes: filename: Path of the source file. - file_format: Format identifier — "bin", "eit", "asc", "txt", or "x". + file_format: Format identifier — "bin", "eit", "asc", "csv", or "txt". vendor: Detected device vendor — "draeger" or "timpel". fs: Sampling rate in Hz, estimated by parser from timestamps. metadata: Parsed header information (framerate, date, firmware, etc.). diff --git a/src/fasteit/models/raw_impedance_data.py b/src/fasteit/models/raw_impedance_data.py index 1de6103..1d279fe 100644 --- a/src/fasteit/models/raw_impedance_data.py +++ b/src/fasteit/models/raw_impedance_data.py @@ -1,6 +1,6 @@ """RawImpedanceData: container for raw transimpedance measurements. -Produced by: DragerEitParser (.eit), TimpelRawParser (.x). +Produced by: DragerEitParser (.eit). Primary use case: pyEIT interface for image reconstruction. """ diff --git a/src/fasteit/models/reconstructed_data.py b/src/fasteit/models/reconstructed_data.py index cdd063e..f93af6a 100644 --- a/src/fasteit/models/reconstructed_data.py +++ b/src/fasteit/models/reconstructed_data.py @@ -22,7 +22,7 @@ class ReconstructedFrameData(BaseData): ``aux_signals`` is a vendor-agnostic dict of named continuous signals that are frame-aligned with ``frames``. For Dräger .bin this is the Medibus dict; - for Timpel .txt this will hold the 5 device channels. + for Timpel tabular exports it holds the 6 device channels. Attributes: frames: Structured numpy array, shape (N,). Required fields: ``ts``