|
STAR
Cal STAR — firmware, DAQ server, and engine design
|
Directories | |
| feedtwin | |
| tests | |
The propellant feed system physics core: real-gas properties, a component library, a network solver, and the transient response of the whole system from COPV to injector face.
This is a library, not a service. It has two callers with incompatible needs — the feed-twin web app, and EngineDesign's optimizer calling it in-process thousands of times per run — and only a plain importable package serves both. Nothing here imports a web framework, and a test enforces that. See ADR-0001.
Phase 04 — the steady network solver is in. Pressure at every node, flow in every branch — the question this project started from.
Unknowns are one pressure per free node and one flow per branch; equations are one mass balance per free node and one pressure relation per branch. Almost all of the Jacobian is the constant incidence matrix — only one scalar per branch is nonlinear, so a new component needs no derivative to work with the solver.
Mass residuals are reported on every run, never sampled. A Newton solve will happily converge on a system that does not conserve mass, and that failure looks exactly like success.
Steady, isothermal, single-phase. Node temperatures are given rather than solved. Phase 05 adds the gas side, Phase 07 the transient.
Phase 03 — the liquid-side component library. Pipes, fittings, orifices, valves and check valves, every correlation from fluids.
Validated against things outside this codebase — a laminar pipe against Hagen–Poiseuille exactly, a fitting against the definition of K, and a valve against the definition of Cv itself (one US gpm of water, one psi), which it reproduces to 0.07% independently of bore.
Each component exposes its physics twice: pressure_drop(mdot, flow) is the causal form that the validation tests call directly, and residuals(ctx) is the acausal form the Phase 04 solver will consume. That interface is still provisional — keeping the physics in pressure_drop is what makes reshaping it cheap.
Every type also carries a measured model that reads a flow-against-drop curve and supersedes the correlation outright. Unlike the property layer, it does not fall through outside its range: a property has a defensible fallback in an equation of state, a component does not, and switching to a correlation halfway up a flow sweep would put a kink in the result nobody would see.
Phase 02 — the parameter and component model. Every number describing hardware carries where it came from, component schemas are data, and a part number resolves into a configured component.
There is no default for source. A number with no provenance is a validation error, because "nobody remembers where this came from" is the state this layer exists to make impossible — and it is the state most of a propulsion team's numbers are in by the second year.
Three things follow from the schema being data:
Phase 01 — the property layer. Real-gas properties for LOX, ethanol, nitrogen and helium, sub-microsecond, with measured data able to override the equation of state.
| Module | Phase | What lands there |
|---|---|---|
| feedtwin.props | 01 | Real-gas properties, tabulated — done |
| feedtwin.model | 02 | Param, ComponentSpec, the part catalog — done |
| feedtwin.comps | 03 | Pipes, fittings, valves, orifices — done |
| feedtwin.solve | 04 / 07 | Steady network — done — then the transient DAE |
| feedtwin.io | 11 | The P&ID reader, DAQ readers |
| feedtwin.fit | 12 | Parameter identification from test data |
A chain, not a branch. A fluid holds an ordered list of backends. Tabulated interpolation answers ordinary states in ~0.13 µs; the Helmholtz equation of state behind it answers whatever the tables refuse. Put measured data at the front and it wins wherever it has coverage:
Nobody writes the routing. That is the Phase 12 principle — anything modelled can be replaced by something measured — working from day one, because a seam added later is a seam every existing call site has to be re-audited against.
Refuse, never extrapolate. Outside its envelope a backend raises OutOfRange and the chain moves on. No backend here will return a plausible number for a state point it cannot actually reach, and physically impossible inputs (negative pressure, NaN) are rejected before any backend is touched — otherwise CoolProp reports a failed Brent bracket, which is true and useless.
Fluids are data. Species live in props/species.toml. Adding a propellant is a data edit; register_species() adds one at runtime without touching the package at all. Same for backends (register_backend) and properties (register_property) — each has a test in tests/test_props_modularity.py that extends the package from outside, so the seams cannot rot silently.
| per call | use | |
|---|---|---|
| fluid.accessor(prop, "p", "T") | ~0.63 µs | inside a solve loop |
| fluid.get(prop, p=…, T=…) | ~0.96 µs | one-off queries |
| fluid.state(p=…, T=…) | ~5 µs | reports, frames, diagnostics (12 reads) |
The accessor resolves once what get re-derives per call — which state pair the keywords form, which backends support the property. Both are far under the 184 µs that motivated this layer.
CoolProp builds interpolation tables on first use: 2–3 s and ~17 MB per fluid, cached in ~/.CoolProp/Tables. Call feedtwin.props.warmup() when a worker process starts so the cost does not land inside the first solve someone is timing — the same reason EngineDesign's accelerator front-loads its JIT.
From a path, like lib/stardesign — repo-internal, versioned with the repo, never published:
Almost none of this physics should be written here. Each dependency carries a body of validated correlations that would take years to reproduce and would be worse:
| Package | Carries | Used from |
|---|---|---|
| CoolProp | Helmholtz-energy equations of state, 122 fluids, tabular backends | Phase 01 |
| fluids | 40+ friction correlations, 244 fittings (Crane TP-410, Hooper 2K, Darby 3K), IEC 60534 valve sizing, ISO 5167 meters, two-phase | Phase 03 |
| ht | Convection, boiling, condensation, insulation resistance | Phase 05, 14 |
| SciPy | Radau/BDF integrators, sparse LU, least-squares | Phase 04, 07, 12 |
Library calls are wrapped, never made raw from solver code. Two reasons: the choice of correlation becomes a parameter like anything else (Decision 02 in the plan — nothing hardcoded), and the untyped third-party surface stays small enough for mypy --strict to be worth running.
Never call PropsSI. Measured on the development host, nitrogen density at one state point:
| Call path | Per call | Throughput |
|---|---|---|
| PropsSI('D', 'T', t, 'P', p, 'Nitrogen') | 184.5 µs | 5.4 k/s |
| AbstractState('HEOS', ...), reused | 3.18 µs | 315 k/s |
| AbstractState('BICUBIC&HEOS', ...) | 0.14 µs | 7.1 M/s |
A 1300× spread across three spellings of one line. PropsSI re-parses the fluid name and rebuilds its backend on every call; the low-level interface does that once. A stiff transient evaluates properties millions of times over Jacobian assembly, so this is the difference between a run that finishes in seconds and one that finishes overnight.
tests/test_property_call_discipline.py fails the build on any PropsSI, Props1SI, HAPropsSI or PhaseSI call under feedtwin/. Tests are exempt — comparing the two paths is how the numbers above were obtained, and Phase 01's regression suite needs the reference implementation to check the tabulated one against.