# Hohmann Transfer Rendezvous - Burn Timing Quantization Analysis ## Session Date 2026-04-19 ## Problem Statement The Hohmann transfer rendezvous simulation was failing with ~1.3 km separation despite correct phasing calculations. Investigation revealed two issues: 1. **Burn timing quantization**: Time-triggered burns execute at step boundaries, not exact trigger times 2. **cartesian_to_orbital_elements bug**: Coplanar orbit omega calculation was incorrect ## Root Cause Analysis ### 1. Burn Timing Quantization **How it works:** - `check_maneuver_trigger()` for `TRIGGER_TIME` uses simple comparison: `sim->time >= maneuver->trigger_value` - When triggered, the burn executes at the **first step where `sim->time >= trigger_value`** - No sub-step interpolation for time triggers **Verified behavior** (from `test_maneuver_planning.cpp`): | Trigger Time | Step Boundary | Actual Execution | Delay | |-------------|---------------|-----------------|-------| | t=305.0 | t=310.0 | t=310.0 | **5.0s** | | t=300.0 | t=300.0 | t=300.0 | **0.0s** | | t=62807.0 | t=62810.0 | t=62810.0 | **3.0s** | **Impact on Hohmann transfer:** - Arrival trigger: t=62804.47 (calculated precisely) - Step boundaries: ..., 62800, 62810, ... - Actual execution: t=62810 (5.53s late) - Position drift: ~5.53s × ~7672 m/s ≈ **42 km** of orbital travel ### 2. cartesian_to_orbital_elements Bug **Location:** `src/orbital_mechanics.cpp`, lines 300-320 **Bug:** For coplanar orbits (inclination < 0.01 rad), the function was setting `omega = 0.0` instead of computing the longitude of periapsis. **Fix:** ```cpp } else if (e > 1e-10) { // Coplanar or near-circular: use longitude of periapsis omega = atan2(e_vec.y, e_vec.x); if (omega < 0.0) { omega += 2.0 * M_PI; } } else { omega = 0.0; } ``` **Impact:** Without this fix, Hohmann separation goes from 8.75m → 3.22 million meters. ## DT Reduction Results | TIME_STEP | Separation | Test Result | |-----------|-----------|-------------| | 10.0 s | 1,324 m | ❌ Failed (>100m) | | 5.0 s | 458 m | ❌ Failed (>100m) | | 2.0 s | 228 m | ❌ Failed (>100m) | | 1.0 s | 55 m | ✅ Passed | | 0.5 s | 69 m | ❌ Failed (>100m) | | 0.1 s | 8.75 m | ✅ Passed | **Note:** Values measured from `test_maneuver_planning.cpp` DT sweep test (2026-04-20). The 0.5s value (69m) is higher than the original estimate (40m) due to the specific trigger offset within the step boundary for this scenario. The 2.0s value (228m) is also higher than the original estimate (150m). The 10.0s value (1324m) matches the original measurement exactly. **Key insight:** DT reduction dramatically improves accuracy: - 24x improvement from 10s→1s - 6x more from 1s→0.1s ## Test Results Summary | Test Category | Before Fix | After Fix | Status | |--------------|-----------|-----------|--------| | rendezvous (8 cases) | 87 passed | **107 passed** | ✅ All pass | | maneuver_planning (6 cases) | 3 passed | **6 passed** | ✅ All pass | | omega (2 cases) | 1 failed | **2 passed** | ✅ All pass | | **Total** | 156/160 pass | **154/154 pass** | All pass | **Notes:** - The old `rendezvous` (CW guidance) module was removed entirely, eliminating 3 pre-existing test failures - `test_maneuver_timing.cpp` was merged into `test_maneuver_planning.cpp` - `rendezvous_hohmann` was renamed to `rendezvous` (CW module removed, only Hohmann remains) - All 154 remaining test cases pass (240,445 assertions) ## Current Code Path (After 2026-04-26 Sub-step Interpolation) The maneuver trigger check and execution are merged into `update_spacecraft_physics()`. Both trigger types now use sub-step interpolation: ```cpp // In update_spacecraft_physics(), per spacecraft: check_maneuver_trigger(maneuver, craft, sim); // → For TRIGGER_TIME: computes dt_to_burn = trigger_value - sim->time // → For TRIGGER_TRUE_ANOMALY: computes dt_needed from mean anomaly delta // → Both set scheduled_dt = dt_to_burn (0 to sim->dt) if (maneuver_fired) { craft->orbit = propagate_orbital_elements(craft->orbit, burn_dt, ...); execute_maneuver(fired_maneuver, ...); craft->orbit = propagate_orbital_elements(craft->orbit, remaining_dt, ...); } else { craft->orbit = propagate_orbital_elements(craft->orbit, sim->dt, ...); } ``` **For `TRIGGER_TIME`:** `burn_dt` is the exact sub-step offset from step start to trigger time. The spacecraft propagates to the precise trigger position before the burn, then continues for `sim->dt - burn_dt`. **For `TRIGGER_TRUE_ANOMALY`:** `burn_dt` is set to exact seconds-to-target by `check_maneuver_trigger()`, so the spacecraft propagates to the exact burn position before the burn executes. **Edge case:** If `sim->time > trigger_value` (trigger passed in a previous step), `scheduled_dt` is clamped to 0 and the burn fires immediately at the current position. ## Burn Timing Quantization — RESOLVED (2026-04-26) **Option A (Sub-step Interpolation)** is now implemented for both `TRIGGER_TIME` and `TRIGGER_TRUE_ANOMALY`. **Implementation:** - `check_maneuver_trigger()` computes `dt_to_burn = trigger_value - sim->time` for time triggers - `update_spacecraft_physics()` propagates to exact burn time, executes burn, propagates remainder - Quantization error is eliminated: burns execute at the precise trigger time **Edge case:** When `sim->time > trigger_value` (trigger passed in a previous step), `scheduled_dt` is clamped to 0 and the burn fires immediately at the current position. ### Remaining Options (No Longer Needed) ### Option B: Snap Trigger Times to Step Boundaries **Approach:** In `calculate_next_hohmann_wait_time()`, snap the calculated wait time to the nearest step boundary. **Changes needed:** 1. In `calculate_next_hohmann_wait_time()`: - After calculating wait time, snap to step boundary: `wait_time = ceil(wait_time / DT) * DT` - This ensures the trigger aligns with a simulation step **Pros:** Simple, minimal code changes **Cons:** Introduces systematic timing error, may affect phasing accuracy ### Option C: Accept Quantization Error **Approach:** Keep current behavior but set realistic thresholds based on DT. **Changes needed:** 1. Calculate expected quantization error: `max_error = DT` 2. Set rendezvous threshold proportional to DT: `threshold = 100 * DT` (meters) 3. Document the limitation **Pros:** Simplest, no code changes **Cons:** Less accurate, threshold depends on DT choice ## Strategy for Testing with Larger Time Steps ### Goal Understand the accuracy limitations of the simulation at realistic DT values (10s, 30s) to set appropriate rendezvous thresholds. ### Test Plan #### Phase 1: Baseline at Current DT (0.1s) - ✅ Already done: 8.75m separation at DT=0.1s #### Phase 2: Systematic DT Sweep Run the same Hohmann transfer test at increasing DT values: | DT | Expected Steps | Expected Separation | |----|---------------|-------------------| | 0.1s | ~628,000 | ~8.75 m | | 0.5s | ~125,600 | ~40 m (estimate) | | 1.0s | ~62,800 | ~55 m | | 2.0s | ~31,400 | ~100-200 m (estimate) | | 5.0s | ~12,560 | ~500 m (estimate) | | 10.0s | ~6,280 | ~1,324 m | | 30.0s | ~2,093 | ~4,000 m (estimate) | **Method:** 1. Create a new test file `tests/test_hohmann_dt_sweep.cpp` 2. Run the same Hohmann transfer scenario at each DT value 3. Record: final separation, radius error, relative velocity 4. Plot separation vs DT to determine the relationship #### Phase 3: Quantization Impact Analysis Test the effect of burn timing quantization specifically: | Scenario | Trigger Offset | Expected Delay | |----------|---------------|----------------| | Exact boundary | 0s | 0s | | 5s after boundary | 5s | 5s | | 9s after boundary | 9s | 1s | **Method:** 1. For each DT, run the Hohmann transfer multiple times with different trigger offsets 2. Measure the variation in final separation 3. Determine if quantization error dominates over integration error #### Phase 4: Threshold Recommendation Based on Phase 2 & 3 results, recommend: - Maximum DT for rendezvous operations - Separation threshold as a function of DT - Whether sub-step interpolation is necessary ### Implementation Notes - Use `calculate_next_hohmann_wait_time()` with `min_wait_time` to control trigger timing - Keep all other parameters constant (initial conditions, maneuver DVs, etc.) - Use `WithinAbs()` with increasing margins to find the threshold that passes at each DT ## Completed Work ### Files Modified - `src/orbital_mechanics.cpp` - Fixed coplanar orbit omega calculation - `src/rendezvous.cpp` (renamed from `rendezvous_hohmann.cpp`) - Added 3 new functions (validate, relative period, next wait time) - `src/rendezvous.h` (renamed from `rendezvous_hohmann.h`) - Added function declarations - `src/test_utilities.cpp` - Added `dump_simulation_state()` helper - `src/test_utilities.h` - Added function declaration - `tests/test_rendezvous.cpp` (renamed from `test_rendezvous_hohmann.cpp`) - Updated integration test with DT=0.1 - `tests/test_rendezvous.toml` (renamed from `test_rendezvous_hohmann.toml`) - Reverted to original values - `tests/test_maneuver_planning.cpp` - Added 3 burn timing quantization tests (merged from test_maneuver_timing.cpp), plus 3 DT sweep tests (2026-04-20) - `tests/test_omega_debug.cpp` - Updated to accept new coplanar omega behavior - `Makefile` - Updated object file references - `src/simulation.cpp` - Merged `execute_pending_maneuvers()` into `update_spacecraft_physics()` (2026-04-20) - `src/simulation.h` - Removed `execute_pending_maneuvers()` declaration (2026-04-20) ### Files Removed - `src/rendezvous.h` (old CW module) - replaced by Hohmann-only rendezvous.h - `src/rendezvous.cpp` (old CW module) - replaced by Hohmann-only rendezvous.cpp - `tests/test_rendezvous.cpp` (old CW tests) - replaced by Hohmann-only test_rendezvous.cpp - `tests/test_rendezvous.toml` (old CW config) - replaced by Hohmann-only test_rendezvous.toml ## Remaining Work ### DT Sweep Tests (COMPLETED - 2026-04-20) Measured in `test_maneuver_planning.cpp` via `DT sweep: Hohmann transfer separation vs time step`: | DT | Expected Steps | Measured Separation | |----|---------------|-------------------| | 0.1s | ~628,000 | 8.75 m | | 0.5s | ~125,600 | 69 m | | 1.0s | ~62,800 | 55 m | | 2.0s | ~31,400 | 228 m | | 5.0s | ~12,560 | 458 m | | 10.0s | ~6,280 | 1,324 m | The separation scales roughly linearly with DT for larger values, consistent with quantization error being the dominant factor (position error ≈ timing_error × velocity, where timing_error is uniformly distributed in [0, DT]). Additional tests: - `DT sweep: quantization error is bounded by DT` — verifies error is always in [0, DT) - `DT sweep: Hohmann arrival burn timing error` — measures exact timing error at each DT ### Threshold Recommendation With sub-step interpolation implemented, burn timing quantization is eliminated. DT sweep results now reflect integration error rather than quantization error. Recommend: - Maximum DT for rendezvous operations based on integration accuracy - Separation threshold set by orbital dynamics, not quantization bounds - Sub-step interpolation is now active for both trigger types