inferenceActivity.ts

Converts raw accelerometer samples into a counts-per-minute (CPM) activity metric

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 null on 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 (matches scipy.signal.lfilter_zi)
  • Odd-reflect padding length is 3 × (max(a.length, b.length) − 1) (matches scipy.signal.filtfilt default)
  • 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

ConstantValueDescription
PROCESS_RATE_HZ30Expected accelerometer sample rate
WINDOW_SECONDS60Rolling window duration
WINDOW_SAMPLES1800PROCESS_RATE_HZ × WINDOW_SECONDS
MIN_PROCESS_SAMPLES1500Minimum samples required to compute CPM
EPOCH_SIZE30Samples per AUC epoch
MIN_EPOCHS55Minimum complete epochs required
SCALING_FACTOR149.37Linear scale applied to summed AUC → CPM
LOWCUT_HZ0.6Bandpass lower cutoff frequency
HIGHCUT_HZ6.2Bandpass upper cutoff frequency
RESULT_LOG_INTERVAL_MS10000Minimum 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

ParameterDescription
samplesRaw 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_SAMPLES samples
  • the bandpass configuration is invalid
  • the filtfilt operation throws
  • fewer than MIN_EPOCHS complete epochs result
  • the computed CPM is non-finite