Skip to content

EngineDesign overnight sweep: solver window, config-driven stability, flight dedupe, lockout heartbeat, UI cleanup - #58

Closed
Carlsaurus wants to merge 5 commits into
mainfrom
engine/overnight-sweep
Closed

Carlsaurus wants to merge 5 commits into
mainfrom
engine/overnight-sweep

Conversation

@Carlsaurus

Copy link
Copy Markdown
Contributor

Overnight sweep of EngineDesign: Layer 1, the flight/propellant optimizer, the stability model, and the frontend. Stacked on #56 and #57 (their commits are included here; merge those first or merge this one and close them).

Physics that was wrong

  • Chamber-pressure solver returned negative thrust on the shipped default config. The supply−demand residual is not monotonic in Pc: negative below ~20 psi (unchoked, efficiency collapses), positive through the operating range, negative again once the injector drop gets small. With Pc_min = 1 bar, Brent converged to the spurious 20 psi root — configs/default.yaml evaluated to F = −333 N at its own configured tank pressures (815/677 psi). The hardcoded 15% "feed loss margin" ceiling also excluded real soft-injector operating points, and the Python solver's endpoint-only sign test reported "no solution" at 550/650/900 psi where the numba kernel found the root. Fix: choked-flow floor (2 atm), tank-pressure ceiling (min tank − 2%), and a highest-Pc bracket scan, mirrored verbatim in both paths. Both now agree at every pressure; canonical results unchanged (0 diffs in a 6-combo snapshot).
  • Stability model was methalox with the serial numbers filed off. Fuel latent heat/boiling-point fallbacks were methane's for every fuel; film-gas cp was a fixed 2200 J/kg·K (now CEA γR/(γ−1)); feed-line length for the chug inertance was a hardcoded 0.305 m with no schema field; nozzle-entrance Mach was 0.2 (now solved from the contraction ratio); comprehensive_stability_analysis looked up feed_system["lox"].length — neither the key nor the attribute ever existed — so feed acoustics always used 1.0 m × 10 mm; the legacy and rich paths used different chamber lengths (1L mode 1772 Hz vs 1284 Hz for the same chamber). New StabilityConfig block (n, χ, Mach, damping fractions, regulator, gate allowance) and feed_system.<side>.length, all editable in the Configuration tab. Chug gate centred at gain margin 1.0 (it was 0.80, which called an unstable loop "stable"). Removed: stability_index floors, the water-hammer→margin piecewise map (one branch was literally a constant), and stability/enhanced.py (pintle spatial model fed hardcoded 50/30 m/s velocities; every output was overwritten).
  • Ea mixture-ratio step in reaction_chemistry smoothed (20% jump at MR 1.5); the dead reaction-progress computation on every chamber solve removed.
  • Layer 1 now refuses a target O/F outside the propellant's CEA table (a propellant switch keeps the previous target; the optimizer used to run against a table that could not evaluate it and returned objective 7317 with nothing to say why), names the hard constraint that held a run infeasible instead of "objective inf / validation failed", and no longer caps chamber length at 1 m. Canonical pintle design_MR 2.55 → 1.4 (ethalox table is [1.0, 2.5]).

Flight

  • Optimizer Layer 4 is now the shared FlightSimulation component. The 1100-line copy it replaced hardcoded RP-1 density and an 85% fill factor client-side, so every non-kerolox design capped its fuel load against the wrong tank.
  • explicit_capacity_kg in the flight diagnostics carried a bool; rocket/environment fallbacks derived from the request models (the literal copy said propulsion dry mass 24 kg while its own component defaults summed to 16).
  • tests/test_flight_propellant_iteration.py read a config out of ~/Downloads; rebuilt on configs/default.yaml, and the tank-cap test now constructs its premise. That test also caught the bool bug.

The lockout ("Take Design 1 before saving — your checkout has lapsed")

Reproduced on an isolated server: hold the checkout, do nothing for five minutes, the next autosave returns 423. The server frees a checkout with no heartbeat inside lock_ttl (300 s); the only heartbeat was an autosave, and autosave writes only when the design changed; the hook also released the token on visibilitychange: hidden, so tabbing away mid-optimization made the run's result write 423. useCheckout now re-takes every 60 s while held and only releases on pagehide.

UI

  • Emoji removed everywhere (118 sites).
  • Legend and axis-title overlap fixed on every chart with the pattern (Layer 1 convergence, pressure curves, controller, custom plotter, heat flux, flight): legend on top, real bottom margin.
  • Design Requirements input caps that assumed one rocket removed (100 kN, O/F 1.5–4.0, 60 s, 3 m…); the propellant's CEA O/F range is shown under the target and flags an out-of-table value; stale stability prose (0.75/0.4 thresholds the code never used) replaced.
  • Time-Series simple profile seeds from the design's tank pressures instead of 750/600 psi.
  • Config editor: Stability Model section; unreachable Coaxial label dropped.

Schema hygiene

Removed fields nothing read (hot_gas_cp, yield_strength, youngs_modulus, mixing_model) plus the stale hot_gas_cp keys in shipped configs; pydantic ConfigDict; fuel-tank descriptions no longer say RP-1.

Verification

  • Python: 433 passed, 81 skipped, 0 failed (base branch: 428 passed, 1 failed). Strict accelerator gate 44 passed; live A/B parity 16/16.
  • Frontend: tsc clean, vitest 4/4, production build OK. ESLint is the pre-existing non-blocking backlog; no new findings on touched lines.
  • Forward-mode snapshot across all 6 injector × propellant combos before/after every physics change (diffs explained in the session; solver-window and later changes: 0 diffs).
  • Short real Layer 1 runs for all 6 combos + a stale-O/F case: no crashes; pintle combos converge in 3 iterations, impinging methalox/ethalox in 30.
  • Driven through the UI on an isolated server: forward evaluate at the previously-crashing pressures, time series, flight optimize (clean "infeasible: reaches only 1747 m" rather than a blank page), config tab.

Not verified visually: the chart overlap fix could not be screenshotted while the browser pane was hidden; it is a deterministic recharts layout change (legend verticalAlign="top", bottom margin 20).

🤖 Generated with Claude Code

Carlsaurus and others added 5 commits September 9, 2026 01:38
chamber_geometry.design_MR / design_pressure / design_thrust are supposed to
describe the design the config represents. Nothing ever wrote them.
config_schemas builds them with `getattr(chamber, 'design_MR', 2.55)`, so an
optimised config carried its template's values forward forever. On a real
emitted design they read MR 2.55 / 350 psi / 7000 N for an engine actually
solved at O/F 1.65 / 416 psi / 7200 N.

Not cosmetic: backend/routers/geometry.py reads design_MR and passes it
straight to solve_chamber_geometry_with_cea, so the Chamber Geometry tab drew
the contour at the stale mixture ratio -- and 2.55 sits OUTSIDE that config's
CEA cache range (MR_range [1.0, 2.5]), i.e. extrapolating past the table edge
in the one place the cache has no data.

_layer1_stamp_design_point() now writes the solved MR / Pc / F onto the config
at the point Layer 1 hands it back, mirrors them onto the legacy `chamber`
section for readers that still fall back to it, skips non-finite values so a
failed evaluate cannot overwrite a good design point with NaN, and warns when
the achieved MR falls outside combustion.cea.MR_range. Verified on a live run:
2.55 / 350 psi / 7000 N in, 1.6461 / 416.1 psi / 7200 N out, matching the
solver exactly.

Three regression tests cover it -- stamping, the range warning, and the
non-finite guard -- because the root problem was that nothing checked these
fields at all.

Also deletes engine/pipeline/comprehensive_geometry_sizing.py: 495 lines, zero
importers, unchanged since the initial commit, and carrying an IndentationError
that means it has NEVER been importable. It is recoverable from history if it
turns out to be wanted.

Audited for the same class of staleness and found two more, NOT fixed here
because they belong to other layers and are inert on the configs I can see:
  - regen_cooling.chamber_inner_diameter / channel_length are synced only by
    chamber_optimizer.py, not Layer 1, so they still read 0.08491 m / 0.18162 m
    against an actual 0.127 m / 0.20337 m chamber. Harmless while
    regen_cooling.enabled is false; wrong channel sizing the moment it is not.
  - pressure_curves.initial_lox/fuel_pressure_pa (537.3 / 523.7 psi) disagree
    with lox_tank/fuel_tank.initial_pressure_psi (548.6), because Layer 2 wrote
    the curves before Layer 1 last moved the tanks. An ordering problem, not a
    missing write.

Separately verified: all 379 config schema fields are referenced somewhere, so
the config is not carrying dead knobs; and comprehensive_geometry_sizing was
the only module in engine/ or backend/ that fails to import.

Verified: pytest 472 passed / 84 skipped with only the 4 pre-existing failures;
accelerator parity 16/16.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The initial tank pressure exists twice, owned by different layers and never
reconciled: Layer 1 writes lox_tank/fuel_tank.initial_pressure_psi
(layer1_static_optimization.py:2435), Layer 2 writes
pressure_curves.initial_lox/fuel_pressure_pa (layer2_pressure.py:312). Re-running
Layer 1 moves the tanks and silently leaves the curves describing the previous
design. Measured on a real emitted config: LOX 548.6 psi vs a 537.3 psi curve
start (11.3 psi), fuel 548.6 vs 523.7 (24.9 psi).

This matters more than it looks. Tank pressure is the UPSTREAM BOUNDARY
CONDITION for the feed-system twin, and per docs/adr/0001 EngineDesign's
optimizer imports lib/feedtwin directly for Layer X rather than calling a
service. Two disagreeing values for one boundary condition is precisely what
silently poisons a twin, so Layer 1 now says so at the end of every run.

Detection only. Which layer should own the value is a design call and guessing
it here would be worse than the warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two problems docs/adr/0001 names by name, both in the COPV -> regulator -> tank
chain that lib/feedtwin absorbs. Fixed surgically: defaults are exactly the old
literals, so nothing in EngineDesign changes behaviour unless someone overrides
them.

1. Orifice geometry was hardcoded in the function body.
   dynamics.step() defined gamma_gas 1.4, Cd_regulator 0.7, Cd_valve 0.65,
   A_regulator 2e-5, A_valve_F/O 5e-5 as locals. These are real hardware numbers
   -- a different regulator or solenoid could not be modelled without editing
   code. They are now DynamicsParams fields, surfaced on ControllerConfig, and
   read through from_config with getattr so an older config still loads.

2. The polytropic reference state was PROCESS-GLOBAL.
   step() stored its reference temperatures, masses and volumes as attributes on
   the FUNCTION object, guarded by `hasattr(step, '_temp_initialized')`. It
   initialised on the first call ever made in a process and never reset, so the
   first trajectory stepped set the reference for every later one -- across
   different configs. An optimizer evaluating thousands of candidates, which is
   exactly the Layer X access pattern the ADR describes, would hand candidates
   2..N the reference state of candidate 1. All 36 references now live on the
   `params` instance, which scopes them per-config.

Verified: two DynamicsParams objects no longer share reference state, and
nothing is left on the step function object. Four regression tests cover both
fixes, including one that fails if process-global state ever returns.

Also checked the other two feed fragments the ADR names, feed_loss.py and
stability/chug.py: neither carries hardcoded physical constants, so they need
nothing before Phase 04 collapses them into feedtwin.

Verified: pytest 477 passed / 84 skipped with only the 4 pre-existing failures;
robust_ddp suite 12 passed; accelerator parity 16/16.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The frontend had NO error boundary anywhere -- grep for ErrorBoundary /
componentDidCatch / getDerivedStateFromError returned nothing. In React that
means any exception thrown during render unmounts the ENTIRE tree, so every
render bug presents identically: a white page, no message, no stack, no way to
report what happened. That is how "press Optimize on the flight page and it
goes blank" arrived with nothing to act on.

Adds components/ErrorBoundary.tsx and wraps each of the nine tab panels in one.
The tab buttons live in <header>, above the panels, so a crashing tab now shows
a copyable error with its component stack and a Try again button while the tab
bar stays alive and every other tab keeps working.

Verified end to end, not just by inspection: injected a throw at the top of
FlightSimulation, reloaded, and confirmed the boundary rendered "Flight
Simulation hit an error" with the stack while the rest of the app -- header,
all nine tabs, the other panels -- stayed mounted and usable. Throw reverted.

Note this does not by itself remove the underlying throw on the flight optimize
path. I could not reproduce that one: the handler is correctly guarded (checks
result.error, checks result.data, wrapped in try/catch -- though try/catch does
not cover render), and every field the flight UI renders is a REQUIRED float or
List[float] in the backend models, with all Optional sub-objects already guarded
(results?.truncation?., results.propellant &&, and the !results?.trajectory
early return). With the boundary in place the next occurrence prints the real
error instead of a blank page, which is what makes it fixable.

The Try again button is declared VIEW_ONLY in the checkout gating audit: it
clears local error state and touches no design state.

Frontend gating audit 4 passed; npm run build clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… flight dedupe, lockout heartbeat, UI cleanup

Physics / solver
- Chamber-pressure root-find window: choked-flow floor (2 atm) and tank-pressure ceiling
  (min tank minus 2%) replace the 1 bar floor and hardcoded 15% "feed loss margin"; both
  Python and the numba kernel now scan for the HIGHEST-Pc sign change before Brent. The old
  window let Brent lock onto a spurious ~20 psi root: configs/default.yaml reported
  F = -333 N at its own configured tank pressures, and the Python side said "no solution"
  at 550/650/900 psi where the kernel solved. Both paths now agree at every pressure.
- Stability model reads config, not constants: StabilityConfig (n, chi, nozzle-entrance
  Mach, damping fractions, regulator, acoustic gate allowance), feed_system.<side>.length
  (the chug inertance length was a hardcoded 0.305 m with no schema field), cp_g from the
  CEA state (was a fixed 2200 J/kg-K), nozzle-entrance Mach solved from the contraction
  ratio (was 0.2), per-fluid handbook fallbacks recorded via assume() (fuel fallbacks were
  methane's for every fuel). comprehensive_stability_analysis looked up feed_system["lox"]
  .length, a key and attribute that never existed, so feed acoustics always used 1.0 m x
  10 mm; the legacy and rich paths also used different chamber lengths. Chug gate centred
  at gain margin 1.0 (0.80 called an unstable loop "stable"). Rigged heuristics removed:
  stability_index floors, the water-hammer-to-margin map, and stability/enhanced.py.
- Ea mixture-ratio step in reaction_chemistry smoothed (20% jump at MR 1.5); the dead
  reaction-progress computation on every chamber solve removed.
- Layer 1: refuses a target O/F outside the propellant's CEA table (a propellant switch
  kept the old target and optimized against a table that could not evaluate it); names the
  blocking hard constraint when a run ends with no feasible candidate; 1 m chamber-length
  cap removed; ED_L1_WORKERS / ED_L1_TRACE_INFEAS debugging hooks.
- Canonical pintle design_MR 2.55 -> 1.4 (ethalox table is [1.0, 2.5]).

Flight
- Optimizer Layer 4 now renders the shared FlightSimulation component; the 1100-line copy
  it replaced hardcoded RP-1 density and an 85% fill factor client-side, so every
  non-kerolox design capped its fuel load against the wrong tank.
- flight router: explicit_capacity_kg carried a bool; rocket/environment fallbacks derived
  from the request models instead of a drifting literal copy.
- tests/test_flight_propellant_iteration.py rebuilt on configs/default.yaml (it read a file
  out of one developer's Downloads folder); the tank-cap test now constructs its premise.

Checkout lockout
- useCheckout re-takes the checkout every 60 s while held and no longer releases on tab
  hide: the token lapsed after 5 min without a changed autosave and the next write came
  back 423 ("Take Design 1 before saving").

UI
- Emoji removed everywhere; legend and axis-title overlap fixed on every chart with the
  pattern (Layer 1 convergence, pressure curves, controller, custom plotter, heat flux,
  flight); Design Requirements input caps that assumed one rocket removed; CEA O/F range
  shown under the target; stale stability prose replaced; Time-Series simple profile seeds
  from the design's tank pressures; Stability Model section in the config editor;
  unreachable Coaxial label dropped.

Schema hygiene
- Removed fields nothing read (hot_gas_cp, yield_strength, youngs_modulus, mixing_model)
  and the stale hot_gas_cp keys in shipped configs; pydantic ConfigDict; fuel tank
  descriptions no longer say RP-1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Carlsaurus

Copy link
Copy Markdown
Contributor Author

Superseded by #57. Three of the five commits here were already on that branch; the two that were not (the overnight sweep and the error boundary) are cherry-picked onto it, so #57 now carries this work as well. Closing so there is one branch to review and merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant