Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
db6ef59
Add numba physics kernels and 3-way Layer-1 benchmark, declare numba dep
swissskimmilk Sep 4, 2026
c308962
Free numba kernels from the C EdEngineState with pure-Python param ex…
swissskimmilk Sep 4, 2026
235cd2b
Port ablative cooling to numba, closing the last coverage gap vs the …
swissskimmilk Sep 4, 2026
f3ee62d
Add live A/B parity suite for the numba accelerator vs C and Python
swissskimmilk Sep 4, 2026
aea1d6a
Move accelerator into engine/accel and drop the C injector solve from…
swissskimmilk Sep 4, 2026
53a610b
Route production through engine.accel: solve, chamber_solve, dispatch…
swissskimmilk Sep 4, 2026
c3296fd
CI: replace native-parity with a matrixed accel-parity job covering n…
swissskimmilk Sep 4, 2026
c4e56d6
Vectorise the chug sweep and add a numba kernel for it, removing the …
swissskimmilk Sep 4, 2026
80d023d
Retire the C oracle from the parity suite; guard numba against Python…
swissskimmilk Sep 4, 2026
4561b11
Drop the ED_ACCEL=c dispatch branch now that the parity suite no long…
swissskimmilk Sep 4, 2026
f76e667
Delete the C physics port; numba is the accelerator
swissskimmilk Sep 4, 2026
1a13957
Port the pintle injector to numba, extending accelerated coverage bey…
swissskimmilk Sep 4, 2026
fb84c7d
Discriminate unsupported-config from non-convergence in the accelerat…
swissskimmilk Sep 4, 2026
9f8588c
Fix parity suite under strict mode: forcing the Python reference is n…
swissskimmilk Sep 4, 2026
761fee3
Add a guard that production actually calls the accelerator, not just …
swissskimmilk Sep 4, 2026
2388118
Strengthen the bypass guard: per-candidate ratio, chug seam, call-sit…
swissskimmilk Sep 4, 2026
4418536
Fix Layer 1 convergence plot x-axis, evaluation counter, and chart le…
swissskimmilk Sep 4, 2026
e347ccc
Memoize decompressed CEA tables so a Layer 1 run reads the .npz once,…
swissskimmilk Sep 4, 2026
b4c9f1b
Floor CMA iter_budget at 1 so max_iterations below the restart count …
swissskimmilk Sep 4, 2026
ce92dd5
Drop two redundant deepcopies of the worker evaluate() payload
swissskimmilk Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 43 additions & 70 deletions .github/workflows/engine-design-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ jobs:
continue-on-error: true

# Browser E2E (Playwright). Playwright's webServer auto-boots the FastAPI
# backend (ED_USE_NATIVE=0 -> pure Python, so no C build here) and Vite, so
# backend (ED_ACCEL=off -> pure Python) and Vite, so
# this job just needs Python deps, node deps, and Chromium. The guard that
# matters is connect-no-loop.spec.ts: it asserts idle /api/config/load traffic
# settles instead of the ~7 req/s bootstrap fetch loop that hung the UI. tsc
Expand Down Expand Up @@ -218,40 +218,17 @@ jobs:
if-no-files-found: warn
retention-days: 7

# Native C physics kernel: compile libed_physics and run its golden-vector /
# unit tests via CTest. Fully self-contained — golden vectors are committed
# (engine/native/tests/golden/), so no rocketcea/Python is involved. ubuntu
# ships gcc + cmake. ED_NATIVE_ARCH=OFF drops -march=native for a portable,
# runner-independent build; chamber_golden self-skips (exit 77) until that
# stage lands (CTest reports it skipped, not failed).
native-kernel:
name: Native kernel (C golden/unit tests)
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Configure + build (portable — no -march=native)
working-directory: EngineDesign/engine/native
run: cmake -S . -B build -DED_NATIVE_ARCH=OFF && cmake --build build -j

- name: Run CTest suite
working-directory: EngineDesign/engine/native
run: ctest --test-dir build --output-on-failure

# Python <-> native parity: re-run the impinging tests with ED_USE_NATIVE=1 so
# chamber_solver's residual/Brent loop and closure's injector flows route
# through the C kernel. autobuild compiles libed_physics on demand.
# Accelerator parity: the numba Layer-1 accelerator vs the authoritative Python
# physics, diffed live on identical inputs.
#
# CRITICAL: the native path silently falls back to Python on any load failure
# (chamber_solver._native_chamber_pc), so `ED_USE_NATIVE=1 pytest` alone can be
# a FALSE GREEN — everything runs on Python and passes. The pre-flight below
# positively proves the lib compiles + ctypes-loads (ensure_lib/load raise on
# failure) before the parity run, so a broken native path fails loudly here.
native-parity:
name: Python-native parity (ED_USE_NATIVE=1)
# CRITICAL: every accelerated entry point returns None on any failure and the
# caller silently falls back to Python (chamber_solver._accel_chamber_pc,
# closure.flows), so `pytest` alone can be a FALSE GREEN — everything runs on
# Python and passes. The pre-flight positively proves the accelerator is real,
# and ED_REQUIRE_ACCEL=1 turns a genuine failure into a raise, not a quiet
# fallback.
accel-parity:
name: Accelerator parity (numba vs Python)
runs-on: ubuntu-latest
timeout-minutes: 15

Expand Down Expand Up @@ -280,52 +257,50 @@ jobs:
- name: Verify committed CEA cache is present
run: test -f output/cache/cea_cache_LOX_CH4_3D.npz

# Positive proof the native path is real (not a silent Python fallback):
# ensure_lib() compiles and raises on build failure; load() ctypes-loads and
# raises if broken; available() confirms the injector shim is wired.
- name: Pre-flight — native library compiles and loads
env:
ED_USE_NATIVE: '1'
# Positive proof the accelerator is real, not a silent Python fallback.
- name: Pre-flight — accelerator is genuinely available
run: |
python -c "
from engine.native.python import autobuild, ed_native, native_injector
p = autobuild.ensure_lib(verbose=True)
ed_native.load(p)
assert native_injector.available(), 'native_injector.available() is False'
print('native lib built + ctypes-loaded OK:', p)
import numba
from engine import accel
assert accel.available(), 'accel.available() is False (numba missing?)'
assert accel.enabled(), 'accel.enabled() is False'
print('accel enabled, numba', numba.__version__)
"

# ED_REQUIRE_NATIVE=1 makes a genuine native failure (lib won't build/load,
# native not enabled, or a solver error) RAISE instead of silently falling
# back to Python — so this job can't pass on the Python path and report a
# false green. NOTE: since the runtime parity self-checks were removed
# (capability-dispatch architecture), this flag alone only proves native RAN;
# it no longer compares native's numbers to Python's.
- name: Run impinging tests through the native path (strict)
# ED_REQUIRE_ACCEL=1 makes a genuine accelerator failure RAISE instead of
# falling back, so this step cannot pass on the Python path and report a false
# green. It proves the backend RAN; the numeric comparison is the next step.
#
# test_accel_is_actually_used closes a different false-green: every other check
# proves the accelerator WORKS, none proves production still CALLS it. With the
# accel.evaluate call removed from Layer-1's _eval_candidate, all of them stay
# green while the optimizer silently runs ~120x slower on Python. That test
# asserts the real call sites are reached, and is mutation-verified against
# both the Layer-1 and chamber_solver/closure seams.
- name: Run impinging tests through the accelerator (strict)
env:
ED_USE_NATIVE: '1'
ED_REQUIRE_NATIVE: '1'
ED_ACCEL: 'numba'
ED_REQUIRE_ACCEL: '1'
run: |
python -m pytest \
tests/test_layer1_impinging_vector.py \
tests/test_flow_capacity_effective_area.py \
tests/test_accel_is_actually_used.py \
tests/test_accel_outcomes.py \
--timeout=180 --timeout-method=thread -q

# Live A/B numeric parity: run the C kernel and the authoritative Python
# physics on the same inputs and diff the outputs field by field — at the
# wrapper level (the dict Layer-1 consumes) AND the raw EdEvaluateResult
# struct level (the C kernel's own numbers, no Python shim). This is the
# actual parity enforcement for this job: unlike the golden-vector C tests
# (frozen snapshots that go stale when the PYTHON side changes), a live
# comparison fails no matter which side drifted. ED_REQUIRE_NATIVE=1 turns
# the test's no-toolchain skip into a hard failure here.
- name: Live A/B parity — native vs Python on identical inputs (strict)
# Live A/B numeric parity: run the accelerator and the authoritative Python
# physics on the same inputs and diff them field by field. A live comparison
# fails no matter which side drifted — unlike a frozen snapshot, which goes
# stale silently when the Python side changes.
- name: Live A/B parity — numba vs Python on identical inputs (strict)
env:
ED_USE_NATIVE: '1'
ED_REQUIRE_NATIVE: '1'
ED_REQUIRE_ACCEL: '1'
ED_AB_PARITY: '1'
run: |
python -m pytest \
tests/test_native_ab_parity.py \
tests/test_numba_ab_parity.py \
--timeout=300 --timeout-method=thread -v

# Aggregate status, mirroring daq-server-ci.yml's build-summary.
Expand All @@ -337,8 +312,7 @@ jobs:
- backend-smoke
- frontend
- frontend-e2e
- native-kernel
- native-parity
- accel-parity
if: always()

steps:
Expand All @@ -353,5 +327,4 @@ jobs:
echo "| Backend smoke | ${{ needs.backend-smoke.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Frontend build | ${{ needs.frontend.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Frontend E2E (Playwright) | ${{ needs.frontend-e2e.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Native kernel (C tests) | ${{ needs.native-kernel.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Python-native parity | ${{ needs.native-parity.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Accelerator parity (numba vs Python) | ${{ needs.accel-parity.result }} |" >> $GITHUB_STEP_SUMMARY
54 changes: 29 additions & 25 deletions EngineDesign/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Liquid Rocket Engine Design Pipeline

A comprehensive physics-based simulation and **multi-layer optimization pipeline** for liquid bipropellant rocket engines. The propellants and the injector type are **whatever you put in the config** — the whole point is to evaluate and compare different engine designs, not to model one fixed engine. Presets ship for **LOX/RP‑1** and **LOX/CH₄ (methalox)** with **pintle** or **impinging** injectors. Takes tank pressures as input and solves for chamber pressure, mass flow rates, thrust, and all performance parameters, accelerated by a native C physics kernel.
A comprehensive physics-based simulation and **multi-layer optimization pipeline** for liquid bipropellant rocket engines. The propellants and the injector type are **whatever you put in the config** — the whole point is to evaluate and compare different engine designs, not to model one fixed engine. Presets ship for **LOX/RP‑1** and **LOX/CH₄ (methalox)** with **pintle** or **impinging** injectors. Takes tank pressures as input and solves for chamber pressure, mass flow rates, thrust, and all performance parameters, accelerated by a Numba‑compiled physics kernel.

## Overview

Expand All @@ -10,37 +10,42 @@ A comprehensive physics-based simulation and **multi-layer optimization pipeline
- Full flow path simulation: tank → feed system → injector → combustion → nozzle → thrust
- **Injector modes:** pintle (`injector.type: pintle`) or twin-jet **impinging** (`injector.type: impinging`); see `docs/optimizer_readme.md` (Injector types) and `configs/canonical/impinging.yaml`
- **Propellants:** LOX/RP‑1 and **LOX/CH₄ (methalox)** via propellant presets; canonical seeds in `configs/canonical/`
- **Native C physics kernel** (`engine/native/`): the chamber solve + stability hot path runs in C, making `evaluate()` ~**88× faster** with machine‑precision parity — see [Native physics kernel](#native-c-physics-kernel-performance)
- **Numba physics accelerator** (`engine/accel/`): the chamber solve + nozzle + stability hot path is JIT‑compiled, making a Layer‑1 candidate ~**120× faster** — see [Numba physics accelerator](#numba-physics-accelerator-performance)
- Multi-layer optimization for complete engine design (geometry, pressure curves, thermal protection)
- Time-varying analysis with ablative recession tracking
- Stability analysis (chugging, acoustic, feed-system coupling)
- Flight simulation validation via RocketPy integration

## Native C physics kernel (performance)
## Numba physics accelerator (performance)

The evaluation hot path — chamber‑pressure solve (injector → CEA → combustion
efficiency → ablative cooling → Brent root‑find) plus the chug/acoustic stability
sweep — is implemented as a standalone **C11 library under `engine/native/`** and
wired into the live path. It is an **opt‑in accelerator with automatic Python
fallback**, not a rewrite: the Python physics remains the reference implementation
and is used whenever native is disabled or a config isn't covered.

- **Enable:** the FastAPI backend sets `ED_USE_NATIVE=1` automatically at startup
(and prebuilds the library), so the **frontend optimizer uses it out of the box**.
For CLI/scripts, `export ED_USE_NATIVE=1`. Set `ED_USE_NATIVE=0` for pure Python.
- **Auto‑build:** on first use the library is compiled with CMake into an
arch‑tagged directory — no manual build step. Requires a C compiler + CMake.
- **Parity & safety:** a one‑time self‑check compares the native result against
Python and falls back on any mismatch. Measured agreement: chamber Pc ~5e‑10,
CEA/stability ~1e‑16. A full `runner.evaluate()` matches Python to ~5e‑10.
- **Speed:** chamber solve ~400× faster; full `evaluate()` ~88× (≈68 ms → ≈0.8 ms),
which is what makes the 15000‑eval Layer‑1 optimizer runs finish in seconds.
efficiency → ablative cooling → Brent root‑find), the nozzle/thrust step, and the
chug stability sweep — is JIT‑compiled with **Numba** under `engine/accel/`. It is
an **accelerator with automatic Python fallback**, not a rewrite: the Python
physics remains the reference implementation and runs whenever the accelerator is
disabled or a config isn't covered.

- **Enable:** on by default. Set `ED_ACCEL=off` for pure Python. `numba` is a
declared dependency (`requirements-base.txt`); if it is missing the accelerator
reports itself unavailable and everything runs on Python rather than failing.
- **No build step:** kernels compile on first use and cache to `__pycache__`
(`@njit(cache=True)`). `accel.warmup()` front‑loads that in the parent process
and in each Layer‑1 pool worker, since the cache is not shared memory.
- **Parity:** enforced ahead of time by `tests/test_numba_ab_parity.py`, which
runs the accelerated and Python paths live on identical inputs and diffs them
field by field. There is **no runtime self‑check**. Measured agreement is
~2.5e‑9 (Brent convergence tolerance, not rounding); the suite asserts 1e‑6.
- **Speed:** ~120× per Layer‑1 candidate (≈327 ms → ≈2.7 ms), which is what makes
the 15000‑eval Layer‑1 optimizer runs finish in seconds. The chug stability
sweep is ~53× (1537 µs → 29 µs); roughly a quarter of that came from
vectorising the pure‑Python sweep, which speeds up uncovered configs too.
- **Coverage today:** impinging injector + ablative cooling + advanced efficiency.
Pintle/coaxial, film/regen‑coupled cooling, and the nozzle/thrust step still run
in Python (the native path falls back automatically for those).
Pintle/coaxial and film/regen‑coupled cooling still run in Python (the
accelerator returns `None` and the caller falls back automatically).

See `engine/native/README.md` for build details, the staged port plan, and the
parity/benchmark methodology.
This replaced a hand‑written C11 port that lived at `engine/native/`. The Numba
path reached numeric parity with it, then measured faster (the C build, its
ctypes marshalling and golden‑vector suite are gone with it).

## Architecture

Expand Down Expand Up @@ -162,7 +167,7 @@ EngineDesign/
│ │ ├── analysis.py
│ │ └── coupling.py
│ │
│ ├── native/ # Native C physics kernel (opt-in accelerator)
│ ├── accel/ # Numba physics accelerator (JIT hot path)
│ │ ├── README.md # Build, staged port plan, parity/benchmarks
│ │ ├── CMakeLists.txt # C11 build (auto-built on first use)
│ │ ├── include/ # Public headers (ed_*.h)
Expand Down Expand Up @@ -485,7 +490,6 @@ See the `docs/` folder for additional documentation:
- `docs/CONFIG_SYSTEM.md` - Config model: two canonical configs, propellant presets, in-memory switch, burn-time sync
- `docs/flight_simulation.md` - `/simulate` endpoint, tank-capacity resolution, and propellant regimes
- `docs/flight_altitude_optimization.md` - Minimum-fuel burn-time optimization for a target apogee
- `engine/native/README.md` - Native C physics kernel: build, staged port plan, and parity/benchmark methodology

**Control System Documentation:**
- `docs/control/README.md` - Control system overview
Expand Down
32 changes: 17 additions & 15 deletions EngineDesign/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,23 @@
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))

# Enable the native physics kernel (engine/native) for backend-launched work,
# including the Layer-1 optimizer. This MUST run before the engine/optimizer
# modules import and before the Layer-1 ProcessPool spawns, so worker processes
# inherit the flag. Opt out with ED_USE_NATIVE=0. The native path self-checks
# against Python on first use and falls back automatically on any mismatch, so
# enabling it cannot change results — only speed (chamber solve ~400x; an
# optimizer candidate ~60x).
os.environ.setdefault("ED_USE_NATIVE", "1")
if os.environ.get("ED_USE_NATIVE") == "1":
try:
from engine.native.python import autobuild as _ed_autobuild
_ed_lib = _ed_autobuild.ensure_lib() # build once here so pool workers don't race
print(f"[native] kernel enabled (ED_USE_NATIVE=1): {_ed_lib}")
except Exception as _ed_err: # pragma: no cover - native is best-effort
print(f"[native] kernel unavailable, using Python path: {_ed_err}")
# Warm the physics accelerator (engine/accel) for backend-launched work, including
# the Layer-1 optimizer. This MUST run before the engine/optimizer modules import
# and before the Layer-1 ProcessPool spawns. Opt out with ED_ACCEL=off.
#
# Correction to what this comment used to claim: there is NO runtime self-check
# against Python (see engine/accel/__init__.py).
# Equivalence is enforced ahead of time by tests/test_numba_ab_parity.py, which
# diffs the accelerated and Python paths live on the same inputs.
try:
from engine import accel as _accel
if _accel.enabled():
_accel.warmup()
print("[accel] numba kernels warmed (ED_ACCEL=numba)")
else:
print("[accel] disabled; using the Python path")
except Exception as _ed_err: # pragma: no cover - accelerator is best-effort
print(f"[accel] unavailable, using Python path: {_ed_err}")

# Import control router first (required for controller)
from backend.routers import control
Expand Down
Loading
Loading