Skip to content

Commit faf2132

Browse files
committed
ENH: extend flight_rocket to multiple active stages
Fourth slice of the multistage/mission architecture. flight_rocket() now composes any number of active stages, not just one: active_stages[0] is the currently firing stage (its motor becomes the composed Rocket's own motor); every stage above it is inert cargo, contributing its full current mass/inertia/CoM (structure + motor + unburned propellant, evaluated at that stage's own t=0) since its own motor clock hasn't started yet. Default stack drag (no stack_power_off_drag/on_drag override) is now an area-weighted sum of each active stage's own drag curve - the approach mission_multistage_design.md itself leans toward for _derive_stack_drag. This subsumes the single-stage case from the previous commit, so that code path is unchanged in behavior. Split flight_rocket() into _compose_mass_and_center_of_mass and _compose_inertia to keep it under pylint's statement-count limit, mirroring how Rocket itself splits mass/inertia composition into separate evaluate_* methods. Deviation from the design doc: _stack_position_of() (deriving each stage's axial position from interstage_lengths + physical extent) is not implemented. Every active stage's Rocket coordinate system is assumed to already share one common stack frame; positions are used as-is. Flagged in the notebook's "Not built yet" section as remaining work. Also updates docs/notebooks/multistage_mission.ipynb with a two-stage (booster + inert sustainer) demo, executed end-to-end and verified against the same hand-computed values used in test_multistage.py.
1 parent 44a0a3b commit faf2132

3 files changed

Lines changed: 329 additions & 71 deletions

File tree

‎docs/notebooks/multistage_mission.ipynb‎

Lines changed: 121 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,19 @@
1111
"**Landed so far:**\n",
1212
"1. `Stage` — wraps a single-stage `Rocket`\n",
1313
"2. `Deployable` — a carried payload released mid-flight\n",
14-
"3. `MultiStageRocket` — composes a stage + its deployables into one flight-ready `Rocket` (single active stage only, for now)\n"
14+
"3. `MultiStageRocket` — composes a stage + its deployables into one flight-ready `Rocket`\n",
15+
"4. `MultiStageRocket.flight_rocket` for more than one active stage (a booster with an inert sustainer riding on top)\n"
1516
]
1617
},
1718
{
1819
"cell_type": "code",
1920
"execution_count": null,
2021
"metadata": {
2122
"execution": {
22-
"iopub.execute_input": "2026-08-14T08:52:32.634504Z",
23-
"iopub.status.busy": "2026-08-14T08:52:32.634292Z",
24-
"iopub.status.idle": "2026-08-14T08:52:33.989471Z",
25-
"shell.execute_reply": "2026-08-14T08:52:33.988977Z"
23+
"iopub.execute_input": "2026-08-14T09:07:27.168733Z",
24+
"iopub.status.busy": "2026-08-14T09:07:27.168494Z",
25+
"iopub.status.idle": "2026-08-14T09:07:28.612708Z",
26+
"shell.execute_reply": "2026-08-14T09:07:28.612298Z"
2627
}
2728
},
2829
"outputs": [],
@@ -45,10 +46,10 @@
4546
"execution_count": null,
4647
"metadata": {
4748
"execution": {
48-
"iopub.execute_input": "2026-08-14T08:52:33.991773Z",
49-
"iopub.status.busy": "2026-08-14T08:52:33.991508Z",
50-
"iopub.status.idle": "2026-08-14T08:52:34.015411Z",
51-
"shell.execute_reply": "2026-08-14T08:52:34.015108Z"
49+
"iopub.execute_input": "2026-08-14T09:07:28.615144Z",
50+
"iopub.status.busy": "2026-08-14T09:07:28.614900Z",
51+
"iopub.status.idle": "2026-08-14T09:07:28.639194Z",
52+
"shell.execute_reply": "2026-08-14T09:07:28.638875Z"
5253
}
5354
},
5455
"outputs": [],
@@ -101,10 +102,10 @@
101102
"execution_count": null,
102103
"metadata": {
103104
"execution": {
104-
"iopub.execute_input": "2026-08-14T08:52:34.043634Z",
105-
"iopub.status.busy": "2026-08-14T08:52:34.043428Z",
106-
"iopub.status.idle": "2026-08-14T08:52:34.046144Z",
107-
"shell.execute_reply": "2026-08-14T08:52:34.045821Z"
105+
"iopub.execute_input": "2026-08-14T09:07:28.666448Z",
106+
"iopub.status.busy": "2026-08-14T09:07:28.666250Z",
107+
"iopub.status.idle": "2026-08-14T09:07:28.668832Z",
108+
"shell.execute_reply": "2026-08-14T09:07:28.668508Z"
108109
}
109110
},
110111
"outputs": [],
@@ -132,10 +133,10 @@
132133
"execution_count": null,
133134
"metadata": {
134135
"execution": {
135-
"iopub.execute_input": "2026-08-14T08:52:34.047635Z",
136-
"iopub.status.busy": "2026-08-14T08:52:34.047511Z",
137-
"iopub.status.idle": "2026-08-14T08:52:34.049736Z",
138-
"shell.execute_reply": "2026-08-14T08:52:34.049445Z"
136+
"iopub.execute_input": "2026-08-14T09:07:28.670308Z",
137+
"iopub.status.busy": "2026-08-14T09:07:28.670180Z",
138+
"iopub.status.idle": "2026-08-14T09:07:28.672458Z",
139+
"shell.execute_reply": "2026-08-14T09:07:28.672176Z"
139140
}
140141
},
141142
"outputs": [],
@@ -158,10 +159,10 @@
158159
"execution_count": null,
159160
"metadata": {
160161
"execution": {
161-
"iopub.execute_input": "2026-08-14T08:52:34.051054Z",
162-
"iopub.status.busy": "2026-08-14T08:52:34.050931Z",
163-
"iopub.status.idle": "2026-08-14T08:52:34.054057Z",
164-
"shell.execute_reply": "2026-08-14T08:52:34.053793Z"
162+
"iopub.execute_input": "2026-08-14T09:07:28.673894Z",
163+
"iopub.status.busy": "2026-08-14T09:07:28.673778Z",
164+
"iopub.status.idle": "2026-08-14T09:07:28.676873Z",
165+
"shell.execute_reply": "2026-08-14T09:07:28.676536Z"
165166
}
166167
},
167168
"outputs": [],
@@ -182,10 +183,10 @@
182183
"execution_count": null,
183184
"metadata": {
184185
"execution": {
185-
"iopub.execute_input": "2026-08-14T08:52:34.055368Z",
186-
"iopub.status.busy": "2026-08-14T08:52:34.055256Z",
187-
"iopub.status.idle": "2026-08-14T08:52:34.057211Z",
188-
"shell.execute_reply": "2026-08-14T08:52:34.056893Z"
186+
"iopub.execute_input": "2026-08-14T09:07:28.678288Z",
187+
"iopub.status.busy": "2026-08-14T09:07:28.678173Z",
188+
"iopub.status.idle": "2026-08-14T09:07:28.680214Z",
189+
"shell.execute_reply": "2026-08-14T09:07:28.679876Z"
189190
}
190191
},
191192
"outputs": [],
@@ -209,10 +210,10 @@
209210
"execution_count": null,
210211
"metadata": {
211212
"execution": {
212-
"iopub.execute_input": "2026-08-14T08:52:34.058606Z",
213-
"iopub.status.busy": "2026-08-14T08:52:34.058510Z",
214-
"iopub.status.idle": "2026-08-14T08:52:34.066016Z",
215-
"shell.execute_reply": "2026-08-14T08:52:34.065749Z"
213+
"iopub.execute_input": "2026-08-14T09:07:28.681606Z",
214+
"iopub.status.busy": "2026-08-14T09:07:28.681507Z",
215+
"iopub.status.idle": "2026-08-14T09:07:28.689190Z",
216+
"shell.execute_reply": "2026-08-14T09:07:28.688895Z"
216217
}
217218
},
218219
"outputs": [],
@@ -245,6 +246,96 @@
245246
") < 1e-9"
246247
]
247248
},
249+
{
250+
"cell_type": "markdown",
251+
"metadata": {},
252+
"source": [
253+
"## Multiple active stages: a booster carrying an inert sustainer\n",
254+
"\n",
255+
"When more than one stage is still attached, `flight_rocket()` treats `active_stages[0]` as the currently firing stage (its motor becomes the composed `Rocket`'s own motor) and every other active stage as inert cargo riding along - contributing its *full* current mass (structure + motor + unburned propellant), since its own motor clock hasn't started yet.\n",
256+
"\n",
257+
"Each stage's own `Rocket` coordinate system is assumed to already be expressed in a shared stack frame - positions are used as-is. Deriving stack positions automatically from `interstage_lengths` and each stage's physical extent isn't implemented yet."
258+
]
259+
},
260+
{
261+
"cell_type": "code",
262+
"execution_count": null,
263+
"metadata": {
264+
"execution": {
265+
"iopub.execute_input": "2026-08-14T09:07:28.690554Z",
266+
"iopub.status.busy": "2026-08-14T09:07:28.690454Z",
267+
"iopub.status.idle": "2026-08-14T09:07:28.708806Z",
268+
"shell.execute_reply": "2026-08-14T09:07:28.708458Z"
269+
}
270+
},
271+
"outputs": [],
272+
"source": [
273+
"from rocketpy.motors.point_mass_motor import PointMassMotor\n",
274+
"\n",
275+
"booster_rocket = Rocket(\n",
276+
" radius=0.1,\n",
277+
" mass=10.0,\n",
278+
" inertia=(1.0, 1.0, 0.01),\n",
279+
" power_off_drag=0.5,\n",
280+
" power_on_drag=0.6,\n",
281+
" center_of_mass_without_motor=0.0,\n",
282+
")\n",
283+
"booster_rocket.add_motor(\n",
284+
" PointMassMotor(\n",
285+
" thrust_source=100, dry_mass=1.0, propellant_initial_mass=2.0, burn_time=1.0\n",
286+
" ),\n",
287+
" position=0.0,\n",
288+
")\n",
289+
"two_stage_booster = Stage(name=\"booster\", rocket=booster_rocket)\n",
290+
"\n",
291+
"sustainer_rocket = Rocket(\n",
292+
" radius=0.08,\n",
293+
" mass=5.0,\n",
294+
" inertia=(0.5, 0.5, 0.005),\n",
295+
" power_off_drag=0.3,\n",
296+
" power_on_drag=0.4,\n",
297+
" center_of_mass_without_motor=2.0,\n",
298+
")\n",
299+
"sustainer_rocket.add_motor(\n",
300+
" PointMassMotor(\n",
301+
" thrust_source=50, dry_mass=0.5, propellant_initial_mass=1.0, burn_time=1.0\n",
302+
" ),\n",
303+
" position=2.0,\n",
304+
")\n",
305+
"sustainer = Stage(name=\"sustainer\", rocket=sustainer_rocket)\n",
306+
"\n",
307+
"two_stage_vehicle = MultiStageRocket(stages=[two_stage_booster, sustainer])"
308+
]
309+
},
310+
{
311+
"cell_type": "code",
312+
"execution_count": null,
313+
"metadata": {
314+
"execution": {
315+
"iopub.execute_input": "2026-08-14T09:07:28.710231Z",
316+
"iopub.status.busy": "2026-08-14T09:07:28.710113Z",
317+
"iopub.status.idle": "2026-08-14T09:07:28.725482Z",
318+
"shell.execute_reply": "2026-08-14T09:07:28.725174Z"
319+
}
320+
},
321+
"outputs": [],
322+
"source": [
323+
"stacked = two_stage_vehicle.flight_rocket(\n",
324+
" active_stages=(two_stage_booster, sustainer)\n",
325+
")\n",
326+
"print(f\"stacked.mass = {stacked.mass} kg (booster structure + full inert sustainer)\")\n",
327+
"print(f\"stacked.center_of_mass_without_motor = {stacked.center_of_mass_without_motor:.4f} m\")\n",
328+
"print(f\"stacked.motor is booster_rocket.motor -> {stacked.motor is booster_rocket.motor}\")\n",
329+
"\n",
330+
"after_separation = two_stage_vehicle.flight_rocket(active_stages=(sustainer,))\n",
331+
"print(f\"\\nafter_separation.mass = {after_separation.mass} kg (sustainer structure only)\")\n",
332+
"print(f\"after_separation.motor is sustainer_rocket.motor -> \"\n",
333+
" f\"{after_separation.motor is sustainer_rocket.motor}\")\n",
334+
"\n",
335+
"assert stacked.mass == 10.0 + (5.0 + 0.5 + 1.0)\n",
336+
"assert after_separation.mass == 5.0"
337+
]
338+
},
248339
{
249340
"cell_type": "markdown",
250341
"metadata": {},
@@ -253,7 +344,7 @@
253344
"\n",
254345
"Deliberately out of scope for the commits so far (see the roadmap in `mission_multistage_design.md`):\n",
255346
"\n",
256-
"- `flight_rocket()` for more than one active stage (booster + sustainer composed together, surfaces repositioned into stack coordinates)\n",
347+
"- Deriving stack positions from `interstage_lengths` and each stage's physical extent (stages are currently assumed to already share one coordinate frame)\n",
257348
"- `Mission` — the orchestrator that runs one `Flight` per vehicle configuration with state handoff between them\n",
258349
"- Deterministic time-based separation (motor burnout + delay) and apogee-triggered deployable ejection\n",
259350
"- `StochasticMission`\n",

0 commit comments

Comments
 (0)