The case file
A cdadt study is defined by one YAML file, not by a script. The file names the black box, every design variable, everything written into the box before it is converged, the ladder that converges it, and – for an optimization – the driver, the constraints and the objective.
The layout follows OpenConcept’s own B738 run scripts block for block, so that anyone who can
read B738.py can read a cdadt case:
Section |
What it is in OpenConcept’s own scripts |
|---|---|
|
|
|
|
|
The second half of |
|
The |
There is deliberately no separate “optimization” section. A variable becomes free for the driver
by gaining an optimize: entry where it is already declared, so a study that frees one more
variable differs from its sizing run by three lines, and no number is ever written twice.
Every section rejects keys it does not recognise. A silently ignored key in a configuration file means the run succeeds and answers a different question than the one that was asked.
Skeleton
black_box: # required
solver: # optional
mission_path: # optional; default "mission"
design_variables: # required
initial_conditions: # optional
continuation: # optional
driver: # optional
constraints: # optional
objective: # optional; present makes it an optimization study
black_box
Key |
Required |
Meaning |
|---|---|---|
|
yes |
The sizing analysis to drive, as |
|
yes |
Analysis points per mission phase. Must be odd: the box integrates fuel burn with Simpson’s rule, which needs 2N + 1 points. 21 for the shipped sizing cases; 11 for the optimizations, where the grid is traded against iteration count. |
|
no |
Passed to the analysis group’s constructor. What is accepted depends on which group
|
black_box.options, and choosing an aerodynamics
model decides what is driven; options configures it. OpenConcept’s own
B738SizingMissionAnalysis accepts nothing, so the shipped reference case gives none.
SizingMissionAnalysis – cdadt’s own group, which exists so that
the aerodynamics can be substituted at all – accepts two:
Option |
Default |
Meaning |
|---|---|---|
|
|
|
|
|
Add OpenConcept’s own Korn-equation transonic drag rise. The shipped lattice cases set it true, because the mission cruises at M 0.7854 and neither a vortex lattice nor OpenConcept’s parasite buildup carries a Mach term. Needs OpenAeroStruct. |
black_box:
model: cdadt.adapter.analysis:SizingMissionAnalysis
options:
aerodynamic_loads: cdadt.adapter.avl:OpenAVLLoads # openavl, far-field
# cdadt.adapter.oas:OpenAeroStructLoads
# cdadt.models.polar:PolarLoads # the default
wave_drag: true
num_nodes: 21
An unknown key here is refused, like every other unknown key in a case file – but the set of
known keys belongs to the group being driven rather than to cdadt, so naming a group that does not
accept aerodynamic_loads and passing one is an error the group raises, not the schema.
One consequence worth stating: ac|aero|polar|e stays in design_variables whichever model is
chosen, because the schema describes the aeroplane rather than the study. A lattice computes the
span efficiency and never reads that value, so under a lattice it is an unused declaration rather
than an input. The models say which of the box’s values they consume; see
build().
solver
Configures the Newton solve that closes the weight loop and the mission’s own balances together. All keys optional; the defaults are OpenConcept’s own sizing run script’s.
Key |
Default |
Meaning |
|---|---|---|
|
20 |
Newton iteration limit. The shipped optimization case raises it to 50: an optimizer visits designs a human would not. |
|
1e-9 |
Absolute and relative residual tolerances. |
|
-1 |
Solver print level. |
|
|
Whether a failed solve raises. Leave it true. A non-converged mission still produces numbers – a negative field length, a range that misses the one requested – and nothing about them says so. |
mission_path
Name of the mission subsystem inside the box. Default mission. It is what lets
initial_conditions write cruise|h0 where the box calls it mission.cruise|h0.
design_variables
One entry per variable the box publishes, keyed by the name it publishes it under:
design_variables:
ac|geom|wing|S_ref:
value: 124.6
units: m**2
source: b737.org.uk technical specifications
optimize: {lower: 90.0, upper: 180.0} # omit to hold it fixed
Key |
Required |
Meaning |
|---|---|---|
|
yes |
The number, in |
|
no |
OpenMDAO unit string ( |
|
no, but |
Where the number came from. Technically optional; in practice required, and the shipped cases have one for every parameter. A number with no provenance is indistinguishable from a guess in the report that quotes it. |
|
no |
Present frees the variable for the driver. Absent holds it at |
Every name here must be an independent variable of the box. Quantities the box computes –
ac|weights|OEW, ac|geom|hstab|S_ref, ac|aero|CLmax_TO – are outputs, and setting one
would be overwritten by the next solve. cdadt checks the whole list against the built model and
names every offender at once, with suggestions. cdadt inspect <case> lists what is available.
The optimize entry
Key |
Default |
Meaning |
|---|---|---|
|
– |
The interval the variable may move in. A scalar, or a list to bound a vector variable element by element. |
|
all |
Which elements of a vector variable are free. |
|
|
OpenMDAO driver scaling, passed through unchanged. |
The ref default is nearly always the right order of magnitude, and is the difference between
an optimizer that converges and one that stalls when a wing area in square metres shares a design
space with an aspect ratio.
Bounds are an engineering statement, not a numerical one: they should be the range over which the empirical correlations inside the box are defensible for the class of aircraft being designed, not the range over which the code happens to run.
initial_conditions
Everything written into the box before it is converged, using the names OpenConcept’s own
set_values uses. Mission-level values, the per-phase speed and vertical-speed schedules, and
the solver’s starting guesses all live here, because to the box they are the same operation:
initial_conditions:
mission_range: {value: 2800, units: nmi}
cruise|h0: {value: 35000, units: ft}
climb.fltcond|Ueas: {value: [230, 252], units: kn}
climb.fltcond|vs: {value: [2300, 400], units: ft/min}
ac|weights|MTOW: {value: 50.0e3, units: kg} # a starting guess, not a design
Only value and units are accepted. A name is resolved first as written and then under
mission_path, so cruise|h0 and mission.cruise|h0 are both legal and a name that
resolves neither way is an error naming both attempts.
A value may be a single number (constant across the phase), two numbers (interpolated across it,
exactly as np.linspace does in OpenConcept’s run script), or exactly as many numbers as the
variable’s own shape. Any other length is an error rather than being broadcast, because
broadcasting would quietly fly a different mission.
Writing a value onto an output is meaningful here and only here. Maximum takeoff weight is what the weight closure solves for, so it can never be a design variable; writing a value onto it before the first solve is where Newton starts, and on a sizing loop that is frequently the difference between converging and not. These decide whether the solver converges, not what it converges to.
continuation
A list of progressively harder missions, each converged before the next is attempted. The box is a Newton-solved implicit system, and started cold on a 2800 nmi mission at FL350 it does not converge:
continuation:
- description: short low-altitude mission, gentle descent
initial_conditions:
mission_range: {value: 500, units: nmi}
cruise|h0: {value: 5000, 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. Rungs are partial by
design: each overrides only what it names, on top of the case’s own initial_conditions, which
are re-applied before every rung. The ladder is a route to the answer and not part of it –
tests.test_verification_reproducibility walks three different ladders and requires them to
reach the same aircraft.
driver
Key |
Default |
Meaning |
|---|---|---|
|
|
|
|
50 |
Optimizer iteration limit. |
|
1e-6 |
Optimizer convergence tolerance. |
|
|
|
|
|
Extra settings passed through to pyOptSparse, on top of cdadt’s per-optimizer defaults. |
constraints
A list, each entry being one add_constraint call plus the two fields OpenConcept’s examples
have nowhere to put – the regulation the number comes from, and the source of that number:
constraints:
- name: takeoff_field_length
upper: 8000.0
units: ft
regulation: 14 CFR 25.113
source: Design field length, 8000 ft dry runway at sea level, ISA
title: Balanced field length within the runway available
- name: climb_throttle # a plain design bound, no provenance claimed
lower: 0.01
upper: 1.05
Key |
Required |
Meaning |
|---|---|---|
|
yes |
A response name any discipline reports – see The interface reference – or a raw black-box path. Response names are preferred: they carry their own units. |
|
one of them |
Any form |
|
no |
Units the bounds are stated in. Defaults to the response’s own. |
|
no |
Which elements of a vector response are constrained. |
|
no |
Whether the constraint is linear in the design variables. Default |
|
no |
Driver scaling. |
|
no |
Provenance. Supplying both is what puts a constraint in the traceability matrix as certification evidence; supplying neither reports it as a design constraint. See The certification basis. |
|
no |
One line naming the constraint in the report. Defaults to |
A vector response is one constraint, not one per node, and the value reported is the governing one: the least margin, whichever side of the bound it is near.
objective
Present makes the case an optimization study; absent makes it a sizing run.
objective: {name: total_fuel, units: kg, sense: minimize, ref: 2.0e4}
name is a response name or a raw path, sense is minimize or maximize, index
picks one element of a vector output, and ref/ref0/scaler/adder are OpenMDAO’s
scaling arguments.
ref is not decoration. SLSQP takes its finite-difference step and its convergence test on the
scaled objective, so an objective of order 1e4 is effectively converged before it starts. IPOPT
scales from its own gradient and does not need it.
Validation
The case file is checked in three passes, each of which names what is wrong:
Structure, when the file is read: unknown keys, missing keys, non-numeric values, an even
num_nodes, inverted or missing bounds, anequalson a design variable, unknown senses and unknown derivative modes.Against the box, before an optimization runs: every freed variable must be an independent variable it publishes, and the objective and every constraint must be a quantity it produces. Checked on a cheaply built probe, which costs a fraction of a second rather than failing inside
setupafter a minute of building.Against the box, when the study builds: every design variable and every initial condition must resolve to something the box accepts, either as written or under
mission_path.