exorl 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. exorl-0.1.1/PKG-INFO +1091 -0
  2. exorl-0.1.1/README.md +1067 -0
  3. exorl-0.1.1/exorl/__init__.py +0 -0
  4. exorl-0.1.1/exorl/cli.py +64 -0
  5. exorl-0.1.1/exorl/commands/__init__.py +12 -0
  6. exorl-0.1.1/exorl/commands/eval_generalisation.py +128 -0
  7. exorl-0.1.1/exorl/commands/generate_demos.py +206 -0
  8. exorl-0.1.1/exorl/commands/pretrain_bc.py +183 -0
  9. exorl-0.1.1/exorl/commands/train_sac.py +424 -0
  10. exorl-0.1.1/exorl/core/__init__.py +222 -0
  11. exorl-0.1.1/exorl/core/atmosphere_science.py +901 -0
  12. exorl-0.1.1/exorl/core/climate.py +798 -0
  13. exorl-0.1.1/exorl/core/comms.py +324 -0
  14. exorl-0.1.1/exorl/core/env.py +813 -0
  15. exorl-0.1.1/exorl/core/generator.py +368 -0
  16. exorl-0.1.1/exorl/core/geology.py +504 -0
  17. exorl-0.1.1/exorl/core/ground_track.py +492 -0
  18. exorl-0.1.1/exorl/core/habitability.py +716 -0
  19. exorl-0.1.1/exorl/core/heliocentric.py +935 -0
  20. exorl-0.1.1/exorl/core/interior.py +664 -0
  21. exorl-0.1.1/exorl/core/interplanetary_env.py +864 -0
  22. exorl-0.1.1/exorl/core/kepler_catalog.py +694 -0
  23. exorl-0.1.1/exorl/core/launch_window.py +383 -0
  24. exorl-0.1.1/exorl/core/mission.py +743 -0
  25. exorl-0.1.1/exorl/core/observation.py +702 -0
  26. exorl-0.1.1/exorl/core/orbital_analysis.py +818 -0
  27. exorl-0.1.1/exorl/core/physics.py +323 -0
  28. exorl-0.1.1/exorl/core/planet.py +530 -0
  29. exorl-0.1.1/exorl/core/planet_io.py +326 -0
  30. exorl-0.1.1/exorl/core/population.py +565 -0
  31. exorl-0.1.1/exorl/core/power.py +291 -0
  32. exorl-0.1.1/exorl/core/science_ops_env.py +497 -0
  33. exorl-0.1.1/exorl/core/soi.py +277 -0
  34. exorl-0.1.1/exorl/core/star.py +475 -0
  35. exorl-0.1.1/exorl/core/surface_energy.py +486 -0
  36. exorl-0.1.1/exorl/core/thermal_evolution.py +596 -0
  37. exorl-0.1.1/exorl/core/tidal.py +516 -0
  38. exorl-0.1.1/exorl/visualization/__init__.py +35 -0
  39. exorl-0.1.1/exorl/visualization/visualizer.py +1836 -0
  40. exorl-0.1.1/exorl.egg-info/PKG-INFO +1091 -0
  41. exorl-0.1.1/exorl.egg-info/SOURCES.txt +45 -0
  42. exorl-0.1.1/exorl.egg-info/dependency_links.txt +1 -0
  43. exorl-0.1.1/exorl.egg-info/entry_points.txt +2 -0
  44. exorl-0.1.1/exorl.egg-info/requires.txt +11 -0
  45. exorl-0.1.1/exorl.egg-info/top_level.txt +1 -0
  46. exorl-0.1.1/pyproject.toml +46 -0
  47. exorl-0.1.1/setup.cfg +4 -0
exorl-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,1091 @@
1
+ Metadata-Version: 2.4
2
+ Name: exorl
3
+ Version: 0.1.1
4
+ Summary: A reinforcement learning + planetary science toolkit.
5
+ Author: ExoRL contributors
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/Weirdnemo/ExoRL/
8
+ Project-URL: Issues, https://github.com/Weirdnemo/ExoRL/issues
9
+ Keywords: reinforcement-learning,astrophysics,planetary-science,exoplanets,gymnasium
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: numpy
16
+ Requires-Dist: matplotlib
17
+ Provides-Extra: rl
18
+ Requires-Dist: gymnasium; extra == "rl"
19
+ Requires-Dist: stable-baselines3; extra == "rl"
20
+ Requires-Dist: torch; extra == "rl"
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest; extra == "dev"
23
+ Requires-Dist: ruff; extra == "dev"
24
+
25
+ # ExoRL
26
+
27
+ ExoRL is a planetary science simulation and reinforcement learning toolkit. It models planets from the inside out — interior structure, atmosphere, climate, habitability, orbital mechanics, and observational signatures — and connects all of that physics to trainable RL environments for spacecraft mission design.
28
+
29
+ The core idea is physical consistency. The J2 oblateness your agent fights during orbital insertion is derived from the same interior model that determines the planet's magnetic field. The atmospheric drag comes from a multi-layer atmosphere whose greenhouse warming uses the same CO₂ pressure the habitability scorer reads. Every number follows from the physics, not from convenience.
30
+
31
+ It was built to answer a research question: can a reinforcement learning agent learn to design planetary missions — choosing departure windows, executing interplanetary transfers, and inserting into science orbits — when trained across a physically diverse population of procedurally generated planets?
32
+
33
+ If you want guided walkthroughs:
34
+
35
+ - RL end-to-end: `docs/TUTORIAL.md`
36
+ - Astrophysics / planetary science (no RL): `docs/ASTROPHYSICS_TUTORIAL.md`
37
+
38
+ ---
39
+
40
+ ## Getting Started
41
+
42
+ ### Dependencies / Installation
43
+
44
+ ExoRL is a Python project. The reinforcement-learning scripts additionally rely on:
45
+
46
+ - `gymnasium` (Gym API)
47
+ - `torch` (policy networks)
48
+ - `stable-baselines3` (SAC baseline)
49
+
50
+ Recommended install (CPU PyTorch; use the CUDA variant if you need GPU):
51
+
52
+ ```bash
53
+ python -m venv .venv
54
+ source .venv/bin/activate
55
+
56
+ # Core (science + visualization helpers)
57
+ pip install -e .
58
+
59
+ # RL scripts (SAC/BC/eval)
60
+ pip install -e ".[rl]"
61
+ ```
62
+
63
+ ### Releasing to PyPI (maintainers)
64
+
65
+ This repo is set up for **Trusted Publishing** to PyPI via GitHub Actions.
66
+
67
+ - **Trigger**: push a version tag like `v0.1.1`
68
+ - **Workflow**: `.github/workflows/workflow.yml` builds and publishes automatically
69
+
70
+ Typical release:
71
+
72
+ ```bash
73
+ # 1) bump version in pyproject.toml
74
+ # 2) commit the version bump
75
+ git tag v0.1.1
76
+ git push --tags
77
+ ```
78
+
79
+ ### CLI shortcuts
80
+
81
+ If you install the project (editable or not), you also get a small convenience CLI that wraps the scripts:
82
+
83
+ ```bash
84
+ exorl generate-demos --episodes 200 --presets-only --out demos/demos_200.npz
85
+ exorl pretrain-bc --demos demos/demos_200.npz --out bc_model_200
86
+ exorl train-sac --mode fixed --planet earth --steps 20000 --tag quick
87
+ exorl eval-generalisation --model training_runs/<run_name>/model_final.zip
88
+ ```
89
+
90
+ You can also run the same commands as modules:
91
+
92
+ ```bash
93
+ python -m exorl.commands.generate_demos --help
94
+ python -m exorl.commands.train_sac --help
95
+ ```
96
+
97
+ ### Quickstart: run a small experiment (SAC with BC warmstart)
98
+
99
+ This pipeline is end-to-end:
100
+
101
+ 1. Generate an expert dataset (successful episodes only)
102
+ 2. Pretrain an actor with Behavioural Cloning (BC)
103
+ 3. Warm-start SAC and train on `OrbitalInsertionEnv`
104
+ 4. Evaluate zero-shot generalisation across planets
105
+
106
+ #### 1) Generate demonstrations
107
+
108
+ ```bash
109
+ python scripts/generate_demos.py \
110
+ --episodes 200 \
111
+ --presets-only \
112
+ --out demos/demos_presets_200.npz \
113
+ --max-steps 4000 \
114
+ --seed 0
115
+ ```
116
+
117
+ If you want a faster, “lighter” pipeline for RL iteration (10-dim obs + simplified planets + no science stack), add `--lite`:
118
+
119
+ ```bash
120
+ python scripts/generate_demos.py \
121
+ --lite \
122
+ --episodes 200 \
123
+ --presets-only \
124
+ --out demos/demos_presets_200_lite.npz \
125
+ --max-steps 4000 \
126
+ --seed 0
127
+ ```
128
+
129
+ #### 2) Train BC (behavioural cloning)
130
+
131
+ ```bash
132
+ python scripts/pretrain_bc.py \
133
+ --demos demos/demos_presets_200.npz \
134
+ --out bc_model_presets_200 \
135
+ --epochs 10 \
136
+ --batch-size 256 \
137
+ --obs-dim 18 \
138
+ --seed 0
139
+ ```
140
+
141
+ If you used `--lite` when generating demos, train BC with `--obs-dim 10` and point `--demos` at the lite dataset:
142
+
143
+ ```bash
144
+ python scripts/pretrain_bc.py \
145
+ --demos demos/demos_presets_200_lite.npz \
146
+ --out bc_model_presets_200_lite \
147
+ --epochs 10 \
148
+ --batch-size 256 \
149
+ --obs-dim 10 \
150
+ --seed 0
151
+ ```
152
+
153
+ This creates a warm-start model at:
154
+
155
+ - `bc_model_presets_200/bc_policy.zip`
156
+
157
+ #### 3) Train SAC (Soft Actor-Critic)
158
+
159
+ ```bash
160
+ python scripts/train_sac.py \
161
+ --mode fixed \
162
+ --planet earth \
163
+ --steps 20000 \
164
+ --tag quick \
165
+ --eval-freq 5000 \
166
+ --eval-episodes 5 \
167
+ --pretrain bc_model_presets_200/bc_policy.zip
168
+ ```
169
+
170
+ Lite training run (faster; disables the habitability-based curriculum):
171
+
172
+ ```bash
173
+ python scripts/train_sac.py \
174
+ --lite \
175
+ --mode fixed \
176
+ --planet earth \
177
+ --steps 20000 \
178
+ --tag quick_lite \
179
+ --eval-freq 5000 \
180
+ --eval-episodes 5 \
181
+ --pretrain bc_model_presets_200/bc_policy.zip
182
+ ```
183
+
184
+ SAC writes outputs to `training_runs/<run_name>/`, where `run_name` already includes the timestamp:
185
+
186
+ - `--tag`
187
+ - `--mode`
188
+ - a timestamp printed at startup
189
+
190
+ #### 4) Evaluate the trained model
191
+
192
+ Replace `<run_name>` with the directory created in `training_runs/`:
193
+
194
+ ```bash
195
+ python scripts/eval_generalisation.py \
196
+ --model training_runs/<run_name>/model_final.zip \
197
+ --planets earth mars \
198
+ --episodes 10
199
+ ```
200
+
201
+ The evaluator produces:
202
+
203
+ - `generalisation_results.json`
204
+ - `generalisation_table.png` (a publication-style figure)
205
+
206
+ #### Expected outputs & files
207
+
208
+ After running the quickstart steps, you should see:
209
+
210
+ - Demonstration dataset:
211
+ - `demos/demos_presets_200.npz` (name depends on your `--out`)
212
+ - Key arrays in the `.npz`:
213
+ - `observations`: `float32`, shape `(N, 18)` (or `(N, 10)` if generated with `--lite`)
214
+ - `actions`: `float32`, shape `(N, 3)`
215
+ - `episode_ids`: `int32`, shape `(N,)`
216
+ - `planet_names`, `successes`, `rewards`: per-episode arrays for the generated episodes (only successful episode steps are kept for training pairs)
217
+ - Behavioural cloning (BC):
218
+ - `bc_model_presets_200/bc_policy.zip` (SB3-compatible model)
219
+ - `bc_model_presets_200/bc_policy_best.pt` and `bc_model_presets_200/bc_policy_final.pt`
220
+ - `bc_model_presets_200/bc_history.json`
221
+ - SAC training run:
222
+ - Output directory: `training_runs/<tag>_<mode>_<timestamp>/`
223
+ - `config.json`
224
+ - `learning_curve.csv`
225
+ - `learning_curve.png`
226
+ - `model_final.zip`
227
+ - `model_best.zip`
228
+ - `eval_results.json`
229
+
230
+ ### Environments at a Glance
231
+
232
+ ExoRL provides multiple `gymnasium.Env`-style environments. The primary training target for the scripts above is:
233
+
234
+ #### `OrbitalInsertionEnv` (`exorl/core/env.py`)
235
+
236
+ - Observation: `obs_dim` floats
237
+ - default: `obs_dim=18` (science stack enabled)
238
+ - legacy: `obs_dim=10` when using `--no-science` in training
239
+ - Action: 3 continuous floats in `[-1, 1]`
240
+ - `action[0]`: thrust magnitude (mapped to `[0, max_thrust]`)
241
+ - `action[1]`: pitch (mapped to `[-pi/2, pi/2]`)
242
+ - `action[2]`: yaw (mapped to `[-pi, pi]`)
243
+ - Episode success (terminal):
244
+ - altitude error < 5% and eccentricity < 0.05, and not crashed
245
+ - Episode termination (crash/escape/overheat/timeout/no-fuel):
246
+ - crash: altitude goes below 0 m
247
+ - overheat: `heat_load > heat_limit`
248
+ - escape: radius becomes too large for the capture problem
249
+ - timeout / no fuel: `max_steps` or fuel < 1 kg
250
+
251
+ #### `InterplanetaryEnv` (`exorl/core/interplanetary_env.py`)
252
+
253
+ - Observation: 28 floats
254
+ - Action: 4 continuous floats in `[-1, 1]`
255
+ - Single episode is 3 phases:
256
+ - `window`: choose departure/arrival slots, commit when `action[2] > 0`
257
+ - `cruise`: one step ~= one simulated day until target SOI entry
258
+ - `capture`: switches to the same orbital insertion physics as `OrbitalInsertionEnv`
259
+
260
+ #### `ScienceOpsEnv` (`exorl/core/science_ops_env.py`)
261
+
262
+ - Observation: 16 floats
263
+ - Action: 4 continuous floats in `[-1, 1]`
264
+ - `action[0]`: altitude change (mapped to a +/- 50 km manoeuvre via Hohmann)
265
+ - `action[1]`: inclination change (mapped to +/- 5 degrees)
266
+ - `action[2]`: observe toggle (instruments on when > 0)
267
+ - `action[3]`: downlink toggle (downlink when > 0)
268
+ - Reward blends science return (coverage + observational metrics), power constraints, data buffer overflow, and manoeuvre `Delta-V` costs.
269
+
270
+ ### Training workflow (demo → BC → SAC → eval)
271
+
272
+ ```mermaid
273
+ flowchart LR
274
+ Demo[Demo generation\nscripts/generate_demos.py] --> BC[BC pretrain\nscripts/pretrain_bc.py]
275
+ BC --> SAC[SAC training\nscripts/train_sac.py]
276
+ SAC --> Eval[Evaluation\nscripts/eval_generalisation.py]
277
+
278
+ Demo --> ArtDemo[Artifact: demos/*.npz]
279
+ BC --> ArtBC[Artifact: bc_model*/bc_policy.zip]
280
+ SAC --> ArtSAC[Artifact: training_runs*/model_final.zip]
281
+ Eval --> ArtEval[Artifact: generalisation_results.json]
282
+ ```
283
+
284
+ ### Background / Reference
285
+
286
+ Everything after `## Contents` is the “reference background” for the simulation stack: how planets are generated (Core/Population), how their atmospheres and climate work (Atmosphere & Climate + Science Modules), and how mission-science quantities feed the RL environments.
287
+
288
+ ---
289
+
290
+ ## Contents
291
+
292
+ - **Getting Started** — dependencies · quickstart · workflow diagram
293
+ - **Environments at a Glance** — observation/action interfaces
294
+ - **Training workflow** — demo → BC → SAC → eval
295
+ - **1. Core** — `planet` · `generator` · `interior` · `star` · `physics`
296
+ - **2. Atmosphere & Climate** — `atmosphere_science` · `climate`
297
+ - **3. Science Modules** — `habitability` · `orbital_analysis` · `ground_track` · `surface_energy` · `tidal` · `observation`
298
+ - **4. Mission Design** — `mission` · `heliocentric` · `soi` · `launch_window`
299
+ - **5. Population** — `population`
300
+ - **6. RL Environments** — `env` · `interplanetary_env`
301
+ - **7. Visualisation** — `visualization`
302
+ - **8. Examples & Scripts** — demos and plotting helpers
303
+ - **Troubleshooting** — setup and runtime issues
304
+ - **9. Known Limitations**
305
+
306
+ ---
307
+
308
+ ## 1. Core
309
+
310
+ ### `planet.py`
311
+
312
+ The `Planet` object is the central data structure that every other module operates on. It holds physical properties and exposes derived quantities as methods.
313
+
314
+ ```python
315
+ from exorl.core.generator import PRESETS
316
+
317
+ earth = PRESETS["earth"]()
318
+
319
+ print(earth.radius / 1e3) # 6371.0 km
320
+ print(earth.surface_gravity) # 9.82 m/s²
321
+ print(earth.escape_velocity / 1e3) # 11.19 km/s
322
+ print(earth.mu) # gravitational parameter m³/s²
323
+ print(earth.circular_orbit_speed(400_000)) # circular orbit speed at 400 km
324
+ print(earth.summary()) # human-readable property table
325
+ ```
326
+
327
+ ---
328
+
329
+ ### `generator.py`
330
+
331
+ Two ways to get planets: use a named preset, or generate one procedurally.
332
+
333
+ **Presets** — five solar system analogues with calibrated properties:
334
+
335
+ ```python
336
+ from exorl.core.generator import PRESETS
337
+
338
+ earth = PRESETS["earth"]()
339
+ mars = PRESETS["mars"]()
340
+ venus = PRESETS["venus"]()
341
+ moon = PRESETS["moon"]()
342
+ titan = PRESETS["titan"]()
343
+ ```
344
+
345
+ ![Preset cross-sections](figures/planet_figures/fig1a_preset_crosssec.png)
346
+
347
+ Each preset carries its atmosphere, J2 coefficient, magnetic field, moon count, and terrain type. The cross-section diagram above shows relative sizes, core structures, and active features.
348
+
349
+ **Random generation** — the generator produces planets across 0.1–4× Earth radius and 0.003–100× Earth mass, with randomised atmosphere composition, oblateness, magnetic dipole, and moon count that are all physically consistent with the planet's size and density:
350
+
351
+ ```python
352
+ from exorl.core.generator import PlanetGenerator
353
+
354
+ gen = PlanetGenerator(seed=42)
355
+
356
+ planet = gen.generate(
357
+ atmosphere_enabled = True,
358
+ oblateness_enabled = True,
359
+ magnetic_field_enabled = True,
360
+ terrain_enabled = True,
361
+ moons_enabled = True,
362
+ )
363
+ ```
364
+
365
+ ![Random planet cross-sections](figures/planet_figures/fig2a_random_crosssec.png)
366
+
367
+ Each call with a different seed produces a physically distinct planet. The same seed always gives the same planet, so experiments are reproducible.
368
+
369
+ **Feature toggles** — you can enable features incrementally on the same seed to isolate their effects:
370
+
371
+ ![Feature toggle on identical seed](figures/planet_figures/fig5_toggle.png)
372
+
373
+ **Airless worlds** — planets without atmospheres are fully supported. Terrain archetypes (cratered, mountainous, volcanic, flat) determine surface roughness for radar and landing simulations:
374
+
375
+ ![Airless terrain archetypes](figures/planet_figures/fig4_airless.png)
376
+
377
+ **Batch statistics** — generating 50 planets with `magnetic_field_enabled=True` produces this distribution across physical properties:
378
+
379
+ ![Batch statistics across 50 planets](figures/planet_figures/fig6_batch.png)
380
+
381
+ The generator spans a wide enough range that agents trained on random planets encounter significantly different physics every episode — from Moon-like bodies with 1.6 m/s² gravity to super-Earths with 40+ m/s².
382
+
383
+ ---
384
+
385
+ ### `interior.py`
386
+
387
+ Attaching an interior model lets the planet derive J2, magnetic field, heat flux, and moment of inertia from its bulk density rather than using hand-set values.
388
+
389
+ ```python
390
+ from exorl.core.interior import interior_from_bulk_density
391
+
392
+ planet.interior = interior_from_bulk_density(planet.mean_density)
393
+
394
+ j2 = planet.derived_J2() # oblateness coefficient
395
+ B = planet.derived_magnetic_field_T() # surface magnetic field [T]
396
+ q = planet.derived_heat_flux() # internal heat flux [W/m²]
397
+ moi = planet.derived_MoI() # moment of inertia factor C/MR²
398
+ ```
399
+
400
+ The model divides the planet into layers (inner core, outer core, lower mantle, upper mantle, crust) based on bulk density. From that layer structure it computes the moment of inertia integral directly, derives J2, checks the dynamo condition for the magnetic field, and estimates heat flux from a radiogenic element budget scaled to the planet's mass.
401
+
402
+ ![Interior-derived quantities for all five presets](figures/science_figures/fig02_interior_profiles.png)
403
+
404
+ The figure shows J2, surface magnetic field, heat flux, and moment of inertia factor derived from the interior model for each solar system analogue. Earth and Mars match geodetic measurements to within 5–22%. Heat flux is ~4× low compared to Earth's real 87 mW/m² because the model uses radiogenic budget only and omits secular cooling — this is a known limitation.
405
+
406
+ ---
407
+
408
+ ### `star.py`
409
+
410
+ Seven stellar presets from M to G spectral type. Attaching a star to a planet enables habitable zone placement, climate calculations, and habitability scoring.
411
+
412
+ ```python
413
+ from exorl.core.star import star_sun, STAR_PRESETS
414
+
415
+ sun = star_sun()
416
+ proxima = STAR_PRESETS["proxima"]()
417
+ trappist = STAR_PRESETS["trappist1"]()
418
+ # also: tau_ceti · kepler452 · alpha_centauri_a · eps_eridani
419
+
420
+ # Habitable zone boundaries (Kopparapu 2013)
421
+ print(sun.hz_inner_m / 1.496e11) # 0.975 AU
422
+ print(sun.hz_outer_m / 1.496e11) # 1.706 AU
423
+
424
+ # Flux, XUV, and orbital period at a given distance
425
+ flux = sun.flux_at_distance(1.496e11) # W/m²
426
+ xuv = sun.xuv_flux_at_distance(1.496e11) # W/m²
427
+ T = sun.orbital_period(1.496e11) # seconds
428
+
429
+ # Attach to a planet
430
+ planet.star_context = sun
431
+ planet.orbital_distance_m = 1.496e11
432
+ ```
433
+
434
+ ![Habitable zones and XUV flux for all seven stellar presets](figures/science_figures/fig03_star_habitable_zones.png)
435
+
436
+ The habitable zones span very different physical scales — TRAPPIST-1's HZ sits at 0.03–0.06 AU while a Sun-like star's extends to over 1.7 AU. XUV flux, which drives atmospheric escape, also varies by several orders of magnitude across spectral types, which is why M-dwarf planets score lower on habitability despite being in the HZ.
437
+
438
+ ---
439
+
440
+ ### `physics.py`
441
+
442
+ The spacecraft dynamics engine. An RK4 integrator propagates a `SpacecraftState` under gravity (including J2), thrust, and aerodynamic drag. This is what `env.py` and `interplanetary_env.py` use internally.
443
+
444
+ ```python
445
+ from exorl.core.physics import SpacecraftState, OrbitalIntegrator, ThrusterConfig, AeroConfig
446
+
447
+ state = SpacecraftState(
448
+ x=planet.radius + 400_000, y=0, z=0,
449
+ vx=0, vy=planet.circular_orbit_speed(400_000), vz=0,
450
+ mass=1000.0, dry_mass=300.0,
451
+ )
452
+
453
+ integrator = OrbitalIntegrator(
454
+ planet = planet,
455
+ thruster = ThrusterConfig(max_thrust=500.0, Isp=320.0),
456
+ aero = AeroConfig(enabled=True),
457
+ )
458
+
459
+ thrust_vec = np.array([0.0, 50.0, 0.0]) # N, in inertial frame
460
+ new_arr = integrator.step_rk4(state.to_array(), thrust_vec, dt=10.0, t=0.0)
461
+ new_state = SpacecraftState.from_array(new_arr, time=10.0, dry_mass=300.0)
462
+
463
+ print(new_state.radius) # distance from planet centre [m]
464
+ print(new_state.speed) # current speed [m/s]
465
+ print(new_state.fuel_mass) # remaining propellant [kg]
466
+ print(new_state.heat_load) # accumulated aeroheating [J/m²]
467
+ ```
468
+
469
+ The integrator calls `planet.gravity_vector_J2()` at each substep, so J2 perturbations are included automatically when the planet has oblateness enabled.
470
+
471
+ ---
472
+
473
+ ## 2. Atmosphere & Climate
474
+
475
+ ### `atmosphere_science.py`
476
+
477
+ Multi-layer atmosphere model with temperature-dependent density profiles, Jeans thermal escape, and greenhouse forcing.
478
+
479
+ ```python
480
+ from exorl.core.atmosphere_science import MultiLayerAtmosphere, analyse_atmosphere
481
+
482
+ # Build layered atmosphere
483
+ atm = MultiLayerAtmosphere.from_atmosphere_config(planet.atmosphere, planet)
484
+
485
+ # Density at altitude — used for drag calculations
486
+ rho = atm.density_at(50_000) # kg/m³ at 50 km altitude
487
+
488
+ # Full atmospheric analysis (requires star and distance attached to planet)
489
+ result = analyse_atmosphere(planet, sun, 1.496e11)
490
+ print(result["surface_temp_K"]) # equilibrium surface temperature
491
+ print(result["greenhouse_dT_K"]) # greenhouse warming above bare equilibrium
492
+ print(result["jeans_escape_rate"]) # atmospheric escape rate [kg/s]
493
+ ```
494
+
495
+ Six composition types are supported: `EARTH_LIKE` (N₂/O₂), `CO2_THIN` (Mars), `CO2_THICK` (Venus), `NITROGEN` (Titan), `METHANE`, and `HYDROGEN` (gas dwarfs). Each has distinct density, pressure, and temperature profiles:
496
+
497
+ ![Atmosphere profiles for all six composition types](figures/planet_figures/fig3_atm_zoo.png)
498
+
499
+ The preset planets use their correct compositions. Randomly generated planets get a composition sampled from this set, weighted by the planet's mass and distance from its star.
500
+
501
+ ![Atmosphere profiles for the five solar system presets](figures/planet_figures/fig1b_preset_atm.png)
502
+
503
+ The atmosphere profiles shown above are for Earth, Mars, Venus, Moon (no atmosphere), and Titan. Venus's 9.2 MPa surface pressure and 737 K surface temperature are reproduced correctly. The Moon shows the "no atmosphere" case, which the integrator handles by setting drag to zero.
504
+
505
+ ---
506
+
507
+ ### `climate.py`
508
+
509
+ A 1D energy balance model (EBM) that finds stable surface temperatures including ice-albedo feedback and the carbonate-silicate thermostat. This connects to the habitability scorer and to the RL reward signal.
510
+
511
+ ```python
512
+ from exorl.core.climate import EnergyBalanceModel, find_bifurcation_points
513
+
514
+ ebm = EnergyBalanceModel(planet, star)
515
+
516
+ result = ebm.solve(1.496e11) # solve at 1 AU
517
+ print(result.T_surface_K) # 288 K for Earth
518
+ print(result.climate_state) # "warm_habitable"
519
+ print(result.OLR_W_m2) # outgoing longwave radiation
520
+
521
+ # Find climate transition distances for this planet-star pair
522
+ bif = find_bifurcation_points(planet, star)
523
+ print(bif.snowball_distance_au) # distance where planet freezes
524
+ print(bif.runaway_distance_au) # distance where greenhouse runaway starts
525
+ print(bif.habitable_range_au) # (inner_au, outer_au) habitable window
526
+ ```
527
+
528
+ The model finds two types of transitions. Moving a planet outward past the snowball bifurcation causes runaway cooling — ice increases albedo, which lowers temperature, which grows more ice, until the planet is fully frozen. Moving it inward past the runaway greenhouse transition causes the opposite: water vapour amplifies warming until oceans evaporate. Both transitions are relevant to the habitability score.
529
+
530
+ Climate states: `warm_habitable`, `snowball`, `moist_greenhouse`, `runaway_greenhouse`.
531
+
532
+ Calibration: Earth → 288 K ✓, Mars → snowball ✓, Venus → runaway greenhouse ✓. Earth greenhouse warming is 19 K vs the real 33 K because water vapour feedback is not yet included.
533
+
534
+ ---
535
+
536
+ ## 3. Science Modules
537
+
538
+ ### `habitability.py`
539
+
540
+ Scores a planet on ten factors and returns a 0–1 composite score, an A–F grade, and a written assessment.
541
+
542
+ ```python
543
+ from exorl.core.habitability import assess_habitability
544
+
545
+ # Requires star and orbital distance attached to the planet
546
+ ha = assess_habitability(planet, sun, 1.496e11)
547
+
548
+ print(ha.overall_score) # 0.842 for Earth
549
+ print(ha.grade) # "A"
550
+ print(ha.report()) # full written summary of each factor
551
+ ```
552
+
553
+ The ten factors are: stellar flux, surface temperature, atmospheric pressure, escape velocity, magnetic field protection, tidal locking, stellar XUV activity, greenhouse warming, orbital stability, and water inventory. Each scores 0–1. The composite is the geometric mean, so a planet that fails badly on any single factor will score poorly overall. Any factor below 0.01 acts as a veto — the planet is essentially uninhabitable regardless of other conditions.
554
+
555
+ Solar system calibration: Earth 0.842 (A), Mars 0.361 (D), Venus 0.238 (F), Moon 0.276 (D), Titan 0.135 (F).
556
+
557
+ ![Habitability radar charts for all five presets plus a random planet](figures/science_figures/fig05_habitability_radar.png)
558
+
559
+ The radar charts show how each factor contributes to the overall score. Earth is strong on almost everything. Mars fails primarily on temperature and pressure. Venus fails on temperature and XUV (despite being in the HZ, it has no magnetic field to protect against solar wind). The random planet illustrates how procedurally generated bodies land across the score space.
560
+
561
+ ---
562
+
563
+ ### `orbital_analysis.py`
564
+
565
+ J2-driven secular perturbations, sun-synchronous orbit design, frozen orbit eccentricity, atmospheric drag lifetime, and station-keeping budgets.
566
+
567
+ ```python
568
+ from exorl.core.orbital_analysis import (
569
+ J2Perturbations, SunSynchronousOrbit, FrozenOrbit, AtmosphericDrag
570
+ )
571
+ import math
572
+
573
+ # Nodal precession rate from J2
574
+ j2p = J2Perturbations(planet)
575
+ omega_dot = j2p.nodal_precession_rate(alt=500_000, inc=math.radians(98))
576
+
577
+ # Sun-synchronous inclination — the inclination at which the orbit precesses
578
+ # at exactly one degree per day to stay aligned with the Sun
579
+ ss_inc = SunSynchronousOrbit.sun_sync_inclination(
580
+ planet, alt=500_000, star_yr=365.25*86400)
581
+ print(f"Sun-sync: {math.degrees(ss_inc):.1f}°")
582
+
583
+ # Frozen orbit — eccentricity that cancels odd J harmonics, so the orbit
584
+ # maintains a stable ground track without eccentricity drift
585
+ fe = FrozenOrbit.frozen_eccentricity(
586
+ planet, planet.radius + 500_000, math.radians(98))
587
+ print(f"Frozen eccentricity: {fe:.5f}")
588
+
589
+ # Atmospheric drag lifetime
590
+ drag = AtmosphericDrag(planet)
591
+ decay = drag.lifetime_days(alt=300_000, area=10.0, mass=1000.0, Cd=2.2)
592
+ print(f"Orbit lifetime: {decay:.0f} days")
593
+ ```
594
+
595
+ ![J2 precession, sun-sync inclinations, frozen orbit map, drag lifetimes](figures/science_figures/fig06_orbital_mechanics.png)
596
+
597
+ The frozen orbit eccentricity feeds directly into the RL reward function in `env.py` — an agent that achieves the frozen eccentricity gets a science orbit quality bonus on top of the insertion reward.
598
+
599
+ ---
600
+
601
+ ### `ground_track.py`
602
+
603
+ Sub-satellite ground track, coverage maps, and pass times over ground targets.
604
+
605
+ ```python
606
+ from exorl.core.ground_track import GroundTrack, CoverageMap
607
+
608
+ gt = GroundTrack(planet, alt=500_000, inc=math.radians(98))
609
+ lats, lons = gt.compute(n_orbits=1)
610
+
611
+ cov = CoverageMap(planet, alt=500_000, inc=math.radians(98))
612
+ cov.simulate(days=3)
613
+
614
+ fraction = cov.coverage_fraction() # 0.0 – 1.0
615
+ grid = cov.coverage_grid() # 2D array [lat × lon]
616
+
617
+ # Find next pass over a ground target
618
+ next_pass = gt.next_pass(lat=35.0, lon=135.0, from_time=0)
619
+ ```
620
+
621
+ ![Ground track and 3-day coverage map](figures/science_figures/fig07_ground_track_coverage.png)
622
+
623
+ ---
624
+
625
+ ### `surface_energy.py`
626
+
627
+ Insolation maps, surface temperature distributions across seasons, and polar ice extent as a function of orbital parameters.
628
+
629
+ ```python
630
+ from exorl.core.surface_energy import SurfaceEnergyMap
631
+
632
+ sem = SurfaceEnergyMap(planet, sun)
633
+ flux = sem.insolation_at(lat=45.0, lon=0.0, day_of_year=172) # W/m²
634
+ T_map = sem.temperature_map(day_of_year=172) # [lat × lon] array in K
635
+ ice_lat = sem.polar_ice_latitude() # degrees
636
+ ```
637
+
638
+ ![Insolation and temperature maps across solstice, equinox, and perihelion](figures/science_figures/fig08_surface_energy.png)
639
+
640
+ ---
641
+
642
+ ### `tidal.py`
643
+
644
+ Tidal heating rate, locking timescale, Roche limit, and orbital migration rate.
645
+
646
+ ```python
647
+ from exorl.core.tidal import TidalModel
648
+
649
+ tidal = TidalModel(planet, star)
650
+ heating = tidal.surface_heating_rate() # W/m²
651
+ t_lock = tidal.locking_timescale() # seconds
652
+ locked = tidal.is_tidally_locked(planet.orbital_distance_m)
653
+ roche = tidal.roche_limit() # m
654
+ da_dt = tidal.orbital_migration_rate() # m/s
655
+ ```
656
+
657
+ Tidal locking status feeds into the habitability scorer. A tidally locked planet receives a heavy penalty because one hemisphere is permanently day-side and the other permanently night — creating extreme temperature gradients that make liquid water unlikely to exist across a significant fraction of the surface.
658
+
659
+ ![Tidal heating, locking map, Roche limits, migration timescales](figures/science_figures/fig09_tidal_dynamics.png)
660
+
661
+ ---
662
+
663
+ ### `observation.py`
664
+
665
+ What the planet looks like to a telescope — transit depth, radial velocity semi-amplitude, transmission spectroscopy metric (TSM), and a basic transmission spectrum.
666
+
667
+ ```python
668
+ from exorl.core.observation import (
669
+ transit_depth_ppm, rv_semi_amplitude,
670
+ transmission_spectroscopy_metric, characterise_observations,
671
+ )
672
+ import math
673
+
674
+ G = 6.674e-11
675
+ T_orb = 2*math.pi*math.sqrt(planet.orbital_distance_m**3 / (G*sun.mass))
676
+
677
+ depth = transit_depth_ppm(planet.radius, sun.radius) # 84 ppm for Earth
678
+ K = rv_semi_amplitude(planet.mass, sun.mass, T_orb) # 0.089 m/s for Earth
679
+ tsm = transmission_spectroscopy_metric(planet, sun, planet.orbital_distance_m)
680
+
681
+ # All quantities in one call
682
+ sig = characterise_observations(planet, sun, planet.orbital_distance_m)
683
+ print(sig.transit_depth_ppm)
684
+ print(sig.rv_semi_amplitude_m_s)
685
+ print(sig.tsm)
686
+ print(sig.biosignature_flags) # list of potentially detectable biosignatures
687
+ ```
688
+
689
+ Calibration: Earth transit depth 83.9 ppm (literature: 84), Earth RV 0.089 m/s (literature: 0.089), Jupiter RV 12.46 m/s (literature: 12.5), TRAPPIST-1e TSM 19.7 (literature: ~14).
690
+
691
+ ---
692
+
693
+ ## 4. Mission Design
694
+
695
+ ### `mission.py`
696
+
697
+ Delta-V budgets, aerobraking corridor analysis, and mission-level planning utilities.
698
+
699
+ ```python
700
+ from exorl.core.mission import MissionDesign, AerobrakingCorridor
701
+
702
+ G = 6.674e-11
703
+ md = MissionDesign(planet, G*planet.mass)
704
+
705
+ # Hohmann transfer from parking orbit to target altitude
706
+ dv1, dv2 = md.hohmann_transfer(r1=planet.radius+400_000,
707
+ r2=planet.radius+1000_000)
708
+
709
+ # Aerobraking corridor boundaries
710
+ corridor = AerobrakingCorridor(planet)
711
+ alt_min, alt_max = corridor.safe_altitude_range(v_entry=6000.0, heat_limit=1e7)
712
+ ```
713
+
714
+ ![Delta-V budgets, aerobraking corridor, porkchop overview](figures/science_figures/fig10_mission_design.png)
715
+
716
+ ---
717
+
718
+ ### `heliocentric.py`
719
+
720
+ Lambert solver, Kepler propagator, and heliocentric integrator for interplanetary trajectory calculations.
721
+
722
+ ```python
723
+ from exorl.core.heliocentric import LambertSolver, KeplerPropagator, planet_state, MU_SUN, AU
724
+ import numpy as np
725
+
726
+ solver = LambertSolver(MU_SUN)
727
+ prop = KeplerPropagator(MU_SUN)
728
+
729
+ # Planet positions at departure and arrival
730
+ r1v, v1p = planet_state(1.0*AU, 0.0) # Earth at t=0
731
+ r2v, v2p = planet_state(1.524*AU, 260*86400) # Mars 260 days later
732
+
733
+ # Solve for the connecting trajectory
734
+ v1_sc, v2_sc = solver.solve(r1v, r2v, 260*86400)
735
+
736
+ vinf_dep = np.linalg.norm(v1_sc - v1p) # departure excess speed [m/s]
737
+ vinf_arr = np.linalg.norm(v2_sc - v2p) # arrival excess speed [m/s]
738
+
739
+ # Generate trajectory points for plotting (400 steps)
740
+ times = np.linspace(0, 260*86400, 400)
741
+ traj = prop.orbit_at_time(r1v, v1_sc, times) # (400, 6) array
742
+ ```
743
+
744
+ The Lambert solver uses the Bate-Mueller-White universal variable method with bisection. Calibration: Earth→Mars near-Hohmann gives v∞_dep = 2.95 km/s and v∞_arr = 2.65 km/s, matching the textbook Hohmann values to within 1%.
745
+
746
+ ![Heliocentric transfer arc coloured by spacecraft speed](figures/science_figures/fig11_heliocentric_transfer.png)
747
+
748
+ The arc is coloured by spacecraft speed — fast near perihelion (bright yellow), slow near aphelion (dark blue). The velocity arrows at Earth and Mars show the departure and arrival directions.
749
+
750
+ ---
751
+
752
+ ### `soi.py`
753
+
754
+ Sphere of influence radius, frame transforms between heliocentric and planet-centred coordinates, and hyperbolic approach/departure geometry.
755
+
756
+ ```python
757
+ from exorl.core.soi import (
758
+ SphereOfInfluence, HyperbolicDeparture,
759
+ HyperbolicArrival, patched_conic_budget
760
+ )
761
+
762
+ soi_mars = SphereOfInfluence.from_planet(mars, 1.524*AU)
763
+ print(soi_mars.r_laplace / 1e6) # 577 Mm
764
+
765
+ # Transform to planet frame at SOI entry
766
+ r_planet, v_planet = soi_mars.to_planet_frame(
767
+ sc_helio_pos, sc_helio_vel,
768
+ mars_helio_pos, mars_helio_vel
769
+ )
770
+
771
+ # Arrival v∞
772
+ vinf = soi_mars.arrival_vinf(sc_helio_vel, mars_helio_vel)
773
+
774
+ # Full mission delta-V budget
775
+ G = 6.674e-11
776
+ budget = patched_conic_budget(
777
+ departure_planet_mass = earth.mass,
778
+ departure_planet_radius = earth.radius,
779
+ departure_parking_alt = 300_000,
780
+ arrival_planet_mass = mars.mass,
781
+ arrival_planet_radius = mars.radius,
782
+ arrival_periapsis_alt = 300_000,
783
+ arrival_target_alt = 300_000,
784
+ vinf_departure_m_s = 2945.0,
785
+ vinf_arrival_m_s = 2648.0,
786
+ )
787
+ print(budget["dv_total_m_s"]) # ~5960 m/s for Earth→Mars
788
+ ```
789
+
790
+ ---
791
+
792
+ ### `launch_window.py`
793
+
794
+ Porkchop grid computation, optimal window selection, and the RL decision space interface.
795
+
796
+ ```python
797
+ from exorl.core.launch_window import PorkchopData, LaunchDecisionSpace, AU
798
+ import numpy as np
799
+
800
+ # Compute the porkchop grid
801
+ dep_days = np.linspace(0, 780, 50)
802
+ arr_days = np.linspace(150, 980, 50)
803
+
804
+ pc = PorkchopData.compute(1.0*AU, 1.524*AU, dep_days, arr_days,
805
+ dep_name="Earth", arr_name="Mars")
806
+ best = pc.best_window(max_c3=15.0, max_vinf_arr=5.0)
807
+ print(best.report())
808
+
809
+ # RL decision space — discretises the porkchop into agent-accessible slots
810
+ space = LaunchDecisionSpace(1.0*AU, 1.524*AU, n_dep=20, n_arr=20,
811
+ window_duration_days=780)
812
+
813
+ cost = space.cost(dep_idx=10, arr_idx=12)
814
+ # {"valid": True, "c3": 9.4, "vinf_arr": 3.1, "tof_days": 294}
815
+
816
+ obs = space.observation(10, 12) # 6-element observation vector
817
+ r = space.reward(10, 12) # scalar reward in [-1, 0]
818
+ bi, bj = space.best_action()
819
+ ```
820
+
821
+ ![C3 porkchop over one Earth–Mars synodic period](figures/science_figures/fig12_porkchop_c3.png)
822
+
823
+ The two green valleys are the two launch opportunities within one 780-day synodic period. The gold circle marks the minimum-C3 window at 9.3 km²/s². Dashed contours show constant time-of-flight.
824
+
825
+ ![Arrival v∞ porkchop with time-of-flight contours](figures/science_figures/fig13_porkchop_vinf.png)
826
+
827
+ ![4-panel mission dashboard](figures/science_figures/fig14_transfer_dashboard.png)
828
+
829
+ The dashboard combines the heliocentric transfer arc (top left), C3 porkchop (top right), arrival v∞ porkchop (bottom left), and SOI approach geometry (bottom right) into a single mission overview.
830
+
831
+ ---
832
+
833
+ ## 5. Population
834
+
835
+ ### `population.py` · `population_demo.py`
836
+
837
+ Generates a large population of planets and computes summary statistics, composition classification, habitability distribution, and property correlations.
838
+
839
+ ```python
840
+ from exorl.core.population import PlanetPopulation
841
+
842
+ pop = PlanetPopulation.generate(n=500, seed=42, verbose=True)
843
+ pop.save("population_500.csv")
844
+
845
+ # Load later
846
+ pop = PlanetPopulation.load("population_500.csv")
847
+ print(pop.summary())
848
+ ```
849
+
850
+ From the command line:
851
+
852
+ ```bash
853
+ python examples/population_demo.py # generate 500 planets
854
+ python examples/population_demo.py --n 2000 --seed 0 # larger run
855
+ python examples/population_demo.py --fast # 100 planets, quick test
856
+ python examples/population_demo.py --load examples/csv-data/population_500.csv # use existing CSV
857
+ ```
858
+
859
+ The CSV contains 22 columns per planet covering physical properties, interior quantities, atmosphere state, habitability score, composition, and observational signatures. See the [population feature reference](#) for the full column list.
860
+
861
+ Key results from a 500-planet run: 16.4% of randomly generated planets score above 0.5 on habitability, even when all are placed inside the stellar habitable zone. The distribution peaks around Grade C/D — most planets are marginal. This has direct implications for RL training: the habitability reward bonus is sparse, which is why curriculum mode exists.
862
+
863
+ ![Mass-radius diagram with Zeng 2013 composition curves, coloured by habitability](figures/science_figures/fig15_mass_radius.png)
864
+
865
+ The composition curves show that most generated planets land between rocky and water-rich. The solar system bodies (Earth, Venus, Mars, Moon) all sit correctly on or near the rocky curve. The green points (high habitability) cluster near Earth-mass.
866
+
867
+ ![Habitability score histogram across 500 planets](figures/science_figures/fig16_habitability_distribution.png)
868
+
869
+ ![Pearson correlation matrix between all physical properties](figures/science_figures/fig17_correlation_heatmap.png)
870
+
871
+ Strong correlations to note: mass and radius (r=0.89, expected), B-field and mass (r=0.50, larger planets sustain stronger dynamos), heat flux and MoI (r=−0.52, denser cores radiate less). Habitability correlates most strongly with orbital distance (r=−0.60) because HZ placement is randomised with spread.
872
+
873
+ ![Full population statistics dashboard](figures/science_figures/fig18_population_dashboard.png)
874
+
875
+ ---
876
+
877
+ ## 6. RL Environments
878
+
879
+ ### `env.py` — OrbitalInsertionEnv
880
+
881
+ Single-planet orbital insertion. The agent fires burns to slow a spacecraft from a hyperbolic approach into a stable circular orbit at the target altitude. The environment is wired directly to the science stack — J2 comes from the interior model, drag comes from the multi-layer atmosphere, and the reward includes a habitability-weighted science bonus.
882
+
883
+ ```python
884
+ from exorl.core.env import OrbitalInsertionEnv
885
+
886
+ env = OrbitalInsertionEnv(
887
+ planet_preset = "earth", # or randomize_planet=True for training
888
+ curriculum_mode = True, # sort episodes by habitability: easy → hard
889
+ obs_dim = 18, # 18 = full science context, 10 = legacy
890
+ target_altitude = 300_000,
891
+ wet_mass = 1000.0,
892
+ dry_mass = 300.0,
893
+ max_thrust = 500.0,
894
+ Isp = 320.0,
895
+ )
896
+
897
+ obs, info = env.reset()
898
+ print(info["j2"]) # interior-derived J2 for this episode
899
+ print(info["habitability"]) # 0–1 habitability score
900
+ print(info["star"]) # host star name
901
+ print(info["atm_model"]) # "multi-layer" or "exponential"
902
+
903
+ action = env.action_space.sample()
904
+ obs, reward, terminated, truncated, info = env.step(action)
905
+ ```
906
+
907
+ **Observation vector (18 floats):**
908
+
909
+ | Index | Feature | Notes |
910
+ |---|---|---|
911
+ | 0–5 | Dynamic state | altitude, speed, FPA, eccentricity, fuel, heat |
912
+ | 6–9 | Planet context | radius, gravity, atmosphere density, target altitude |
913
+ | 10–13 | Interior & atmosphere | J₂, magnetic field, surface pressure, habitability score |
914
+ | 14–15 | Stellar context | star type, orbital distance |
915
+ | 16–17 | Orbit design | frozen orbit eccentricity, sun-sync inclination |
916
+
917
+ Indices 0–9 change every step. Indices 10–17 are constant per episode — they encode the task identity and allow a single policy to generalise across physically diverse planets.
918
+
919
+ **Curriculum mode** generates a pool of planets, ranks them by habitability score, and serves them from easiest to hardest. Earth-like planets come first; exotic high-gravity or airless worlds come later.
920
+
921
+ ```python
922
+ env = OrbitalInsertionEnv(
923
+ randomize_planet = True,
924
+ curriculum_mode = True,
925
+ curriculum_pool_size = 200,
926
+ curriculum_easy_first = True,
927
+ )
928
+ ```
929
+
930
+ ---
931
+
932
+ ### `interplanetary_env.py` — InterplanetaryEnv
933
+
934
+ Full planet-to-planet mission in a single episode across three sequential phases: launch window selection, heliocentric cruise, and capture orbit insertion. Each phase uses the correct underlying physics.
935
+
936
+ ```python
937
+ from exorl.core.interplanetary_env import InterplanetaryEnv
938
+ import numpy as np
939
+
940
+ env = InterplanetaryEnv(
941
+ departure_planet_name = "earth",
942
+ arrival_planet_name = "mars",
943
+ n_dep_slots = 20,
944
+ n_arr_slots = 20,
945
+ wet_mass = 1500.0, # needs fuel for both departure burn AND capture
946
+ dry_mass = 400.0,
947
+ )
948
+
949
+ obs, info = env.reset()
950
+ ```
951
+
952
+ **Phase A — Window selection** (`info["phase"] == "window"`)
953
+
954
+ The agent adjusts `action[0]` (departure slot) and `action[1]` (arrival slot) continuously. Setting `action[2] > 0` commits the choice. On commit, the Lambert solver runs, the departure burn is applied via the rocket equation, and the spacecraft is placed at the departure planet with the correct heliocentric velocity.
955
+
956
+ ```python
957
+ # Immediately commit to the best window
958
+ bi, bj = env._space.best_action()
959
+ a = np.array([(bi / 19)*2 - 1, (bj / 19)*2 - 1, 0.9, 0.0])
960
+ obs, reward, done, trunc, info = env.step(a)
961
+ ```
962
+
963
+ **Phase B — Heliocentric cruise** (`info["phase"] == "cruise"`)
964
+
965
+ One step equals one simulated day. The Kepler propagator advances the spacecraft. The agent can fire mid-course corrections via `action[3]` (magnitude) and `action[0:3]` (RTN direction). The phase ends automatically when the spacecraft enters the target SOI.
966
+
967
+ ```python
968
+ while info["phase"] == "cruise":
969
+ obs, reward, done, trunc, info = env.step(np.zeros(4)) # coast
970
+ ```
971
+
972
+ **Phase C — SOI capture** (`info["phase"] == "capture"`)
973
+
974
+ Identical physics to `OrbitalInsertionEnv`. The agent fires retrograde burns to circularise at the target altitude.
975
+
976
+ **Observation vector (28 floats):**
977
+
978
+ | Index | Content |
979
+ |---|---|
980
+ | 0 | Phase indicator (0=window, 0.5=cruise, 1=capture) |
981
+ | 1–6 | Window context: departure slot, arrival slot, C3, v∞_arr, ToF, valid flag |
982
+ | 7–13 | Heliocentric state: radius, distance to target, speed, angle to target, elapsed time, fuel, in-SOI flag |
983
+ | 14–23 | Planetocentric state (same layout as OrbitalInsertionEnv obs[0:10]) |
984
+ | 24–27 | Target planet context: habitability, mass, radius, surface pressure |
985
+
986
+ **Typical episode — Earth to Mars:**
987
+
988
+ | Phase | Steps | Simulated time | What happens |
989
+ |---|---|---|---|
990
+ | Window | 1–5 | instant | Lambert solve, departure burn (3638 m/s), 440 kg fuel remains |
991
+ | Cruise | ~264 | 264 days | Kepler propagation, arrives at Mars SOI |
992
+ | Capture | ~500 | ~80 min | Retrograde burns, 2153 m/s needed, feasible |
993
+
994
+ ---
995
+
996
+ ## 7. Visualisation
997
+
998
+ ### `exorl/visualization/visualizer.py`
999
+
1000
+ All plot functions follow the same style — white background, Wong colour palette, no decorative elements, publication-quality at 300 DPI.
1001
+
1002
+ ```python
1003
+ from exorl.visualization.visualizer import (
1004
+ plot_planet_cross_section,
1005
+ plot_atmosphere_profile,
1006
+ plot_heliocentric_transfer,
1007
+ plot_porkchop,
1008
+ plot_soi_approach,
1009
+ plot_transfer_dashboard,
1010
+ plot_mass_radius,
1011
+ plot_habitability_distribution,
1012
+ plot_correlation_heatmap,
1013
+ plot_population_dashboard,
1014
+ save_figure,
1015
+ apply_journal_style,
1016
+ )
1017
+
1018
+ apply_journal_style()
1019
+
1020
+ fig = plot_porkchop(pc, quantity="c3", best_window=best)
1021
+ save_figure(fig, "my_porkchop", output_dir="./output")
1022
+ # Saves my_porkchop.png and my_porkchop.pdf at 300 DPI
1023
+ ```
1024
+
1025
+ `save_figure` always saves both PNG and PDF. The PDF is vector and suitable for journal submission.
1026
+
1027
+ ---
1028
+
1029
+ ## 8. Examples & Scripts
1030
+
1031
+ All commands in this section assume you're running from the repository root (`ExoRL/`). Figures are written to:
1032
+
1033
+ - `figures/science_figures/`
1034
+ - `figures/planet_figures/`
1035
+
1036
+ ### `examples/science_demo.py`
1037
+
1038
+ Runs the full science feature demonstration and produces figures `fig01` through `fig10` in `figures/science_figures/`.
1039
+
1040
+ ```bash
1041
+ python examples/science_demo.py
1042
+ ```
1043
+
1044
+ ### `examples/planets_demo.py`
1045
+
1046
+ Produces the generator figures (`fig1a` through `fig6`) in `figures/planet_figures/`.
1047
+
1048
+ ```bash
1049
+ python examples/planets_demo.py
1050
+ ```
1051
+
1052
+ ### `examples/population_demo.py`
1053
+
1054
+ Generates a planet population and produces figures `fig15` through `fig18`.
1055
+
1056
+ ```bash
1057
+ python examples/population_demo.py --n 500
1058
+ python examples/population_demo.py --load examples/csv-data/population_500.csv # skip generation
1059
+ ```
1060
+
1061
+ ### `examples/transfer_viz_demo.py`
1062
+
1063
+ Produces the interplanetary transfer figures (`fig11` through `fig14`).
1064
+
1065
+ ```bash
1066
+ python examples/transfer_viz_demo.py
1067
+ ```
1068
+
1069
+ ---
1070
+
1071
+ ## Troubleshooting
1072
+
1073
+ ### “ModuleNotFoundError” for RL dependencies (gymnasium / torch / stable-baselines3)
1074
+ - Recreate a clean venv and follow the install commands in `## Getting Started` (use `pip install -e ".[rl]"`).
1075
+
1076
+ ### “Observation dimension mismatch” when warm-starting SAC from BC
1077
+ - For BC/SAC warm-start, keep `obs_dim` consistent across all steps.
1078
+ - Recommended path: leave science enabled (do not pass `--no-science` to `train_sac.py`), so the default `obs_dim=18` is used everywhere.
1079
+
1080
+ ### Figures/data go into unexpected folders
1081
+ - All scripts use relative paths from the repository root. Run from `ExoRL/` and expect:
1082
+ - figure outputs under `figures/science_figures/` and `figures/planet_figures/`
1083
+ - demo datasets under `demos/`
1084
+ - training artifacts under `training_runs/`
1085
+
1086
+ ### Visualization import errors
1087
+ - If you see import errors here, verify `matplotlib` is installed (it is part of the base dependencies) and that you are importing from `exorl.visualization`.
1088
+
1089
+ ### “File not found” for evaluation
1090
+ - `eval_generalisation.py` expects a path to the trained SB3 model zip:
1091
+ - `training_runs/<run_name>/model_final.zip`