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:
ExceptionRaised 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:
objectAn 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 + 1points.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_nodesis 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:
- property model_spec: str
The
module:Classstring 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 takesaerodynamic_loadsthis way.
- property run_directory: RunDirectory | None
Where this box’s run writes its files, or
Nonefor 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 writesIPOPT.outinto the problem’s output directory, and OpenMDAO creates that directory on demand –<cwd>/__main__<n>_outfor an unnamed problem.reports=Falsesuppresses 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. SeeOptimizer.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.
Noneleaves 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.
- 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:
dict –
promoted name -> VariableInfo.- Return type:
dict[str, VariableInfo]
- readable()[source]
Return every output the box produces, by promoted name.
- Returns:
dict –
promoted 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
nameaddresses 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
namesis 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
namesis 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.Nonemeans 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.
Nonemeans 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_convergeis 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_driverhistorically returned a failure flag and now returns a result object whose truthiness reproduces that, under a deprecation warning. Readingsuccesswhere 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:
objectWhere one run leaves everything it produces.
OpenMDAO already has this concept: a
openmdao.api.Problemgiven anamewrites into<work_dir>/<name>_out, and everything downstream of it follows – its reports, and the optimizer’s own log, since pyOptSparse writesIPOPT.outinto 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
_outsuffix 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 producesreports/n2.htmlandreports/inputs.htmlbeside 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:
- 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:
objectConfiguration 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:
-1silent,2per-iteration. Default-1.err_on_non_converge (bool, optional) – Raise instead of returning the state of a failed solve. Default
True, and it should stayTrue: 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:
objectMetadata 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
Noneif 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.