# SE3Utils Helper Module

**Task**: AZ-277_se3_utils
**Name**: SE3Utils Helper
**Description**: Implement the shared `SE3Utils` helper for SE(3) ↔ 4×4-matrix conversion and Lie-algebra exp/log/adjoint, backed by GTSAM `Pose3` primitives. Used wherever a consumer needs a 6-vector twist, a Jacobian over an SE(3) operation, or a deterministic conversion between matrix and pose forms — i.e., C1, C2.5, C3, C3.5, C4, C5, C8. Stateless; pure functions; strict caller-orthogonalisation contract.
**Complexity**: 2 points
**Dependencies**: AZ-263_initial_structure
**Component**: shared.helpers.se3_utils (cross-cutting; epic AZ-264 / E-CC-HELPERS)
**Tracker**: AZ-277
**Epic**: AZ-264 (E-CC-HELPERS)

### Document Dependencies

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

## Problem

Seven components (C1, C2.5, C3, C3.5, C4, C5, C8) need to cross the matrix-vs-pose boundary:
- C4's `solvePnPRansac` returns a 4×4 matrix; C5's iSAM2 graph wants a GTSAM `Pose3`.
- C1's relative-pose update needs `log_map` for covariance recovery.
- C8 encodes pose as a 6-vector for FC adapter emission.

Without a shared helper:
- Each component re-implements the conversion, drifting on rotation conventions, sign conventions, or near-identity edge cases.
- Subtle differences in `det(R)` validation (some silently re-orthogonalise, others reject) break the "same pose in, same pose out" invariant across components.
- Any future change (e.g., switching from GTSAM `Pose3` to `manifpy`) becomes a 7-place coordinated edit.

## Outcome

- A single `helpers.se3_utils` module is the only place that constructs a `Pose3` from a matrix or vice-versa across the codebase. Component imports go through the helper.
- All conversions are pure functions: same input → byte-equal numpy / GTSAM output.
- Strict orthogonal-rotation contract: `matrix_to_se3` rejects non-orthogonal or negative-determinant rotations with `Se3InvalidMatrixError` instead of silently fixing them. Callers are responsible for orthogonalisation; the rejection forces the bug back to the source.
- Near-identity Lie-algebra inputs (twist norm < 1e-10) are stable — `exp_map` falls back to the small-angle Taylor expansion documented in GTSAM rather than NaN-ing on `sin(θ)/θ`.

## Scope

### Included

- `matrix_to_se3(T_4x4) -> SE3`, `se3_to_matrix(SE3) -> np.ndarray`.
- `exp_map(xi) -> SE3`, `log_map(SE3) -> np.ndarray`, `adjoint(SE3) -> np.ndarray`.
- `is_valid_rotation(R_3x3, *, atol)` predicate for callers to check before calling `matrix_to_se3`.
- `Se3InvalidMatrixError` exception type.
- Re-export of GTSAM `Pose3` as `SE3` so consumers do not import GTSAM directly.
- Public interface contract published at `_docs/02_document/contracts/shared_helpers/se3_utils.md`.

### Excluded

- Quaternion conversions — consumers convert via numpy / GTSAM directly.
- SE(2) helpers — out of scope.
- Pose interpolation / Slerp — out of scope.
- Higher-order manifold ops (parallel transport, composition Jacobians) — out of scope.

## Acceptance Criteria

**AC-1: 4×4 ↔ SE3 round-trip**
Given a randomly-sampled valid `T_4x4` (orthogonal rotation, positive determinant, identity bottom row)
When `matrix_to_se3` then `se3_to_matrix` runs
Then the recovered matrix matches the input via `np.allclose(..., atol=1e-9)`

**AC-2: Lie-algebra round-trip**
Given a random twist `xi` of shape `(6,)` and norm ≈ 1.0
When `exp_map(xi)` then `log_map(...)` runs
Then the recovered twist matches `xi` via `np.allclose(..., atol=1e-9)`

**AC-3: Near-identity Lie stability**
Given `xi = [1e-12, 1e-12, 1e-12, 1e-12, 1e-12, 1e-12]`
When `exp_map(xi)` runs
Then the result is the identity pose within `atol=1e-9`; no exception, no NaN

**AC-4: Strict orthogonality rejection**
Given `T_4x4` whose `R` has `||R^T R - I||_F = 1e-3`
When `matrix_to_se3(T)` runs
Then `Se3InvalidMatrixError` is raised AND the helper does NOT silently re-orthogonalise (the message names the deviation magnitude)

**AC-5: Mirror rejection**
Given `T_4x4` with `det(R) ≈ -1`
When `matrix_to_se3(T)` runs
Then `Se3InvalidMatrixError` is raised mentioning the negative determinant

**AC-6: Block-layout guard**
Given `T_4x4` with bottom row `[0, 0, 0, 2]` (or any deviation from `[0, 0, 0, 1]`)
When `matrix_to_se3(T)` runs
Then `Se3InvalidMatrixError` is raised mentioning the bottom row

**AC-7: dtype contract**
Given `T_4x4` with `dtype=float32`
When `matrix_to_se3(T)` runs
Then `Se3InvalidMatrixError` is raised mentioning dtype (helpers operate strictly on `float64`)

**AC-8: Determinism**
Given the same `T_4x4` (or `xi`)
When converted twice through any helper function
Then both outputs are byte-equal

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

## Non-Functional Requirements

**Performance**
- Each helper function p99 ≤ 50 µs on Tier-2 — overhead vs. inline GTSAM ≤ 5 % (per E-CC-HELPERS hot-path NFR).

**Reliability**
- Pure deterministic; same input → byte-equal output.
- `Se3InvalidMatrixError` is the ONLY exception type the public surface raises on shape / orthogonality / dtype violations.

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|-------------|-----------------|
| AC-1 | `np.allclose(se3_to_matrix(matrix_to_se3(T)), T)` for 100 random valid `T` | all pass within `atol=1e-9` |
| AC-2 | `np.allclose(log_map(exp_map(xi)), xi)` for 100 random `xi` (norm ≈ 1.0) | all pass within `atol=1e-9` |
| AC-3 | `exp_map([1e-12]*6)` | identity pose within `atol=1e-9`; no NaN |
| AC-4 | non-orthogonal `T` | `Se3InvalidMatrixError`; message names deviation |
| AC-5 | `det(R) = -1` `T` | `Se3InvalidMatrixError`; mentions determinant |
| AC-6 | bottom row `[0, 0, 0, 2]` | `Se3InvalidMatrixError`; mentions bottom row |
| AC-7 | `float32` dtype | `Se3InvalidMatrixError`; mentions dtype |
| AC-8 | call any helper twice with same input | byte-equal outputs |
| AC-9 | static import scan | only `_types`, GTSAM, numpy, stdlib |
| NFR-perf | microbench each helper (10k iterations on Tier-2 fixture) | p99 ≤ 50 µs each |

## Constraints

- Public surface frozen by `_docs/02_document/contracts/shared_helpers/se3_utils.md` v1.0.0.
- Layer 1 Foundation only.
- GTSAM is the single math backend; numpy fallback only when GTSAM does not expose the primitive.
- No new dependency beyond what AZ-263 / E-BOOT pinned.

## Risks & Mitigation

**Risk 1: Silent re-orthogonalisation hides upstream rotation drift**
- *Risk*: A future change "softens" `matrix_to_se3` to silently re-orthogonalise inputs; consumers no longer learn that their rotation source is producing non-orthogonal matrices.
- *Mitigation*: AC-4 makes strict rejection part of the contract. The contract test enforces that `Se3InvalidMatrixError` is raised, not absorbed.

**Risk 2: GTSAM API drift between minor versions**
- *Risk*: `Pose3.expmap` signature changes; this helper breaks on a GTSAM upgrade.
- *Mitigation*: GTSAM is pinned in `pyproject.toml` at AZ-263 / E-BOOT; this helper's tests are the canary that detects drift before consumers do.

## Runtime Completeness

- **Named capability**: SE(3) ↔ matrix conversion + Lie-algebra exp/log/adjoint via GTSAM `Pose3` primitives (architecture / E-CC-HELPERS / `02_helper_se3_utils.md`).
- **Production code that must exist**: real GTSAM-backed conversions; real strict-orthogonality guard; real small-angle Taylor fallback for near-identity exp.
- **Allowed external stubs**: numpy fallback only where GTSAM does not expose the primitive (e.g., adjoint matrix construction).
- **Unacceptable substitutes**: silent re-orthogonalisation; "for now we just call `np.linalg.logm`" (numerically inferior, no Jacobian); skipping near-identity small-angle handling (NaN risk).

## Contract

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