The four lessons in this module steered through a complete journey from a physical model to a numerical result that can support an engineering judgment. Writing code was one necessary but not sufficient component. You reach a trustworthy computation over a thoughtful build-up of technical steps.
So far you have:
derived a model based on ordinary differential equations,
reconstructed time-stepping methods, and explored their order of accuracy
located events that fall between stored times,
studied convergence with numerical experiments, and
compared methods at a required accuracy.
You also practiced specifying tasks for an AI agent and auditing the code it proposed. This is a key new technical skill in the era of agentic AI, and like any other, it requires practice. That is the purpose of this exercise.
This capstone brings all these ideas together in a new setting: the vertical flight of a small rocket. The physical model involves gravity, aerodynamic drag, changing propellant mass, and known transitions from powered flight to coasting flight, reaching apogee, and falling back to hit the ground.
You will first do a fixed-step RK2 calculation and then compare it with an adaptive solver from the SciPy library. Neither result is authoritative simply because the code runs or comes from a standard library, or produces correct-looking results. Your task as always is to decide what evidence makes the reported flight quantities trustworthy.
Engineering brief¶
A simulation team needs reference results for an idealized vertical-flight model. These results will be used to check later implementations of the same mathematical model. The quantities of interest are the rocket’s maximum speed, apogee, and impact conditions.
The engineering question is:
Can we make accurate predictions of the rocket’s apogee and impact conditions to the required numerical accuracy, using a fixed-step RK2 calculation and/or a properly configured adaptive SciPy solver?
These are the accuracy targets:
maximum speed to within ;
apogee altitude to within ;
apogee time to within ;
impact time to within ; and
impact speed to within .
The targets apply to numerical error within the stated model. They do not claim that this simplified model predicts a real rocket to the same accuracy.
Your final verdict will identify the evidence supporting an RK2 step size and a SciPy tolerance configuration, explain how burnout and the flight events were handled, and separate numerical uncertainty from limitations of the model. Code generation may be assisted, but you remain responsible for recording the specification of delegated work, auditing the result, and deciding what the evidence supports.
The vertical-flight problem¶
We model the rocket as a point mass constrained to move vertically. Altitude is measured upward from the launch point, and velocity is positive during ascent and negative during descent. The rocket launches from rest at ground level with of propellant. Here are all the problem settings:
| Symbol | Description | Value |
|---|---|---|
| dry mass of the rocket shell | ||
| initial propellant mass | ||
| gravitational acceleration | ||
| air density | ||
| rocket radius | ||
| reference area | ||
| drag coefficient | 0.15 | |
| effective exhaust speed relative to the rocket | ||
| powered-flight propellant burn rate |
The idealized model assumes constant gravity, air density, drag coefficient, reference area, exhaust speed, and burn rate during powered flight. It neglects wind, lateral motion, atmospheric variation, rocket attitude, and any change in aerodynamic properties. The engine switches off when the propellant is exhausted.
Propellant burn rate¶
A positive burn rate removes propellant mass from the rocket, thus . With the stated initial mass and constant powered-flight burn rate, burnout occurs at
The prescribed mass-flow history is a step function:
The value changes discontinuously at the known burnout time, . You should treat burnout as an explicit boundary between two integrations, rather than allowing one numerical step to cross it unnoticed.
Let’s begin by loading the numerical Python libraries, and setting up the problem. Then make a plot of the burn rate.
import numpy as np
import matplotlib.pyplot as plt# Model parameters:
m_s = 50.0 # dry mass (kg)
m_p0 = 100.0 # initial propellant mass (kg)
g = 9.81 # gravitational acceleration (m/s**2)
rho = 1.091 # air density (kg/m**3)
r = 0.5 # rocket radius (m)
A = np.pi * r**2 # reference area (m**2)
C_D = 0.15 # drag coefficient
v_e = 325.0 # effective exhaust speed (m/s)
mu_0 = 20.0 # powered-flight burn rate (kg/s)
t_burn = m_p0 / mu_0
def mass_flow_rate(t):
'''Return the positive propellant burn rate at time t.'''
t = np.asarray(t)
return np.where((t >= 0.0) & (t < t_burn), mu_0, 0.0)t_plot = np.linspace(0.0, 10.0, 201)
fig, ax = plt.subplots(figsize=(5.0, 3.0))
ax.step(t_plot, mass_flow_rate(t_plot), where='post')
ax.axvline(t_burn, color='tab:blue', linestyle='--', label='burnout')
ax.set_xlabel('Time, $t$ (s)')
ax.set_ylabel(r'Burn rate, $\mu$ (kg/s)')
ax.set_xlim(0.0, 10.0)
ax.set_ylim(-1.0, 25.0)
ax.grid()
ax.legend()
fig.tight_layout()Integrating Equation 2 gives the exact remaining propellant mass:
This exact history will provide a simple but important check of the numerical state.
Derivation of the equations of motion¶
On paper — reproduce the model derivation
Follow the derivation below with upward as the positive direction. Reproduce the mass balance, draw and label the forces on the rocket, and write the final three-equation initial-value problem in your paper record. Mark the sign of every force and write the units of each term in Newton’s second law.
Keep this derivation beside your computational notebook. During review of your completed capstone, you should be ready to explain any step or sign choice.
Mass balance¶
The rocket’s instantaneous mass is the sum of its constant dry mass and remaining propellant mass:
During a short interval , a positive mass leaves the rocket. Consequently,
Using a separate symbol for the positive outflow rate avoids a common sign ambiguity: is positive while propellant is burning, whereas is negative.
Newton’s second law and thrust¶
The engine expels propellant downward at the positive rate and effective exhaust speed relative to the rocket. In this model, the corresponding upward thrust is
Once the exhaust momentum flux is represented by the thrust, Newton’s second law for the remaining rocket is simply
where is the sum of gravity and aerodynamic drag. This is an effective exhaust-speed model and other engine-flow details are ignored.
Why is the thrust ?
During a short interval , a positive mass leaves the rocket. The rocket begins with mass and velocity ; it ends with mass and velocity . To first order, the expelled mass has velocity in the stationary reference frame. A momentum balance gives
After expanding, canceling equal terms, and neglecting the second-order product ,
The last term is the upward momentum delivered to the rocket per unit time, which is the thrust.
Gravity and aerodynamic drag¶
Gravity acts downward with force . Aerodynamic drag has magnitude and always opposes the motion. In one signed velocity component, both directions are represented by
When , the drag force is negative and opposes ascent. When , , so the drag force is positive and opposes descent. Replacing by would silently give the wrong direction during descent.
The total external force is therefore
The initial-value problem¶
Substituting Equation 6 and Equation 11 into Equation 7, using , and adding the altitude and propellant equations gives
The three initial conditions are
In vector form, with , the model has the non-autonomous form . The explicit appearance of time through means that the RK2 step used later must evaluate its second stage at both the midpoint state and the midpoint time.
Flight regimes and reported events¶
The solution passes through three physical regimes and three transitions:
| Regime or transition | Numerical condition | What changes or is recorded |
|---|---|---|
| Powered ascent | ; mass decreases and thrust acts | |
| Burnout | known breakpoint | set , record the state, and continue with |
| Coasting ascent | and | mass is constant; gravity and drag slow the rocket |
| Apogee | first crossing from to | interpolate the time and altitude where |
| Descent | and | gravity acts downward and drag acts upward |
| Impact | first downward crossing from to | interpolate the time and velocity where |
Burnout is a breakpoint known in advance, while apogee and impact are events discovered from the computed solution. Impact must mean the downward ground crossing after the rocket has been aloft; the initial condition is the launch point, not an immediate impact.
Predictions and checks before computing¶
A numerical trajectory should be judged against expectations that do not come from the same computation. Three useful checks follow directly from the model.
Exact propellant history. Use Equation 3 to calculate the remaining propellant at and confirm that it reaches exactly zero at burnout.
No-drag burnout-speed bound. If drag is removed from the model during powered flight, integrating the velocity equation gives
During ascent, drag can only reduce the velocity from this ideal value. Evaluate Equation 14 at burnout and use it as an upper bound for the powered-flight speed.
Empty-shell terminal speed. During descent after burnout, the mass is and a steady downward velocity satisfies . Its magnitude is
The rocket begins its descent from rest at apogee, so its impact-speed magnitude should approach this value from below.
Finally, every term in Equation 7 has units of force: , , and each have units . Take notice of dimensional agreement and any sign conventions.
On paper — record independent expectations
Before beginning the numerical calculation, your paper record should contain:
the mass balance, thrust relation, and Newton’s-law model;
the initial-value problem with the state order and sign convention;
a hand-drawn diagram of the burn, coast, descent, and impact sequence;
the remaining propellant at ;
the no-drag burnout-speed bound and empty-shell terminal speed; and
your predicted sign for every force term during ascent and descent.
These expectations become evidence when you later audit the RK2 and SciPy calculations.
Build a time-aware RK2 flight solver¶
The numerical calculation needs three distinct layers:
| Layer | Responsibility | Question it answers |
|---|---|---|
| Model right-hand side | translate the three differential equations into derivatives | What is the instantaneous rate of change? |
| RK2 step | advance the complete state from one time to the next | How is one numerical update formed? |
| Flight driver | repeat steps, stop at the known breakpoint, and locate events | How is a complete flight assembled and reported? |
Keeping these responsibilities separate makes each part easier to inspect and test.
The state equation depends explicitly on time because the mass-flow rate changes at burnout. The RK2 step will need to adopt this time dependence explicitly.
In your notebook — reconstruct the RK2 flight solver
Create a section with the same title in your working notebook. Reconstruct the four function-definition blocks below one at a time, in order. Before entering each block, write a one-sentence specification of its purpose, inputs, and output. Run the cell that defines the function, then compare your version line by line with the published version before continuing. After the definitions, reconstruct and run the diagnostic cells as instructed.
If you use an agent to help reproduce a block, save the instruction you gave it. Bound the request to that one named function and its stated interface, and require the agent to leave all other cells unchanged. You still need to audit every line against the equations and explain the result. A definition that happens to execute is not yet evidence that its logic is correct.
Compute the model’s derivatives¶
The right-hand-side function returns the derivatives in the same order as the state-vector components . Naming the three forces separately makes their signs inspectable before they are combined into the acceleration. The mass-flow function is passed as an argument so that the time dependence is an explicit dependency of the model.
def rocket_rhs(t, u, mass_flow, m_s, g, rho, A, C_D, v_e):
'''Return [dh/dt, dv/dt, dm_p/dt] for the rocket model.
Parameters
----------
t : float
Current time in seconds.
u : numpy.ndarray
Current state [altitude, velocity, propellant mass].
mass_flow : callable
Function returning the positive propellant burn rate at time t.
m_s, g, rho, A, C_D, v_e : float
Physical parameters of the rocket model.
Returns
-------
numpy.ndarray
Derivatives in the same order as u.
'''
h, v, m_p = u
total_mass = m_s + m_p
mu = float(mass_flow(t))
thrust_force = mu * v_e
weight_force = -total_mass * g
drag_force = -0.5 * rho * A * C_D * v * abs(v)
altitude_rate = v
acceleration = (thrust_force + weight_force + drag_force) / total_mass
propellant_rate = -mu
return np.array([altitude_rate, acceleration, propellant_rate])Self-check — explain the model implementation
You should be ready to answer these questions, if asked:
What state order does
rocket_rhs()require, and where is that order preserved?Why does
drag_forcehave the correct sign during both ascent and descent?Why is positive while
propellant_rateis negative?
Advance one time-aware midpoint step¶
Lesson 4 introduced explicit-midpoint RK2 for an autonomous system (meaning, time does not appear explicitly on the right-hand side). For , both the midpoint state and midpoint time must be used. Rewrite the RK2 algorithm in this textbook form:
The function below is still a single numerical step. It knows nothing about burnout, apogee, or impact. In particular, shortening a step to land exactly at burnout belongs in the driver; it is not part of the RK2 formula.
def rk2_step(t, u, f, dt, *args):
'''Return one explicit-midpoint RK2 step for u' = f(t, u).'''
slope_start = f(t, u, *args)
t_midpoint = t + 0.5 * dt
u_midpoint = u + 0.5 * dt * slope_start
slope_midpoint = f(t_midpoint, u_midpoint, *args)
return u + dt * slope_midpointSelf-check — explain one RK2 step
You should be ready to answer these questions, if asked:
At what time and state is each call to
f()evaluated?Why does the final update start from
u, rather than fromu_midpoint?Which line would be wrong if the model depended on time but the autonomous Lesson 4 step were used unchanged?
Interpolate a bracketed event¶
A step brackets an event when the monitored component is positive at the beginning and nonpositive at the end. The same linear interpolation used for touchdown in Lesson 4 estimates the event time and the complete state. This helper function assumes that the driver has already checked the bracket.
def interpolate_zero_crossing(t, u, t_next, u_next, component):
'''Linearly interpolate the state where one component reaches zero.'''
value = u[component]
value_next = u_next[component]
fraction = value / (value - value_next)
event_time = t + fraction * (t_next - t)
event_state = u + fraction * (u_next - u)
return event_time, event_stateDrive the calculation through burnout and flight events¶
The driver normally advances by the requested fixed step . It shortens a step only when necessary to end exactly at the known burnout time or at the time limit. This is breakpoint handling, not adaptive time stepping (whose goal is error control).
After each completed step, the driver looks for the first downward crossing of and then the first downward crossing of . It stores interpolated apogee and impact states and returns a status rather than assuming that impact must have occurred.
def integrate_rocket_rk2(u_0, dt, time_limit, t_burn, *rhs_args):
'''Integrate the rocket through burnout, apogee, and impact.'''
if dt <= 0.0:
raise ValueError('dt must be positive.')
if time_limit <= t_burn:
raise ValueError('time_limit must extend beyond burnout.')
t = 0.0
u = np.array(u_0, dtype=float)
times = [t]
states = [u.copy()]
steps = 0
status = 'time_limit'
burnout_state = None
apogee_time = None
apogee_state = None
impact_time = None
impact_state = None
while t < time_limit:
dt_step = min(dt, time_limit - t)
# End a powered-flight step exactly at the known breakpoint.
ending_at_burnout = t < t_burn <= t + dt_step
if ending_at_burnout:
dt_step = t_burn - t
u_next = rk2_step(t, u, rocket_rhs, dt_step, *rhs_args)
t_next = t + dt_step
steps += 1
if ending_at_burnout:
t_next = t_burn
# The exact mass balance gives zero propellant at burnout.
u_next[2] = 0.0
burnout_state = u_next.copy()
if not np.all(np.isfinite(u_next)) or u_next[2] < -1e-12:
status = 'invalid_state'
break
# Apogee is the first crossing from upward to downward velocity.
if apogee_time is None and u[1] > 0.0 and u_next[1] <= 0.0:
apogee_time, apogee_state = interpolate_zero_crossing(
t, u, t_next, u_next, component=1
)
# Requiring h > 0 at the start avoids treating launch as impact.
if u[0] > 0.0 and u_next[0] <= 0.0:
impact_time, impact_state = interpolate_zero_crossing(
t, u, t_next, u_next, component=0
)
times.append(impact_time)
states.append(impact_state.copy())
status = 'impact'
break
times.append(t_next)
states.append(u_next.copy())
t, u = t_next, u_next
times = np.array(times)
states = np.array(states)
speed_index = np.argmax(np.abs(states[:, 1]))
return {
'status': status,
'times': times,
'states': states,
'steps': steps,
'rhs_evaluations': 2 * steps,
'burnout_time': t_burn,
'burnout_state': burnout_state,
'apogee_time': apogee_time,
'apogee_state': apogee_state,
'impact_time': impact_time,
'impact_state': impact_state,
'maximum_speed': abs(states[speed_index, 1]),
'maximum_speed_time': times[speed_index],
}Self-check — explain the flight logic
You should be ready to answer these questions, if asked:
Why is burnout handled by ending a step at , rather than by stepping past it and clamping a negative remaining mass to zero?
What is the difference between the known burnout breakpoint and the discovered apogee and impact events?
Which condition prevents the initial state from being reported as impact?
What assumption is made when an event is interpolated between two RK2 states?
Make one provisional flight¶
Use for a first diagnostic run. This value deliberately does not divide the burn time, so the result exposes whether the driver lands on the breakpoint as intended. It is a provisional step, not a claim that the engineering accuracy targets have been met. A time limit comfortably allows the anticipated trajectory to reach the ground.
Before running the cell, predict the result status and the order of burnout, maximum speed, apogee, and impact.
u_0 = np.array([0.0, 0.0, m_p0])
dt_trial = 0.6 # deliberately does not divide the burnout time
time_limit = 60.0 # seconds
rhs_args = (mass_flow_rate, m_s, g, rho, A, C_D, v_e)
rk2_result = integrate_rocket_rk2(
u_0, dt_trial, time_limit, t_burn, *rhs_args
)
times = rk2_result['times']
states = rk2_result['states']
propellant_at_3_2 = np.interp(3.2, times, states[:, 2])
print(f'Status: {rk2_result["status"]}')
print(f'Completed steps: {rk2_result["steps"]}')
print(f'RHS evaluations: {rk2_result["rhs_evaluations"]}')
print(f'Propellant at 3.2 s: {propellant_at_3_2:.6f} kg')
if rk2_result['burnout_state'] is not None:
h_burn, v_burn, m_p_burn = rk2_result['burnout_state']
print(
f'Burnout: t = {rk2_result["burnout_time"]:.6f} s, '
f'h = {h_burn:.6f} m, v = {v_burn:.6f} m/s, '
f'm_p = {m_p_burn:.6f} kg'
)
print(
f'Maximum speed: {rk2_result["maximum_speed"]:.6f} m/s '
f'at t = {rk2_result["maximum_speed_time"]:.6f} s'
)
if rk2_result['apogee_state'] is not None:
print(
f'Apogee: t = {rk2_result["apogee_time"]:.6f} s, '
f'h = {rk2_result["apogee_state"][0]:.6f} m'
)
if rk2_result['impact_state'] is not None:
print(
f'Impact: t = {rk2_result["impact_time"]:.6f} s, '
f'v = {rk2_result["impact_state"][1]:.6f} m/s'
)Plot the three state components against time. Use the figure as a diagnostic: look for the expected linear decrease of propellant during the burn, continuity of altitude and velocity at burnout, one apogee, and a return to . A smooth-looking curve cannot establish numerical accuracy.
fig, axes = plt.subplots(3, 1, figsize=(5.0, 5.0), sharex=True)
axes[0].plot(times, states[:, 0])
axes[0].set_ylabel('Altitude, $h$ (m)')
axes[1].plot(times, states[:, 1])
axes[1].axhline(0.0, color='0.7', linewidth=1.0)
axes[1].set_ylabel('Velocity, $v$ (m/s)')
axes[2].plot(times, states[:, 2])
axes[2].set_ylabel('Propellant, $m_p$ (kg)')
axes[2].set_xlabel('Time, $t$ (s)')
for ax in axes:
ax.axvline(t_burn, color='tab:orange', linestyle='--')
ax.grid()
fig.tight_layout()Decide what the first run supports¶
Develop preliminary confidence by combining evidence that would fail in different ways:
Code-to-equation inspection: point from each returned derivative to the matching term in Equation 12, including its sign.
Exact mass check: compare the reported propellant at and at burnout with your paper values. Confirm that burnout occurs at exactly even though does not divide it.
Event logic: confirm that the status is
impact, the event order is physically sensible, the apogee state has , and the impact state has .Independent bounds: compare the maximum speed with the no-drag burnout-speed bound and the impact-speed magnitude with the empty-shell terminal speed from your paper record.
Trajectory inspection: use the plot to look for wrong signs, jumps, repeated events, or propellant used after burnout.
Record any discrepancy before changing code. Passing these checks means that the calculation has survived useful attempts to expose an obvious defect. It does not show that meets the acceptance targets, and agreement of printed digits from one run is not an accuracy argument. Keep this result unchanged as the baseline for the verification and refinement work that follows later.
Self-check — defend your confidence
You should be ready to answer these questions, if asked:
Choose two checks above and explain what different mistake each could reveal.
Which claims can you make from this one run, and which claims still require refinement or an independent solver?
Verify and refine RK2 with Richardson extrapolation¶
The provisional trajectory was physically plausible and passed several exact and qualitative checks. We now ask a narrower numerical question: how much discretization error remains in each reported quantity, and what time step is supported by the evidence?
Following the terminology introduced by Roache (1998) (and now standard), this is solution verification: estimating how accurately the differential equations have been solved for a particular problem setting. It is not validation of the rocket model against reality. Refining one RK2 implementation also cannot expose every shared coding or modeling error, which is why the independent checks already performed—and the different solver introduced later—remain necessary.
Estimate discretization error from a grid family¶
Let be one quantity of interest, such as apogee altitude, and let be its value computed with time step . In the asymptotic refinement regime, a method of order has an error model of the form
where is the exact value of the quantity for the stated continuous model and does not depend on . For a refinement ratio , compare a coarse result with a finer result . Eliminating the leading error term gives the Richardson-extrapolated value
The magnitude of the correction is an estimate of the discretization error in the finer result:
For midpoint RK2 with step halving, , , and the denominator is . The difference between two runs is therefore not itself the estimated error in the finer result; the leading-order estimate is one third of that difference.
The assumed order should also be tested. Three successive grids give the observed order
Values approaching 2 support the RK2 error model. Do not expect exact agreement on every triplet: coarse runs may not yet be asymptotic, while event interpolation and very small successive differences can make an observed-order estimate irregular. The absolute value makes the logarithm usable, but a reversal in the signs of successive differences is still a warning to investigate. Inspect a sequence rather than selecting one convenient value.
On paper — reconstruct the Richardson argument
Starting from Equation 17, write the leading-error expressions for and . Eliminate to obtain Equation 18, and explain why the correction for second-order RK2 is one third of the fine–coarse difference.
Then use three symbolic results, , , and , to reproduce Equation 20. Record the assumptions that make these estimates meaningful.
Plan the refinement study before generating code¶
Use the halving family
Because every integration stops explicitly at , the discontinuity is a common boundary for every grid. The refinement study will examine these targets:
| Quantity | Numerical-error target |
|---|---|
| Maximum speed | |
| Apogee altitude | |
| Apogee time | |
| Impact time | |
| Impact speed |
The maximum-speed target matches the impact-speed target and corresponds to about of the anticipated maximum speed. It is tight enough to discriminate a coarse result while remaining proportionate to the other requirements.
With an agent — drive solution verification
Use an agent to generate the repetitive experiment driver to perform numerical solution verification. This is a natural place for agent assistance, provided you own the verification design and verdict.
Before invoking an agent, record which quantity you predict will control the time-step choice. A supported candidate must have successful flights, Richardson estimates below every target, evidence that refinement is behaving consistently with second order, and at least one finer run that preserves the conclusion.
Record the agent task brief¶
You can use the brief below, copied into a Markdown cell in your working notebook, but feel free to improve it as you see fit. Above it, record the date and the agent or persona used.
Before attaching your working notebook, confirm that it contains the NumPy and Matplotlib imports, model parameters and mass-flow function, all four solver function definitions, and the setup cell defining u_0, time_limit, and rhs_args. Include these cells even if you have not copied the surrounding lesson narrative. The agent needs their definitions in the saved notebook; values that exist only in your running kernel are not available in the attachment.
Agent RK2-refinement task brief¶
Goal and scope— Your goal is to provide the driver code to perform solution verification of a vertical rocket-flight numerical solver. Add two readable, unexecuted Python cells immediately below this brief. Cell 1 runs the RK2 rocket calculation over the prescribed time-step family. Cell 2 computes and plots the Richardson quantities. Generate code that will report numerical evidence when I run it; leave the supported time-step choice and all conclusions to me.
Non-negotiables–
Before adding code, inspect the attached notebook for the NumPy (np) and Matplotlib (plt) imports; mass_flow_rate(), rocket_rhs(), rk2_step(), interpolate_zero_crossing(), and integrate_rocket_rk2(); and the model parameters and setup values u_0, time_limit, t_burn, rhs_args, m_p0, and mu_0. Confirm that the driver accepts integrate_rocket_rk2(u_0, dt, time_limit, t_burn, *rhs_args) and returns the fields needed below: status, times, states, steps, rhs_evaluations, maximum_speed, maximum_speed_time, apogee_time, apogee_state, impact_time, and impact_state. State components are ordered as altitude, velocity, and propellant mass; states has one row per stored time. If a required definition is missing or the interface differs, stop before adding cells and identify what I need to supply or clarify. Do not invent or reconstruct missing dependencies.
Reuse the existing functions and setup values exactly as defined. Do not redefine or modify the model, RK2 step, event interpolation, or flight driver.
Cell 1 must use
dt_values = [0.6 / 2**level for level in range(7)], in that order. Run one flight for each value and retain one dictionary per run in a list namedrk2_refinement_runs.Each run record must contain the time step, status, completed steps, RHS evaluations, maximum speed and its time, apogee altitude and time, impact time, positive impact-speed magnitude, and maximum absolute propellant error relative to at the stored times. If a run does not end with
status == 'impact', retain its status and work but useNonefor unavailable flight quantities.Print one readable row per run. Do not hide failed or unfinished runs.
Cell 2 must define these five quantities and targets: maximum speed,
0.1m/s; apogee altitude,1.0m; apogee time,0.02s; impact time,0.05s; and positive impact-speed magnitude,0.1m/s.A pair or triplet is usable only when every run has
status == 'impact'and the runs occupy consecutive entries in the originaldt_valuesfamily. Do not filter out unsuccessful runs and then compare across the resulting gaps.For each usable adjacent coarse–fine pair, let
Q_coarsebe the result at step2*handQ_finethe result at steph. With and , calculateQ_ext = Q_fine + (Q_fine - Q_coarse) / 3,estimated_error = abs(Q_fine - Q_coarse) / 3, andnormalized_error = estimated_error / target. Preserve the signed correction inQ_ext; the error estimate applies to the fine result.For every usable three-grid sequence, calculate the observed order as , using three consecutive results in coarse-to-fine order. If either required difference is zero, record
Noneinstead of dividing by zero. Attach the observed order from steps(4*h, 2*h, h)to the Richardson record for the pair(2*h, h). RecordNonewhen the required usable triplet is absent, including for the first pair.Retain one dictionary per quantity and grid comparison in a list named
richardson_records. Print the fine time step, fine result, extrapolated result, estimated error, normalized error, and observed order.Plot normalized Richardson error against the fine time step for all five quantities on logarithmic axes. Include a horizontal line at 1 for the acceptance target. Use the natural numerical ordering of the time-step axis: fine steps on the left and coarse steps on the right.
Use ordinary loops, dictionaries, NumPy, and Matplotlib. Keep units visible in printed headings or labels. Do not select a time step, label a run accepted, or write an engineering conclusion.
Allowed actions–
Read the attached notebook and add only the two requested cells. Do not run code, change existing cells, create or modify other files, access the network, install packages, import scipy or pandas, or replace the requested calculations with a new helper library.
Done when– Both cells are present without outputs or execution counts. Stop and report where they were inserted, what variables they create, and any ambiguity you encountered. Do not report numerical results.
Invoke the agent¶
Save your working notebook, attach it to a new agent conversation, and send only this request:
Read the Agent RK2-refinement task brief in the attached notebook and add the two requested code cells. Follow its allowed actions and return only the requested change record.
An agent may produce correct-looking formulas with the wrong grid orientation, compare the wrong rows, or silently redefine a function. Its completion report is a claim to inspect, not evidence that the study is correct.
Audit before running¶
Inspect the proposed experiment. Confirm that earlier cells are unchanged and the two new cells have no outputs. Then inspect the code against the brief:
Are the seven time steps generated by repeated halving, with the same initial state, model parameters, event logic, and time limit in every run?
Does every run remain visible, including a non-success status?
Are impact speed and both error measures treated as positive magnitudes, while the Richardson extrapolation retains the signed fine–coarse correction?
Is the fine-grid error estimate divided by ?
Does each observed-order calculation use three consecutive grids in the correct coarse-to-fine order?
Does the propellant comparison use the exact history at the stored times?
Does the plot divide each estimated error by the matching target, without comparing quantities that have different units directly?
Is there any new model, solver, import, file operation, execution command, hidden conclusion, or code you cannot explain?
If a line of code is too compact or dense to audit, ask the agent to rewrite only that cell with ordinary loops and intermediate names. Read the revision before running it. Do not use Run All.
Run the study and judge the evidence¶
Run Cell 1 first. Confirm that the row reproduces your saved provisional result, every status is impact, the work grows as expected under halving, and the exact propellant errors remain near floating-point roundoff. Investigate any discrepancy before continuing.
Run Cell 2. Choose one quantity and one three-grid sequence, then reproduce its two differences, observed order, extrapolated value, and error estimate with a calculator. Check these against the generated table before interpreting the whole study.
Look for the region where successive changes decrease and the observed orders tend toward 2. A candidate RK2 step is supported when all five estimated errors are below their targets and the next finer run gives the same outcome with smaller estimates. Prefer the coarsest step supported by that evidence; a smaller step costs more without improving the stated decision.
Do not conceal an irregular observed order. If an output is already changing by much less than its target but its observed order is unsettled, record both facts and retain the next finer run. Event-derived quantities can show a less regular sequence because the event falls at a different fraction of each step. The later comparison with another solver will provide complementary evidence.
In your notebook, record:
the RK2 step you provisionally support;
the controlling quantity and its Richardson estimate;
the observed-order evidence;
the supporting finer run;
the total RHS work for the selected run; and
any irregularity or limitation that remains.
This is the end of the RK2 verification and refinement stage. Do not yet treat agreement within this one implementation as an independent confirmation of the answer.
Self-check — defend the refinement decision
You should be ready to answer these questions, if asked:
Where does the factor in the RK2 error estimate come from?
Why must the observed order be inspected before relying on Richardson extrapolation?
Which quantity controls your selected time step, and what finer run supports it?
Why is this refinement study evidence of solution verification but not model validation or a fully independent code check?
Bridge to adaptive ODE integration in SciPy¶
Our RK2 solver uses a step size chosen before the run and repeats the calculation on finer grids to estimate its error. Scientific libraries also provide adaptive solvers that estimate local error while integrating and change their step size accordingly.
SciPy’s solve_ivp() documentation describes a common interface to several ODE methods. We will use RK45, an explicit Runge–Kutta method suitable as a starting point for this nonstiff model. A library implementation reduces the code we must write, but it does not choose meaningful tolerances, handle known discontinuities automatically, or decide whether the reported quantities are trustworthy.
The solve_ivp() contract¶
The solver call has this shape:
from scipy.integrate import solve_ivp
result = solve_ivp(
fun=right_hand_side,
t_span=(t_start, t_end),
y0=initial_state,
method='RK45',
rtol=relative_tolerance,
atol=absolute_tolerances,
events=event_functions,
dense_output=True,
max_step=maximum_step,
)The right-hand side model function must have the signature fun(t, y) and return derivatives with the same shape as y. The pair t_span=(t_start, t_end) defines the integration interval, and y0 is the complete state vector at its beginning. Extra model data can be supplied with args or captured in a small wrapper function.
SciPy’s ODE solvers can track events, which are the zeros of a continuous function of time and the state vector. The argument events expects a function name, or list of functions, with signature event(t, y) and additional arguments passed via args. The solver looks for the root of event(t, y(t)) = 0 in each time step, and returns the values of t and y corresponding to the event taking place.
What RK45 adapts¶
RK45 uses an embedded pair: formulas of two different orders estimate the local error, and the solver accepts, rejects, and resizes steps to satisfy a tolerance test. SciPy compares the estimated error in each state component with a scale of the form
The relative tolerance rtol is dimensionless. Because altitude, velocity, and propellant mass have different units and scales, a component-wise array such as atol=np.array([atol_h, atol_v, atol_mp]) is more interpretable than one absolute tolerance for all three components.
These are controls on estimated local state error, not direct guarantees on apogee or impact error. We must still tighten the tolerances and examine changes in the quantities of interest.
| Fixed-step RK2 | Adaptive RK45 |
|---|---|
dt sets every ordinary step | rtol and atol guide internally selected steps |
refinement changes dt | refinement tightens the tolerances |
| work is two RHS calls per completed step | result.nfev reports RHS calls |
Two options are easy to confuse. max_step places a ceiling on an internal step; it does not make the method fixed-step. t_eval requests times at which results are stored; it does not prescribe the solver’s internal steps. With dense_output=True, result.sol(t) evaluates the solver’s continuous interpolant at other times.
Events are roots; burnout is a boundary¶
A SciPy event function should return a scalar that defines the event when it becomes zero. These functions also have optional attributes that specify whether integration stops and which crossing direction counts. For example, we might assign:
def apogee_event(t, y):
return y[1] # velocity
apogee_event.direction = -1 # detect positive-to-negative crossings
apogee_event.terminal = False # record the event and continue
def impact_event(t, y):
return y[0] # altitude
impact_event.direction = -1
impact_event.terminal = TrueThe assignments after each function definition do not call the function. In Python, a function is an object, so it can carry attributes accessed with dot notation. solve_ivp() reads the direction and terminal attributes as configuration metadata when it receives the function through events.
The negative direction selects downward zero crossings. It distinguishes apogee from any upward crossing of and impact from the upward departure at . SciPy locates a detected root within an accepted step and returns event times and states in t_events and y_events. Because detection begins with a sign change over a step, multiple crossings within one step can be missed; max_step can provide an additional safeguard when the event behavior demands it.
The attribute terminal tells the solver whether to stop at the event. We record apogee and continue, but stop at impact.
Burnout is different: its time is known, and the right-hand side changes discontinuously there. The SciPy calculation should therefore use two calls—powered flight ending exactly at , followed by unpowered flight beginning from that state. Adaptive stepping is not a reason to integrate blindly across a known discontinuity.
Inspect the returned result¶
The value returned by solve_ivp() is an object that contains both solution data and diagnostic information in fields as follows:
| Field | What to inspect |
|---|---|
success, status, message | whether and why the solver stopped |
t | stored times selected by the solver or requested through t_eval |
y | state history with shape (number_of_states, number_of_times) |
t_events, y_events | detected event times and corresponding states |
nfev | number of right-hand-side evaluations |
sol | continuous interpolant, available when dense output is requested |
Notice that result.y stores state components by rows, whereas our RK2 driver’s states array stores them by columns. A successful status means that the numerical algorithm reached its requested endpoint or terminal event; it does not establish that the tolerances were adequate or that the physical model is valid.
The next stage will ask an agent to propose the two-stage (burn, coast/fall) SciPy implementation. Your task will be to audit it against this interface before running or trusting it.
Self-check — explain the SciPy interface
You should be ready to answer these questions, if asked:
What is the difference between
t_evalandmax_step?Why are separate absolute tolerances useful for , , and ?
Why should burnout separate two
solve_ivp()calls instead of being left for adaptive step-size control to cross?Which result fields would you inspect before believing an event time or computational-work count?
Agent-proposed SciPy implementation¶
You now know enough about solve_ivp() to inspect its interface, but not enough to be expected to design a complete two-stage implementation from scratch. An agent will propose that code under a detailed numerical specification. You will control its editing authority, inspect the proposal before execution, test the important interfaces, and decide what the results support.
First revisit Lesson 4’s agent task brief and pre-run audit. You will emulate the parts of that workflow that are already familiar:
record the date, agent or persona, and working-notebook filename;
authorize edits only in the requested location;
require unexecuted cells and a change record;
inspect every proposed line before running it; and
restrict any correction to the cell that needs it.
The SciPy-specific requirements are supplied below. You are not being asked to invent an unfamiliar library interface.
Preserve the accepted RK2 comparison¶
In your working notebook, add the following cell immediately before the agent brief. Replace the blank with the RK2 step supported by your Richardson study. This creates a fresh result with the standard driver interface, independent of any key names chosen by the earlier agent-generated table.
dt_rk2_accepted = _____
rk2_accepted = integrate_rocket_rk2(
u_0, dt_rk2_accepted, time_limit, t_burn, *rhs_args
)
assert rk2_accepted['status'] == 'impact'Run this cell and confirm that it reproduces the row you selected. The SciPy agent may read rk2_accepted but may not modify it.
Record the agent task brief¶
Copy the brief below into a Markdown cell in your working notebook. Fill in the notebook filename and place your provenance note above it, following the Lesson 4 pattern.
Agent SciPy-implementation task brief¶
Goal and scope
Add three readable, unexecuted Python cells immediately below this brief in [notebook filename]. Cell 1 defines a two-stage SciPy RK45 flight integrator. Cell 2 runs a prescribed tolerance study. Cell 3 compares successive SciPy results, the accepted RK2 result, and one maximum-step sensitivity run. Report evidence only; do not select a tolerance configuration or write a verdict.
Non-negotiables
Do not change any existing cell or redefine rocket_rhs(), rk2_step(), interpolate_zero_crossing(), or integrate_rocket_rk2().
Cell 1 — SciPy functions
Import only
solve_ivpfromscipy.integrate. Define readablepowered_rhs(t, y)andcoast_rhs(t, y)wrappers that call the existingrocket_rhs()with the existing parameters. The powered wrapper must use the constant ratemu_0throughout the powered phase, including its endpoint; the coast wrapper must use zero mass flow.Define
apogee_event(t, y)returning velocity andimpact_event(t, y)returning altitude. Both havedirection = -1; apogee is nonterminal and impact is terminal.Define
integrate_rocket_scipy(u_0, rtol, atol, max_step, time_limit). It must callsolve_ivp()withmethod='RK45',dense_output=True, not_eval, and the suppliedrtol, component-wiseatol, andmax_step.Use two solver calls. Powered flight integrates from
0.0tot_burnwithout event functions. Check itssuccessbefore continuing. Save its final propellant value asburnout_mass_residual, then copy the burnout state and set its propellant component exactly to zero from the known mass balance. Unpowered flight starts from that state att_burnand integrates totime_limitwith events ordered as(apogee_event, impact_event).Do not integrate across burnout with the piecewise
mass_flow_rate()function. Do not locate SciPy events with the RK2 linear-interpolation helper.Combine the two histories without duplicating the burnout point and return them in the RK2 convention:
timeshas shape(n_times,)andstateshas shape(n_times, 3). Inspect the event arrays rather than assuming events exist.Return a dictionary containing
status('impact','time_limit', or'solver_failure'),times,states, totalrhs_evaluationsfrom bothnfevvalues, both raw SciPy solution objects, burnout time and state,burnout_mass_residual, apogee time and state, impact time and state, maximum speed, and its time. Determine maximum speed from the combined stored states.
Cell 2 — tolerance study
Use
max_step = 1.0s for every tolerance run. Use these four configurations, in this order:rtol=1e-4,atol=np.array([1e-4, 1e-6, 1e-6]);rtol=1e-6,atol=np.array([1e-6, 1e-8, 1e-8]);rtol=1e-8,atol=np.array([1e-8, 1e-10, 1e-10]);rtol=1e-10,atol=np.array([1e-10, 1e-12, 1e-12]).
Run
integrate_rocket_scipy()once per configuration. Retain one dictionary per run inscipy_tolerance_runs, including configuration, status, five quantities of interest, burnout-mass residual, and RHS evaluations. The five quantities are maximum speed, apogee altitude, apogee time, impact time, and positive impact-speed magnitude. A run is usable for this complete-flight comparison only whenstatus == 'impact'; otherwise recordNonefor all five quantities. Keep every status visible.Print one readable row per run. Do not claim that a configuration is adequate.
Cell 3 — comparison evidence
Define targets of
0.1m/s for maximum speed,1.0m for apogee altitude,0.02s for apogee time,0.05s for impact time, and0.1m/s for impact speed.For every adjacent pair of usable SciPy runs (
status == 'impact'), compute the absolute change in each quantity and divide it by that quantity’s target. Compare every usable SciPy run with the matching quantity fromrk2_acceptedin the same way. Retain and print all records; do not turn these comparisons into pass/fail labels.Repeat the tightest tolerance configuration once with
max_step=0.5s and store it asscipy_max_step_check. Compare it with the tightestmax_step=1.0s result using the same quantities and targets.Plot normalized successive-tolerance changes against
rtolfor all five quantities on logarithmic axes, with a horizontal line at1. Keep the numericalrtolordering rather than reversing the axis.Do not use Richardson extrapolation on the tolerance sequence: tightening
rtolby a factor of 100 does not define a uniform grid-refinement ratio or a known output-error order.
Allowed actions
Read the attached notebook and add only the three requested cells. Do not run code, alter existing cells, create or modify other files, access the network, install packages, or write my interpretation.
Done when
All three cells are inserted without outputs or execution counts. Stop and report where they were added, which functions and variables they create, and any ambiguity encountered. Do not report numerical results.
Invoke the agent¶
Save and attach your working notebook as in Lesson 4. Send this short request:
Read the Agent SciPy-implementation task brief in the attached notebook and add the three requested code cells. Follow its allowed actions and return only the requested change record.
Do not ask the agent to run or debug the cells yet. First inspect what it proposed.
Audit the proposal before execution¶
Confirm that the agent changed only the requested location and that all three cells are unexecuted. Then trace the implementation against the brief:
Do the powered and coast wrappers use constant, phase-specific mass-flow rates, including at ?
Are there exactly two
solve_ivp()calls, with the second initialized from the first call’s final state after the exact propellant reset?Is the burnout residual saved before the reset rather than hidden by it?
Do both event functions have the correct component, direction, and terminal setting, and are they passed in the stated order?
Does the code use SciPy’s event results rather than the RK2 interpolation helper?
Are
rtol, component-wiseatol,max_step, anddense_outputpassed explicitly, with not_eval?Is the duplicate burnout column removed when histories are combined, and is
result.ytransposed into the RK2 state-history convention?Does a run enter the five-quantity comparisons only when
status == 'impact', withNonerecorded for every quantity otherwise?Are work, successive changes, RK2 differences, and the maximum-step check computed exactly as specified?
Has the agent avoided Richardson extrapolation on the tolerance sequence and avoided choosing a configuration?
If a line is too compact to explain, ask the agent to rewrite only the affected cell using intermediate variables and comments. If you find a defect, describe the violated requirement and authorize a correction only to that cell. Record the exchange and inspect the revision before execution.
Test the interfaces, then run the study¶
Run only Cell 1. Before starting a trajectory, call the two phase wrappers once at with the same physically valid state. Confirm that the powered wrapper returns while the coast wrapper returns . Evaluate both event functions on simple states and confirm that each returns the intended component. These focused checks can expose a phase or state-index error before it is buried in a full trajectory.
Next run Cell 2. Require successful powered and unpowered solves, one apogee, one terminal impact, event states close to their defining zeros, a small burnout-mass residual, and physically ordered events. Inspect message on either raw solution object if the status is not impact.
Finally run Cell 3 and reproduce at least one normalized change and one RK2 comparison from the printed values. Choose the loosest tolerance configuration supported by all of the following:
the next tighter configuration changes every quantity by less than its target;
agreement with the accepted RK2 result is consistent with both calculations’ estimated numerical uncertainty;
the tighter `max_step=0.5\ \mathrm{s}$ check does not change the conclusion; and
all solver statuses, events, and physical checks remain valid.
A smaller rtol is not automatically a better engineering choice if it increases nfev without affecting any required quantity. Conversely, agreement between two solvers is not persuasive if they share the same incorrect model translation or mishandle the same breakpoint.
Check the coast against an analytical solution¶
The RK2 and SciPy calculations are independent numerical implementations, but both translate the same model. We can add a different kind of evidence after burnout. During the upward coast, the propellant is gone, the mass is the constant , and . Define
so the velocity equation becomes
Let and be the altitude and velocity at burnout. Separating variables and integrating from to zero gives the coast time to apogee:
The speed is the empty-shell terminal-speed magnitude already found in Equation 15. Using gives the altitude gained during the coast:
These expressions are exact for the stated coast model. They are conditional on the supplied burnout state: they can expose a wrong coast equation, drag sign, phase switch, or apogee calculation, but they cannot reveal an error already present in or .
def exact_coast_apogee(burnout_state, burnout_time,
m_s, g, rho, A, C_D):
'''Return the exact coast time and altitude at apogee.'''
h_b, v_b, m_p_b = np.asarray(burnout_state, dtype=float)
if v_b <= 0.0:
raise ValueError('Burnout velocity must be positive.')
if not np.isclose(m_p_b, 0.0, atol=1e-12):
raise ValueError('The coast check requires zero propellant.')
k_drag = rho * A * C_D / (2.0 * m_s)
v_terminal = np.sqrt(g / k_drag)
coast_time = (
v_terminal / g * np.arctan(v_b / v_terminal)
)
altitude_gain = (
np.log(1.0 + (v_b / v_terminal)**2) / (2.0 * k_drag)
)
return {
'apogee_time': burnout_time + coast_time,
'apogee_altitude': h_b + altitude_gain,
'coast_time': coast_time,
'altitude_gain': altitude_gain,
}Reconstruct exact_coast_apogee() in your working notebook and map each expression to Equation 25 or Equation 26. Then identify the tolerance configuration you accepted in the preceding study and run it once more to retain its complete result:
selected_scipy_index = _____
selected_configuration = (
scipy_tolerance_runs[selected_scipy_index]['configuration']
)
scipy_accepted = integrate_rocket_scipy(
u_0,
selected_configuration['rtol'],
selected_configuration['atol'],
selected_configuration['max_step'],
time_limit,
)
assert scipy_accepted['status'] == 'impact'For both accepted calculations, use that method’s own burnout state as input to exact_coast_apogee(). Reconstruct and run this comparison cell:
coast_check_records = []
for method_name, numerical_result in (
('RK2', rk2_accepted),
('SciPy', scipy_accepted),
):
exact_result = exact_coast_apogee(
numerical_result['burnout_state'],
numerical_result['burnout_time'],
m_s, g, rho, A, C_D,
)
record = {
'method': method_name,
'apogee_time_difference': abs(
numerical_result['apogee_time']
- exact_result['apogee_time']
),
'apogee_altitude_difference': abs(
numerical_result['apogee_state'][0]
- exact_result['apogee_altitude']
),
}
coast_check_records.append(record)
print(record)Reproduce at least one difference from the printed quantities, then compare both records with the apogee-time and apogee-altitude acceptance targets.
A disagreement points specifically to the numerical treatment between burnout and apogee. Agreement does not verify the powered phase, so also compare the two methods’ burnout altitudes and velocities and keep the earlier powered-flight checks in your final chain of evidence.
Self-check — explain the analytical coast check
You should be ready to answer these questions, if asked:
Why is the quadratic-drag coast equation analytically solvable after burnout?
Why does the terminal-speed scale appear in an upward-coast calculation?
Which implementation defects could this comparison reveal?
Why can it pass even if the powered-flight calculation is wrong?
Write the capstone verdict¶
State whether the fixed-step RK2 and adaptive SciPy calculations support the requested predictions. Your verdict should include:
the selected RK2 step and its Richardson evidence;
the selected SciPy tolerances,
max_step, and supporting tolerance change;the two methods’ reported maximum speed, apogee, and impact conditions;
their differences relative to the numerical-error targets;
the analytical coast-to-apogee differences and what that check cannot test;
RHS evaluations for each selected calculation;
how burnout and events were handled and checked;
what agent-produced code you audited or corrected; and
which uncertainties belong to numerical solution and which remain limitations of the physical model.
Do not report more digits than the accepted numerical uncertainty supports.
Self-check — defend the SciPy evidence
You should be ready to answer these questions, if asked:
Which parts of the agent contract came from the familiar Lesson 4 workflow, and which parts were new SciPy requirements?
Why must the powered wrapper still return at the endpoint ?
What evidence supports your tolerance choice, beyond
result.success?Why is agreement between RK2 and RK45 complementary evidence rather than proof that the model is correct?
- Roache, P. J. (1998). Verification and Validation in Computational Science and Engineering. Hermosa Publishers.