# Factor Graph Optimizer

## Interface Definition

**Interface Name**: `IFactorGraphOptimizer`

### Interface Methods

```python
class IFactorGraphOptimizer(ABC):
    """
    GTSAM-based factor graph optimizer for trajectory estimation.
    
    ## Multi-Flight Support
    All state-modifying methods require `flight_id` parameter.
    Each flight maintains an independent factor graph state.
    F10 internally manages Dict[str, FactorGraph] keyed by flight_id.
    
    This enables:
    - Concurrent processing of multiple flights
    - Flight-scoped optimization without cross-contamination
    - Independent cleanup via delete_flight_graph()
    """
    
    @abstractmethod
    def add_relative_factor(self, flight_id: str, frame_i: int, frame_j: int, relative_pose: RelativePose, covariance: np.ndarray) -> bool:
        pass
    
    @abstractmethod
    def add_absolute_factor(self, flight_id: str, frame_id: int, gps: GPSPoint, covariance: np.ndarray, is_user_anchor: bool) -> bool:
        pass
    
    @abstractmethod
    def add_altitude_prior(self, flight_id: str, frame_id: int, altitude: float, covariance: float) -> bool:
        pass
    
    @abstractmethod
    def optimize(self, flight_id: str, iterations: int) -> OptimizationResult:
        pass
    
    @abstractmethod
    def get_trajectory(self, flight_id: str) -> Dict[int, Pose]:
        pass
    
    @abstractmethod
    def get_marginal_covariance(self, flight_id: str, frame_id: int) -> np.ndarray:
        pass
    
    # Chunk operations - F10 only manages factor graph subgraphs
    # F12 owns chunk metadata (status, is_active, etc.)
    @abstractmethod
    def create_chunk_subgraph(self, flight_id: str, chunk_id: str, start_frame_id: int) -> bool:
        pass
    
    @abstractmethod
    def add_relative_factor_to_chunk(self, flight_id: str, chunk_id: str, frame_i: int, frame_j: int, relative_pose: RelativePose, covariance: np.ndarray) -> bool:
        pass
    
    @abstractmethod
    def add_chunk_anchor(self, flight_id: str, chunk_id: str, frame_id: int, gps: GPSPoint, covariance: np.ndarray) -> bool:
        pass
    
    @abstractmethod
    def merge_chunk_subgraphs(self, flight_id: str, new_chunk_id: str, main_chunk_id: str, transform: Sim3Transform) -> bool:
        """Merges new_chunk INTO main_chunk. Extends main_chunk with new_chunk's subgraph."""
        pass
    
    @abstractmethod
    def get_chunk_trajectory(self, flight_id: str, chunk_id: str) -> Dict[int, Pose]:
        pass
    
    @abstractmethod
    def optimize_chunk(self, flight_id: str, chunk_id: str, iterations: int) -> OptimizationResult:
        pass
    
    @abstractmethod
    def optimize_global(self, flight_id: str, iterations: int) -> OptimizationResult:
        pass
    
    @abstractmethod
    def delete_flight_graph(self, flight_id: str) -> bool:
        """Cleanup factor graph when flight is deleted."""
        pass
```

## Component Description

### Responsibilities
- GTSAM-based fusion of relative and absolute measurements
- Incremental optimization (iSAM2) for real-time performance
- Robust kernels (Huber/Cauchy) for 350m outlier handling
- Scale resolution through altitude priors and absolute GPS
- Trajectory smoothing and global consistency
- Back-propagation of refinements to previous frames
- **Low-level factor graph chunk operations** (subgraph creation, factor addition, optimization)
- **Sim(3) transformation for chunk merging**

### Chunk Responsibility Clarification

**F10 provides low-level factor graph operations only**:
- `create_chunk_subgraph()`: Creates subgraph in factor graph (returns bool, not ChunkHandle)
- `add_relative_factor_to_chunk()`: Adds factors to chunk's subgraph
- `add_chunk_anchor()`: Adds GPS anchor to chunk's subgraph
- `merge_chunk_subgraphs()`: Applies Sim(3) transform and merges subgraphs
- `optimize_chunk()`, `optimize_global()`: Runs optimization

**F10 does NOT own chunk metadata** - only factor graph data structures.

**F12 is the source of truth for ALL chunk state** (see F12 spec):
- ChunkHandle with all metadata (is_active, has_anchor, matching_status)
- Chunk lifecycle management
- Chunk readiness determination
- High-level chunk queries
- F12 calls F10 for factor graph operations

**F11 coordinates recovery** (see F11 spec):
- Triggers chunk creation via F12
- Coordinates matching workflows
- Emits chunk-related events

### Scope
- Non-linear least squares optimization
- Factor graph representation of SLAM problem
- Handles monocular scale ambiguity
- Real-time incremental updates
- Asynchronous batch refinement
- **Multi-chunk factor graph with independent subgraphs**
- **Chunk-level optimization and global merging**
- **Sim(3) similarity transformation for chunk alignment**

### Design Pattern: Composition Over Complex Interface

F10 uses **composition** to keep the interface manageable. Rather than exposing 20+ methods in a monolithic interface, complex operations are composed from simpler primitives:

**Primitive Operations**:
- `add_relative_factor()`, `add_absolute_factor()`, `add_altitude_prior()` - Factor management
- `optimize()`, `get_trajectory()`, `get_marginal_covariance()` - Core optimization

**Chunk Operations** (composed from primitives):
- `create_new_chunk()`, `add_relative_factor_to_chunk()`, `add_chunk_anchor()` - Chunk factor management
- `merge_chunks()`, `optimize_chunk()`, `optimize_global()` - Chunk optimization

**Callers compose these primitives** for complex workflows (e.g., F11 composes anchor + merge + optimize_global).

## API Methods

### `add_relative_factor(frame_i: int, frame_j: int, relative_pose: RelativePose, covariance: np.ndarray) -> bool`

**Description**: Adds relative pose measurement between consecutive frames.

**Called By**:
- F07 Sequential VO (frame-to-frame odometry)

**Input**:
```python
frame_i: int  # Previous frame ID
frame_j: int  # Current frame ID (typically frame_i + 1)
relative_pose: RelativePose:
    translation: np.ndarray  # (3,) - unit vector (scale ambiguous from VO)
    rotation: np.ndarray  # (3, 3) or quaternion
covariance: np.ndarray  # (6, 6) - uncertainty
```

**Scale Resolution**:
F07 returns unit translation vectors due to monocular scale ambiguity. F10 resolves scale by:
1. Using altitude prior to constrain Z-axis
2. **Computing expected displacement**: Call H02 GSD Calculator.compute_gsd() to get GSD
   - GSD = (sensor_width × altitude) / (focal_length × resolution_width)
   - expected_displacement ≈ frame_spacing × GSD (typically ~100m)
3. Scaling: scaled_translation = unit_translation × expected_displacement
4. Global refinement using absolute GPS factors from F09 LiteSAM

**Explicit Flow**:
```python
# In add_relative_factor():
# altitude comes from F17 Configuration Manager (predefined operational altitude)
# focal_length, sensor_width from F17 Configuration Manager
config = F17.get_flight_config(flight_id)
altitude = config.altitude  # Predefined altitude, NOT from EXIF
gsd = H02.compute_gsd(altitude, config.camera_params.focal_length, 
                       config.camera_params.sensor_width, 
                       config.camera_params.resolution_width)
expected_displacement = frame_spacing * gsd  # ~100m typical at 300m altitude
scaled_translation = relative_pose.translation * expected_displacement
# Add scaled_translation to factor graph
```

**Note**: Altitude comes from F17 Configuration Manager (predefined operational altitude), NOT from EXIF metadata. The problem statement specifies images don't have GPS metadata.

**Output**:
```python
bool: True if factor added successfully
```

**Processing Flow**:
1. Create BetweenFactor in GTSAM
2. Apply robust kernel (Huber) to handle outliers
3. Add to factor graph
4. Mark graph as needing optimization

**Robust Kernel**:
- **Huber loss**: Downweights large errors (>threshold)
- **Critical** for 350m outlier handling from tilt

**Test Cases**:
1. **Normal motion**: Factor added, contributes to optimization
2. **Large displacement** (350m outlier): Huber kernel reduces weight
3. **Consecutive factors**: Chain of relative factors builds trajectory

---

### `add_absolute_factor(frame_id: int, gps: GPSPoint, covariance: np.ndarray, is_user_anchor: bool) -> bool`

**Description**: Adds absolute GPS measurement for drift correction or user anchor.

**Called By**:
- F09 Metric Refinement (after LiteSAM alignment)
- F11 Failure Recovery Coordinator (user-provided anchors)

**Input**:
```python
frame_id: int
gps: GPSPoint:
    lat: float
    lon: float
covariance: np.ndarray  # (2, 2) or (3, 3) - GPS uncertainty
is_user_anchor: bool  # True for user-provided fixes (high confidence)
```

**Output**:
```python
bool: True if factor added
```

**Processing Flow**:
1. Convert GPS to local ENU coordinates (East-North-Up)
2. Create PriorFactor or UnaryFactor
3. Set covariance (low for user anchors, higher for LiteSAM)
4. Add to factor graph
5. Trigger optimization (immediate for user anchors)

**Covariance Settings**:
- **User anchor**: σ = 5m (high confidence)
- **LiteSAM match**: σ = 20-50m (depends on confidence)

**Test Cases**:
1. **LiteSAM GPS**: Adds absolute factor, corrects drift
2. **User anchor**: High confidence, immediately refines trajectory
3. **Multiple absolute factors**: Graph optimizes to balance all

---

### `add_altitude_prior(frame_id: int, altitude: float, covariance: float) -> bool`

**Description**: Adds altitude constraint to resolve monocular scale ambiguity.

**Called By**:
- Main processing loop (for each frame)

**Input**:
```python
frame_id: int
altitude: float  # Predefined altitude in meters
covariance: float  # Altitude uncertainty (e.g., 50m)
```

**Output**:
```python
bool: True if prior added
```

**Processing Flow**:
1. Create UnaryFactor for Z-coordinate
2. Set as soft constraint (not hard constraint)
3. Add to factor graph

**Purpose**:
- Resolves scale ambiguity in monocular VO
- Prevents scale drift (trajectory collapsing or exploding)
- Soft constraint allows adjustment based on absolute GPS

**Test Cases**:
1. **Without altitude prior**: Scale drifts over time
2. **With altitude prior**: Scale stabilizes
3. **Conflicting measurements**: Optimizer balances VO and altitude

---

### `optimize(iterations: int) -> OptimizationResult`

**Description**: Runs optimization to refine trajectory.

**Called By**:
- Main processing loop (incremental after each frame)
- Asynchronous refinement thread (batch optimization)

**Input**:
```python
iterations: int  # Max iterations (typically 5-10 for incremental, 50-100 for batch)
```

**Output**:
```python
OptimizationResult:
    converged: bool
    final_error: float
    iterations_used: int
    optimized_frames: List[int]  # Frames with updated poses
```

**Processing Details**:
- **Incremental** (iSAM2): Updates only affected nodes
- **Batch**: Re-optimizes entire trajectory when new absolute factors added
- **Robust M-estimation**: Automatically downweights outliers

**Optimization Algorithm** (Levenberg-Marquardt):
1. Linearize factor graph around current estimate
2. Solve linear system
3. Update pose estimates
4. Check convergence (error reduction < threshold)

**Test Cases**:
1. **Incremental optimization**: Fast (<100ms), local update
2. **Batch optimization**: Slower (~500ms), refines entire trajectory
3. **Convergence**: Error reduces, converges within iterations

---

### `get_trajectory() -> Dict[int, Pose]`

**Description**: Retrieves complete optimized trajectory.

**Called By**:
- F13 Result Manager (for publishing results)
- F12 Coordinate Transformer (for GPS conversion)

**Input**: None

**Output**:
```python
Dict[int, Pose]:
    frame_id -> Pose:
        position: np.ndarray  # (x, y, z) in ENU
        orientation: np.ndarray  # Quaternion or rotation matrix
        timestamp: datetime
```

**Processing Flow**:
1. Extract all pose estimates from graph
2. Convert to appropriate coordinate system
3. Return dictionary

**Test Cases**:
1. **After optimization**: Returns all frame poses
2. **Refined trajectory**: Poses updated after batch optimization

---

### `get_marginal_covariance(frame_id: int) -> np.ndarray`

**Description**: Gets uncertainty (covariance) of a pose estimate.

**Called By**:
- F11 Failure Recovery Coordinator (to detect high uncertainty)

**Input**:
```python
frame_id: int
```

**Output**:
```python
np.ndarray: (6, 6) covariance matrix [x, y, z, roll, pitch, yaw]
```

**Purpose**:
- Uncertainty quantification
- Trigger user input when uncertainty too high (> 50m radius)

**Test Cases**:
1. **Well-constrained pose**: Small covariance
2. **Unconstrained pose**: Large covariance
3. **After absolute factor**: Covariance reduces

---

### `create_new_chunk(chunk_id: str, start_frame_id: int) -> ChunkHandle`

**Description**: Creates a new map fragment/chunk with its own subgraph.

**Called By**:
- F02 Flight Processor (when tracking lost)
- F12 Route Chunk Manager (chunk lifecycle)

**Input**:
```python
chunk_id: str  # Unique chunk identifier
start_frame_id: int  # First frame in chunk
```

**Output**:
```python
ChunkHandle:
    chunk_id: str
    flight_id: str
    start_frame_id: int
    end_frame_id: Optional[int]
    frames: List[int]
    is_active: bool
    has_anchor: bool
    anchor_frame_id: Optional[int]
    anchor_gps: Optional[GPSPoint]
    matching_status: str  # "unanchored", "matching", "anchored", "merged"
```

**Processing Flow**:
1. Create new subgraph for chunk
2. Initialize first frame pose in chunk's local coordinate system
3. Mark chunk as active
4. Return ChunkHandle

**Test Cases**:
1. **Create chunk**: Returns ChunkHandle with is_active=True
2. **Multiple chunks**: Can create multiple chunks simultaneously
3. **Chunk isolation**: Factors added to chunk don't affect other chunks

---

### `get_chunk_for_frame(frame_id: int) -> Optional[ChunkHandle]`

**Description**: Gets the chunk containing the specified frame (low-level factor graph query).

**Called By**:
- F12 Route Chunk Manager (for internal queries)
- F07 Sequential VO (to determine chunk context for factor graph operations)

**Note**: This is a low-level method for factor graph operations. For high-level chunk queries, use F12.get_active_chunk(flight_id).

**Input**:
```python
frame_id: int
```

**Output**:
```python
Optional[ChunkHandle]  # Active chunk or None if frame not in any chunk
```

**Test Cases**:
1. **Frame in active chunk**: Returns ChunkHandle
2. **Frame not in chunk**: Returns None
3. **Multiple chunks**: Returns correct chunk for frame

---

### `add_relative_factor_to_chunk(chunk_id: str, frame_i: int, frame_j: int, relative_pose: RelativePose, covariance: np.ndarray) -> bool`

**Description**: Adds relative pose measurement to a specific chunk's subgraph.

**Called By**:
- F07 Sequential VO (chunk-scoped operations)

**Input**:
```python
chunk_id: str
frame_i: int  # Previous frame ID
frame_j: int  # Current frame ID
relative_pose: RelativePose
covariance: np.ndarray  # (6, 6)
```

**Output**:
```python
bool: True if factor added successfully
```

**Processing Flow**:
1. Verify chunk exists and is active
2. Create BetweenFactor in chunk's subgraph
3. Apply robust kernel (Huber)
4. Add to chunk's factor graph
5. Mark chunk as needing optimization

**Test Cases**:
1. **Add to active chunk**: Factor added successfully
2. **Add to inactive chunk**: Returns False
3. **Multiple chunks**: Factors isolated to respective chunks

---

### `add_chunk_anchor(chunk_id: str, frame_id: int, gps: GPSPoint, covariance: np.ndarray) -> bool`

**Description**: Adds absolute GPS anchor to a chunk, enabling global localization.

**Called By**:
- F09 Metric Refinement (after chunk LiteSAM matching)
- F11 Failure Recovery Coordinator (chunk matching)

**Input**:
```python
chunk_id: str
frame_id: int  # Frame within chunk to anchor
gps: GPSPoint
covariance: np.ndarray  # (2, 2) or (3, 3)
```

**Output**:
```python
bool: True if anchor added
```

**Processing Flow**:
1. Convert GPS to ENU coordinates
2. Create PriorFactor in chunk's subgraph
3. Mark chunk as anchored
4. Trigger chunk optimization
5. Enable chunk merging

**Test Cases**:
1. **Anchor unanchored chunk**: Anchor added, has_anchor=True
2. **Anchor already anchored chunk**: Updates anchor
3. **Chunk optimization**: Chunk optimized after anchor

---

### `merge_chunk_subgraphs(flight_id: str, new_chunk_id: str, main_chunk_id: str, transform: Sim3Transform) -> bool`

**Description**: Merges new_chunk INTO main_chunk using Sim(3) similarity transformation. Extends main_chunk with new_chunk's frames.

**Called By**:
- F12 Route Chunk Manager (via merge_chunks method)

**Input**:
```python
flight_id: str        # Flight identifier
new_chunk_id: str     # New chunk being merged (source, typically newer/recently anchored)
main_chunk_id: str    # Main chunk being extended (destination, typically older/established)
transform: Sim3Transform:
    translation: np.ndarray  # (3,)
    rotation: np.ndarray  # (3, 3) or quaternion
    scale: float
```

**Output**:
```python
bool: True if merge successful
```

**Processing Flow**:
1. Verify both chunks exist and new_chunk is anchored
2. Apply Sim(3) transform to all poses in new_chunk
3. Merge new_chunk's subgraph into main_chunk's subgraph
4. Update frame-to-chunk mapping (new_chunk frames now belong to main_chunk)
5. Mark new_chunk subgraph as merged (F12 handles state updates)
6. Optimize merged graph globally

**Sim(3) Transformation**:
- Accounts for translation, rotation, and scale differences
- Critical for merging chunks with different scales (monocular VO)
- Preserves internal consistency of both chunks

**Test Cases**:
1. **Merge anchored chunks**: new_chunk merged into main_chunk successfully
2. **Merge unanchored chunk**: Returns False
3. **Global consistency**: Merged trajectory is globally consistent

---

### `get_chunk_trajectory(chunk_id: str) -> Dict[int, Pose]`

**Description**: Retrieves optimized trajectory for a specific chunk.

**Called By**:
- F12 Route Chunk Manager (chunk state queries)
- F14 Result Manager (result publishing)

**Input**:
```python
chunk_id: str
```

**Output**:
```python
Dict[int, Pose]  # Frame ID -> Pose in chunk's local coordinate system
```

**Test Cases**:
1. **Get chunk trajectory**: Returns all poses in chunk
2. **Empty chunk**: Returns empty dict
3. **After optimization**: Returns optimized poses

---

### `get_all_chunks() -> List[ChunkHandle]`

**Description**: Retrieves all chunks in the factor graph.

**Called By**:
- F11 Failure Recovery Coordinator (chunk matching coordination)
- F12 Route Chunk Manager (chunk state queries)

**Input**: None

**Output**:
```python
List[ChunkHandle]  # All chunks (active and inactive)
```

**Test Cases**:
1. **Multiple chunks**: Returns all chunks
2. **No chunks**: Returns empty list
3. **Mixed states**: Returns chunks in various states (active, anchored, merged)

---

### `optimize_chunk(chunk_id: str, iterations: int) -> OptimizationResult`

**Description**: Optimizes a specific chunk's subgraph independently.

**Called By**:
- F02 Flight Processor (after chunk anchor added)
- F12 Route Chunk Manager (periodic chunk optimization)

**Input**:
```python
chunk_id: str
iterations: int  # Max iterations (typically 5-10 for incremental)
```

**Output**:
```python
OptimizationResult:
    converged: bool
    final_error: float
    iterations_used: int
    optimized_frames: List[int]
    mean_reprojection_error: float
```

**Processing Flow**:
1. Extract chunk's subgraph
2. Run Levenberg-Marquardt optimization
3. Update poses in chunk's local coordinate system
4. Return optimization result

**Test Cases**:
1. **Optimize active chunk**: Chunk optimized successfully
2. **Optimize anchored chunk**: Optimization improves consistency
3. **Chunk isolation**: Other chunks unaffected

---

### `optimize_global(iterations: int) -> OptimizationResult`

**Description**: Optimizes all chunks and performs global merging.

**Called By**:
- Background optimization task (periodic)
- F11 Failure Recovery Coordinator (after chunk matching)

**Input**:
```python
iterations: int  # Max iterations (typically 50-100 for global)
```

**Output**:
```python
OptimizationResult:
    converged: bool
    final_error: float
    iterations_used: int
    optimized_frames: List[int]  # All frames across all chunks
    mean_reprojection_error: float
```

**Processing Flow**:
1. Collect all chunks
2. For anchored chunks, transform to global coordinate system
3. Optimize merged global graph
4. Update all chunk trajectories
5. Return global optimization result

**Test Cases**:
1. **Global optimization**: All chunks optimized together
2. **Multiple anchored chunks**: Global consistency achieved
3. **Performance**: Completes within acceptable time (<500ms)

## Integration Tests

### Test 1: Incremental Trajectory Building
1. Initialize graph with first frame
2. Add relative factors from VO × 100
3. Add altitude priors × 100
4. Optimize incrementally after each frame
5. Verify smooth trajectory

### Test 2: Drift Correction with Absolute GPS
1. Build trajectory with VO only (will drift)
2. Add absolute GPS factor at frame 50
3. Optimize → trajectory corrects
4. Verify frames 1-49 also corrected (back-propagation)

### Test 3: Outlier Handling
1. Add normal relative factors
2. Add 350m outlier factor (tilt error)
3. Optimize with robust kernel
4. Verify outlier downweighted, trajectory smooth

### Test 4: User Anchor Integration
1. Processing blocked at frame 237
2. User provides anchor (high confidence)
3. add_absolute_factor(is_user_anchor=True)
4. Optimize → trajectory snaps to anchor

### Test 5: Multi-Chunk Creation and Isolation
1. Create chunk_1 with frames 1-10
2. Create chunk_2 with frames 20-30 (disconnected)
3. Add relative factors to each chunk
4. Verify chunks optimized independently
5. Verify factors isolated to respective chunks

### Test 6: Chunk Anchoring and Merging
1. Create chunk_1 (frames 1-10), chunk_2 (frames 20-30)
2. Add chunk_anchor to chunk_2 (frame 25)
3. Optimize chunk_2 → local consistency improved
4. Merge chunk_2 into chunk_1 with Sim(3) transform
5. Optimize global → both chunks globally consistent
6. Verify final trajectory coherent across chunks

### Test 7: Simultaneous Multi-Chunk Processing
1. Create 3 chunks simultaneously (disconnected segments)
2. Process frames in each chunk independently
3. Anchor each chunk asynchronously
4. Merge chunks as anchors become available
5. Verify final global trajectory consistent

## Non-Functional Requirements

### Performance
- **Incremental optimize**: < 100ms per frame (iSAM2)
- **Batch optimize**: < 500ms for 100 frames
- **get_trajectory**: < 10ms
- Real-time capable: 10 FPS processing

### Accuracy
- **Mean Reprojection Error (MRE)**: < 1.0 pixels
- **GPS accuracy**: Meet 80% < 50m, 60% < 20m criteria
- **Trajectory smoothness**: No sudden jumps (except user anchors)

### Reliability
- Numerical stability for 2000+ frame trajectories
- Graceful handling of degenerate configurations
- Robust to missing/corrupted measurements

## Dependencies

### Internal Components
- **H03 Robust Kernels**: For Huber/Cauchy loss functions
- **H02 GSD Calculator**: For GSD computation and scale resolution

### External Dependencies
- **GTSAM**: Graph optimization library
- **numpy**: Matrix operations
- **scipy**: Sparse matrix operations (optional)

## Data Models

### Pose
```python
class Pose(BaseModel):
    frame_id: int
    position: np.ndarray  # (3,) - [x, y, z] in ENU
    orientation: np.ndarray  # (4,) quaternion or (3,3) rotation matrix
    timestamp: datetime
    covariance: Optional[np.ndarray]  # (6, 6)
```

### RelativePose
```python
class RelativePose(BaseModel):
    translation: np.ndarray  # (3,)
    rotation: np.ndarray  # (3, 3) or (4,)
    covariance: np.ndarray  # (6, 6)
```

### OptimizationResult
```python
class OptimizationResult(BaseModel):
    converged: bool
    final_error: float
    iterations_used: int
    optimized_frames: List[int]
    mean_reprojection_error: float
```

### FactorGraphConfig
```python
class FactorGraphConfig(BaseModel):
    robust_kernel_type: str = "Huber"  # or "Cauchy"
    huber_threshold: float = 1.0  # pixels
    cauchy_k: float = 0.1
    isam2_relinearize_threshold: float = 0.1
    isam2_relinearize_skip: int = 1
    max_chunks: int = 100  # Maximum number of simultaneous chunks
    chunk_merge_threshold: float = 0.1  # Error threshold for chunk merging
```

### ChunkHandle
```python
class ChunkHandle(BaseModel):
    chunk_id: str
    flight_id: str
    start_frame_id: int
    end_frame_id: Optional[int]
    frames: List[int]
    is_active: bool
    has_anchor: bool
    anchor_frame_id: Optional[int]
    anchor_gps: Optional[GPSPoint]
    matching_status: str  # "unanchored", "matching", "anchored", "merged"
```

### Sim3Transform
```python
class Sim3Transform(BaseModel):
    translation: np.ndarray  # (3,) - translation vector
    rotation: np.ndarray  # (3, 3) rotation matrix or (4,) quaternion
    scale: float  # Scale factor
```

