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.
- delsys-0.1.0/.github/workflows/tests.yml +35 -0
- delsys-0.1.0/.gitignore +52 -0
- delsys-0.1.0/.readthedocs.yaml +29 -0
- delsys-0.1.0/CHANGELOG.md +101 -0
- delsys-0.1.0/LICENSE +21 -0
- delsys-0.1.0/PKG-INFO +173 -0
- delsys-0.1.0/README.md +138 -0
- delsys-0.1.0/TODO.md +88 -0
- delsys-0.1.0/docs/Makefile +20 -0
- delsys-0.1.0/docs/api.md +56 -0
- delsys-0.1.0/docs/conf.py +71 -0
- delsys-0.1.0/docs/index.md +10 -0
- delsys-0.1.0/docs/make.bat +35 -0
- delsys-0.1.0/docs/requirements.txt +4 -0
- delsys-0.1.0/examples/delsys_channelmap.txt +22 -0
- delsys-0.1.0/pyproject.toml +92 -0
- delsys-0.1.0/scripts/make_fixture.py +155 -0
- delsys-0.1.0/src/delsys/__init__.py +78 -0
- delsys-0.1.0/src/delsys/_constants.py +67 -0
- delsys-0.1.0/src/delsys/_metadata.py +27 -0
- delsys-0.1.0/src/delsys/_parse.py +648 -0
- delsys-0.1.0/src/delsys/_util.py +42 -0
- delsys-0.1.0/src/delsys/ekg.py +280 -0
- delsys-0.1.0/src/delsys/emg.py +357 -0
- delsys-0.1.0/src/delsys/log.py +633 -0
- delsys-0.1.0/src/delsys/sensor.py +126 -0
- delsys-0.1.0/src/delsys/signals.py +230 -0
- delsys-0.1.0/tests/__init__.py +0 -0
- delsys-0.1.0/tests/conftest.py +41 -0
- delsys-0.1.0/tests/fixtures/.gitkeep +0 -0
- delsys-0.1.0/tests/fixtures/discover142.csv +256 -0
- delsys-0.1.0/tests/fixtures/discover150.csv +257 -0
- delsys-0.1.0/tests/fixtures/discover164_basic.csv +257 -0
- delsys-0.1.0/tests/fixtures/discover164_link.csv +257 -0
- delsys-0.1.0/tests/fixtures/discover164_mvc.csv +507 -0
- delsys-0.1.0/tests/fixtures/discover170.csv +207 -0
- delsys-0.1.0/tests/fixtures/emgworks.csv +201 -0
- delsys-0.1.0/tests/test_ekg.py +144 -0
- delsys-0.1.0/tests/test_emg.py +152 -0
- delsys-0.1.0/tests/test_log.py +193 -0
- delsys-0.1.0/tests/test_parse.py +201 -0
- 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
|
delsys-0.1.0/.gitignore
ADDED
|
@@ -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
|
+
[](https://github.com/praneethnamburi/delsys)
|
|
38
|
+
[](https://pypi.org/project/delsys/)
|
|
39
|
+
[](https://delsys.readthedocs.io)
|
|
40
|
+
[](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
|
+
[](https://github.com/praneethnamburi/delsys)
|
|
4
|
+
[](https://pypi.org/project/delsys/)
|
|
5
|
+
[](https://delsys.readthedocs.io)
|
|
6
|
+
[](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)
|