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

design_variables

B738.py line 86 – “Define a bunch of design variables and airplane-specific parameters” – and the dv_comp.add_output_from_dict(...) calls that follow it.

initial_conditions

set_values(prob, num_nodes) in B738.py, plus the first half of set_mission_profile(prob) in B738_sizing.py. Same names.

continuation

The second half of set_mission_profile: the run_model() calls that converge an easy mission before the design one is attempted.

driver, constraints, objective

The add_constraint / add_objective calls in the examples that optimize (B738_aerostructural.py, B738_VLM_drag.py).

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

model

yes

The sizing analysis to drive, as module.path:ClassName. Loaded by name at run time; never imported by cdadt, never modified, never subclassed. See The OpenConcept black box.

num_nodes

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.

options

no

Passed to the analysis group’s constructor. What is accepted depends on which group model names – OpenConcept’s own takes none – and it is how a study chooses its aerodynamics. See below.

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

aerodynamic_loads

cdadt.models.polar:PolarLoads

module:ClassName naming the AerodynamicLoads to fly with, resolved the same way model is. Three ship; see Supplying your own aerodynamics.

wave_drag

false

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

maxiter

20

Newton iteration limit. The shipped optimization case raises it to 50: an optimizer visits designs a human would not.

atol, rtol

1e-9

Absolute and relative residual tolerances.

iprint

-1

Solver print level. 2 prints every iteration.

err_on_non_converge

true

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

value

yes

The number, in units. A list is a vector value, element by element.

units

no

OpenMDAO unit string (m**2, lbf, kn, deg). Omit for a dimensionless quantity such as aspect ratio.

source

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.

optimize

no

Present frees the variable for the driver. Absent holds it at value.

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

lower, upper

The interval the variable may move in. A scalar, or a list to bound a vector variable element by element.

indices

all

Which elements of a vector variable are free.

ref, ref0, scaler, adder

ref = larger bound

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

name

SLSQP

SLSQP (SciPy, always available) or a pyOptSparse optimizer such as IPOPT or SNOPT. See Optimization for why the shipped case uses IPOPT.

maxiter

50

Optimizer iteration limit.

tol

1e-6

Optimizer convergence tolerance.

derivative_mode

fwd

auto, fwd or rev. Forward is right here: a handful of design variables, many vector responses.

options

{}

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

name

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.

lower, upper, equals

one of them

Any form add_constraint accepts: one-sided, two-sided, or equality.

units

no

Units the bounds are stated in. Defaults to the response’s own.

indices

no

Which elements of a vector response are constrained.

linear

no

Whether the constraint is linear in the design variables. Default false.

ref, ref0, scaler, adder

no

Driver scaling. ref defaults to the magnitude of the bound, without which a climb gradient in hundredths of a radian is numerically invisible next to a field length in thousands of feet.

regulation, source

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.

title

no

One line naming the constraint in the report. Defaults to name.

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:

  1. Structure, when the file is read: unknown keys, missing keys, non-numeric values, an even num_nodes, inverted or missing bounds, an equals on a design variable, unknown senses and unknown derivative modes.

  2. 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 setup after a minute of building.

  3. 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.