Skip to content

Fix simulation failing for every preset; add tests, CI and deployment - #10

Open
RedRangerWentWild wants to merge 4 commits into
mainfrom
fix/simulation-broken-and-deployment
Open

Fix simulation failing for every preset; add tests, CI and deployment#10
RedRangerWentWild wants to merge 4 commits into
mainfrom
fix/simulation-broken-and-deployment

Conversation

@RedRangerWentWild

Copy link
Copy Markdown
Member

Why

simulateRocket() threw Maximum number of iterations reached for all five built-in presets, so every launch returned HTTP 500 after ~7s. The UI, API and physics were each individually well-built, but nothing worked end to end.

Root cause: guidance/peg/peg.ts set the PEG blend weight with three hard step functions, making the ODE right-hand side discontinuous. The RK45 embedded error estimate at a jump does not shrink with step size, so the step controller chattered until maxIter and mathjs threw. guidance.ts already documented this exact failure mode and smoothed its own schedule — peg.ts reintroduced it downstream.

Before / after, same 1e-9 solver tolerance:

preset before after
vikram-1 throws (6.8s) suborbital, 0.44s
lvm3 throws (6.8s) suborbital, 0.35s
falcon9 throws (6.7s) suborbital, 0.26s
pslvxl throws (6.8s) orbit, 0.26s
sslv throws (7.0s) orbit, 0.30s

Physics

  • Replace the PEG weight steps with additive smoothstep ramps (new shared guidance/smoothing.ts). Reproduces the original 0.3/0.6/1.0 plateaus exactly, and works at the original tolerance rather than loosening it.
  • Pass mission elapsed time to guidance — the pitch program was restarting at every staging and snapping attitude back to vertical.
  • Drive the gravity turn from launchAngleDeg / gravityTurnStartAltitudeM / gravityTurnRateDegS, which were declared, validated and set in every preset but read by zero lines of physics code.
  • Detect ground impact during powered flight. Only the coast phase checked, so a vehicle descending under thrust flew through the Earth (LVM3 reached -467 km and reported a negative apogee).
  • Classify orbits by perigee rather than "did not hit the ground in 3000s"; stop emitting a fabricated impact event for orbital runs.
  • Record max-Q during ascent only — it was reporting the re-entry peak (T+2070s on a flight with apogee at T+1065s).
  • Sort events chronologically; apply telemetry stride during coast. Responses drop 2.11 MB → 0.35 MB.

Correctness and tooling

  • validate.ts now reports a throwing check as FAIL instead of crashing the process before the summary table prints.
  • New node:test suite — 61 tests covering every preset, the input caps, and the guidance-continuity invariant that broke here. Verified as a real gate: reinjecting the original bug fails 26 of 27 preset tests.
  • Migrate to eslint.config.js. ESLint 9 never read .eslintrc.json, so npm run lint had never run successfully; fixed the 6 errors it surfaced.
  • Add a prepare hook so a fresh clone works after npm install (verified by cloning to a temp dir), plus root build/dev/typecheck scripts and GitHub Actions CI.

Frontend

  • Normalise API error details. The backend sends ConfigValidationError[] on 400 but the client typed it as string and rendered it into JSX — any validation error whitescreened the app, reachable by typing 0 into a mass field. Added an ErrorBoundary.

Deployment and hardening

  • Backend serves the built SPA, so API and UI share one origin (no proxy, no CORS).
  • Multi-stage Dockerfile, .dockerignore, .env.example, .nvmrc, engines.
  • CORS denies by default in production (previously unset CORS_ORIGIN allowed every origin); 500s no longer echo internals.
  • Cap config magnitudes and array sizes in validateRocket — 1e300 masses, 1000 stages and 100k-point curves previously reached the solver.
  • Enforce a per-simulation wall-clock budget inside the integrator, since a synchronous solver cannot be timed out from outside. Returns 503, tunable via SIMULATION_BUDGET_MS.

Verification

typecheck, lint, 61 tests, physics validation suite and full build all pass. Verified end to end against a live server: all 5 presets return HTTP 200, SPA serves from the same origin, unknown /api/* still 404s as JSON, and production CORS returns no Access-Control-Allow-Origin for a foreign origin.

Known limitations (documented in the README, not addressed here)

  • Simulations run synchronously on the request thread, so concurrent launches queue.
  • Run history is in-memory — cleared on restart, not shared across instances.
  • No per-client rate limiting.
  • Trajectories are stable and plausible but not yet accurate; apogees are still lofted. Genuinely realistic ascent needs the real PEG solver, which remains a stub.
  • The Docker image is unbuilt here (no daemon available locally) — the build sequence, prune and runtime were verified step-by-step instead, and CI now builds and smoke-tests it.

🤖 Generated with Claude Code

RedRangerWentWild and others added 4 commits August 18, 2026 16:21
simulateRocket() threw "Maximum number of iterations reached" for all five
built-in presets, so every launch returned HTTP 500 after ~7s and no part of
the product worked end to end.

Root cause: guidance/peg/peg.ts set the PEG blend weight with three hard step
functions, making the ODE right-hand side discontinuous. The RK45 embedded
error estimate at a jump does not shrink with step size, so the step
controller chattered until maxIter and mathjs threw. guidance.ts already
documented this exact failure mode and smoothed its own schedule; peg.ts
reintroduced it downstream.

Physics
- Replace the PEG weight steps with additive smoothstep ramps (new shared
  guidance/smoothing.ts). Reproduces the original 0.3/0.6/1.0 plateaus and
  works at the original 1e-9 tolerance. All 5 presets now run in ~0.3s.
- Pass mission elapsed time to guidance; the pitch program was restarting at
  every staging and snapping attitude back to vertical.
- Drive the gravity turn from launchAngleDeg / gravityTurnStartAltitudeM /
  gravityTurnRateDegS, which were validated but read by no physics code.
- Detect ground impact during powered flight. Previously only the coast phase
  checked, so a vehicle descending under thrust flew through the Earth.
- Classify orbits by perigee instead of "did not hit the ground in 3000s",
  and stop emitting a fabricated impact event for orbital runs.
- Record max-Q during ascent only; it was reporting the re-entry peak.
- Sort events chronologically and apply telemetry stride during coast
  (responses drop from 2.11 MB to 0.35 MB).

Correctness and tooling
- validate.ts reports a throwing check as FAIL instead of crashing the suite
  before the summary prints.
- Add a node:test suite (61 tests) covering every preset, the input caps and
  the guidance-continuity invariant that broke here.
- Migrate to eslint.config.js; ESLint 9 never read .eslintrc.json, so lint had
  never run. Fix the 6 errors it surfaced.
- Add a prepare hook so a fresh clone works after npm install, plus root
  build/dev/typecheck scripts and GitHub Actions CI.

Frontend
- Normalise API error details. The backend sends ConfigValidationError[] on
  400 but the client typed it as a string and rendered it into JSX, so any
  validation error whitescreened the app. Add an ErrorBoundary.

Deployment and hardening
- Serve the built SPA from the backend (single origin, no CORS needed).
- Multi-stage Dockerfile, .dockerignore, .env.example, .nvmrc, engines.
- CORS denies by default in production; 500s no longer echo internals.
- Cap config magnitudes and array sizes in validateRocket.
- Enforce a per-simulation wall-clock budget inside the integrator, since a
  synchronous solver cannot be timed out from the outside. Returns 503.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three failures from the first CI run, all in the new tooling:

- The test script quoted its glob, so the shell never expanded it. Node 20
  (CI) does not expand globs in --test file arguments; Node 25 (local) does,
  which is why it passed here and failed there. Unquoted so the shell globs
  on both.

- The Docker build installed manifests only, then ran `npm ci
  --ignore-scripts`. physics-engine's `prepare` hook still invoked tsc, which
  failed with no inputs because the sources had not been copied yet. Copy the
  source tree before installing and let `prepare` build the engine.

- `continue-on-error` renders a job as failed, so the advisory formatting
  check looked like a broken build. Made it explicitly non-blocking instead.

Verified: 61 tests pass under bash, and the corrected build sequence
(ci -> build -> prune -> boot) serves the API and SPA from a pruned tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The camera was set once at y=80 looking at y=50 and never moved, while the
vehicle climbs past 2500 km. At the old 1/500 scale that is 5,000,000 world
units, so the rocket left frame within a couple of seconds and the panel
showed an empty ground plane for the rest of the flight.

Scene
- One world unit is now one kilometre, and Earth is a real 6371-unit sphere
  with its surface at the origin, so the horizon curves on its own as the
  vehicle climbs. Logarithmic depth buffer to survive the scale range.
- Camera tracks the rocket, pulling back with altitude and easing toward its
  target; the rocket is scaled to camera distance so it holds a constant
  apparent size. Verified numerically over vikram-1 and pslvxl: 6.4-9.7 deg
  apparent height in a 45 deg FOV for every frame, none out of frame.
- Sky lerps from daylight to space and a star field fades in over the first
  100 km; thin atmospheric shell reads as a limb glow from altitude.
- Exhaust plume is driven by telemetry thrustMagnitude, so it tracks the
  actual burn, cuts out on staging and coast, and flares in vacuum.
- Rocket is now body, interstage, nose cone and fins rather than a bare
  cylinder and cone.

Fixes carried over from the audit
- Dispose every geometry and material and force context loss on unmount.
  renderer.dispose() does not free them, and StrictMode mounts twice, so each
  load was orphaning a scene and a WebGL context.
- Preallocate the trail buffer once per run and scrub with setDrawRange.
  It previously reallocated the whole position buffer every frame and
  appended on each index change, drawing a zig-zag when scrubbing backwards.
- Drive the render loop from a ref instead of React state, so camera easing
  is smooth and independent of the 30 Hz playback re-renders.
- Remove the unused sceneRef and assign the rAF id at the call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems with the previous pass: tracking felt laggy, and the look was
illustrative rather than technical.

Tracking
- The vehicle's position was read straight from the telemetry sample, which
  only advances at the ~30 Hz playback rate, while the scene renders at 60+.
  The vehicle therefore stepped between discrete positions while the camera
  eased behind it, which is what read as lag. Position, velocity and thrust
  are now interpolated across the sub-frame interval, and the interval is
  measured from actual index changes rather than assumed, so motion stays
  continuous and matches playback speed.
- Attitude eases along the shortest arc instead of snapping.
- Removed the slow camera orbit. It added a constant swim that made tracking
  feel unsteady; a fixed three-quarter view reads as an instrument and the
  altitude change supplies the motion. Follow gains tightened so the vehicle
  stays locked to frame centre.

Look
- Palette and treatment now match the SAST site: near-black void, thin blue
  line work, restrained accents.
- Earth is a dark occluding sphere with a lat/long wireframe over it, plus a
  faint atmospheric limb, instead of a shaded ball under a daylight sky.
- The vehicle is drawn as an outline over a dark fill rather than a shaded
  model, so it reads as a blueprint and stays legible at any zoom.
- Exhaust is a narrow pale additive taper driven by thrustMagnitude, not a
  saturated flame.
- One lime accent: a camera-facing ring marking the live position, faded in
  with altitude so the vehicle stays findable when the camera is thousands
  of kilometres out.
- Blueprint polar grid at the pad for scale in the first seconds.

Framing verified numerically over vikram-1 and pslvxl: constant 4.7 deg
apparent height in a 42 deg field of view, no frame out of view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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