Architecture
Every piece of cdadt is a class with encapsulated state. There is no global state, no module-level cache, and no free function that does discipline work. This page says what the classes are, what each owns, and why the division is drawn where it is.
The layers
CommandLineInterface composes Command subclasses; owns the parser and the dispatch
| SizeCommand / OptimizeCommand are StudyCommands
| InspectCommand reads the interface without running
|
Config one YAML case file, validated hard
| read through CaseFileSection, which carries its own address
|
+-- Aircraft composes the airframe disciplines, routes every ac| name
| +-- Geometry wing, fuselage, nacelles, gear
| +-- Aerodynamics span efficiency, airfoil, flaps, envelope
| +-- Propulsion engine rating and count
| +-- Stability empennage shape, tail areas
| +-- Structures load-carrying airframe mass breakdown
| +-- Weights payload, cabin, the closed weight rollup
|
+-- Performance owns the InitialConditions and the ContinuationLadder
|
+-- OpenConceptSizingBox the black box: loaded by name, set, converged, read
| RunDirectory decides where its problem writes
|
+-- cdadt.models physics cdadt owns. OpenMDAO and numpy only; imports no dependency
| AerodynamicLoads the pluggable slot -- coefficients from a flight condition
| PolarLoads a parabolic polar; reproduces the reference exactly
| TrapezoidalPlanform the case file's four wing numbers -> lifting surface
|
+-- cdadt.adapter the only package that may import a dependency
AerodynamicLoadsComp evaluates a loads model over a phase, publishes `drag`
DifferentiableLattice openavl, and the exact geometry Jacobian
OpenAVLLoads that lattice as an AerodynamicLoads
WingSectionsComp the planform as sections, for OpenConcept's wave drag
CdadtAircraftModel cdadt's drag + OpenConcept's engine and weights
SizingMissionAnalysis installs it into FullMissionWithReserve
SizingAnalysis builds, converges, reads -> SizingResults
| the coordinator; every collaborator is injectable
Optimizer declares design variables, objective and CertificationBasis,
| converges a baseline, drives -> OptimizationOutcome
|
StudyArtifacts writes the record into the run directory
MissionTrajectory the seven steady phases, discovered and ordered
TakeoffTrajectory the balanced field: the continued and rejected paths
The command line is at the top rather than off to one side because it is the only caller that
supplies a RunDirectory, and therefore the only one that causes anything
to be written to disk in a place of cdadt’s choosing. See
Output files.
The arrangement is open/closed in four places, each tested rather than asserted: a new
discipline is a new Discipline passed to Aircraft, a new
command is a new Command passed to CommandLineInterface, a new black
box is a different model: line in the case file, and a new aerodynamics is a new
AerodynamicLoads named in that case file’s options. None of the four
requires editing anything that already exists.
One run, end to end
What actually happens when cdadt size cases/b738.yaml is typed. Read it downwards; the only loop
is the one marked, and it is the weight closure.
cases/b738.yaml
|
| Config.from_dict -- every section validated, unknown keys refused
v
+-------------------------------------------------------------------------+
| SizingAnalysis the coordinator; collaborators injected |
| |
| Aircraft ---> 7 disciplines, each owning one slice of the ac| names |
| Performance -> initial conditions + the continuation ladder |
| CertificationBasis -> the regulations this study is judged against |
+-------------------------------------------------------------------------+
| writes ac| values, seeds, solver settings
v
+-------------------------------------------------------------------------+
| OpenConceptSizingBox loaded by name from the case file |
| |
| +------------------------------------------------------------+ |
| | the analysis group (OpenConcept's, or cdadt's own) | |
| | | |
| | geometry -> tails, wetted areas, MAC | |
| | maximum lift -> CLmax clean and flapped | |
| | empty weight -> OEW | |
| | | |
| | FullMissionWithReserve | |
| | v0v1 v1v0 v1vr rotate | climb cruise descent | reserve | |
| | balanced field | design mission | loiter | |
| | | |
| | each phase instantiates ONE aircraft model per phase: | |
| | B738AircraftModel (OpenConcept's) | |
| | CdadtAircraftModel (cdadt's -- see below) | |
| +------------------------------------------------------------+ |
| ^ | |
| '---- Newton: MTOW <-> fuel ---' <== the one loop |
+-------------------------------------------------------------------------+
| read back by name, in declared units
v
SizingResults -> ResponseCatalog -> report.txt, results.json, 3 figures, n2.html
Inside one phase, when the aerodynamics is cdadt’s:
phase gives: fltcond|CL fltcond|q fltcond|M fltcond|h throttle ac|...
| | | | |
+---------------+----------+----------+----------+ |
| |
v v
+--------------------------------+ +------------------------+
| cdadt: the drag | | OpenConcept: the rest |
| | | |
| ParasiteDragCoefficient CD0 | | RubberizedTurbofan |
| [+ WaveDragFromSections] --. | | -> thrust, fuel flow|
| v | | Integrator -> fuel |
| AerodynamicLoadsComp | | AddSubtract -> weight |
| builds a planform | +------------------------+
| builds the loads model | |
| model.coefficients() -> CD | |
| drag = CD * q * S | |
+--------------------------------+ |
| |
v v
drag thrust, weight
'----------> back to the phase <-----------'
The loads model itself is chosen by the case file and knows nothing about any of this:
AerodynamicLoads (cdadt.models -- imports no dependency)
|
+-- PolarLoads CD = CD0 + CL^2 / (pi e AR)
|
+-- OpenAVLLoads (cdadt.adapter) -.
| >-- LatticeSolver
+-- OpenAeroStructLoads (cdadt.adapter) -' |
+-- DifferentiableLattice --> openavl
+-- OpenAeroStructLattice --> OpenConcept -> OpenAeroStruct
|
LatticeLibrary: solved polars,
one per study, injected, never global
Why the physics is split in two
cdadt.models and cdadt.adapter look like one layer and are deliberately two.
A discipline is an interface to the box: it owns a slice of the variable namespace, computes nothing, and that invariant is what the ownership tests enforce. A model is physics installed into the box: it computes, and it is chosen per study. Neither could absorb the other without losing the property that makes it useful.
The split inside the physics is by dependency, not by subject. cdadt.models imports neither
OpenConcept nor openavl, so a loads model can be written, differentiated and tested on a machine
with neither installed – and a contract test enforces it. cdadt.adapter is the one package
allowed to import a dependency, because installing cdadt’s aerodynamics into OpenConcept’s mission
means composing OpenConcept’s propulsion and weight blocks around it, and an aircraft model must be
an OpenMDAO group OpenConcept instantiates. That exemption is one package wide and is the whole of
it. See Supplying your own aerodynamics for the layer itself and What is checked against what for what each part is validated
against.
What a discipline is
A discipline is one engineering domain, modelled as a class, and it owns exactly two things:
The parameters it puts into the box. Which ac| variables belong to its domain, their
values, their units and their provenance. Held as Parameter objects,
reachable only through methods that validate what they are given – a non-finite value is
rejected at the point of assignment rather than surfacing later as an unexplained convergence
failure.
The responses it reads back out. Which quantities the box produces belong to its domain,
where they live inside it, and in what units to read them. Held as
Response objects, which is what allows the whole interface to be
listed without running anything.
What a discipline is not
It does not compute physics. There is no compute, no residual and no partial derivative in
any of them, because the aerodynamics, the weight correlations, the engine deck, the tail
sizing and the entire mission are computed inside the black box by OpenConcept. A cdadt
discipline is the encapsulated, validated interface to its slice of that box.
This is stated bluntly because the alternative is worse. A cdadt.disciplines.aerodynamics
module that looked like it modelled aerodynamics, in a tool whose results are quoted in a
thesis, would be a page of documentation away from a false claim. It sets the inputs to somebody
else’s drag buildup and reads two lift coefficients back.
Discipline is an abstract base, and discipline_name is
genuinely abstract – it is the one thing no domain can inherit, being the key it is reported and
looked up under. A subclass that omits it is refused when the class is written, by
__init_subclass__, rather than surfacing later as a duplicate-name error from Aircraft or
not at all. Nothing else is abstract, because a subclass overrides no methods: it declares three
class attributes and that is the whole contract.
Certification is a domain, and is not a discipline
CertificationBasis encapsulates the certification domain, and
SizingAnalysis owns it alongside the disciplines. It is deliberately
not a Discipline, and the reason is the definition above: a
discipline owns a slice of the black box’s variable interface – variables it sets, responses
it reports. Certification sets nothing and publishes nothing. It reads quantities the other
disciplines already report and judges them against stated limits, which is a different
relationship to the box, and forcing it into the same base class would describe it wrongly: it
would appear in the ownership map owning nothing and in the results table reporting nothing.
It is owned by the analysis rather than by Optimizer, which used to
build it. That mattered in practice: asking whether a sized aeroplane meets its certification
basis required constructing an optimizer that was never going to run. A sizing study can now
answer it directly, and a test does exactly that.
Ownership, and why it is checked
Each discipline declares the variables it owns as shell-style patterns:
Geometry |
|
Aerodynamics |
|
Propulsion |
|
Stability |
|
Structures |
nothing |
Weights |
|
Performance |
|
Aircraft routes each declared parameter to the one discipline that
claims it, and refuses a parameter that no discipline owns or that two would claim. A contract
test then walks the whole settable set of a built box – 241 variables for the shipped case,
read off the live model rather than from a list – and asserts that every one of them has
exactly one owner. A variable cdadt could set but no discipline claims is a hole in the model
of the interface, whether or not any case file happens to set it.
Two of those rows deserve explanation.
Structures owns nothing, and that is the honest answer. Primary structure is sized inside the box by correlations that read geometry, maximum takeoff weight and maximum landing weight – all owned elsewhere or computed by the box. There is no structural parameter left for cdadt to set: no material, no load factor, no spar layout. Inventing one so that the class had state would be a parameter nothing reads. What it does own is the reporting of structure: the seven component masses, so that a weight figure can be argued with rather than only quoted.
The empennage is Stability, not Geometry. Its areas are outputs, not inputs – the box sizes both surfaces by tail volume coefficient – so the shape parameters and the resulting areas belong together, in the domain the method belongs to.
Performance is the odd one
Performance is the one discipline whose encapsulated
state is not a bag of scalars. It owns the pair OpenConcept’s own run scripts write by hand: the
InitialConditions – everything set_values(prob, num_nodes) writes,
which is the design range, the cruise altitude, the reserve mission, the per-phase schedules and
the solver’s starting guesses – and the ContinuationLadder that makes
the hard ones reachable. Those are design inputs in exactly the sense the geometry parameters
are: change the range and you change the aeroplane. See The mission.
It is also the discipline the certification constraints are written against: field length, decision and safety speeds, engine-out climb gradient, fuel, and the throttle history of every phase.
Adding a discipline
Subclass Discipline, declare what it owns and what it reports,
and pass it to Aircraft:
from cdadt import Aircraft, Discipline, Response
from cdadt.disciplines import AIRCRAFT_DISCIPLINES
class Cost(Discipline):
"""Acquisition and operating cost, if the box publishes any."""
discipline_name = "cost"
description = "Acquisition and operating cost"
owned_patterns = ("ac|cost|*",)
reported = (Response("acquisition_cost", "costs.acquisition", "USD", optional=True),)
aircraft = Aircraft(parameters, disciplines=(*AIRCRAFT_DISCIPLINES, Cost))
Nothing else changes. The ownership check will immediately tell you if the new patterns overlap
an existing discipline’s, and collect() will tell you
if the box does not publish what the new discipline claims to report.
The rules, and the tests that enforce them
Rule |
Test that enforces it |
|---|---|
No cdadt module imports OpenConcept |
|
No cdadt class inherits from OpenConcept |
|
The OpenConcept working tree is untouched |
|
No local OpenConcept commit affects a module cdadt loads |
|
Every settable variable of the box has exactly one owner |
|
Every required response exists in the box |
|
The base |
|
Design decisions worth knowing
Values are validated where they are set, not where they are used. A
Parameter refuses a non-numeric or non-finite value in its setter. A
NaN that reaches OpenMDAO propagates silently through a Newton solve and emerges as a
convergence failure with no attribution.
Case files reject unknown keys. A silently ignored key is the failure mode a configuration file is most prone to: the run succeeds and answers a different question than the one that was asked.
Optimizations are checked against a cheap probe of the box before they start. A small build
costs a fraction of a second and knows every name the box publishes, so a misspelled design
variable is a message with suggestions rather than an OpenMDAO error thrown out of setup.
Results are read whole. Every discipline’s responses are collected on every run, not whichever few a caller asked for. That is what makes a run report complete and two runs comparable without re-running either.