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.

Sources

REFERENCES

The turbomachinery loss models, slip factors, and structural checks used across all 16 tabs trace to these.