CMF in Practice
This page walks through two real .cmf packages to show how CMF handles different model types end to end:
- Welch Freeze Index exemplar — a fixed signal estimator plus a fitted classifier
- Walking Classifier — a trained ONNX model, two input streams, explicit tensor shapes
Example 1: Welch Freeze Index exemplar
The model
The exemplar computes a three-second Welch Freeze Index and applies a fixed one-feature logistic classifier. It is the same signal estimator used in the Welch freeze-index feature. The classifier makes that score usable within CMF's probability-to-interval workflow; it is a development-calibrated demonstration operator, not a clinically validated FOG detector.
Package structure
freeze-index.cmf/
├── SHA256SUMS
├── classifier.json
├── config.json
├── labels.json
├── README.md
└── wrapper.py
wrapper.py contains the complete Welch estimator. classifier.json stores
the fitted logistic coefficient, intercept, decision-rule record, and
provenance.
config.json walkthrough
{
"cmf_version": "1.1",
"name": "Welch Freeze Index exemplar",
"version": "3.0.0",
"description": "Windowed FOG annotation exemplar using the same three-second Welch Freeze Index estimator as the packaged feature implementation and a fixed one-feature logistic classifier calibrated on the FOG-COA development corpus."
}
The name and description are what appear in RIME's model browser. The cmf_version tells RIME how to interpret the rest of the file.
Inputs
"inputs": [
{
"name": "accel_window",
"type": "signal",
"channels": ["acc_z"],
"sampling_rate_hz": 128,
"description": "Vertical acceleration proxy from the left-shank sensor; bind the physical vertical axis explicitly."
}
]
RIME uses this to:
- Check whether the current session has a compatible signal before offering the model
- Verify the declared 128 Hz sampling rate and slice complete windows before calling
wrapper.py
The wrapper receives inputs["accel_window"] as a NumPy array with no parsing required.
Outputs
"outputs": [
{
"name": "fog_probability",
"type": "probability",
"labels": ["no_fog", "fog"]
}
]
type: probability tells RIME the model returns a value in [0, 1] per window. RIME thresholds this to create annotation intervals.
The three output types across the bundled models illustrate the range:
| Model | Output type | What it produces |
|---|---|---|
| Welch Freeze Index exemplar | probability |
Confidence score per window → intervals |
| Walking Classifier | probability |
Confidence score per window → intervals |
| Step Detector | point |
Discrete timestamps (heel strikes) |
Inference mode
"inference": {
"mode": "windowed",
"window_size_ms": 3000,
"stride_ms": 500,
"window_anchor": "start",
"terminal_window": "drop",
"threshold": 0.5,
"postprocessing": {
"merge_gap_ms": 0,
"min_duration_ms": 0
}
}
mode: windowed means RIME slides a complete 3-second window over the signal
in 500 ms steps, timestamps it at the window start, and drops any terminal
partial window. Values greater than or equal to the conventional probability
cutoff of 0.50 are positive. Their complete window extents are unioned into
annotations; the zero merge gap adds no bridge between separated extents, and
the zero minimum duration removes none of the reconstructed intervals.
Fixed estimator profile
"parameters": []
The estimator itself has no hidden runtime controls: Welch preprocessing, the 0.5–3 Hz and 3–8 Hz bands, log-ratio scaling, and the absence of score smoothing are fixed in the package declaration. Window size, stride, decision threshold, merge gap, and minimum duration remain explicit CMF execution declarations.
Output mapping
"output_mappings": [
{
"output_name": "fog_probability",
"lane": "FOG",
"label": "FOG"
}
]
This tells RIME where to display the model's output: thresholded intervals go into the FOG annotation lane, labeled FOG. If that lane doesn't exist yet, RIME creates it.
wrapper.py walkthrough
The wrapper implements a single class with two methods:
class CMFModel:
def __init__(self, model_dir: str) -> None:
# Load the declared defaults and fitted classifier artifact
...
def predict(self, inputs, params=None) -> dict[str, np.ndarray]:
# Compute the three-second Welch log band-power ratio
# Apply the stored logistic classifier
# Return {"fog_probability": array([p])}
...
RIME calls __init__ once when the model is loaded, then calls predict for each window. The contract is simple:
inputs— a dict of{input_name: np.ndarray}, already resampled and shaped perconfig.jsonparams— a dict of{param_name: value}reflecting current UI settings- returns — a dict of
{output_name: np.ndarray}matching the declared outputs
The wrapper doesn't need to know anything about RIME, sessions, or file formats.
What RIME does with this
When you load the Welch Freeze Index exemplar on a session:
- RIME reads
config.jsonand checks the bound signal has the declared vertical proxy channel at 128 Hz - RIME slides the 3 s window over the signal in 500 ms steps
- Each window is passed to
predict()asinputs["accel_window"] - Returned probabilities are thresholded at the declared 0.50 cutoff and reconstructed using the declared window, merge-gap, and minimum-duration semantics
- Intervals are written into the FOG lane as FOG annotations
The result appears in the timeline alongside manual annotations, ready for side-by-side comparison or model evaluation.
Example 2: Walking Classifier (ONNX)
The model
The Walking Classifier is a small 1D CNN trained to distinguish walking from non-walking using bilateral ankle accelerometry. Unlike the Freeze Index, it has no hand-crafted feature extraction as the ONNX weights encode everything learned from training data.
This example shows:
- A trained model with binary weights (
model.onnx) - Two separate input streams
- Explicit tensor shape declarations
- Preprocessing handled entirely inside the wrapper
Package structure
walking-classifier.cmf/
├── config.json
├── model.onnx
└── wrapper.py
Inputs
"inputs": [
{
"name": "left_ankle",
"type": "signal",
"channels": ["acc_x", "acc_y", "acc_z"],
"sampling_rate_hz": 100,
"shape": [1, 300, 3],
"description": "Left ankle 3-axis accelerometer at 100Hz over a 3 s window."
},
{
"name": "right_ankle",
"type": "signal",
"channels": ["acc_x", "acc_y", "acc_z"],
"sampling_rate_hz": 100,
"shape": [1, 300, 3],
"description": "Right ankle 3-axis accelerometer at 100Hz over a 3 s window."
}
]
Two key differences from the Freeze Index:
Multiple inputs. RIME passes both left_ankle and right_ankle as separate arrays to predict().
Explicit shape. [1, 300, 3] means batch=1, 300 time steps, 3 channels. RIME validates that the sliced window matches this before calling the wrapper. If the shapes don't match, the model won't be offered for that session.
Outputs and inference mode
Same probability output type as the Freeze Index, with shorter windows (3 s, stride 1 s) appropriate for activity classification rather than event detection.
Parameters
{
"name": "scale_to_g",
"type": "bool",
"label": "Convert m/s² to g",
"default": true
}
A bool parameter is rendered as a checkbox in the UI. This handles the common case where different sensor systems record acceleration in different units. The conversion happens inside the wrapper before any inference.
wrapper.py walkthrough
The ONNX wrapper lazy-loads the model session to avoid importing onnxruntime at startup:
class CMFModel:
def __init__(self, model_dir: str) -> None:
# Stores path to model.onnx — does NOT load the ONNX session yet
...
def predict(self, inputs, params=None) -> dict[str, np.ndarray]:
left = self._prepare_ankle_input(inputs["left_ankle"], params)
right = self._prepare_ankle_input(inputs["right_ankle"], params)
outputs = self._get_session().run(
["walking_probability"],
{"left_dynamic_magnitude": left, "right_dynamic_magnitude": right},
)
return {"walking_probability": np.asarray(outputs[0]).reshape(-1)}
All preprocessing (unit conversion, gravity removal via median subtraction, L2 magnitude) runs inside the wrapper. From RIME's perspective it just passes raw arrays in and receives a probability out.
What RIME does with this
- RIME checks both
left_ankleandright_anklecan be satisfied from the session's signals - RIME slides a 3 s window (stride 1 s) over both signals simultaneously, keeping them time-aligned
- Both windows are passed to
predict()— the wrapper handles preprocessing and ONNX inference - Probabilities above 0.9 are merged into walking intervals in the Tasks lane, labeled Walk
Writing your own model
To package your own detector as a .cmf file, see Loading a Model.