First Principles  ·  Real Thermochemistry  ·  FastAPI-Native

TURBO
FORGE
Turbopump Design Suite

A 16-tab, browser-based preliminary design suite for liquid rocket engine turbopumps — centrifugal/axial pumps, axial/radial turbines, inducer cavitation, shaft rotordynamics, and gas-generator/expander drive-gas balance. Every number is derived, not looked up: real NASA CEA combustion equilibrium, real NIST-grade fluid equations of state, real finite-element rotordynamics, and a validation suite that reproduces 10 independently hand-worked engineering cases live against the running backend.

FastAPI + Pydantic v2 RocketCEA (NASA CEA) CoolProp (NIST EOS) Cantera (GRI-Mech 3.0) pymoo NSGA-II SALib Sobol 379 Passing Tests Zero Desktop GUI
turbopump_suite · uvicorn
$ uvicorn app.main:app --reload
# FastAPI backend + static website, one process, one port
→ GET /api/validation/run-all
  Case 1: Specific speed conversion  PASS
  Case 4: NPSH (LOX, CoolProp EOS)  PASS
  Case 6: Zweifel number check  PASS
  Case 8: Critical speed (FEM x-check)  PASS
  Case 9: Optimizer == direct chain  PASS
  10 / 10 validation cases passing
→ pytest tests/ -q
  379 passed in 10.6s
→ core/hot_gas_properties.py
  RocketCEA: T_gas=1098.9K MW=11.40  real CEA
→ core/propellant_properties.py
  CoolProp: ρ(LOX,90K)=1142.1 kg/m³  NIST EOS
$
16
Website Tabs
379/379
Tests Passing
23
Core Physics Modules
~28K
Lines of Python
10/10
Validation Cases Passing
System Design

REQUEST → PHYSICSRESPONSE

There is no desktop app here — no PyQt6, no Tkinter, no native window at all. Turbo Forge is a single FastAPI process (app/main.py) that serves a JSON API under /api/* and a static vanilla-JS website from the same origin on the same port. The browser holds no physics — it is a thin, stateless view that fetches, renders, and forgets. Every gram of state and every equation lives server-side, in Python.

One Calculate-Button Click, Traced End-to-End
01 🌐

Browser fetch()

static/ JS reads the tab's form, builds a <Tab>Request JSON body, POSTs it to that tab's endpoint. No framework, no bundler — plain fetch.

02 🚦

Router + Pydantic

An APIRouter in app/routers/ (e.g. pump_meanline.py) validates the body against a Pydantic v2 <Tab>Request schema — bad ranges/types 422 before any physics runs.

03 🔗

services/ chain

chain_state.py pulls the cached FullDesignChainInputs, swaps in just this tab's slice, and calls a composable run_through_* function from design_chain.py.

04 🧮

core/ physics

The chain calls into core/ modules — pure, framework-agnostic numerics plus real solvers: RocketCEA, CoolProp, Cantera, brentq root-finds, FEM assembly.

05 📦

Response model

Results are packed into a TabResponseBase subclass: every physical quantity as a DualValue (SI + display unit), plus warnings/checks banners. Cached in-memory for GET replay.

06 📈

Plotly.js render

The JSON lands back in the browser and Plotly.js draws velocity triangles, performance maps, Campbell diagrams — all client-side, zero server-rendered images.

Why zero desktop GUI

The master spec is explicit and non-negotiable on this point: "No desktop GUI framework of any kind. No PyQt/PySide/Tkinter/Kivy. The only user interface is the browser-rendered website in static/, served by FastAPI. All computation and state lives server-side in Python; the browser is a thin, stateless view over the REST API."

app/main.py's own module docstring restates the same rule in code: this is deliberately the ONLY server process — it mounts StaticFiles(directory="static", html=True) at / last, so it never shadows an /api/* route, and CORS is locked to 127.0.0.1:8000/localhost:8000 only, since frontend and backend are the same origin by construction.

This is a hard break from this user's other tools (the Kantrowitz inlet designer and the CEM rocket sizer are both PyQt6 desktop apps). Here the interaction surface is a REST API on purpose: sixteen tabs sharing one running design state, cross-tab defaults (Tab 4 reading Tab 3's converged Cu1), and a validation suite that hits the live server — all things a stateless single-file HTML page can't hold on its own, and a desktop GUI would only complicate.

Why FastAPI + static JS

One process, one port, zero build toolinguvicorn app.main:app is the entire deployment. That matches this user's own standing convention across every web tool in the portfolio: single-file, vanilla JS, CSS variables for theming, no React, no bundler.

Pydantic v2 gives the API something a hand-rolled Flask app wouldn't: every request is schema-validated before it reaches a solver, and every response is schema-guaranteed on the way out — so the frontend never has to defensively check for a missing field.

Long-running work (Tab 15's NSGA-II Pareto sweeps, wide-bounds differential_evolution searches) runs via FastAPI's BackgroundTasks against an in-memory job store (app/dependencies.py's _JobStore), polled by the browser — no Celery, no Redis, no separate worker process, because this is a local, single-user tool and that infrastructure would be pure overhead.

In-memory state, not a session framework
_DesignState: one slot per tab + one "_chain_inputs" slot, process-wide, no auth, no per-user scoping — exactly the amount of state a local single-user design tool needs.
⚙️ Backend

FastAPI + Pydantic v2

Uvicorn ASGI server, APIRouter per tab, Pydantic v2 models for every request and response — validation and serialization, not hand-written dict-parsing.

Live
🔢 Physics Core

NumPy + SciPy

Vectorized array math and brentq/fsolve root-finding underpin every iterative convergence — pump psi-coefficient solves, turbine off-design matching, FEM eigenvalue extraction.

Live
🧪 Real External Data

RocketCEA · CoolProp · Cantera

NASA CEA combustion equilibrium, NIST-grade fluid equations of state, GRI-Mech 3.0 kinetics — name-dropped here, full deep-dive in the Real Data section below.

Live
🎯 Optimization / Stats

pymoo NSGA-II + SALib Sobol

Multi-objective genetic optimization for Pareto sweeps, scipy.optimize.differential_evolution for single-objective searches, SALib Sobol indices for sensitivity analysis.

Live
📐 CAD / Export

ezdxf + Matplotlib

ezdxf writes real DXF geometry (impeller/volute/blade profiles) for Tab 15's CAD export; Matplotlib renders server-side static plots where a report PDF needs a baked-in figure.

Live
🖥️ Frontend

Vanilla JS + Plotly.js

Zero build step, zero framework — one static site, fetch() for API calls, Plotly.js for every interactive chart. No React, matching this user's standing web-tool convention.

Live
The 16 Tabs · Part 1

TABS 1–5 — PUMP-SIDE DESIGN

The suite's first five tabs carry a single design thread from a bare flow/pressure requirement down to a converged, geometry-complete hydraulic end. Each tab consumes the previous tab's converged result rather than re-asking for it: Tab 1's turbine power target and required head feed Tab 2's specific-speed classification; Tab 2's stage-split and recommended type feed Tab 3's meanline solve; Tab 3's converged D1/Cu1 feed Tab 4's inducer/cavitation margin; Tab 4's inducer-exit state feeds Tab 5's volute and diffuser. By Tab 5, every hydraulic dimension on the wet end of the pump has been derived, not assumed.

01

Cycle Requirements & Power Balance

The entry point of the whole chain: given a volumetric flow, tank/discharge pressures, inlet temperature, and a propellant, it derives the required head and hydraulic/shaft power for the primary pump, then rolls in any additional_pumps sharing the same turbine/shaft into a multi-pump architecture summary table.

H_required = (p_discharge − p_inlet) / (ρ·g), then P_shaft = ρ·g·Q·H_required / (η_h·η_mη_v). The turbine's own required shaft power is the sum of every pump's shaft power plus a margin: P_turbine = ΣP_shaft,pump · (1+margin) — the number every downstream turbine tab is ultimately sized against.

In: Q, p_inlet, p_discharge, T_inlet, propellant, η_h, η_mη_v → Out: H_required, P_shaft_pump(s), P_turbine_required, Sankey power-balance data
02

Specific Speed & Type Selection

Takes Tab 1's N/Q/H and computes all three specific-speed conventions this suite carries side by side — US-dimensional Ns, metric-hybrid Nq, and the fully dimensionless Balje Ω_s — then looks Ω_s up against a digitized Balje/Cordier chart to recommend an impeller family and flag proximity to a radial/mixed/axial boundary.

Ns = N·√Q[gpm] / H[ft]^0.75,   Ω_s = ω·√Q / (g·H)^0.75. When a single stage would exceed the head-coefficient ceiling ψ_max, it recommends splitting the required head across multiple stages and returns the per-stage head table Tab 3 stages against.

In: N, Q, H_required (chained), ψ_max → Out: Ns/Nq/Ω_s, impeller type + boundary note, n_stages & H_per_stage, Cordier chart marker
03

Pump Impeller Meanline Design

The heaviest hydraulic solve in the suite: full inlet/exit velocity-triangle design via the Euler equation and a slip factor, an internal-loss breakdown converted to shaft power, and a brentq root-find on the head coefficient ψ to close the loop between D2 and the actual (slip- and loss-corrected) head delivered.

H_euler = (U2·Cu2 − U1·Cu1)/g, with slip via Wiesner or Stanitz correlating Cu2_actual against the blade-angle-implied Cu2_ideal. Two independent internal-loss models are selectable and cross-checkable against each other at the same design point both live: Classical (Daily & Nece disk friction + fixed-fraction leakage/recirculation) and Japikse (incidence, passage skin-friction, blade-loading/diffusion, and clearance-leakage, broken out separately). Optional quasi-3D extensions add a hub-to-tip free-vortex span distribution and a prescribed chordwise loading curve; and setting n_stages>1 triggers a genuine N-identical-stage chain with interstage crossover loss.

In: φ1, blade angle β2b, slip model, loss model, n_stages (chained N/H_stage/Q/ρ) → Out: D1, D2, U1/U2, velocity triangles, η_overall, P_shaft_actual
04

Inducer & Cavitation

Computes NPSH available from tank pressure, static head, and line losses (direct input or a full Darcy-Weisbach + minor-loss solve), designs an axial inducer (Free-Vortex or Constant Blade Angle) sized off Tab 3's impeller-eye D1, and derives NPSH required two independent ways — suction-specific-speed inversion and a critical cavitation number — as a cross-check.

S_US = N·√Q[gpm] / NPSH[ft]^0.75 inverted for NPSH_required; NPSH_required = σ_cav·W1_tip²/(2g) as the independent check. For cryogens it also solves the Thermodynamic Suppression Head (B-factor) that effectively enlarges NPSH available. Optional: cryogenic chilldown — a lumped-capacitance transient wall-cooldown estimate (core/chilldown.py) solving T_wall(t) = T_sat + (T_wall,0−T_sat)·e^(−t/τ), reporting time-to-complete and propellant boiloff mass, lives inside this same tab as an opt-in extra.

In: p_tank, static head, line geometry, target S_US, σ_cav (chained D1/Cu1) → Out: NPSH available/required, TSH, cavitation margin, inducer blade angles, [chilldown time/boiloff]
05

Volute & Diffuser

Closes out the pump-side chain: sizes the volute throat directly from continuity against a target discharge velocity, then propagates the impeller-exit swirl outward through a vaneless diffuser under the free-vortex law before recovering static pressure as the flow decelerates.

A_throat = Q / V_discharge_target (5–15 m/s typical). Through the diffuser, Cu(r) = Cu2·r2/r (free-vortex, r·Cu=const) and P(r) − P2 = 0.5·ρ·(C2² − C(r)²)·η_diffuser, giving a full C(r)/Cu(r)/Cm(r)/ΔP(r) profile from impeller exit to volute throat.

In: V_discharge_target, r_exit/r2, η_diffuser (chained D2/Cu2/Cm2/b2) → Out: throat area/diameter, diffuser C/Cu/Cm/ΔP profile, exit pressure recovery
The 16 Tabs · Part 2

TABS 6–10 — TURBINE, ROTORDYNAMICS & DRIVE-GAS

The chain crosses over from wet end to hot end: Tab 1's turbine power requirement, sitting idle since Tab 2, finally gets spent here. Tab 6 or Tab 7 (axial or radial, picked per architecture) converts it into a converged stage; Tab 8 stress-tests that stage off its design point; Tab 9 carries the resulting torque, disk loads, and speed onto a physical shaft and bearing set; Tab 10 closes the loop by proving the drive-gas cycle can actually deliver the power Tab 6/7 assumed. Five tabs, one continuous energy path from combustion-product enthalpy to a spinning, balanced rotor.

06

Axial Turbine Stage Design

Sizes an impulse or reaction axial stage off Tab 1's required turbine power: isentropic spouting velocity sets the ideal enthalpy drop, then a full velocity-triangle solve (via degree of reaction, using the axial-machine angle convention — deliberately distinct from the pump-side radial convention, since the two are genuinely different machine families) delivers Cx/Cu/W at rotor inlet and exit, checked against the Zweifel blade-loading criterion for both stator and rotor rows.

C0 = √(2·Cp·T01·(1−(P2/P01)^((γ−1)/γ))),   Zw = 2·(s/cx)·(tanα_in−tanα_out)·cos²α_out, targeted to a 0.8–1.0 band (Zweifel, 1945). Two independent stage-loss correlations are selectable and cross-checkable at the same design point both live: Soderberg (turning-angle profile loss with aspect-ratio/tip-clearance/secondary-flow corrections) and Craig & Cox (Reynolds- and Mach-corrected profile loss plus separate secondary, clearance, and trailing-edge terms). An optional quasi-3D chordwise loading curve optional visualizes front/aft blade loading per row, and n_stages>1 chains a work split across a genuine multi-stage turbine.

In: P_turbine (chained), mdot, T01/P01/P2, γ/Cp, N, stage_type, φ, loss_model, n_stages → Out: velocity triangles, Zw_stator/Zw_rotor + flags, η_stage, P_stage, T02/P02
07

Radial Inflow Turbine

A 90-degree IFR (inward-flow radial) turbine sized around the classic Whitfield & Baines optimum velocity ratio: rotor tip speed is pinned to U2 = 0.7·C0 under the zero-incidence design condition (radial relative flow, Cu2=U2), then the exducer (rotor exit) is sized off a digitized Rohlik-chart-style D3/D2 correlation, with total-to-static efficiency read from a digitized efficiency-vs-specific-speed curve.

A single stage genuinely spans both of this suite's velocity-triangle conventions: Station 2 (rotor inlet) reuses the pump-side radial-machine triangle directly, while Station 3 (exducer exit) reuses Tab 6's axial-machine convention, with zero exit swirl assumed (Cu3=0) for maximum work extraction. Ω_s,turbine is built off the actual (not isentropic) exit density via T3 = T01 − ΔW_actual/Cp, since the real lossy expansion — not the ideal one — sets the true exit thermodynamic state.

In: P_turbine (chained), mdot, T01/P01/P2, N, velocity_ratio, cm2/U2, exducer hub/tip ratio → Out: D2/D3, U2/U3, velocity triangles, Ω_s,turbine, η_ts, efficiency-vs-Ω_s chart
08

Turbine Off-Design & Performance Map

Takes whichever of Tab 6's axial stage or Tab 7's radial turbine ran most recently as the on-design reference point and sweeps it across a pressure-ratio × speed-ratio grid, generating the classic turbine-map presentation: efficiency contours, power contours, and a corrected-mass-flow speed-line family, returned as plain grids ready for a Plotly contour/heatmap.

The isentropic-nozzle mass-flux function Φ(PR) = √[(2γ/(γ−1))·((1/PR)^(2/γ) − (1/PR)^((γ+1)/γ))] proxies how both corrected mass flow and axial velocity scale with pressure ratio, feeding a quadratic flow-coefficient/incidence-deviation loss penalty constructed to vanish exactly at the design point — algebraically guaranteed to reproduce the reference efficiency there. Flagged as first-order: an explicitly preliminary-design trend model for efficiency sensitivity, not a CFD-grade map, a disclaimer this tab surfaces directly per the spec.

In: on-design reference (chained from Tab 6/7), PR sweep bounds, speed-ratio sweep bounds, grid resolution → Out: efficiency/power/corrected-mass-flow grids, design-point marker
09

Shaft, Bearings & Critical Speed

The most feature-dense tab in the suite: torsional shaft sizing (solid or hollow bore) from torque and an allowable shear stress, a Rayleigh-Ritz lateral critical-speed solve generalized to multiple lumped masses via superposed beam-deflection influence coefficients, axial thrust balance with balance-piston sizing, and bearing DN checked against a 1.8×10&sup6; practical ceiling.

ω_cr² ≈ g·Σ(m_i·δ_i) / Σ(m_i·δ_i²), verified to collapse exactly to the closed-form √(48EI/mL³) single-mass case. Three optional rotordynamics extensions live in this same tab optional, all live: a multi-mode 2-node Euler-Bernoulli beam FEM lateral critical-speed solve (Guyan-condensed bearing stiffness, optional shaft self-mass), a single-disk torsional critical speed, and a gyroscopic forward/backward whirl split with a full Campbell-diagram sweep. A fourth optional block, core/blade_disk_stress.py's rotating-disk analysis optional, closes the loop with Timoshenko & Goodier's closed-form hoop/radial stress distribution, a burst-speed margin (ω_burst = ω_op·√(σ_ult/σ_peak)), and a modified-Goodman LCF/HCF fatigue check.

In: material, shaft power/N (chained), masses/positions, support type, [FEM modes, gyroscopic spin, disk r_outer/bore] → Out: shaft diameters, ω_cr + margin, bearing DN, balance piston, [FEM mode set, ω_fwd/ω_bwd, disk σ_θ/burst margin/Goodman SF]
10

Drive-Gas Balance (GG / Expander)

Closes the turbine power loop for whichever cycle architecture is selected: Gas Generator and Staged Combustion draw drive-gas state off a digitized O/F-vs-combustion-product-property table and iterate (brentq) on pressure ratio until available turbine power matches Tab 1's required shaft power; Expander instead fixes a heat-exchanger-exit temperature/pressure and iterates on mdot_turbine to the same balance.

Expander-cycle Cp(T) and γ(T) now come from real Cantera NASA-9/7-polynomial thermo (gri30.yaml, H2/CH4) rather than a fixed-γ lookup — H2's γ genuinely drops from ~1.46 at 150K to ~1.38 at 900K across the expander-relevant range. For GG/Staged Combustion, a section-10.5 side calculation quantifies the open-cycle tax: flow_tax = mdot_turbine/mdot_main, blended against the GG-exhaust's lower Isp fraction into an effective mission Isp penalty, extending Tab 1's Sankey diagram with the now-quantified drive-gas branch.

In: cycle_type, η_turbine, GG combo/O-F/mdot or expander fluid/T_exit/P_exit, [main mdot/Isp_chamber for flow-tax] → Out: converged PR or mdot_turbine, P_turbine_available, [flow_tax, Isp_effective/penalty], power-balance curve, Sankey
The 16 Tabs · Part 3

TABS 11–16 — ANALYSIS, PERSISTENCE & DOCUMENTATION

The last six tabs are not a continuation of the pump→turbine→shaft physics thread — they are the layer built on top of it. Tab 11 re-runs Tabs 1–10's own chain as a read-only what-if explorer; Tab 12 persists whatever that chain has converged to; Tab 13 proves, live, that the chain reproduces ten independently hand-worked answers; Tabs 14 and 16 are the suite's own internal documentation, prose and formulae respectively — the same job this website is doing, one level down; and Tab 15 is the heaviest single tab in the app, wrapping the entire chain in an optimizer, a Pareto search, a Monte Carlo tolerance stack-up, and a formal sensitivity study. Architecturally these six tabs are each doing something none of Tabs 1–10 do: sweeping, storing, self-testing, or explaining, rather than designing.

11

Parametric Study

A read-only what-if explorer over the SAME cached chain every other tab shares, but it never writes back to it — running a sweep must not silently move the design point every other tab is looking at. Four named sweeps re-run slices of Tabs 2–9 across a linspace of one free variable (speed trade, head-coefficient trade, inducer design, turbine pressure ratio), each resolving a real structural mismatch: Sweep 2 needs ψ as a free input where core/centrifugal_pump.py only exposes it as a solved convergence root, so it calls a direct non-iterative evaluate_meanline_at_fixed_psi instead; Sweep 3 needs an inducer tip diameter that varies with target S_US, so it inverts the cavitation-number NPSH_required formula via a bracketed brentq search on tip radius.

A fifth, generic custom sweep lets any registered input drive any registered output across the full 10-tab chain, optionally as a 2D heatmap over two inputs at once. Because an HTTP body can't carry a Python callable, app/routers/parametric_study.py resolves this with an explicit INPUT_SETTERS/OUTPUT_GETTERS registry of dotted "tab.field" strings (e.g. pump.beta2b_degpump.eta_overall) rather than unrestricted getattr/setattr reflection over the dataclass tree — a deliberately bounded subset of "any input vs any output," not literal open reflection.

In: sweep bounds/n_points (named 1–4) or input_path/output_path/bounds (custom, +optional 2nd axis) → Out: parallel-list sweep data ready for Plotly line/heatmap, per-point null+warning on infeasible points
12

Case Library

SQLite persistence (stdlib sqlite3, framework-agnostic core/database.py) for the running design: save/load/duplicate/export/import a case as one row across 7 JSON-blob columns. The spec's own schema has exactly 7 columns but the chain spans 10 cached tab results, so an explicit mapping folds Tab 6-or-7 into one turbine_design_json column (tagged with which) and bundles the 3 columnless tabs (specific-speed, volute-diffuser, turbine-offdesign) into a nested results_summary_json.

A later phase added case_versions, an additive revision-history table: every update_case call snapshots the case's pre-update row into case_versions FIRST, with version_number = MAX(existing)+1, before applying the overwrite — so version N always means "the state right before the Nth update," and the live/current state is never duplicated into history (no stale-copy drift between "latest version" and "current"). diff_case_versions then does a shallow, top-level-key diff of the 7 blobs between any two states, where either side may be a version number or the literal string "current".

In: case name, current session's tab results (save); case id or {version_a, version_b} (load/diff) → Out: CaseRecord row, version list, {changed_fields, added_blobs, removed_blobs} diff
13

Validation & Worked Examples

Arguably the single most important endpoint in the app: GET /api/validation/run-all re-runs all 10 hand-worked cases from the master spec's own "VALIDATION & REFERENCE CASES" section LIVE, against the exact same core/services functions the pytest suite already exercises — imported directly, never re-implemented — so a passing response is proof the shipped, running app reproduces every cited calculation, not just that the test suite does in isolation.

Each of the 10 case functions cites, in-line, the exact tests/test_core/ or tests/test_services/ test it mirrors, since several of the spec's own hand-worked reference numbers contained independently-verified arithmetic slips (Cases 3 and 5) or an internally-inconsistent LOX vapor-pressure figure (Case 4) — this endpoint always reports the corrected reference value with the discrepancy documented at point of use, never a loosened tolerance to force a pass. Case 9 needs the same "reasonable, validated" GG-cycle design point the test fixtures use; rather than importing tests/ from application code, it duplicates that construction inline, field for field. The full 10-case table itself is presented separately, later on this page — this tab's own job is just running that table live.

In: none (self-contained, hardcoded reference cases) → Out: 10× {title, expected, actual, tolerance, passed}, n_passed/n_total, all_passed, generated_at
14

Theory & References

The suite's own in-app equivalent of what this website is doing at a smaller scale: a prose walkthrough of every method the 16 tabs actually use — Euler turbomachinery theory, Wiesner and Stanitz slip correlations, Japikse & Baines loss modeling, Zweifel blade-loading, Craig & Cox turbine loss, Rayleigh-Ritz and Timoshenko & Goodier rotordynamics/stress theory, Shigley shaft design, Lobanoff & Ross pump engineering, NSGA-II multi-objective search, and Sobol sensitivity — each tied back to a real, citable reference rather than an unsourced formula.

Where Tab 16 is the equation sheet, Tab 14 is the explanation of why each equation is the right one to reach for: derivation context, applicability limits, and the literature a user could go re-derive it from independently. It exists so every number this suite produces is traceable to a named, checkable source — the same non-negotiable "derived, not looked up" principle the whole tool is built on, expressed here as citations rather than code.

In: none (static reference content) → Out: per-topic prose sections with inline citations (Wiesner, Stanitz, Japikse & Baines, Craig & Cox, Timoshenko & Goodier, Shigley, Lobanoff & Ross, and more)
15

Design Optimizer & Monte Carlo Tolerance Study

The densest tab in the suite — five sub-panels, all operating over the SAME cached chain as an exploration, never mutating it. All five share one 5-dimensional decision vector where relevant — [N_rpm, φ1, β2b_deg, b2/D2, target_S_US] — because ψ itself is a solved convergence root in core/centrifugal_pump.py, not a free variable; every objective evaluation instead fixes a ψ-search bracket and re-converges through it.

  • 15.1 — Single-objective optimizer. scipy.optimize.differential_evolution (global, derivative-free) maximizes η_overall through the full chained pipeline, with NPSH-margin/critical-speed-margin/bearing-DN/Zweifel-band constraints folded in as a large penalty on the negated objective. Every generation's (params, objective, feasible) is logged for a Plotly parallel-coordinates plot at zero extra chain evaluations.
  • 15.3 — Multi-objective Pareto search. pymoo's NSGA-II searches the identical decision vector against three simultaneous objectives, returning the non-dominated front plus a hypervolume indicator.
  • 15.2 — Monte Carlo tolerance stack-up. Samples manufacturing tolerances (D2, b2, β2b, clearance, D1 — the last needed so NPSH margin scatters at all) through the meanline+inducer chain thousands of times, returning full statistics (mean/std/p5/p95) and histograms on H_actual, η_overall, and NPSH margin, plus a probability-of-violation figure.
  • 15.4 — Sobol sensitivity + OAT tornado. SALib Sobol first-order/total indices (N·(2k+2) model evaluations for k=5 toleranced parameters) rank which tolerance actually drives output variance; a companion one-at-a-time tornado chart shows each parameter's low/high swing on the same three outputs for a cheaper, non-probabilistic cross-check.
  • Async job variants. /run-async and /pareto-async accept the identical request bodies but hand the search to FastAPI BackgroundTasks against an in-memory _JobStore, returning a job_id immediately (202) instead of blocking tens of seconds; GET /jobs/{job_id} polls {status, progress, result}, with progress reported live as "generation X/Y" via an on_generation callback wired into both the DE and NSGA-II loops.
Pareto search — 3 simultaneous objectives (pymoo minimizes; two are negated maximize-objectives)
f1 = −η_overall  (maximize efficiency)
f2 = D2  (minimize impeller size proxy)
f3 = −NPSH_margin  (maximize cavitation margin)
In: decision-vector bounds, ψ search bracket, constraint fractions/ceilings (15.1/15.3); tolerance table, n_samples/seed (15.2); tolerance table, n_base_samples (15.4) → Out: best_params/eta_overall/parallel-coordinates (15.1); Pareto front + hypervolume (15.3); stat summaries + histograms + violation probability (15.2); S1/ST indices + tornado swings (15.4); job_id/status/progress (async)
16

Formula Reference

The dense equation-sheet companion to Tab 14's prose: every formula the suite evaluates, organized by topic and roughly ordered foundational-to-advanced — unit conversions (gpm/ft/RPM ↔ SI) and specific-speed definitions first, through Euler turbomachinery, slip and loss correlations, cavitation and suction specific speed, velocity-triangle and Zweifel relations, rotordynamics (Rayleigh-Ritz, FEM, gyroscopic whirl) and rotating-disk stress, up to the suite's most advanced machinery: NSGA-II's genetic operators and Sobol's variance-decomposition indices.

Where Tab 14 explains why a method is appropriate, Tab 16 is the fast lookup for what to actually type — every symbol used elsewhere in the 16 tabs defined once, in one place, so a user mid-design can confirm a formula without leaving the app. It is the same "everything derived, nothing looked up" commitment as Tab 14, condensed into reference-card form rather than narrative form.

In: none (static reference content) → Out: per-topic equation blocks, foundational → advanced, spanning every formula cited across Tabs 1–15
This Website's Own Tab 16

EVERY FORMULA, FOUNDATIONAL → ADVANCED

From the most basic unit conversion to the gyroscopic whirl-speed equation — nothing in this tool is a black box. What follows is transcribed directly from the core//services/ module docstrings the running app itself evaluates, the same source Tab 16's in-app Formula Reference reads from — every symbol defined once, every constant's exact numeric value cited, every formula attributed to the module that actually runs it.

01 · Unit Conversions

The ONLY place unit-conversion factors are defined, per the project's global rule: display-layer conversion happens exclusively on the backend, and every internal physics computation is SI throughout.

Volumetric flow — gpm
1 m³/s = 15,850.32 US gpm
(1 US gal = 3.785411784×10⁻³ m³, exact)
— core/units.py: GPM_PER_M3S
Length — ft
1 m = 3.28084 ft (NIST)
1 ft = 0.3048 m (exact, by definition)
— core/units.py: FT_PER_M
Pressure — psi / bar
1 psi = 6894.757 Pa (NIST)
1 bar = 1×10⁵ Pa (exact, by definition)
— core/units.py: PA_PER_PSI, PA_PER_BAR
Power — hp
1 hp (mechanical) = 745.699872 W (NIST)
— core/units.py: W_PER_HP
Rotational speed — RPM ↔ rad/s
ω = N·2π/60    N = ω·60/(2π)
1 rad/s = 9.549297 RPM  ·  1 RPM = 0.104720 rad/s
— core/units.py: rpm_to_rad_s / RPM_PER_RAD_S
Mass — lbm
1 kg = 2.204623 lb (NIST)
1 lb = 0.45359237 kg (exact, by definition)
— core/units.py: LB_PER_KG

02 · Similarity & Specific Speed

Tab 1's entry-point power-balance algebra, then Tab 2's three specific-speed conventions carried side by side — US-dimensional, metric-hybrid, and fully dimensionless — plus suction specific speed.

Cycle requirements & power balance (Tab 1)
H_required = (P_discharge − P_inlet) / (ρ·g)
P_shaft = ρ·g·Q·H_required / (η_h·η_mη_v)
P_turbine = ΣP_shaft,pump · (1 + margin)
— services/design_chain.py
US-dimensional / metric-hybrid specific speed
Ns_US = N[RPM]·√Q[gpm] / H[ft]^0.75
Nq = N[RPM]·√Q[m³/s] / H[m]^0.75
— core/specific_speed.py: specific_speed_us / specific_speed_nq
Balje dimensionless specific speed
Ω_s = ω·√Q / (g·H)^0.75
— core/specific_speed.py: specific_speed_omega_s  (Balje, 1981)
Suction specific speed & its inversion
S_US = N[RPM]·√Q[gpm] / NPSH[ft]^0.75
NPSH_required[ft] = (N·√Q[gpm] / S_US)^(4/3)
— core/specific_speed.py: suction_specific_speed_us / npsh_required_from_s_us

03 · Velocity Triangles & Euler Equation

Two DISTINCT, deliberately non-interchangeable angle conventions live in this suite — radial-machine (pump/inducer, tangential-referenced β) and axial-machine (turbine, axial-referenced β) — plus both slip-factor correlations that connect a blade-angle-implied triangle to the actual, slipped one.

Euler turbomachinery equation
H_euler = (U2·Cu2 − U1·Cu1) / g
P_euler = ρ·Q·(U2·Cu2 − U1·Cu1)
— core/velocity_triangles.py: euler_head / euler_power
Radial-machine β (pump/inducer, tangential-referenced)
tan(β) = Cm / (U − Cu)    [β=90° at Cu=U, no backsweep]
tan(α) = Cu / Cm
— core/conventions.py Section 2 (Stepanoff/pump convention)
Axial-machine β (turbine, axial-referenced)
tan(β) = (Cu − U) / Cx    tan(α) = Cu / Cx
NOT the same sign rule as the radial convention — kept textually and numerically separate.
— core/conventions.py Section 3 (Dixon & Hall Ch. 4)
Wiesner slip factor + validity correction
σ_slip = 1 − √(sinβ2b) / Z^0.7
ε_limit = exp(−8.16·sinβ2b/Z); if D1/D2 > ε_limit:
σ_corrected = σ_slip·[1 − ((D1/D2−ε_limit)/(1−ε_limit))³]
— core/velocity_triangles.py: wiesner_slip_factor  (Wiesner, 1967)
Stanitz slip factor
σ_slip = 1 − 0.63π/Z    (independent of blade angle)
— core/velocity_triangles.py: stanitz_slip_factor  (Stanitz, NACA TN-2408)
Slip-corrected actual tangential velocity
Cu2_ideal = U2 − Cm2/tan(β2b)
Cu2_actual = Cu2_ideal − (1−σ_slip)·U2
— core/velocity_triangles.py: actual_tangential_velocity

04 · Centrifugal Pump Meanline

Tab 3's heaviest solve: D2 sized off a ψ-convergence root-find, Pfleiderer's blade-count correlation, two independently cross-checkable internal-loss breakdowns, and the genuine N-stage crossover-loss chain.

D2 sizing (ψ-convergence root)
U2 = √(g·H_stage/ψ)    D2 = 2·U2/ω
ψ solved by brentq s.t. H_actual(ψ) = H_stage
— core/centrifugal_pump.py: design_impeller_meanline
Pfleiderer blade number
Z = k·(D2+D1)/(D2−D1)·sin((β1b+β2b)/2)
k ∈ [6.5, 8]; clamped to [5, 12]
— core/centrifugal_pump.py: pfleiderer_blade_number
Classical losses
P_df = Kf·ρ·ω³·D2⁵
P_leak = leak_frac·P_euler   P_recirc = recirc_frac·P_euler
P_shaft = P_euler + P_df + P_leak + P_recirc
— core/centrifugal_pump.py  (Daily & Nece, 1960)
Japikse alternate — incidence + skin-friction
P_inc = 0.5·mdot·(Wu1_actual−Wu1_opt)²
P_sf = mdot·Cf·(Lb/Dh)·Wavg²/2
— core/centrifugal_pump.py  (Japikse & Baines, Ch. 5)
Japikse alternate — diffusion + clearance
P_diff = mdot·K_diff·max(0,W1/W2−1)²·W2²/2
Q_clear = Cd·(clearance·π·D2)·√(2Δp/ρ); P_clear = ρQ_clear·g·H_euler
— core/centrifugal_pump.py  (Japikse & Baines, Ch. 5)
Multi-stage interstage crossover loss
ΔH_crossover = K_crossover·C2²/(2g)
H_total = n·H_stage − (n−1)·ΔH_crossover
— services/design_chain.py: crossover_channel_loss

05 · Inducer & Cavitation

NPSH available from tank state and line losses, NPSH required two independent ways (S_US-inversion and a critical cavitation number), and the implicit Thermodynamic Suppression Head solve for cryogens.

NPSH available
NPSH_available = (P_tank−P_vapor)/(ρg) + z_static − h_losses
— core/inducer_cavitation.py: npsh_available
Cavitation-number cross-check
σ_cav = (P_inlet−P_vapor)/(0.5ρ·W1_tip²)
NPSH_required = σ_cav,critical·W1_tip²/(2g)
— core/inducer_cavitation.py: cavitation_number
S_US inversion for NPSH required
NPSH_required[ft] = (N·√Q[gpm] / S_US)^(4/3)
— core/inducer_cavitation.py: npsh_required_from_target_s_us
Thermodynamic Suppression Head (B-factor, implicit)
B = (1−α)/α
ΔT_sup = B·h_fg·ρ_v(T−ΔT_sup) / (Cp_l·ρ_l)  [brentq]
TSH = ΔT_sup·(dP_sat/dT) / (ρ_l·g)
— core/inducer_cavitation.py: solve_thermodynamic_suppression_head  (Ruggeri & Moore, NASA TN D-5292)

06 · Volute & Diffuser

Throat sizing directly from continuity, then the impeller-exit swirl carried outward through a vaneless diffuser under the free-vortex law as static pressure recovers.

Volute throat sizing
A_throat = Q / V_discharge_target  (5–15 m/s typical)
D_throat = √(4·A_throat/π)
— core/volute_diffuser.py: design_volute_throat
Vaneless diffuser — free-vortex swirl decay
Cu(r) = Cu2·r2/r    Cm(r) = Cm2·r2·b2 / (r·b(r))
— core/volute_diffuser.py: diffuser_tangential_velocity
Static pressure recovery
P(r) − P2 = 0.5·ρ·(C2² − C(r)²)·η_diffuser
— core/volute_diffuser.py: diffuser_static_pressure_recovery

07 · Axial & Radial Turbines

Isentropic spouting velocity sets the ideal enthalpy drop; a closed-form two-equation solve delivers the velocity triangle from stage loading and reaction; Zweifel checks blade loading; Soderberg and Craig & Cox are two independently cross-checkable loss correlations; the IFR turbine pins rotor tip speed to the classic 0.7·C0 optimum.

Isentropic spouting velocity
C0 = √(2·Cp·T01·(1−(P2/P01)^((γ−1)/γ)))
— core/axial_turbine.py: spouting_velocity
Stage loading, reaction & velocity triangle
ψ = ΔW/U²    R = 1−(Cu1+Cu2)/(2U)
Cu1 = U·(ψ/2+(1−R))   Cu2 = U·((1−R)−ψ/2)
— core/axial_turbine.py: solve_velocity_triangle
Zweifel blade loading number
Zw = 2·(s/cx)·(tanα_in−tanα_out)·cos²α_out
target band 0.8–1.0
— core/axial_turbine.py: zweifel_number  (Zweifel, 1945)
Soderberg loss (digitized + aspect-ratio correction)
ζ*(turning angle) at AR_ref=3, digitized table
factor = 1 + 0.33·(3/AR−1)/3
— core/axial_turbine.py: soderberg_profile_loss  (Soderberg, 1949)
Craig & Cox alternate loss
ζ ~ Re^−0.2 correction on the Soderberg base curve, plus Mach, quadratic secondary-flow, Dunham&Came clearance^0.78, and a standalone trailing-edge term
— core/axial_turbine.py  (Craig & Cox, 1971)
IFR turbine optimum velocity ratio
U2 = 0.7·C0  (Cu2=U2, zero-incidence design)
D3/D2, η_ts digitized vs Ω_s,turbine
— core/radial_turbine.py  (Whitfield & Baines; Rohlik)

08 · Shaft, Bearings & Rotordynamics

Torsional sizing from allowable shear, a closed-form single-mass critical speed generalized to Rayleigh-Ritz for multiple lumped masses, the full first-principles gyroscopic forward/backward whirl derivation, axial thrust balance, and bearing DN.

Torsional shaft sizing (solid & hollow)
T = P/ω    d_solid = (16T/(πτ_allow))^(1/3)
d_o = (16T/(πτ_allow·(1−k⁴)))^(1/3), d_i=k·d_o
— core/shaft_bearings.py: solid_shaft_diameter / hollow_shaft_diameter
Critical speed — closed form & Rayleigh-Ritz
ω_cr = √(48EI/(mL³))  (simply-supported, central mass)
ω_cr² ≈ g·Σ(m_iδ_i) / Σ(m_iδ_i²)  (general, N masses)
— core/shaft_bearings.py: critical_speed_simply_supported_central_mass / rayleigh_ritz
Gyroscopic whirl speeds (derived closed form)
Id·ω² − Ip·Ω·ω − κ = 0
ω_fwd/bwd = [IpΩ ± √((IpΩ)²+4Idκ)] / (2Id)
— core/shaft_bearings.py: gyroscopic_whirl_speeds  (Genta; Childs)
Axial thrust & balance piston
F_net = ΣF_i,signed
A_piston = (|F_net|−C_bearing)/Δp, D_piston=√(4A_piston/π+d_shaft²)
— core/shaft_bearings.py: net_axial_thrust / balance_piston_sizing
Torsional critical speed (single lumped disk)
ω_t = √((G·J/L) / I_disk,polar),  J=2I, I_disk=0.5·m·r²
— core/shaft_bearings.py: torsional_critical_speed
Bearing DN
DN = d_bearing[mm] · N[RPM]    ceiling 1.8×10⁶
— core/shaft_bearings.py: bearing_dn  (NASA SP-8107)

09 · Disk Stress & Fatigue

Timoshenko & Goodier's closed-form rotating-disk hoop/radial stress distribution, solid AND bored, plus a burst-speed margin and a modified-Goodman low/high-cycle-fatigue check.

Solid rotating disk (Timoshenko & Goodier Art. 39)
σ_r(r) = (3+ν)/8·ρω²·(b²−r²)
σ_θ(r) = ρω²/8·[(3+ν)b²−(1+3ν)r²]
— core/blade_disk_stress.py: _solid_disk_stress
Bored disk (peak hoop stress at the bore)
σ_r(r) = (3+ν)/8·ρω²·(a²+b²−a²b²/r²−r²)
σ_θ(a) = ρω²/4·[(3+ν)b²+(1−ν)a²]  (doubles the solid-disk peak as a→0)
— core/blade_disk_stress.py: _bored_disk_stress
Burst-speed margin
ω_burst = ω_operating·√(σ_ultimate/σ_peak)
— core/blade_disk_stress.py: disk_burst_speed
Modified-Goodman fatigue criterion
σ_alt/Se + σ_mean/Su ≤ 1/SF
— core/blade_disk_stress.py: goodman_fatigue_check  (Shigley/Budynas & Nisbett)

10 · Drive-Gas Balance

Turbine available power balanced (via brentq) against Tab 1's required shaft power, iterating on pressure ratio (GG/Staged Combustion) or turbine mass flow (Expander); the GG-cycle open-loop flow tax quantified as an Isp penalty.

Turbine available power
P_available = mdot_turbine·Cp·T01·η_turbine·(1−PR^((γ−1)/γ))
— core/drive_gas_balance.py: turbine_available_power
Power-balance iteration
brentq on PR (GG/Staged Combustion) or on mdot_turbine (Expander),
s.t. P_available = P_shaft_turbine_required
— core/drive_gas_balance.py
GG flow tax
flow_tax = mdot_turbine / (mdot_main + mdot_turbine)
— core/drive_gas_balance.py: gg_flow_tax_isp_penalty
Blended-exhaust Isp penalty
Isp_effective = (mdot_main·Isp_chamber + mdot_turbine·Isp_gg) / (mdot_main+mdot_turbine)
penalty = (Isp_chamber − Isp_effective) / Isp_chamber
— core/drive_gas_balance.py: gg_flow_tax_isp_penalty

11 · Chilldown

A lumped-capacitance transient wall-cooldown estimate: the wetted metal's energy balance against film/nucleate boiling, closed-form exponential in time, plus a simple boiloff-mass energy balance.

Lumped-capacitance ODE
m_wall·Cp_wall·dT_wall/dt = −h·A·(T_wall(t) − T_sat)
— core/chilldown.py (module docstring)
Closed-form exponential solution
T_wall(t) = T_sat + (T_wall,0−T_sat)·e^(−t/τ)
τ = m_wall·Cp_wall/(h·A)    t_complete = −τ·ln(tolerance)
— core/chilldown.py: solve_chilldown
Boiloff energy balance
m_boiloff·h_fg = m_wall·Cp_wall·(T_wall,0 − T_wall,final)
— core/chilldown.py: solve_chilldown  (Barron, "Cryogenic Heat Transfer")

12 · Optimization & Statistics

The suite's most advanced machinery: a penalized single-objective search, a 3-objective NSGA-II Pareto front, truncated-normal Monte Carlo tolerance sampling, and Sobol variance-decomposition sensitivity indices.

Penalized single-objective (differential_evolution minimizes)
objective = −η_overall + Σ penalty_scale·max(deficit, 0)
over NPSH-margin / critical-speed-margin / bearing-DN / Zweifel-band constraints
— services/optimizer_service.py: _evaluate_design_point
Pareto search — 3 simultaneous objectives (pymoo NSGA-II)
f1 = −η_overall  (maximize efficiency)
f2 = D2  (minimize size)   f3 = −NPSH_margin  (maximize margin)
— services/optimizer_service.py: _pareto_objectives_and_constraints
Monte Carlo — truncated-normal tolerance basis
X ~ TruncNormal(nominal, σ=tolerance/3, ±3σ=tolerance)
via scipy.stats.truncnorm, zero-tolerance → exact constant (no NaN)
— core/monte_carlo.py: sample_truncated_normal
Sobol S1 / ST variance-decomposition indices
S1 = first-order index (this parameter alone)   ST = total-order (+ interactions)
N·(2k+2) model evaluations for k=5 toleranced parameters (SALib Saltelli sampling)
— core/sensitivity.py: run_sobol_sensitivity

13 · CAD / Geometry

Bridging this suite's meanline blade-ANGLE outputs into an actual 2D blade profile for DXF/CAD export: a circular-arc camberline with exact tangent match at both ends, plus a simple parabolic thickness envelope.

Circular-arc camberline construction
R = chord / (sinβ2 − sinβ1)
x(φ) = R·(sinφ−sinβ1),   y(φ) = −R·(cosφ−cosβ1),   φ: β1→β2 linear
— core/blade_profile_geometry.py: circular_arc_camberline  (Dixon & Hall, Ch. 3)
Parabolic thickness envelope
t(x) = t_max·4·(x/c)·(1−x/c)  (peaks at midchord, t_max default 8% chord)
offset along the local normal to the camberline tangent, not vertical
— core/blade_profile_geometry.py: parabolic_thickness / generate_blade_profile
The Repo, Not The Diagram

PROJECT FILE STRUCTURE

Five layers, one direction of dependency. core/ is framework-agnostic physics — 23 modules, zero HTTP imports, importable from a bare Python shell with no server running. services/ is the orchestration layer that chains core/ calls into the 16-tab pipeline. app/ is the thin FastAPI skin — routers and Pydantic schemas, one pair per tab. static/ is the website itself: HTML/CSS/vanilla JS, zero build step, zero framework. tests/ mirrors core//services//app/ one-for-one — 379 tests, none of them touching a browser.

turbopump_suite/ ├── app/ # FastAPI skin — zero physics, all validation + wiring │ ├── main.py 92 # ASGI app, CORS lock, static mount (mounted LAST) │ ├── dependencies.py 127 # in-memory _DesignState + _JobStore (no Celery/Redis) │ ├── routers/ # 18 files · one APIRouter per tab · 3,908 lines │ │ ├── specific_speed.py, pump_meanline.py, inducer_cavitation.py, volute_diffuser.py │ │ ├── axial_turbine.py, radial_turbine.py, turbine_offdesign.py │ │ ├── shaft_bearings.py, drive_gas_balance.py, cycle_requirements.py │ │ ├── optimizer.py, parametric_study.py, case_library.py │ │ └── cad_export.py, report_export.py, validation.py, chain_state.py, status.py │ └── schemas/ # 15 files · one Pydantic module per tab · 2,112 lines │ └── (common.py DualValue/TabResponseBase base, then one file per tab schema above) │ ├── core/ # 23 files · pure physics · zero HTTP imports · 11,090 lines │ ├── conventions.py 261 units.py 182 │ ├── materials.py 264 specific_speed.py 239 │ ├── velocity_triangles.py 287 │ ├── centrifugal_pump.py 1,037 inducer_cavitation.py 681 │ ├── volute_diffuser.py 217 │ ├── axial_turbine.py 987 radial_turbine.py 329 │ ├── turbine_offdesign.py 317 hot_gas_properties.py 398 │ ├── shaft_bearings.py 1,105 blade_disk_stress.py 445 │ ├── propellant_properties.py 750 drive_gas_balance.py 715 │ ├── monte_carlo.py 530 sensitivity.py 438 │ ├── database.py 677 report_export.py 393 │ ├── chilldown.py 208 blade_profile_geometry.py 232 │ └── cad_export.py 398 │ ├── services/ # orchestration — chains core/ calls · 3,253 lines │ ├── design_chain.py 1,842 # composable run_through_* pipeline, Tabs 1–10 │ ├── optimizer_service.py 898 # Tab 15 — optimizer / Pareto / Sobol / Monte Carlo │ └── sweep_service.py 513 # Tab 11 — 4 named parametric sweeps │ ├── static/ # the website itself — zero build step · 7,825 lines │ ├── index.html 2,007 │ ├── css/style.css 207 │ └── js/ │ ├── main.js, api.js, charts.js, schematics.js, export.js # shared, 5 files │ └── tabs/ # 13 per-tab controllers · 4,516 lines │ ├── tests/ # mirrors core/ · services/ · app/ · 7,519 lines │ ├── test_core/ 18 files — one per core/ module (+ conftest) │ ├── test_api/ 18 files — one per router, httpx.AsyncClient │ └── test_services/ 5 files — design_chain, sweep, optimizer (+ conftest) │ ├── requirements.txt 14 ├── RUN_APP.bat 11 └── README.md 27

23 Core Modules, One Line Each

Every physics module in core/ does exactly one job and imports nothing from app/ or services/ — the dependency arrow only ever points inward. Grouped below by the chain they belong to, not by alphabet.

services/ & app/

design_chain.py is the composable Tabs 1–10 pipeline; sweep_service.py runs Tab 11's four named sweeps; optimizer_service.py backs Tab 15's optimizer, Pareto front, Sobol indices, and Monte Carlo stack-up. In app/, schemas/ holds one Pydantic module per tab and routers/ holds one FastAPI router per tab — every one of them exposing the same POST "" (run the tab) / GET "" (replay cached result) pattern.

Foundational

Shared Primitives

  • conventions.py sign/axis conventions & naming shared across every module
  • units.py SI ↔ display unit conversion, the DualValue wrapper
  • materials.py alloy property table — density, yield, UTS, E, fatigue
  • specific_speed.py Ns/Nss dimensionless groups, pump/turbine type charts
  • velocity_triangles.py Euler turbomachinery velocity-triangle solver
Pump Chain

Centrifugal Path

  • centrifugal_pump.py meanline sizing — Euler work, slip factor, ψ iteration
  • inducer_cavitation.py inducer sizing, NPSH margin, suction specific speed
  • volute_diffuser.py volute throat area, cross-section growth, diffusion ratio
Turbine Chain

Drive-Side Path

  • axial_turbine.py multi-stage axial meanline — Zweifel/AR loading
  • radial_turbine.py radial-inflow turbine meanline sizing
  • turbine_offdesign.py off-design matching, choked-nozzle corrected maps
  • hot_gas_properties.py RocketCEA drive-gas T/γ/MW for the turbine
Rotordynamics & Structure

Shaft & Blade

  • shaft_bearings.py FEM critical speed, Campbell diagram, bearing DN/loads
  • blade_disk_stress.py centrifugal + bending stress, Goodman fatigue margin
  • materials.py (shared) — alloy table feeding both stress checks
Real Data & Thermo

Fluids & Gas

  • propellant_properties.py CoolProp EOS — ρ, μ, vapor pressure, real fluids
  • drive_gas_balance.py GG/expander/staged-combustion shaft power balance
Statistics, Optimization & Export

Analysis & Output

  • monte_carlo.py truncated-normal tolerance stack-up across the chain
  • sensitivity.py Sobol S1/ST indices via SALib Saltelli sampling
  • database.py SQLite case-library save/load/compare of chain runs
  • report_export.py PDF/text design report generation from a converged chain
  • chilldown.py cryogenic line/pump chilldown thermal transient
  • blade_profile_geometry.py circular-arc camberline + parabolic thickness
  • cad_export.py DXF export of blade profiles and flow-path sections
Not Another Curve Fit

REAL DATA, NOT PLACEHOLDERS

Most preliminary-design tools at this level lean on a lookup table or a hand-fit correlation — calibrated once against a handbook or a textbook figure, frozen forever, and quietly wrong everywhere the calibration point wasn't. Three modules in this suite don't do that anymore. core/hot_gas_properties.py, core/propellant_properties.py, and core/drive_gas_balance.py each call the actual scientific-computing library the aerospace and combustion-chemistry community itself uses — live, on every request, at the exact operating point the user asked for — with the old digitized tables demoted to a documented, warned-on fallback rather than deleted outright.

RocketCEA — Real NASA CEA, Not a Table Lookup

core/hot_gas_properties.py used to run every gas-generator/preburner query through a small hand-digitized 5–6-point PCHIP-interpolated O/F table — a placeholder its own docstring flagged explicitly as standing in "for a full CEA/CEM tool for final gas-generator design." That placeholder is gone. Every call now goes through RocketCEA's CEA_Obj, a Python wrapper around the actual NASA CEA (Gordon & McBride, NASA RP-1311) Fortran code, and returns a genuine chamber-state thermochemical-equilibrium solve at the requested O/F and chamber pressure — not an interpolation between five fixed control points.

The old table isn't deleted, either — it's demoted to _FALLBACK_TABLES, used only if RocketCEA itself raises or silently returns a non-physical degenerate result (observed in practice: T=0 K / γ=0 with no exception, at O/F values outside CEA's numerically stable domain for a given propellant pair). Every fallback use appends a warning, so a caller can always tell a real solve from a rescued one.

RocketCEA · NASA CEA Fortran wrapper
OLD — digitized 5-point O/F table, LOX/RP-1 fuel-rich, O/F=0.30
T_gas = 1020.0 K · MW = 22.0 kg/kmol
γ = 1.133 · Cp = 2,100 J/(kg·K)
— core/hot_gas_properties.py: _LOX_RP1_FUEL_RICH (kept as _FALLBACK_TABLES, used only if RocketCEA itself fails)
NEW — real RocketCEA equilibrium solve, same O/F=0.30, Pc=2.0 MPa
T_gas = 1098.9 K · MW = 11.40 kg/kmol
γ = 1.127 · Cp = 10,795 J/(kg·K)
— core/hot_gas_properties.py: get_hot_gas_properties("LOX/RP-1 fuel-rich", 0.3)  (CEA_Obj.get_Tcomb / get_Chamber_MolWt_gamma / get_Chamber_Cp, eps=1)

MW nearly halves, Cp goes up almost 5×. Not a bug: at O/F=0.30 the combustion is deeply fuel-rich, and a real chemical-equilibrium solve puts most of the product mass into free H2 and CO — low molecular weight, high specific heat — exactly the regime a coarse hand-picked 5-point table, smoothed for convenience, never actually captured.

CoolProp — NIST-Grade EOS, Where It Exists

Every fluid in core/propellant_properties.py used to run on a hand-fit Antoine / Clausius–Clapeyron / Andrade correlation — reasonable, NIST-anchored engineering fits, but fits nonetheless, calibrated once at one or two reference points and frozen. Five of the eight working fluids now call CoolProp.CoolProp.PropsSI(...) directly for a real Span-Wagner-class, NIST-quality saturated-liquid/vapor equation of state, evaluated live along the whole saturation curve — not just at the old fit's anchor point.

The other three — N2H4, MMH, NTO — stay exactly as they were, on purpose. Confirmed directly: a PropsSI(..., 'Hydrazine')-style lookup for any of the three simply raises — they are not in CoolProp's fluid database. Silently working around that would be worse than being explicit about it.

Fluid Coverage Map

  • LOX → Oxygen  (real EOS)
  • RP-1 → n-Dodecane surrogate  (real EOS, ~8% density gap — see below)
  • LH2 → Hydrogen  (real EOS)
  • LCH4 → Methane  (real EOS)
  • Water → Water, IAPWS-95  (real EOS)
  • N2H4 not in CoolProp — hand-fit NIST Antoine, unchanged
  • MMH not in CoolProp — hand-fit NIST Antoine, unchanged
  • NTO not in CoolProp — Clausius–Clapeyron fit, unchanged
Cross-Check

LOX @ 90 K

ρ = 1142.1 kg/m³ (CoolProp) vs. 1141 kg/m³ (old NIST-Antoine hand fit) — within 0.1%. P_vapor = 0.0994 MPa vs. 0.0991 MPa — within 0.3%. The old fit was already careful at this one point; CoolProp now covers the entire curve, not just the anchor.

Honest Gap

RP-1 Surrogate @ 300 K

n-Dodecane (CoolProp, real EOS): 744.3 kg/m³. RP-1's own MIL-DTL-25576 spec density: ~810 kg/m³. A real ~8% gap, documented directly in the module — RP-1 is a multi-component kerosene blend with no single-species EOS of its own, and n-Dodecane is the standard preliminary-design surrogate, not a bug to hide.

Fidelity Upgrade

h_fg(T), Not a Constant

Latent heat of vaporization was one fixed reference value for every fluid before this upgrade. For the five CoolProp-backed fluids it's now a genuine saturation-curve enthalpy difference, h_vapor(T) − h_liquid(T), feeding Tab 4's Thermodynamic Suppression Head correlation with an actually T-dependent number.

Cantera — Real T-Dependent γ for Expander Drive Gas

core/drive_gas_balance.py's expander-cycle path used to pair a small digitized Cp(T) PCHIP table with a FIXED per-fluid reference γ — 1.40 for LH2, 1.28 for LCH4 — regardless of the actual heat-exchanger exit temperature. It now queries Cantera's gri30.yaml mechanism for the pure species backing each fluid (H2 for LH2, CH4 for LCH4), reading genuine NASA-9/7-polynomial ideal-gas thermo — gas.cp_mass, gas.mean_molecular_weight, and γ = cp_mass/cv_mass — all evaluated at the actual requested T_exit.

gri30.yaml is a combustion mechanism file, but querying one pure species' thermo state with zero reactions in play is standard, legitimate usage — it just reads the bundled NASA polynomial thermo data for one species at a time. The two rows on the right are the same fluid at two different exit temperatures — proof γ genuinely moves with T now, not a restated claim.

Cantera · GRI-Mech 3.0 species thermo
LH2 (pure H2) expander drive gas, P_exit=5 MPa
300 K: Cp = 14,311 J/(kg·K), γ = 1.4049
600 K: Cp = 14,511 J/(kg·K), γ = 1.3971
— core/drive_gas_balance.py: expander_drive_gas_properties("LH2", T_exit, 5e6)  (old fallback: fixed γ=1.40 at every T)
LCH4 (pure CH4) expander drive gas, P_exit=5 MPa
300 K: Cp = 2,229 J/(kg·K), γ = 1.3029
600 K: Cp = 3,287 J/(kg·K), γ = 1.1872
— core/drive_gas_balance.py: expander_drive_gas_properties("LCH4", T_exit, 5e6)  (old fallback: fixed γ=1.28 at every T)
Verified, Not Just Swapped

None of the three upgrades above shipped on faith. Each real-library result was cross-checked against the old implementation's own frozen digitized/fixed values at that value's own reference point first — LOX density and vapor pressure within 0.1–0.3% at 90 K, LH2 density within 0.3% at its 20.369 K NBP, LCH4 within 0.2% at its 111.66 K NBP, water vapor pressure and viscosity within 0.4% — before trusting the rest of each curve the old fit never covered. The exact-value test assertions those old tables used to support were deliberately rewritten as physical-plausibility sanity bounds (a real CEA/EOS/NASA-polynomial result won't reproduce a hand-digitized number bit-for-bit, and shouldn't be forced to), and one outright spec error was caught in the process rather than quietly matched: the master spec's illustrative LOX vapor-pressure figure (0.234 MPa @ 90 K) is physically inconsistent with oxygen's own 90.188 K normal boiling point at 1 atm — this module keeps the physically correct ~0.099 MPa CoolProp/NIST-Antoine result and documents the discrepancy explicitly instead. The full 379-test suite passes with all three real libraries live.

— tests/test_core/test_hot_gas_properties.py · test_propellant_properties.py · test_drive_gas_balance.py
Audited, Not Asserted

VALIDATION & ENGINEERING RIGOR

Real engineering tools get audited — every formula cross-checked against a hand-worked case, every hand-worked case cross-checked against the source it cites. This suite's master spec, 07_turbopump_prelim_sizing.md, is 1,100+ lines written before a line of physics code existed, and the build process didn't just implement it — it checked it. Below are both halves of that honestly: the 10 validation cases currently passing live against the running backend, and the real errors — in the spec itself, not just the code — that were caught and fixed along the way. A tool that never admits a mistake at this scope either had none, which is unlikely, or isn't being checked closely enough.

GET /api/validation/run-all · live backend response, 10/10 passing
# Case Expected Actual (live) Tolerance Status
1 Specific speed conversion Q≈554 gpm, H≈984 ft (unit cross-check); Ω_s ∈ [0.3, 1.0] Q=554.8 gpm, H=984.3 ft, Ω_s=0.9809 (Ns_US=2680.73, Nq=51.91) gpm/ft within 1% rel; Ω_s range check PASS
2 Euler equation sanity (no-slip, no-loss) H_euler = U2²/g ≈ 9174 m (U2=300 m/s, β2b=90°, σ_slip=1) H_euler = 9177.45 m rel 0.1% PASS
3 Wiesner slip factor σ_slip ≈ 0.8335 (Z=7, β2b=25°) — corrects the spec's own stated 0.78–0.80 σ_slip = 0.8335 rel 0.1% (4 decimal places) PASS
4 NPSH calculation (LOX) NPSH_available ≈ 18.1 m (P_vapor(90K)≈0.100 MPa, ρ(90K)≈1141 kg/m³) — corrects the spec's inconsistent 0.234 MPa figure NPSH_available = 18.11 m (P_vapor=0.0994 MPa, ρ=1142.1 kg/m³) rel 2% PASS
5 Suction specific speed reference band NPSH_required ≈ 20.6 m (N=20000 RPM, Q=554 gpm, S_US=20000) — corrects the spec's own arithmetic slip of ≈11.8 m NPSH_required = 20.56 m rel 2% PASS
6 Zweifel number check Zw ≈ 2.67 (α1=65°, α2=10°, s/cx=0.7), correctly flagged as exceeding the [0.8, 1.0] target band Zw = 2.672, flagged: exceeds [0.8, 1.0] — blade row over-loaded rel 1% PASS
7 Radial turbine U2/C0 check U2 = 0.7·C0 exactly; D2 ∈ [20, 300] mm at N=30000 RPM C0=634.90 m/s, U2=444.43 m/s (U2/C0=0.7000), D2=282.93 mm U2/C0 exact; D2 range check PASS
8 Critical speed order-of-magnitude I≈3.976e-8 m⁴, ω_cr ≈ 631 rad/s ≈ 6024 RPM (E=200 GPa, d=30mm, L=0.4m, m=15kg) I=3.9761e-08 m⁴, ω_cr=630.6 rad/s (6021 RPM) rel 2% PASS
9 Optimizer sanity (Tab 15.1) Optimizer η_overall matches direct chain η_overall within 0.5% at a tightly-pinned known-good point direct η_overall=0.407641, optimizer η_overall=0.408097 (0.1117% diff) < 0.5% relative PASS
10 Monte Carlo statistics (Tab 15.2) All tolerances zero → every sample collapses to nominal H_actual/η_overall/NPSH_margin (std==0, no NaN) std(H_actual)=5.684e-14 m, std(η_overall)=0.000e+00, std(NPSH_margin)=0.000e+00 m std < 1e-8–1e-12; no NaN PASS
Not Embarrassment — Evidence

CAUGHT DURING DEVELOPMENT

Seven independent errors, found by re-deriving the master spec's own worked numbers rather than trusting them, spanning slip-factor correlations, cryogenic fluid state, cavitation correlations, dimensional analysis, and a Monte Carlo tolerance table that looked complete but couldn't actually perturb the one output it claimed to analyze. Every one of these is documented in the source itself — not cleaned up after the fact.

Spec Error · Slip Factor

Wiesner Target Value

The spec's own worked example for Z=7, β2b=25° stated the Wiesner slip factor should land around 0.78–0.80. Independent recomputation of the same formula gives 0.8335 — the spec's illustrative figure was simply wrong, not the tolerance band. Corrected and cited directly in the validation case.

core/velocity_triangles.py · tests/test_core/test_velocity_triangles.py
Spec Error · Cryogenic State

LOX Vapor Pressure

The spec paired a LOX vapor-pressure figure of 0.234 MPa with a 90 K inlet case — but 0.234 MPa is LOX's saturation pressure at ≈99 K, not 90 K, and is inconsistent with oxygen's own 90.188 K normal boiling point at 1 atm. The physically correct value, ≈0.099–0.100 MPa, is what CoolProp/NIST-Antoine actually returns at 90 K — kept, with the discrepancy documented rather than silently matched to the wrong figure.

core/propellant_properties.py · test_propellant_properties.py
Spec Error · Arithmetic

Suction Specific Speed

The spec's own worked NPSH_required example, inverting S_US=20000 at N=20000 RPM/Q=554 gpm, had an arithmetic slip and stated ≈38.6 ft (≈11.8 m). Recomputing (N·√Q/S_US)^(4/3) by hand gives ≈67.5 ft ≈ 20.6 m — nearly double the spec's stated figure. Corrected, with the hand-recomputation shown alongside the validation case.

core/specific_speed.py · test_specific_speed.py “SPEC DISCREPANCY NOTE”
Spec Error · Dimensional

Leakage-Loss Power

The spec's Step 7 formula, literally P_leak = mṱ_leak·g·H_euler / ρ, is dimensionally inconsistent — the divide-by-ρ leaves units of m⁵/s³, not watts, since mṱ_leak already carries a factor of ρ. Dropped the erroneous , leaving the physically correct P_leak = leakage_fraction·P_euler.

core/centrifugal_pump.py, module docstring point 3
Spec Error · Wrong Velocity

Soderberg Nozzle Loss

The spec's stage-efficiency formula used W1 (relative velocity) in the nozzle/stator loss term — but a stationary row has no physical relative-velocity frame; per Dixon & Hall, its loss coefficient is defined against the absolute velocity leaving it, C1. Corrected to ζ_nozzle·C1²/2.

core/axial_turbine.py, module docstring: “SPEC ERROR, CORRECTED”
Spec Error · Unit Mismatch

Tip-Clearance & Secondary Loss

The same stage-efficiency step added tip-clearance and secondary-flow loss as raw energy-unit terms bolted directly onto the numerator — but they are dimensionless loss coefficients, not energy terms. Folded them into ζ_rotor_total before multiplying by W2²/2, restoring dimensional consistency.

core/axial_turbine.py, module docstring point 1
Spec Gap · Tab 15.2 Monte Carlo Tolerance Table

The spec's default Tab 15.2 tolerance table listed four parameters — D2, b2, β2b, clearance — to Monte Carlo-perturb through the full chain. But under the spec's own Tab 4 formula set, NPSH_required depends only on N, Q, and target S_US, or on inducer tip geometry via W1_tip — none of those four listed parameters can perturb NPSH_margin at all. That would make the tab's own headline output, the probability of an NPSH-margin violation, degenerate at zero variance for every single sample — a tolerance table that looked complete but had a genuine physics gap in exactly the output it claimed to analyze. Fixed by adding D1 (impeller-eye/inducer-matching diameter, ±0.05 mm default) to the table and routing the Monte Carlo's NPSH chain through the cavitation-number path, where the inducer tip radius is sized from D1 — so NPSH_margin actually scatters sample-to-sample, and Validation Case 10's zero-tolerance degenerate check has something real to collapse from.

— core/monte_carlo.py, module docstring & TOLERANCE TABLE comment (“judgment call 3's D1 addition”) · 07_turbopump_prelim_sizing.md §15.2
Development History

HOW TURBO FORGE GOT HERE

The suite was built in two passes: a 15-phase greenfield build (spec → core physics → services → FastAPI → frontend, one phase at a time, each verified before the next started), then a 16-phase upgrade pass that replaced every remaining approximation with real data and added the capability an ISRO-grade prelim-design tool actually needs.

Build Phases 0–14 · Complete ✓

GREENFIELD BUILD

core/ physics (18 modules) services/ orchestration FastAPI + Pydantic v2 backend Static JS website, 15 tabs 212 passing tests 10/10 validation cases
Upgrade Phases R1–R14 · Complete ✓

REAL DATA & ADVANCED PHYSICS

RocketCEA / CoolProp / Cantera Japikse & Craig-Cox loss models Quasi-3D span/loading distributions FEM + gyroscopic + torsional rotordynamics Disk stress & Goodman fatigue NPSH(Q) curve + cryogenic chilldown Genuine N-stage pump loop NSGA-II Pareto optimizer Sobol sensitivity + tornado chart Async background optimizer jobs DXF blade-profile export Case library version history
Content Phases R15–R16 · Complete ✓

DOCUMENTATION AS A FIRST-CLASS DELIVERABLE

Theory & References, 16 new sections Formula Reference tab (16th tab) This website
Explicitly Out of Scope

WHAT THIS ISN'T

Bridge to the sibling CEM rocket-engine sizer CI pipeline Docker packaging Full 3D STEP export (DXF 2D only)
Why It's Built This Way

Every upgrade phase followed the same discipline: read the existing, working code and tests first; make the smallest change that adds real capability without breaking what already worked; add tests that prove the new path AND that the old default path is byte-for-byte unchanged; verify live against a running server, not just unit tests; report exact before/after numbers, not a summary. That discipline is why a tool this large — ~28,000 lines of Python, ~7,800 lines of frontend — still passes all 379 of its own tests and all 10 of its own worked engineering examples, every time, on demand.