# Feature: Rotation Sweep Orchestration

## Description
Coordinates rotation sweeps by rotating images at 30° steps and delegating matching to an injected matcher (F09 Metric Refinement). Calculates precise angles from homography matrices after successful matches. This feature ties together image rotation and heading management with external matching.

## Component APIs Implemented
- `try_rotation_steps(flight_id: str, frame_id: int, image: np.ndarray, satellite_tile: np.ndarray, tile_bounds: TileBounds, timestamp: datetime, matcher: IImageMatcher) -> Optional[RotationResult]`
- `try_chunk_rotation_steps(chunk_images: List[np.ndarray], satellite_tile: np.ndarray, tile_bounds: TileBounds, matcher: IImageMatcher) -> Optional[RotationResult]`
- `calculate_precise_angle(homography: np.ndarray, initial_angle: float) -> float`

## External Tools and Services
- **H07 Image Rotation Utils**: Angle extraction from homography
- **IImageMatcher (injected)**: F09 Metric Refinement for align_to_satellite and align_chunk_to_satellite

## Internal Methods
- `_get_rotation_steps() -> List[float]`: Returns [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330]
- `_extract_rotation_from_homography(homography: np.ndarray) -> float`: Extracts rotation component from 3x3 homography
- `_combine_angles(initial_angle: float, delta_angle: float) -> float`: Combines step angle with homography delta, normalizes result
- `_select_best_result(results: List[Tuple[float, AlignmentResult]]) -> Tuple[float, AlignmentResult]`: Selects highest confidence match if multiple found

## Unit Tests

### calculate_precise_angle
1. **Identity homography**: Returns initial_angle unchanged
2. **Small rotation delta**: initial=60°, homography shows +2.5° → returns 62.5°
3. **Negative delta**: initial=30°, homography shows -3° → returns 27°
4. **Large delta normalization**: initial=350°, delta=+20° → returns 10°
5. **Invalid homography (singular)**: Returns initial_angle as fallback
6. **Near-zero homography**: Returns initial_angle as fallback

### try_rotation_steps (with mock matcher)
1. **Match at 0°**: First rotation matches, returns RotationResult with initial_angle=0
2. **Match at 60°**: Third rotation matches, returns RotationResult with initial_angle=60
3. **Match at 330°**: Last rotation matches, returns RotationResult with initial_angle=330
4. **No match**: All 12 rotations fail, returns None
5. **Multiple matches**: Returns highest confidence result
6. **Heading updated**: After match, flight heading is updated
7. **Confidence threshold**: Match below threshold rejected

### try_chunk_rotation_steps (with mock matcher)
1. **Match at 0°**: First rotation matches chunk
2. **Match at 120°**: Returns RotationResult with initial_angle=120
3. **No match**: All 12 rotations fail, returns None
4. **Chunk consistency**: All images rotated by same angle before matching
5. **Does not update heading**: Chunk matching doesn't affect flight state

## Integration Tests

### Test 1: First Frame Rotation Sweep
1. Create flight with no heading
2. Call try_rotation_steps with image, satellite tile, mock matcher
3. Mock matcher returns match at 60° rotation
4. Verify RotationResult.initial_angle = 60
5. Verify RotationResult.precise_angle refined from homography
6. Verify flight heading updated to precise_angle

### Test 2: Full Sweep No Match
1. Call try_rotation_steps with mock matcher that never matches
2. Verify all 12 rotations attempted (0°, 30°, ..., 330°)
3. Verify returns None
4. Verify flight heading unchanged

### Test 3: Chunk Rotation Sweep
1. Create chunk with 10 images
2. Call try_chunk_rotation_steps with mock matcher
3. Mock matcher returns match at 90° rotation
4. Verify all 10 images were rotated before matching call
5. Verify RotationResult returned with correct angles

### Test 4: Precise Angle Calculation
1. Perform rotation sweep, match at 60° step
2. Homography indicates +2.3° additional rotation
3. Verify precise_angle = 62.3°
4. Verify heading updated to 62.3° (not 60°)

