Tutorials
Six things a study actually needs to do, in increasing order of how much code they take. The first four take none.
1. Change the aircraft
Copy cases/b738.yaml, edit the numbers, run it. Everything in design_variables is a
design parameter, and the black box is a sizing model – empirical weight and drag buildups
and a rubberized engine – so a changed parameter produces a consistent clean-sheet design
rather than an inconsistent 737.
design_variables:
ac|geom|wing|AR:
value: 11.5 # was 9.45
source: Design study, higher aspect ratio wing
ac|propulsion|engine|rating:
value: 24.0e3 # was 27.0e3
units: lbf
source: Design study, reduced thrust
cdadt size cases/my_aircraft.yaml
Keep the source field truthful as you edit. It is what separates a design decision from a
leftover.
If a parameter name is wrong, cdadt says so before anything runs, and suggests the nearest
match. cdadt inspect lists everything the box accepts.
2. Change the mission
initial_conditions:
mission_range: {value: 3500, units: nmi} # was 2800
cruise|h0: {value: 37000, units: ft} # was 35000
Watch the continuation ladder when you make the mission substantially harder. Its job is to walk the solver from something easy to the design mission, and the shipped ladder was built for a 2800 nmi mission at FL350. If a run fails to converge, add a rung:
continuation:
- description: short range at low altitude, gentle descent
initial_conditions:
mission_range: {value: 500, units: nmi}
cruise|h0: {value: 5000, units: ft}
reserve_range: {value: 100, units: nmi}
reserve|h0: {value: 1000, units: ft}
descent.fltcond|vs: {value: -800, units: ft/min}
- description: half range at intermediate altitude # <- new rung
initial_conditions:
mission_range: {value: 1800, units: nmi}
cruise|h0: {value: 25000, units: ft}
descent.fltcond|vs: {value: -800, units: ft/min}
- description: design range and altitude, gentle descent
initial_conditions:
descent.fltcond|vs: {value: -800, units: ft/min}
A rung overrides only what it names; the case’s own initial_conditions are re-applied
underneath it every time. Run with -v to watch each rung as it converges. See The mission.
3. Free a design variable
A sizing case becomes an optimization by adding an objective and putting optimize: on the
variables the driver may move. The variable stays exactly where it was declared:
design_variables:
ac|geom|wing|AR:
value: 9.45
source: b737.org.uk technical specifications
optimize: {lower: 7.0, upper: 13.0} # <- the only line added
objective: {name: total_fuel, units: kg, sense: minimize, ref: 2.0e4}
Delete the optimize: line and the variable is fixed again at the same value, from the same
source. Nothing moves between sections, so a sizing run and an optimization of the same aeroplane
cannot drift apart.
4. Ask a different question, and add a constraint
To minimize maximum takeoff weight instead of fuel, over span alone, on a shorter runway:
driver: {name: IPOPT, maxiter: 40, tol: 1.0e-6, derivative_mode: fwd}
objective: {name: MTOW, units: kg, sense: minimize, ref: 8.0e4}
constraints:
- name: takeoff_field_length
upper: 7000.0
units: ft
regulation: 14 CFR 25.113
source: Shorter runway, 7000 ft dry at sea level, ISA
title: Balanced field length within the runway available
Any quantity the disciplines report can be constrained the same way – there is no requirement type to look up and no Python to write, because a constraint is a bound on a named response plus its provenance:
- name: MLW
upper: 66360.0
units: kg
regulation: 14 CFR 25.473
source: Boeing 737-800 certificated maximum landing weight
title: Landing weight within the structural limit
- name: total_fuel
upper: 20000.0
units: kg
regulation: design
source: Usable fuel volume of the wing box as laid out
regulation: design is the honest label for a programme decision or a modelling limit rather
than a rule. Leave both fields out entirely and the constraint still applies – it is reported as
a design constraint rather than as certification evidence. See The certification basis.
If the quantity you need is not something the black box publishes, stop. Adding a calculation to cdadt to produce it is exactly what the boundary exists to prevent; the gap belongs in Validation.
5. Add a discipline
from typing import ClassVar
from cdadt import Aircraft, Config, Discipline, Response, SizingAnalysis
from cdadt.disciplines import AIRCRAFT_DISCIPLINES
class Cost(Discipline):
"""Acquisition and operating cost, as far as the box reports it."""
discipline_name: ClassVar[str] = "cost"
description: ClassVar[str] = "Acquisition and operating cost"
owned_patterns: ClassVar[tuple[str, ...]] = ("ac|cost|*",)
reported: ClassVar[tuple[Response, ...]] = (
Response("acquisition_cost", "costs.acquisition", "USD", optional=True),
)
Declare what it owns and what it reports; that is the whole interface. The ownership check will
tell you immediately if the patterns overlap an existing discipline’s, and
collect() will tell you if the box does not publish
what the discipline claims to report.
Remember what a discipline is and is not: it owns the interface to its domain, not the physics. If your new class starts computing something, it is in the wrong repository.
6. Drive a different black box
cdadt is not tied to the B738 case. Any OpenConcept group that takes num_nodes and exposes
its design parameters as independent variables can be named in a case file:
black_box:
model: my_package.my_sizing:MySizingAnalysis
num_nodes: 11
Then run cdadt inspect against it first. Whatever it publishes is what cdadt can set,
constrain and report; the ownership patterns of the existing disciplines will route the
ac|-named ones, and anything else needs a discipline of its own – which the ownership check
will point out by name.