diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..54c1f3f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,109 @@ +# Continuous Integration workflow +# Runs tests on multiple Python versions and OS + +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + workflow_dispatch: # Allow manual trigger + +jobs: + test: + name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.10', '3.11', '3.12'] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + pip install pytest pytest-cov + + - name: Run tests with coverage + run: | + pytest --cov=labchart_parser --cov-report=xml --cov-report=term-missing + + - name: Upload coverage to Codecov + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' + uses: codecov/codecov-action@v4 + with: + files: ./coverage.xml + fail_ci_if_error: false + verbose: true + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install linting tools + run: | + python -m pip install --upgrade pip + pip install ruff black isort + + - name: Check formatting with Black + run: black --check --diff src/ tests/ + + - name: Check imports with isort + run: isort --check-only --diff --profile black src/ tests/ + + - name: Lint with Ruff + run: ruff check src/ tests/ + + build: + name: Build package + runs-on: ubuntu-latest + needs: [test, lint] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install build tools + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Check package + run: twine check dist/* + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8bf52ce --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,68 @@ +# Pre-commit hooks configuration +# See https://pre-commit.com for more information +# Install: pip install pre-commit && pre-commit install + +repos: + # General hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-added-large-files + args: ['--maxkb=1000'] + - id: check-merge-conflict + - id: detect-private-key + - id: debug-statements + + # Black - Code formatter + - repo: https://github.com/psf/black + rev: 24.3.0 + hooks: + - id: black + language_version: python3 + args: ['--line-length=88'] + + # Ruff - Fast Python linter (replaces flake8, isort, etc.) + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.4 + hooks: + # Run the linter + - id: ruff + args: ['--fix', '--exit-non-zero-on-fix'] + # Run the formatter (optional, can use instead of black) + # - id: ruff-format + + # isort - Import sorting (compatible with black) + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + args: ['--profile=black'] + + # nbstripout - Strip output cells from Jupyter notebooks before commit. + # Notebooks with embedded outputs produce noisy diffs and balloon repo size. + - repo: https://github.com/kynan/nbstripout + rev: 0.7.1 + hooks: + - id: nbstripout + + # MyPy - Static type checking (optional, uncomment if needed) + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: v1.9.0 + # hooks: + # - id: mypy + # additional_dependencies: [numpy, pandas-stubs] + # args: ['--ignore-missing-imports'] + +# Configuration for specific tools +# These can also be in pyproject.toml + +# Ruff configuration (can be moved to pyproject.toml) +# [tool.ruff] +# line-length = 88 +# target-version = "py310" +# select = ["E", "F", "I", "N", "W", "UP"] +# ignore = ["E501"] # Line too long (handled by black) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c269b8c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,54 @@ +# Changelog + +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); +the project follows [SemVer](https://semver.org). + +## [Unreleased] + +## [0.2.0] - 2026-04-29 + +- Per-block metadata exposed under `meta["blocks"]: list[dict]`. Top-level + `meta` mirrors block 0 for back-compat, except `ChannelTitle` (now the + DataFrame columns) and `Range`/`TopValue`/`BottomValue` (per-block only). +- `FileParsingError` raised if `ChannelTitle` differs between blocks. +- Parser rewritten around per-block segmentation + bulk `pd.read_csv`; + ~320k rows/s on a synthetic 500k×5 file. Per-cell-NaN A2 behavior + preserved via `pd.to_numeric(errors="coerce")`. +- `time_abs` stitching uses each block's own `Interval_s`. +- `LabChartFile.blocks` now returns `list[int]` (Python ints, not numpy scalars). +- `matplotlib` imported lazily inside `plot_channel()` to avoid import-time + overhead and backend issues in headless environments. + +## [0.1.2] - 2026-04-28 + +- `plot_channel()` method; matplotlib is a default dependency. +- `__version__` via `importlib.metadata`. +- `from_file()` / `parse_labchart_txt()` accept `os.PathLike`. +- `time_abs` strictly monotonic across block boundaries (advances by one + sample interval). +- A single bad cell in a numeric row becomes `NaN` instead of nullifying + the whole row. +- utf-8 read falls back to latin-1 instead of `errors="ignore"`. +- `UserWarning` when `TimeFormat` isn't `StartOfBlock`. +- Python ≥3.10 (was ≥3.8); CI on Linux/macOS/Windows × 3.10–3.12. +- Custom exceptions: `FileParsingError`, `NoDataError`, `InvalidChannelError`. +- Tests, pre-commit (black/ruff/isort/nbstripout), `[project.urls]`, + centralized tool config in `pyproject.toml`. +- Type annotations modernized (PEP 604/585). +- All error messages translated to English. + +## [0.1.1] - 2024-01-15 + +- `get_block_comments_excluding()`, `slice_time_abs()`. +- Multi-block file support. + +## [0.1.0] - 2024-01-01 + +- Initial release: `LabChartFile`, `parse_labchart_txt()`, block / + channel / comment extraction. + +[Unreleased]: https://github.com/Neures-1158/labchart_txt_parser/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/Neures-1158/labchart_txt_parser/compare/v0.1.2...v0.2.0 +[0.1.2]: https://github.com/Neures-1158/labchart_txt_parser/compare/v0.1.1...v0.1.2 +[0.1.1]: https://github.com/Neures-1158/labchart_txt_parser/compare/v0.1.0...v0.1.1 +[0.1.0]: https://github.com/Neures-1158/labchart_txt_parser/releases/tag/v0.1.0 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e1f3906 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project + +`labchart_parser` parses ADInstruments LabChart `.txt` exports into a pandas DataFrame. Small lab tool, `src/` layout, package +`labchart_parser`. + +## Commands + +```bash +pip install -e ".[dev]" # dev install +pytest # all tests +pytest tests/test_parser.py::TestParserBulk -v # one class / test +ruff check src/ tests/ # lint +black --target-version py310 src/ tests/ # format +isort --profile black src/ tests/ # imports +``` + +CI runs Linux/macOS/Windows × Python 3.10/3.11/3.12. Floor is **Python ≥3.10**; +keep `pyproject.toml`, `README.md`, `CONTRIBUTING.md`, and the CI matrix in sync. + +## Architecture + +Two layers: a stateless functional parser +([src/labchart_parser/parser.py](src/labchart_parser/parser.py)) and a thin +OOP wrapper ([src/labchart_parser/core.py](src/labchart_parser/core.py)). + +### Parser pipeline + +`parse_labchart_txt(path) -> (df, meta)` runs three phases — read the +helper functions, not just `parse_labchart_txt`: + +1. **Read.** UTF-8 first, latin-1 fallback. `errors="ignore"` is **not** + used (silently dropped bytes corrupted French/Spanish exports). +2. **Segment** (`_segment_into_blocks`). Single linear pass produces + `list[{meta, data_lines}]`. Block boundaries: + - Header line (`Interval=`, `ChannelTitle=`, `UnitName=`, `Range=`, + `TopValue=`, `BottomValue=`, `ExcelDateTime=`, `TimeFormat=`, + `DateFormat=`) appears after data lines. + - `Time` resets within a contiguous data run (covers one-header / + multi-block files; metadata is shallow-copied). +3. **Validate** (`_validate_channel_consistency`). `ChannelTitle` must + match across blocks, otherwise `FileParsingError`. +4. **Bulk-parse each block** (`_parse_block_data`). Classification pass + tags lines as full-width numeric / short comment / skip, then loads + the numeric portion via one `pd.read_csv(StringIO(buf), dtype=str, + na_values=["*"])` call. `pd.to_numeric(errors="coerce")` per column + turns unparseable cells into `NaN` without dropping the row. +5. **Stitch.** `block` ids come from segmentation order. `time_abs` + advances by each block's own `Interval_s` at boundaries (median of + diffs as fallback) — strictly monotonic. + +### Metadata shape + +- Top-level `meta`: block 0's `Interval`, `Interval_s`, `TimeFormat`, + `DateFormat`, `ExcelDateTime`, `UnitName`. +- `meta["blocks"]: list[dict]` — full per-block metadata. +- **Dropped** from top level: `ChannelTitle` (becomes columns), + `Range` / `TopValue` / `BottomValue` (vary per block). + +### Errors + +- `FileNotFoundError`: missing path. +- `FileParsingError`: bad extension, empty file, no data section, + <2 columns, ChannelTitle mismatch. +- `NoDataError`: segmentation succeeded, every block empty. +- `InvalidChannelError` (subclass of `KeyError`, raised by `core`): + unknown channel. +- `UserWarning` if `TimeFormat` isn't `StartOfBlock`. + +### Wrapper invariants + +`LabChartFile._data` carries the parsed frame. `channels` excludes the +`_SYSTEM_COLS` set — **add new computed columns to that tuple** or they +leak into the user-visible channel list. `get_block_df` / +`get_channel` / `slice_time_abs` return slices of `_data` **without +resetting the index**. `slice_time_abs` is inclusive on both ends. + +### Test data + +Integration tests use real fixtures in [examples/data/](examples/data/): + +- `labchart_file.example.txt` — canonical multi-block, multi-channel. +- `labchart_file_negTime.txt` — exercises negative-time `time_block`; + do not delete (`test_negative_time_file_parses_correctly`). + +For new behavior, synthesize tab-delimited fixtures via `tmp_path` — +see `TestParserErrorPaths`, `TestParserMetaBlocks`. + +### Versioning + +`__version__` reads from package metadata via +`importlib.metadata.version("labchart_parser")`. Bump the `version` field in `pyproject.toml` only. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..31ec491 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,131 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..03f4fbf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing + +Issues and PRs welcome. + +## Setup + +```bash +git clone https://github.com/Neures-1158/labchart_txt_parser.git +cd labchart_txt_parser +python -m venv venv && source venv/bin/activate +pip install -e ".[dev]" +pre-commit install # optional but recommended +``` + +Requires Python ≥3.10. + +## Workflow + +```bash +pytest # run tests +pytest tests/test_parser.py::TestParserBulk -v # one class +ruff check src/ tests/ # lint +black src/ tests/ && isort --profile black src/ tests/ # format +``` + +CI runs the same checks across Linux/macOS/Windows × Python 3.10–3.12. + +## Pull requests + +- Branch from `main`, keep PRs small. +- Add a test for any behavior change in the parser. Synthesizing tiny + tab-delimited fixtures via `tmp_path` is the standard pattern (see + `tests/test_parser.py::TestParserErrorPaths`). +- Update [CHANGELOG.md](CHANGELOG.md) under `## [Unreleased]` for + user-visible changes. +- Pre-commit must pass; CI must be green. + +By contributing you agree to the [Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/README.md b/README.md index 4a1abe8..402bd95 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,57 @@ # LabChart Parser -Parser for ADInstruments LabChart text exports (`.txt` files). +[![CI](https://github.com/Neures-1158/labchart_txt_parser/actions/workflows/ci.yml/badge.svg)](https://github.com/Neures-1158/labchart_txt_parser/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) -Converts exported LabChart data into a pandas DataFrame with blocks, time, and comments. +Parses ADInstruments LabChart `.txt` exports into a pandas DataFrame with +blocks, continuous time, and comments. -## Export from labchart as .txt +## Export from LabChart -Export from labchart as follows: +LabChart export dialog - LabChart screenshot showing signal export dialog +Set time display to **"Start from Block"** before exporting. -Before exporting in LabChart, make sure time is displayed as "Start from Block" - - -## Installation - -**Prerequisites**: Python ≥3.8, pip ≥21, and git must be installed. - - -### For users - -Install directly from GitHub: +## Install ```bash pip install git+https://github.com/Neures-1158/labchart_txt_parser.git ``` -### For developers +For development: `pip install -e ".[dev]"` (adds pytest, ruff, black, isort). -Clone this repository and install in editable mode: +## Quick start -```bash -git clone https://github.com/Neures-1158/labchart_txt_parser.git -cd labchart_txt_parser -pip install -e . -``` +```python +from labchart_parser import LabChartFile -## Usage +lc = LabChartFile.from_file("data/recording.txt") -See the [examples/example_usage.py](examples/example_usage.py) script and [notebook](examples/labchart_parser_walkthrough.ipynb) for a complete demonstration of: - -- Loading a LabChart text file. -- Accessing metadata, blocks, and channel names. -- Plotting signals - -See https://github.com/Neures-1158/resp_metrics for breath by breath analysis. - - -## Test data -Test data is provided in the repository in the examples/data folder. +lc.metadata # dict; per-block metadata under lc.metadata["blocks"] +lc.channels # ['Flow', 'Pressure', 'Volume', ...] +lc.blocks # [1, 2, 3, ...] +lc.get_block_df(1) # one block as a DataFrame +lc.get_channel(1, "Pressure") +lc.slice_time_abs(10.0, 20.0) +lc.plot_channel("Flow", block=1) +``` +A walkthrough notebook lives at [examples/labchart_parser_walkthrough.ipynb](examples/labchart_parser_walkthrough.ipynb). +For breath-by-breath analysis on top of this parser, see [resp_metrics](https://github.com/Neures-1158/resp_metrics). -## Contributors & Maintainers +## Tests -This project is maintained under the [NEURES](https://github.com/Neures-1158) GitHub organization. +```bash +pytest +``` -Contributions from lab members, collaborators, and the wider community are very welcome. Please feel free to contribute by submitting issues or pull requests on GitHub. +## Maintainer -## License +Maintained under [NEURES](https://github.com/Neures-1158). Lead: Damien +Bachasson, PhD ([GitHub](https://github.com/dambach) · +[ORCID](https://orcid.org/0000-0001-6335-9916) · +[Lab](https://sante.sorbonne-universite.fr/structures-de-recherche/neurophysiologie-respiratoire-experimentale-et-clinique)). +Issues and PRs welcome. -✨ MIT License. See the [LICENSE](LICENSE) file for details. +MIT licensed — see [LICENSE](LICENSE). diff --git a/examples/example_usage.py b/examples/example_usage.py index 37ad68f..a9fcf0f 100644 --- a/examples/example_usage.py +++ b/examples/example_usage.py @@ -1,24 +1,20 @@ """Example usage of the LabChart parser API. -This script demonstrates how to load a LabChart export, access its -metadata, channels, blocks and comments, and how to plot a single -channel from a specific block. The file path used here assumes the -exported text file is located in ``examples/data/labchart_file.example.txt`` -relative to the project root. +Demonstrates loading a LabChart export, inspecting metadata, channels, +blocks, and comments, and plotting a channel with the high-level +``plot_channel`` method. The path used here assumes the exported text +file is at ``examples/data/labchart_file.example.txt`` relative to the +project root. """ -from labchart_parser import LabChartFile import matplotlib.pyplot as plt -import pandas as pd + +from labchart_parser import LabChartFile + def main(): - # Load the exported file lc = LabChartFile.from_file("examples/data/labchart_file.example.txt") - - # lc = LabChartFile.from_file("examples/data/labchart_file_negTime.txt") - - # Display metadata and column preview print("Metadata:", lc.metadata) print("Channels:", lc.channels) print("Number of blocks:", len(lc.blocks)) @@ -28,22 +24,13 @@ def main(): event = lc.get_block_comments_excluding(1, exclude_values=["INSPI", "EXPI"])[0] print("First comment in block 1 excluding INSPI/EXPI:", event) - # Get df from block 1 - df_bloc1 = lc.get_block_df(1) - # Plot pressure (channel "Pressure") for block 1 - plt.figure(figsize=(5, 2)) - plt.grid(True) - plt.plot(df_bloc1["Time"], df_bloc1["Flow"]) - + # Plot a single channel from a single block — replaces the manual + # plt.figure / plt.plot pair from earlier versions. + lc.plot_channel("Flow", block=1, figsize=(8, 3)) + lc.plot_channel("Pressure", block=1, color="tomato", figsize=(8, 3)) - # Get pressure (channel "Pressure") for block 1 - pressure_df = lc.get_channel(1, "Pressure") - plt.figure(figsize=(5, 2)) - plt.plot(pressure_df["Time"], pressure_df["value"]) - plt.grid(True) - plt.tight_layout() plt.show() - - + + if __name__ == "__main__": main() diff --git a/examples/labchart_parser_walkthrough.ipynb b/examples/labchart_parser_walkthrough.ipynb index f656406..f615f5b 100644 --- a/examples/labchart_parser_walkthrough.ipynb +++ b/examples/labchart_parser_walkthrough.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "65ae6e56", + "id": "0", "metadata": {}, "source": [ "# LabChart Parser walkthrough\n", @@ -13,7 +13,7 @@ }, { "cell_type": "markdown", - "id": "3db2a482", + "id": "1", "metadata": {}, "source": [ "## Imports and configuration\n", @@ -22,21 +22,10 @@ }, { "cell_type": "code", - "execution_count": 3, - "id": "47e756bd", + "execution_count": null, + "id": "2", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "PosixPath('examples/data/labchart_file.example.txt')" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from pathlib import Path\n", "import pandas as pd\n", @@ -49,7 +38,7 @@ }, { "cell_type": "markdown", - "id": "ec47a7f0", + "id": "3", "metadata": {}, "source": [ "## Load the export\n", @@ -58,27 +47,10 @@ }, { "cell_type": "code", - "execution_count": 4, - "id": "e083b773", + "execution_count": null, + "id": "4", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Interval 0.01 s\n", - "ExcelDateTime 4.5855649139942347e+04\\t07/17/25 15:34:45.691019\n", - "TimeFormat StartOfBlock\n", - "DateFormat \n", - "Interval_s 0.01\n", - "UnitName [*, , *, cmH2O, *, *, *, *, *]\n", - "dtype: object" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "lc = LabChartFile.from_file(str(data_path))\n", "\n", @@ -88,54 +60,27 @@ }, { "cell_type": "code", - "execution_count": 5, - "id": "83eed192", + "execution_count": null, + "id": "5", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['PushSWE',\n", - " 'Flow',\n", - " 'EMG',\n", - " 'Pressure',\n", - " 'Flow BIS',\n", - " 'Inspi cyclic',\n", - " 'Expi cyclic',\n", - " 'L/m -> L/s',\n", - " 'VolumeResp']" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "lc.channels" ] }, { "cell_type": "code", - "execution_count": 9, - "id": "c0847b18", + "execution_count": null, + "id": "6", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "number of blocks: 6\n" - ] - } - ], + "outputs": [], "source": [ "print(\"number of blocks:\", len(lc.blocks))" ] }, { "cell_type": "markdown", - "id": "04bf72f9", + "id": "7", "metadata": {}, "source": [ "## Comments overview\n", @@ -144,104 +89,17 @@ }, { "cell_type": "code", - "execution_count": 10, - "id": "3a70e4e5", + "execution_count": null, + "id": "8", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Timetime_blocktime_absblockComment
02.431.931.931INSPI
12.692.192.191Dia1
22.912.412.411EXPI
34.744.244.241INSPI
45.224.724.721EXPI
\n", - "
" - ], - "text/plain": [ - " Time time_block time_abs block Comment\n", - "0 2.43 1.93 1.93 1 INSPI\n", - "1 2.69 2.19 2.19 1 Dia1\n", - "2 2.91 2.41 2.41 1 EXPI\n", - "3 4.74 4.24 4.24 1 INSPI\n", - "4 5.22 4.72 4.72 1 EXPI" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "lc.comments.head()" ] }, { "cell_type": "markdown", - "id": "3e1b0c92", + "id": "9", "metadata": {}, "source": [ "### Filter comments\n", @@ -250,21 +108,10 @@ }, { "cell_type": "code", - "execution_count": 13, - "id": "263de602", + "execution_count": null, + "id": "10", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Dia1'" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "exclude_values = ['INSPI', 'EXPI']\n", "lc.get_block_comments_excluding(1, exclude_values=exclude_values)[0]" @@ -272,7 +119,7 @@ }, { "cell_type": "markdown", - "id": "0b182368", + "id": "11", "metadata": {}, "source": [ "## Block data\n", @@ -281,152 +128,10 @@ }, { "cell_type": "code", - "execution_count": 12, - "id": "42250b26", + "execution_count": null, + "id": "12", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Timetime_blocktime_absCommentPushSWEFlowEMGPressureFlow BISInspi cyclicExpi cyclicL/m -> L/sVolumeResp
00.500.000.00None3.202508-42.388330.64765624.43683-42.388331.469056e-081.469056e-08-0.706472-0.003621
10.510.010.01None3.202531-42.376960.10351624.43413-42.376961.469056e-081.469056e-08-0.706283-0.010684
20.520.020.02None3.202563-42.376960.64359424.43413-42.376961.469056e-081.469056e-08-0.706283-0.017747
30.530.030.03None3.202594-42.376960.08390624.43413-42.376961.469056e-081.469056e-08-0.706283-0.024810
40.540.040.04None3.202609-42.376960.61125024.43413-42.376961.469056e-081.469056e-08-0.706283-0.031873
\n", - "
" - ], - "text/plain": [ - " Time time_block time_abs Comment PushSWE Flow EMG Pressure \\\n", - "0 0.50 0.00 0.00 None 3.202508 -42.38833 0.647656 24.43683 \n", - "1 0.51 0.01 0.01 None 3.202531 -42.37696 0.103516 24.43413 \n", - "2 0.52 0.02 0.02 None 3.202563 -42.37696 0.643594 24.43413 \n", - "3 0.53 0.03 0.03 None 3.202594 -42.37696 0.083906 24.43413 \n", - "4 0.54 0.04 0.04 None 3.202609 -42.37696 0.611250 24.43413 \n", - "\n", - " Flow BIS Inspi cyclic Expi cyclic L/m -> L/s VolumeResp \n", - "0 -42.38833 1.469056e-08 1.469056e-08 -0.706472 -0.003621 \n", - "1 -42.37696 1.469056e-08 1.469056e-08 -0.706283 -0.010684 \n", - "2 -42.37696 1.469056e-08 1.469056e-08 -0.706283 -0.017747 \n", - "3 -42.37696 1.469056e-08 1.469056e-08 -0.706283 -0.024810 \n", - "4 -42.37696 1.469056e-08 1.469056e-08 -0.706283 -0.031873 " - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df_block1 = lc.get_block_df(1)\n", "df_block1.head()" @@ -434,74 +139,65 @@ }, { "cell_type": "markdown", - "id": "fccc4542", + "id": "13", "metadata": {}, "source": [ "## Plot a channel\n", - "Visualise the `Flow` channel for block 1 and show the `Pressure` trace separately." + "Visualise signals using the built-in `plot_channel()` method or manually with matplotlib." ] }, { "cell_type": "code", - "execution_count": 19, - "id": "60f85b30", + "execution_count": null, + "id": "14", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAk4AAAEhCAYAAACJEy/nAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjUsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvWftoOwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAoHpJREFUeJztfXmYXEX19tvrdM8+k5ksZCNkJYRAICSEhJ0QZBEJIAgGRBExCIoYAVHADfGHsgRlMygifChGQEAE1ChhCYQlQAiEhED2bSazLz293e+P2/feult3nVud7klPvc+TJ913+vSpW33r1FvnnDrlUxRFgYSEhISEhISERE74i90ACQkJCQkJCYl9BZI4SUhISEhISEhwQhInCQkJCQkJCQlOSOIkISEhISEhIcEJSZwkJCQkJCQkJDghiZOEhISEhISEBCckcZKQkJCQkJCQ4IQkThISEhISEhISnJDESUJCQkJCQkKCE5I4SUgUGQsWLMDEiRNN/6ZPn46LLroIK1eutH12wYIFedV/9913Y+LEiULf8a1vfQvXXXddnlpkxxNPPGHrI/bfDTfcACA/9yIhISGRDcFiN0BCQgKYPHkybrrpJgBAKpVCa2srHnvsMXzta1/DE088gfHjxxe5hc5IpVK45ZZb8K9//QtnnXXWXtf3m9/8Bo2NjbbrgwYN2uu6JSQkJABJnCQk+gUqKytx6KGHmq4dddRRmDVrFp544glce+21xWlYFqxduxY//elP8cEHHyASiRRE54EHHogRI0YURJeEhISEE2SoTkKinyIajaKsrAw+n8/1M319ffjtb3+LU045BQcffDBOPvlkPPDAA0in06bP/eMf/8D8+fNxyCGH4LjjjsNtt92GeDzu+J3bt2/Hcccdh7POOgvt7e2uuq+99lqk02n85S9/6dcen+eeew7z58/HtGnTMHv2bNx44436ff3xj3/EgQceiNbWVv3z9913HyZOnIiXX35Zv/bSSy9h4sSJ2LJlS8HbLyEh0b8giZOERD+AoihIJpNIJpNIJBJoamrC7bffjng8jrPPPttV5vLLL8eSJUtwzjnn4L777sMpp5yCO++8Uw/7AcCf//xnfPe738WBBx6I3/zmN/jGN76B//f//h9uvvlm23c2NTXhK1/5CmpqavD73/8eNTU1rm3+5S9/icceewyTJk0Svn9epNNpvZ+0f1aSyOKee+7B1VdfjUMOOQSLFy/GFVdcgRdeeAELFixALBbD8ccfj3Q6jddff12X0V6/+eab+rWXX34Z48ePx8iRI/fezUlISOwTkKE6CYl+gDfffBMHHXSQ7fp3v/tdjB071lFm+fLleO2113Dbbbfh85//PABg9uzZiEQiuOuuu3DxxRfjgAMOwN133425c+fi5z//uS7b19eHJ5980uR1am1txVe/+lVEIhE89NBDqKury9rmQhImDXPnzrVdmzVrFh566CHb9fb2dtx7770499xzTURywoQJuPDCC/HEE0/gggsuwJgxY7BixQp87nOfQzwexzvvvIODDjrIlJi/fPlyzJs3b6/ck4SExL4FSZwkJPoBDjroIPz4xz8GoHqSOjo6sHz5ctxxxx3o6enB1VdfbZNZuXIlAoEATj31VNP1z3/+87jrrrvwxhtvwOfzobm5GSeddJLpM1/5ylfwla98xXTt0ksvxbp167hIkxekUikoipL1M36/H36/uyP83nvvtSWHV1ZWOn723XffRTwexxlnnGG6Pn36dAwfPhxvvPEGLrjgAhx33HH497//DQB4++234ff7cfHFF+OGG25Ab28vdu/ejU2bNuH444/nuU0JCYkShyROEhL9ABUVFTj44INN1+bMmYOenh4sWbIEF110kS2PqL29HXV1dQgGzcNYIxadnZ1oa2sDwLfrrKenB6NHj8avfvUrPP744wgEAgJ3ZMdXvvIVW3kFK771rW/hyiuvdP37hAkTuJPDtTymhoYG298aGhrQ2dkJADj22GPxhz/8AVu2bMHrr7+Oww47DHPmzEEikcA777yDDRs2oK6uzpa8LyEhMTAhiZOERD/GgQceiL/+9a/YunWrjfzU1NSgtbUVyWTSRJ52794NAKirq0N1dTUAoKWlxSTb1taGNWvWmMjAww8/jHXr1uGrX/0qHn74YVxyySV5vZcf//jH6O7uzvqZwYMH502flp/V3NxsC3c2NTXp+UrTp09HZWUlVqxYgddffx3HH388Bg0ahHHjxmHlypVYs2YNjjvuuKyeMAkJiYEDaQkkJPoxVq1ahUAg4JiUPGPGDKRSKTz33HOm608//TQA4PDDD8cBBxyAuro6/Oc//zF95plnnsHXv/519PX16dcaGxsxe/ZsnHbaabjrrrvyvoPsgAMOwMEHH5z135Ah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H4dzjhs74HfnSojBp1jPkhhAaGrq9CwbDPr11eVArQyrFgz0YeiQ6gHdD/JZ6D99kE4reOvj3dhvUAVGDPbm4fOK/tIHxYbsh73fB42NMs+qmJAeJwnPCPhpJ6pLSOxt+P30kJWEhIQEBXLWk5CQkJCQkJDghCROEhISEhISEhKckMRJQkJCQkJCQoITkjhJSEhISEhISHBCEicJCQkJCQkJCU5I4iQhISEhISEhwQlJnCQkJCQkJCQkODGgC2BKSEhISEhISFAgPU4SEhISEhISEpyQxElCQkJCQkJCghOSOElISEhISEhIcEISJwkJCQkJCQkJTkjiJCEhISEhISHBCUmcJCQkJCQkJCQ4IYmThISEhISEhAQnJHGSkJCQkJCQkOCEJE4SEhISEhISEpyQxMkD9uzZg4ULF2L69OmYOXMmfv7znyOZTBa7WQVBS0sL5s6dizfeeEO/9t577+Hcc8/FtGnTcMIJJ+Cvf/1rEVu4d7F27VpccsklmDFjBmbPno3vf//7aGlpATBw+mHFihU499xzcdhhh2H27Nn46U9/ilgsBmDg9IGGVCqFBQsW4LrrrtOvDaQ+eO655zB58mRMmzZN/7do0SIAA6cf2tra8P3vfx8zZ87EEUccgYULF2L37t0ABk4fDDgoEmR8+ctfVq655hqlp6dH2bx5s3Laaacpv/vd74rdrL2Ot956SznppJOUCRMmKK+//rqiKIrS1tamzJgxQ3nkkUeURCKhvPbaa8q0adOU9957r8itzT96e3uV2bNnK3fddZfS19entLS0KF//+teVb3zjGwOmH/bs2aMcfPDByt/+9jcllUopu3btUk4//XTlrrvuGjB9wOLOO+9UJk2apFx77bWKogys8aAoinLrrbcq1113ne36QOqHL3/5y8oVV1yhtLe3K52dncq3vvUt5bLLLhtQfTDQID1ORGzatAkrV67EokWLEI1GMXLkSCxcuBCPPvposZu2V/Hkk0/ie9/7Hq6++mrT9RdffBG1tbW48MILEQwGMWvWLJxxxhkl2R/bt2/HpEmTcMUVVyAcDqOurg7nnXce3nzzzQHTD/X19Xjttdcwf/58+Hw+tLW1oa+vD/X19QOmDzSsWLECL774Ik4++WT92kDrg9WrV2PKlCm26wOlHz744AO89957uPXWW1FdXY3Kykr89Kc/xfe+970B0wcDEZI4EbF+/XrU1tZiyJAh+rWxY8di+/bt6OjoKGLL9i7mzJmDf/3rXzj11FNN19evX48JEyaYro0bNw5r164tZPMKggMOOABLlixBIBDQr73wwgs46KCDBlQ/VFZWAgCOPfZYnHHGGWhsbMT8+fMHVB/s2bMHN9xwA379618jGo3q1wdSH6TTaaxZswb/+9//cPzxx+OYY47Bj370I7S3tw+Yfnj//fcxbtw4PP7445g7dy7mzJmDX/7yl2hsbBwwfTAQIYkTEd3d3SZDCUB/39PTU4wmFQSNjY0IBoO26079EYlESrovAEBRFNxxxx3473//ixtuuGFA9sOLL76I5cuXw+/346qrrhowfZBOp7Fo0SJccsklmDRpkulvA6UPADXfcfLkyZg3bx6ee+45/PnPf8bGjRuxaNGiAdMP7e3t+Pjjj7Fx40Y8+eSTeOqpp7Br1y5ce+21A6YPBiIkcSKivLwcvb29pmva+4qKimI0qaiIRqN6YrCGWCxW0n3R1dWFq666Cs888wweeeQRTJw4cUD2QyQSwZAhQ7Bo0SK8/PLLA6YP7r//foTDYSxYsMD2t4HSBwDQ0NCARx99FOeccw6i0Sj2228/LFq0CMuXL4eiKAOiH8LhMADghhtuQGVlJRoaGvCd73wHL7300oDpg4EISZyIGD9+PNra2tDc3Kxf27BhA4YOHYqqqqoitqw4mDBhAtavX2+69sknn2D8+PFFatHexebNm3H22Wejq6sLS5cuxcSJEwEMnH545513cMoppyAej+vX4vE4QqEQxo0bNyD64O9//ztWrlyJ6dOnY/r06Xj22Wfx7LPPYvr06QPmOQDUHaa/+tWvoCiKfi0ej8Pv92Pq1KkDoh/GjRuHdDqNRCKhX0un0wCAAw88cED0wUCEJE5E7L///jj88MNxyy23oKurC1u2bME999yDc845p9hNKwrmzp2L5uZmPPTQQ0gkEnj99dfxzDPP4Oyzzy520/KO9vZ2XHzxxTjssMPw4IMPor6+Xv/bQOmHiRMnIhaL4de//jXi8Ti2bduGX/7ylzjnnHMwb968AdEHzz//PN555x289dZbeOutt3D66afj9NNPx1tvvTVgngMAqK2txaOPPoolS5YgmUxi+/btuO2223DWWWcNmGfhqKOOwsiRI/GDH/wA3d3daGlpwR133IGTTjoJp59++oDog4EIn8IuFyS40NzcjJ/85Cd444034Pf78YUvfAHf+973TEnDpYyJEyfi4YcfxsyZMwGoO2t+/vOfY926daivr8fChQsxf/78Ircy//jDH/6AW2+9FdFoFD6fz/S3VatWDZh++OSTT3DLLbdg9erVqKqqwhlnnKHvNBwofcBCq+F06623Ahg44wEAVq5cidtvvx3r1q1DWVkZTjvtNCxatAhlZWUDph927dqFW2+9FW+++Sb6+vpwwgkn4IYbbkB1dfWA6YOBBkmcJCQkJCQkJCQ4IUN1EhISEhISEhKckMRJQkJCQkJCQoITkjhJSEhISEhISHBCEicJCQkJCQkJCU5I4iQhISEhISEhwQlJnCQkJCQkJCQkOCGJk4SEhISEhIQEJyRxkpCQkJCQkJDghCROEhIDBNdddx0mTpyY9d8TTzyhn79XaCxbtgzf/OY3uT77s5/9DA899NDebZCEhISEA2TlcAmJAYLOzk7Tae1z5szBD37wA5x66qn6taqqKnR2dqKxsbGgbevo6MAZZ5yBhx56CGPGjMn5+fb2dpx22ml49NFHMXr06AK0UEJCQkKF9DhJSAwQVFVVobGxUf/ndC0SiRScNAHAQw89hClTpnCRJgCoqanBaaedhrvvvnsvt0xCQkLCDEmcJCQkdFhDdRMnTsSzzz6Liy66CFOnTsXcuXOxbNkyLFu2DPPmzcOhhx6KSy+9FC0tLbrMhg0b8PWvfx3Tpk3DnDlzcM0116CpqclVZ19fHx599FGcdtpp+rXe3l7ccMMNmD17Ng4++GB84QtfwIsvvmiS+9znPod//vOf2LlzZx57QEJCQiI7JHGSkJDIip/97Ge48MIL8eyzz2LcuHG45pprcO+99+K2227Dfffdh/fffx+/+93vAKgnxV9wwQUYOXIkli5divvuuw9dXV04//zz0dPT4/j9b731Fjo6OnDsscfq1+666y58/PHHeOCBB/Dcc8/hmGOOwdVXX42tW7fqnzn00ENRXV2Nl156ae92gISEhAQDSZwkJCSy4qyzzsK8efMwatQonQBdffXVmDp1Ko488kjMnj0b69atAwA89thjGDx4MG688UaMHTsWU6ZMwZ133onm5mY8//zzjt//7rvvYsSIEaioqNCvbd68GZWVlRg1ahRGjhyJb3/727j33ntRU1Njkp0wYQLee++9vXfzEhISEhYEi90ACQmJ/g027ygSiQAARo4cqV8rKytDPB4HAHz44YfYsGEDpk2bZvqOvr4+bNiwwfH7m5ubUV9fb7r29a9/HZdffjlmzZqFadOmYfbs2TjttNNQVVVl+lx9fT2am5u935yEhIQEEZI4SUhIZEUwaDcTPp/P8bPpdBpHHnkkbrrpJtvfrKSH/a50Om26Nm3aNLz00kt49dVXsWLFCixduhR33303lixZglmzZumfS6VS8Pul41xCQqJwkBZHQkIibxg/fjw2bNiAYcOGYfTo0Rg9ejRqampwyy236OE8K4YMGWJKLgeAxYsX4+2338aJJ56IH/7wh3jhhRcwcuRIvPDCC6bPtba2YvDgwXvtfiQkJCSskMRJQkIib7jgggvQ2dmJ7373u/joo4+wdu1aXHPNNXj//fcxfvx4R5mpU6di27ZtaGtr069t2rQJN910E1asWIFt27bh+eefx/bt200hwHQ6jbVr1+KQQw7Z27clISEhoUMSJwkJibxh5MiReOSRR9Db24sLLrgAX/7yl+Hz+fDHP/4RgwYNcpQ54ogjUFNTgzfeeEO/9uMf/xizZs3CokWLMG/ePCxevBjf+973cOaZZ+qfWbNmDbq7u3H88cfv9fuSkJCQ0CArh0tISBQdd955Jz766CPcf//93DI333wzenp68H//9397sWUSEhISZkiPk4SERNFxySWX4IMPPnDdeWdFS0sLXnjhBSxcuHAvt0xCQkLCDEmcJCQkio6amhrcfPPNuO2227g+f/fdd+PSSy/F/vvvv3cbJiEhIWGBDNVJSEhISEhISHBCepwkJCQkJCQkJDghiZOEhISEhISEBCckcZKQkJCQkJCQ4IQkThISEhISEhISnJDESUJCQkJCQkKCE5I4SUhISEhISEhwQhInCQkJCQkJCQlOSOIkISEhISEhIcGJ/w8gH9cwEiOnoQAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "fig, ax = plt.subplots(figsize=(6, 3))\n", - "ax.plot(df_block1['Time'], df_block1['Flow'], label='Flow')\n", - "ax.set_xlabel('Time (s)')\n", - "ax.set_ylabel('Flow')\n", - "ax.set_title('Block 1 – Flow')\n", - "ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))\n", - "ax.grid(True)\n", - "fig.tight_layout()" + "# Quick plot using the built-in method\n", + "lc.plot_channel('Flow', block=1)" ] }, { "cell_type": "code", - "execution_count": 20, - "id": "204b02e6", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "# Plot Pressure with custom styling\n", + "lc.plot_channel('Pressure', block=1, color='tomato', figsize=(6, 3))" + ] + }, + { + "cell_type": "markdown", + "id": "16", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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zZs3o0ePHob3unXrhg0bNmSzeRnBIYccghkzZsDj8Wjvvf/++zj88MNb9XXHUVhYCAAYOnQoRo4ciXbt2uGss85q9de+e/duTJw4EY888giCwaD2fmu/bkVRsG7dOvz3v//FH/7wBxx33HG4/fbbUV1d3aqvfe3atejWrRv+9a9/Yfjw4RgyZAgeeOABtGvXrlVfNyE7IOKkQ319vWFSBaD93dDQkIsmZRTt2rWD1+tNet+qHwKBQKvrA1VV8dhjj+Gjjz7CxIkT95nrBqLWhk8++QSyLOO6665r1deuKArGjx+PsWPHolevXobPWvN1A1H94mGHHYaTTz4Z7777LubOnYvvv/8e48ePb9XXXl1djY0bN+L777/HG2+8gX//+9/YsWMHJkyY0Kqvm5AdEHHSIT8/H42NjYb34n8XFBTkokk5QTAY1ATDcTQ1NbWqPqirq8N1112HBQsW4OWXX0bPnj33ieuOIxAIoH379hg/fjz+97//teprf+655+D3+3HBBRckfdaarxsA2rZtizlz5uDss89GMBjE/vvvj/Hjx+OTTz6Bqqqt9tr9fj8AYOLEiSgsLETbtm3x97//HR9//HGrvm5CdkDESYfu3bujqqoKu3bt0t7bunUrOnTogKKiohy2LLvo0aMHNm/ebHhvy5Yt6N69e45a5C62bduGUaNGoa6uDvPmzUPPnj0BtP7r/vzzz3HKKaegpaVFe6+lpQU+nw/dunVrtdf+5ptvYuXKlejXrx/69euHt99+G2+//Tb69evX6u/5hg0b8PDDD0NVVe29lpYWyLKMPn36tNpr79atGxRFQSgU0t5TFAUAcOihh7ba6yZkB0ScdDjooIPwu9/9DlOmTEFdXR1+/PFHPP300zj77LNz3bSsYvjw4di1axdmzZqFUCiE5cuXY8GCBRg1alSum+YY1dXVuOiii3D00UfjxRdfRHl5ufZZa75uAOjZsyeamprwyCOPoKWlBT///DMeeOABnH322Tj55JNb7bUvXLgQn3/+OVavXo3Vq1fj1FNPxamnnorVq1e3+nteWlqKOXPmYMaMGQiHw9i+fTseeughnHnmma36ng8aNAidOnXCbbfdhvr6elRUVOCxxx7DiSeeiFNPPbXVXjchO5BU/VaEgF27duGee+7BihUrIMsyzjjjDNx0000GMXFrRM+ePTF79mwMGDAA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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ - "pressure_df = lc.get_channel(1, 'Pressure')\n", - "fig, ax = plt.subplots(figsize=(6, 3))\n", - "ax.plot(pressure_df['Time'], pressure_df['value'], color='tomato', label='Pressure')\n", - "ax.set_xlabel('Time (s)')\n", - "ax.set_ylabel('Pressure')\n", - "ax.set_title('Block 1 – Pressure')\n", - "ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))\n", - "ax.grid(True)\n", + "### Multiple channels in subplots\n", + "Use the `ax` parameter to combine multiple channels in a single figure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "# Combine multiple channels in subplots\n", + "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(6, 6), sharex=True)\n", + "\n", + "lc.plot_channel('Flow', block=1, ax=ax1, ylabel='Flow (L/s)')\n", + "lc.plot_channel('Pressure', block=1, ax=ax2, color='tomato', ylabel='Pressure (cmH2O)')\n", + "\n", + "fig.suptitle('Block 1 – Flow and Pressure', fontsize=12)\n", "fig.tight_layout()" ] } ], "metadata": { "kernelspec": { - "display_name": "ultraPrevent", + "display_name": "venv (3.10.13)", "language": "python", "name": "python3" }, @@ -515,7 +211,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.5" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/pyproject.toml b/pyproject.toml index cbbf11c..3a6326b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,25 +4,64 @@ build-backend = "setuptools.build_meta" [project] name = "labchart_parser" -version = "0.1.1" +version = "0.2.0" description = "Parser for ADInstruments LabChart text exports" readme = "README.md" license = { file = "LICENSE" } authors = [ {name = "Damien Bachasson", email = "damien.bachasson@gmail.com"} ] -requires-python = ">=3.8" +requires-python = ">=3.10" dependencies = [ - "numpy", - "pandas", - "matplotlib" + "numpy>=1.20", + "pandas>=1.3", + "matplotlib>=3.4", ] classifiers = [ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent" ] keywords = ["LabChart", "parser", "ADInstruments", "physiology", "signals"] +[project.urls] +Homepage = "https://github.com/Neures-1158/labchart_txt_parser" +Repository = "https://github.com/Neures-1158/labchart_txt_parser" +Changelog = "https://github.com/Neures-1158/labchart_txt_parser/blob/main/CHANGELOG.md" +Issues = "https://github.com/Neures-1158/labchart_txt_parser/issues" + +[project.optional-dependencies] +dev = [ + "pytest>=7.0", + "pytest-cov>=4.0", + "ruff>=0.3", + "black>=24.0", + "isort>=5.13", +] + [tool.setuptools.packages.find] -where = ["src"] \ No newline at end of file +where = ["src"] + +[tool.black] +line-length = 88 +target-version = ["py310", "py311", "py312"] + +[tool.isort] +profile = "black" +line_length = 88 + +[tool.ruff] +line-length = 88 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "W", "UP", "B"] +ignore = ["E501"] # line length is enforced by black + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +addopts = "-ra" diff --git a/src/labchart_parser/__init__.py b/src/labchart_parser/__init__.py index fadc3fa..275642f 100644 --- a/src/labchart_parser/__init__.py +++ b/src/labchart_parser/__init__.py @@ -1,4 +1,13 @@ """Top-level package for LabChart parser library.""" + +from importlib.metadata import PackageNotFoundError, version + from .core import LabChartFile from .parser import parse_labchart_txt -__all__ = ["LabChartFile", "parse_labchart_txt"] \ No newline at end of file + +try: + __version__ = version("labchart_parser") +except PackageNotFoundError: + __version__ = "0.0.0+unknown" + +__all__ = ["LabChartFile", "parse_labchart_txt", "__version__"] diff --git a/src/labchart_parser/core.py b/src/labchart_parser/core.py index bc42f46..9dc37e7 100644 --- a/src/labchart_parser/core.py +++ b/src/labchart_parser/core.py @@ -1,70 +1,135 @@ +"""High-level API: ``LabChartFile`` wraps the parsed DataFrame + metadata.""" + +from __future__ import annotations + +import os +from typing import Any + +import pandas as pd + +from .exceptions import InvalidChannelError from .parser import parse_labchart_txt -from .exceptions import FileParsingError, InvalidChannelError + +_SYSTEM_COLS = ("Time", "time_block", "time_abs", "block", "Comment") + class LabChartFile: - def __init__(self, df, meta): + """Parsed LabChart text export. Construct with ``LabChartFile.from_file(path)``.""" + + def __init__(self, df: pd.DataFrame, meta: dict[str, Any]) -> None: self._data = df self._metadata = meta @classmethod - def from_file(cls, path: str) -> "LabChartFile": - """ - Load a LabChart text export file - """ + def from_file(cls, path: str | os.PathLike[str]) -> LabChartFile: + """Parse a LabChart ``.txt`` export.""" df, meta = parse_labchart_txt(path) return cls(df, meta) @property - def metadata(self): + def metadata(self) -> dict[str, Any]: return self._metadata @property - def channels(self): - return [c for c in self._data.columns - if c not in ("Time", "time_block", "time_abs", "block", "Comment")] + def channels(self) -> list[str]: + """Channel names — every column in ``_data`` except the system columns. + + If you add a new computed column, add it to ``_SYSTEM_COLS`` too, + otherwise it leaks into the user-visible channel list. + """ + return [c for c in self._data.columns if c not in _SYSTEM_COLS] @property - def blocks(self): - return list(self._data["block"].unique()) + def blocks(self) -> list[int]: + return [int(block) for block in self._data["block"].unique()] @property - def comments(self): - """ - Return a DataFrame with all comments and their associated time and block. - """ - return self._data[self._data["Comment"].notna()][["Time", "time_block", "time_abs", "block", "Comment"]].reset_index(drop=True) + def comments(self) -> pd.DataFrame: + """Rows with a non-null ``Comment``, columns: ``Time, time_block, time_abs, block, Comment``.""" + return self._data[self._data["Comment"].notna()][ + ["Time", "time_block", "time_abs", "block", "Comment"] + ].reset_index(drop=True) - def get_block_comments_excluding(self, block: int, exclude_values: list): - """ - Return comments for a block, excluding those that match any value in the provided list (case-insensitive). - """ + def get_block_comments_excluding( + self, block: int, exclude_values: list + ) -> list[str]: + """Comments in ``block``, with case-insensitive exclusion of ``exclude_values``.""" comments = self.comments - # Filter on the selected block comments_block = comments[comments["block"] == block] - exclude_normalized = {str(v).strip().casefold() for v in exclude_values} - comment_norm = comments_block["Comment"].str.strip().str.casefold() - mask = ~comment_norm.isin(exclude_normalized) - return comments_block.loc[mask, "Comment"].tolist() + excluded = {str(v).strip().casefold() for v in exclude_values} + norm = comments_block["Comment"].str.strip().str.casefold() + return comments_block.loc[~norm.isin(excluded), "Comment"].tolist() - def get_block_df(self, b: int): - """ - Return a DataFrame for a specific block, including time and all channels. - """ - return self._data.loc[self._data["block"] == b, ["Time", "time_block", "time_abs", "Comment", *self.channels]] - - def get_channel(self, b: int, channel: str): - """ - Return a DataFrame for a specific channel in a specific block, including time and comments. - """ + def get_block_df(self, b: int) -> pd.DataFrame: + """Slice of ``_data`` where ``block == b``. Index is **not** reset.""" + return self._data.loc[ + self._data["block"] == b, + ["Time", "time_block", "time_abs", "Comment", *self.channels], + ] + + def get_channel(self, b: int, channel: str) -> pd.DataFrame: + """Slice for one ``channel`` in block ``b``. Channel column is renamed to ``value``.""" if channel not in self.channels: - raise InvalidChannelError(f"Canal inconnu: {channel}") - d = self._data.loc[self._data["block"] == b, ["Time", "time_block", "time_abs", "Comment", channel]].copy() + raise InvalidChannelError(f"Unknown channel: {channel}") + d = self._data.loc[ + self._data["block"] == b, + ["Time", "time_block", "time_abs", "Comment", channel], + ].copy() d.rename(columns={channel: "value"}, inplace=True) return d - def slice_time_abs(self, tmin: float, tmax: float): - """ - Return a DataFrame sliced between two absolute time points, including time and all channels. - """ + def slice_time_abs(self, tmin: float, tmax: float) -> pd.DataFrame: + """Rows with ``tmin <= time_abs <= tmax`` (both ends inclusive).""" m = (self._data["time_abs"] >= tmin) & (self._data["time_abs"] <= tmax) - return self._data.loc[m, ["Time", "time_block", "time_abs", "block", "Comment", *self.channels]] \ No newline at end of file + return self._data.loc[ + m, ["Time", "time_block", "time_abs", "block", "Comment", *self.channels] + ] + + def plot_channel( + self, + channel: str, + block: int | None = None, + time_col: str = "time_block", + ax: Any | None = None, + figsize: tuple = (10, 4), + title: str | None = None, + xlabel: str | None = None, + ylabel: str | None = None, + grid: bool = True, + **kwargs, + ) -> Any: + """Plot ``channel`` (one block or all). Returns the ``Axes``. + + ``time_col`` is one of ``"Time"``, ``"time_block"``, ``"time_abs"``. + Extra ``**kwargs`` are forwarded to ``ax.plot``. + """ + if channel not in self.channels: + raise InvalidChannelError(f"Unknown channel: {channel}") + + if block is not None: + df = self.get_block_df(block) + block_label = f"Block {block}" + else: + df = self._data + block_label = "All blocks" + + if ax is None: + import matplotlib.pyplot as plt + + _, ax = plt.subplots(figsize=figsize) + + ax.plot(df[time_col], df[channel], **kwargs) + ax.set_title(title or f"{channel} – {block_label}") + ax.set_xlabel( + xlabel + or { + "Time": "Time (s)", + "time_block": "Time in block (s)", + "time_abs": "Absolute time (s)", + }.get(time_col, time_col) + ) + ax.set_ylabel(ylabel or channel) + if grid: + ax.grid(True) + ax.figure.tight_layout() + return ax diff --git a/src/labchart_parser/exceptions.py b/src/labchart_parser/exceptions.py index 768c477..27e3891 100644 --- a/src/labchart_parser/exceptions.py +++ b/src/labchart_parser/exceptions.py @@ -1,13 +1,19 @@ """Custom exception classes for the labchart_parser package.""" + class FileParsingError(Exception): """Raised when a file cannot be parsed due to invalid format or content.""" + pass + class NoDataError(Exception): """Raised when no data could be extracted from the file.""" + pass + class InvalidChannelError(KeyError): """Raised when an invalid channel name is requested.""" - pass \ No newline at end of file + + pass diff --git a/src/labchart_parser/parser.py b/src/labchart_parser/parser.py index 5bac60f..7a0dafb 100644 --- a/src/labchart_parser/parser.py +++ b/src/labchart_parser/parser.py @@ -18,29 +18,38 @@ from __future__ import annotations +import io +import os import re -import math +import warnings from pathlib import Path -from typing import Tuple, Dict, List, Optional import numpy as np import pandas as pd -# Regular expression to detect a numeric field (float) in the first column +from .exceptions import FileParsingError, NoDataError + FLOAT_START = re.compile(r"^\s*[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?\s*$") +_HEADER_KEYS_SCALAR = ("Interval=", "ExcelDateTime=", "TimeFormat=", "DateFormat=") +_HEADER_KEYS_LIST = ( + "ChannelTitle=", + "UnitName=", + "Range=", + "TopValue=", + "BottomValue=", +) +_ALL_HEADER_PREFIXES = _HEADER_KEYS_SCALAR + _HEADER_KEYS_LIST -def parse_labchart_txt(path: str) -> Tuple[pd.DataFrame, Dict[str, object]]: - """Parse an exported LabChart text file. - The export must be tab-delimited and include a ``Time`` column. If - comments were included during export, this parser will capture them in - the resulting DataFrame. +def parse_labchart_txt( + path: str | os.PathLike[str], +) -> tuple[pd.DataFrame, dict[str, object]]: + """Parse a LabChart text export. Returns ``(df, meta)``. - Parameters - ---------- - path : str - Path to the LabChart text file to parse. + ``df`` columns: ``Time``, one column per channel, ``Comment``, ``block``, + ``time_abs``, ``time_block``. ``meta`` mirrors block 0's metadata at the + top level and exposes the per-block list under ``meta["blocks"]``. Returns ------- @@ -48,140 +57,290 @@ def parse_labchart_txt(path: str) -> Tuple[pd.DataFrame, Dict[str, object]]: A tuple ``(df, meta)`` where ``df`` contains the parsed data and ``meta`` holds the parsed metadata. ``df`` includes the columns: - * ``Time`` – time within each block. - * ``time_block`` – time with each block rebased to 0 + * ``Time`` – time within each block. + * ``time_block`` – time within each block rebased to 0. * one column per channel detected in the header. * ``Comment`` – annotation text (``None`` on numeric rows). * ``block`` – block index (1-based). * ``time_abs`` – continuous time across blocks. - Raises ------ - ValueError - If the file cannot be parsed or if no data lines are found. + FileNotFoundError + If the file does not exist. + FileParsingError + If the file has a bad extension, is empty, has no data section, + has fewer than 2 columns, or has mismatched ``ChannelTitle`` across blocks. + NoDataError + If segmentation succeeded but every block was empty. """ p = Path(path) - lines = p.read_text(encoding="utf-8", errors="ignore").splitlines() - - meta: Dict[str, object] = {} - chan_titles: Optional[List[str]] = None - unit_names: Optional[List[str]] = None - first_data_idx: Optional[int] = None - - # Parse metadata lines before the data section - for i, ln in enumerate(lines): - if ln.startswith("Interval="): - meta["Interval"] = ln.split("\t", 1)[1].strip() - elif ln.startswith("ExcelDateTime="): - meta["ExcelDateTime"] = ln.split("\t", 1)[1].strip() - elif ln.startswith("TimeFormat="): - meta["TimeFormat"] = ln.split("\t", 1)[1].strip() - elif ln.startswith("DateFormat="): - meta["DateFormat"] = ln.split("\t", 1)[1].strip() - elif ln.startswith("ChannelTitle="): - chan_titles = [c.strip() for c in ln.split("\t")[1:]] - elif ln.startswith("UnitName="): - unit_names = [c.strip() for c in ln.split("\t")[1:]] - elif (ln and ln[0].isdigit()) or ln.startswith(("+", "-", ".")): - # Potentially a data line; check first token is numeric - if FLOAT_START.match(ln.split("\t", 1)[0]): - first_data_idx = i - break - - if first_data_idx is None: - raise ValueError("Début des données introuvable.") - - # Determine number of columns from first data row - first_row = lines[first_data_idx].split("\t") - n_cols = len(first_row) - - data: List[List[object]] = [] - for ln in lines[first_data_idx:]: - if not ln.strip(): - continue - parts = ln.split("\t") + if not p.exists(): + raise FileNotFoundError(f"File not found: {path}") + if p.suffix.lower() not in (".txt", ".text"): + raise FileParsingError( + f"Unsupported file extension: {p.suffix}. " + "Please use a .txt file exported from LabChart." + ) - # Identify the numeric part and any trailing comment part - numeric_parts = parts[:n_cols] - extra_parts = parts[n_cols:] - - # Try to parse the numeric fields; treat '*' and empty fields as NaN - numeric_row: List[Optional[float]] = [] - is_numeric = True - for x in numeric_parts: - val = x.strip() - if val in ("*", ""): - numeric_row.append(math.nan) - else: - try: - numeric_row.append(float(val)) - except ValueError: - is_numeric = False - break - - # Assemble any extra fields into a comment string - comment_text: Optional[str] = None - if extra_parts: - comment_text = "\t".join(extra_parts).strip() - # Remove '#*' marker if present at the start of the comment - if comment_text.startswith("#*"): - comment_text = comment_text[2:].lstrip() - - if is_numeric: - # Numeric row (with or without a trailing comment) - numeric_row.append(comment_text) - data.append(numeric_row) - else: - # Pure comment row: use time value and fill numeric columns with NaN - try: - t = float(parts[0]) - except ValueError: - continue - c_text = "\t".join(parts[1:]).strip() - if c_text.startswith("#*"): - c_text = c_text[2:].lstrip() - row = [t] + [math.nan] * (n_cols - 1) + [c_text] - data.append(row) - - if not data: - raise ValueError("Aucune ligne de données valide après parsing.") - - # Construct DataFrame with appropriate column names - if chan_titles and len(chan_titles) == (n_cols - 1): - cols = ["Time"] + chan_titles + # utf-8 strictly first, then latin-1. errors="ignore" silently drops + # bytes from cp1252 exports — don't go back to that. + try: + text = p.read_text(encoding="utf-8") + except UnicodeDecodeError: + try: + text = p.read_text(encoding="latin-1") + except OSError as e: + raise FileParsingError(f"Unable to read file: {e}") from e + except OSError as e: + raise FileParsingError(f"Unable to read file: {e}") from e + lines = text.splitlines() + if not lines: + raise FileParsingError("The file is empty.") + + blocks = _segment_into_blocks(lines) + if not blocks: + raise FileParsingError( + "Data start not found. Please verify that the file is a valid " + "LabChart export with tab-delimited numeric data." + ) + + _validate_channel_consistency(blocks) + + chan_titles = blocks[0]["meta"].get("ChannelTitle") + if chan_titles: + n_cols = 1 + len(chan_titles) + else: + n_cols = len(blocks[0]["data_lines"][0].split("\t")) + if n_cols < 2: + raise FileParsingError( + f"The file contains only {n_cols} column(s). " + "A valid LabChart export must contain at least Time + 1 channel." + ) + + if chan_titles: + channel_cols: list[str] = ["Time"] + chan_titles else: - cols = ["Time"] + [f"Ch{i}" for i in range(1, n_cols)] - cols.append("Comment") - df = pd.DataFrame(data, columns=cols) - - # Compute block indices and continuous time - t = df["Time"].to_numpy(float) - jumps = np.where(np.diff(t) < 0)[0] + 1 - starts = np.r_[0, jumps] - ends = np.r_[jumps, len(df)] + channel_cols = ["Time"] + [f"Ch{i}" for i in range(1, n_cols)] + + time_format = blocks[0]["meta"].get("TimeFormat") + if time_format and time_format != "StartOfBlock": + warnings.warn( + f"TimeFormat={time_format!r} (expected 'StartOfBlock'). " + "Time-reset-based block detection relies on negative time jumps; " + "with this format, only header transitions will create blocks. " + "Re-export from LabChart with 'Start from Block' selected.", + stacklevel=2, + ) + + block_dfs = [ + _parse_block_data(b["data_lines"], channel_cols, n_cols) for b in blocks + ] + nonempty = [(b, d) for b, d in zip(blocks, block_dfs, strict=True) if len(d) > 0] + if not nonempty: + raise NoDataError( + "No valid data lines found after parsing. " + "Please verify that the file contains numeric data." + ) + blocks = [b for b, _ in nonempty] + block_dfs = [d for _, d in nonempty] + + df = pd.concat(block_dfs, ignore_index=True) + block_ids = np.empty(len(df), dtype=int) + pos = 0 + for b_idx, df_b in enumerate(block_dfs, start=1): + block_ids[pos : pos + len(df_b)] = b_idx + pos += len(df_b) + df["block"] = block_ids + + # time_abs: each block shifted by the cumulative offset, advanced by + # one sample interval at every boundary so consecutive samples in + # different blocks never share a time_abs. time_abs = np.empty(len(df), dtype=float) offset = 0.0 - for b, (s, e) in enumerate(zip(starts, ends), start=1): - block_ids[s:e] = b - tb = t[s:e] + pos = 0 + for b, df_b in zip(blocks, block_dfs, strict=True): + sz = len(df_b) + tb = df_b["Time"].to_numpy(float) tb0 = tb - tb[0] - time_abs[s:e] = tb0 + offset - offset += tb0[-1] if len(tb0) else 0.0 - - df["block"] = block_ids + time_abs[pos : pos + sz] = tb0 + offset + block_interval = b["meta"].get("Interval_s") + if block_interval is None: + block_interval = float(np.median(np.diff(tb0))) if len(tb0) > 1 else 0.0 + offset += tb0[-1] + block_interval + pos += sz df["time_abs"] = time_abs - # Zero-based time within each block (starts at 0.0 for every block) - # Compute directly from relative Time within each block df["time_block"] = df["Time"] - df.groupby("block")["Time"].transform("first") - # Additional metadata: convert interval to seconds if possible - try: - meta["Interval_s"] = float(str(meta.get("Interval", "")).split()[0]) - except Exception: - pass - if unit_names and len(unit_names) == (n_cols - 1): - meta["UnitName"] = unit_names + meta: dict[str, object] = dict(blocks[0]["meta"]) + # ChannelTitle becomes the columns; Range/TopValue/BottomValue vary + # per block and live under meta["blocks"]. + for k in ("ChannelTitle", "Range", "TopValue", "BottomValue"): + meta.pop(k, None) + unit_names = blocks[0]["meta"].get("UnitName") + if not (unit_names and len(unit_names) == n_cols - 1): + meta.pop("UnitName", None) + meta["blocks"] = [b["meta"] for b in blocks] return df, meta + + +def _parse_header_line(ln: str, target: dict[str, object]) -> bool: + """Write a recognized ``Key=...`` header into ``target``. Returns True iff matched.""" + for prefix in _HEADER_KEYS_SCALAR: + if ln.startswith(prefix): + target[prefix.rstrip("=")] = ( + ln.split("\t", 1)[1].strip() if "\t" in ln else "" + ) + return True + for prefix in _HEADER_KEYS_LIST: + if ln.startswith(prefix): + target[prefix.rstrip("=")] = [c.strip() for c in ln.split("\t")[1:]] + return True + return False + + +def _finalize_block_meta(meta: dict[str, object]) -> None: + """Add ``Interval_s`` derived from ``Interval`` if present and parseable.""" + if "Interval" in meta and "Interval_s" not in meta: + try: + meta["Interval_s"] = float(str(meta["Interval"]).split()[0]) + except (ValueError, IndexError, AttributeError): + pass + + +def _segment_into_blocks(lines: list[str]) -> list[dict[str, object]]: + """Split ``lines`` into per-block ``{meta, data_lines}`` dicts. + + Block boundaries: (a) header line after data lines, (b) ``Time`` resets + inside a contiguous data run. On (b), metadata is shallow-copied so + the sub-block carries the parent section's metadata. + """ + blocks: list[dict[str, object]] = [] + current_meta: dict[str, object] = {} + current_data: list[str] = [] + last_time: float | None = None + in_data = False + + for ln in lines: + if ln.startswith(_ALL_HEADER_PREFIXES): + if in_data: + _finalize_block_meta(current_meta) + blocks.append({"meta": current_meta, "data_lines": current_data}) + current_meta = {} + current_data = [] + last_time = None + in_data = False + _parse_header_line(ln, current_meta) + continue + + if not ln.strip(): + continue + + first_token = ln.split("\t", 1)[0] + if not FLOAT_START.match(first_token): + continue + try: + t = float(first_token) + except ValueError: + continue + + if in_data and last_time is not None and t < last_time: + _finalize_block_meta(current_meta) + blocks.append({"meta": dict(current_meta), "data_lines": current_data}) + current_data = [] + + current_data.append(ln) + last_time = t + in_data = True + + if in_data: + _finalize_block_meta(current_meta) + blocks.append({"meta": current_meta, "data_lines": current_data}) + + return blocks + + +def _validate_channel_consistency(blocks: list[dict[str, object]]) -> None: + """Raise FileParsingError if ``ChannelTitle`` differs between blocks.""" + ref = blocks[0]["meta"].get("ChannelTitle") + if ref is None: + return + for i, b in enumerate(blocks[1:], start=2): + ct = b["meta"].get("ChannelTitle") + if ct is not None and ct != ref: + raise FileParsingError( + "ChannelTitle differs between blocks " + f"(block 1: {ref}; block {i}: {ct}). " + "Cannot align channels into a single DataFrame." + ) + + +def _parse_block_data( + data_lines: list[str], + cols: list[str], + n_cols: int, +) -> pd.DataFrame: + """Bulk-parse one block's data lines into a DataFrame. + + Classification pass identifies full-width numeric rows, short comment + rows (``time + comment text``, narrower than a data row), and skips + out-of-place lines. The numeric portion is loaded with one + ``pd.read_csv`` call; ``pd.to_numeric(errors="coerce")`` per column + then turns any unparseable cell into ``NaN`` without dropping the row. + """ + numeric_lines: list[str] = [] + comments: list[str | None] = [] + + for ln in data_lines: + parts = ln.split("\t") + + if len(parts) < n_cols: + try: + float(parts[0]) + except ValueError: + continue + c = "\t".join(parts[1:]).strip() + if c.startswith("#*"): + c = c[2:].lstrip() + numeric_lines.append("\t".join([parts[0]] + [""] * (n_cols - 1))) + comments.append(c or None) + continue + + first_val = parts[0].strip() + if first_val in ("*", ""): + continue + try: + float(first_val) + except ValueError: + continue + + numeric_lines.append("\t".join(parts[:n_cols])) + extras = parts[n_cols:] + if extras: + c = "\t".join(extras).strip() + if c.startswith("#*"): + c = c[2:].lstrip() + comments.append(c or None) + else: + comments.append(None) + + if not numeric_lines: + return pd.DataFrame({c: [] for c in (*cols, "Comment")}) + + df = pd.read_csv( + io.StringIO("\n".join(numeric_lines)), + sep="\t", + header=None, + names=cols, + dtype=str, + na_values=["*"], + ) + for c in cols: + df[c] = pd.to_numeric(df[c], errors="coerce") + df["Comment"] = comments + return df + + +__all__ = ["parse_labchart_txt"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..8c28bb8 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test suite for the labchart_parser package.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e833818 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,17 @@ +"""Pytest configuration and shared fixtures for labchart_parser tests.""" + +from pathlib import Path + +import pytest + + +@pytest.fixture(scope="session") +def examples_data_dir(): + """Return the path to the examples/data directory.""" + return Path(__file__).parent.parent / "examples" / "data" + + +@pytest.fixture(scope="session") +def example_file_path(examples_data_dir): + """Return the path to the main example file.""" + return examples_data_dir / "labchart_file.example.txt" diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..f2f6f1e --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,217 @@ +"""Tests for the LabChartFile class in core module.""" + +from pathlib import Path + +import pandas as pd +import pytest + +from labchart_parser.core import LabChartFile +from labchart_parser.exceptions import InvalidChannelError + +# Path to the example data file +EXAMPLE_FILE = ( + Path(__file__).parent.parent / "examples" / "data" / "labchart_file.example.txt" +) + + +@pytest.fixture +def lab_file(): + """Fixture providing a loaded LabChartFile instance.""" + return LabChartFile.from_file(str(EXAMPLE_FILE)) + + +class TestLabChartFileLoading: + """Tests for LabChartFile loading and initialization.""" + + def test_from_file_returns_instance(self, lab_file): + """from_file should return a LabChartFile instance.""" + assert isinstance(lab_file, LabChartFile) + + def test_from_file_nonexistent_raises_error(self): + """from_file with non-existent file should raise FileNotFoundError.""" + with pytest.raises(FileNotFoundError): + LabChartFile.from_file("nonexistent_file.txt") + + +class TestLabChartFileProperties: + """Tests for LabChartFile properties.""" + + def test_metadata_is_dict(self, lab_file): + """metadata property should return a dictionary.""" + assert isinstance(lab_file.metadata, dict) + + def test_channels_is_list(self, lab_file): + """channels property should return a list.""" + assert isinstance(lab_file.channels, list) + + def test_channels_contains_strings(self, lab_file): + """channels should contain string channel names.""" + for ch in lab_file.channels: + assert isinstance(ch, str) + + def test_channels_excludes_system_columns(self, lab_file): + """channels should not include Time, block, time_abs, time_block, Comment.""" + system_cols = {"Time", "block", "time_abs", "time_block", "Comment"} + assert not any(ch in system_cols for ch in lab_file.channels) + + def test_blocks_is_list(self, lab_file): + """blocks property should return a list.""" + assert isinstance(lab_file.blocks, list) + + def test_blocks_contains_integers(self, lab_file): + """blocks should contain integer block numbers.""" + for b in lab_file.blocks: + assert isinstance(b, int) + + def test_canonical_fixture_has_multiple_blocks(self, lab_file): + """The canonical fixture is multi-block; regression guard for block detection.""" + assert len(lab_file.blocks) > 1 + + def test_comments_is_dataframe(self, lab_file): + """comments property should return a DataFrame.""" + assert isinstance(lab_file.comments, pd.DataFrame) + + def test_comments_has_required_columns(self, lab_file): + """comments DataFrame should have Time, time_block, time_abs, block, Comment columns.""" + required = {"Time", "time_block", "time_abs", "block", "Comment"} + assert required == set(lab_file.comments.columns) + + +class TestLabChartFileGetBlockDf: + """Tests for get_block_df method.""" + + def test_get_block_df_returns_dataframe(self, lab_file): + """get_block_df should return a DataFrame.""" + df = lab_file.get_block_df(1) + assert isinstance(df, pd.DataFrame) + + def test_get_block_df_contains_all_channels(self, lab_file): + """get_block_df should include all channel columns.""" + df = lab_file.get_block_df(1) + for ch in lab_file.channels: + assert ch in df.columns + + def test_get_block_df_filters_correct_block(self, lab_file): + """get_block_df should only return rows whose block matches the request.""" + df = lab_file.get_block_df(1) + assert len(df) > 0 + # Cross-reference against the underlying frame: same row count as block==1. + expected_len = int((lab_file._data["block"] == 1).sum()) + assert len(df) == expected_len + + def test_get_block_df_includes_time_columns(self, lab_file): + """get_block_df should include Time, time_block, time_abs columns.""" + df = lab_file.get_block_df(1) + assert "Time" in df.columns + assert "time_block" in df.columns + assert "time_abs" in df.columns + + +class TestLabChartFileGetChannel: + """Tests for get_channel method.""" + + def test_get_channel_returns_dataframe(self, lab_file): + """get_channel should return a DataFrame.""" + if lab_file.channels: + df = lab_file.get_channel(1, lab_file.channels[0]) + assert isinstance(df, pd.DataFrame) + + def test_get_channel_has_value_column(self, lab_file): + """get_channel should rename the channel column to 'value'.""" + if lab_file.channels: + df = lab_file.get_channel(1, lab_file.channels[0]) + assert "value" in df.columns + + def test_get_channel_invalid_raises_error(self, lab_file): + """get_channel with invalid channel name should raise InvalidChannelError.""" + with pytest.raises(InvalidChannelError): + lab_file.get_channel(1, "NonExistentChannel") + + def test_get_channel_includes_time_columns(self, lab_file): + """get_channel should include Time, time_block, time_abs, Comment columns.""" + if lab_file.channels: + df = lab_file.get_channel(1, lab_file.channels[0]) + assert "Time" in df.columns + assert "time_block" in df.columns + assert "time_abs" in df.columns + assert "Comment" in df.columns + + +class TestLabChartFileSliceTimeAbs: + """Tests for slice_time_abs method.""" + + def test_slice_time_abs_returns_dataframe(self, lab_file): + """slice_time_abs should return a DataFrame.""" + df = lab_file.slice_time_abs(0.0, 1.0) + assert isinstance(df, pd.DataFrame) + + def test_slice_time_abs_filters_correctly(self, lab_file): + """slice_time_abs should only include rows within the time range.""" + df = lab_file.slice_time_abs(0.0, 1.0) + if len(df) > 0: + assert df["time_abs"].min() >= 0.0 + assert df["time_abs"].max() <= 1.0 + + def test_slice_time_abs_empty_range_returns_empty(self, lab_file): + """slice_time_abs with out-of-range times should return empty DataFrame.""" + df = lab_file.slice_time_abs(999999.0, 999999.1) + assert len(df) == 0 + + +class TestLabChartFileGetBlockCommentsExcluding: + """Tests for get_block_comments_excluding method.""" + + def test_returns_list(self, lab_file): + """get_block_comments_excluding should return a list.""" + result = lab_file.get_block_comments_excluding(1, []) + assert isinstance(result, list) + + def test_excludes_specified_values(self, lab_file): + """get_block_comments_excluding should exclude specified values.""" + # Get all comments first + all_comments = lab_file.get_block_comments_excluding(1, []) + if all_comments: + # Exclude the first comment + excluded = lab_file.get_block_comments_excluding(1, [all_comments[0]]) + assert all_comments[0] not in excluded + + def test_case_insensitive_exclusion(self, lab_file): + """Exclusion should be case-insensitive.""" + all_comments = lab_file.get_block_comments_excluding(1, []) + if all_comments: + # Try excluding with different case + first_upper = all_comments[0].upper() + excluded = lab_file.get_block_comments_excluding(1, [first_upper]) + # The original comment should not be in the result + assert all_comments[0].strip().casefold() not in [ + c.strip().casefold() for c in excluded + ] + + +class TestLabChartFilePlotChannel: + """Tests for plot_channel method (uses the non-interactive Agg backend).""" + + def test_plot_channel_returns_axes(self, lab_file): + matplotlib = pytest.importorskip("matplotlib") + matplotlib.use("Agg", force=True) + from matplotlib.axes import Axes + + ax = lab_file.plot_channel(lab_file.channels[0], block=1) + assert isinstance(ax, Axes) + + def test_plot_channel_invalid_raises(self, lab_file): + pytest.importorskip("matplotlib") + from labchart_parser.exceptions import InvalidChannelError + + with pytest.raises(InvalidChannelError): + lab_file.plot_channel("NonExistentChannel", block=1) + + +class TestLabChartFilePathInput: + """Test that the API accepts Path-like inputs (not just str).""" + + def test_from_file_accepts_path(self): + from pathlib import Path + + lc = LabChartFile.from_file(Path(EXAMPLE_FILE)) + assert isinstance(lc, LabChartFile) diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..2bc0141 --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,355 @@ +"""Tests for the low-level parser module.""" + +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from labchart_parser.exceptions import FileParsingError +from labchart_parser.parser import parse_labchart_txt + +# Path to the example data file +EXAMPLE_FILE = ( + Path(__file__).parent.parent / "examples" / "data" / "labchart_file.example.txt" +) +EXAMPLE_FILE_NEG_TIME = ( + Path(__file__).parent.parent / "examples" / "data" / "labchart_file_negTime.txt" +) + + +class TestParseLabchartTxt: + """Tests for parse_labchart_txt function.""" + + def test_parse_returns_dataframe_and_dict(self): + """parse_labchart_txt should return a tuple of (DataFrame, dict).""" + df, meta = parse_labchart_txt(str(EXAMPLE_FILE)) + assert isinstance(df, pd.DataFrame) + assert isinstance(meta, dict) + + def test_dataframe_has_required_columns(self): + """Parsed DataFrame should have Time, block, time_abs, time_block, Comment columns.""" + df, _ = parse_labchart_txt(str(EXAMPLE_FILE)) + required_cols = {"Time", "block", "time_abs", "time_block", "Comment"} + assert required_cols.issubset(set(df.columns)) + + def test_dataframe_has_channel_columns(self): + """Parsed DataFrame should have at least one channel column.""" + df, _ = parse_labchart_txt(str(EXAMPLE_FILE)) + channel_cols = [ + c + for c in df.columns + if c not in ("Time", "time_block", "time_abs", "block", "Comment") + ] + assert len(channel_cols) > 0 + + def test_block_column_is_integer(self): + """Block column should contain integers.""" + df, _ = parse_labchart_txt(str(EXAMPLE_FILE)) + assert pd.api.types.is_integer_dtype(df["block"]) + + def test_time_columns_are_float(self): + """Time columns should be numeric (float).""" + df, _ = parse_labchart_txt(str(EXAMPLE_FILE)) + assert pd.api.types.is_float_dtype(df["Time"]) + assert pd.api.types.is_float_dtype(df["time_abs"]) + assert pd.api.types.is_float_dtype(df["time_block"]) + + def test_metadata_contains_interval(self): + """Metadata should contain Interval if present in file.""" + _, meta = parse_labchart_txt(str(EXAMPLE_FILE)) + # Interval is typically present in LabChart exports + assert "Interval" in meta or "Interval_s" in meta + + def test_time_block_starts_at_zero_for_each_block(self): + """time_block should start at 0.0 for each block.""" + df, _ = parse_labchart_txt(str(EXAMPLE_FILE)) + for block in df["block"].unique(): + block_df = df[df["block"] == block] + assert block_df["time_block"].iloc[0] == pytest.approx(0.0, abs=1e-9) + + def test_time_abs_is_strictly_monotonic(self): + """time_abs must be strictly increasing — no duplicates at block boundaries.""" + df, _ = parse_labchart_txt(str(EXAMPLE_FILE)) + time_abs = df["time_abs"].to_numpy() + diffs = np.diff(time_abs) + assert np.all(diffs > 0), ( + "time_abs has non-strict steps " + f"(min diff = {diffs.min()}); block-boundary stitching may have regressed" + ) + + def test_file_not_found_raises_error(self): + """Parsing a non-existent file should raise an error.""" + with pytest.raises(FileNotFoundError): + parse_labchart_txt("non_existent_file.txt") + + def test_negative_time_file_parses_correctly(self): + """File with negative time values should parse without error.""" + if EXAMPLE_FILE_NEG_TIME.exists(): + df, meta = parse_labchart_txt(str(EXAMPLE_FILE_NEG_TIME)) + assert isinstance(df, pd.DataFrame) + assert len(df) > 0 + + +class TestParserEdgeCases: + """Edge case tests for the parser.""" + + def test_empty_comments_are_none(self): + """Rows without comments should have Comment as None or NaN.""" + df, _ = parse_labchart_txt(str(EXAMPLE_FILE)) + # At least some rows should have no comment + assert df["Comment"].isna().any() + + def test_blocks_are_one_indexed(self): + """Block numbers should start at 1.""" + df, _ = parse_labchart_txt(str(EXAMPLE_FILE)) + assert df["block"].min() == 1 + + def test_accepts_path_object(self): + """parse_labchart_txt should accept a Path, not just a string.""" + df, _ = parse_labchart_txt(EXAMPLE_FILE) + assert isinstance(df, pd.DataFrame) + + +class TestParserErrorPaths: + """Tests for the parser's error paths.""" + + def test_unsupported_extension_raises(self, tmp_path): + bad = tmp_path / "recording.csv" + bad.write_text("Time\tCh1\n0.0\t1.0\n") + with pytest.raises(FileParsingError, match="Unsupported file extension"): + parse_labchart_txt(str(bad)) + + def test_empty_file_raises(self, tmp_path): + empty = tmp_path / "empty.txt" + empty.write_text("") + with pytest.raises(FileParsingError, match="empty"): + parse_labchart_txt(str(empty)) + + def test_no_data_section_raises(self, tmp_path): + header_only = tmp_path / "header_only.txt" + header_only.write_text( + "Interval=\t0.001 s\n" "ChannelTitle=\tFlow\n" "UnitName=\tL/s\n" + ) + with pytest.raises(FileParsingError, match="Data start not found"): + parse_labchart_txt(str(header_only)) + + def test_single_column_raises(self, tmp_path): + single_col = tmp_path / "single.txt" + single_col.write_text("0.0\n0.001\n0.002\n") + with pytest.raises(FileParsingError, match="at least Time"): + parse_labchart_txt(str(single_col)) + + def test_unparseable_channel_becomes_nan(self, tmp_path): + """Per-cell parse failures yield NaN; rows are not silently dropped (A2).""" + path = tmp_path / "bad_cells.txt" + path.write_text( + "Interval=\t0.001 s\n" + "ChannelTitle=\tFlow\n" + "+0.000\toops\n" + "+0.001\toops\n" + ) + df, _ = parse_labchart_txt(str(path)) + assert len(df) == 2 + # Time column survives, channel column is NaN, Comment stays None + # (the bad cell is no longer promoted into Comment). + assert df["Time"].tolist() == [0.0, 0.001] + assert df["Flow"].isna().all() + assert df["Comment"].isna().all() + + def test_mid_row_bad_cell_preserves_other_channels(self, tmp_path): + """A bad cell in a multi-channel row only NaNs that cell, not the row (A2).""" + path = tmp_path / "mid_bad.txt" + path.write_text( + "Interval=\t0.001 s\n" + "ChannelTitle=\tFlow\tPressure\tEMG\n" + "0.000\t1.0\t2.0\t3.0\n" + "0.001\t1.5\tBAD\t3.5\n" + "0.002\t2.0\t4.0\t5.0\n" + ) + df, _ = parse_labchart_txt(str(path)) + assert len(df) == 3 + # Pressure on row 1 should be NaN; the surrounding Flow / EMG must survive. + assert df["Flow"].tolist() == [1.0, 1.5, 2.0] + assert df["EMG"].tolist() == [3.0, 3.5, 5.0] + assert df["Pressure"].iloc[0] == 2.0 + assert pd.isna(df["Pressure"].iloc[1]) + assert df["Pressure"].iloc[2] == 4.0 + + +class TestParserBlockBoundaries: + """A1: time_abs must advance by one sample interval at each block boundary.""" + + def test_block_boundary_advances_by_interval(self, tmp_path): + path = tmp_path / "two_block.txt" + # Two blocks, 0.001 s sample interval, 3 samples each. + # Block 1: t=0.000, 0.001, 0.002 ; Block 2: t=0.000, 0.001, 0.002 . + path.write_text( + "Interval=\t0.001 s\n" + "TimeFormat=\tStartOfBlock\n" + "ChannelTitle=\tFlow\n" + "0.000\t1.0\n" + "0.001\t2.0\n" + "0.002\t3.0\n" + "0.000\t4.0\n" + "0.001\t5.0\n" + "0.002\t6.0\n" + ) + df, meta = parse_labchart_txt(str(path)) + assert len(df["block"].unique()) == 2 + time_abs = df["time_abs"].to_numpy() + # Strictly monotonic, including at the block boundary. + assert np.all(np.diff(time_abs) > 0) + # First sample of block 2 = last sample of block 1 + interval. + assert time_abs[3] == pytest.approx(time_abs[2] + meta["Interval_s"], abs=1e-12) + + +class TestParserEncoding: + """B1: utf-8 first, latin-1 fallback. Bytes must not be silently dropped.""" + + def test_latin1_fallback_preserves_french_comment(self, tmp_path): + """A cp1252-encoded file containing 'Inspiration forcée' must round-trip.""" + path = tmp_path / "fr.txt" + body = ( + "Interval=\t0.001 s\n" + "ChannelTitle=\tFlow\n" + "0.000\t1.0\n" + "0.001\t2.0\tInspiration forcée\n" + "0.002\t3.0\n" + ) + # Write as cp1252 — utf-8 decoding will raise UnicodeDecodeError on 'é'. + path.write_bytes(body.encode("cp1252")) + df, _ = parse_labchart_txt(str(path)) + comments = df["Comment"].dropna().tolist() + assert "Inspiration forcée" in comments + + +class TestParserTimeFormatWarning: + """B2: warn when TimeFormat is not StartOfBlock (block detection degrades).""" + + def test_continuous_time_format_emits_warning(self, tmp_path): + path = tmp_path / "continuous.txt" + path.write_text( + "Interval=\t0.001 s\n" + "TimeFormat=\tContinuous\n" + "ChannelTitle=\tFlow\n" + "0.000\t1.0\n" + "0.001\t2.0\n" + ) + with pytest.warns(UserWarning, match="StartOfBlock"): + parse_labchart_txt(str(path)) + + def test_start_of_block_does_not_warn(self, tmp_path): + path = tmp_path / "sob.txt" + path.write_text( + "Interval=\t0.001 s\n" + "TimeFormat=\tStartOfBlock\n" + "ChannelTitle=\tFlow\n" + "0.000\t1.0\n" + "0.001\t2.0\n" + ) + with warnings.catch_warnings(): + warnings.simplefilter("error") # any warning becomes a failure + parse_labchart_txt(str(path)) + + +class TestParserMetaBlocks: + """A3: per-block metadata is exposed under meta["blocks"].""" + + def test_canonical_fixture_exposes_per_block_metadata(self): + """The shipped multi-block fixture has 6 metadata sections (verified + via ``grep -c ^Interval= examples/data/labchart_file.example.txt``).""" + _, meta = parse_labchart_txt(str(EXAMPLE_FILE)) + assert isinstance(meta["blocks"], list) + assert len(meta["blocks"]) == 6 + for entry in meta["blocks"]: + assert "Interval" in entry + assert "Interval_s" in entry + # Per-block list metadata should also be preserved. + assert "Range" in entry + + def test_per_block_intervals_when_blocks_differ(self, tmp_path): + """Each metadata section's Interval is preserved per-block.""" + path = tmp_path / "mixed.txt" + # Two blocks, with different sample intervals declared per section. + path.write_text( + "Interval=\t0.001 s\n" + "TimeFormat=\tStartOfBlock\n" + "ChannelTitle=\tFlow\n" + "0.000\t1.0\n" + "0.001\t2.0\n" + "0.002\t3.0\n" + "Interval=\t0.010 s\n" + "TimeFormat=\tStartOfBlock\n" + "ChannelTitle=\tFlow\n" + "0.000\t4.0\n" + "0.010\t5.0\n" + "0.020\t6.0\n" + ) + df, meta = parse_labchart_txt(str(path)) + assert len(meta["blocks"]) == 2 + assert meta["blocks"][0]["Interval_s"] == pytest.approx(0.001) + assert meta["blocks"][1]["Interval_s"] == pytest.approx(0.010) + # time_abs stitching uses each block's own interval at the boundary. + time_abs = df["time_abs"].to_numpy() + assert time_abs[3] == pytest.approx(time_abs[2] + 0.001, abs=1e-12) + + def test_time_jump_within_single_metadata_section_creates_block(self, tmp_path): + """One header section but two time-reset runs → two blocks, shared meta.""" + path = tmp_path / "single_section.txt" + path.write_text( + "Interval=\t0.001 s\n" + "TimeFormat=\tStartOfBlock\n" + "ChannelTitle=\tFlow\n" + "0.000\t1.0\n" + "0.001\t2.0\n" + "0.000\t3.0\n" + "0.001\t4.0\n" + ) + df, meta = parse_labchart_txt(str(path)) + assert len(meta["blocks"]) == 2 + assert df["block"].unique().tolist() == [1, 2] + # Both blocks share the same Interval since metadata wasn't re-emitted. + assert ( + meta["blocks"][0]["Interval"] == meta["blocks"][1]["Interval"] == "0.001 s" + ) + + def test_channel_title_mismatch_between_blocks_raises(self, tmp_path): + """Different ChannelTitle per block produces a misaligned DataFrame.""" + path = tmp_path / "mismatch.txt" + path.write_text( + "Interval=\t0.001 s\n" + "ChannelTitle=\tFlow\tPressure\n" + "0.000\t1.0\t2.0\n" + "Interval=\t0.001 s\n" + "ChannelTitle=\tEMG\tVolume\n" + "0.000\t3.0\t4.0\n" + ) + with pytest.raises(FileParsingError, match="ChannelTitle differs"): + parse_labchart_txt(str(path)) + + +class TestParserBulk: + """B4: smoke-test the bulk read_csv path on a moderately large file. + + No timing assertion (flaky in CI). Just confirms shape and block id.""" + + def test_large_synthetic_file_parses(self, tmp_path): + rows = 50_000 + path = tmp_path / "big.txt" + with path.open("w") as f: + f.write( + "Interval=\t0.001 s\n" + "TimeFormat=\tStartOfBlock\n" + "ChannelTitle=\tA\tB\tC\tD\tE\n" + ) + for i in range(rows): + f.write(f"{i * 0.001:.6f}\t1.0\t2.0\t3.0\t4.0\t5.0\n") + df, meta = parse_labchart_txt(str(path)) + assert df.shape == ( + rows, + 1 + 5 + 1 + 2 + 1, + ) # Time+5ch+Comment+block+time_abs+time_block + assert df["block"].unique().tolist() == [1] + assert meta["Interval_s"] == pytest.approx(0.001)