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.
 
 
 
 
 

11 KiB

Periapsis Burn Bug Analysis

Overview

Two separate issues causing periapsis burns to execute incorrectly:

  1. Issue 1: Omega calculation unstable for near-zero inclination orbits
  2. Issue 2: True anomaly trigger fires early, executing burn at wrong location

Issue 1: Omega = π Instead of 0 for Near-Zero Inclination

Description

When cartesian_to_orbital_elements() reconstructs orbital elements from state vectors for orbits with tiny non-zero inclination, the argument of periapsis (ω) is calculated as π (180°) instead of 0.

Root Cause

Location: src/orbital_mechanics.cpp:275-282

if (e > 1e-10 && n_mag > 1e-10) {
    double cos_omega = vec3_dot(e_vec, n) / (e * n_mag);
    Vec3 n_cross_e = vec3_cross(n, e_vec);
    double sin_omega = vec3_dot(n_cross_e, h_vec) / (e * n_mag * h);
    omega = atan2(sin_omega, cos_omega);  // PROBLEM: atan2(1.68e-15, -1) = π
    if (omega < 0.0) {
        omega += 2.0 * M_PI;
    }
}

The Problem:

  1. For near-zero inclination (e.g., i = 1e-12 rad), n_mag is non-zero due to tiny h_vec.y component
  2. Eccentricity vector e_vec has negative x-component (points toward periapsis from current position at apoapsis)
  3. Node vector n has positive x-component (due to tiny inclination)
  4. cos_omega = (e_vec · n) / (e * n_mag) = -1 (opposite signs → negative)
  5. sin_omega = 1.68e-15 (tiny numerical noise, but non-zero)
  6. atan2(1.68e-15, -1) = π (selects wrong quadrant due to tiny positive sin)

Calculation Chain

i = 1e-12 (tiny inclination)
    ↓
orbital_elements_to_cartesian() applies R_x(1e-12)
    ↓
pos.z = 1.59e-21, vel.z = -4.63e-9  (tiny but non-zero)
    ↓
vec3_cross(pos, vel) = h_vec
    ↓
h_vec = (0, -0.0602259220236602, 60225922023.6602)  (h_vec.y ≠ 0)
    ↓
vec3_cross(k, h_vec) = n
    ↓
n = (0.0602259220236602, 0, 0), n_mag = 0.0602
    ↓
cos_omega = -1, sin_omega = 1.68e-15
    ↓
omega = atan2(1.68e-15, -1) = π (WRONG!)

Impact

When ω = π instead of 0:

  • Periapsis and apoapsis locations are flipped in the reconstructed orbit
  • True anomaly triggers fire at correct angle (0), but at wrong physical location (apoapsis instead of periapsis)
  • Spacecraft position says "I'm at periapsis" but actual radius shows apoapsis

Affected Scenarios

  • Orbits with inclination < 1e-8 rad (~0.0005 degrees) that undergo element reconstruction
  • Can occur after:
    • Any maneuver execution (burn reconstruction)
    • SOI transitions
    • Velocity deviation correction

Proposed Fix

Location: src/orbital_mechanics.cpp:275-285

double omega;
double inclination_threshold = 0.01;  // ~0.5 degrees

if (e > 1e-10 && n_mag > 1e-10 && i > inclination_threshold) {
    // Calculate omega using atan2 (only for orbits with significant inclination)
    double cos_omega = vec3_dot(e_vec, n) / (e * n_mag);
    Vec3 n_cross_e = vec3_cross(n, e_vec);
    double sin_omega = vec3_dot(n_cross_e, h_vec) / (e * n_mag * h);
    omega = atan2(sin_omega, cos_omega);
    if (omega < 0.0) {
        omega += 2.0 * M_PI;
    }
} else {
    // For zero/low inclination or equatorial orbits, omega is not meaningful
    omega = 0.0;
}

Why this works:

  • For zero/low inclination, ω is not physically meaningful (no unique ascending node)
  • For orbits with inclination > 0.5°, ω is well-defined and should be calculated
  • Uses inclination angle (physical quantity) instead of n_mag (derived quantity)

Test Coverage

  • Existing: tests/test_omega_debug.cpp explicitly tests i = 1e-12 case
  • Needed: Tests for orbits at various inclinations (0, 1e-9, 1e-7, 1e-5, 1e-3, 0.1 rad)

Issue 2: True Anomaly Trigger Fires Early at Wrong Location

Description

True anomaly triggers execute burns immediately when an upcoming angle crossing is detected, rather than waiting until the spacecraft actually reaches the target angle. This causes periapsis burns to execute at apoapsis when the spacecraft is approaching periapsis from the opposite side of the orbit.

Root Cause

Location: src/maneuver.cpp:131-176 - check_maneuver_trigger()

Current Logic:

// Step 1: Check if currently close enough
if (current_diff < 0.01) return true;

// Step 2: Check if we'll cross trigger in next dt
OrbitalElements future_elements = propagate_orbital_elements(craft->orbit, sim->dt, parent->mass);
// ... calculate future_true_anomaly ...

// Step 3: Check if target lies between current and future
return angle_between(current_nu_norm, future_nu_norm, target_nu);

The Problem:

  1. Spacecraft at apoapsis (true_anomaly ≈ π)
  2. Propagate by dt: moves to true_anomaly ≈ π - 0.02
  3. angle_between(π, π-0.02, 0) returns TRUE (wraparound: π → ... → 0)
  4. Trigger fires immediately at current position (apoapsis)
  5. Burn executes at apoapsis

Why angle_between returns true:

// src/maneuver.cpp:114-124
static bool angle_between(double current, double next, double target) {
    double curr_norm = normalize_angle(current);   // π
    double next_norm = normalize_angle(next);       // π - 0.02 ≈ 3.12
    double target_norm = normalize_angle(target);   // 0

    if (curr_norm <= next_norm) {  // FALSE (π > 3.12)
        // ...
    } else {
        // Wraparound case: checks if target >= curr OR target <= next
        return (target_norm >= curr_norm) || (target_norm <= next_norm);
        // (0 >= π) = FALSE || (0 <= 3.12) = TRUE
        // Returns TRUE
    }
}

Impact

When trigger fires early:

  • Burn executes at current position (e.g., apoapsis) instead of target position (periapsis)
  • Orbital elements are reconstructed from incorrect location
  • Spacecraft thinks "I'm at periapsis" but is actually at apoapsis
  • Next frame: Spacecraft continues toward actual periapsis, but reconstructed orbit says it should move away

Example Scenario

Test: tests/test_periapsis_burn.cpp - "Two periapsis burns execute at same location"

What happens:

Frame 0: Spacecraft at true_anomaly = 0.1 (near periapsis)
         angle_between(0.1, 0.12, 0) = FALSE (0 < 0.1)
         No trigger

...

Frame N: Spacecraft at true_anomaly = π (apoapsis)
         Propagate: π → π - 0.02 (crossing periapsis next)
         angle_between(π, π-0.02, 0) = TRUE (wraparound)
         Trigger fires! But position is still at apoapsis

Burn executes: Add Δv at apoapsis position
Orbital elements reconstructed: "I'm at true_anomaly = 0" (from trigger)
But actual radius: 13.48M m (apoapsis), not 7.26M m (periapsis)

Affected Scenarios

  • All true anomaly triggers when spacecraft is approaching target from opposite side of orbit
  • Particularly problematic for:
    • Periapsis burns when approaching from apoapsis
    • Apoapsis burns when approaching from periapsis
    • Any true anomaly trigger near the 0/2π wraparound boundary

Possible Solutions

Option 1: Defer Burn Execution

When trigger activates, mark it as "pending" and execute in a future frame when spacecraft actually reaches the target angle.

Pros:

  • Burn executes at correct physical location
  • Consistent with trigger semantics

Cons:

  • Requires state tracking (pending triggers)
  • Multiple triggers could become pending simultaneously
  • Adds complexity to simulation loop

Option 2: Interpolate to Exact Trigger Time

Find exact time when spacecraft reaches target angle, then advance simulation to that point before executing burn.

Pros:

  • Executes burn at exactly the right moment
  • No additional state tracking

Cons:

  • Requires solving for exact time (iterative)
  • Time steps become irregular
  • Complex to implement

Option 3: Remove Future Check (Simplest)

Only fire when spacecraft is already within 0.01 rad of target (remove step 2).

Pros:

  • Simple fix, no state tracking
  • Executes burn at correct position

Cons:

  • May miss fast crossings if dt is large
  • Depends on dt being small enough for accuracy

Option 4: Detect Crossing, Wait for Crossing

Detect upcoming crossing, then wait until angular_distance(current_nu, target) decreases instead of increases.

Pros:

  • Executes burn at correct position
  • No state tracking needed

Cons:

  • Requires tracking direction of approach
  • More complex check in trigger function

Option 3 (Remove Future Check) is simplest and likely sufficient:

  • Current dt = 60 seconds, orbital period ~10500 seconds
  • Spacecraft moves ~0.02 rad per dt near periapsis
  • 0.01 rad tolerance is half this, so spacecraft will be caught
  • For very fast orbits, user can reduce dt

Implementation:

// src/maneuver.cpp:131-176
case TRIGGER_TRUE_ANOMALY: {
    // ... calculate current_nu ...

    double current_diff = angular_distance(current_nu_norm, target_nu);
    return current_diff < 0.01;  // Remove future check
}

Test Coverage

  • Existing: tests/test_periapsis_burn.cpp
    • "Two periapsis burns execute at same location" - FAILS (Issue 2)
    • "Periapsis burn fires when crossing periapsis" - FAILS (Issue 2)
  • Needed: Tests for:
    • True anomaly triggers at various angles (not just 0 and π)
    • Different dt values to verify accuracy
    • Multiple sequential true anomaly triggers

Issue Interaction

Do these issues compound?

  • Issue 1 (ω = π): Affects orbits with tiny inclination
  • Issue 2 (early trigger): Affects ALL orbits with true anomaly triggers

Test results:

  • Periapsis burn test has i = 0 (exactly zero)
  • Debug output shows omega: 0.000000 rad (correct)
  • So Issue 1 is NOT causing test failures
  • Test failures are purely due to Issue 2

Potential interaction: If both issues occur simultaneously:

  1. Issue 2 causes burn to execute early at wrong location (e.g., apoapsis)
  2. Issue 1 causes ω to be calculated as π instead of 0 during reconstruction
  3. Result: Double corruption - wrong location AND wrong periapsis/apoapsis assignment

Fix Order

Priority:

  1. Fix Issue 2 first (affects all true anomaly triggers, causes visible test failures)
  2. Fix Issue 1 second (affects narrow edge case, may be rare in practice)

Rationale:

  • Issue 2 causes visible bug for any true anomaly trigger
  • Issue 1 is edge case that may not be noticed unless orbits have tiny non-zero inclination
  • Fixing Issue 2 will make tests pass, providing stable baseline for Issue 1 fix

Debug Output Added

Location: src/orbital_mechanics.cpp:274-285

Added debug output to trace omega calculation:

  • e (eccentricity)
  • n_mag (node vector magnitude)
  • h (angular momentum magnitude)
  • e_vec components
  • n components
  • cos_omega and sin_omega values
  • omega before and after normalization

Usage:

./orbit_test -s '[test_periapsis_burn]'

Note: Debug output should be removed after fixes are implemented and tested.


Status

  • Issue 1: Understood, fix ready (add inclination threshold)
  • Issue 2: Understood, multiple solution options (recommend removing future check)
  • Test Coverage: Tests added to capture both issues (commit 49c748f)
  • Next Steps:
    1. Implement fix for Issue 2 (remove future check)
    2. Run tests to verify Issue 2 resolved
    3. Implement fix for Issue 1 (add inclination threshold)
    4. Run tests to verify Issue 1 resolved
    5. Remove debug output
    6. Commit all fixes