vibe coding an orbital mechanics simulation to try out claude code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

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 from check_maneuver_trigger() — eliminates ~24k redundant Kepler solves per Hohmann transfer wait period
  • Removed dead angle_between() static helper
  • Replaced the angle_between, future_diff, and wraparound_crossing checks with a cleaner dt_needed <= 0.0 guard
  • 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_maneuvers declaration from simulation.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:

  1. Checking trigger conditions (calls check_maneuver_trigger())
  2. 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, where D = tan(ν/2)
  • 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.