delsys 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. delsys-0.1.0/.github/workflows/tests.yml +35 -0
  2. delsys-0.1.0/.gitignore +52 -0
  3. delsys-0.1.0/.readthedocs.yaml +29 -0
  4. delsys-0.1.0/CHANGELOG.md +101 -0
  5. delsys-0.1.0/LICENSE +21 -0
  6. delsys-0.1.0/PKG-INFO +173 -0
  7. delsys-0.1.0/README.md +138 -0
  8. delsys-0.1.0/TODO.md +88 -0
  9. delsys-0.1.0/docs/Makefile +20 -0
  10. delsys-0.1.0/docs/api.md +56 -0
  11. delsys-0.1.0/docs/conf.py +71 -0
  12. delsys-0.1.0/docs/index.md +10 -0
  13. delsys-0.1.0/docs/make.bat +35 -0
  14. delsys-0.1.0/docs/requirements.txt +4 -0
  15. delsys-0.1.0/examples/delsys_channelmap.txt +22 -0
  16. delsys-0.1.0/pyproject.toml +92 -0
  17. delsys-0.1.0/scripts/make_fixture.py +155 -0
  18. delsys-0.1.0/src/delsys/__init__.py +78 -0
  19. delsys-0.1.0/src/delsys/_constants.py +67 -0
  20. delsys-0.1.0/src/delsys/_metadata.py +27 -0
  21. delsys-0.1.0/src/delsys/_parse.py +648 -0
  22. delsys-0.1.0/src/delsys/_util.py +42 -0
  23. delsys-0.1.0/src/delsys/ekg.py +280 -0
  24. delsys-0.1.0/src/delsys/emg.py +357 -0
  25. delsys-0.1.0/src/delsys/log.py +633 -0
  26. delsys-0.1.0/src/delsys/sensor.py +126 -0
  27. delsys-0.1.0/src/delsys/signals.py +230 -0
  28. delsys-0.1.0/tests/__init__.py +0 -0
  29. delsys-0.1.0/tests/conftest.py +41 -0
  30. delsys-0.1.0/tests/fixtures/.gitkeep +0 -0
  31. delsys-0.1.0/tests/fixtures/discover142.csv +256 -0
  32. delsys-0.1.0/tests/fixtures/discover150.csv +257 -0
  33. delsys-0.1.0/tests/fixtures/discover164_basic.csv +257 -0
  34. delsys-0.1.0/tests/fixtures/discover164_link.csv +257 -0
  35. delsys-0.1.0/tests/fixtures/discover164_mvc.csv +507 -0
  36. delsys-0.1.0/tests/fixtures/discover170.csv +207 -0
  37. delsys-0.1.0/tests/fixtures/emgworks.csv +201 -0
  38. delsys-0.1.0/tests/test_ekg.py +144 -0
  39. delsys-0.1.0/tests/test_emg.py +152 -0
  40. delsys-0.1.0/tests/test_log.py +193 -0
  41. delsys-0.1.0/tests/test_parse.py +201 -0
  42. delsys-0.1.0/tests/test_signals.py +171 -0
@@ -0,0 +1,35 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+ types: [opened, synchronize, reopened]
7
+
8
+ jobs:
9
+ test:
10
+ name: pytest (Python ${{ matrix.python-version }} on ${{ matrix.platform }})
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12"]
15
+ platform: [ubuntu-22.04, macos-latest, windows-latest]
16
+ runs-on: ${{ matrix.platform }}
17
+ timeout-minutes: 30
18
+
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Set up Python ${{ matrix.python-version }}
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+ cache: pip
28
+
29
+ - name: Install package + dev deps
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ python -m pip install -e ".[dev]"
33
+
34
+ - name: Run pytest
35
+ run: pytest
@@ -0,0 +1,52 @@
1
+ # Byte-compiled / optimized
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # Test / coverage
18
+ .pytest_cache/
19
+ .coverage
20
+ .coverage.*
21
+ htmlcov/
22
+ .tox/
23
+ .hypothesis/
24
+
25
+ # Editor / IDE
26
+ .vscode/
27
+ .idea/
28
+ *.swp
29
+ *.swo
30
+ .DS_Store
31
+
32
+ # Type-checker caches
33
+ .mypy_cache/
34
+ .ruff_cache/
35
+
36
+ # Sphinx build artifacts
37
+ docs/_build/
38
+
39
+ # Generated by Delsys side-channel files (not part of fixtures)
40
+ *_dropped_samples.txt
41
+ *_ekginfo.json
42
+ *_ica_settings.json
43
+
44
+ # Local sample data (reference files; not committed)
45
+ _data/
46
+ samples/
47
+
48
+ # Transient resume notes from assistant sessions
49
+ RESUME.md
50
+
51
+ # Maintainer-only release runbook
52
+ PUBLISH.md
@@ -0,0 +1,29 @@
1
+ # Read the Docs configuration file for Sphinx projects
2
+ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3
+
4
+ # Required
5
+ version: 2
6
+
7
+ # Set the OS, Python version and other tools you might need
8
+ build:
9
+ os: ubuntu-22.04
10
+ tools:
11
+ python: "3.12"
12
+
13
+ # Build documentation in the "docs/" directory with Sphinx
14
+ sphinx:
15
+ configuration: docs/conf.py
16
+
17
+ # Optionally build your docs in additional formats such as PDF and ePub
18
+ formats:
19
+ - pdf
20
+ - epub
21
+
22
+ # Optional but recommended, declare the Python requirements required
23
+ # to build your documentation
24
+ # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
25
+ python:
26
+ install:
27
+ - requirements: docs/requirements.txt
28
+ - method: pip
29
+ path: .
@@ -0,0 +1,101 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ First public release. The package is a standalone extraction and polish of the
11
+ Delsys CSV loader that previously lived in
12
+ `immersionToolbox/immersionlab/delsys.py` for several years. The
13
+ `immersionlab.delsys` module is now a thin shim that re-exports `delsys`, so
14
+ existing callers continue to work without code changes.
15
+
16
+ ### Added
17
+
18
+ - Modular package layout under `src/delsys/`: `log`, `sensor`, `signals`,
19
+ `emg`, `ekg`, plus internal `_parse`, `_metadata`, `_constants`, `_util`.
20
+ - `Log.find()` — typed query API for sensors, modality bundles, or raw
21
+ signals, with filters by modality / side / location / sensor number / name.
22
+ - Direct typed accessors on `Log`: `lf.emg`, `lf.ekg`, `lf.acc`, `lf.gyro`,
23
+ `lf.fsr`, `lf.analog`, `lf.vo2master`, `lf.hrstrap`. Side accessors
24
+ `lf.left` / `lf.right` / `lf.center` return lists of `Sensor`.
25
+ - `EMG.rms()` — clean RMS amplitude pipeline (shift_baseline → highpass →
26
+ lowpass → notch → running RMS) producing an envelope at the requested
27
+ output sampling rate.
28
+ - `Log.add_sensor_group()` for user-defined groupings.
29
+ - `_metadata.py` module holding the three foundational namedtuples
30
+ (`SensorLog`, `SensorInfo`, `SigInfoDelsys`) — extracted to break the
31
+ `signals.py` ↔ `sensor.py` import cycle.
32
+ - Sensor metadata is now stored in `pysampled.Data.meta['sensor']` and
33
+ propagates automatically through filtering, slicing, and resampling.
34
+ - Type hints (PEP 484) on every public function/method signature.
35
+ - Google-style docstrings throughout, ready for Sphinx + napoleon.
36
+ - Test suite: 94 tests across `test_parse.py`, `test_log.py`,
37
+ `test_signals.py`, `test_emg.py`, `test_ekg.py` with fixtures for all
38
+ five header formats (EMGworks, Discover 1.4.2 / 1.5.0 / 1.6.4 / 1.7.0).
39
+ - `scripts/make_fixture.py` for trimming real CSVs into committable fixtures.
40
+ - Standalone PyPI-publishable package with `pyproject.toml`.
41
+
42
+ ### Changed
43
+
44
+ - **Minimum Python is now 3.10** (was 3.9). Bumped because a required
45
+ dependency (`neurokit2`) ships PEP 604 union syntax (``X | None``) at
46
+ module-import time, which fails on 3.9. Python 3.9 itself reached
47
+ end-of-life in October 2025.
48
+ - **Bug fix:** `VO2Master` column ordering — all 8 channel properties used
49
+ to map to column 1; now correctly map to columns 0..7 per
50
+ `SUBCHANNEL_MAP['VO2']`.
51
+ - **Bug fix:** `_parse_sig_name` raises `ValueError` for an unknown modality
52
+ instead of silently `print`ing and then raising a less-helpful `KeyError`
53
+ downstream.
54
+ - **Bug fix:** `_detect_parser` raises `ValueError` instead of bare
55
+ `Exception` when link data is exported without time-series columns.
56
+ - **Bug fix:** Frame-count invariant in the Discover parsers uses explicit
57
+ `if not ...: raise ValueError(...)` (was `assert`, which is disabled by
58
+ `python -O`).
59
+ - **Bug fix:** VO2 / HR detection in `_parse_sig_name_discover` uses
60
+ `if`/`elif`/`else` (was two `if`s, with the second checking `'HR'`
61
+ substring rather than `'HR Strap'` — could mis-trigger).
62
+ - **Bug fix:** `EMG.process()` preserves sensor metadata on the result
63
+ (was lost via direct `self.__class__(...)` construction).
64
+ - **Bug fix:** `EMG.process()` no longer mutates `self._history` in place;
65
+ history is built fresh on the new instance.
66
+ - **Bug fix:** `EMG.tkeo()` history entry is a single tuple (was a
67
+ list-containing-tuple).
68
+ - O(N×M) sensor lookup inside the per-channel parser loops replaced with a
69
+ pre-built `{sensor_number: SensorInfo}` dict (O(1) per channel).
70
+ - All regexes hoisted to module-level constants in `_parse.py`.
71
+ - Build backend: standalone `pyproject.toml` (no longer dependent on
72
+ `immersionToolbox`'s build setup).
73
+ - Dependencies trimmed: only `pysampled`, `numpy`, `scipy`, `pandas`,
74
+ `matplotlib`, `scikit-learn`, `heartpy`, `neurokit2`. No
75
+ `immersionlab.utils`, no `pntools.sampled`.
76
+
77
+ ### Removed
78
+
79
+ - `DataMod` base class — modality classes (`Signal`, `IMU`, `FSR`,
80
+ `VO2Master`, `EMG`, `EKG`) now inherit from `pysampled.Data` directly.
81
+ `envelope2` was upstreamed to pysampled in the same release window.
82
+ - `ica.py` (the accelerometer-impact ICA cleaning utility) — its scope
83
+ didn't match the package's primary purpose (EMG-from-EKG cleaning).
84
+ A `Log`-integrated EMG/EKG cleaning method is planned for a follow-up
85
+ release; see `TODO.md` for the design questions.
86
+ - `decreturn` decorator and the `to=` parameter from `EMG.process_nk`,
87
+ `EMG.get_features_nk`, `EMG.get_features`, `EKG.process_nk`,
88
+ `EKG.get_features_hp`. These methods now return a plain `dict`; callers
89
+ wrap with `pd.DataFrame(...)` if they want a tabular result.
90
+ - `EKG.find_rpeaks_hp`, `EKG.find_rpeaks_nk`, `EKG.clean_rpeaks`,
91
+ `EKG.hrv`, `EKG.find_rr`, `EKG.find_rr_nk`, `EKG._get_sav_name` —
92
+ alternative R-peak back-ends, the manual JSON-annotation workflow, and
93
+ related downstream methods. `EKG.find_rpeaks_pn` (alias `find_rpeaks`)
94
+ remains as the canonical detector.
95
+ - `if __name__ == '__main__'` smoke-test block with hard-coded UNC paths
96
+ from the original monolithic file.
97
+
98
+ ### Provenance
99
+
100
+ Extracted from `C:/dev/immersionToolbox/immersionlab/delsys.py` (1316 lines
101
+ monolithic). Built on top of [pysampled](https://github.com/praneethnamburi/pysampled).
delsys-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Praneeth Namburi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
delsys-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: delsys
3
+ Version: 0.1.0
4
+ Summary: Load and analyze Delsys CSV exports as ``pysampled.Data`` time series.
5
+ Author-email: Praneeth Namburi <praneeth.namburi@gmail.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering
17
+ License-File: LICENSE
18
+ Requires-Dist: numpy
19
+ Requires-Dist: scipy
20
+ Requires-Dist: pandas
21
+ Requires-Dist: matplotlib
22
+ Requires-Dist: scikit-learn
23
+ Requires-Dist: heartpy
24
+ Requires-Dist: neurokit2
25
+ Requires-Dist: pysampled>=1.1.1
26
+ Requires-Dist: pytest>=7 ; extra == "dev"
27
+ Requires-Dist: pytest-cov ; extra == "dev"
28
+ Requires-Dist: black ; extra == "dev"
29
+ Requires-Dist: isort ; extra == "dev"
30
+ Project-URL: Documentation, https://delsys.readthedocs.io
31
+ Project-URL: Homepage, https://github.com/praneethnamburi/delsys
32
+ Project-URL: Issues, https://github.com/praneethnamburi/delsys/issues
33
+ Provides-Extra: dev
34
+
35
+ # delsys
36
+
37
+ [![src](https://img.shields.io/badge/src-github-blue)](https://github.com/praneethnamburi/delsys)
38
+ [![PyPI - Version](https://img.shields.io/pypi/v/delsys.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/delsys/)
39
+ [![Documentation Status](https://readthedocs.org/projects/delsys/badge/?version=latest)](https://delsys.readthedocs.io)
40
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/praneethnamburi/delsys/main/LICENSE)
41
+
42
+ *Load Delsys CSV exports into Python as `pysampled.Data` time series.*
43
+
44
+ `delsys` reads CSV files exported from EMGworks and Trigno Discover, normalizes their
45
+ many per-format quirks (header layouts, sub-channel orderings, link-device
46
+ asynchrony), resamples each channel to a configurable per-modality target
47
+ rate, and groups the result into per-sensor modality bundles (EMG, EKG,
48
+ IMU, FSR, VO2 Master, HR Strap, Analog) ready for analysis.
49
+
50
+ ## Installation
51
+
52
+ ```sh
53
+ pip install delsys
54
+ ```
55
+
56
+ For local development:
57
+
58
+ ```sh
59
+ git clone https://github.com/praneethnamburi/delsys
60
+ pip install -e "./delsys[dev]"
61
+ ```
62
+
63
+ ## Quickstart
64
+
65
+ ```python
66
+ import delsys
67
+
68
+ lf = delsys.Log("path/to/Trial.csv", sensor_map="path/to/delsys_channelmap.txt")
69
+
70
+ # Direct accessors — list of typed bundles, one per sensor with that modality.
71
+ lf.emg # list of EMG bundles
72
+ lf.ekg # list of EKG bundles
73
+ lf.acc, lf.gyro # tri-axial IMU bundles
74
+ lf.fsr # 4-channel FSR bundles
75
+ lf.analog # raw Analog signals
76
+ lf.vo2master # VO2 Master link device (8 channels)
77
+ lf.hrstrap # HR Strap link device
78
+
79
+ # Side accessors return whole Sensor objects.
80
+ lf.left, lf.right, lf.center
81
+
82
+ # Filtered queries.
83
+ lf.find(modality="EMG", side="R") # right-side EMG bundles
84
+ lf.find(location="Forearm") # any sensor at "Forearm"
85
+ lf.find(sensor_number=5)
86
+ lf.find(modality="EMG", as_="signal") # raw per-channel Signal objects
87
+
88
+ # A typical EMG envelope pipeline.
89
+ for emg in lf.emg:
90
+ envelope = emg.process(amp_kind="envelope2")
91
+ rms = emg.rms(envelope_sr=240) # clean RMS amplitude pipeline
92
+ ```
93
+
94
+ See the full API reference at <https://delsys.readthedocs.io>.
95
+
96
+ ## Channelmap files (optional)
97
+
98
+ When you pass `sensor_map="path/to/delsys_channelmap.txt"` to `Log()`, that
99
+ file labels each sensor number with a sensor type and a body-location tag.
100
+ This lets you query by side (`lf.find(side="R")`) and by location
101
+ (`lf.find(location="Forearm")`).
102
+
103
+ The format is one sensor per line, three fields separated by `" - "`:
104
+
105
+ ```
106
+ Ch 1 - EMG - LBicep
107
+ Ch 2 - EMG - RBicep
108
+ Ch 11 - EKG - Chest
109
+ Ch 12 - Sync - Optitrack Recording Gate
110
+ Ch 19 - Quattro - LForearmExtensors (A-Index, B-Middle, C-Ring, D-Little)
111
+ Ch 21 - FSR - LFoot (1-Heel, 2-OuterEdge, 3-Ball, 4-Toe)
112
+ ```
113
+
114
+ - **Field 1**: any text whose last whitespace-token is the sensor's channel
115
+ number (`Ch 1`, `Channel 01`, `1` all work).
116
+ - **Field 2**: a type tag (free text — common values: `EMG`, `Quattro`,
117
+ `Snap`, `EKG`, `FSR`, `Sync`).
118
+ - **Field 3**: a location label. Its *first character* is interpreted as the
119
+ side (`L`/`R`/`C` for left/right/center). Anything else still loads but
120
+ won't match `lf.find(side=...)`.
121
+
122
+ Trailing parenthetical notes are informational only — they remain in
123
+ `location` but the parser doesn't extract sub-channel labels from them.
124
+ Blank lines and lines without two `" - "` separators are silently skipped.
125
+
126
+ A more comprehensive reference file lives at
127
+ [`examples/delsys_channelmap.txt`](https://github.com/praneethnamburi/delsys/blob/main/examples/delsys_channelmap.txt).
128
+
129
+ ## Supported export formats
130
+
131
+ - EMGworks
132
+ - Trigno Discover 1.4.2
133
+ - Trigno Discover 1.5.0
134
+ - Trigno Discover 1.6.4 (with and without link devices)
135
+ - Trigno Discover 1.7.0
136
+
137
+ ## Supported sensors
138
+
139
+ EMG (Avanti single, Duo, Quattro, Snap-Lead), EKG, ACC, GYRO, FSR, Analog,
140
+ VO2 Master (link), HR Strap (link).
141
+
142
+ ## Scope and contributions
143
+
144
+ Supported sensor types are limited to those the maintainer has access to.
145
+ Delsys ships other hardware (e.g. SmO2/Thb appear as stubs in `TARGET_SR`
146
+ but are not exercised end-to-end). Contributions adding parsers and tests
147
+ for additional sensors are very welcome.
148
+
149
+ If you'd like to contribute, the dev install above pulls `pytest`, `black`,
150
+ and `isort`. Format with `black` and `isort` before opening a PR:
151
+
152
+ ```sh
153
+ isort src/ tests/ scripts/
154
+ black src/ tests/ scripts/
155
+ pytest
156
+ ```
157
+
158
+ ## License
159
+
160
+ Distributed under the MIT License. See [LICENSE](https://github.com/praneethnamburi/delsys/blob/main/LICENSE) for details.
161
+
162
+ ## Contact
163
+
164
+ [Praneeth Namburi](https://praneethnamburi.com)
165
+
166
+ Project link: <https://github.com/praneethnamburi/delsys>
167
+
168
+ ## Acknowledgments
169
+
170
+ This package was developed as part of the ImmersionToolbox initiative at the
171
+ [MIT.nano Immersion Lab](https://immersion.mit.edu). Thanks to NCSOFT for
172
+ supporting this initiative.
173
+
delsys-0.1.0/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # delsys
2
+
3
+ [![src](https://img.shields.io/badge/src-github-blue)](https://github.com/praneethnamburi/delsys)
4
+ [![PyPI - Version](https://img.shields.io/pypi/v/delsys.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/delsys/)
5
+ [![Documentation Status](https://readthedocs.org/projects/delsys/badge/?version=latest)](https://delsys.readthedocs.io)
6
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/praneethnamburi/delsys/main/LICENSE)
7
+
8
+ *Load Delsys CSV exports into Python as `pysampled.Data` time series.*
9
+
10
+ `delsys` reads CSV files exported from EMGworks and Trigno Discover, normalizes their
11
+ many per-format quirks (header layouts, sub-channel orderings, link-device
12
+ asynchrony), resamples each channel to a configurable per-modality target
13
+ rate, and groups the result into per-sensor modality bundles (EMG, EKG,
14
+ IMU, FSR, VO2 Master, HR Strap, Analog) ready for analysis.
15
+
16
+ ## Installation
17
+
18
+ ```sh
19
+ pip install delsys
20
+ ```
21
+
22
+ For local development:
23
+
24
+ ```sh
25
+ git clone https://github.com/praneethnamburi/delsys
26
+ pip install -e "./delsys[dev]"
27
+ ```
28
+
29
+ ## Quickstart
30
+
31
+ ```python
32
+ import delsys
33
+
34
+ lf = delsys.Log("path/to/Trial.csv", sensor_map="path/to/delsys_channelmap.txt")
35
+
36
+ # Direct accessors — list of typed bundles, one per sensor with that modality.
37
+ lf.emg # list of EMG bundles
38
+ lf.ekg # list of EKG bundles
39
+ lf.acc, lf.gyro # tri-axial IMU bundles
40
+ lf.fsr # 4-channel FSR bundles
41
+ lf.analog # raw Analog signals
42
+ lf.vo2master # VO2 Master link device (8 channels)
43
+ lf.hrstrap # HR Strap link device
44
+
45
+ # Side accessors return whole Sensor objects.
46
+ lf.left, lf.right, lf.center
47
+
48
+ # Filtered queries.
49
+ lf.find(modality="EMG", side="R") # right-side EMG bundles
50
+ lf.find(location="Forearm") # any sensor at "Forearm"
51
+ lf.find(sensor_number=5)
52
+ lf.find(modality="EMG", as_="signal") # raw per-channel Signal objects
53
+
54
+ # A typical EMG envelope pipeline.
55
+ for emg in lf.emg:
56
+ envelope = emg.process(amp_kind="envelope2")
57
+ rms = emg.rms(envelope_sr=240) # clean RMS amplitude pipeline
58
+ ```
59
+
60
+ See the full API reference at <https://delsys.readthedocs.io>.
61
+
62
+ ## Channelmap files (optional)
63
+
64
+ When you pass `sensor_map="path/to/delsys_channelmap.txt"` to `Log()`, that
65
+ file labels each sensor number with a sensor type and a body-location tag.
66
+ This lets you query by side (`lf.find(side="R")`) and by location
67
+ (`lf.find(location="Forearm")`).
68
+
69
+ The format is one sensor per line, three fields separated by `" - "`:
70
+
71
+ ```
72
+ Ch 1 - EMG - LBicep
73
+ Ch 2 - EMG - RBicep
74
+ Ch 11 - EKG - Chest
75
+ Ch 12 - Sync - Optitrack Recording Gate
76
+ Ch 19 - Quattro - LForearmExtensors (A-Index, B-Middle, C-Ring, D-Little)
77
+ Ch 21 - FSR - LFoot (1-Heel, 2-OuterEdge, 3-Ball, 4-Toe)
78
+ ```
79
+
80
+ - **Field 1**: any text whose last whitespace-token is the sensor's channel
81
+ number (`Ch 1`, `Channel 01`, `1` all work).
82
+ - **Field 2**: a type tag (free text — common values: `EMG`, `Quattro`,
83
+ `Snap`, `EKG`, `FSR`, `Sync`).
84
+ - **Field 3**: a location label. Its *first character* is interpreted as the
85
+ side (`L`/`R`/`C` for left/right/center). Anything else still loads but
86
+ won't match `lf.find(side=...)`.
87
+
88
+ Trailing parenthetical notes are informational only — they remain in
89
+ `location` but the parser doesn't extract sub-channel labels from them.
90
+ Blank lines and lines without two `" - "` separators are silently skipped.
91
+
92
+ A more comprehensive reference file lives at
93
+ [`examples/delsys_channelmap.txt`](https://github.com/praneethnamburi/delsys/blob/main/examples/delsys_channelmap.txt).
94
+
95
+ ## Supported export formats
96
+
97
+ - EMGworks
98
+ - Trigno Discover 1.4.2
99
+ - Trigno Discover 1.5.0
100
+ - Trigno Discover 1.6.4 (with and without link devices)
101
+ - Trigno Discover 1.7.0
102
+
103
+ ## Supported sensors
104
+
105
+ EMG (Avanti single, Duo, Quattro, Snap-Lead), EKG, ACC, GYRO, FSR, Analog,
106
+ VO2 Master (link), HR Strap (link).
107
+
108
+ ## Scope and contributions
109
+
110
+ Supported sensor types are limited to those the maintainer has access to.
111
+ Delsys ships other hardware (e.g. SmO2/Thb appear as stubs in `TARGET_SR`
112
+ but are not exercised end-to-end). Contributions adding parsers and tests
113
+ for additional sensors are very welcome.
114
+
115
+ If you'd like to contribute, the dev install above pulls `pytest`, `black`,
116
+ and `isort`. Format with `black` and `isort` before opening a PR:
117
+
118
+ ```sh
119
+ isort src/ tests/ scripts/
120
+ black src/ tests/ scripts/
121
+ pytest
122
+ ```
123
+
124
+ ## License
125
+
126
+ Distributed under the MIT License. See [LICENSE](https://github.com/praneethnamburi/delsys/blob/main/LICENSE) for details.
127
+
128
+ ## Contact
129
+
130
+ [Praneeth Namburi](https://praneethnamburi.com)
131
+
132
+ Project link: <https://github.com/praneethnamburi/delsys>
133
+
134
+ ## Acknowledgments
135
+
136
+ This package was developed as part of the ImmersionToolbox initiative at the
137
+ [MIT.nano Immersion Lab](https://immersion.mit.edu). Thanks to NCSOFT for
138
+ supporting this initiative.
delsys-0.1.0/TODO.md ADDED
@@ -0,0 +1,88 @@
1
+ # TODO
2
+
3
+ Items deferred during the initial standalone-package extraction.
4
+
5
+ ## Coverage snapshot (2026-05-09)
6
+
7
+ Recorded after Phase E. Overall **81% line / 74% branch** across 766 statements.
8
+ Treat as informational — the user's direction was *measure-and-report only* for
9
+ 0.1.0; no new tests were written to chase numbers.
10
+
11
+ | Module | Line cov | Branch cov | Note |
12
+ |---|---|---|---|
13
+ | `__init__.py` | 100% | n/a | Re-exports + module docstring. |
14
+ | `_constants.py` | 100% | n/a | Constants only. |
15
+ | `_metadata.py` | 100% | n/a | Three namedtuples. |
16
+ | `signals.py` | 96% | 98% | Sensor / shape / column properties exercised by F3. |
17
+ | `_parse.py` | 95% | 93% | Three uncovered branches: `_fix_corrupted_sensor_names` early-return for empty replace dict, an EMGworks edge case, two link-parser error paths. |
18
+ | `sensor.py` | 90% | 90% | Uncovered: `Sensor.get_signal()` priority-sequence walk (lines 122–126). |
19
+ | `ekg.py` | 81% | 73% | Uncovered: `process_nk` (no integration test), `get_features_hp` (no integration test), `_get_t_noisy_segments` non-empty branch, `flip_signal` re-detection on already-flipped signal. |
20
+ | `emg.py` | 80% | 80% | Uncovered: `_freq_funcs` lambdas (the freq-domain feature dict only verified by key, not by per-feature value), `process_nk`, `get_features_nk`. |
21
+ | `_util.py` | 78% | 60% | Uncovered: the `EMG`-prefix early-return path of `_mod_to_attr` is exercised only by lower-case input; uppercase input not directly tested. |
22
+ | **`log.py`** | **57%** | **47%** | Biggest gap. Uncovered: `__getitem__` / `_getitem_onekey` legacy lookup (lines 437–473), `export_to_csv` (603–633), several `find()` branches (501–530), the `_combine_signal_sensor_info` error path with the channelmap-mismatch traceback (256–260). |
23
+
24
+ Follow-ups (deferred to 0.1.1+):
25
+
26
+ - **`log.py` coverage push.** The legacy `__getitem__` and `export_to_csv`
27
+ paths are public surface — a few targeted tests would close the biggest
28
+ branch-coverage gap. The legacy bracket lookup is a candidate for
29
+ deprecation, so test value depends on whether we plan to keep or remove
30
+ it.
31
+ - **`emg.py` / `ekg.py` NeuroKit-backed features.** `process_nk`,
32
+ `get_features_nk`, `process_nk` (EKG), `get_features_hp` are uncovered.
33
+ These wrap upstream libraries; tests would mostly verify the dict-shape
34
+ contract.
35
+
36
+ To regenerate this snapshot::
37
+
38
+ pytest --cov
39
+
40
+ (Source filter, branch tracking, and exclude rules live in
41
+ `[tool.coverage.run]` / `[tool.coverage.report]` in `pyproject.toml`.)
42
+
43
+ ## Post-0.1.0 roadmap
44
+
45
+ - **EMG/EKG artifact cleaning at the `Log` level.** Port `C:\dev\pn-projects\projects\emg_ica_cleaning.py` (multi-stage pipeline: harmonization → preprocess → ICA-based ECG suppression with auto-component-detection by lagged correlation → ACC-guided motion regression with safety gates) into `delsys` as a `Log.clean_emg_ekg_artifact(...)` method. The integration work is to (a) gather all EMG `Signal`s + the EKG `Signal` (and optional per-EMG ACC predictors) from `lf`, (b) run the pipeline, (c) splice cleaned samples back into `lf.signals` per channel and rebuild affected `EMG` bundles in `lf.sensors[*].emg`, since `EMG._sig` is constructed by stacking signals at `Sensor.__init__` time. The previous ACC-only `ica.py` was removed in 0.1.0 because it didn't serve this stated primary purpose.
46
+
47
+ ## Resampling defaults (post-0.1.0)
48
+
49
+ - **Reconsider the `TARGET_SR` defaults.** Currently every modality has a non-`None` default rate, so loading a CSV always resamples. A "preserve native rate" mode is conceptually attractive but not a five-minute change. Open design questions:
50
+
51
+ 1. EMGworks parser (`_parse_dataframe_emgworks`) doesn't handle a `None` target rate; needs a "skip resample" branch like the Discover-basic parser.
52
+ 2. Link devices (VO2 Master, HR Strap) are *asynchronous* — they have no native sampling rate, so `pysampled.uniform_resample` always needs a target. The "all-None" idea has no clean answer here. Possible fallback: pick rate from the median inter-sample interval, or require an explicit value just for link devices.
53
+ 3. API surface: a `mode='native'`/`'analysis'` knob vs. just changing default `TARGET_SR` values? The latter is a breaking change for downstream code that depends on current uniform rates.
54
+
55
+ Per-modality `None` is already supported via explicit `target_sr={'EMGS': None, ...}`, so power users have an escape hatch today.
56
+
57
+ ## Restructure / API
58
+
59
+ - **`Sensor.__init__` modality dispatch.** It uses an if/elif chain on modality strings to choose which class to instantiate. A small `MODALITY_REGISTRY` mapping `{'EMG': EMG, 'EKG': EKG, ...}` would be cleaner and would make adding modalities (e.g. SmO2/Thb that already appear in `TARGET_SR`) one-line changes.
60
+
61
+ - **`Log.__getitem__` overloading.** Mixes sensor-lookup and signal-lookup based on key type (int, single-letter, modality string, location, name). Works but is undiscoverable. The new `lf.find(...)` is the public-facing replacement; `__getitem__` could be deprecated when there's a transition window.
62
+
63
+ - **VO2 / HR identity.** `VO2_SENSOR_NUM` and `HR_SENSOR_NUM` are placeholder integers chosen to not collide with Trigno-base sensor numbers. Brittle if Delsys ever ships a third link device. A real fix identifies link sensors by type rather than number — would also clean up the special-cased branches in `_parse_sig_name_discover` and `Sensor.__init__`.
64
+
65
+ ## Pre-existing in-code TODOs
66
+
67
+ - **EKG**: "Slicing will not work well with the cached rpeak indices. TODO: modify the `__getitem__` method."
68
+ - **`_parse_dataframe_discover_with_link`**: "Make the exception explicit. The 13.5 ms sampling-rate calculation only applies for trigno base, not link devices."
69
+ - **VO2 link**: "VO2 can start delayed (fill these data points with zeros), VO2 can finish before the other system."
70
+ - **`discover_basic` parser path**: "Check if this works for a file with timestamps exported." Needs a fixture with timestamps but no link sensors.
71
+
72
+ ## Domain / units
73
+
74
+ - VO2Master `VO2_absolute` returns raw CSV values (~37000 for moderate exercise). Likely a units issue, not a column-mapping one — verify against a known-correct VO2 reading and add unit conversion if needed.
75
+
76
+ ## Stage 4 status (added 2026-05-08)
77
+
78
+ All six features have docs + tests in `tests/`:
79
+
80
+ 1. ✅ CSV header parsing — `tests/test_parse.py` (22 tests).
81
+ 2. ✅ End-to-end `Log` loading — `tests/test_log.py` (20 tests, parametrized over all 7 fixtures).
82
+ 3. ✅ Signal classes — `tests/test_signals.py` (30 tests).
83
+ 4. ✅ `EMG.process` envelope pipeline — `tests/test_emg.py` (14 tests).
84
+ 5. ✅ `EKG` R-peak detection — `tests/test_ekg.py` (NeuroKit's `ecg_simulate`).
85
+ 6. ✅ ICA cleaning — `tests/test_ica.py` (using a `Log`-shaped mock).
86
+
87
+ Fixtures live under `tests/fixtures/` and are generated from `_data/` via
88
+ `scripts/make_fixture.py`.
@@ -0,0 +1,20 @@
1
+ # Minimal makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line, and also
5
+ # from the environment for the first two.
6
+ SPHINXOPTS ?=
7
+ SPHINXBUILD ?= sphinx-build
8
+ SOURCEDIR = .
9
+ BUILDDIR = _build
10
+
11
+ # Put it first so that "make" without argument is like "make help".
12
+ help:
13
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14
+
15
+ .PHONY: help Makefile
16
+
17
+ # Catch-all target: route all unknown targets to Sphinx using the new
18
+ # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19
+ %: Makefile
20
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)