# Contract: imu_preintegrator

**Component**: shared_helpers / `helpers.imu_preintegrator` (cross-cutting concern owned by E-CC-HELPERS / AZ-264)
**Producer task**: AZ-276 — `_docs/02_tasks/todo/AZ-276_imu_preintegrator.md`
**Consumer tasks**: every C1 VIO task that consumes IMU windows; every C5 state-estimator task that builds GTSAM `CombinedImuFactor`s
**Version**: 1.0.0
**Status**: draft
**Last Updated**: 2026-05-10

## Purpose

Centralise GTSAM `CombinedImuFactor` preintegration so C1 (VIO) and C5 (StateEstimator) cannot drift into two slightly-different IMU integrations of the same FC IMU window. The helper owns the GTSAM `PreintegrationCombinedParams` + `PreintegratedCombinedMeasurements` lifecycle; consumers feed samples and read closed factors. Per `_docs/02_document/common-helpers/01_helper_imu_preintegrator.md`.

## Shape

### For function / method APIs

```python
class ImuPreintegrator:
    def __init__(self, params: PreintegrationCombinedParams) -> None: ...
    def reset_with_bias(self, bias: ImuBias) -> None: ...
    def integrate_sample(self, sample: ImuSample) -> None: ...
    def integrate_window(self, window: ImuWindow) -> None: ...
    def current_preintegration(self) -> CombinedImuFactor: ...
    def reset_for_new_keyframe(self) -> CombinedImuFactor: ...
```

| Name | Signature | Throws / Errors | Blocking? |
|------|-----------|-----------------|-----------|
| `reset_with_bias` | `(bias: ImuBias) -> None` | none | sync, in-memory |
| `integrate_sample` | `(sample: ImuSample) -> None` | `ImuPreintegrationError` if `sample.ts_ns` is not strictly monotonic vs. last sample | sync, hot-path |
| `integrate_window` | `(window: ImuWindow) -> None` | `ImuPreintegrationError` on monotonicity violation | sync, hot-path |
| `current_preintegration` | `() -> CombinedImuFactor` | `ImuPreintegrationError` if zero samples integrated since last reset | sync |
| `reset_for_new_keyframe` | `() -> CombinedImuFactor` | `ImuPreintegrationError` if zero samples integrated since last reset | sync; clears internal state |

`ImuSample`, `ImuWindow`, `ImuBias` types are imported from `gps_denied_onboard._types.nav`. `CombinedImuFactor` is the GTSAM-native factor type (re-exported from `helpers.imu_preintegrator` so consumers do not import GTSAM directly).

### Construction

```python
def make_imu_preintegrator(calibration: CameraCalibration) -> ImuPreintegrator: ...
```

`make_imu_preintegrator` reads gyro/accel noise covariances from `CameraCalibration` (which carries the IMU noise model per-deployment per `_docs/02_document/components/01_c1_vio/description.md`) and returns an instance with the right `PreintegrationCombinedParams`. Composition root binds one instance per writer thread.

## Invariants

- **Single-threaded by design**: no internal lock. The composition root binds ONE preintegrator instance to ONE writer thread; concurrent calls from multiple threads are undefined behaviour. The contract test asserts the helper does not acquire any locks.
- **Strict monotonic timestamps**: every sample fed through `integrate_sample` / `integrate_window` MUST have `ts_ns` strictly greater than the previously-integrated sample's `ts_ns`. Violations raise `ImuPreintegrationError`; the preintegrator state is NOT mutated by a rejected sample.
- **Bias drift is the consumer's responsibility**: the preintegrator never re-estimates bias internally. Consumers (C1, C5) call `reset_with_bias(...)` whenever their bias estimate changes; until then, integration uses the last-set bias.
- **No clock ownership**: every IMU sample carries its own monotonic timestamp. The preintegrator never reads a wall clock and never injects timestamps.
- **Consumers receive GTSAM types**: `current_preintegration()` and `reset_for_new_keyframe()` return GTSAM `CombinedImuFactor` instances that consumers attach to their factor graphs. The factor object is owned by the caller after return (no lingering references inside the helper).
- **`reset_for_new_keyframe` is destructive**: it returns the closed factor AND resets internal accumulators. Callers MUST capture the return value or lose the integration.

## Non-Goals

- Bias estimation / re-bias logic — owned by C1 and C5.
- Multi-threaded sample feeding — out of scope; helper is single-thread by contract.
- IMU sample acquisition / FC adapter integration — owned by C8.
- Serialising preintegrated factors to FDR records — owned by C13 / E-CC-FDR-CLIENT.

## Versioning Rules

- **Breaking changes** (method renamed/removed, parameter type changed, return type changed, monotonicity invariant relaxed) require a new major version + a deprecation pass through C1 and C5.
- **Non-breaking additions** (new optional method, new diagnostic accessor that does not mutate state) require a minor version bump.

## Test Cases

| Case | Input | Expected | Notes |
|------|-------|----------|-------|
| valid-monotonic-sequence | 100 samples with strictly increasing `ts_ns`, then `current_preintegration()` | factor returned with `deltaTij` matching the time span; non-zero `delta_pose` | Round-trip happy path |
| valid-window-then-keyframe | one `integrate_window(N samples)` then `reset_for_new_keyframe()` | factor returned; subsequent `current_preintegration()` raises `ImuPreintegrationError` (state cleared) | Confirms destructive reset |
| invalid-non-monotonic-sample | sample with `ts_ns < last_ts_ns` | `ImuPreintegrationError` raised; internal state unchanged (next valid sample integrates as if rejected sample never came) | Strict-monotonic invariant |
| valid-rebias | `reset_with_bias(bias_a)`, integrate 50 samples, `reset_with_bias(bias_b)`, integrate 50 more, `current_preintegration()` | factor reflects bias_b applied to second half | Re-bias mid-window |
| invalid-empty-preintegration | `current_preintegration()` after `reset_for_new_keyframe()` with no further samples | `ImuPreintegrationError` mentions "no samples since reset" | Guard against empty factor |
| determinism | same `(bias, samples)` integrated twice into two instances | deep-equal `CombinedImuFactor` outputs | Pure-function determinism |

## Change Log

| Version | Date | Change | Author |
|---------|------|--------|--------|
| 1.0.0 | 2026-05-10 | Initial contract derived from `_docs/02_document/common-helpers/01_helper_imu_preintegrator.md` | autodev decompose Step 2 |
