# ImuPreintegrator Helper Module

**Task**: AZ-276_imu_preintegrator
**Name**: ImuPreintegrator Helper
**Description**: Implement the shared `ImuPreintegrator` helper that wraps GTSAM's `PreintegrationCombinedParams` + `PreintegratedCombinedMeasurements` so C1 (VIO) and C5 (StateEstimator) consume one canonical preintegration of every FC IMU window. Single-threaded by design; one instance per writer thread, bound by the composition root. Bias drift remains the consumer's responsibility.
**Complexity**: 2 points
**Dependencies**: AZ-263_initial_structure
**Component**: shared.helpers.imu_preintegrator (cross-cutting; epic AZ-264 / E-CC-HELPERS)
**Tracker**: AZ-276
**Epic**: AZ-264 (E-CC-HELPERS)

### Document Dependencies

- `_docs/02_document/contracts/shared_helpers/imu_preintegrator.md` — frozen public interface this task produces.
- `_docs/02_document/common-helpers/01_helper_imu_preintegrator.md` — design rationale and consumer mapping.

## Problem

C1's VIO loop and C5's state estimator both consume the same FC IMU window every keyframe. Without a shared preintegrator:
- They drift into two slightly-different integrations of the same physical IMU stream (different sample-rejection rules, different bias-application order).
- The GTSAM `CombinedImuFactor` shape that goes into C5's iSAM2 graph diverges from the one C1 uses for its own pose update, breaking the "single source of IMU truth" invariant in `solution.md`.
- Per-deployment IMU noise covariances (which live in `CameraCalibration`) get parsed twice, with subtle unit differences.

## Outcome

- A single `ImuPreintegrator` is the only path through which any onboard process integrates IMU samples for a GTSAM `CombinedImuFactor`. C1 and C5 import it; nothing else does.
- The composition root binds ONE instance per writer thread; the helper's contract test confirms it does not acquire any locks (so no surprise serialisation under load).
- Sample monotonicity is enforced — non-monotonic samples raise `ImuPreintegrationError` before any state is mutated.
- Re-bias is explicit: `reset_with_bias` is called by consumers when their bias estimate changes; the helper never re-estimates bias internally.

## Scope

### Included

- `ImuPreintegrator` class + factory `make_imu_preintegrator(calibration: CameraCalibration) -> ImuPreintegrator`.
- Integration entrypoints: `integrate_sample(ImuSample)`, `integrate_window(ImuWindow)`.
- Factor accessors: `current_preintegration() -> CombinedImuFactor`, `reset_for_new_keyframe() -> CombinedImuFactor` (destructive).
- Bias control: `reset_with_bias(ImuBias) -> None`.
- `ImuPreintegrationError` exception type re-exported alongside the helper.
- Re-export of GTSAM's `CombinedImuFactor` (or a thin alias) so consumers do not import GTSAM directly.
- Public interface contract published at `_docs/02_document/contracts/shared_helpers/imu_preintegrator.md`.

### Excluded

- IMU sample acquisition / FC adapter integration — C8.
- Bias estimation / re-bias logic — C1, C5.
- Multi-threaded sample feeding — out of contract; helper is single-thread by design.
- Serialising preintegrated factors to FDR records — C13.
- The ImuSample / ImuWindow / ImuBias DTOs themselves — owned by `_types/nav.py` (AZ-263).

## Acceptance Criteria

**AC-1: Round-trip preintegration**
Given a synthetic IMU sequence of 100 samples with strictly-monotonic `ts_ns`
When the producer calls `integrate_sample` 100 times then `current_preintegration()`
Then a `CombinedImuFactor` is returned whose `deltaTij` equals the time span and whose `delta_pose` is non-zero

**AC-2: Strict monotonicity rejects non-monotonic samples**
Given a preintegrator with the last integrated sample at `ts_ns = T`
When `integrate_sample(sample)` is called with `sample.ts_ns <= T`
Then `ImuPreintegrationError` is raised AND the preintegrator's internal accumulators are unchanged (a subsequent valid sample integrates as if the bad one never came)

**AC-3: `reset_for_new_keyframe` is destructive**
Given a preintegrator with N integrated samples
When `reset_for_new_keyframe()` is called
Then the returned factor reflects all N samples AND a subsequent `current_preintegration()` (with no further samples) raises `ImuPreintegrationError`

**AC-4: Re-bias affects subsequent samples only**
Given a sequence: `reset_with_bias(bias_a)`, integrate 50 samples, `reset_with_bias(bias_b)`, integrate 50 more
When `current_preintegration()` is called
Then the resulting factor reflects bias_a applied to samples 1–50 and bias_b applied to samples 51–100 (not bias_b retroactively)

**AC-5: Determinism**
Given two instances constructed from the same calibration and fed the same `(bias, samples)` sequence
When both call `current_preintegration()`
Then the outputs are deep-equal

**AC-6: Single-threaded, lock-free**
Given the helper's source code
When inspected by the contract test (static analysis OR runtime reflection)
Then no `threading.Lock`, `RLock`, `Semaphore`, or `mutex` is acquired anywhere in the integration path

**AC-7: No upward imports (Layer 1 invariant)**
Given the helper module
When a static-import check runs across `gps_denied_onboard.helpers.imu_preintegrator`
Then it imports ONLY from `_types`, GTSAM, numpy, and stdlib — no `gps_denied_onboard.components.*` imports anywhere

## Non-Functional Requirements

**Performance**
- `integrate_sample` p99 ≤ 200 µs on Tier-2 (Jetson Orin Nano Super) — overhead vs. inline GTSAM PIM ≤ 5 % (per E-CC-HELPERS hot-path NFR).
- `current_preintegration` p99 ≤ 100 µs on the same hardware.

**Reliability**
- Pure deterministic: same inputs → byte-equal `CombinedImuFactor` outputs.
- `ImuPreintegrationError` is the ONLY exception type the public surface raises on schema/timestamp violation; GTSAM's lower-level exceptions MUST be wrapped.

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|-------------|-----------------|
| AC-1 | 100 monotonic samples → `current_preintegration()` | factor `deltaTij` ≈ time span; non-zero `delta_pose` |
| AC-2 | non-monotonic sample injection | `ImuPreintegrationError`; internal state unchanged (next valid sample integrates correctly) |
| AC-3 | `reset_for_new_keyframe` then `current_preintegration` | second call raises `ImuPreintegrationError` (state cleared) |
| AC-4 | re-bias mid-window | resulting factor distinguishes bias_a vs bias_b epochs |
| AC-5 | two instances, same input | deep-equal factor outputs |
| AC-6 | static / runtime lock check | no lock acquisition on the integration path |
| AC-7 | importlinter / grep gate | no `gps_denied_onboard.components.*` imports |
| NFR-perf | microbench `integrate_sample` (10k iterations on Tier-2 fixture) | p99 ≤ 200 µs; overhead ≤ 5 % |

## Constraints

- Public surface frozen by `_docs/02_document/contracts/shared_helpers/imu_preintegrator.md` v1.0.0.
- Layer 1 Foundation only (per `module-layout.md` § Allowed Dependencies). NO upward imports.
- GTSAM is the single math backend — do not introduce a second IMU-preintegration library.
- No new dependency beyond what AZ-263 / E-BOOT pinned.

## Risks & Mitigation

**Risk 1: Concurrent calls from a misconfigured composition root silently corrupt the GTSAM PIM accumulator**
- *Risk*: Two threads call `integrate_sample` simultaneously; GTSAM's PIM is not thread-safe; numerical drift goes undiagnosed.
- *Mitigation*: Helper is single-threaded by contract; the composition root binds one instance per writer thread. The contract test (AC-6) asserts no internal locking — making it a hard error if a future change tries to "make it thread-safe" instead of fixing the composition.

**Risk 2: Sample-monotonicity-rejection silently masks an upstream FC clock skew**
- *Risk*: A real IMU stream produces a non-monotonic sample (clock jitter); the helper rejects it; the consumer never learns.
- *Mitigation*: `ImuPreintegrationError` carries the offending vs. previous timestamp in its message so the consumer's catch-and-log path can record it as an FDR `kind="imu.skew"` event.

## Runtime Completeness

- **Named capability**: GTSAM `CombinedImuFactor` preintegration via `PreintegrationCombinedParams` + `PreintegratedCombinedMeasurements` (architecture / E-CC-HELPERS / `01_helper_imu_preintegrator.md`).
- **Production code that must exist**: real GTSAM-backed integration; real noise-model parsing from `CameraCalibration`; real strict-monotonic guard.
- **Allowed external stubs**: none — GTSAM is the production runtime.
- **Unacceptable substitutes**: pure-numpy "approximate" preintegration that ignores GTSAM's covariance propagation; deterministic-fallback that returns a zero factor; "for now we just integrate position with Euler" placeholder. Each would silently break C5's iSAM2 covariance honesty (AC-NEW-4).

## Contract

This task produces the contract at `_docs/02_document/contracts/shared_helpers/imu_preintegrator.md`.
Consumers MUST read that file — not this task spec — to discover the interface.
