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.
 
 
 
 
 

8.6 KiB

Propagation Call Chain Analysis

Session Date

2026-04-20

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.


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.

Call Chain: update_simulation()

update_simulation()
│
├── reset_spacecraft_tracking()
│
├── 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()
│
├── execute_pending_maneuvers()
│   └── for each unexecuted maneuver:
│       ├── check_maneuver_trigger()
│       │   ├── TRIGGER_TIME: sim->time >= trigger_value
│       │   └── TRIGGER_TRUE_ANOMALY:
│       │       ├── propagate_orbital_elements() ← per-frame probe
│       │       └── Kepler equation solving
│       └── if triggered:
│           ├── propagate_orbital_elements(dt_to_burn)
│           ├── execute_maneuver()
│           │   ├── apply_impulsive_burn()
│           │   └── cartesian_to_orbital_elements()
│           └── propagate_orbital_elements(remaining_dt)
│
├── update_spacecraft_physics()
│   └── for each craft NOT handled:
│       ├── orbital_elements_to_cartesian() → expected_vel
│       ├── velocity drift check
│       ├── cartesian_to_orbital_elements() (if drift)
│       └── propagate_orbital_elements()
│
├── 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()

Location: src/simulation.cpp, execute_pending_maneuvers()

The function performs two distinct responsibilities:

  1. Checking trigger conditions (calls check_maneuver_trigger())
  2. Executing the burn (propagation → burn → propagation)
void execute_pending_maneuvers(SimulationState* sim) {
    for (...) {
        if (check_maneuver_trigger(maneuver, craft, sim)) {  // CHECKING
            // ... propagation, burn, more propagation         // EXECUTING
        }
    }
}

The trigger-checking logic (especially the true-anomaly branch with its Kepler equation solving) is interleaved with the execution logic. This makes it difficult to reason about what happens in each phase and complicates testing.

Issue 3: Ambiguous scheduled_dt Semantics

The scheduled_dt field in Maneuver has different meanings depending on 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 execute_pending_maneuvers():

double dt_to_burn = maneuver->scheduled_dt;
if (dt_to_burn > 0.0) {
    craft->orbit = propagate_orbital_elements(craft->orbit, dt_to_burn, ...);
}
// ... burn ...
double remaining_dt = sim->dt - dt_to_burn;
craft->orbit = propagate_orbital_elements(craft->orbit, remaining_dt, ...);

For TRIGGER_TIME, scheduled_dt == 0 is a coincidence — the field is never set. The code works, but the reason is opaque. The branching (if dt_to_burn > 0) exists only for true-anomaly triggers, but a reader cannot tell this from the code alone.

Issue 4: Time-Triggered Burns Propagate from Wrong State

For time triggers, the sequence is:

Frame N:  sim->time = 310.0  (trigger_value = 305.0)
          check: 310 >= 305 → true
          dt_to_burn = 0
          remaining_dt = 10
          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.

Issue 5: Hardcoded Array Size

Location: src/simulation.cpp, static declaration

static bool spacecraft_handled_this_frame[256];

A global array with a hardcoded size of 256. If more than 256 spacecraft are added, the tracking silently truncates. It should be part of SimulationState or dynamically sized. There is already a FIXME comment on this line.

Issue 6: Duplicated Propagation Logic

The "normal" propagation path in update_spacecraft_physics() and the maneuver path in execute_pending_maneuvers() are essentially duplicates:

Normal path (update_spacecraft_physics):

propagate_orbital_elements(craft->orbit, sim->dt, ...);
orbital_elements_to_cartesian(...);

Maneuver path (execute_pending_maneuvers, when dt_to_burn == 0):

propagate_orbital_elements(craft->orbit, 0, ...);     // no-op
orbital_elements_to_cartesian(...);
execute_maneuver(...);
propagate_orbital_elements(craft->orbit, sim->dt, ...);
orbital_elements_to_cartesian(...);

When no sub-step is needed, the maneuver path is: propagate → convert → burn → propagate → convert. The propagation and coordinate conversion logic is duplicated across both code paths.

Call Sites Summary

Before fix (5 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:343 execute_pending_maneuvers() Pre-burn sub-step propagation dt_to_burn (0 to sim->dt)
simulation.cpp:350 execute_pending_maneuvers() Post-burn remaining propagation sim->dt - dt_to_burn
maneuver.cpp:147 check_maneuver_trigger() True-anomaly look-ahead probe sim->dt

After fix (4 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:343 execute_pending_maneuvers() Pre-burn sub-step propagation dt_to_burn (0 to sim->dt)
simulation.cpp:350 execute_pending_maneuvers() Post-burn remaining propagation sim->dt - dt_to_burn

Remaining: 4 call sites, 2 distinct contexts for spacecraft (normal propagation, sub-step execution). Issue 6 (duplicated propagation) remains.