resolve burn timing quantization: sub-step interpolation for TRIGGER_TIME, update docs
- Remove TODO from simulation.cpp (interpolated burns now implemented)
- Update technical_reference.md: both trigger types now use sub-step
propagation to exact trigger time, not step boundary
- Update hohmann-rendezvous-quantization-fix.md: mark Option A as
resolved, remove outdated quantization fix suggestions
- Update propagation_refactor.md: resolve Issues 3 and 4, replace
interpolated time triggers TODO with code path unification TODO
2. `execute_maneuver()` → burn applies at current position
3. `propagate_orbital_elements(craft->orbit, sim->dt, ...)` → propagate full step
**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.
## Suggested Fixes for Burn Timing Quantization
**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.
**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
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`
**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.
**Pros:** Exact timing, no analytical drift
**Cons:** More complex, requires careful handling of edge cases
### 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.
@ -235,11 +227,6 @@ Based on Phase 2 & 3 results, recommend:
## 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`:
@ -263,8 +250,7 @@ Additional tests:
### 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
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
| `TRIGGER_TRUE_ANOMALY` | Seconds until exact burn position | `check_maneuver_trigger()` |
In `update_spacecraft_physics()`:
@ -133,26 +133,28 @@ if (maneuver_fired) {
}
```
For `TRIGGER_TIME`, `burn_dt == 0` is still a coincidence — the field is never set. The branching is now explicit via `maneuver_fired` flag, which is clearer.
Both trigger types now produce the same `scheduled_dt` semantics: seconds from step start until the burn executes. The only difference is how that value is computed (simple subtraction vs. mean anomaly propagation).
### Issue 4: Time-Triggered Burns Propagate from Wrong State
**Status:** UNRESOLVED — root cause of burn timing quantization.
execute_maneuver() → burn applies at sim->time=305
propagate(5.0) → continue simulation
```
The craft's orbit state is at `sim->time=310`, not at the trigger time `305`. The burn fires 5s late and the post-burn propagation starts from the wrong orbital position. This is the root cause of the burn timing quantization problem documented in `docs/planning/hohmann-rendezvous-quantization-fix.md`.
The spacecraft propagates to the exact trigger position before the burn, then continues for the remaining timestep. This eliminates the burn timing quantization problem documented in `docs/planning/hohmann-rendezvous-quantization-fix.md`.
**TODO in code:** `update_spacecraft_physics()` has a TODO suggesting `dt_to_burn = trigger_value - (sim->time - sim->dt)` for exact placement.
**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.
### Issue 5: Hardcoded Array Size
@ -200,18 +202,12 @@ Remaining: 4 call sites (1 for bodies, 3 for spacecraft), 2 distinct contexts fo
## Remaining Work
### Interpolated Time Triggers (TODO in code)
For `TRIGGER_TIME`, `scheduled_dt` is always 0, so the burn applies at `sim->time` (step boundary). To achieve exact placement:
1. In `check_maneuver_trigger()` for `TRIGGER_TIME`: compute `dt_to_burn = trigger_value - (sim->time - sim->dt)` when `sim->time >= trigger_value`
2. Set `scheduled_dt = dt_to_burn` (positive value, 0 to `sim->dt`)
3. The propagation loop then does: `propagate(dt_to_burn)` → burn → `propagate(remaining)`
This would place the burn at the exact trigger time, not the step boundary. See `docs/planning/hohmann-rendezvous-quantization-fix.md` for quantization analysis.
### Parabolic/Hyperbolic Orbit Support (TODO in code)
The analytical `dt_needed` calculation in `check_maneuver_trigger()` only handles elliptical orbits. TODO comment added for:
- Hyperbolic: `e·sinh(H) - H = M`, using hyperbolic anomaly `H`
### Code Path Unification (TODO in maneuver.cpp)
A TODO was added to combine the TRIGGER_TIME and TRIGGER_TRUE_ANOMALY branches in `check_maneuver_trigger()` into a single code path. Both now compute `scheduled_dt` the same way (sub-step offset), so the logic can be unified. The main complication is maneuver array ordering: when an earlier maneuver fires and changes the orbit, cached trigger times for later maneuvers become stale and must be recomputed.
True anomaly triggers use analytical mean anomaly delta to compute exact time to target, eliminating per-frame propagation probes:
Both trigger types use sub-step propagation to execute burns at the exact trigger time, eliminating quantization error:
1. `check_maneuver_trigger()` converts current and target true anomaly to mean anomaly, computes delta-M, divides by mean motion to get `dt_needed`
2. If `0 < dt_needed <= sim->dt`, trigger fires and `scheduled_dt` is set
3. In `update_spacecraft_physics()`, for each spacecraft: check all pending maneuvers for that craft
4. If a maneuver fires: propagate by `burn_dt` (scheduled_dt), execute burn, propagate remaining (`sim->dt - burn_dt`)
5. No separate maneuver execution step — all inline in the spacecraft propagation loop
1. **TRIGGER_TIME**: `check_maneuver_trigger()` computes `dt_to_burn = trigger_value - sim->time` (sim->time is step start). If the trigger falls within `[sim->time, sim->time + sim->dt]`, `scheduled_dt` is set to this offset.
2. **TRIGGER_TRUE_ANOMALY**: `check_maneuver_trigger()` converts current and target true anomaly to mean anomaly, computes delta-M, divides by mean motion to get `dt_needed`.
3. If `0 < scheduled_dt <= sim->dt`, trigger fires and `scheduled_dt` is set.
4. In `update_spacecraft_physics()`, for each spacecraft: check all pending maneuvers for that craft.
5. If a maneuver fires: propagate by `burn_dt` (scheduled_dt), execute burn, propagate remaining (`sim->dt - burn_dt`).
6. No separate maneuver execution step — all inline in the spacecraft propagation loop.
**TRIGGER_TIME**: `scheduled_dt` is always 0 (burn at step boundary, quantization error in [0, DT)).
**TRIGGER_TRUE_ANOMALY**: Sub-step timing supported via analytical mean anomaly calculation.
**TRIGGER_TIME**: `scheduled_dt` is 0 only when the trigger time has already passed (clamped to fire immediately). Otherwise it is the exact sub-step offset from step start.
**TRIGGER_TRUE_ANOMALY**: Sub-step timing via analytical mean anomaly calculation.
**Future TODO**: Parabolic (Barker's equation) and hyperbolic branches for `check_maneuver_trigger()`.