11 KiB
Periapsis Burn Bug Analysis
Overview
Two separate issues causing periapsis burns to execute incorrectly:
- Issue 1: Omega calculation unstable for near-zero inclination orbits
- 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:
- For near-zero inclination (e.g., i = 1e-12 rad),
n_magis non-zero due to tinyh_vec.ycomponent - Eccentricity vector
e_vechas negative x-component (points toward periapsis from current position at apoapsis) - Node vector
nhas positive x-component (due to tiny inclination) cos_omega = (e_vec · n) / (e * n_mag) = -1(opposite signs → negative)sin_omega = 1.68e-15(tiny numerical noise, but non-zero)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.cppexplicitly 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:
- Spacecraft at apoapsis (true_anomaly ≈ π)
- Propagate by dt: moves to true_anomaly ≈ π - 0.02
angle_between(π, π-0.02, 0)returns TRUE (wraparound: π → ... → 0)- Trigger fires immediately at current position (apoapsis)
- 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
Recommended Approach
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:
- Issue 2 causes burn to execute early at wrong location (e.g., apoapsis)
- Issue 1 causes ω to be calculated as π instead of 0 during reconstruction
- Result: Double corruption - wrong location AND wrong periapsis/apoapsis assignment
Fix Order
Priority:
- Fix Issue 2 first (affects all true anomaly triggers, causes visible test failures)
- 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:
- Implement fix for Issue 2 (remove future check)
- Run tests to verify Issue 2 resolved
- Implement fix for Issue 1 (add inclination threshold)
- Run tests to verify Issue 1 resolved
- Remove debug output
- Commit all fixes