A GE F404 twin-spool, low-bypass, mixed-flow, afterburning turbofan cycle model with design-point sizing and off-design flight sweeps, built on NASA Glenn's pyCycle and OpenMDAO.
Given a target thrust and component parameters (fan and HPC pressure ratios, component efficiencies, cooling bleed fractions), the model sizes the engine at sea-level-static conditions and sweeps altitude, ambient temperature offset, and throttle to generate a converged off-design performance deck.
Status: In development. One sized engine, with DESIGN at sea-level-static
dry mil power, operated both dry (military power and below) and wet
(afterburning) on two off-design points of the same model. Also:
altitude/dTs/throttle sweep infrastructure, and an f404 command line for
sweeps and single design points. See Current status for
convergence coverage and Roadmap for planned work. Not yet
validated against public F404 performance data; the public reports are
archived and indexed in
docs/unclassified_perf_data/.
- Repo layout
- Cycle architecture
- Software architecture and data flow
- Installation
- Usage
- Testing
- Current status
- Roadmap
- Acknowledgments
- License
F404 application code lives under src/f404/, separate from the
vendored upstream pycycle library at the repo root (see
#4).
| Path | Role |
|---|---|
src/f404/engine_model.py |
MixedFlowTurbofan(pyc.Cycle): single-point thermodynamic cycle (fan, HPC, burner, HPT/LPT, mixer, afterburner, nozzle) |
src/f404/mp_cycle.py |
MPMixedFlowTurbofan(pyc.MPCycle): one DESIGN point (dry mil) feeding two off-design points, OD_dry and OD_wet, with map scalars and station areas |
src/f404/problems.py |
build_problem(): design targets, the max-AB calibration, initial guesses and input validation |
src/f404/sweep_utils.py |
Sweep infrastructure: snake-pattern sweep grid, bridge-point warm-starting, SweepRunner, result extraction |
src/f404/cli.py |
f404 command line: sweep and design subcommands, min,max,step range parsing and validation |
src/f404/sweep_full_envelope.py |
Sweep driver: default grid, run_sweeps() for dry, wet, or both modes, and the cycle-deck CSV writer |
src/f404/printer.py |
Console table formatter for DESIGN/OD results |
tests/ |
pytest suite, one <module>_test.py per module (#5) |
deck/ |
Cycle-deck output CSVs |
docs/ |
System architecture diagrams (f404_cycle.typ, f404_cycle.svg), architecture decision records (adr/), planning docs (improvements/), and reference material (unclassified_perf_data/: an index and cross-study reference map of published F404 performance data, with source PDFs in pdf/ and Obsidian-compatible markdown conversions with their figures in md/) |
AGENTS/ |
Session handoff notes |
meta/ |
Vendored-library provenance (LICENSE.txt) |
pycycle/, setup.py, pyproject.toml, example_cycles/ |
Vendored upstream pyCycle library |
The thermodynamic cycle model in engine_model.py (MixedFlowTurbofan) represents the twin-spool, mixed-flow, augmented F404 turbofan engine:
Diagram source maintained in docs/f404_cycle.typ.
Key cycle components and mechanical couplings:
- Low Pressure (LP) Spool: 3-stage fan driven by the single-stage LP turbine via
lp_shaft(10,000 rpm). - High Pressure (HP) Spool: 7-stage HP compressor driven by the single-stage HP turbine via
hp_shaft(14,000 rpm, 250 hp customer power extraction). - Cooling Bleeds: HPC interstage bleed (
cool1, 5.07% flow) cools the LPT; compressor discharge bleed (cool3, 11.0% flow) cools the HPT. - Mixed Exhaust & Augmentor: Core flow and bypass flow mix in a confluent mixer, then pass through the afterburner duct and expand through a variable convergent-divergent nozzle (
mixed_nozz). The afterburner is an active combustor in wet mode, with a 5.4% hot pressure loss, and a lossless pass-through in dry mode. - One engine, two modes: the engine is sized once, at SLS dry mil (11,000 lbf, Tt4 3100 R). Dry points run the DESIGN nozzle throat (A8). Wet points open A8 to hold the fan on its design operating line, so lighting the afterburner leaves the gas generator where dry mil put it. Max AB is Tt7 3608 R, calibrated to 17,700 lbf SLS. See ADR-0001 for the trade study behind this.
Current data flow from CLI invocation to output CSV.
flowchart TD
subgraph Drivers["Entry point (src/f404/)"]
CLI["cli.py (f404)<br/>sweep · design<br/>--mode · --alt · --dts · --throttle · --out"]
A["sweep_full_envelope.py<br/>run_sweeps · write_deck_csv"]
end
subgraph Model["Cycle model"]
B["problems.py<br/>build_problem<br/>targets, calibration, guesses, validation"]
C["mp_cycle.py<br/>MPMixedFlowTurbofan(pyc.MPCycle)<br/>wires DESIGN + OD_dry + OD_wet"]
D["engine_model.py<br/>MixedFlowTurbofan(pyc.Cycle)<br/>single-point thermodynamic cycle"]
end
subgraph Sweep["Sweep infrastructure"]
E["sweep_utils.py<br/>build_snake_sweep · generate_bridge_points<br/>SweepRunner · extract_od_results"]
end
subgraph Output["Output"]
F["printer.py<br/>page_viewer() console tables"]
G["deck/*.csv<br/>cycle_deck_dry / _wet / _full_envelope"]
end
CLI --> A
CLI --> B
A --> B
B --> C
C --> D
B --> F
A --> E
E --> C
E --> G
mp_cycle.py instantiates MixedFlowTurbofan three times:
- with
design=True: the DESIGN point, solved once to size the engine; - with
design=Falseandafterburn=False:OD_dry; - with
design=Falseandafterburn=True:OD_wet.
It passes converged map scalars and station areas from DESIGN into both OD points, so off-design calculations use the one sized engine geometry. A sweep re-solves the OD point for its mode at each condition and freezes the other.
This repository vendors OpenMDAO's pyCycle library. Install in editable mode
from a local clone:
git clone git@github.com:Jhawk414/F404.git
cd F404
pip install -e .[all]Requires Python 3.9+ and OpenMDAO 3.10.0+.
pip install -e .[all] installs an f404 command (re-run it in an existing
checkout to pick up the entry point). python -m f404 is equivalent
and needs no install beyond the package being importable.
f404 sweep # dry + wet, default grid, decks in the cwd
f404 sweep --mode wet # one mode only
f404 sweep --alt 0 --dts -20,20,10 # one altitude, a dTs range
f404 design # solve and print DESIGN, OD_dry and OD_wet
f404 --help # and `f404 sweep --help`, `f404 design --help`
# (rich panels; plain argparse text for usage errors)Sweeps the sized engine over altitude, temperature offset and throttle, and
writes cycle_deck_dry.csv / cycle_deck_wet.csv (plus
cycle_deck_full_envelope.csv for --mode both). With no range flags the
grid is the default one below, so the output matches earlier decks.
| Flag | Meaning | Default |
|---|---|---|
--mode {dry,wet,both} |
afterburner mode | both |
--alt MIN,MAX,STEP |
altitude, ft | 0,5000,2500 |
--dts MIN,MAX,STEP |
ISA temperature offset, R | -50,50,10 |
--throttle MIN,MAX,STEP |
Tt4 (dry) or Tt7 (wet), R | 2500,3100,200 dry, 3008,3608,200 wet |
--out DIR |
deck directory, created if missing | . |
# Wet sweep: T7 from 3608 down to 3008 R (Tt4 held at mil, 3100 R)
f404 sweep --mode wet --alt 0,10000,2500 --dts 0,20,10 \
--throttle 3008,3608,200 --out /tmp/run1--throttle is shown here at its wet default for completeness; leave it off
to get the same T7 values.
- Each range is one
min,max,steptriple, inclusive ofmax, bare or bracketed (--alt [0,10000,1000]). The order is fixed. - A single value holds the axis constant (
--alt 0,--throttle 3100). Step 0 is not a constant axis; it is an error. - A range may start with a negative number (
--dts -20,20,10).main()joins it to its flag before argparse, which would otherwise read it as a flag (#22). - Axes are walked in the order the solver's warm-starting expects, whatever
the flag order:
--dtshot side first (0, +10 … then −10 …),--throttlehigh power to low. --throttleneeds--mode dryor--mode wet: it is a different temperature in each.- There is no
--machyet; Mach is pinned at 0.001 until a Mach sweep dimension exists insweep_utils.py(#13).
A malformed flag stops the run before any model is built, with a message
naming the problem: a range that isn't exactly three finite numbers, a
non-positive step, min above max, a step that doesn't divide max − min
evenly (so the sweep would silently stop short of max), an unusable
--throttle or --out. Exit status 2 for bad arguments.
Sizes the engine and prints the DESIGN, OD_dry (mil) and OD_wet (max AB)
result tables at sea-level static, without sweeping. There is one engine, so
there is no --mode. Three flags override the targets:
--fn-target: dry mil sizing thrust.--mil-tt4: core burner exit temperature.--max-tt7: max-AB afterburner exit temperature.
A non-converging solve exits 1 with the solver's message.
python -m f404.sweep_full_envelope [--mode ...] still works and is
the same as f404 sweep.
pytest tests # full suite, ~2 min
pytest tests -m "not slow" # structural and unit tests only, ~7 sOne tests/<module>_test.py per module (#5).
Tests marked slow build and solve a full om.Problem; the rest either use
pure functions or build a model without solving it. pytest needs no prior
install — pythonpath in pyproject.toml puts src/ on the path.
CI (#31): a PR runs only the fast
tests (-m "not slow", Ubuntu 3.12, about a minute). The full suite on Ubuntu
(3.9, 3.12) and macOS (3.12) runs on every push to main, on manual dispatch,
and on a PR carrying the full-ci label. Apply that label when the PR is about
to merge: the golden baselines and convergence regressions are all slow.
190 tests (155 fast), 97% coverage. The driver and
CLI are importable and covered; what remains uncovered is mostly
sweep_utils.py's solver-failure paths and the one-line __main__ shims.
Latest full-envelope sweep (--mode both, OpenMDAO 3.39.0) at
alt ∈ {0, 2500, 5000} ft, dTs ∈ {0, ±10, ±20, ±30, ±40, ±50} R, static
(MN ≈ 0.001), 4 throttle levels per mode, all on the one sized engine:
| Mode | Converged | Throttle sweep |
|---|---|---|
| Dry | 118 / 132 | Tt4 3100 → 2500 R |
| Wet | 100 / 132 | Tt7 3608 → 3008 R (Tt4 fixed at 3100 R MIL) |
Decks: deck/cycle_deck_dry.csv,
deck/cycle_deck_wet.csv.
Where the failures are, tracked in issue #3:
-
Wet: every point with dTs ≥ 0 R converges; all failures are on the cold (dTs < 0 R) side, worst at altitude.
-
Dry: 67 of the 72 non-cold points converge. Five hot-day, low-power points fail at Tt4 = 2500 R: dTs +40 and +50 R at 0 and 2500 ft, and +50 R at 5000 ft. The rest of the dry failures are on the cold side.
-
Wet rose from 89/132 when the wet A8 stopped being frozen at its DESIGN value (ADR-0001).
-
Dry has measured 118/132 on every run since PR #19, including a clean reinstall. The 125/132 in earlier notes is not reproducible on the current OpenMDAO.
Done:
- Modular cycle model (
engine_model.py/mp_cycle.py), refactored off the original monolithicMFTF_od_CRZ.py - Full alt/dTs/throttle sweep infrastructure with bridge-point
warm-starting (
sweep_utils.py) - Dry (MIL) / wet (max-AB) mode split, with convergence-detection bugs fixed (bound-saturated states no longer reported as converged)
- Repo-root cleanup: removed the superseded
MFTF_od_CRZ.pymonolith and unused vendor/CI cruft (release_notes.md,.travis.yml,.bumpversion.cfg), moved reference PDFs intodocs/unclassified_perf_data/ - Cap cycle-deck CSV precision — single
write_deck_csv()writer with per-column decimals (scientific%.4efor the fuel-air ratios) (#12) - Per-module test suite (
tests/<module>_test.py), 87% coverage, with golden DESIGN/OD baselines and regression cover on the false-convergence guards (#5) - Single-engine sizing: one DESIGN at dry mil feeding dry and wet OD points, wet A8 control law, max AB calibrated to 17,700 lbf (#2, ADR-0001)
-
src/restructure: F404 app code separated from the vendored pyCycle library, now thef404package undersrc/f404/(#4) - Vendored
pycycle/synced against upstream (#6) -
f404CLI withsweepanddesignand--alt/--dts/--throttlerange flags; single-value constant axes (#24) and negative-start ranges (#22) - CI: fast tests on PRs, full matrix on merge or the
full-cilabel (#31) - Published F404 performance reports archived as Obsidian-compatible
markdown, with an index and cross-study reference map
(
docs/unclassified_perf_data/)
Planned (see docs/improvements/IMPROVEMENTS.md for full detail), roughly in
order:
- CLI sweep-axis follow-ups:
--mach(needs a Mach sweep dimension, #13) and a PLA axis (needs PLA defined in the cycle first, #30) - Write failed sweep points to
deck/beside each cycle deck (#27) - Pydantic models for design-point inputs, sweep points and the duplicated OD bounds table (#17)
- PLA/T7-scheduled nozzle throat area (#8), together with defining idle and calibrating the dry part-power end against the published data (#25)
- Resolve remaining cold/high-alt/max-AB Newton convergence failures (#3)
- CLI
init-configand YAML configuration (#16) - Inline creep/LCF life estimation (#10)
-
deck/as a durable, reviewed home for cycle-deck CSVs + solver logs - YAML-driven run configuration (
run.yml), on top of #17's models - Auto-generated sweep-envelope coverage plot
This repo is a fork of NASA Glenn's
pyCycle (om-pycycle on PyPI),
built on the OpenMDAO framework. The pycycle/
library code, setup.py, and example_cycles/ are vendored upstream
scaffolding, not F404-specific.
If you use pyCycle itself, please cite:
E. S. Hendricks and J. S. Gray, "pyCycle: A Tool for Efficient Optimization of Gas Turbine Engine Cycles," Aerospace, vol. 6, iss. 87, 2019. doi:10.3390/aerospace6080087
Apache License 2.0. See meta/LICENSE.txt.