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.