Files
IMU/scripts/imu_static_calibrator.py
T

181 lines
5.4 KiB
Python

"""Numeric-only stationary detector and yaw gyro bias estimator."""
from __future__ import annotations
from dataclasses import dataclass
import math
import numpy as np
MOVING = 0
CANDIDATE = 1
STATIC = 2
TIME_EPSILON_S = 1.0e-12
@dataclass(frozen=True)
class StaticCorrectionConfig:
enter_seconds: float = 2.0
gyro_threshold_dps: float = 0.5
acc_norm_tolerance_g: float = 0.2
acc_stability_threshold_g: float = 0.02
@dataclass
class StaticCorrectionState:
config: StaticCorrectionConfig
mode: int
active_yaw_bias_z_dps: float
candidate_elapsed_s: float
candidate_count: int
candidate_acc_mean_g: np.ndarray
candidate_gyro_z_mean_dps: float
static_count: int
static_acc_mean_g: np.ndarray
static_gyro_z_mean_dps: float
def initialize(
config: StaticCorrectionConfig,
initial_yaw_bias_z_dps: float,
) -> StaticCorrectionState:
_validate_config(config)
if not math.isfinite(initial_yaw_bias_z_dps):
raise ValueError("initial_yaw_bias_z_dps must be finite")
return StaticCorrectionState(
config=config,
mode=MOVING,
active_yaw_bias_z_dps=float(initial_yaw_bias_z_dps),
candidate_elapsed_s=0.0,
candidate_count=0,
candidate_acc_mean_g=np.zeros(3),
candidate_gyro_z_mean_dps=0.0,
static_count=0,
static_acc_mean_g=np.zeros(3),
static_gyro_z_mean_dps=0.0,
)
def step(
state: StaticCorrectionState,
dt_s: float,
acc_g: np.ndarray,
gyro_dps: np.ndarray,
gyro_bias_xy_dps: np.ndarray,
) -> bool:
if not math.isfinite(dt_s) or dt_s < 0.0:
raise ValueError("dt_s must be finite and non-negative")
acc = _vector(acc_g, 3, "acc_g")
gyro = _vector(gyro_dps, 3, "gyro_dps")
bias_xy = _vector(gyro_bias_xy_dps, 2, "gyro_bias_xy_dps")
gyro_residual = np.array(
[
gyro[0] - bias_xy[0],
gyro[1] - bias_xy[1],
gyro[2] - state.active_yaw_bias_z_dps,
]
)
absolute_gate_ok = (
abs(float(np.linalg.norm(acc)) - 1.0) <= state.config.acc_norm_tolerance_g
and float(np.linalg.norm(gyro_residual)) <= state.config.gyro_threshold_dps
)
if state.mode == STATIC:
stable_acc = (
float(np.linalg.norm(acc - state.static_acc_mean_g))
<= state.config.acc_stability_threshold_g
)
if not absolute_gate_ok or not stable_acc:
_reset_candidate(state)
state.mode = MOVING
return False
state.static_count += 1
state.static_acc_mean_g += (acc - state.static_acc_mean_g) / state.static_count
state.static_gyro_z_mean_dps += (
gyro[2] - state.static_gyro_z_mean_dps
) / state.static_count
state.active_yaw_bias_z_dps = state.static_gyro_z_mean_dps
return True
if not absolute_gate_ok:
_reset_candidate(state)
state.mode = MOVING
return False
if state.mode == MOVING:
_start_candidate(state, acc, gyro[2])
return False
stable_acc = (
float(np.linalg.norm(acc - state.candidate_acc_mean_g))
<= state.config.acc_stability_threshold_g
)
if not stable_acc:
_start_candidate(state, acc, gyro[2])
return False
state.candidate_count += 1
state.candidate_elapsed_s += dt_s
state.candidate_acc_mean_g += (
acc - state.candidate_acc_mean_g
) / state.candidate_count
state.candidate_gyro_z_mean_dps += (
gyro[2] - state.candidate_gyro_z_mean_dps
) / state.candidate_count
if state.candidate_elapsed_s + TIME_EPSILON_S < state.config.enter_seconds:
return False
state.mode = STATIC
state.static_count = state.candidate_count
state.static_acc_mean_g = state.candidate_acc_mean_g.copy()
state.static_gyro_z_mean_dps = state.candidate_gyro_z_mean_dps
state.active_yaw_bias_z_dps = state.static_gyro_z_mean_dps
return True
def _start_candidate(
state: StaticCorrectionState,
acc_g: np.ndarray,
gyro_z_dps: float,
) -> None:
state.mode = CANDIDATE
state.candidate_elapsed_s = 0.0
state.candidate_count = 1
state.candidate_acc_mean_g = acc_g.copy()
state.candidate_gyro_z_mean_dps = float(gyro_z_dps)
def _reset_candidate(state: StaticCorrectionState) -> None:
state.candidate_elapsed_s = 0.0
state.candidate_count = 0
state.candidate_acc_mean_g.fill(0.0)
state.candidate_gyro_z_mean_dps = 0.0
def _vector(value, size: int, name: str) -> np.ndarray:
vector = np.asarray(value, dtype=float)
if vector.shape != (size,) or not np.all(np.isfinite(vector)):
raise ValueError(f"{name} must be a finite {size}-element vector")
return vector
def _validate_config(config: StaticCorrectionConfig) -> None:
values = (
config.enter_seconds,
config.gyro_threshold_dps,
config.acc_norm_tolerance_g,
config.acc_stability_threshold_g,
)
if not all(math.isfinite(value) for value in values):
raise ValueError("static correction configuration must be finite")
if config.enter_seconds <= 0.0:
raise ValueError("enter_seconds must be positive")
if config.gyro_threshold_dps <= 0.0:
raise ValueError("gyro_threshold_dps must be positive")
if config.acc_norm_tolerance_g <= 0.0:
raise ValueError("acc_norm_tolerance_g must be positive")
if config.acc_stability_threshold_g <= 0.0:
raise ValueError("acc_stability_threshold_g must be positive")