Product documentation — installation, licensing, and integration guides.
SolvFilter
Examples

SolvFilter: Examples

Each example is a verified run of a guided tour script from the shipped python/examples/ bundle (see PROGRESSION.md for the full 01→06 sequence). Run the guided tour first: it narrates the scenario, explains each metric, and shows when to trust the result. Use the production one-liner when wiring the same API call into your pipeline.

Beta release. Products in this catalog other than the SolvSRK family and SolvScout / SolvTune are beta — suitable for trials and evaluation; APIs and packaging may change before GA. Do not deploy beta builds in production programs without a signed agreement with Resonix. Activate a trial license before running examples — see Install and Licensing.

Journeys

Example 1: Journey 01 — First contact

Version, license, and the smallest successful predict. Every support ticket starts with the package version. Machine-locked .lic files gate production use.

Walkthrough:

  1. What you are doing — Confirm the native library loads, check your license, and run one prediction step so you know SolvFilter is alive before wiring it into a filter.

  2. Version — Every support ticket starts with the package version.

    • version: 0.1.0
    • package_version: 0.1.0
  3. License — Machine-locked .lic files gate production use.

    • license_valid: True
  4. Simplest call — A constant-velocity target starts at the origin moving (1, 2) m/s. One predict step of dt=0.5 s should advance the position to (0.5, 1.0) and grow the position uncertainty.

    • x_pred: [0.5, 1.0, 1.0, 2.0]
    • pos_var_before: 100.0
    • pos_var_after: 102.501 — Position advanced by velocity*dt and the position variance grew: the predict works.

Guided tour output (from a verified run):

Version, license, and the smallest successful predict

-- Act 1 - What you are doing --
  -> Confirm the native library loads, check your license, and run one prediction step so you know SolvFilter is alive before wiring it into a filter.

-- Act 2 - Version --
  -> Every support ticket starts with the package version.
  version: 0.1.0
  package_version: 0.1.0

-- Act 3 - License --
  -> Machine-locked .lic files gate production use.
  license_valid: True

-- Act 4 - Simplest call --
  -> A constant-velocity target starts at the origin moving (1, 2) m/s. One predict step of dt=0.5 s should advance the position to (0.5, 1.0) and grow the position uncertainty.
  x_pred: [0.5, 1.0, 1.0, 2.0]
  pos_var_before: 100.0
  pos_var_after: 102.501
  -> Position advanced by velocity*dt and the position variance grew: the predict works.

-- Run complete --
  next_journey: 02_predict.py
cd python
# Guided tour — narrated scenario walkthrough (recommended first run):
python examples/journeys/01_first_contact.py
 
# Production one-liner — same API call you ship:
python -c "import solvfilter; print(solvfilter.version(), solvfilter.license_valid())"

Example 2: Journey 02 — The predict step

Propagate mean + covariance | EKF vs UKF agree on linear dynamics. mean+covariance propagation, EKF/UKF agreement on linear dynamics.

Walkthrough:

  1. What predict does — predict() answers 'where will the target be, and how sure am I?'. It propagates the mean through your dynamics (RK4) and grows the covariance P -> F P F^T + Q, where F linearises your dynamics over dt.

    • x_pred: [0.5, 1.0, 1.0, 2.0]
    • pos_var_grew: True
    • symmetric: True — Covariance stays symmetric and position variance increases with time.
  2. EKF and UKF should agree on linear dynamics — On a linear system the Extended and Unscented filters must give the same answer. If they diverge on something linear, something is wrong.

    • ekf_mean: [1.092614, 0.237052]
    • ukf_mean: [1.092614, 0.237052]
    • analytic_rotation: [1.092614, 0.237052]
    • ekf_ukf_agree: True — Both filters match the closed-form rotation to RK4 truncation.
  3. Knobs that matter — rk4_substeps trades speed for propagation accuracy; iekf_iters>0 turns on the iterated EKF for stiff nonlinear dynamics; use_ukf switches to sigma points.

    • rk4_substeps: 8
    • iekf_iters: 0
    • use_ukf: False

Guided tour output (from a verified run):

Propagate mean + covariance | EKF vs UKF agree on linear dynamics

-- Act 1 - What predict does --
  -> predict() answers 'where will the target be, and how sure am I?'. It propagates the mean through your dynamics (RK4) and grows the covariance P -> F P F^T + Q, where F linearises your dynamics over dt.
  x_pred: [0.5, 1.0, 1.0, 2.0]
  pos_var_grew: True
  symmetric: True
  -> Covariance stays symmetric and position variance increases with time.

-- Act 2 - EKF and UKF should agree on linear dynamics --
  -> On a linear system the Extended and Unscented filters must give the same answer. If they diverge on something linear, something is wrong.
  ekf_mean: [1.092614, 0.237052]
  ukf_mean: [1.092614, 0.237052]
  analytic_rotation: [1.092614, 0.237052]
  ekf_ukf_agree: True
  -> Both filters match the closed-form rotation to RK4 truncation.

-- Act 3 - Knobs that matter --
  -> rk4_substeps trades speed for propagation accuracy; iekf_iters>0 turns on the iterated EKF for stiff nonlinear dynamics; use_ukf switches to sigma points.
  rk4_substeps: 8
  iekf_iters: 0
  use_ukf: False

-- Run complete --
  next_journey: 03_update.py
cd python
# Guided tour — narrated scenario walkthrough (recommended first run):
python examples/journeys/02_predict.py
 
# Production one-liner — same API call you ship:
python -c "import numpy as np, solvfilter as s; cfg=s.Config(4, lambda t,x: np.array([x[2],x[3],0,0])); print(s.predict(cfg, np.array([0,0,1,2.]), np.diag([100,100,10,10.]), np.eye(4)*1e-3, 0.5)[0])"

Example 3: Journey 03 — The Joseph measurement update

Fuse a noisy sensor reading | covariance shrinks and stays SPD. fusing a noisy measurement, covariance shrink, SPD preservation, innovation.

Walkthrough:

  1. Predict, then measure — We predict the target forward, then a position sensor returns a noisy reading z. update() fuses z into the estimate using the Joseph form, which keeps the covariance symmetric positive-definite even after many steps.

  2. What the update returns — Posterior mean/covariance plus the innovation nu and its covariance S.

    • x_post: [0.548122, 1.096244, 1.002347, 2.004695]
    • innovation_nu: [0.05, 0.1]
    • pos_var_before: 102.501
    • pos_var_after: 3.8498
  3. Did the measurement help? — A good update reduces uncertainty (position variance drops) and preserves a valid covariance (symmetric). Both must hold.

    • variance_reduced: True
    • symmetric: True
    • innovation_matches_prediction: True — The sensor pulled the estimate toward z and shrank the covariance -> the fusion works.

Guided tour output (from a verified run):

Fuse a noisy sensor reading | covariance shrinks and stays SPD

-- Act 1 - Predict, then measure --
  -> We predict the target forward, then a position sensor returns a noisy reading z. update() fuses z into the estimate using the Joseph form, which keeps the covariance symmetric positive-definite even after many steps.

-- Act 2 - What the update returns --
  -> Posterior mean/covariance plus the innovation nu and its covariance S.
  x_post: [0.548122, 1.096244, 1.002347, 2.004695]
  innovation_nu: [0.05, 0.1]
  pos_var_before: 102.501
  pos_var_after: 3.8498

-- Act 3 - Did the measurement help? --
  -> A good update reduces uncertainty (position variance drops) and preserves a valid covariance (symmetric). Both must hold.
  variance_reduced: True
  symmetric: True
  innovation_matches_prediction: True
  -> The sensor pulled the estimate toward z and shrank the covariance -> the fusion works.

-- Run complete --
  next_journey: 04_consistency.py
cd python
# Guided tour — narrated scenario walkthrough (recommended first run):
python examples/journeys/03_update.py
 
# Production one-liner — same API call you ship:
python -c "import numpy as np, solvfilter as s; o=s.update(np.array([0.5,1,1,2.]), np.diag([50,50,10,10.]), np.array([0.55,1.1]), np.array([[1.,0,0,0],[0,1.,0,0]]), np.eye(2)*4.); print(o['x'], o['P'][0,0])"

Example 4: Journey 04 — Consistency diagnostics (NEES / NIS)

NEES answers 'is my covariance honest?' | NIS answers 'is R honest?'. whether your covariance and measurement noise are honest.

Walkthrough:

  1. Why diagnostics matter — A filter can look confident and still be wrong. NEES compares the true error against the reported covariance; NIS compares innovations against S. For a consistent filter, NEES ~ n (state dim) and NIS ~ m (measurement dim).

  2. NEES on textbook cases — Normalised Estimation Error Squared: e^T P^-1 e.

    • nees_unit_error_unit_cov: 1.0
    • nees_scaled: 2.0 — Both equal their analytic values (1.0 and 2.0) - the diagnostic is exact.
  3. NIS on a textbook case — Normalised Innovation Squared: nu^T S^-1 nu.

    • nis_value: 1.0
  4. Read it like a filter engineer — Now interpret a realistic 4-state estimate against its covariance and a 2-D innovation against its S.

    • nees: 0.003
    • expected_mean: 4 — NEES well below n: the filter is over-confident-safe (covariance too large).
    • nis: 0.0001
    • expected_mean: 2 — NIS off m: revisit R (measurement noise) or the observation model H.

Guided tour output (from a verified run):

NEES answers 'is my covariance honest?' | NIS answers 'is R honest?'

-- Act 1 - Why diagnostics matter --
  -> A filter can look confident and still be wrong. NEES compares the true error against the reported covariance; NIS compares innovations against S. For a consistent filter, NEES ~ n (state dim) and NIS ~ m (measurement dim).

-- Act 2 - NEES on textbook cases --
  -> Normalised Estimation Error Squared: e^T P^-1 e.
  nees_unit_error_unit_cov: 1.0
  nees_scaled: 2.0
  -> Both equal their analytic values (1.0 and 2.0) - the diagnostic is exact.

-- Act 3 - NIS on a textbook case --
  -> Normalised Innovation Squared: nu^T S^-1 nu.
  nis_value: 1.0

-- Act 4 - Read it like a filter engineer --
  -> Now interpret a realistic 4-state estimate against its covariance and a 2-D innovation against its S.
  nees: 0.003
  expected_mean: 4
  -> NEES well below n: the filter is over-confident-safe (covariance too large).
  nis: 0.0001
  expected_mean: 2
  -> NIS off m: revisit R (measurement noise) or the observation model H.

-- Run complete --
  next_journey: 05_track_cv.py
cd python
# Guided tour — narrated scenario walkthrough (recommended first run):
python examples/journeys/04_consistency.py
 
# Production one-liner — same API call you ship:
python -c "import numpy as np, solvfilter as s; print(s.nees([1,0.],[0,0.],np.eye(2)), s.nis([1,2.],np.diag([2.,8.])))"

Example 5: Journey 05 — 05 Track Cv

A target moves at (3, -1.5) m/s. Our estimate starts deliberately wrong at (5, -5). A position sensor with 2 m noise reports every 0.1 s. Can the filter lock on and stay consistent?. predict -> simulate a noisy measurement -> Joseph update, 200 times. How close did we track, and was the reported uncertainty honest?.

Walkthrough:

  1. The scenario — A target moves at (3, -1.5) m/s. Our estimate starts deliberately wrong at (5, -5). A position sensor with 2 m noise reports every 0.1 s. Can the filter lock on and stay consistent?

  2. Run the loop — predict -> simulate a noisy measurement -> Joseph update, 200 times.

  3. Results — How close did we track, and was the reported uncertainty honest?

    • position_rmse_m: 0.9441
    • meas_sigma_m: 2.0
    • steps: 200 — RMSE well below the raw sensor sigma means the filter is extracting signal from noise.
    • nees: 3.0862
    • expected_mean: 4 — NEES near n=4: covariance and error agree -> the filter is consistent.

Guided tour output (from a verified run):

200 steps @ 10 Hz | position-only sensor | RMSE + mean NEES

-- Act 1 - The scenario --
  -> A target moves at (3, -1.5) m/s. Our estimate starts deliberately wrong at (5, -5). A position sensor with 2 m noise reports every 0.1 s. Can the filter lock on and stay consistent?

-- Act 2 - Run the loop --
  -> predict -> simulate a noisy measurement -> Joseph update, 200 times.

-- Act 3 - Results --
  -> How close did we track, and was the reported uncertainty honest?
  position_rmse_m: 0.9441
  meas_sigma_m: 2.0
  steps: 200
  -> RMSE well below the raw sensor sigma means the filter is extracting signal from noise.
  nees: 3.0862
  expected_mean: 4
  -> NEES near n=4: covariance and error agree -> the filter is consistent.

-- Run complete --
  next_journey: 06_limits.py
  position_rmse_m: 0.9441
cd python
# Guided tour — narrated scenario walkthrough (recommended first run):
python examples/journeys/05_track_cv.py
 
# Production one-liner — same API call you ship:
python examples/journeys/05_track_cv.py

Example 6: Journey 06 — Limits: mean propagation vs SolvSRK

predict_with (bring your own mean) | when to reach for SolvSRK. Propagate the mean with whatever integrator is right for your dynamics, then pass that mean to predict_with. SolvFilter linearises rhs at the prior to build F and returns P_pred = F P F^T + Q; the returned mean is exactly yours. Consistency checks need an invertible covariance. Ask for NEES with a singular P and SolvFilter refuses instead of returning garbage.

Walkthrough:

  1. Where the built-in propagator stops — predict() uses RK4 on your rhs. That is ideal for tracking (constant velocity, coordinated turns) but not for stiff systems (fast chemistry, electrical transients) where you need an implicit integrator like SolvSRK.

  2. predict_with: covariance-only predict — Propagate the mean with whatever integrator is right for your dynamics, then pass that mean to predict_with. SolvFilter linearises rhs at the prior to build F and returns P_pred = F P F^T + Q; the returned mean is exactly yours.

    • supplied_mean: [0.5, 1.0, 1.0, 2.0]
    • returned_mean: [0.5, 1.0, 1.0, 2.0]
    • mean_is_yours: True
    • covariance_grew: True — SolvFilter did the covariance bookkeeping; your integrator owned the mean.
  3. Honest refusal: a degenerate diagnostic — Consistency checks need an invertible covariance. Ask for NEES with a singular P and SolvFilter refuses instead of returning garbage.

    • refused_as_expected: True
    • error: solvfilter.nees: singular — A singular covariance is caught, not silently inverted.
  4. What SolvFilter is NOT — Not a stiff-ODE integrator (use SolvSRK for the mean), not a smoother or batch estimator, not a data association / multi-target tracker. It is the single-target predict+update core you compose those around.

Guided tour output (from a verified run):

predict_with (bring your own mean) | when to reach for SolvSRK

-- Act 1 - Where the built-in propagator stops --
  -> predict() uses RK4 on your rhs. That is ideal for tracking (constant velocity, coordinated turns) but not for stiff systems (fast chemistry, electrical transients) where you need an implicit integrator like SolvSRK.

-- Act 2 - predict_with: covariance-only predict --
  -> Propagate the mean with whatever integrator is right for your dynamics, then pass that mean to predict_with. SolvFilter linearises rhs at the prior to build F and returns P_pred = F P F^T + Q; the returned mean is exactly yours.
  supplied_mean: [0.5, 1.0, 1.0, 2.0]
  returned_mean: [0.5, 1.0, 1.0, 2.0]
  mean_is_yours: True
  covariance_grew: True
  -> SolvFilter did the covariance bookkeeping; your integrator owned the mean.

-- Act 3 - Honest refusal: a degenerate diagnostic --
  -> Consistency checks need an invertible covariance. Ask for NEES with a singular P and SolvFilter refuses instead of returning garbage.
  refused_as_expected: True
  error: solvfilter.nees: singular
  -> A singular covariance is caught, not silently inverted.

-- Act 4 - What SolvFilter is NOT --
  -> Not a stiff-ODE integrator (use SolvSRK for the mean), not a smoother or batch estimator, not a data association / multi-target tracker. It is the single-target predict+update core you compose those around.

-- Run complete --
  next_journey: (end of suite) - see examples/PROGRESSION.md
cd python
# Guided tour — narrated scenario walkthrough (recommended first run):
python examples/journeys/06_limits.py
 
# Production one-liner — same API call you ship:
python -c "import numpy as np, solvfilter as s; cfg=s.Config(4, lambda t,x: np.array([x[2],x[3],0,0])); print(s.predict_with(cfg, np.array([0,0,1,2.]), np.eye(4)*10, np.eye(4)*1e-3, 0.5, np.array([0.5,1,1,2.]))[0])"

Example bundle

Every journey is a domain scenario with narrated acts — run the guided tour first to see what each number means, then copy the production one-liner into your pipeline.

ResourcePurpose
PROGRESSION.mdOrdered runbook — journeys 01→06 with dual commands
COVERAGE.mdCapability matrix — which APIs each journey exercises
APPLICATIONS.mdWhere the product applies in real programs
run_examples.pyInteractive menu to launch any journey

Guided journeys

#ScriptScenario
0101_first_contact.pyVersion, license, and the smallest successful predict
0202_predict.pyPropagate mean + covariance
0303_update.pyFuse a noisy sensor reading
0404_consistency.pyNEES answers 'is my covariance honest?'
0505_track_cv.pyA target moves at (3, -1.5) m/s. Our estimate starts deliberately wrong at (5, -5). A posi…
0606_limits.pypredict_with (bring your own mean)

Run from the python/ directory after install and license activation. Set SOLVFILTER_QUIET=1 only when you want silent CLI runs (no narration).