The OpenConcept black box

The sizing analysis cdadt drives is an OpenConcept group, named in the case file:

black_box:
  model: openconcept.examples.B738_sizing:B738SizingMissionAnalysis
  num_nodes: 21

cdadt sets its inputs, converges it, and reads its outputs. It does nothing else to it.

What “black box” means here, precisely

Three specific things, each enforced by tests.test_boundary rather than claimed here.

No cdadt module imports OpenConcept, outside one package. Every cdadt source file is parsed and its imports inspected. The dependency is a string in a YAML file, resolved by importlib.import_module() at run time. That is a stronger guarantee than “cdadt does not modify OpenConcept”, because it removes the possibility rather than the practice: there is no imported class to subclass, no imported component to re-wire, and no way for “compose one part of it” to drift into “reimplement half of it”.

No cdadt class inherits from OpenConcept. Overriding a method changes a component’s behaviour while leaving its source untouched – a modification no diff of the clone would ever show. Every class cdadt defines is walked and its whole inheritance chain checked.

No OpenConcept source is modified. git status is run in the OpenConcept working tree and any uncommitted change fails the suite. Separately, every commit the installed clone carries that upstream does not is examined, and the files it touches are compared against the modules actually loaded when a box is built – taken from sys.modules, so it is measured rather than assumed. The clone these results were produced against carries none: it is exactly mdolab/openconcept origin/main. See Validation.

The one package that opens it, and why

cdadt.adapter imports OpenConcept. That is a widening of the rule above and it was argued for rather than assumed, so it is worth stating exactly how far it goes.

The reason is that OpenConcept’s mission profiles already take the aircraft model as an option – self.options.declare("aircraft_model", ...), documented as “OpenConcept-compliant airplane model” – and its own B738_VLM_drag.py uses that hook to swap in a vortex-lattice drag. The only obstacle is that B738_sizing.py writes aircraft_model=B738AircraftModel at the line where it builds the mission, so the choice cannot be reached from outside that group. Substituting the aerodynamics therefore means owning an aircraft model and the group that installs it, and an aircraft model is an OpenMDAO group OpenConcept instantiates. It cannot be a string in a YAML file.

What the exemption does not cover:

  • The physics cdadt owns. cdadt.models – the loads abstraction, the parabolic polar, the planform – imports neither OpenConcept nor openavl, so a model can be written, differentiated and tested on a machine with neither installed. Separately checked.

  • The disciplines, the config, the optimizer, the command line. Unchanged: for all of them the dependency is still a name in a file.

  • OpenAeroStruct. cdadt reaches it through OpenConcept – WaveDragFromSections and VLM are OpenConcept’s components – and may not import it anywhere at all, adapter included. A separate contract test with no exemption enforces that.

And the substitution changes nothing about the four rules: OpenConcept is still not modified, not moved, not copied, not patched. The aerodynamics is installed into it through an interface it already published. See Supplying your own aerodynamics.

What is inside

Everything that computes anything. For the shipped case, openconcept.examples.B738_sizing:B738SizingMissionAnalysis contains:

  • the per-phase aircraft model OpenConcept instantiates once per mission phase;

  • the component-by-component parasite drag buildup and the parabolic drag polar;

  • the rubberized CFM56 engine deck – thrust and fuel flow scaled from a fixed deck;

  • the jet-transport empty-weight correlations, structure and equipment;

  • horizontal and vertical tail sizing by tail volume coefficient;

  • the clean and takeoff maximum lift estimates;

  • the weight closure that makes takeoff weight consistent with the fuel burned;

  • FullMissionWithReserve: the balanced-field takeoff, climb, cruise, descent, the Part 25 reserve diversion and loiter, and the implicit solves that make all of it consistent.

cdadt computes none of it.

What is fixed inside, and cannot be reached

Some of the box’s modelling choices are options of its components rather than inputs to them. They are therefore not settable from a case file, and no amount of configuration will change them. They are listed because a design study that does not know about them is quoting numbers whose assumptions it cannot state:

Horizontal tail volume coefficient, 1.00

Raymer Table 6.4, jet transport

Vertical tail volume coefficient, 0.09

Raymer Table 6.4, jet transport

Tail lever arm, half the fuselage length

An estimate made inside the box

Maximum landing weight, 0.8 x MTOW

An estimate, used only to size the gear

Ultimate load factor and structural allowances

Constants of the weight correlations

Engine deck, CFM56, scaled by rated thrust

Not a cycle analysis; a scaled surrogate

Fuel burn integration, Simpson’s rule

Why num_nodes must be odd

Changing any of these means changing the box, which means writing or choosing a different sizing analysis and naming it in black_box.model. It does not mean editing OpenConcept.

What goes in

The complete, generated list is The interface reference; cdadt inspect <case> prints it. In summary, cdadt sets:

Design parameters

Every ac| variable the box publishes as an independent variable – 34 of them for the shipped case: the wing planform, the empennage shape, the fuselage, nacelles and gear, the aerodynamic parameters, the engine rating and count, the payload and the cabin. Declared in the aircraft section and routed to the discipline that owns each.

The mission

mission_range, reserve_range, cruise|h0, reserve|h0, loiter|h0, loiter_duration and takeoff|h, plus the equivalent-airspeed and vertical-speed schedule flown at every node of every steady-flight phase, and a true-airspeed seed for the three ground-roll phases. See The mission.

Starting guesses

Values written onto the box’s coupled states before the first solve. Maximum takeoff weight is the important one. These are held to a looser standard than design parameters on purpose: they are outputs the box solves for, so they are not independent variables and can never be design variables, but where Newton starts still decides whether it arrives.

Solver settings

Iteration limit and tolerances for the Newton solve that closes the weight loop and the mission’s balances together, plus whether a failed solve raises.

Design variables, an objective and constraints

For an optimization, declared on the box’s group before setup using OpenMDAO’s public API. Declaring a design variable does not change what any OpenConcept component computes; it states which of the box’s existing independent variables a driver may move.

What comes out

Every discipline reads its own responses. The ones that carry the certification argument:

Response

Path inside the box

Units

block_fuel

mission.descent.fuel_burn_integ.fuel_burn_final

kg

total_fuel

mission.loiter.fuel_burn_integ.fuel_burn_final

kg

takeoff_field_length

mission.bfl.distance_continue

ft

abort_distance

mission.bfl.distance_abort

ft

V1

mission.takeoff|v1

kn

V2

mission.engineoutclimb.takeoff|v2

kn

engine_out_climb_gradient

mission.engineoutclimb.gamma

rad

MTOW

ac|weights|MTOW

kg

OEW

ac|weights|OEW

kg

Two of those deserve a note.

total_fuel is read at the end of loiter, not at the end of descent. Fuel accumulates across every phase because the box links each phase’s final integrated state to the next phase’s initial value, and loiter is the last phase it accumulates through. Closing the weight loop on block fuel instead would size an aircraft that carries no reserves – and would converge just as readily.

takeoff_field_length and abort_distance are equal at convergence by construction: the box solves V1 implicitly so that continuing after an engine failure and rejecting the takeoff cover the same distance. That is what makes it a balanced field length, and the validation suite asserts the equality rather than trusting it.

The twelve phases

Takeoff, balanced field

v0v1 ground roll to the decision speed; v1vr decision speed to rotation; rotate the transition to 35 ft; v1v0 the rejected takeoff; and EngineOutClimbAngle, a single-point engine-out climb condition at V2.

Design mission

climb, cruise, descent.

Part 25 reserves

reserve_climb, reserve_cruise, reserve_descent, loiter.

Why the boundary is where it is

An earlier version of cdadt drew it differently: it imported twenty-odd OpenConcept components and re-wired them into its own discipline groups, which reproduced the reference example exactly and was, in every honest sense, a reimplementation of OpenConcept’s own example living inside cdadt. It was called a black box and was not one.

The boundary that survives is the one that cannot be crossed by accident. cdadt does not import OpenConcept, so it cannot compose it, so the question of how much composition is too much never arises. What cdadt contributes is on its own side of the line: the case file, the discipline model of the interface, the continuation ladder, the certification basis, the optimizer and the reporting.

The cost is real and is stated in Validation: cdadt can only constrain what the box publishes, so a regulation whose governing quantity the box does not produce cannot be enforced.