Configuration

The case file: one YAML document that describes an entire study.

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.

design_variables

B738.py line 86 reads “Define a bunch of design variables and airplane-specific parameters” and then lists every ac| variable through dv_comp.add_output_from_dict(...). That block is this one: every design variable, with its value, its units and where the number came from. A variable becomes free for the optimizer by gaining an optimize: entry with bounds – nothing else moves between sections, and a study that frees one more variable differs by three lines.

initial_conditions

set_values(prob, num_nodes) in B738.py, and the first half of set_mission_profile(prob) in B738_sizing.py: the per-phase speed and vertical-speed schedules, the cruise altitude, the range, and the solver’s starting guesses. Written with the same names those functions use.

continuation

The second half of set_mission_profile: the easy mission converged first, then stepped up. OpenConcept writes it as two run_model() calls between blocks of assignments; here it is a list of rungs.

driver, constraints, objective

What the examples that do optimize – B738_aerostructural.py, B738_VLM_drag.py – write as add_constraint/add_objective calls. Every form those calls accept is expressible: one-sided, two-sided, equality, with any of OpenMDAO’s scaling and index arguments.

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.

class cdadt.config.BlackBoxConfig(model, num_nodes, options=None)[source]

Bases: object

Which sizing analysis to drive, on what grid, and with what options.

Parameters:
  • model (str) – "module:ClassName" naming the analysis.

  • num_nodes (int) – Analysis points per phase.

  • options (mapping, optional) –

    Anything else the analysis declares as an OpenMDAO option, passed straight to its constructor. This is how a case file configures a black box that has choices to make – cdadt’s own SizingMissionAnalysis takes aerodynamic_loads this way, naming which aerodynamics to fly with.

    Not validated here, deliberately. cdadt does not know what options an arbitrary analysis declares, and OpenMDAO already refuses an option a group does not recognise, by name. A list of allowed keys here would be a second source of truth that could only ever be more wrong than the group itself.

ALLOWED = ('model', 'num_nodes', 'options')
classmethod from_section(section)[source]

Build from the black_box section.

Parameters:

section (CaseFileSection)

Return type:

BlackBoxConfig

property options: dict[str, Any]

Extra options passed to the analysis, as a copy so a caller cannot mutate the case.

property model: str

The module:Class string naming the analysis.

property num_nodes: int

Analysis points per mission phase.

class cdadt.config.Bounds(lower=None, upper=None, equals=None)[source]

Bases: object

The bound of a constraint: an upper limit, a lower limit, both, or an equality.

Every form add_constraint accepts, because every one appears in OpenConcept’s own examples: a one-sided limit, a two-sided band such as lower: 0.01, upper: 1.05 on a throttle history, and an equality such as equals: 0.

Parameters:
  • lower (Any)

  • upper (Any)

  • equals (Any)

property lower: Any

The lower side, if there is one.

property upper: Any

The upper side, if there is one.

property equals: Any

The equality value, if this is an equality.

property is_equality: bool

Whether this is an equality constraint.

property is_two_sided: bool

Whether both sides are bounded.

property magnitude: float

A representative size of the bound, used to scale the constraint by default.

as_kwargs()[source]

Return the keyword arguments OpenMDAO’s add_constraint takes.

Return type:

dict[str, Any]

describe()[source]

Return the bound as it should read in a report.

Return type:

str

class cdadt.config.CaseFileSection(data, where)[source]

Bases: object

One block of a case file, together with the address it lives at.

Every reader in this module goes through one of these. The point is that a section carries its own address, so a message can say where in the file the mistake is without every call site passing that string along by hand – and so a nested block cannot be given the wrong address, because it derives its own from its parent.

A case file is the only thing a user of cdadt writes by hand, and it fails late or not at all if it is read leniently: a misspelled key means the run succeeds and answers a different question. So a section validates on construction, rejects keys it does not recognise, and names the block in every message it raises.

Parameters:
  • data (Any) – The parsed block. Must be a mapping; anything else is the error this raises.

  • where (str) – The address, as it should read in a message – "solver", "design_variables.ac|geom|wing|AR", "constraints[0]".

Raises:

ConfigError – If data is not a mapping, naming where and what was found instead.

Examples

>>> section = CaseFileSection({"value": 124.6, "units": "m**2"}, "design_variables.S_ref")
>>> section.require("value")
124.6
>>> section.child_of("units")
Traceback (most recent call last):
ConfigError: 'design_variables.S_ref.units' must be a mapping; got str.
property where: str

The address this section lives at, as it reads in a message.

get(key, default=None)[source]

Return key’s raw value, or default if the block does not have it.

Parameters:
  • key (str)

  • default (Any)

Return type:

Any

require(key)[source]

Return key’s raw value.

Raises:

ConfigError – If the block does not have it, naming the block and the key.

Parameters:

key (str)

Return type:

Any

reject_unknown(allowed)[source]

Raise if the block carries any key outside allowed.

Raises:

ConfigError – Naming the offending keys and listing what is allowed. A silently ignored key means the study runs and answers a different question than the one that was written.

Parameters:

allowed (Sequence[str])

Return type:

None

number_or_vector(key)[source]

Return key as a float, or an array when the file gave a sequence.

Parameters:

key (str)

Return type:

Any

child_of(key, default=ABSENT, *, where=None)[source]

Return the block at key as a section of its own.

Parameters:
  • key (str) – Which block to descend into.

  • default (Any, optional) – Used when the key is absent, so an optional block reads the same as a present one – pass {} for a block whose keys all have defaults of their own. Omit it to make the block required, in which case an absent key raises naming this section.

  • where (str, optional) – Address to give the child. Defaults to "<this section>.<key>", which is what a nested block wants. The top-level blocks of a case file pass their own bare name instead: a reader is looking for 'objective' in the message, not 'the case file.objective'.

Return type:

CaseFileSection

entries()[source]

Iterate (name, section) for a block whose keys are themselves blocks.

This is what design_variables and initial_conditions are: a mapping from a black-box variable name to its entry.

Return type:

Iterator[tuple[str, CaseFileSection]]

series(key)[source]

Return the list at key as sections addressed key[0], key[1], …

Absent means empty, because a case file that constrains nothing simply omits the block.

Raises:

ConfigError – If the value is present but is not a list.

Parameters:

key (str)

Return type:

tuple[CaseFileSection, …]

static as_number(value, where)[source]

Return value as a float, or raise naming where it came from.

Parameters:
  • value (Any)

  • where (str)

Return type:

float

static as_number_or_vector(value, where)[source]

Return a float, or an array when the case file gave a sequence.

A sequence is how a per-node schedule is written – [2300, 400] for the endpoints of a climb rate – and how a vector design variable is bounded element by element, which is what OpenConcept’s aerostructural example does for a spanwise thickness distribution.

Parameters:
  • value (Any)

  • where (str)

Return type:

Any

class cdadt.config.Config(black_box, design_variables, initial_conditions=None, continuation=None, solver=None, driver=None, constraints=(), objective=None, mission_path='mission', path=None)[source]

Bases: object

A whole case file.

Examples

>>> config = Config.from_yaml("cases/b738.yaml")
>>> config.black_box.num_nodes
21
>>> [name for name, spec in config.design_variables.items() if spec.is_free]
['ac|geom|wing|S_ref', ...]
Parameters:
ALLOWED = ('black_box', 'solver', 'mission_path', 'design_variables', 'initial_conditions', 'continuation', 'driver', 'constraints', 'objective')
classmethod from_yaml(path)[source]

Load a case file.

Parameters:

path (str | Path)

Return type:

Config

classmethod from_dict(data, path=None)[source]

Build a configuration from an already-parsed mapping.

This is where the raw document becomes a CaseFileSection; every reader below this point takes a section and therefore knows its own address in the file.

Parameters:
  • data (Mapping[str, Any])

  • path (Path | None)

Return type:

Config

classmethod from_section(section, path=None)[source]

Build a configuration from the top-level section of a case file.

Parameters:
Return type:

Config

property black_box: BlackBoxConfig

Which sizing analysis to drive.

property solver: SolverConfig

How hard to converge the box.

property driver: DriverConfig

Which optimizer to use.

property mission_path: str

Subsystem the mission lives under inside the black box.

property design_variables: dict[str, VariableSpec]

Every design variable, free or fixed, as B738.py lists them.

property initial_conditions_specs: dict[str, VariableSpec]

Every initial condition, as set_values writes them.

property free_variables: dict[str, VariableSpec]

The variables that carry an optimize: entry, in declaration order.

property constraints: tuple[ConstraintSpec, ...]

The constraints, in the order the case file states them.

property objective: ObjectiveSpec | None

The objective, or None for a sizing-only case.

property continuation: ContinuationLadder

The ladder walked before the design mission.

property path: Path | None

Where the case file was read from.

property is_optimization: bool

Whether this case declares an objective to drive.

initial_conditions()[source]

Return everything that is written into the box before it is converged.

Both blocks: the design variables carry values too, and a value is a value whichever block it was declared in. Keeping them in separate sections is about what a reader is being told, not about what the box is sent.

Return type:

InitialConditions

parameters()[source]

Return the scalar design variables as routable parameters.

Return type:

list[Parameter]

exception cdadt.config.ConfigError[source]

Bases: Exception

Raised when a case file is missing something, has something extra, or has it wrong.

class cdadt.config.ConstraintSpec(name, bounds, units=None, scaling=None, indices=None, linear=False, regulation='', source='', title='')[source]

Bases: object

One constraint on the design.

Parameters:
  • name (str) – Either a response name any discipline reports – takeoff_field_length, climb_throttle – or a raw black-box path. Response names are preferred: they carry their own units.

  • bounds (Bounds) – Any form add_constraint accepts.

  • units (str or None, optional) – Units the bounds are stated in.

  • scaling (Scaling, optional) – Driver scaling. Defaults to the magnitude of the bound, so that constraints of wildly different magnitudes – a field length in thousands of feet and a climb gradient in hundredths of a radian – are comparable to the optimizer.

  • indices (sequence of int or None, optional) – Which elements of a vector response are constrained.

  • linear (bool, optional) – Whether the constraint is linear in the design variables. Default False.

  • regulation (str, optional) – Optional provenance. Supplying both is what puts a constraint in the traceability matrix as a defensible requirement rather than as a bare number; leaving them out is allowed, and the matrix says so.

  • source (str, optional) – Optional provenance. Supplying both is what puts a constraint in the traceability matrix as a defensible requirement rather than as a bare number; leaving them out is allowed, and the matrix says so.

  • title (str, optional) – One line naming the constraint in a report. Defaults to the variable name.

ALLOWED = ('name', 'lower', 'upper', 'equals', 'units', 'indices', 'linear', 'regulation', 'source', 'title', 'ref', 'ref0', 'scaler', 'adder')
classmethod from_section(section)[source]

Build one constraint entry.

Parameters:

section (CaseFileSection)

Return type:

ConstraintSpec

property name: str

Response name or black-box path being constrained.

property bounds: Bounds

The bound imposed.

property units: str | None

Units the bounds are stated in.

property scaling: Scaling

Driver scaling for this constraint.

property indices: list[int] | None

Which elements of a vector response are constrained.

property linear: bool

Whether the constraint is linear in the design variables.

property regulation: str

The regulation this constraint comes from, if it was given one.

property source: str

Where the number came from, if it was given.

property title: str

One line naming the constraint in a report.

property is_traceable: bool

Whether this constraint names both a regulation and a source.

class cdadt.config.DriverConfig(name='SLSQP', maxiter=50, tol=1e-06, derivative_mode='fwd', options=None)[source]

Bases: object

Which optimizer to use, and how hard to drive it.

Parameters:
  • name (str)

  • maxiter (int)

  • tol (float)

  • derivative_mode (str)

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

ALLOWED = ('name', 'maxiter', 'tol', 'derivative_mode', 'options')
MODES = ('auto', 'fwd', 'rev')
classmethod from_section(section)[source]

Build from the driver section.

Parameters:

section (CaseFileSection)

Return type:

DriverConfig

property name: str

Optimizer name.

property maxiter: int

Optimizer iteration limit.

property tol: float

Optimizer convergence tolerance.

property derivative_mode: str

Total-derivative mode.

property options: Mapping[str, Any]

Extra optimizer settings, passed through to pyOptSparse.

class cdadt.config.ObjectiveSpec(name, units=None, scaling=None, sense='minimize', index=None)[source]

Bases: object

What the optimizer minimizes or maximizes.

Parameters:
  • name (str) – A response name or a raw black-box path.

  • units (str or None, optional) – Units to optimize in.

  • scaling (Scaling, optional) – Driver scaling. It matters: SciPy’s 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.

  • sense (str, optional) – "minimize" or "maximize". Default "minimize".

  • index (int or None, optional) – Which element of a vector output is the objective.

ALLOWED = ('name', 'units', 'sense', 'index', 'ref', 'ref0', 'scaler', 'adder')
SENSES = ('minimize', 'maximize')
classmethod from_section(section)[source]

Build from the objective section.

Parameters:

section (CaseFileSection)

Return type:

ObjectiveSpec

property name: str

Response name or black-box path being optimized.

property units: str | None

Units the objective is optimized in, if the case file fixed them.

property scaling: Scaling

Driver scaling for the objective.

property sense: str

"minimize" or "maximize".

property index: int | None

Which element of a vector output is the objective.

property direction: float

Return +1 to minimize or -1 to maximize.

as_kwargs(units)[source]

Return the keyword arguments OpenMDAO’s add_objective takes.

The sense is folded into the scaling, because OpenMDAO drivers only minimize. Both spellings are handled, so a case file may scale with ref or with scaler and still say sense: maximize.

Parameters:

units (str | None)

Return type:

dict[str, Any]

class cdadt.config.OptimizeSpec(bounds, scaling=None, indices=None)[source]

Bases: object

The optimize: entry that frees a design variable for the driver.

Parameters:
  • bounds (Bounds) – The interval the variable may move in.

  • scaling (Scaling, optional) – Driver scaling. Defaults to ref at the larger bound magnitude, which is nearly always the right order 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.

  • indices (sequence of int or None, optional) – Which elements of a vector variable are free. None frees all of them.

ALLOWED = ('lower', 'upper', 'indices', 'ref', 'ref0', 'scaler', 'adder')
classmethod from_section(section)[source]

Build one optimize: entry.

Parameters:

section (CaseFileSection)

Return type:

OptimizeSpec

property bounds: Bounds

The interval the variable may move in.

property lower: Any

Lower bound.

property upper: Any

Upper bound.

property scaling: Scaling

Driver scaling for this variable.

property indices: list[int] | None

Which elements are free, or None for all of them.

property ref: float

The reference this variable is scaled by, defaulted from its bounds if not given.

as_kwargs(units)[source]

Return the keyword arguments OpenMDAO’s add_design_var takes.

Parameters:

units (str | None)

Return type:

dict[str, Any]

class cdadt.config.Scaling(ref=None, ref0=None, scaler=None, adder=None)[source]

Bases: object

How a quantity is scaled for the driver: ref/ref0 or scaler/adder.

OpenMDAO accepts either pair and refuses both, because they are two spellings of the same affine map. cdadt refuses both here instead, where the offending key can be named.

Parameters:
  • ref (float | None)

  • ref0 (float | None)

  • scaler (float | None)

  • adder (float | None)

ALLOWED = ('ref', 'ref0', 'scaler', 'adder')
classmethod from_section(section)[source]

Build from whichever of the four keys an entry carries.

Scaling shares its block with whatever else the entry declares – bounds, indices, a regulation – so this reads the four keys it owns and leaves the rest to the caller.

Parameters:

section (CaseFileSection)

Return type:

Scaling

property ref: float | None

Value the driver should see as one, if given.

property ref0: float | None

Value the driver should see as zero, if given.

property scaler: float | None

Multiplicative scaling, if given.

property adder: float | None

Additive offset, if given.

property is_empty: bool

Whether no scaling at all was given.

as_kwargs(default_ref=None)[source]

Return the keyword arguments OpenMDAO takes, omitting anything not given.

Parameters:

default_ref (float | None)

Return type:

dict[str, float]

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

Bases: object

How hard to converge the black box. Defaults are OpenConcept’s own sizing settings.

Parameters:
  • maxiter (int)

  • atol (float)

  • rtol (float)

  • iprint (int)

  • err_on_non_converge (bool)

ALLOWED = ('maxiter', 'atol', 'rtol', 'iprint', 'err_on_non_converge')
classmethod from_section(section)[source]

Build from the solver section, which may be empty.

Parameters:

section (CaseFileSection)

Return type:

SolverConfig

settings()[source]

Return the SolverSettings this section describes.

Return type:

SolverSettings

property maxiter: int

Newton iteration limit.

class cdadt.config.VariableSpec(name, value, units=None, source='', optimize=None)[source]

Bases: object

One entry of design_variables or initial_conditions.

Parameters:
  • name (str) – Name of a variable the black box publishes.

  • value (float or sequence of float) – Its value. A sequence is a per-node schedule: two entries are interpolated across the phase exactly as np.linspace does in OpenConcept’s own run script.

  • units (str or None, optional) – Units of the value.

  • source (str, optional) – Where the number came from. A number with no provenance is indistinguishable from a guess in the report that quotes it.

  • optimize (OptimizeSpec or None, optional) – Present makes this variable a design variable the driver may move.

ALLOWED = ('value', 'units', 'source', 'optimize')
classmethod from_section(name, section)[source]

Build one entry.

Parameters:
Return type:

VariableSpec

property name: str

Name of the black-box variable.

property value: Any

Its value, in units.

property units: str | None

Units of the value.

property source: str

Where the number came from.

property optimize: OptimizeSpec | None

The optimize: entry, if this variable is free.

property is_free: bool

Whether the driver may move this variable.

parameter()[source]

Return this entry as a Parameter.

Only meaningful for a scalar; a per-node schedule is written straight into the box rather than routed through a discipline.

Return type:

Parameter