inferenceActivity.ts
Purpose
This module converts a rolling window of raw 3-axis accelerometer samples into a single physical activity intensity value expressed as Counts Per Minute (CPM).
Its responsibilities include:
- Slicing the most recent 60-second window of samples from a stream
- Computing a vector magnitude signal from XYZ axes
- Applying a zero-phase bandpass filter (PAL4 coefficients, 0.6–6.2 Hz) to isolate locomotion frequencies
- Segmenting the filtered signal into 30-sample epochs
- Computing mean-removed area-under-the-curve (AUC) per epoch via trapezoidal integration
- Scaling the summed AUC into a CPM value
- Returning
nullon insufficient data, filter failures, or non-finite results
The resulting CPM value is suitable for downstream core temperature inference.
Invariants
Minimum Sample Count
The input window must contain at least 1500 samples before any computation is attempted.
Windows below this threshold return null with a debug log and are never processed.
Bandpass Validity
The bandpass band [0.6 Hz, 6.2 Hz] is validated against the Nyquist frequency of the 30 Hz process rate before filtering.
Invalid band configurations return null with an error log and are never filtered.
Epoch Completeness
Only complete 30-sample epochs are included in the AUC calculation.
Partial trailing epochs are discarded.
Minimum Epoch Count
At least 55 complete epochs must exist after filtering.
Insufficient epoch counts return null with a debug log.
Finite Output Guard
The computed CPM is checked for finiteness before being returned.
Non-finite results return null with an error log.
Result Log Throttling
Repeated identical log messages (same key, same result class) are throttled to one emission per 10 seconds to prevent log flooding.
Variants
Filter Initialization
The zero-phase filtfilt implementation matches SciPy’s default behavior:
- Initial conditions are computed via
lfilterZi(matchesscipy.signal.lfilter_zi) - Odd-reflect padding length is
3 × (max(a.length, b.length) − 1)(matchesscipy.signal.filtfiltdefault) - Forward and reverse passes each use scaled initial conditions seeded from the padded signal edge
Vector Magnitude
Each sample is reduced to its Euclidean magnitude:
vm = sqrt(x² + y² + z²)
The magnitude is computed from the raw values before filtering.
Window Slicing
The input slice always takes the last WINDOW_SAMPLES entries from the provided array, so callers may pass a continuously growing buffer without manual management.
Constants
| Constant | Value | Description |
|---|---|---|
PROCESS_RATE_HZ | 30 | Expected accelerometer sample rate |
WINDOW_SECONDS | 60 | Rolling window duration |
WINDOW_SAMPLES | 1800 | PROCESS_RATE_HZ × WINDOW_SECONDS |
MIN_PROCESS_SAMPLES | 1500 | Minimum samples required to compute CPM |
EPOCH_SIZE | 30 | Samples per AUC epoch |
MIN_EPOCHS | 55 | Minimum complete epochs required |
SCALING_FACTOR | 149.37 | Linear scale applied to summed AUC → CPM |
LOWCUT_HZ | 0.6 | Bandpass lower cutoff frequency |
HIGHCUT_HZ | 6.2 | Bandpass upper cutoff frequency |
RESULT_LOG_INTERVAL_MS | 10000 | Minimum ms between identical log lines |
Exports
RawAccelerometerSample
type RawAccelerometerSample = {
x: number;
y: number;
z: number;
timestamp: number;
};
Represents a single raw accelerometer reading. The timestamp field is carried through for bookkeeping but is not used in CPM computation.
computeCpmFrom60s(...)
function computeCpmFrom60s(samples: RawAccelerometerSample[]): number | null
Converts approximately 60 seconds of 30 Hz accelerometer samples into a single CPM value.
Parameters
| Parameter | Description |
|---|---|
samples | Raw accelerometer readings (stream or rolling buffer) |
Returns
A finite number representing the CPM activity level, or null if:
- the window contains fewer than
MIN_PROCESS_SAMPLESsamples - the bandpass configuration is invalid
- the
filtfiltoperation throws - fewer than
MIN_EPOCHScomplete epochs result - the computed CPM is non-finite