A from-scratch Python implementation of the orbital-mechanics pipeline a GNC operator uses to assess a conjunction and plan a collision-avoidance maneuver.
Try it live → | Quick start | The math | Verification
Every piece of the orbital physics — Kepler's laws, Newton's law of gravitation, the RK4 propagator, the universal-variable Lambert solver, the B-plane collision-probability integral — is written directly from the equations. The only third-party dependencies are NumPy (linear algebra), SciPy (dblquad, erf) and Matplotlib (plotting). No poliastro, no astropy.coordinates, no sgp4.
Blue = nominal satellite · red = debris · dashed green = post-burn evasion trajectory.
- Live Demo
- Overview
- Quick Start
- Operational Impact
- Sample Output
- Scenario
- Maneuver Strategies
- Project Layout
- Mathematical Foundations
- Verification
- Continuous Integration
- Scope and Limitations
- References
A zero-install web UI built with Streamlit is provided in app.py. Sliders control satellite/debris altitude, inclination, and the Pc threshold; the app renders the full 3D scene and prints the burn plan. Deploy it free on Streamlit Community Cloud — or run locally:
pip install -r requirements.txt
streamlit run app.pyDeployment note: after connecting this repo to Streamlit Cloud, paste the live URL into the Live demo badge above so recruiters can launch the scenario from their phone.
Given the classical orbital elements of a primary satellite and a piece of debris, the tool:
- Propagates both orbits forward with a fixed-step RK4 integrator (
dt = 1 s). - Locates the point of closest approach (PCA) and refines the time parabolically between samples.
- Computes the probability of collision
Pcby integrating a 2D Gaussian over the combined hard-body disk in the encounter B-plane. - Plans an evasion maneuver if
Pcexceeds the threshold (default1e-4), then verifies the post-burn miss distance by re-propagating with the impulses applied. - Renders the scenario in 3D and prints a conjunction-assessment report.
pip install -r requirements.txt
python main.py| Flag | Purpose |
|---|---|
--lambert |
Use the 3-burn Lambert return instead of the mirrored simple return |
--scenario path.json |
Load a scenario override (see scenarios/default.json) |
--save-plot out.png |
Save the 3D visualization to disk |
--animate out.gif |
Render the encounter as a looping GIF |
--no-show |
Skip the interactive Matplotlib window |
--verify |
Run the numerical sanity-check suite and exit |
python main.py --verify # run the 8-check math suite
python main.py # default scenario, simple mode
python main.py --lambert # default scenario, Lambert mode
python main.py --scenario scenarios/default.json # load from JSON
python main.py --save-plot out.png --no-show # headless render
python main.py --animate out.gif --no-show # export looping GIF
streamlit run app.py # launch the web UIBridges complex flight kinematics with real-world logistics, calculating the exact fuel budget (Δv) required to secure high-value physical assets while minimizing mission downtime. For a fleet operator this pipeline is the kernel of a conjunction-screening service: triage thousands of close approaches per day, escalate only the high-Pc cases, and attach a quantified Δv cost to each maneuver decision before it reaches an operator's queue.
====================================================================
KESSLER EVADER - Conjunction Assessment Report
====================================================================
Scenario: ISS-like primary at 400 km altitude, 51.6 deg inclination;
high-inclination (82 deg) debris crosser at the same altitude.
--- Pre-maneuver encounter ---
Time to PCA : 1799.04 s (29.98 min)
Miss distance : 0.5254 km
Relative speed : 5.3291 km/s
Probability Pc : 1.895e-03
Threshold : 1.000e-04
--- Evasion plan (simple) ---
Required miss margin : 1.1357 km
dV1 at t = 1199.0 s : 1.5352 m/s [+0.2811, -0.9374, -1.1828] m/s
dV2 at t = 2399.0 s : 1.5352 m/s [-0.2811, +0.9374, +1.1828] m/s
Total dV budget : 3.0703 m/s
--- Post-maneuver encounter ---
Miss distance : 1.2790 km
Probability Pc : 4.874e-04
[OK] Satellite is clear of the debris.
Static renders (via --save-plot):
| Simple return | Lambert return |
|---|---|
![]() |
![]() |
The default case places an ISS-like satellite (400 km circular, 51.6° inclination) at the same altitude as a piece of debris on an 82° inclined circular orbit. Their orbital planes intersect, and the initial true anomalies are tuned so both objects arrive at the intersection near t = 1800 s with a sub-kilometre miss distance and ~5.3 km/s closing speed — geometrically analogous to the Iridium-33 / Kosmos-2251 conjunction.
Edit constants.DEFAULT_SCENARIO or copy scenarios/default.json to substitute another case. The JSON format accepts degrees or radians via an angles_unit field.
An along-track impulse dV1 applied at t_pca - lead_time, sized from the full Clohessy-Wiltshire along-track response:
delta_along_track(dt) = ( 4*sin(n*dt)/n - 3*dt ) * dV_T
The sign of dV_T is chosen so the resulting displacement has a component along the nominal miss vector (i.e. the satellite moves away from the debris, not into it). A mirrored burn dV2 = -dV1 at t_pca + return_time restores the original semi-major axis and eccentricity. The along-track phase shifts permanently — the honest simplification of simple mode.
Same dV1 as above. After propagating with the first burn applied, solve Lambert's problem from the actual post-burn state to the nominal orbit's position at t_burn2 + return_arc, yielding a pair (dV2, dV3) that exactly restores the full state vector rather than just (a, e). Implemented with a universal-variable Newton iteration on z and hand-coded Stumpff C(z), S(z) functions.
kessler-evader/
├── main.py # CLI, orchestration, console report, --verify
├── app.py # Streamlit web UI
├── constants.py # mu, R_earth, default scenario, tuning knobs
├── orbital_mechanics.py # COE <-> ECI, two-body accel, RK4, propagator
├── encounter.py # PCA finder, B-plane basis, Pc via 2D quadrature
├── lambert.py # Universal-variable Lambert with Stumpff C, S
├── maneuver.py # Simple (CW) and Lambert evasion planners
├── visualize.py # 3D scene + looping GIF animation
├── scenarios/
│ └── default.json # Example scenario file (mirrors the hardcoded default)
├── .github/workflows/
│ └── python-tests.yml # CI: run --verify on every push
├── requirements.txt
└── README.md
| Module | Key exports |
|---|---|
orbital_mechanics.py |
coe_to_rv, rv_to_coe, two_body_accel, rk4_step, propagate |
encounter.py |
find_pca, build_bplane_basis, probability_of_collision, compute_pc_from_pca |
lambert.py |
stumpff_c, stumpff_s, solve_lambert |
maneuver.py |
plan_simple_avoidance, plan_lambert_return, plan_evasion, total_dv_mps |
visualize.py |
plot_scenario, animate_scenario |
main.py |
compute_scenario (pure), run (prints + plots), run_verification |
| Step | Formulation |
|---|---|
| Two-body EOM | `a = -mu * r / |
| Propagator | Classical 4th-order Runge-Kutta on [r; v], fixed dt = 1 s |
| COE ↔ ECI | Vallado Algorithms 9 and 10 (perifocal frame + 3-1-3 rotation) |
| PCA refinement | Parabolic fit on squared relative distance, linear state interpolation |
| B-plane basis | Right-handed (xi, eta) perpendicular to relative velocity |
| Covariance | Diagonal RTN per object, rotated to ECI, summed, projected onto the B-plane |
Pc |
integral of 2D Gaussian over circular hard-body disk, via scipy.integrate.dblquad in polar coordinates |
| Simple burn sizing | Full CW along-track response (not the 3 * dt asymptote) |
| Lambert | Universal-variable Newton iteration on z, with Stumpff C(z), S(z) |
python main.py --verify runs eight numerical sanity checks:
| # | Check | Result |
|---|---|---|
| 1 | rv_to_coe(coe_to_rv(coe)) round-trip |
max err 6e-12 |
| 2 | Energy conservation over one orbital period at dt = 1 s |
rel err 1e-14 |
| 3 | Circular orbit satisfies ` | v |
| 4 | Pc zero-miss matches 1 - exp(-HBR^2 / (2 sigma^2)); far-miss ~ 0 |
match to 4 sig figs |
| 5 | Lambert self-consistency: propagate, then recover both velocities | 1e-13 km/s |
| 6 | Default scenario fires the evasion gate | Pc = 1.9e-3 > 1e-4 |
| 7 | Simple evasion achieves >= 0.9 * required miss |
pre 0.525 km → post 1.279 km |
| 8 | Lambert evasion achieves >= 0.9 * required miss |
post 1.279 km |
All eight pass on the current default scenario.
A GitHub Actions workflow at .github/workflows/python-tests.yml re-runs the full --verify suite on every push and pull request against Python 3.11 and 3.12. Any maths regression (a sign flip in the CW formula, a broken Stumpff branch, a drift in the RK4 step) shows up as a red check before merge.
- Physics: pure two-body Keplerian. No J2, drag, SRP, third-body or relativistic effects.
- Inputs: hardcoded scenario in
constants.pyor a JSON override via--scenario. TLE parsing is intentionally out of scope. - Covariance: a diagonal RTN position covariance is assumed per object (1-sigma values in
constants.py). Real CDMs supply a full 6×6 covariance; that extension is mechanical but omitted for clarity. - Hard-body radius: 50 m combined by default. Edit
constants.HARD_BODY_RADIUS. - Sim horizon:
SIM_DURATION_S = 3600 s— long enough to contain the three-burn sequence but short enough to exclude the second-orbit re-encounter that recurs naturally at ~t_pca + T/2.
- Vallado, D. A. — Fundamentals of Astrodynamics and Applications, 4th ed. (Algorithms 9, 10, 58).
- Curtis, H. D. — Orbital Mechanics for Engineering Students, 3rd ed. (Ch. 5, universal-variable Lambert).
- Foster, J. L. — A Parametric Analysis of Orbital Debris Collision Probability and Maneuver Rate for Space Vehicles, NASA JSC-25898 (1992).
- Chan, F. K. — Spacecraft Collision Probability, Aerospace Press (2008).
Built as a portfolio demonstration of spacecraft GNC math. Every formula is in the source; the comments say why, and the equations say what.

