Skip to content

Commit 3c62aa7

Browse files
committed
ENH: add deployable ejection at apogee to Mission
Final slice of the multistage/mission architecture roadmap. A single-stage vehicle with exactly one deployable whose ejection is the string "apogee" now runs three Flights: the carrier stage with the deployable aboard up to apogee (reusing Flight's existing terminate_on_apogee flag - no new event machinery needed, matching the plan's stated approach), then two children from that apogee state onward - the carrier continuing without the deployable's mass, and the deployable flying its own free_rocket. Generalizes the two-stage commit's momentum split (_split_separation_delta_v) into _momentum_split(mass_a, mass_b, delta_v), reused as-is for the carrier/deployable split: same physics, same _handoff_state, just different masses (carrier's total_mass at apogee time vs. the deployable's free_rocket total mass at its own t=0). Deployables built from add_surface() instead of a fully-built free_rocket, and ejection triggers other than "apogee", raise NotImplementedError - out of scope, per the roadmap. Also updates docs/notebooks/multistage_mission.ipynb with the deployable-ejection demo, executed end-to-end, and finalizes the notebook's "Not built yet" section now that all 8 planned commits have landed.
1 parent caebcb9 commit 3c62aa7

3 files changed

Lines changed: 408 additions & 79 deletions

File tree

‎docs/notebooks/multistage_mission.ipynb‎

Lines changed: 153 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,25 @@
66
"source": [
77
"# Multistage & Mission Architecture\n",
88
"\n",
9-
"Demonstrates the multistage/mission architecture as it is built up commit by commit (see `mission_multistage_design.md`). Each section below corresponds to one landed commit and is added to as the next commit lands.\n",
9+
"Demonstrates the multistage/mission architecture as it is built up commit by commit (see `mission_multistage_design.md`). Each section below corresponds to one landed commit. This is the last item on the original roadmap - see \"Not built yet\" at the bottom for what's deliberately still out of scope.\n",
1010
"\n",
1111
"**Landed so far:**\n",
1212
"1. `Stage` — wraps a single-stage `Rocket`\n",
1313
"2. `Deployable` — a carried payload released mid-flight\n",
1414
"3. `MultiStageRocket` — composes a stage + its deployables into one flight-ready `Rocket`\n",
1515
"4. `MultiStageRocket.flight_rocket` for more than one active stage (a booster with an inert sustainer riding on top)\n",
16-
"5. `Mission` — orchestrates one `Flight` per vehicle configuration (single-stage degenerate case, and now a full two-stage mission: deterministic burnout+delay separation, state handoff, and sustainer ignition timing)\n"
16+
"5. `Mission` — orchestrates one `Flight` per vehicle configuration: the single-stage degenerate case, a full two-stage mission (deterministic burnout+delay separation, state handoff, ignition timing), and a deployable ejecting at apogee\n"
1717
]
1818
},
1919
{
2020
"cell_type": "code",
2121
"execution_count": null,
2222
"metadata": {
2323
"execution": {
24-
"iopub.execute_input": "2026-08-14T09:32:53.749600Z",
25-
"iopub.status.busy": "2026-08-14T09:32:53.749365Z",
26-
"iopub.status.idle": "2026-08-14T09:32:55.135838Z",
27-
"shell.execute_reply": "2026-08-14T09:32:55.135430Z"
24+
"iopub.execute_input": "2026-08-14T09:43:41.487732Z",
25+
"iopub.status.busy": "2026-08-14T09:43:41.487551Z",
26+
"iopub.status.idle": "2026-08-14T09:43:42.862293Z",
27+
"shell.execute_reply": "2026-08-14T09:43:42.861881Z"
2828
}
2929
},
3030
"outputs": [],
@@ -47,10 +47,10 @@
4747
"execution_count": null,
4848
"metadata": {
4949
"execution": {
50-
"iopub.execute_input": "2026-08-14T09:32:55.138191Z",
51-
"iopub.status.busy": "2026-08-14T09:32:55.137935Z",
52-
"iopub.status.idle": "2026-08-14T09:32:55.162940Z",
53-
"shell.execute_reply": "2026-08-14T09:32:55.162606Z"
50+
"iopub.execute_input": "2026-08-14T09:43:42.864593Z",
51+
"iopub.status.busy": "2026-08-14T09:43:42.864357Z",
52+
"iopub.status.idle": "2026-08-14T09:43:42.889043Z",
53+
"shell.execute_reply": "2026-08-14T09:43:42.888691Z"
5454
}
5555
},
5656
"outputs": [],
@@ -103,10 +103,10 @@
103103
"execution_count": null,
104104
"metadata": {
105105
"execution": {
106-
"iopub.execute_input": "2026-08-14T09:32:55.190343Z",
107-
"iopub.status.busy": "2026-08-14T09:32:55.190147Z",
108-
"iopub.status.idle": "2026-08-14T09:32:55.193022Z",
109-
"shell.execute_reply": "2026-08-14T09:32:55.192688Z"
106+
"iopub.execute_input": "2026-08-14T09:43:42.917413Z",
107+
"iopub.status.busy": "2026-08-14T09:43:42.917223Z",
108+
"iopub.status.idle": "2026-08-14T09:43:42.919902Z",
109+
"shell.execute_reply": "2026-08-14T09:43:42.919550Z"
110110
}
111111
},
112112
"outputs": [],
@@ -134,10 +134,10 @@
134134
"execution_count": null,
135135
"metadata": {
136136
"execution": {
137-
"iopub.execute_input": "2026-08-14T09:32:55.194536Z",
138-
"iopub.status.busy": "2026-08-14T09:32:55.194412Z",
139-
"iopub.status.idle": "2026-08-14T09:32:55.196728Z",
140-
"shell.execute_reply": "2026-08-14T09:32:55.196474Z"
137+
"iopub.execute_input": "2026-08-14T09:43:42.921466Z",
138+
"iopub.status.busy": "2026-08-14T09:43:42.921344Z",
139+
"iopub.status.idle": "2026-08-14T09:43:42.923802Z",
140+
"shell.execute_reply": "2026-08-14T09:43:42.923374Z"
141141
}
142142
},
143143
"outputs": [],
@@ -160,10 +160,10 @@
160160
"execution_count": null,
161161
"metadata": {
162162
"execution": {
163-
"iopub.execute_input": "2026-08-14T09:32:55.198195Z",
164-
"iopub.status.busy": "2026-08-14T09:32:55.198072Z",
165-
"iopub.status.idle": "2026-08-14T09:32:55.201107Z",
166-
"shell.execute_reply": "2026-08-14T09:32:55.200801Z"
163+
"iopub.execute_input": "2026-08-14T09:43:42.925422Z",
164+
"iopub.status.busy": "2026-08-14T09:43:42.925275Z",
165+
"iopub.status.idle": "2026-08-14T09:43:42.928373Z",
166+
"shell.execute_reply": "2026-08-14T09:43:42.928094Z"
167167
}
168168
},
169169
"outputs": [],
@@ -184,10 +184,10 @@
184184
"execution_count": null,
185185
"metadata": {
186186
"execution": {
187-
"iopub.execute_input": "2026-08-14T09:32:55.202470Z",
188-
"iopub.status.busy": "2026-08-14T09:32:55.202372Z",
189-
"iopub.status.idle": "2026-08-14T09:32:55.204229Z",
190-
"shell.execute_reply": "2026-08-14T09:32:55.203950Z"
187+
"iopub.execute_input": "2026-08-14T09:43:42.929756Z",
188+
"iopub.status.busy": "2026-08-14T09:43:42.929637Z",
189+
"iopub.status.idle": "2026-08-14T09:43:42.931975Z",
190+
"shell.execute_reply": "2026-08-14T09:43:42.931472Z"
191191
}
192192
},
193193
"outputs": [],
@@ -211,10 +211,10 @@
211211
"execution_count": null,
212212
"metadata": {
213213
"execution": {
214-
"iopub.execute_input": "2026-08-14T09:32:55.205694Z",
215-
"iopub.status.busy": "2026-08-14T09:32:55.205590Z",
216-
"iopub.status.idle": "2026-08-14T09:32:55.214233Z",
217-
"shell.execute_reply": "2026-08-14T09:32:55.213919Z"
214+
"iopub.execute_input": "2026-08-14T09:43:42.933547Z",
215+
"iopub.status.busy": "2026-08-14T09:43:42.933420Z",
216+
"iopub.status.idle": "2026-08-14T09:43:42.941719Z",
217+
"shell.execute_reply": "2026-08-14T09:43:42.941413Z"
218218
}
219219
},
220220
"outputs": [],
@@ -263,10 +263,10 @@
263263
"execution_count": null,
264264
"metadata": {
265265
"execution": {
266-
"iopub.execute_input": "2026-08-14T09:32:55.215684Z",
267-
"iopub.status.busy": "2026-08-14T09:32:55.215576Z",
268-
"iopub.status.idle": "2026-08-14T09:32:55.233550Z",
269-
"shell.execute_reply": "2026-08-14T09:32:55.233244Z"
266+
"iopub.execute_input": "2026-08-14T09:43:42.943171Z",
267+
"iopub.status.busy": "2026-08-14T09:43:42.943050Z",
268+
"iopub.status.idle": "2026-08-14T09:43:42.961567Z",
269+
"shell.execute_reply": "2026-08-14T09:43:42.961261Z"
270270
}
271271
},
272272
"outputs": [],
@@ -313,10 +313,10 @@
313313
"execution_count": null,
314314
"metadata": {
315315
"execution": {
316-
"iopub.execute_input": "2026-08-14T09:32:55.234958Z",
317-
"iopub.status.busy": "2026-08-14T09:32:55.234836Z",
318-
"iopub.status.idle": "2026-08-14T09:32:55.250443Z",
319-
"shell.execute_reply": "2026-08-14T09:32:55.250148Z"
316+
"iopub.execute_input": "2026-08-14T09:43:42.963017Z",
317+
"iopub.status.busy": "2026-08-14T09:43:42.962909Z",
318+
"iopub.status.idle": "2026-08-14T09:43:42.978386Z",
319+
"shell.execute_reply": "2026-08-14T09:43:42.978094Z"
320320
}
321321
},
322322
"outputs": [],
@@ -353,10 +353,10 @@
353353
"execution_count": null,
354354
"metadata": {
355355
"execution": {
356-
"iopub.execute_input": "2026-08-14T09:32:55.251978Z",
357-
"iopub.status.busy": "2026-08-14T09:32:55.251876Z",
358-
"iopub.status.idle": "2026-08-14T09:32:55.314671Z",
359-
"shell.execute_reply": "2026-08-14T09:32:55.314335Z"
356+
"iopub.execute_input": "2026-08-14T09:43:42.979794Z",
357+
"iopub.status.busy": "2026-08-14T09:43:42.979692Z",
358+
"iopub.status.idle": "2026-08-14T09:43:43.042549Z",
359+
"shell.execute_reply": "2026-08-14T09:43:43.042227Z"
360360
}
361361
},
362362
"outputs": [],
@@ -395,10 +395,10 @@
395395
"execution_count": null,
396396
"metadata": {
397397
"execution": {
398-
"iopub.execute_input": "2026-08-14T09:32:55.316243Z",
399-
"iopub.status.busy": "2026-08-14T09:32:55.316120Z",
400-
"iopub.status.idle": "2026-08-14T09:32:55.334402Z",
401-
"shell.execute_reply": "2026-08-14T09:32:55.334090Z"
398+
"iopub.execute_input": "2026-08-14T09:43:43.044565Z",
399+
"iopub.status.busy": "2026-08-14T09:43:43.044439Z",
400+
"iopub.status.idle": "2026-08-14T09:43:43.062916Z",
401+
"shell.execute_reply": "2026-08-14T09:43:43.062594Z"
402402
}
403403
},
404404
"outputs": [],
@@ -447,10 +447,10 @@
447447
"execution_count": null,
448448
"metadata": {
449449
"execution": {
450-
"iopub.execute_input": "2026-08-14T09:32:55.335889Z",
451-
"iopub.status.busy": "2026-08-14T09:32:55.335782Z",
452-
"iopub.status.idle": "2026-08-14T09:32:55.377981Z",
453-
"shell.execute_reply": "2026-08-14T09:32:55.377662Z"
450+
"iopub.execute_input": "2026-08-14T09:43:43.064399Z",
451+
"iopub.status.busy": "2026-08-14T09:43:43.064295Z",
452+
"iopub.status.idle": "2026-08-14T09:43:43.106120Z",
453+
"shell.execute_reply": "2026-08-14T09:43:43.105806Z"
454454
}
455455
},
456456
"outputs": [],
@@ -478,21 +478,117 @@
478478
" print(f\" t={time:8.3f}s {event}\")"
479479
]
480480
},
481+
{
482+
"cell_type": "markdown",
483+
"metadata": {},
484+
"source": [
485+
"## `Mission`: a deployable ejecting at apogee\n",
486+
"\n",
487+
"A single-stage vehicle with exactly one deployable whose `ejection` is the string `\"apogee\"` runs a different three-`Flight` sequence: the carrier stage flying with the deployable aboard (using the existing `terminate_on_apogee` flag on `Flight` - no new event machinery needed), then two children from the apogee state onward - the carrier stage continuing without the deployable's mass, and the deployable flying its own `free_rocket`.\n",
488+
"\n",
489+
"Same `_handoff_state` transform as the two-stage case, and the same momentum-conserving delta_v split, just generalized: any two children of a split share `separation_delta_v` in inverse proportion to their post-split masses.\n",
490+
"\n",
491+
"`ejection` other than `\"apogee\"`, and deployables defined via `add_surface()` instead of `free_rocket`, aren't supported yet."
492+
]
493+
},
494+
{
495+
"cell_type": "code",
496+
"execution_count": null,
497+
"metadata": {
498+
"execution": {
499+
"iopub.execute_input": "2026-08-14T09:43:43.107691Z",
500+
"iopub.status.busy": "2026-08-14T09:43:43.107585Z",
501+
"iopub.status.idle": "2026-08-14T09:43:43.123541Z",
502+
"shell.execute_reply": "2026-08-14T09:43:43.123192Z"
503+
}
504+
},
505+
"outputs": [],
506+
"source": [
507+
"carrier_rocket = Rocket(\n",
508+
" radius=0.1,\n",
509+
" mass=10.0,\n",
510+
" inertia=(1.0, 1.0, 0.01),\n",
511+
" power_off_drag=0.5,\n",
512+
" power_on_drag=0.6,\n",
513+
" center_of_mass_without_motor=0.0,\n",
514+
")\n",
515+
"carrier_rocket.add_motor(\n",
516+
" PointMassMotor(\n",
517+
" thrust_source=400, dry_mass=1.0, propellant_initial_mass=2.0, burn_time=1.0\n",
518+
" ),\n",
519+
" position=0.0,\n",
520+
")\n",
521+
"carrier = Stage(name=\"carrier\", rocket=carrier_rocket)\n",
522+
"\n",
523+
"payload_rocket = Rocket(\n",
524+
" radius=0.02,\n",
525+
" mass=1.0,\n",
526+
" inertia=(0.001, 0.001, 0.0001),\n",
527+
" power_off_drag=0.5,\n",
528+
" power_on_drag=0.5,\n",
529+
" center_of_mass_without_motor=0.0,\n",
530+
")\n",
531+
"\n",
532+
"deployable_vehicle = MultiStageRocket(stages=[carrier])\n",
533+
"deployable_vehicle.add_deployable(\n",
534+
" name=\"payload\",\n",
535+
" mass=1.0,\n",
536+
" inertia=(0.001, 0.001, 0.0001),\n",
537+
" position=1.0,\n",
538+
" free_rocket=payload_rocket,\n",
539+
" ejection=\"apogee\",\n",
540+
")"
541+
]
542+
},
543+
{
544+
"cell_type": "code",
545+
"execution_count": null,
546+
"metadata": {
547+
"execution": {
548+
"iopub.execute_input": "2026-08-14T09:43:43.124953Z",
549+
"iopub.status.busy": "2026-08-14T09:43:43.124823Z",
550+
"iopub.status.idle": "2026-08-14T09:43:43.148440Z",
551+
"shell.execute_reply": "2026-08-14T09:43:43.148160Z"
552+
}
553+
},
554+
"outputs": [],
555+
"source": [
556+
"deployable_mission = Mission(\n",
557+
" vehicle=deployable_vehicle,\n",
558+
" environment=Environment(),\n",
559+
" rail_length=1.0,\n",
560+
" inclination=90,\n",
561+
" heading=0,\n",
562+
" max_time=20,\n",
563+
")\n",
564+
"\n",
565+
"print(f\"bodies flown: {list(deployable_mission.flights.keys())}\")\n",
566+
"print(\n",
567+
" \"'carrier' flights (with payload, then alone): \"\n",
568+
" f\"{len(deployable_mission.flights['carrier'])}\"\n",
569+
")\n",
570+
"print(\n",
571+
" \"'payload' flights (with carrier, then alone): \"\n",
572+
" f\"{len(deployable_mission.flights['payload'])}\"\n",
573+
")\n",
574+
"print(\"\\ntimeline:\")\n",
575+
"for time, event in deployable_mission.timeline:\n",
576+
" print(f\" t={time:8.3f}s {event}\")"
577+
]
578+
},
481579
{
482580
"cell_type": "markdown",
483581
"metadata": {},
484582
"source": [
485583
"## Not built yet\n",
486584
"\n",
487-
"Deliberately out of scope for the commits so far (see the roadmap in `mission_multistage_design.md`):\n",
585+
"Deliberately out of scope, not part of the original roadmap's 8 commits (see `mission_multistage_design.md`):\n",
488586
"\n",
587+
"- More than two stages, or a deployable combined with a multi-stage vehicle\n",
489588
"- Deriving stack positions from `interstage_lengths` and each stage's physical extent (stages are currently assumed to already share one coordinate frame)\n",
490-
"- `Mission` for more than two stages\n",
491-
"- `Mission` deployable ejection (apogee-triggered, via `terminate_on_apogee`)\n",
492-
"- Deterministic time-based separation (motor burnout + delay) and apogee-triggered deployable ejection\n",
493-
"- `StochasticMission`\n",
494-
"\n",
495-
"Each lands as its own commit with its own tests; this notebook grows alongside them."
589+
"- Event-triggered separation/ignition/ejection beyond deterministic delays and `\"apogee\"` (no generic mid-flight trigger/event solver exists in Flight today)\n",
590+
"- Deployables built from `add_surface()` instead of `free_rocket`\n",
591+
"- `StochasticMission`, `to_dict`/`from_dict`, `draw()`/`info()`\n"
496592
]
497593
}
498594
],

0 commit comments

Comments
 (0)