Fix simulation failing for every preset; add tests, CI and deployment - #10
Open
RedRangerWentWild wants to merge 4 commits into
Open
Fix simulation failing for every preset; add tests, CI and deployment#10RedRangerWentWild wants to merge 4 commits into
RedRangerWentWild wants to merge 4 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
simulateRocket()threwMaximum number of iterations reachedfor 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.tsset 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 untilmaxIterand mathjs threw.guidance.tsalready documented this exact failure mode and smoothed its own schedule —peg.tsreintroduced it downstream.Before / after, same 1e-9 solver tolerance:
Physics
guidance/smoothing.ts). Reproduces the original 0.3/0.6/1.0 plateaus exactly, and works at the original tolerance rather than loosening it.launchAngleDeg/gravityTurnStartAltitudeM/gravityTurnRateDegS, which were declared, validated and set in every preset but read by zero lines of physics code.Correctness and tooling
validate.tsnow reports a throwing check asFAILinstead of crashing the process before the summary table prints.node:testsuite — 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.eslint.config.js. ESLint 9 never read.eslintrc.json, sonpm run linthad never run successfully; fixed the 6 errors it surfaced.preparehook so a fresh clone works afternpm install(verified by cloning to a temp dir), plus rootbuild/dev/typecheckscripts and GitHub Actions CI.Frontend
details. The backend sendsConfigValidationError[]on 400 but the client typed it asstringand rendered it into JSX — any validation error whitescreened the app, reachable by typing0into a mass field. Added anErrorBoundary.Deployment and hardening
Dockerfile,.dockerignore,.env.example,.nvmrc,engines.CORS_ORIGINallowed every origin); 500s no longer echo internals.validateRocket— 1e300 masses, 1000 stages and 100k-point curves previously reached the solver.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 noAccess-Control-Allow-Originfor a foreign origin.Known limitations (documented in the README, not addressed here)
🤖 Generated with Claude Code