Browse Source

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
main
cinnaboot 3 months ago
parent
commit
eade44a245
  1. 54
      docs/planning/hohmann-rendezvous-quantization-fix.md
  2. 40
      docs/planning/propagation_refactor.md
  3. 17
      docs/technical_reference.md
  4. 5
      src/simulation.cpp

54
docs/planning/hohmann-rendezvous-quantization-fix.md

@ -88,52 +88,44 @@ original measurement exactly.
- `rendezvous_hohmann` was renamed to `rendezvous` (CW module removed, only Hohmann remains) - `rendezvous_hohmann` was renamed to `rendezvous` (CW module removed, only Hohmann remains)
- All 154 remaining test cases pass (240,445 assertions) - All 154 remaining test cases pass (240,445 assertions)
## Current Code Path (After 2026-04-20 Refactoring) ## Current Code Path (After 2026-04-26 Sub-step Interpolation)
The maneuver trigger check and execution are now merged into `update_spacecraft_physics()`: The maneuver trigger check and execution are merged into `update_spacecraft_physics()`. Both trigger types now use sub-step interpolation:
```cpp ```cpp
// In update_spacecraft_physics(), per spacecraft: // In update_spacecraft_physics(), per spacecraft:
check_maneuver_trigger(maneuver, craft, sim); check_maneuver_trigger(maneuver, craft, sim);
// → For TRIGGER_TIME: returns true if sim->time >= trigger_value // → For TRIGGER_TIME: computes dt_to_burn = trigger_value - sim->time
// → Sets scheduled_dt = 0 (never set for time triggers) // → 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) { if (maneuver_fired) {
craft->orbit = propagate_orbital_elements(craft->orbit, burn_dt, ...); craft->orbit = propagate_orbital_elements(craft->orbit, burn_dt, ...);
// burn_dt = 0 for TRIGGER_TIME → no-propagate
execute_maneuver(fired_maneuver, ...); execute_maneuver(fired_maneuver, ...);
craft->orbit = propagate_orbital_elements(craft->orbit, remaining_dt, ...); craft->orbit = propagate_orbital_elements(craft->orbit, remaining_dt, ...);
// remaining_dt = sim->dt
} else { } else {
craft->orbit = propagate_orbital_elements(craft->orbit, sim->dt, ...); craft->orbit = propagate_orbital_elements(craft->orbit, sim->dt, ...);
} }
``` ```
**For `TRIGGER_TIME`:** `burn_dt` is always 0, so the sequence is: **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`.
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. **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 (Recommended) ## Burn Timing Quantization — RESOLVED (2026-04-26)
**Approach:** When a burn trigger is detected between steps, propagate to the exact trigger time before executing.
**Changes needed:** **Option A (Sub-step Interpolation)** is now implemented for both `TRIGGER_TIME` and `TRIGGER_TRUE_ANOMALY`.
1. In `check_maneuver_trigger()` for `TRIGGER_TIME`:
- When `sim->time >= trigger_value`, calculate `dt_to_burn = trigger_value - (sim->time - sim->dt)` **Implementation:**
- Set `maneuver->scheduled_dt = dt_to_burn` - `check_maneuver_trigger()` computes `dt_to_burn = trigger_value - sim->time` for time triggers
- Return `true` - `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()`: **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.
- 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 ### Remaining Options (No Longer Needed)
**Cons:** More complex, requires careful handling of edge cases
### Option B: Snap Trigger Times to Step Boundaries ### 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. **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 ## 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) ### DT Sweep Tests (COMPLETED - 2026-04-20)
Measured in `test_maneuver_planning.cpp` via `DT sweep: Hohmann transfer separation vs time step`: 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
### Threshold Recommendation With sub-step interpolation implemented, burn timing quantization is eliminated. DT sweep results now reflect integration error rather than quantization error. Recommend:
Based on DT sweep results, recommend: - Maximum DT for rendezvous operations based on integration accuracy
- Maximum DT for rendezvous operations - Separation threshold set by orbital dynamics, not quantization bounds
- Separation threshold as a function of DT - Sub-step interpolation is now active for both trigger types
- Whether sub-step interpolation is necessary for production use

40
docs/planning/propagation_refactor.md

@ -116,11 +116,11 @@ The function previously performed two distinct responsibilities:
### Issue 3: Ambiguous `scheduled_dt` Semantics ### Issue 3: Ambiguous `scheduled_dt` Semantics
**Status:** PARTIALLY RESOLVED — semantics are now clearer but still differ by trigger type. **Status:** RESOLVED — both trigger types now set `scheduled_dt` to the exact sub-step offset.
| Trigger Type | `scheduled_dt` meaning | Set by | | Trigger Type | `scheduled_dt` meaning | Set by |
|-------------|----------------------|--------| |-------------|----------------------|--------|
| `TRIGGER_TIME` | Always 0.0 (never set) | Never | | `TRIGGER_TIME` | `trigger_value - sim->time` (step start) | `check_maneuver_trigger()` |
| `TRIGGER_TRUE_ANOMALY` | Seconds until exact burn position | `check_maneuver_trigger()` | | `TRIGGER_TRUE_ANOMALY` | Seconds until exact burn position | `check_maneuver_trigger()` |
In `update_spacecraft_physics()`: 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 ### Issue 4: Time-Triggered Burns Propagate from Wrong State
**Status:** UNRESOLVED — root cause of burn timing quantization. **Status:** RESOLVED — sub-step interpolation implemented (2026-04-26).
For time triggers, the sequence is: For time triggers, the sequence is now:
``` ```
Frame N: sim->time = 310.0 (trigger_value = 305.0) Frame N: sim->time = 310.0 (trigger_value = 305.0)
check: 310 >= 305 → true check: sim->time >= trigger_value → true
burn_dt = 0 dt_to_burn = trigger_value - (sim->time - sim->dt) = 305 - 300 = 5.0
remaining_dt = 10 burn_dt = 5.0
execute_maneuver() → burn applies at sim->time=310 remaining_dt = 5.0
propagate craft by 10s starting from sim->time=310 propagate(5.0) → craft at trigger position
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 ### 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 ## 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) ### Parabolic/Hyperbolic Orbit Support (TODO in code)
The analytical `dt_needed` calculation in `check_maneuver_trigger()` only handles elliptical orbits. TODO comment added for: The analytical `dt_needed` calculation in `check_maneuver_trigger()` only handles elliptical orbits. TODO comment added for:
- Parabolic: Barker's equation `D + D³/3 = M`, where `D = tan(ν/2)` - Parabolic: Barker's equation `D + D³/3 = M`, where `D = tan(ν/2)`
- Hyperbolic: `e·sinh(H) - H = M`, using hyperbolic anomaly `H` - 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.

17
docs/technical_reference.md

@ -131,16 +131,17 @@ Sequence: argument_of_periapsis (ω) → inclination (i) → longitude_of_ascend
- BURN_CUSTOM: user-specified vector - BURN_CUSTOM: user-specified vector
### Exact Position Execution ### Exact Position Execution
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` 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. If `0 < dt_needed <= sim->dt`, trigger fires and `scheduled_dt` is set 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. In `update_spacecraft_physics()`, for each spacecraft: check all pending maneuvers for that craft 3. If `0 < scheduled_dt <= sim->dt`, trigger fires and `scheduled_dt` is set.
4. If a maneuver fires: propagate by `burn_dt` (scheduled_dt), execute burn, propagate remaining (`sim->dt - burn_dt`) 4. In `update_spacecraft_physics()`, for each spacecraft: check all pending maneuvers for that craft.
5. No separate maneuver execution step — all inline in the spacecraft propagation loop 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_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 supported via analytical mean anomaly calculation. **TRIGGER_TRUE_ANOMALY**: Sub-step timing via analytical mean anomaly calculation.
**Future TODO**: Parabolic (Barker's equation) and hyperbolic branches for `check_maneuver_trigger()`. **Future TODO**: Parabolic (Barker's equation) and hyperbolic branches for `check_maneuver_trigger()`.

5
src/simulation.cpp

@ -301,11 +301,6 @@ void update_spacecraft_physics(SimulationState* sim) {
// to the burn time, execute the burn, then propagate the remaining // to the burn time, execute the burn, then propagate the remaining
// timestep. This enables sub-step interpolation for true-anomaly // timestep. This enables sub-step interpolation for true-anomaly
// triggers and sets up future interpolated time triggers. // triggers and sets up future interpolated time triggers.
// TODO: Test interpolated burns for TRIGGER_TIME — currently only
// TRIGGER_TRUE_ANOMALY provides sub-step timing via scheduled_dt.
// For time triggers, scheduled_dt is always 0, so the burn applies
// at sim->time (step boundary). Could compute dt_to_burn =
// trigger_value - (sim->time - sim->dt) for exact placement.
bool maneuver_fired = false; bool maneuver_fired = false;
double burn_dt = 0.0; double burn_dt = 0.0;
Maneuver* fired_maneuver = NULL; Maneuver* fired_maneuver = NULL;

Loading…
Cancel
Save