# 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-20 Refactoring) The maneuver trigger check and execution are now merged into `update_spacecraft_physics()`: ```cpp // In update_spacecraft_physics(), per spacecraft: check_maneuver_trigger(maneuver, craft, sim); // → For TRIGGER_TIME: returns true if sim->time >= trigger_value // → Sets scheduled_dt = 0 (never set for time triggers) if (maneuver_fired) { craft->orbit = propagate_orbital_elements(craft->orbit, burn_dt, ...); // burn_dt = 0 for TRIGGER_TIME → no-propagate execute_maneuver(fired_maneuver, ...); craft->orbit = propagate_orbital_elements(craft->orbit, remaining_dt, ...); // remaining_dt = sim->dt } else { craft->orbit = propagate_orbital_elements(craft->orbit, sim->dt, ...); } ``` **For `TRIGGER_TIME`:** `burn_dt` is always 0, so the sequence is: 1. `propagate_orbital_elements(craft->orbit, 0, ...)` → no-op 2. `execute_maneuver()` → burn applies at current position 3. `propagate_orbital_elements(craft->orbit, sim->dt, ...)` → propagate full step **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. ## Suggested Fixes for Burn Timing Quantization ### Option A: Sub-step Interpolation (Recommended) **Approach:** When a burn trigger is detected between steps, propagate to the exact trigger time before executing. **Changes needed:** 1. In `check_maneuver_trigger()` for `TRIGGER_TIME`: - When `sim->time >= trigger_value`, calculate `dt_to_burn = trigger_value - (sim->time - sim->dt)` - Set `maneuver->scheduled_dt = dt_to_burn` - Return `true` 2. In `update_spacecraft_physics()`: - When `burn_dt > 0`, propagate the spacecraft to the exact burn time - Execute the burn - Propagate the remaining `sim->dt - burn_dt` **Pros:** Exact timing, no analytical drift **Cons:** More complex, requires careful handling of edge cases ### 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 ### Burn Timing Quantization (Optional - future) 1. **Option A**: Implement sub-step interpolation in `check_maneuver_trigger()` for `TRIGGER_TIME` — set `scheduled_dt = trigger_value - (sim->time - sim->dt)` for exact placement 2. **Option B**: Snap trigger times to step boundaries in `calculate_next_hohmann_wait_time()` 3. **Option C**: Accept current behavior and set realistic thresholds based on DT ### 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 ### Threshold Recommendation Based on DT sweep results, recommend: - Maximum DT for rendezvous operations - Separation threshold as a function of DT - Whether sub-step interpolation is necessary for production use