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.
- exorl-0.1.1/PKG-INFO +1091 -0
- exorl-0.1.1/README.md +1067 -0
- exorl-0.1.1/exorl/__init__.py +0 -0
- exorl-0.1.1/exorl/cli.py +64 -0
- exorl-0.1.1/exorl/commands/__init__.py +12 -0
- exorl-0.1.1/exorl/commands/eval_generalisation.py +128 -0
- exorl-0.1.1/exorl/commands/generate_demos.py +206 -0
- exorl-0.1.1/exorl/commands/pretrain_bc.py +183 -0
- exorl-0.1.1/exorl/commands/train_sac.py +424 -0
- exorl-0.1.1/exorl/core/__init__.py +222 -0
- exorl-0.1.1/exorl/core/atmosphere_science.py +901 -0
- exorl-0.1.1/exorl/core/climate.py +798 -0
- exorl-0.1.1/exorl/core/comms.py +324 -0
- exorl-0.1.1/exorl/core/env.py +813 -0
- exorl-0.1.1/exorl/core/generator.py +368 -0
- exorl-0.1.1/exorl/core/geology.py +504 -0
- exorl-0.1.1/exorl/core/ground_track.py +492 -0
- exorl-0.1.1/exorl/core/habitability.py +716 -0
- exorl-0.1.1/exorl/core/heliocentric.py +935 -0
- exorl-0.1.1/exorl/core/interior.py +664 -0
- exorl-0.1.1/exorl/core/interplanetary_env.py +864 -0
- exorl-0.1.1/exorl/core/kepler_catalog.py +694 -0
- exorl-0.1.1/exorl/core/launch_window.py +383 -0
- exorl-0.1.1/exorl/core/mission.py +743 -0
- exorl-0.1.1/exorl/core/observation.py +702 -0
- exorl-0.1.1/exorl/core/orbital_analysis.py +818 -0
- exorl-0.1.1/exorl/core/physics.py +323 -0
- exorl-0.1.1/exorl/core/planet.py +530 -0
- exorl-0.1.1/exorl/core/planet_io.py +326 -0
- exorl-0.1.1/exorl/core/population.py +565 -0
- exorl-0.1.1/exorl/core/power.py +291 -0
- exorl-0.1.1/exorl/core/science_ops_env.py +497 -0
- exorl-0.1.1/exorl/core/soi.py +277 -0
- exorl-0.1.1/exorl/core/star.py +475 -0
- exorl-0.1.1/exorl/core/surface_energy.py +486 -0
- exorl-0.1.1/exorl/core/thermal_evolution.py +596 -0
- exorl-0.1.1/exorl/core/tidal.py +516 -0
- exorl-0.1.1/exorl/visualization/__init__.py +35 -0
- exorl-0.1.1/exorl/visualization/visualizer.py +1836 -0
- exorl-0.1.1/exorl.egg-info/PKG-INFO +1091 -0
- exorl-0.1.1/exorl.egg-info/SOURCES.txt +45 -0
- exorl-0.1.1/exorl.egg-info/dependency_links.txt +1 -0
- exorl-0.1.1/exorl.egg-info/entry_points.txt +2 -0
- exorl-0.1.1/exorl.egg-info/requires.txt +11 -0
- exorl-0.1.1/exorl.egg-info/top_level.txt +1 -0
- exorl-0.1.1/pyproject.toml +46 -0
- 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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
376
|
+
|
|
377
|
+
**Batch statistics** — generating 50 planets with `magnetic_field_enabled=True` produces this distribution across physical properties:
|
|
378
|
+
|
|
379
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
826
|
+
|
|
827
|
+

|
|
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
|
+

|
|
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
|
+

|
|
868
|
+
|
|
869
|
+

|
|
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
|
+

|
|
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`
|