Black box

The black box: OpenConcept’s full-mission sizing analysis, used as published.

This module is the whole of cdadt’s contact with OpenConcept, and it contains no OpenConcept import. The sizing model is named in the case file as module:ClassName and loaded by importlib.import_module(), so the dependency is data rather than code. A contract test asserts that no cdadt module imports openconcept at all.

What that buys is a boundary that cannot erode. cdadt cannot subclass an OpenConcept component it never imports, cannot re-wire one, and cannot quietly re-implement half of one and call the result “composition”. The only things that cross the boundary are numbers going in and numbers coming out – and the name of the model, in a YAML file.

What is inside

Everything. For the shipped case, openconcept.examples.B738_sizing:B738SizingMissionAnalysis contains the per-phase aircraft model, the parasite drag buildup, the drag polar, the rubberized CFM56 deck, the jet-transport empty-weight correlations, the tail volume coefficient sizing, the maximum lift estimates, the weight closure, and OpenConcept’s FullMissionWithReserve trajectory: balanced-field takeoff, climb, cruise, descent, the Part 25 reserve diversion and loiter. cdadt computes none of it.

What cdadt supplies

The values of the box’s independent variables, a mission profile, a Newton solver configuration, and – when optimizing – design variables, an objective and constraints registered on the box’s own group before setup. Registering a design variable on a group is OpenMDAO’s public API and changes no OpenConcept behaviour; it declares which of the box’s existing independent variables a driver may move.

Notes

Attaching the nonlinear and linear solvers is cdadt’s job because the box does not attach its own: OpenConcept’s sizing group is written to be dropped into a problem whose run script supplies them, exactly as its own example does. The settings are configuration, not tuning constants, and live in the case file.

exception cdadt.blackbox.BlackBoxError[source]

Bases: Exception

Raised when the black box cannot be loaded, built, or addressed as asked.

class cdadt.blackbox.OpenConceptSizingBox(model, num_nodes, solver=None, run=None, options=None)[source]

Bases: object

An OpenConcept sizing analysis, driven from outside as a black box.

Parameters:
  • model (str) – The sizing group to load, as "module.path:ClassName". For the shipped case, "openconcept.examples.B738_sizing:B738SizingMissionAnalysis".

  • num_nodes (int) – Analysis points per mission phase. Must be odd: the box integrates fuel burn with Simpson’s rule, which needs 2N + 1 points.

  • solver (SolverSettings, optional) – Newton solve configuration. Defaults to OpenConcept’s own sizing settings.

  • run (RunDirectory | None)

  • options (Mapping[str, Any] | None)

Raises:
  • ValueError – If num_nodes is even.

  • BlackBoxError – If the model cannot be imported or is not a class.

Examples

>>> box = OpenConceptSizingBox("openconcept.examples.B738_sizing:B738SizingMissionAnalysis", 11)
>>> box.build()
>>> box.set("ac|geom|wing|S_ref", 124.6, units="m**2")
>>> box.run()
>>> float(box.get("ac|weights|MTOW", units="kg"))
78345.6...
INDEP_VAR_TAG = 'openmdao:indep_var'

Tag OpenMDAO puts on the outputs of an IndepVarComp. Those, plus inputs that no component drives, are exactly the variables a caller may set and a driver may move.

classmethod describe(model, num_nodes=3, options=None)[source]

Return a cheaply built box, for reading the interface without running anything.

Building on the smallest legal grid and with the solver disabled costs a fraction of a real setup, and the names a box publishes do not depend on the grid – only their shapes do. That is what makes it possible to tell a user their design variable is misspelled, with suggestions, before a long optimization starts rather than as an OpenMDAO error thrown out of setup.

Parameters:
  • model (str) – The analysis to load, as "module.path:ClassName".

  • num_nodes (int, optional) – Grid to build on. Default 3, the smallest odd grid.

  • options (mapping, optional) – The case file’s options for the analysis. Passed on, because an analysis whose options change what it contains would otherwise be described in a configuration nobody asked for – cdadt’s own group would report the interface of the default aerodynamics rather than the one the study names.

Returns:

OpenConceptSizingBox – Built, never converged. Its numbers are meaningless; its interface is not.

Return type:

OpenConceptSizingBox

property model_spec: str

The module:Class string the box was loaded from.

property model_class: type

The class the box instantiates. Loaded, never modified and never subclassed.

property num_nodes: int

Analysis points per mission phase.

property solver: SolverSettings

The Newton solve configuration.

property model_options: dict[str, Any]

Options the case file passes to the analysis, beyond the grid.

Empty for OpenConcept’s own group, which declares only num_nodes. cdadt’s group takes aerodynamic_loads this way.

property run_directory: RunDirectory | None

Where this box’s run writes its files, or None for OpenMDAO’s own default.

With no run directory the problem is built unnamed and with reports=False, and building one then writes nothing. Running a driver is different. pyOptSparse writes IPOPT.out into the problem’s output directory, and OpenMDAO creates that directory on demand – <cwd>/__main__<n>_out for an unnamed problem. reports=False suppresses the reports, not the directory a driver writes into.

That is OpenMDAO’s normal behaviour and is left alone, but it is the reason the command line always supplies a run directory: without one, an optimization leaves its log wherever it happened to be run from, under a name that says nothing about the case.

Named for the directory rather than run, which is the method that converges the box.

build(register=(), derivative_mode='auto', driver=None)[source]

Instantiate the model, attach solvers, run any registrations, and set up.

Parameters:
  • register (sequence of callable, optional) – Called with the box’s group after the solvers are attached and before setup. This is where design variables, an objective and constraints are declared, because OpenMDAO requires all three before setup. See Optimizer.

  • derivative_mode (str, optional) – "auto", "fwd" or "rev". Default "auto". Forward is usually right here: a sizing optimization has a handful of design variables and many vector responses.

  • driver (openmdao.api.Driver, optional) – Driver to attach before setup. None leaves OpenMDAO’s default.

Returns:

openmdao.api.Problem – The problem, set up but not converged. No mission has been applied yet.

Return type:

om.Problem

property problem: Problem

The built problem.

Raises:

BlackBoxError – If build() has not been called.

property model: Group

The box’s own group instance.

property is_built: bool

Whether build() has been called.

settable()[source]

Return every variable a caller may set, by promoted name.

These are the box’s independent variables: the outputs of its IndepVarComps and the inputs no component drives. They are also exactly the variables that may legally become optimizer design variables.

The set is introspected from the built model, not written down, so a variable added to or removed from the box shows up here immediately.

Returns:

dictpromoted name -> VariableInfo.

Return type:

dict[str, VariableInfo]

readable()[source]

Return every output the box produces, by promoted name.

Returns:

dictpromoted name -> VariableInfo. Inputs are omitted: a promoted input name can address several components at once and is therefore not unambiguously readable. Every quantity a discipline reports is an output.

Return type:

dict[str, VariableInfo]

has(name)[source]

Return whether name addresses something readable in the box.

Parameters:

name (str)

Return type:

bool

shape_of(name)[source]

Return the shape the box declares for name.

Used to resample an initial condition written as a pair of endpoints onto the grid the box actually declares, which is what np.linspace(2300.0, 600.0, num_nodes) does by hand in OpenConcept’s own run script.

Parameters:

name (str)

Return type:

tuple[int, …]

check_settable(names)[source]

Raise if any of names is not an independent variable of the box.

Parameters:

names (iterable of str) – Promoted names to check.

Raises:

BlackBoxError – Naming the offenders and, for each, the closest settable variables. Setting a computed output would be silently overwritten by the next solve, and declaring one as a design variable is an OpenMDAO error thrown far from its cause.

Return type:

None

check_addressable(names)[source]

Raise if any of names is not a variable of the box at all.

Looser than check_settable(), and used for the solver’s starting guesses. A coupled state such as maximum takeoff weight is a computed output – it is what the weight closure solves for – so it is not an independent variable and can never be a design variable. Writing a value onto it before the first solve is nonetheless meaningful and often decisive: it is where Newton starts.

Parameters:

names (Iterable[str])

Return type:

None

set(name, value, units=None)[source]

Set a variable of the box.

Parameters:
  • name (str) – Promoted name.

  • value (float or array_like) – Value to set, interpreted in units.

  • units (str or None, optional) – Units of value. None means the box’s own declared units.

Raises:

BlackBoxError – If the box does not have that variable.

Return type:

None

get(name, units=None)[source]

Read a variable of the box.

Parameters:
  • name (str) – Promoted name or full path.

  • units (str or None, optional) – Units to convert to. None means the box’s own declared units.

Returns:

float or numpy.ndarray – A float for a single-element variable, an array otherwise. Scalars are unwrapped because a one-element array propagates into reports and comparisons as array([1.0]).

Raises:

BlackBoxError – If the box does not publish that variable.

Return type:

Any

run()[source]

Converge the box at the current inputs.

Raises:

openmdao.core.analysis_error.AnalysisError – If the Newton solve fails and err_on_non_converge is set.

Return type:

None

run_driver()[source]

Run the attached driver.

Returns:

bool – Whether the driver reported success.

Return type:

bool

Notes

OpenMDAO’s run_driver historically returned a failure flag and now returns a result object whose truthiness reproduces that, under a deprecation warning. Reading success where it exists keeps cdadt on the supported attribute and, more usefully, means the value this method returns says what it means.

class cdadt.blackbox.RunDirectory(name, root='run_outputs', reports=True)[source]

Bases: object

Where one run leaves everything it produces.

OpenMDAO already has this concept: a openmdao.api.Problem given a name writes into <work_dir>/<name>_out, and everything downstream of it follows – its reports, and the optimizer’s own log, since pyOptSparse writes IPOPT.out into the working directory the problem set up. cdadt does not reimplement any of that. It names the problem and says where the work directory is, then writes its own files into the directory OpenMDAO made.

So a run leaves one folder holding both halves of the record:

run_outputs/b738_20260728_193000_out/
    n2.html          report.txt    results.json      <- cdadt
    mission.pdf      trajectory.pdf   takeoff.pdf    <- cdadt
    .openmdao_out    reports/       IPOPT.out        <- OpenMDAO and the driver

The _out suffix is OpenMDAO’s, not a choice made here. Matching its convention is what makes the driver log and the reports land in the same place as everything else rather than somewhere they have to be collected from.

Parameters:
  • name (str) – Name for this run, used as the directory name. Usually the case file’s stem and a timestamp, so runs of the same case accumulate rather than overwrite.

  • root (str or Path, optional) – Where run directories are created. Default "run_outputs".

  • reports (bool, optional) – Whether OpenMDAO writes its own reports. Default True, which is what produces reports/n2.html and reports/inputs.html beside cdadt’s files.

Examples

>>> run = RunDirectory("b738_20260728_193000")
>>> run.name
'b738_20260728_193000'
DEFAULT_ROOT: str = 'run_outputs'

Default parent of every run directory.

classmethod for_case(case, stamp, root='run_outputs')[source]

Return a run directory named for a case file and a timestamp.

The stamp is passed in rather than read from a clock here, so that a study can be reproduced into a named directory and a test can assert on the path.

Parameters:
  • case (str | Path)

  • stamp (str)

  • root (str | Path)

Return type:

RunDirectory

property name: str

The run’s name, which is also its directory name without OpenMDAO’s suffix.

property root: Path

Where run directories are created.

problem(model)[source]

Return the OpenMDAO problem that writes into this run’s directory.

Parameters:

model (Group)

Return type:

Problem

property path: Path

The directory OpenMDAO made for this run.

Raises:

BlackBoxError – If no problem has been built yet, since OpenMDAO decides the path and has not been asked.

class cdadt.blackbox.SolverSettings(maxiter=20, atol=1e-09, rtol=1e-09, iprint=-1, err_on_non_converge=True)[source]

Bases: object

Configuration of the Newton solve that converges the black box.

Parameters:
  • maxiter (int, optional) – Newton iteration limit. Default 20, as in OpenConcept’s own sizing run script.

  • atol (float, optional) – Absolute and relative residual tolerances. Default 1e-9 for both.

  • rtol (float, optional) – Absolute and relative residual tolerances. Default 1e-9 for both.

  • iprint (int, optional) – Solver print level: -1 silent, 2 per-iteration. Default -1.

  • err_on_non_converge (bool, optional) – Raise instead of returning the state of a failed solve. Default True, and it should stay 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.

property maxiter: int

Newton iteration limit.

property atol: float

Absolute residual tolerance.

property rtol: float

Relative residual tolerance.

property iprint: int

Solver print level.

property err_on_non_converge: bool

Whether a failed solve raises.

class cdadt.blackbox.VariableInfo(name, units, shape, kind)[source]

Bases: object

Metadata about one variable of the black box.

Parameters:
  • name (str) – Promoted name, as cdadt addresses it.

  • units (str or None) – Units the box declares.

  • shape (tuple of int) – Shape the box declares.

  • kind (str) – "input" for a variable the box takes, "output" for one it produces.

property name: str

Promoted name of the variable.

property units: str | None

Units the box declares, or None if dimensionless.

property shape: tuple[int, ...]

Shape the box declares.

property kind: str

"input" or "output".

property is_scalar: bool

Whether the variable holds a single number.