v0.2.0
Breaking change to the typed Log.<modality> accessors plus a
parse-time fix for same-rate sample-count drift. Bumped to 0.2.0
under the "breaking change → minor on 0.x" semver-on-0.x convention.
Changed (BREAKING)
Log.emg/Log.ekg/Log.acc/Log.gyro/Log.fsr/
Log.analog/Log.vo2master/Log.hrstrapnow return a single
aggregatedpysampled.Dataper modality (channels stacked across
every sensor that carries the modality), orNoneif no sensor
does. Previously each returned aList[Bundle](one entry per
sensor). Usebundle.split_by_signal_name()to recover the
per-sensor list.EMG.process(amp_kind="nk")andEKG.find_rpeaks_pnnow raise
NotImplementedErroron multi-channel input with a hint to use
split_by_signal_name. Previously both silently flattened
column-major and produced nonsense.FSR.a/.b/.c/.draiseNotImplementedErroron
aggregate FSR (≠ 4 channels) since "the 4th channel" is meaningless
across heterogeneous sensors. The per-Sensor 4-channel view is
unchanged.
Added
_normalize_signal_lengths— same-rate length-drift normalization
in_util.py, called fromLog.__init__between parser dispatch
and the per-Sensor stack. Tail-trims each(modality, sr)group to
its shortest length so floating-point drift from the per-format
resample step no longer tripsSensor's same-modality length
assert. Drift exceeding_DRIFT_TOLERANCE(4 samples) emits a
UserWarning._aggregate_bundleshelper — stacks per-Sensor bundles along the
signal axis, validatessignal_coords/axis/t0agreement, and
downsamples higher-rate parts to the lowest sampling rate (with
UserWarning) when the input is multi-rate within one modality.bundle.sensors(plural) property on every modality bundle
(Signal,IMU,FSR,VO2Master,EMG,EKG). Returns the
aggregate'smeta["sensors"]list when present, else
[meta["sensor"]]for the per-Sensor case (length 1).meta["sensors"]convention on aggregate bundles, aligned with
signal_names(solen(bundle.meta["sensors"]) == len(bundle.signal_names)).
Fixed
IMU.x/.y/.zuse coord-lookup (s["x"]) instead of
positional column slicing, so the same accessor works on per-Sensor
IMU(n, 3)and aggregate IMU(n, 3*N). The previous positional
slice silently returned only the first sensor's column on multi-
sensor input.
Internal
- Moved
_SUBCHANNEL_KEYS(FSR / Quattro channelmap parenthetical
key format) from_util.pyto_constants.pyalongside
SUBCHANNEL_MAP. No public API change.
Migration
| Before (0.1.x) | After (0.2.0) |
|---|---|
for emg in lf.emg: |
for emg in lf.emg.split_by_signal_name(): |
lf.acc[0].magnitude() |
lf.acc.split_by_signal_name()[0].magnitude() |
len(lf.emg) |
len(lf.emg.signal_names) if lf.emg else 0 |
if lf.acc: |
if lf.acc is not None: |
lf.acc[0] |
lf.acc.split_by_signal_name()[0] |
lf.fsr('')[1] |
lf.fsr.split_by_signal_name()[1] (or by name) |
Downstream consumers that iterate Log.<modality> lists need updating;
consumers that use Sensor.<modality> (per-Sensor view) are unchanged.
Provenance
The accessor reshape and the drift fix are coupled: one bundle per
modality only makes sense if every sensor's channels for that modality
end up at identical post-resample lengths. The smoking-gun pickle is
at C:/dev/immersionToolbox/_data/_resampling numbers/, where two
ACC channels nominally at the same rate ended up 1 sample apart
because of a Trigno frame quantization boundary. Centralizing the fix
post-parse keeps the per-format parsers' quirks intact.