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: RESOLVED — both trigger types now set scheduled_dt to the exact sub-step offset.
| Trigger Type | scheduled_dt meaning |
Set by |
|---|---|---|
TRIGGER_TIME |
trigger_value - sim->time (step start) |
check_maneuver_trigger() |
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, ...);
}
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: RESOLVED — sub-step interpolation implemented (2026-04-26).
For time triggers, the sequence is now:
Frame N: sim->time = 310.0 (trigger_value = 305.0)
check: sim->time >= trigger_value → true
dt_to_burn = trigger_value - (sim->time - sim->dt) = 305 - 300 = 5.0
burn_dt = 5.0
remaining_dt = 5.0
propagate(5.0) → craft at trigger position
execute_maneuver() → burn applies at sim->time=305
propagate(5.0) → continue simulation
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.
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
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
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
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.