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.
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.
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.
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.
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.
The chain calls into core/ modules — pure, framework-agnostic numerics plus real solvers: RocketCEA, CoolProp, Cantera, brentq root-finds, FEM assembly.
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.
The JSON lands back in the browser and Plotly.js draws velocity triangles, performance maps, Campbell diagrams — all client-side, zero server-rendered images.
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.
One process, one port, zero build tooling — uvicorn 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.
_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.
Uvicorn ASGI server, APIRouter per tab, Pydantic v2 models for every request and response — validation and serialization, not hand-written dict-parsing.
Vectorized array math and brentq/fsolve root-finding underpin every iterative convergence — pump psi-coefficient solves, turbine off-design matching, FEM eigenvalue extraction.
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.
LiveMulti-objective genetic optimization for Pareto sweeps, scipy.optimize.differential_evolution for single-objective searches, SALib Sobol indices for sensitivity analysis.
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.
LiveZero 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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_deg → pump.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.
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".
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.
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.
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.
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.pymoo's NSGA-II searches the identical decision vector against three simultaneous objectives, returning the non-dominated front plus a hypervolume indicator.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./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.f1 = −η_overall (maximize efficiency)f2 = D2 (minimize impeller size proxy)f3 = −NPSH_margin (maximize cavitation margin)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
ρ = 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.
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.
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.
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.
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.
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.
| # | 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 |
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.