9.6 KiB
Propagation Call Chain Analysis
Session Date
2026-04-20
Objective
Audit all call sites of propagate_orbital_elements() for spacecraft and trace the update_simulation() call chain through execute_pending_maneuvers() and update_spacecraft_physics() to identify inefficiencies and confusing branching.
Recent Changes
2026-04-20: Eliminated Redundant Propagation in check_maneuver_trigger()
Replaced the per-frame propagation probe in check_maneuver_trigger() (true-anomaly branch) with a direct analytical calculation using mean anomaly delta.
What changed:
- Removed
propagate_orbital_elements()call fromcheck_maneuver_trigger()— eliminates ~24k redundant Kepler solves per Hohmann transfer wait period - Removed dead
angle_between()static helper - Replaced the
angle_between,future_diff, andwraparound_crossingchecks with a cleanerdt_needed <= 0.0guard - Added TODO comment noting elliptical-orbits-only limitation
Impact:
- Same correctness, fewer function calls, simpler logic
- All 154 tests pass (240,445 assertions)
- For a Hohmann transfer with ~244,000s wait time and DT=10s: ~24,400 fewer
propagate_orbital_elements()calls
Remaining: See Issue 1 below — still has a TODO for parabolic/hyperbolic orbit support.
2026-04-20: Merged Maneuver Execution into Single Propagation Pass
Consolidated execute_pending_maneuvers() into update_spacecraft_physics() so every spacecraft goes through exactly one propagation path.
What changed:
- Removed
spacecraft_handled_this_frame[256]static array (magic number) - Removed
reset_spacecraft_tracking()function - Removed
execute_pending_maneuvers()function - Check maneuver triggers before propagation in
update_spacecraft_physics - Propagate to burn time, execute burn, propagate remainder (same order as old code)
- Removed
execute_pending_maneuversdeclaration fromsimulation.h
Impact:
- Single propagation call per spacecraft per frame (was 2 separate code paths)
- No static array with hardcoded size
- Simpler call chain: no separate maneuver pass
- Enables sub-step interpolation for true-anomaly triggers
- Sets up foundation for interpolated time triggers
Remaining: Adds TODO in update_spacecraft_physics about testing interpolated burns.
Call Chain: update_simulation() (Current)
update_simulation()
│
├── update_bodies_physics()
│ └── for each body:
│ ├── find_dominant_body()
│ ├── orbital_elements_to_cartesian() → expected_vel
│ ├── velocity drift check (every frame)
│ ├── cartesian_to_orbital_elements() (if drift > 1e-6)
│ └── propagate_orbital_elements()
│
├── compute_global_coordinates()
│
├── update_spacecraft_physics()
│ └── for each craft:
│ ├── orbital_elements_to_cartesian() → expected_vel
│ ├── velocity drift check
│ ├── cartesian_to_orbital_elements() (if drift)
│ ├── check_maneuver_trigger() ← inline, before propagation
│ │ ├── TRIGGER_TIME: sim->time >= trigger_value
│ │ └── TRIGGER_TRUE_ANOMALY:
│ │ └── analytical mean anomaly delta (no propagation)
│ └── if maneuver fired:
│ │ ├── propagate_orbital_elements(burn_dt)
│ │ ├── execute_maneuver()
│ │ │ ├── apply_impulsive_burn()
│ │ │ └── cartesian_to_orbital_elements()
│ │ └── propagate_orbital_elements(remaining_dt)
│ └── else:
│ └── propagate_orbital_elements(sim->dt)
│
├── compute_spacecraft_globals()
└── sim->time += sim->dt
Issues Found
Issue 1: Redundant Propagation for True-Anomaly Triggers
Status: RESOLVED — eliminated in favor of analytical mean anomaly delta calculation.
Location: src/maneuver.cpp, check_maneuver_trigger() (true-anomaly branch)
Previously, for every frame that a true-anomaly maneuver was pending, propagate_orbital_elements() was called as a "look-ahead" probe to determine if the target angle was approaching.
// OLD (maneuver.cpp:147) — REMOVED
OrbitalElements future_elements = propagate_orbital_elements(craft->orbit, sim->dt, parent->mass);
Resolution: Replaced with direct analytical computation of dt_needed from mean anomaly delta. The analytical solution is more precise (no discretization error) and eliminates ~24k redundant Kepler solves per Hohmann transfer wait period at DT=10s.
Remaining: The current implementation only handles elliptical orbits. TODO comment added for parabolic (Barker's equation) and hyperbolic branches.
Issue 2: Mixed Concerns in execute_pending_maneuvers()
Status: RESOLVED — merged into update_spacecraft_physics().
The function previously performed two distinct responsibilities:
- Checking trigger conditions (calls
check_maneuver_trigger()) - Executing the burn (propagation → burn → propagation)
Resolution: Trigger checking now happens inline in update_spacecraft_physics() before the propagation decision. The code is cleaner: check → decide propagation amount → propagate → burn (if needed) → propagate remainder.
Issue 3: Ambiguous scheduled_dt Semantics
Status: PARTIALLY RESOLVED — semantics are now clearer but still differ by trigger type.
| Trigger Type | scheduled_dt meaning |
Set by |
|---|---|---|
TRIGGER_TIME |
Always 0.0 (never set) | Never |
TRIGGER_TRUE_ANOMALY |
Seconds until exact burn position | check_maneuver_trigger() |
In update_spacecraft_physics():
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, ...);
}
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.
Issue 4: Time-Triggered Burns Propagate from Wrong State
Status: UNRESOLVED — root cause of burn timing quantization.
For time triggers, the sequence is:
Frame N: sim->time = 310.0 (trigger_value = 305.0)
check: 310 >= 305 → true
burn_dt = 0
remaining_dt = 10
execute_maneuver() → burn applies at sim->time=310
propagate craft by 10s starting from sim->time=310
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.
TODO in code: update_spacecraft_physics() has a TODO suggesting dt_to_burn = trigger_value - (sim->time - sim->dt) for exact placement.
Issue 5: Hardcoded Array Size
Status: RESOLVED — removed spacecraft_handled_this_frame[256] static array.
Location: Previously src/simulation.cpp, static declaration
// REMOVED
static bool spacecraft_handled_this_frame[256];
Resolution: The array is no longer needed since maneuver checking is inline in the propagation loop. No separate tracking mechanism required.
Issue 6: Duplicated Propagation Logic
Status: RESOLVED — single propagation path.
The "normal" propagation path and the maneuver path are now the same code path:
if (maneuver_fired) {
craft->orbit = propagate_orbital_elements(craft->orbit, burn_dt, ...);
execute_maneuver(...);
craft->orbit = propagate_orbital_elements(craft->orbit, remaining_dt, ...);
} else {
craft->orbit = propagate_orbital_elements(craft->orbit, sim->dt, ...);
}
When burn_dt == 0 and remaining_dt == sim->dt, the maneuver path is: propagate(0) → burn → propagate(sim->dt). The normal path is: propagate(sim->dt). The propagation logic is no longer duplicated.
Call Sites Summary
Final State (3 call sites)
| Location | Function | Context | dt value |
|---|---|---|---|
simulation.cpp:287 |
update_bodies_physics() |
Normal body propagation | sim->dt |
simulation.cpp:315 |
update_spacecraft_physics() |
Normal craft propagation | sim->dt |
simulation.cpp:338 |
update_spacecraft_physics() |
Pre-burn sub-step propagation | burn_dt (0 to sim->dt) |
simulation.cpp:343 |
update_spacecraft_physics() |
Post-burn remaining propagation | sim->dt - burn_dt |
Remaining: 4 call sites (1 for bodies, 3 for spacecraft), 2 distinct contexts for spacecraft (normal propagation, sub-step execution).
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:
- In
check_maneuver_trigger()forTRIGGER_TIME: computedt_to_burn = trigger_value - (sim->time - sim->dt)whensim->time >= trigger_value - Set
scheduled_dt = dt_to_burn(positive value, 0 tosim->dt) - 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:
- Parabolic: Barker's equation
D + D³/3 = M, whereD = tan(ν/2) - Hyperbolic:
e·sinh(H) - H = M, using hyperbolic anomalyH