Output files ============ A run that prints its numbers and exits leaves nothing to attach to a design review. **Every** study writes a complete record -- there is no flag to remember, because a record that depends on remembering a flag is a record you find out you do not have afterwards: .. code-block:: bash cdadt size cases/b738.yaml cdadt optimize cases/b738_optimization.yaml Each invocation gets its own directory under ``run_outputs/``, named for the case and the moment it ran, so repeated runs of one case accumulate instead of overwriting each other:: run_outputs/ b738_20260728_201512_out/ report.txt results.json n2.html <- cdadt mission.pdf trajectory.pdf takeoff.pdf <- cdadt .openmdao_out reports/ <- OpenMDAO b738_optimization_20260728_201551_out/ ... the same, plus IPOPT.out <- the driver .. list-table:: :header-rows: 1 :widths: 24 76 * - File - What it is * - ``n2.html`` - OpenMDAO's N2 diagram of the built model: every component, every connection, every solver. Generated from the live problem, so it is the model that was actually run rather than a drawing of one. * - ``mission.pdf`` - The mission profile, reproducing ``B738_sizing.py``'s own ``plot_results``. * - ``trajectory.pdf`` - The trajectory, reproducing ``B738.py``'s ``show_outputs``. * - ``takeoff.pdf`` - The balanced field: the continued takeoff and the rejected one. * - ``report.txt`` - The same text the command printed. * - ``results.json`` - The same numbers as structured data. For an optimization this is the full record: baseline, optimum, and every constraint with its provenance, margin and status. * - ``.openmdao_out``, ``reports/`` - OpenMDAO's own, not cdadt's: its N2, its inputs report, and for an optimization its driver-scaling and optimizer reports. * - ``IPOPT.out`` - The optimizer's own log, written by pyOptSparse. Where the directory comes from ------------------------------ cdadt does not invent this layout. OpenMDAO already has it: a :class:`openmdao.api.Problem` given a ``name`` and a ``work_dir`` writes into ``/_out``, and everything downstream follows it -- its reports, and the optimizer's log, since pyOptSparse writes ``IPOPT.out`` into the directory the problem set up in. So :class:`~cdadt.blackbox.RunDirectory` names the problem and says where the work directory is, and then cdadt writes its own six files into the directory OpenMDAO made. Both halves of the record land in one place because they are the *same* place, not because anything is collected afterwards. The ``_out`` suffix is OpenMDAO's convention, kept rather than fought. ``--run-outputs `` moves the root; ``--json `` additionally writes the numbers somewhere of your choosing. ``run_outputs/`` and ``*_out/`` are both in ``.gitignore``, since every file in them is regenerated by one command. Two mission figures, because there are two reference figures ------------------------------------------------------------- OpenConcept plots the mission twice, and differently. ``B738.py``'s ``show_outputs`` draws seven panels; ``B738_sizing.py``'s ``plot_results`` draws a 2x3 of different quantities, one of which carries two series. cdadt drives ``B738_sizing.py``, so *that* is the figure it must reproduce -- but reproducing only it would drop the fuel, airspeed and lift-coefficient panels the other example considers worth showing. Both are written rather than choosing which example to be faithful to. ``mission.pdf`` -- ``B738_sizing.py``, ``plot_results``: .. list-table:: :header-rows: 1 :widths: 34 66 * - Panel - Variables * - Altitude (ft) - ``fltcond|h`` * - Mach number - ``fltcond|M`` * - Vertical speed (ft/min) - ``fltcond|vs`` * - Weight (lb) - ``weight`` * - Longitudinal force (lb) - ``drag`` and ``thrust``, two series on one axis * - Throttle (%) - ``throttle``, as a percentage ``trajectory.pdf`` -- ``B738.py``, ``show_outputs``: .. list-table:: :header-rows: 1 :widths: 30 16 54 * - Variable - Units - Panel * - ``fltcond|h`` - ft - Altitude * - ``fltcond|Ueas`` - kn - Equivalent airspeed * - ``fuel_burn_integ.fuel_burn`` - lbm - Fuel burned * - ``throttle`` - -- - Throttle setting * - ``fltcond|vs`` - ft/min - Vertical speed * - ``fltcond|M`` - -- - Mach number * - ``fltcond|CL`` - -- - Lift coefficient All against ``range`` in nautical miles. One difference from ``B738.py``, and it is only in where the number is read from: it plots ``fuel_used``, which is a promoted *input* of each phase and therefore not unambiguously readable -- a promoted input name can address several components at once, which is why :meth:`~cdadt.blackbox.OpenConceptSizingBox.readable` omits them. What the box publishes as an output is each phase's fuel integrator, and each phase's carries on from the end of the one before rather than restarting at zero. So the panel is cumulative mission fuel, the same curve ``B738.py`` draws. That is an identity, not an observation: the response catalogue defines ``total_fuel`` as ``mission.loiter.fuel_burn_integ.fuel_burn_final`` -- the last node of the last phase of this very trace -- and ``block_fuel`` as the same thing at the end of ``descent``. Both are therefore readable straight off the panel. On the shipped case the trace runs from 179.31 lbm at the first climb node, the takeoff roll already in it, to 41,000.07 lbm at the end of the loiter, which is ``total_fuel`` = 18,597.3177 kg. The phases are discovered, and ordered by where they happen ----------------------------------------------------------- Two questions, and neither is answered with a list of names. **Which subsystems are phases.** A steady flight phase integrates its own state and so publishes ``ode_integ_phase.range_final``. The ground-roll phases -- ``v0v1``, ``v1vr``, ``rotate`` and the rejected takeoff ``v1v0`` -- publish ``range`` but not that, so they are not part of the flown trajectory. Keeping ``v1v0`` out matters: it retraces ground the aircraft has already covered, and drawn on a range axis it is a trajectory that doubles back. It gets :ref:`its own figure ` instead, where doubling back is the point. The discriminator is *published as an output*, which means :meth:`~cdadt.blackbox.OpenConceptSizingBox.readable` and not :meth:`~cdadt.blackbox.OpenConceptSizingBox.has`. The two disagree here, and only on the phases this has to exclude: ``has`` falls back to ``get_val``, and ``v0v1``, ``v1vr`` and ``v1v0`` all answer it for the marker while publishing no such output. A test holds that distinction, because the failure it prevents is a plot that still looks like a plot. **What order they are flown in.** Not alphabetical -- that puts loiter before the reserve climb. Not the order the box lists them in either, since it publishes its outputs sorted by name. Phases are ordered by the abscissa itself: where each one starts. For the shipped case that recovers climb, cruise, descent, the three reserve phases, then loiter, and it is a fact read off the converged mission rather than a convention written down here. Nothing about those seven is written down in cdadt, which is the same reason the black box itself is named in the case file rather than imported. A mission model with a second diversion, or without a loiter, plots correctly with nothing changed here. A quantity that only *some* phases publish is dropped from the figure rather than drawn with a gap, because a trajectory drawn across a hole reads as a trajectory rather than as missing data. Asking for one by name instead raises, naming the phase that does not publish it. .. note:: Read the trajectory off a **converged** box. Reading one mid-solve produces a plot of a state the solver was passing through, and nothing about the picture says so. The command line only writes artefacts after the run has converged; the classes will do as they are told. .. _takeoff-figure: The takeoff, which neither example plots ----------------------------------------- ``takeoff.pdf`` has no counterpart in ``B738.py`` or ``B738_sizing.py``. It is here because the takeoff is where the field length *comes from*, and "balanced" is a claim about two paths that a number in a table states but cannot show. From brake release the aeroplane accelerates to V\ :sub:`1` (``v0v1``). There it either continues -- accelerating to rotation and lifting off, ``v1vr`` then ``rotate`` -- or rejects and brakes to a stop (``v1v0``). Both are drawn against ground distance, as true airspeed, altitude, throttle and weight. On the shipped case the two paths lie on top of each other to V\ :sub:`1` = 135.1 kn at about 3,150 ft, then separate: the continued takeoff accelerates to V\ :sub:`2` and climbs through the 35 ft screen height, while the rejected one chops the throttle to zero and decelerates. **Both end at about 5,250 ft** -- which is the reported balanced field length of 5,247.79 ft. That coincidence is the definition of balanced, and it is visible rather than asserted. A test holds it, comparing both path ends against ``mission.bfl.distance_continue``. This is also why the ground roll cannot simply be prepended to the mission figure: on a range axis the rejected takeoff runs back over ground the aeroplane has already covered. Unlike the mission phases, these four are **named rather than discovered**. Which phase is the continued takeoff and which is the rejected one is semantics, and no output the box publishes carries it. The names are OpenConcept's own, and a box without them raises rather than guessing. From Python ----------- Pass a :class:`~cdadt.blackbox.RunDirectory` to get the same layout the command line produces, including OpenMDAO's own files: .. code-block:: python from cdadt import Config, RunDirectory, SizingAnalysis, StudyArtifacts run = RunDirectory.for_case("cases/b738.yaml", stamp="20260728_193000") analysis = SizingAnalysis(Config.from_yaml("cases/b738.yaml"), run=run) results = analysis.run() artifacts = StudyArtifacts(run.path) artifacts.write_text(results.report(analysis.catalog), "report.txt") artifacts.write_json(results.to_dict(), "results.json") artifacts.write_n2(analysis.box) artifacts.write_mission_profile(analysis.box, title="B738 design mission") artifacts.write_trajectory(analysis.box, title="B738 design mission") artifacts.write_takeoff(analysis.box, title="B738 design mission") The stamp is passed in rather than read from a clock, so a study can be reproduced into a named directory and a test can assert on the path. Omitting ``run`` entirely -- the default -- builds the problem with reports off, so nothing is written when it is *built*. A driver is the exception: pyOptSparse writes ``IPOPT.out`` into the problem's output directory and OpenMDAO creates that directory on demand, which is exactly why the command line always supplies a run directory rather than leaving an optimization's log wherever it was run from. Matplotlib is an optional dependency, since only the figures need it: .. code-block:: bash pip install -e ".[plot]" Without it, everything else still works and the plot raises an :class:`~cdadt.artifacts.ArtifactError` naming the extra to install.