The mission
The mission is what the aircraft is sized against. It is owned by the
Performance discipline, as the same pair OpenConcept’s
own run scripts write by hand: the InitialConditions – everything
set_values(prob, num_nodes) writes into the problem – and the
ContinuationLadder that makes the hard ones reachable.
What is flown
The shipped case flies a 2800 nmi design mission at FL350, followed by a 200 nmi diversion at FL150 and a 30-minute loiter:
initial_conditions:
mission_range: {value: 2800, units: nmi}
cruise|h0: {value: 35000, units: ft}
reserve_range: {value: 200, units: nmi}
reserve|h0: {value: 15000, units: ft}
loiter|h0, loiter_duration and takeoff|h are not set, so they keep the box’s own
defaults: 1500 ft, 30 minutes and sea level. That is a deliberate choice about which numbers the
case owns; setting them is a one-line addition.
Names are written as the box publishes them, and resolved first as written and then under the
case’s mission_path – so cruise|h0 and mission.cruise|h0 both work, and a name that
resolves neither way is an error naming both attempts rather than a silently ignored line.
The schedules
Each steady-flight phase is flown along an equivalent-airspeed and vertical-speed schedule,
written under the phase’s own name exactly as set_values writes it:
initial_conditions:
climb.fltcond|Ueas: {value: [230, 252], units: kn}
climb.fltcond|vs: {value: [2300, 400], units: ft/min}
cruise.fltcond|Ueas: {value: 252, units: kn}
cruise.fltcond|vs: {value: 0, units: ft/min}
descent.fltcond|Ueas: {value: [252, 250], units: kn}
descent.fltcond|vs: {value: [-1300, -800], units: ft/min}
The shipped case schedules all seven steady phases – climb, cruise, descent, the three reserve phases and loiter – and both quantities in each. Nothing forces that, and nothing can: to the box these are ordinary variables carrying their own declared defaults. What an unscheduled phase flies is whatever placeholder its component declared, which is a different mission than the one the case appears to ask for. Schedule them all.
A value may be one number, two numbers to interpolate between across the phase (exactly as
np.linspace does in OpenConcept’s own run script), or exactly as many numbers as the
variable’s shape. Any other length is an error rather than being broadcast, because broadcasting
would quietly fly a different mission.
The three ground-roll phases – v0v1, v1vr, v1v0 – have no schedule. They integrate
acceleration from a standstill, so what they need is a starting guess for true airspeed:
initial_conditions:
v0v1.fltcond|Utrue: {value: 100, units: kn}
v1vr.fltcond|Utrue: {value: 100, units: kn}
v1v0.fltcond|Utrue: {value: 100, units: kn}
ac|weights|MTOW: {value: 50.0e3, units: kg}
Those four are seeds, not designs. Maximum takeoff weight is an output – it is what the weight closure solves for – and writing a value onto it before the first solve chooses where Newton starts, not what it converges to. They are re-applied before every continuation rung and before the design run, which is not cosmetic: it is why the box converges at every tolerance probed down to 1e-12 on every grid, where writing them once left some grids stalling near 9e-9. See Verification.
Continuation, and why it is part of the interface
The black box is a coupled implicit system. Phase durations are solved against altitude and range targets; throttle against zero acceleration; the decision speed V1 so that the continue and abort distances match; and above all of it, maximum takeoff weight against the fuel burned. A Newton solver started cold on a 2800 nmi mission at FL350 does not generally reach it.
OpenConcept’s own sizing example handles this by converging an easy mission first and stepping
up – written as a sequence of bare assignments in a run script, between two run_model
calls. cdadt does the same thing, as data:
continuation:
- description: short low-altitude mission, gentle descent
initial_conditions:
mission_range: {value: 500, units: nmi}
cruise|h0: {value: 5000, units: ft}
reserve_range: {value: 100, units: nmi}
reserve|h0: {value: 1000, units: ft}
descent.fltcond|vs: {value: -800, units: ft/min}
- description: design range and altitude, still with the gentle descent
initial_conditions:
descent.fltcond|vs: {value: -800, units: ft/min}
A rung takes description and initial_conditions, nothing else. Each is written on top of
the case’s own initial conditions – which are re-applied underneath it every time – and
converged, so the solver enters the next rung from a converged neighbour. The last rung is
followed by the design mission itself. The shipped ladder is two: first shrink the mission to
500 nmi at 5000 ft and soften the descent, then restore the range and altitude but keep the
gentle descent, and finally apply the real descent schedule.
Making this data rather than statements has three consequences worth the trouble. The ladder travels with the mission it converges, so a case file that is copied and edited keeps working. It can be inspected and changed per study without touching code. And it is the same object in a sizing run and in the baseline of an optimization, so the two cannot silently diverge.
It also must not change the answer. The ladder is a path to the solution, not part of it, and
tests.test_validation is what proves the path taken here arrives where OpenConcept’s own
run script arrives – to 1e-6, with the reference executed live.
During an optimization
The ladder is walked once, to establish the baseline. Every later design the driver proposes starts from its predecessor’s converged state, which is both far cheaper and far more reliable than re-walking a ladder from a cold start at each iteration.
That is also why an optimizer will occasionally propose a design the solver cannot converge from where it currently is. The shipped optimization case uses IPOPT for exactly this reason; see Optimization.
Reading the mission back
Performance reports what flying it produced: block fuel
and fuel with reserves, the balanced field length and its abort distance, V1 and
V2, the engine-out climb gradient, the range actually flown on the design mission and
on the diversion, the throttle history of every phase, and every phase duration.
mission_range_flown is worth checking on any run: the box solves the cruise duration to
reach the requested range, so a converged run flies exactly the mission that was asked for, and
a run where it does not has not converged.