From af8419051ec12012aa0008f58edb1f9c2d235f2f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 06:56:12 +0000 Subject: [PATCH 01/21] =?UTF-8?q?Viewer=20setup:=20spec=20006=20and=20the?= =?UTF-8?q?=20SRD=20amendments=20from=20Annex=20A=20=C2=A7B.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/SRD.md: §7 stop clause lifted (Annex A Part B), hyperparameter cap replaced by pre-registration; §4 wall-clock cap replaced by chained CI jobs; §6 packages/demos holds the viewer; §8 decision 21 and D1-D3. - AGENTS.md: gate status (what comes next) and the demos package. - specs/006-engagement-viewer: spec (V-001..V-018 minus V-014), plan (browser-safe subpaths, @tma/demos core + React viewer, site page, tests), tasks. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- AGENTS.md | 9 ++- docs/SRD.md | 17 +++-- specs/006-engagement-viewer/plan.md | 52 +++++++++++++++ specs/006-engagement-viewer/spec.md | 97 ++++++++++++++++++++++++++++ specs/006-engagement-viewer/tasks.md | 25 +++++++ 5 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 specs/006-engagement-viewer/plan.md create mode 100644 specs/006-engagement-viewer/spec.md create mode 100644 specs/006-engagement-viewer/tasks.md diff --git a/AGENTS.md b/AGENTS.md index 927b423f..9e2b31b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,7 @@ packages/ classical/ naive baseline, least-squares solver; CLIs tma-naive, tma-solve eval/ metrics, slices, curves, gates, bootstrap; CLIs tma-eval, tma-gate (g0, g1), tma-g0 models/ tfjs-node models, training, predictions; CLIs tma-train, tma-model-predict, tma-g1 + demos/ browser engagement and model viewer (spec 006); site page /viewer configs/ generator ranges (iter1-*.json) and training configs (iter1/.json) experiments// committed run outputs: manifest, metrics, curves, verdict, seeds site/ Docusaurus site (docs, blog); data dictionary generated at build @@ -188,9 +189,11 @@ scoring) to allow one more iteration, 2c (spec 005, the last under G2): 2 × 128 layers with `loss_weight_speed` 4, from scratch, judged on fresh seed 203: plan `configs/iter2/d`, decision `experiments/20261015-i2d-decision/`. **G2 failed a third time** (panel round 2, `experiments/20261016-i2d-g2/`): range 13.5% / 12.1% and course 7.4° / 6.1° pass, speed 1.47 / -1.53 kt fails (classical 0.61 kt). **The project stops at G2: no-go** on speed, pass on range and -course (write-up: `site/blog/2026-10-17-iteration-2c.md`). Seeds 201–203 are reserved; no further -training is planned. Tooling follow-ups are recorded in that experiment's `review.md`. +1.53 kt fails (classical 0.61 kt), no-go on speed and pass on range and course (write-up: +`site/blog/2026-10-17-iteration-2c.md`). Seeds 201–203 are reserved. **SRD Annex A** (accepted +2026-10-17) lifts the effort budget: next is the engagement and model viewer (spec 006, +`packages/demos`), then the owner's realism check, then pre-registered levers (Annex A Part B), +each with a decision record committed before training and a fresh test seed (204 onward). Gate decisions come from an **agent review panel**, never from waiting on humans (constitution 1.2.0). For each gate experiment: diff --git a/docs/SRD.md b/docs/SRD.md index 41aaed78..1b6cc273 100644 --- a/docs/SRD.md +++ b/docs/SRD.md @@ -115,7 +115,7 @@ The time-late variant has strictly more information at every timestep, so it mus **TensorFlow.js constraints** - `@tensorflow/tfjs-node` for training; `tfjs-node-gpu` optional, not assumed. -- Expect throughput roughly an order of magnitude below PyTorch; iteration-1 datasets are about 10^5 scenarios. Each model trains for at most 6 hours wall-clock on a 4-core CPU (amended 2026-10-11 from "under an hour": measured throughput of ≈ 110–200 windows/s allows only ≈ 4 BiLSTM epochs in an hour; see spec 002 research R1). Training stops earlier on early stopping. +- Expect throughput roughly an order of magnitude below PyTorch; iteration-1 datasets are about 10^5 scenarios. ~~Each model trains for at most 6 hours wall-clock on a 4-core CPU~~ (amended 2026-10-11 from "under an hour", see spec 002 research R1; replaced 2026-10-17 by Annex A §B.4): training runs as chained jobs on CI with checkpoints after every epoch (AGENTS.md); there is no cap beyond the runners' limits, and every job's duration is recorded. Training stops on early stopping or `max_epochs`. - Only layers with direct Keras equivalents (LSTM, Bidirectional, Conv1D, Dense, Masking), so a port to Keras or PyTorch is mechanical. - Models saved in the `tfjs` layers format plus a JSON manifest (hyperparameters, dataset seed, normalisation constants, generator version); every run is reproducible from manifest plus dataset. @@ -178,7 +178,7 @@ packages/ models/ tfjs model definitions + training CLI + manifests classical/ naive baseline, least-squares bearings-only solver eval/ metrics, slicing, gate checks, report generation - demos/ browser apps for PR previews (tfjs in-browser) + demos/ browser apps (tfjs in-browser): the engagement and model viewer (Annex A, spec 006) experiments/ one folder per run: manifest, metrics, plots site/ GH Pages content: welcome, blog, glossary, data dictionary, algorithms .github/ CI, schema generation, PR preview + GH Pages deploy workflows @@ -199,7 +199,7 @@ Four one-week iterations, each ending at a gate. Iteration 0 is infrastructure; | --- | --- | --- | --- | | 0 — Pipeline | Generator, store, classical solver, naive baseline, eval harness, golden tests | G0 | Fix; do not start training on an unverified generator | | 1 — Signal | Both variants on 10^5 clean scenarios (σ = 0.5°, no dropouts) | G1 | Stop and report | -| 2 — Noise | Same models on σ = 1–2°, 10% dropouts; one round of feature/architecture changes | G2 | One more iteration allowed, then stop (amended 2026-10-15, after iteration 2b's validation result and before its test scoring: a second extra iteration, 2c, is allowed, combining the two levers each shown to move speed on validation, a speed-weighted loss (2b) and model capacity (spec 003 research R20), judged on a fresh test split; then stop. The hyperparameter cap stands: 2c is one configuration, chosen from those two measured levers, not a search) | +| 2 — Noise | Same models on σ = 1–2°, 10% dropouts; one round of feature/architecture changes | G2 | One more iteration allowed, then stop (amended 2026-10-15, after iteration 2b's validation result and before its test scoring: a second extra iteration, 2c, is allowed, combining the two levers each shown to move speed on validation, a speed-weighted loss (2b) and model capacity (spec 003 research R20), judged on a fresh test split; then stop. The hyperparameter cap stands: 2c is one configuration, chosen from those two measured levers, not a search. Amended 2026-10-17 by Annex A: the stop was an effort-budget rule and is lifted; after 2c, Annex A Part B: further iterations at the owner's discretion, one pre-registered lever each, judged on fresh seeds) | | 3 — Parity | 10^6 scenarios, held-out geometry set, classical comparison by slice | G3 | Not a stop; result feeds the next-phase decision | **Each iteration produces** @@ -208,7 +208,7 @@ Four one-week iterations, each ending at a gate. Iteration 0 is infrastructure; - A blog post on the project site with the sliced metrics, an interactive results review, and the gate decision taken (section 9). - No model is reported without its naive and classical numbers on the same test set beside it. -**Working pattern.** Infrastructure is front-loaded into iteration 0 so later iterations are mostly configuration changes. Hyperparameter search is capped at a handful of runs per iteration; the question is whether the signal is there, not how good the best possible model is. +**Working pattern.** Infrastructure is front-loaded into iteration 0 so later iterations are mostly configuration changes. ~~Hyperparameter search is capped at a handful of runs per iteration~~ (replaced 2026-10-17 by Annex A §B.3): any number of configurations may be trained, but what is scored on a test split is declared, with its forecast, in a decision record committed before it is trained; the question is still whether the signal is there, not how good the best possible model is. ## 8. Risks, assumptions and decisions @@ -235,6 +235,11 @@ Four one-week iterations, each ending at a gate. Iteration 0 is infrastructure; - Batch least-squares is the only classical reference up to G3; an EKF is added only if G3 is reached. - Gate verdicts are taken by an agent review panel (amended 2026-10-11; replaces the team review): independent AI reviewers with distinct specialities review the evidence, their findings are fixed or answered, and they are re-invited until all approve. No human sign-off is required. +**Decisions taken** (17 Oct 2026, Annex A) + +- Decision 21: the effort budget is lifted; failing G2 no longer ends the project. Further work follows Annex A: the engagement and model viewer first (Part A), then pre-registered levers (Part B). +- Viewer: inference in the browser on the committed weights (D1); bearing-window trimming deferred (D2); every gate's carried pair offered by default (D3). + ## 9. Development practice and public site The repository is public on GitHub from day one; nothing above OFFICIAL enters it, and the mock data has no real-world provenance. The project's public face is a GH Pages site that grows with every PR. @@ -279,5 +284,5 @@ The repository is public on GitHub from day one; nothing above OFFICIAL enters i - [Annex A: Engagement viewer and phase 2](SRD-annex-A.md) (2026-10-17): the effort budget is lifted; an interactive engagement and model viewer is built and verified first, then further - pre-registered attempts at G2 at the owner's discretion. Its B.4 lists the amendments to apply - to this body once the annex is accepted. + pre-registered attempts at G2 at the owner's discretion. Accepted 2026-10-17; its §B.4 + amendments are applied above (§4, §6, §7, §8). diff --git a/specs/006-engagement-viewer/plan.md b/specs/006-engagement-viewer/plan.md new file mode 100644 index 00000000..4e4368a1 --- /dev/null +++ b/specs/006-engagement-viewer/plan.md @@ -0,0 +1,52 @@ +# Implementation Plan: Engagement and model viewer + +**Spec**: [spec.md](spec.md) · **SRD**: Annex A Part A + +## Constitution check (1.2.0) + +| Principle | How it holds | +| --- | --- | +| I. Types from LinkML | The viewer uses `Scenario`, `Prediction`, `PredictionRow`, `NormalisationConstants`, `ParameterRanges` and `ModelManifest`; exports are those types. The URL state is not persisted as a record, so it is not a schema type. | +| II. Determinism | Scenarios come from the seeded generator; inference runs on tfjs's CPU backend, which is deterministic across machines (WebGL is not); a golden test pins a fixed scenario's view model. | +| III. Portability | `sim`, `features` and `classical` are reused through browser-safe subpath exports; the model-output decoding moves from `models` into `features` (pure) so the browser and the CLI share it. | +| IV. Baselines alongside | Classical estimates and naive numbers are shown beside every model figure. | +| V. Gates | No gate is affected; nothing is trained or scored. | + +## Design + +- **`packages/demos`** (`@tma/demos`), the slot SRD §6 names: + - `src/core/*`: pure TypeScript, browser-safe: the dataset catalogue type, `scenarioAt`, + plan-view geometry (bounds, grid spacing), per-scenario errors, inference glue (feature window + → model → decoded rows) taking a tfjs `LayersModel`, classical and naive rows, URL state + encode/decode, canonical NDJSON export. + - `src/viewer/*`: the React component (TSX, compiled by `tsc`), SVG plots drawn by hand (no chart + library), `@tensorflow/tfjs` loaded lazily with the CPU backend. +- **Browser-safe subpaths**: `@tma/features/browser` (layout, derived, normalise, decode), + `@tma/classical/browser` (lsq, estimate, naive), `@tma/data/canonical`. `outputsToPrediction` + moves from `models/src/predict.ts` to `features/src/decode.ts`; `models` re-exports it, so the + CLI is unchanged and the goldens stay byte-identical. +- **Site**: `site/src/pages/viewer.jsx` mounts the component in ``; `prebuild.mjs` + writes `src/generated/viewer.json` (datasets from the plans and ranges files, the four carried + pairs with their manifests' normalisation, feature set and source header, and each gate's naive + means from its experiment manifest) and copies the eight models' `model.json` and `weights.bin` + into `static/viewer/models/` (generated, git-ignored). +- **Datasets** (from the committed configs): iteration 1 (seed 101, `iter1-ranges`), iteration 2 + (seed 201, `iter2-ranges`, with its held-out split), the 2b test split (seed 202) and the 2c + test split (seed 203); splits from `planSplits` (80/10/10 and held-out after them). +- **Tests** (`packages/demos/test`): + - browser bundle: esbuild with `platform: browser` over the core, run in a `node:vm` context + with no Node built-ins; scenarios equal the Node generator's byte for byte (FR-001); + - agreement: the 2c LSTM and BiLSTM on fixed scenarios through the core on the CPU backend + against `tfjs-node` via `@tma/models` (FR-007); + - golden: a fixed scenario's view model (geometry, classical rows, errors), byte-identical; + - a server render of the component, so a broken import or render fails CI. +- **Smoke**: Playwright (Chromium is installed) on the built site: the page loads, inference + completes, no console errors, a screenshot per mode. + +## Risks + +- tfjs CPU inference for the 2c BiLSTM (538k parameters, 240 steps) may exceed two seconds on + slow machines; the viewer shows progress and runs inference once per scenario and model. +- Iteration-1 models were trained on generator 0.1.x data; regenerated with 0.2.0 the iteration-1 + dataset still has an instantaneous zig (its ranges have no turn rate). The viewer shows the + generator version and the model's source dataset. diff --git a/specs/006-engagement-viewer/spec.md b/specs/006-engagement-viewer/spec.md new file mode 100644 index 00000000..da4744a5 --- /dev/null +++ b/specs/006-engagement-viewer/spec.md @@ -0,0 +1,97 @@ +# Feature Specification: Engagement and model viewer + +**Feature Branch**: `006-engagement-viewer` + +**Created**: 2026-10-17 + +**Status**: Planned. SRD Annex A accepted 2026-10-17 (PR #49); decisions D1–D3 taken. + +**Input**: SRD Annex A, Part A. The owner wants to see engagements, to check they look realistic, +and to see what the trained models do on one engagement, in both TMA modes. One interactive +viewer serves both, with a toggle between truth, instantaneous and time-late. + +**SRD trace**: Annex A §A.1–A.7 (requirements V-001 to V-018), §2 (frame, the two variants), §3 +(generator), §4 (inputs, outputs), §5 (baselines beside every figure), §6 (portability, +`packages/demos/`), §9 (site, PR previews). + +**Decisions** (Annex A §B.5): D1 inference runs in the browser on the committed weights; D2 +bearing-window trimming (V-014) is deferred; D3 every gate's carried pair ships by default +(iterations 1, 2, 2b and 2c). + +**Out of scope**: V-014; the realism check itself (A.5, the owner's); any Part B change; editing +scenarios; training in the browser. + +## User stories + +### US1 — See an engagement (P1) + +Choose a dataset and a scenario index (or step, or pick at random, or open a bookmark) and see the +2-D plan with a scaled grid, both platforms' tracks, the turn, the bearings, and the course, +speed and bearing plots for both platforms, with the scenario's parameters and provenance. + +**Acceptance**: the scenario shown is byte-identical to `generateScenario(seed, index, ranges)` +in Node for the same dataset; the grid is labelled at every zoom; the cursor moves the plan and +every plot together. + +### US2 — See the instantaneous model (P1) + +With a "now" cursor at time t, see the model's estimate at t (target position on the current +bearing, velocity vector, range ring; range, bearing, course and speed beside truth and the +error) and the trail of earlier estimates; estimate-vs-truth plots for range, course and speed +with the gate points marked; the classical solver's estimates and the naive numbers beside it. + +**Acceptance**: the estimate at t equals the pipeline's per-timestep prediction for that row (the +causal model's row t depends only on bearings up to t, so one forward pass serves every cursor +position); before the zig the estimate is shown and labelled unobservable. + +### US3 — See the time-late model (P1) + +See the model's estimated target track over the whole window over the truth, with the same plots, +this scenario's summary errors, and the classical track. + +### US4 — Choose the model (P2) + +Choose any of the four carried pairs; see its manifest (id, config, the dataset it was trained +on, the gate it was scored in), and a note when the scenario comes from a different dataset than +the model was trained on. + +### US5 — Share and export (P2) + +The URL holds the view; reopening it shows the same view on any machine. Export the plan and plots +as PNG and the scenario and estimates as NDJSON in the schema's types. + +## Functional requirements + +- **FR-001** (V-001): Scenarios are regenerated in the browser with `@tma/sim`; no scenario files + ship. A test bundles the viewer's core for the browser platform (no Node built-ins) and shows it + regenerates fixed scenarios byte-identically with the Node generator. +- **FR-002** (V-002, V-017): Dataset, index, mode, cursor time, model pair and overlays live in the + URL query; next, previous, random and bookmarks (featured bookmarks committed with the site; the + viewer's own in browser storage). +- **FR-003** (V-003–V-005): The plan view (equal aspect, north up, pan and zoom, grid of 1–2–5 × + 10ⁿ m labelled), legs, turn, zig marker (the first sample after the turn), cursor, headings, + observed bearing rays (dropouts omitted), true bearings optional. +- **FR-004** (V-006–V-008): Course, speed, bearing and bearing-rate plots against time on a shared + axis with the cursor; in model modes, range, course and speed estimate and truth, with zig + 5 + min and zig + 10 min marked. +- **FR-005** (V-009): Parameters, derived keys, seed, index, generator version. +- **FR-006** (V-010–V-013): Modes truth / instantaneous / time-late; classical overlay in both + model modes; naive numbers beside every model figure. +- **FR-007** (V-015, V-016): Models load from the site's static copy of the committed experiment + folders; inference with `@tensorflow/tfjs` on the CPU backend (deterministic across machines) + through the same `@tma/features` encoding and decoding the pipeline uses. A test shows the + browser path agrees with the Node (`tfjs-node`) path to float32 tolerance. +- **FR-008** (V-018): PNG export of the plan and plots; NDJSON export of the scenario and the + model, classical and naive predictions, canonical JSON in the schema's types. +- **FR-009** (A.3): No browser-only forks of `sim`, `features` or `classical`: browser-safe subpath + exports of the existing modules. + +## Success criteria + +- **SC-001**: Inference for one scenario with the 2c BiLSTM under two seconds on a laptop; the + page's weights total under 5 MB. +- **SC-002**: The viewer page builds in `pnpm site:build` and in PR previews; no console errors on + load in Chromium. +- **SC-003**: A blog post links views of a typical engagement, the 2c instantaneous model's + largest speed error at zig + 10 min on the seed-203 split, and a held-out 20–30 km engagement. +- **SC-004**: The owner performs the realism check (A.5) and the record (A.7) is committed. diff --git a/specs/006-engagement-viewer/tasks.md b/specs/006-engagement-viewer/tasks.md new file mode 100644 index 00000000..f35d3f09 --- /dev/null +++ b/specs/006-engagement-viewer/tasks.md @@ -0,0 +1,25 @@ +# Tasks: Engagement and model viewer + +**Spec**: [spec.md](spec.md) · **Plan**: [plan.md](plan.md) + +## Setup PR + +- [x] T001 SRD amendments from Annex A §B.4 (§4, §6, §7, §8 decision 21); AGENTS.md +- [x] T002 Spec 006 (spec, plan, tasks) + +## Implementation PR + +- [ ] T003 Browser-safe subpaths: `@tma/features/browser` (with `decode.ts` moved from models), + `@tma/classical/browser`, `@tma/data/canonical`; goldens unchanged +- [ ] T004 `@tma/demos` core: catalogue types, `scenarioAt`, geometry, inference glue, errors, + classical and naive rows, URL state, NDJSON export +- [ ] T005 Tests: browser bundle (FR-001), agreement (FR-007), golden, server render +- [ ] T006 React viewer: controls, plan view, time plots, panels, three modes, model choice, + overlays, PNG and NDJSON export, URL state +- [ ] T007 Site: prebuild catalogue and model copy, `/viewer` page, navbar link, featured bookmarks +- [ ] T008 Playwright smoke on the built site; screenshots +- [ ] T009 Blog post with linked views (SC-003); site pages (algorithm note on the viewer) + +## Owner + +- [ ] T010 Realism check (Annex A §A.5) and its record (§A.7) From a12814f0e2f3498e3102626f59749ced6471e8f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 07:15:15 +0000 Subject: [PATCH 02/21] Viewer: @tma/demos core and React viewer, browser-safe subpaths Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- packages/classical/package.json | 4 + packages/classical/src/browser.ts | 4 + packages/data/package.json | 4 + packages/demos/package.json | 40 ++ packages/demos/src/core/catalogue.ts | 67 ++ packages/demos/src/core/errors.ts | 37 + packages/demos/src/core/estimates.ts | 98 +++ packages/demos/src/core/export.ts | 8 + packages/demos/src/core/geometry.ts | 63 ++ packages/demos/src/core/scenario.ts | 20 + packages/demos/src/core/series.ts | 85 +++ packages/demos/src/core/view-model.ts | 30 + packages/demos/src/core/view-state.ts | 50 ++ packages/demos/src/index.ts | 10 + .../demos/src/viewer/EngagementViewer.tsx | 640 ++++++++++++++++++ packages/demos/src/viewer/Panels.tsx | 285 ++++++++ packages/demos/src/viewer/PlanView.tsx | 470 +++++++++++++ packages/demos/src/viewer/TimePlot.tsx | 232 +++++++ packages/demos/src/viewer/download.ts | 67 ++ packages/demos/src/viewer/index.ts | 4 + packages/demos/src/viewer/runtime.ts | 36 + packages/demos/src/viewer/style.ts | 41 ++ packages/demos/test/agreement.test.ts | 93 +++ packages/demos/test/browser-bundle.test.ts | 52 ++ packages/demos/test/core.test.ts | 147 ++++ packages/demos/test/golden.test.ts | 20 + packages/demos/test/golden/views.ndjson | 2 + packages/demos/test/helpers.ts | 26 + packages/demos/test/render.test.tsx | 80 +++ packages/demos/tsconfig.json | 19 + packages/eval/package.json | 4 + packages/eval/src/browser.ts | 3 + packages/features/package.json | 4 + packages/features/src/browser.ts | 5 + packages/features/src/decode.ts | 82 +++ packages/features/src/index.ts | 1 + packages/models/src/architectures.ts | 6 +- packages/models/src/predict.ts | 77 +-- pnpm-lock.yaml | 58 ++ tsconfig.check.json | 12 +- tsconfig.json | 3 + vitest.config.ts | 29 +- 42 files changed, 2927 insertions(+), 91 deletions(-) create mode 100644 packages/classical/src/browser.ts create mode 100644 packages/demos/package.json create mode 100644 packages/demos/src/core/catalogue.ts create mode 100644 packages/demos/src/core/errors.ts create mode 100644 packages/demos/src/core/estimates.ts create mode 100644 packages/demos/src/core/export.ts create mode 100644 packages/demos/src/core/geometry.ts create mode 100644 packages/demos/src/core/scenario.ts create mode 100644 packages/demos/src/core/series.ts create mode 100644 packages/demos/src/core/view-model.ts create mode 100644 packages/demos/src/core/view-state.ts create mode 100644 packages/demos/src/index.ts create mode 100644 packages/demos/src/viewer/EngagementViewer.tsx create mode 100644 packages/demos/src/viewer/Panels.tsx create mode 100644 packages/demos/src/viewer/PlanView.tsx create mode 100644 packages/demos/src/viewer/TimePlot.tsx create mode 100644 packages/demos/src/viewer/download.ts create mode 100644 packages/demos/src/viewer/index.ts create mode 100644 packages/demos/src/viewer/runtime.ts create mode 100644 packages/demos/src/viewer/style.ts create mode 100644 packages/demos/test/agreement.test.ts create mode 100644 packages/demos/test/browser-bundle.test.ts create mode 100644 packages/demos/test/core.test.ts create mode 100644 packages/demos/test/golden.test.ts create mode 100644 packages/demos/test/golden/views.ndjson create mode 100644 packages/demos/test/helpers.ts create mode 100644 packages/demos/test/render.test.tsx create mode 100644 packages/demos/tsconfig.json create mode 100644 packages/eval/src/browser.ts create mode 100644 packages/features/src/browser.ts create mode 100644 packages/features/src/decode.ts diff --git a/packages/classical/package.json b/packages/classical/package.json index c79afb84..3961758d 100644 --- a/packages/classical/package.json +++ b/packages/classical/package.json @@ -7,6 +7,10 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" + }, + "./browser": { + "types": "./dist/browser.d.ts", + "default": "./dist/browser.js" } }, "scripts": {}, diff --git a/packages/classical/src/browser.ts b/packages/classical/src/browser.ts new file mode 100644 index 00000000..8fd0c9a5 --- /dev/null +++ b/packages/classical/src/browser.ts @@ -0,0 +1,4 @@ +// Browser-safe entry point (spec 006 FR-009): the solver and baselines, no file I/O. +export * from "./estimate.js"; +export * from "./lsq.js"; +export * from "./naive.js"; diff --git a/packages/data/package.json b/packages/data/package.json index 70bc427e..c0cf8a7b 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -7,6 +7,10 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" + }, + "./canonical": { + "types": "./dist/canonical.d.ts", + "default": "./dist/canonical.js" } }, "scripts": {}, diff --git a/packages/demos/package.json b/packages/demos/package.json new file mode 100644 index 00000000..f49cfe81 --- /dev/null +++ b/packages/demos/package.json @@ -0,0 +1,40 @@ +{ + "name": "@tma/demos", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Browser engagement and model viewer (SRD Annex A, spec 006).", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./viewer": { + "types": "./dist/viewer/index.d.ts", + "default": "./dist/viewer/index.js" + } + }, + "scripts": {}, + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "4.22.0", + "@tensorflow/tfjs-core": "4.22.0", + "@tensorflow/tfjs-layers": "4.22.0", + "@tma/classical": "workspace:*", + "@tma/data": "workspace:*", + "@tma/eval": "workspace:*", + "@tma/features": "workspace:*", + "@tma/sim": "workspace:*", + "@tma/types": "workspace:*" + }, + "peerDependencies": { + "react": "^19.2.0" + }, + "devDependencies": { + "@tma/models": "workspace:*", + "@types/react": "19.3.0", + "@types/react-dom": "19.2.3", + "esbuild": "0.28.2", + "react": "19.3.0", + "react-dom": "19.3.0" + } +} diff --git a/packages/demos/src/core/catalogue.ts b/packages/demos/src/core/catalogue.ts new file mode 100644 index 00000000..bfecc190 --- /dev/null +++ b/packages/demos/src/core/catalogue.ts @@ -0,0 +1,67 @@ +// What the viewer can show (spec 006): scenario sources, model pairs and bookmarks. The site's +// prebuild writes one catalogue from the committed configs and experiment folders; these types +// describe that file, which is viewer configuration, not a pipeline record (plan: Constitution I). +import type { FeatureSet, NaiveMeans, NormalisationConstants, ParameterRanges } from "@tma/types"; + +/** A contiguous index range of one seeded dataset: one split, regenerated on demand. */ +export interface ScenarioSource { + id: string; + label: string; + /** Dataset seed, decimal uint64. */ + seed: string; + /** First index (inclusive) and last (exclusive), as `planSplits` assigns them. */ + start: number; + end: number; + ranges: ParameterRanges; + note?: string; +} + +export type Variant = "instantaneous" | "time_late"; + +export interface ModelEntry { + /** Model id from its manifest, e.g. `lstm-instantaneous`. */ + id: string; + variant: Variant; + /** Path of the model folder under the viewer's model base URL. */ + path: string; + featureSet: FeatureSet; + normalisation: NormalisationConstants; + parameterCount: number; + /** One line describing the architecture, e.g. `LSTM 2 × 128`. */ + architecture: string; + /** Seed of the dataset the model was trained on, when its manifest records it. */ + trainedOnSeed: string | null; + bestEpoch: number; +} + +/** One gate's carried pair, with that gate's naive per-timestep means (SRD §5 baseline). */ +export interface ModelPair { + id: string; + label: string; + /** Experiment folder the pair was scored in. */ + experiment: string; + gate: string; + instantaneous: ModelEntry; + timeLate: ModelEntry; + naive: NaiveMeans; +} + +export type Mode = "truth" | "instantaneous" | "time-late"; + +export interface Bookmark { + label: string; + source: string; + index: number; + mode?: Mode; + /** Cursor time, seconds from window start. */ + t?: number; + pair?: string; + note?: string; +} + +export interface ViewerCatalogue { + generatorVersion: string; + sources: ScenarioSource[]; + pairs: ModelPair[]; + bookmarks: Bookmark[]; +} diff --git a/packages/demos/src/core/errors.ts b/packages/demos/src/core/errors.ts new file mode 100644 index 00000000..fe188480 --- /dev/null +++ b/packages/demos/src/core/errors.ts @@ -0,0 +1,37 @@ +// One scenario's errors (spec 006 FR-006): the SRD §5 per-row definitions from `@tma/eval`. +import { median, mean, rowError, type RowError } from "@tma/eval/browser"; +import type { Prediction, PredictionRow, Scenario } from "@tma/types"; + +export type { RowError }; + +export const errorOf = (s: Scenario, row: PredictionRow): RowError => rowError(s, row); + +export interface ScenarioErrors { + /** Post-zig rows scored (as the gates score them). */ + rows: number; + rangeRelMedian: number; + courseDegMedian: number; + speedKtMedian: number; + /** Mean position error over the post-zig rows, metres (SRD §5 time-late metric). */ + positionMeanM: number; + nonConverged: number; +} + +/** Medians over the post-zig rows; a non-converged row counts as infinite error, as `tma-eval` scores it. */ +export function scenarioErrors(s: Scenario, p: Prediction): ScenarioErrors { + const rows = p.rows.filter( + (r) => Math.round(r.timestamp_s / s.metadata.parameters.dt_s) > s.metadata.zig_index, + ); + const e = rows.map((r) => errorOf(s, r)); + const ok = (v: number, r: RowError): number => (r.converged ? v : Infinity); + const sorted = (f: (r: RowError) => number): number[] => + e.map((r) => ok(f(r), r)).sort((a, b) => a - b); + return { + rows: e.length, + rangeRelMedian: e.length === 0 ? NaN : median(sorted((r) => r.range_rel)), + courseDegMedian: e.length === 0 ? NaN : median(sorted((r) => r.course_deg)), + speedKtMedian: e.length === 0 ? NaN : median(sorted((r) => r.speed_kt)), + positionMeanM: e.length === 0 ? NaN : mean(e.map((r) => ok(r.position_m, r))), + nonConverged: e.filter((r) => !r.converged).length, + }; +} diff --git a/packages/demos/src/core/estimates.ts b/packages/demos/src/core/estimates.ts new file mode 100644 index 00000000..38b4959b --- /dev/null +++ b/packages/demos/src/core/estimates.ts @@ -0,0 +1,98 @@ +// Estimates on one scenario (spec 006 FR-006, FR-007): the models through the pipeline's own +// encoding (`buildWindow`) and decoding (`outputsToPrediction`), the classical solver and the +// naive baseline. tfjs is passed in, so this module carries no tfjs import and stays testable +// on any backend. +import { + bearingData, + classicalPredict, + naivePredict, + plausible, + rowAt, + solveBatch, +} from "@tma/classical/browser"; +import { buildWindow, inputWidth, outputsToPrediction, ROWS } from "@tma/features/browser"; +import type { NaiveMeans, Prediction, PredictionRow, Scenario } from "@tma/types"; +import type { ModelEntry } from "./catalogue.js"; + +export interface TensorLike { + dataSync(): ArrayLike; + dispose(): void; +} +export interface TfLike { + tensor3d(values: Float32Array, shape: [number, number, number]): T; +} +export interface ModelLike { + predict(x: T): TensorLike | TensorLike[]; +} + +/** + * The model's estimate at every sample of `s` (rows from 0, so the pre-zig rows can be shown and + * labelled unobservable). The instantaneous model is causal, so its row t depends only on + * bearings up to t: one forward pass serves every cursor position. + */ +export function modelPrediction( + tf: TfLike, + model: ModelLike, + s: Scenario, + entry: Pick, +): Prediction { + const w = buildWindow(s, entry.normalisation, entry.featureSet); + const x = tf.tensor3d(w.inputs, [1, ROWS, inputWidth(entry.featureSet)]); + const outs: TensorLike[] = []; + try { + const y = model.predict(x); + outs.push(...(Array.isArray(y) ? y : [y])); + const first = outs[0]; + if (first === undefined) throw new Error(`${entry.id}: no output`); + return outputsToPrediction( + s, + Float32Array.from(first.dataSync()), + entry.normalisation, + entry.id, + 0, + ); + } finally { + x.dispose(); + for (const t of outs) t.dispose(); + } +} + +export const classicalRows = (s: Scenario): Prediction => classicalPredict(s, "per_timestep"); + +/** + * The classical solver's time-late counterpart: one batch solution over the whole window, + * evaluated at every sample (constant-velocity track). Rows are flagged not converged when the + * solution is implausible, as `classicalPredict` flags them. + */ +export function classicalTrack(s: Scenario): Prediction { + const d = bearingData(s); + const n = s.metadata.samples; + const sol = solveBatch(d, n); + const rows: PredictionRow[] = []; + if (sol !== null) { + const ok = sol.converged && plausible(d, sol, n - 1); + for (let i = 0; i < n; i++) rows.push({ ...rowAt(d, sol, i), converged: ok }); + } + return { + record: "prediction", + scenario_index: s.metadata.index, + estimator: "classical", + mode: "final", + rows, + }; +} + +export const naiveRows = (s: Scenario, means: NaiveMeans): Prediction => + naivePredict( + s, + { schema_version: "", train_digest: "", final: means, per_timestep: means }, + "per_timestep", + ); + +/** The prediction's row at sample i, if it has one. */ +export function rowAtSample(p: Prediction | null, s: Scenario, i: number): PredictionRow | null { + if (p === null) return null; + const dt = s.metadata.parameters.dt_s; + for (const r of p.rows) if (Math.round(r.timestamp_s / dt) === i) return r; + return null; +} diff --git a/packages/demos/src/core/export.ts b/packages/demos/src/core/export.ts new file mode 100644 index 00000000..87b006cc --- /dev/null +++ b/packages/demos/src/core/export.ts @@ -0,0 +1,8 @@ +// NDJSON export (spec 006 FR-008): the scenario and the estimates shown, canonical JSON in the +// schema's types, so a viewed case can be fed back into the CLI pipeline. +import { canonicalJson } from "@tma/data/canonical"; +import type { Prediction, Scenario } from "@tma/types"; + +export function viewNdjson(s: Scenario, predictions: readonly Prediction[]): string { + return [s, ...predictions].map((r) => canonicalJson(r)).join("\n") + "\n"; +} diff --git a/packages/demos/src/core/geometry.ts b/packages/demos/src/core/geometry.ts new file mode 100644 index 00000000..355c4a28 --- /dev/null +++ b/packages/demos/src/core/geometry.ts @@ -0,0 +1,63 @@ +// Plan-view geometry (spec 006 FR-003): bounds, a 1–2–5 grid between 100 m and 10 km, labels. + +export interface Bounds { + minX: number; + maxX: number; + minY: number; + maxY: number; +} + +export const GRID_MIN_M = 100; +export const GRID_MAX_M = 10_000; + +/** The 1–2–5 × 10ⁿ step giving about `lines` grid lines across `span` metres, within 100 m–10 km. */ +export function gridStep(span: number, lines = 8): number { + if (!(span > 0)) return GRID_MIN_M; + const raw = span / lines; + const p = 10 ** Math.floor(Math.log10(raw)); + const m = raw / p; + const nice = (m < 1.5 ? 1 : m < 3.5 ? 2 : m < 7.5 ? 5 : 10) * p; + return Math.min(GRID_MAX_M, Math.max(GRID_MIN_M, nice)); +} + +/** Grid line positions covering [lo, hi] at `step`, aligned to multiples of it. */ +export function gridLines(lo: number, hi: number, step: number): number[] { + const out: number[] = []; + for (let v = Math.ceil(lo / step) * step; v <= hi + 1e-9; v += step) out.push(v + 0); + return out; +} + +/** Bounds of the points with a margin, at least `minSpan` metres wide and tall, centred. */ +export function boundsOf( + points: Iterable, + margin = 0.08, + minSpan = 2000, +): Bounds { + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + for (const [x, y] of points) { + if (!Number.isFinite(x) || !Number.isFinite(y)) continue; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + if (!Number.isFinite(minX)) + return { minX: -minSpan / 2, maxX: minSpan / 2, minY: -minSpan / 2, maxY: minSpan / 2 }; + const w = Math.max(maxX - minX, minSpan); + const h = Math.max(maxY - minY, minSpan); + const cx = (minX + maxX) / 2; + const cy = (minY + maxY) / 2; + const half = (Math.max(w, h) * (1 + 2 * margin)) / 2; + return { minX: cx - half, maxX: cx + half, minY: cy - half, maxY: cy + half }; +} + +/** "500 m", "2 km", "1.5 km". */ +export function formatDistance(m: number): string { + const a = Math.abs(m); + if (a < 1000) return `${String(Math.round(m))} m`; + const km = m / 1000; + return `${Number.isInteger(km) ? String(km) : km.toFixed(1)} km`; +} diff --git a/packages/demos/src/core/scenario.ts b/packages/demos/src/core/scenario.ts new file mode 100644 index 00000000..cdbcb8a0 --- /dev/null +++ b/packages/demos/src/core/scenario.ts @@ -0,0 +1,20 @@ +// Scenarios are regenerated from their seed (spec 006 FR-001): the same `@tma/sim` function the +// pipeline's `tma-generate` calls, so no scenario files ship with the viewer. +import { generateScenario, parseSeed } from "@tma/sim"; +import type { Scenario } from "@tma/types"; +import type { ScenarioSource } from "./catalogue.js"; + +export function scenarioAt(source: ScenarioSource, index: number): Scenario { + if (!Number.isInteger(index) || index < source.start || index >= source.end) { + throw new RangeError( + `${source.id}: index ${String(index)} outside [${String(source.start)}, ${String(source.end)})`, + ); + } + return generateScenario(parseSeed(source.seed), index, source.ranges).scenario; +} + +/** Clamp an index into a source's range (for typed input and next / previous). */ +export function clampIndex(source: ScenarioSource, index: number): number { + if (!Number.isFinite(index)) return source.start; + return Math.min(source.end - 1, Math.max(source.start, Math.trunc(index))); +} diff --git a/packages/demos/src/core/series.ts b/packages/demos/src/core/series.ts new file mode 100644 index 00000000..77d8f2c3 --- /dev/null +++ b/packages/demos/src/core/series.ts @@ -0,0 +1,85 @@ +// Time series for the viewer's plots (spec 006 FR-004): both platforms' course and speed, the +// observed and true bearing, the bearing rate the `derived` feature set computes, and the range. +import { scenarioBearingRate } from "@tma/features/browser"; +import { bearingOf, mpsToKt, radToDeg } from "@tma/sim"; +import type { Scenario } from "@tma/types"; + +export interface Series { + /** Seconds from window start, one per sample. */ + t: number[]; + ownCourseDeg: number[]; + ownSpeedKt: number[]; + tgtCourseDeg: number[]; + tgtSpeedKt: number[]; + /** Observed bearing, degrees true; null on dropouts. */ + bearingObsDeg: (number | null)[]; + bearingTrueDeg: number[]; + /** Trailing least-squares bearing rate (features `derived.ts`), degrees per minute; null where undefined. */ + bearingRateDegMin: (number | null)[]; + rangeKm: number[]; +} + +const deg360 = (rad: number): number => { + const d = radToDeg(rad) % 360; + return (d < 0 ? d + 360 : d) + 0; +}; + +/** True bearing from ownship to target at sample i, radians. */ +export function trueBearing(s: Scenario, i: number): number { + const o = s.ownship.states[i]; + const t = s.target.states[i]; + if (o === undefined || t === undefined) throw new RangeError(`no sample ${String(i)}`); + return bearingOf(t.x_m - o.x_m, t.y_m - o.y_m); +} + +export function seriesOf(s: Scenario): Series { + const n = s.metadata.samples; + const rate = scenarioBearingRate(s); + const out: Series = { + t: [], + ownCourseDeg: [], + ownSpeedKt: [], + tgtCourseDeg: [], + tgtSpeedKt: [], + bearingObsDeg: [], + bearingTrueDeg: [], + bearingRateDegMin: [], + rangeKm: [], + }; + for (let i = 0; i < n; i++) { + const o = s.ownship.states[i]; + const g = s.target.states[i]; + const b = s.observations[i]; + if (o === undefined || g === undefined || b === undefined) + throw new RangeError(`no sample ${String(i)}`); + out.t.push(o.timestamp_s); + out.ownCourseDeg.push(deg360(o.course_rad)); + out.ownSpeedKt.push(mpsToKt(o.speed_mps)); + out.tgtCourseDeg.push(deg360(g.course_rad)); + out.tgtSpeedKt.push(mpsToKt(g.speed_mps)); + out.bearingObsDeg.push(b.valid ? deg360(b.bearing_rad) : null); + out.bearingTrueDeg.push(deg360(trueBearing(s, i))); + out.bearingRateDegMin.push(rate.valid[i] === 1 ? radToDeg(rate.rate[i] ?? 0) * 60 : null); + out.rangeKm.push(Math.hypot(g.x_m - o.x_m, g.y_m - o.y_m) / 1000); + } + return out; +} + +/** Ownship's legs as sample index ranges: leg 1, the turn (empty for an instantaneous zig), leg 2. */ +export interface Legs { + leg1: [number, number]; + turn: [number, number]; + leg2: [number, number]; +} + +export function legsOf(s: Scenario): Legs { + const m = s.metadata; + const turnStart = m.turn_start_index ?? m.zig_index; + return { leg1: [0, turnStart], turn: [turnStart, m.zig_index], leg2: [m.zig_index, m.samples] }; +} + +/** Sample index nearest a time, within the window. */ +export function sampleAt(s: Scenario, t: number): number { + const i = Math.round(t / s.metadata.parameters.dt_s); + return Math.min(s.metadata.samples - 1, Math.max(0, Number.isFinite(i) ? i : 0)); +} diff --git a/packages/demos/src/core/view-model.ts b/packages/demos/src/core/view-model.ts new file mode 100644 index 00000000..881900f9 --- /dev/null +++ b/packages/demos/src/core/view-model.ts @@ -0,0 +1,30 @@ +// A scenario's view model without the models (golden-tested): what the plan and plots draw. +import type { Prediction, Scenario } from "@tma/types"; +import { classicalRows } from "./estimates.js"; +import { type Bounds, boundsOf, gridStep } from "./geometry.js"; +import { type Legs, legsOf, type Series, seriesOf } from "./series.js"; + +export interface EngagementView { + index: number; + seed: string; + legs: Legs; + bounds: Bounds; + gridStepM: number; + series: Series; + classical: Prediction; +} + +export function engagementView(s: Scenario): EngagementView { + const pts: [number, number][] = []; + for (const p of [...s.ownship.states, ...s.target.states]) pts.push([p.x_m, p.y_m]); + const bounds = boundsOf(pts); + return { + index: s.metadata.index, + seed: s.metadata.seed, + legs: legsOf(s), + bounds, + gridStepM: gridStep(bounds.maxX - bounds.minX), + series: seriesOf(s), + classical: classicalRows(s), + }; +} diff --git a/packages/demos/src/core/view-state.ts b/packages/demos/src/core/view-state.ts new file mode 100644 index 00000000..4f7bf567 --- /dev/null +++ b/packages/demos/src/core/view-state.ts @@ -0,0 +1,50 @@ +// The view in the URL (spec 006 FR-002, Annex A V-017): reopening a link shows the same view. +import type { Mode, ViewerCatalogue } from "./catalogue.js"; + +export interface ViewState { + source: string; + index: number; + mode: Mode; + /** Cursor, seconds from window start; null = the zig + 10 min gate point (or window end). */ + t: number | null; + pair: string; + classical: boolean; + trueBearings: boolean; +} + +const MODES: readonly Mode[] = ["truth", "instantaneous", "time-late"]; + +export function encodeView(v: ViewState): string { + const q = new URLSearchParams(); + q.set("src", v.source); + q.set("i", String(v.index)); + q.set("mode", v.mode); + if (v.t !== null) q.set("t", String(Math.round(v.t))); + q.set("pair", v.pair); + q.set("cl", v.classical ? "1" : "0"); + q.set("tb", v.trueBearings ? "1" : "0"); + return q.toString(); +} + +/** Parse a query string; anything missing or unknown falls back to the catalogue's defaults. */ +export function decodeView(search: string, cat: ViewerCatalogue): ViewState { + const q = new URLSearchParams(search); + const source = cat.sources.find((s) => s.id === q.get("src")) ?? cat.sources[0]; + const pair = cat.pairs.find((p) => p.id === q.get("pair")) ?? cat.pairs.at(-1); + if (source === undefined || pair === undefined) throw new Error("empty viewer catalogue"); + const i = Number(q.get("i")); + const index = Number.isInteger(i) && i >= source.start && i < source.end ? i : source.start; + const m = q.get("mode"); + const mode = MODES.find((x) => x === m) ?? "truth"; + const tRaw = q.get("t"); + const tNum = tRaw === null ? NaN : Number(tRaw); + return { + source: source.id, + index, + mode, + t: Number.isFinite(tNum) && tNum >= 0 ? tNum : null, + pair: pair.id, + classical: q.get("cl") !== "0", + trueBearings: q.get("tb") === "1", + }; +} diff --git a/packages/demos/src/index.ts b/packages/demos/src/index.ts new file mode 100644 index 00000000..ae398933 --- /dev/null +++ b/packages/demos/src/index.ts @@ -0,0 +1,10 @@ +// @tma/demos core (spec 006): browser-safe, no tfjs import, no file I/O. +export * from "./core/catalogue.js"; +export * from "./core/errors.js"; +export * from "./core/estimates.js"; +export * from "./core/export.js"; +export * from "./core/geometry.js"; +export * from "./core/scenario.js"; +export * from "./core/series.js"; +export * from "./core/view-model.js"; +export * from "./core/view-state.js"; diff --git a/packages/demos/src/viewer/EngagementViewer.tsx b/packages/demos/src/viewer/EngagementViewer.tsx new file mode 100644 index 00000000..01d31420 --- /dev/null +++ b/packages/demos/src/viewer/EngagementViewer.tsx @@ -0,0 +1,640 @@ +// The engagement and model viewer (SRD Annex A Part A, spec 006): one page, three modes. +// Truth shows the regenerated engagement; instantaneous shows the causal model's estimate at a +// "now" cursor with its trail; time-late shows the bidirectional model's whole-window track. The +// classical solver and the naive baseline sit beside every model figure (SRD §5 reporting rule). +import { mpsToKt, radToDeg } from "@tma/sim"; +import type { Tensor } from "@tensorflow/tfjs-core"; +import type { Prediction, PredictionRow } from "@tma/types"; +import { type JSX, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { Bookmark, Mode, ModelEntry, ViewerCatalogue } from "../core/catalogue.js"; +import { scenarioErrors } from "../core/errors.js"; +import { classicalTrack, modelPrediction, naiveRows, rowAtSample } from "../core/estimates.js"; +import { viewNdjson } from "../core/export.js"; +import { clampIndex, scenarioAt } from "../core/scenario.js"; +import { sampleAt } from "../core/series.js"; +import { engagementView } from "../core/view-model.js"; +import { decodeView, encodeView, type ViewState } from "../core/view-state.js"; +import { savePng, saveText } from "./download.js"; +import { type Estimate, Metadata, ModelInfo, Readout, Summary } from "./Panels.js"; +import { PlanView } from "./PlanView.js"; +import { runtime } from "./runtime.js"; +import { C, CSS } from "./style.js"; +import { type Line, type Marker, TimePlot } from "./TimePlot.js"; + +export interface EngagementViewerProps { + catalogue: ViewerCatalogue; + /** URL of the folder holding `/model.json` and its weights. */ + modelBaseUrl: string; + /** Query string to start from (the page's `location.search`). */ + initialSearch?: string; + /** Keep the page URL in step with the view (FR-002). */ + syncUrl?: boolean; +} + +interface Inference { + key: string; + prediction: Prediction | null; + ms: number; + error: string | null; +} + +const SAVED = "tma-viewer-bookmarks"; + +function loadSaved(): Bookmark[] { + try { + if (typeof localStorage === "undefined") return []; + const raw = localStorage.getItem(SAVED); + if (raw === null) return []; + const v: unknown = JSON.parse(raw); + return Array.isArray(v) ? (v as Bookmark[]) : []; + } catch { + return []; + } +} + +function storeSaved(b: readonly Bookmark[]): void { + try { + if (typeof localStorage !== "undefined") localStorage.setItem(SAVED, JSON.stringify(b)); + } catch { + // Storage unavailable (private window): saved bookmarks last for this visit only. + } +} + +function randomBelow(n: number): number { + const a = new Uint32Array(1); + globalThis.crypto.getRandomValues(a); + return (a[0] ?? 0) % n; +} + +const degOf = (rad: number): number => { + const d = radToDeg(rad) % 360; + return d < 0 ? d + 360 : d; +}; + +const urlJoin = (base: string, path: string): string => + `${base.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`; + +export function EngagementViewer(props: EngagementViewerProps): JSX.Element { + const { catalogue, modelBaseUrl, syncUrl = false } = props; + const [v, setV] = useState(() => decodeView(props.initialSearch ?? "", catalogue)); + const update = useCallback((patch: Partial) => { + setV((old) => ({ ...old, ...patch })); + }, []); + + const source = catalogue.sources.find((x) => x.id === v.source) ?? catalogue.sources[0]; + const pair = catalogue.pairs.find((x) => x.id === v.pair) ?? catalogue.pairs.at(-1); + if (source === undefined || pair === undefined) throw new Error("empty viewer catalogue"); + + const s = useMemo(() => scenarioAt(source, v.index), [source, v.index]); + const view = useMemo(() => engagementView(s), [s]); + const dt = s.metadata.parameters.dt_s; + const n = s.metadata.samples; + const windowS = (n - 1) * dt; + const zigT = s.metadata.zig_index * dt; + const tCur = Math.min(windowS, v.t ?? Math.min(zigT + 600, windowS)); + const cursor = sampleAt(s, tCur); + + const entry: ModelEntry | null = + v.mode === "instantaneous" ? pair.instantaneous : v.mode === "time-late" ? pair.timeLate : null; + const key = entry === null ? "" : `${source.id}:${String(v.index)}:${pair.id}:${entry.id}`; + const cache = useRef(new Map()); + const [inference, setInference] = useState(null); + + useEffect(() => { + if (entry === null) return; + const hit = cache.current.get(key); + if (hit !== undefined) { + setInference(hit); + return; + } + const run = { live: true }; + setInference({ key, prediction: null, ms: 0, error: null }); + void (async () => { + let result: Inference; + try { + const rt = await runtime(); + const model = await rt.load(urlJoin(modelBaseUrl, `${entry.path}/model.json`)); + const t0 = performance.now(); + const prediction = modelPrediction(rt.tf, model, s, entry); + result = { key, prediction, ms: performance.now() - t0, error: null }; + cache.current.set(key, result); + } catch (e) { + result = { + key, + prediction: null, + ms: 0, + error: e instanceof Error ? e.message : String(e), + }; + } + if (run.live) setInference(result); + })(); + return () => { + run.live = false; + }; + }, [key, entry, s, modelBaseUrl]); + + const model = inference?.key === key ? inference.prediction : null; + const loading = + entry !== null && + (inference?.key !== key || (inference.prediction === null && inference.error === null)); + const classical: Prediction | null = useMemo( + () => + !v.classical || v.mode === "truth" + ? null + : v.mode === "time-late" + ? classicalTrack(s) + : view.classical, + [v.classical, v.mode, s, view], + ); + const naive = useMemo(() => naiveRows(s, pair.naive), [s, pair]); + + // URL state. + useEffect(() => { + if (!syncUrl) return; + const { pathname, hash } = globalThis.location; + globalThis.history.replaceState(null, "", `${pathname}?${encodeView(v)}${hash}`); + }, [v, syncUrl]); + + // Play: one sample per tick to the window's end. + const [playing, setPlaying] = useState(false); + const tRef = useRef(tCur); + tRef.current = tCur; + useEffect(() => { + if (!playing) return; + const id = setInterval(() => { + setV((old) => ({ ...old, t: Math.min(windowS, (old.t ?? tRef.current) + dt) })); + }, 80); + return () => { + clearInterval(id); + }; + }, [playing, windowS, dt]); + useEffect(() => { + if (playing && tCur >= windowS) setPlaying(false); + }, [playing, tCur, windowS]); + + // Index input commits on Enter or blur. + const [draft, setDraft] = useState(String(v.index)); + useEffect(() => { + setDraft(String(v.index)); + }, [v.index]); + const goTo = (i: number): void => { + update({ index: clampIndex(source, i), t: null }); + }; + + const [saved, setSaved] = useState([]); + useEffect(() => { + setSaved(loadSaved()); + }, []); + const bookmarks = [ + ...catalogue.bookmarks.map((b) => ({ b, saved: false })), + ...saved.map((b) => ({ b, saved: true })), + ]; + const openBookmark = (b: Bookmark): void => { + const src = catalogue.sources.find((x) => x.id === b.source); + if (src === undefined) return; + setV((old) => ({ + ...old, + source: src.id, + index: clampIndex(src, b.index), + mode: b.mode ?? old.mode, + t: b.t ?? null, + pair: catalogue.pairs.some((p) => p.id === b.pair) ? (b.pair ?? old.pair) : old.pair, + })); + }; + + const root = useRef(null); + const planRef = useRef(null); + const fileStem = `tma-${source.id}-${String(v.index)}-${v.mode}`; + + // Plot series. + const ser = view.series; + const live = v.mode === "instantaneous"; + const valuesOf = (p: Prediction | null, f: (r: PredictionRow) => number): (number | null)[] => { + const out: (number | null)[] = new Array(n).fill(null); + if (p === null) return out; + for (const r of p.rows) { + const i = Math.round(r.timestamp_s / dt); + if (r.converged && i >= 0 && i < n && !(live && i > cursor)) out[i] = f(r); + } + return out; + }; + const est = (f: (r: PredictionRow) => number, wrap?: number): Line[] => { + if (v.mode === "truth") return []; + const out: Line[] = [ + { label: "model", values: valuesOf(model, f), color: C.model, wrap, width: 2 }, + ]; + if (classical !== null) + out.push({ label: "classical", values: valuesOf(classical, f), color: C.classical, wrap }); + out.push({ + label: "naive", + values: valuesOf(naive, f), + color: C.naive, + dash: "6 4", + wrap, + width: 1.2, + }); + return out; + }; + const markers: Marker[] = [{ t: zigT, label: "zig" }]; + if (v.mode !== "truth") { + for (const [m, label] of [ + [300, "zig+5"], + [600, "zig+10"], + ] as const) { + if (zigT + m <= windowS) markers.push({ t: zigT + m, label }); + } + } + const turnShade: [number, number] | null = + view.legs.turn[1] > view.legs.turn[0] ? [view.legs.turn[0] * dt, view.legs.turn[1] * dt] : null; + const common = { + t: ser.t, + cursorT: tCur, + windowS, + markers, + shade: turnShade, + unobservableUntil: v.mode === "truth" ? null : zigT, + onCursor: (t: number) => { + update({ t: Math.round(t / dt) * dt }); + }, + }; + const angleTicks = [0, 90, 180, 270, 360]; + const truthAndEst = v.mode !== "truth"; + const plots = ( + <> + {truthAndEst && ( + r.range_m / 1000), + ]} + /> + )} + degOf(r.course_rad), 360), + ]} + /> + mpsToKt(r.speed_mps)), + ]} + /> + + + {!truthAndEst && ( + + )} + + ); + + const estimates: Estimate[] = + v.mode === "truth" + ? [] + : [ + { label: "model", color: C.model, row: rowAtSample(model, s, cursor) }, + ...(classical === null + ? [] + : [{ label: "classical", color: C.classical, row: rowAtSample(classical, s, cursor) }]), + { label: "naive", color: C.naive, row: rowAtSample(naive, s, cursor) }, + ]; + + const summary = useMemo(() => { + if (v.mode === "truth") return []; + return [ + { label: "model", color: C.model, e: model === null ? null : scenarioErrors(s, model) }, + ...(classical === null + ? [] + : [{ label: "classical", color: C.classical, e: scenarioErrors(s, classical) }]), + { label: "naive", color: C.naive, e: scenarioErrors(s, naive) }, + ]; + }, [v.mode, model, classical, naive, s]); + + const modeLabel: Record = { + truth: "truth", + instantaneous: "instantaneous", + "time-late": "time-late", + }; + + return ( +
+ +
+ + + + + + + + {saved.length > 0 && ( + + )} +
+
+ + {(["truth", "instantaneous", "time-late"] as const).map((m) => ( + + ))} + + + + + + +
+
+ + { + update({ t: Number(e.target.value) }); + }} + /> + + {(tCur / 60).toFixed(1)} / {(windowS / 60).toFixed(0)} min ( + {tCur >= zigT ? "zig+" : "zig−"} + {(Math.abs(tCur - zigT) / 60).toFixed(1)}) + +
+ {entry !== null && ( +

+ {loading + ? `Running ${entry.id}…` + : inference?.error != null + ? `Could not run ${entry.id}: ${inference.error}` + : `${entry.id}: ${String(Math.round(inference?.ms ?? 0))} ms on the CPU backend.`} +

+ )} +
+
+ +

+ Drag to pan, wheel to zoom, double-click to reset. Velocity vectors show 5 min of + motion. +

+ {v.mode !== "truth" && ( +
+

At the cursor

+ +
+ )} + {v.mode !== "truth" && ( +
+

This scenario

+ +
+ )} +
+
+ {plots} +
+

Scenario

+ +
+
+

Models

+ +
+
+
+
+ ); +} diff --git a/packages/demos/src/viewer/Panels.tsx b/packages/demos/src/viewer/Panels.tsx new file mode 100644 index 00000000..a6a2ed61 --- /dev/null +++ b/packages/demos/src/viewer/Panels.tsx @@ -0,0 +1,285 @@ +// Side panels (spec 006 FR-005, FR-006, US4): the readout at the cursor (truth, model, classical, +// naive and their errors), the scenario's summary errors, its parameters and provenance, and the +// chosen model pair's manifest. +import { bearingOf, mpsToKt, radToDeg } from "@tma/sim"; +import type { PredictionRow, Scenario } from "@tma/types"; +import type { JSX } from "react"; +import type { ModelEntry, ModelPair, ScenarioSource } from "../core/catalogue.js"; +import { errorOf, type ScenarioErrors } from "../core/errors.js"; +import { trueBearing } from "../core/series.js"; + +const deg = (rad: number): number => { + const d = radToDeg(rad) % 360; + return d < 0 ? d + 360 : d; +}; +const dash = "—"; +const fx = (v: number | null | undefined, digits: number): string => + v === null || v === undefined || !Number.isFinite(v) ? dash : v.toFixed(digits); + +export interface Estimate { + label: string; + color: string; + row: PredictionRow | null; +} + +export interface ReadoutProps { + s: Scenario; + cursor: number; + estimates: readonly Estimate[]; +} + +/** Values and errors at the cursor, one column pair per estimator. */ +export function Readout({ s, cursor, estimates }: ReadoutProps): JSX.Element { + const o = s.ownship.states[cursor]; + const g = s.target.states[cursor]; + if (o === undefined || g === undefined) return

no sample {cursor}

; + const zig = s.metadata.zig_index; + const truth = { + range: Math.hypot(g.x_m - o.x_m, g.y_m - o.y_m) / 1000, + bearing: deg(trueBearing(s, cursor)), + course: deg(g.course_rad), + speed: mpsToKt(g.speed_mps), + }; + const cols = estimates.map((e) => { + const r = e.row; + if (r === null) return { e, v: null, err: null }; + if (!r.converged) return { e, v: "nc" as const, err: null }; + return { + e, + v: { + range: r.range_m / 1000, + bearing: deg(bearingOf(r.x_m - o.x_m, r.y_m - o.y_m)), + course: deg(r.course_rad), + speed: mpsToKt(r.speed_mps), + }, + err: errorOf(s, r), + }; + }); + const bearingErr = (b: number): number => { + const d = Math.abs(b - truth.bearing) % 360; + return d > 180 ? 360 - d : d; + }; + const line = ( + name: string, + t: string, + get: (v: { range: number; bearing: number; course: number; speed: number }) => number, + err: (c: (typeof cols)[number]) => string, + digits: number, + ): JSX.Element => ( + + {name} + {t} + {cols.map((c) => ( + + {c.v === null + ? dash + : c.v === "nc" + ? "not conv." + : `${get(c.v).toFixed(digits)} (${err(c)})`} + + ))} + + ); + return ( +
+ + + + + + {estimates.map((e) => ( + + ))} + + + + {line( + "range, km", + truth.range.toFixed(2), + (v) => v.range, + (c) => `${fx((c.err?.range_rel ?? NaN) * 100, 0)}%`, + 2, + )} + {line( + "bearing, °", + truth.bearing.toFixed(1), + (v) => v.bearing, + (c) => fx(c.v !== null && c.v !== "nc" ? bearingErr(c.v.bearing) : NaN, 1), + 1, + )} + {line( + "course, °", + truth.course.toFixed(0), + (v) => v.course, + (c) => fx(c.err?.course_deg, 0), + 0, + )} + {line( + "speed, kt", + truth.speed.toFixed(1), + (v) => v.speed, + (c) => fx(c.err?.speed_kt, 1), + 1, + )} + + + + ))} + + +
at t = {(o.timestamp_s / 60).toFixed(1)} mintruth + {e.label} (error) +
position error, m + {cols.map((c) => ( + + {fx(c.err?.position_m, 0)} +
+

+ Ownship ({(o.x_m / 1000).toFixed(2)}, {(o.y_m / 1000).toFixed(2)}) km, course{" "} + {deg(o.course_rad).toFixed(0)}°, speed {mpsToKt(o.speed_mps).toFixed(1)} kt + {cursor < (s.metadata.turn_start_index ?? zig) + ? ", leg 1" + : cursor < zig + ? ", turning" + : ", leg 2"} + . +

+ {cursor <= zig && ( +

+ Before the zig the target's range is unobservable from bearings alone: the estimates + here are shown but not scored. +

+ )} +
+ ); +} + +export interface SummaryProps { + rows: readonly { label: string; color: string; e: ScenarioErrors | null }[]; +} + +/** Whole-scenario medians over the post-zig rows, as the gates score them. */ +export function Summary({ rows }: SummaryProps): JSX.Element { + return ( + + + + + + + + + + + + + {rows.map(({ label, color, e }) => ( + + + + + + + + + ))} + +
post-zig mediansrangecoursespeedmean positionnot conv.
{label}{e === null ? dash : `${fx(e.rangeRelMedian * 100, 1)}%`}{e === null ? dash : `${fx(e.courseDegMedian, 1)}°`}{e === null ? dash : `${fx(e.speedKtMedian, 2)} kt`}{e === null ? dash : `${fx(e.positionMeanM, 0)} m`}{e === null ? dash : `${String(e.nonConverged)} / ${String(e.rows)}`}
+ ); +} + +export interface MetadataProps { + s: Scenario; + source: ScenarioSource; + generatorVersion: string; +} + +/** Parameters, derived keys and provenance (FR-005). */ +export function Metadata({ s, source, generatorVersion }: MetadataProps): JSX.Element { + const m = s.metadata; + const p = m.parameters; + const rate = p.ownship_turn_rate_rad_s ?? 0; + const items: [string, string][] = [ + [ + "dataset", + `${source.label} (seed ${source.seed}, indices ${String(source.start)}–${String(source.end - 1)})`, + ], + ["scenario", `index ${String(m.index)}, scenario seed ${m.seed}`], + ["generator", generatorVersion], + [ + "window", + `${(p.window_s / 60).toFixed(0)} min, ${String(m.samples)} samples at ${String(p.dt_s)} s`, + ], + ["noise σ", `${radToDeg(p.noise_sigma_rad).toFixed(2)}°`], + ["dropouts", `${(p.dropout_fraction * 100).toFixed(1)}%`], + [ + "ownship", + `${mpsToKt(p.ownship_speed_mps).toFixed(1)} kt, leg 1 course ${deg(p.ownship_course0_rad).toFixed(0)}°`, + ], + [ + "zig", + `${radToDeg(p.zig_angle_rad).toFixed(0)}° at ${(m.zig_time_s / 60).toFixed(1)} min` + + (rate > 0 ? `, turn rate ${radToDeg(rate).toFixed(2)}°/s` : ", instantaneous") + + `, zig index ${String(m.zig_index)}`, + ], + [ + "target", + `${(p.target_range0_m / 1000).toFixed(1)} km on ${deg(p.target_bearing0_rad).toFixed(0)}°, course ${deg(p.target_course_rad).toFixed(0)}°, ${mpsToKt(p.target_speed_mps).toFixed(1)} kt`, + ], + [ + "range", + `mean ${(m.mean_range_m / 1000).toFixed(1)} km, min ${(m.min_range_m / 1000).toFixed(1)} km`, + ], + ]; + return ( + + + {items.map(([k, v]) => ( + + + + + ))} + +
{k}{v}
+ ); +} + +export interface ModelInfoProps { + pair: ModelPair; + source: ScenarioSource; +} + +const entryLine = (e: ModelEntry): string => + `${e.id}: ${e.architecture}, ${e.parameterCount.toLocaleString("en-GB")} parameters, ${e.featureSet} features, best epoch ${String(e.bestEpoch)}`; + +/** The pair's manifest summary and a note when the scenario is not from its training dataset (US4). */ +export function ModelInfo({ pair, source }: ModelInfoProps): JSX.Element { + const seeds = [pair.instantaneous.trainedOnSeed, pair.timeLate.trainedOnSeed]; + const trained = seeds[0] ?? seeds[1]; + return ( +
+

+ {pair.label}: {pair.gate}, scored in {pair.experiment}. +

+
    +
  • {entryLine(pair.instantaneous)}
  • +
  • {entryLine(pair.timeLate)}
  • +
+ {trained === null || trained === undefined ? ( +

The manifest does not record the training dataset's seed.

+ ) : trained === source.seed ? ( +

+ Trained on seed {trained}; this scenario is from the same dataset's {source.label}{" "} + split, which training never saw. +

+ ) : ( +

+ Trained on seed {trained}; this scenario comes from a different dataset (seed{" "} + {source.seed}). + {source.note === undefined ? "" : ` ${source.note}`} +

+ )} +
+ ); +} diff --git a/packages/demos/src/viewer/PlanView.tsx b/packages/demos/src/viewer/PlanView.tsx new file mode 100644 index 00000000..99934eeb --- /dev/null +++ b/packages/demos/src/viewer/PlanView.tsx @@ -0,0 +1,470 @@ +// The plan view (spec 006 FR-003): equal aspect, north up, pan and zoom, a labelled 1–2–5 grid, +// both tracks with the turn and zig marked, observed bearing rays up to the cursor, headings, and +// in model modes the estimates. Everything is drawn in fixed pixel coordinates through a +// world → pixel map, so text and markers keep their size at every zoom. +import type { Prediction, PredictionRow, Scenario } from "@tma/types"; +import { type JSX, type RefObject, useEffect, useRef, useState } from "react"; +import type { Mode } from "../core/catalogue.js"; +import { formatDistance, gridLines, gridStep } from "../core/geometry.js"; +import { rowAtSample } from "../core/estimates.js"; +import type { EngagementView } from "../core/view-model.js"; +import { C, FONT } from "./style.js"; + +export interface PlanViewProps { + s: Scenario; + view: EngagementView; + /** Cursor sample index. */ + cursor: number; + mode: Mode; + model: Prediction | null; + classical: Prediction | null; + trueBearings: boolean; + svgRef: RefObject; +} + +const S = 640; +const HALF = S / 2; +/** Seconds of motion the velocity vectors show. */ +export const VECTOR_S = 300; + +interface Viewport { + cx: number; + cy: number; + /** Half the visible width, metres. */ + half: number; +} + +const initial = (v: EngagementView): Viewport => ({ + cx: (v.bounds.minX + v.bounds.maxX) / 2, + cy: (v.bounds.minY + v.bounds.maxY) / 2, + half: (v.bounds.maxX - v.bounds.minX) / 2, +}); + +const f1 = (v: number): string => v.toFixed(1); + +/** Polyline path through the points; null points break it. */ +function pathOf(pts: readonly ([number, number] | null)[]): string { + let d = ""; + let pen = false; + for (const p of pts) { + if (p === null) { + pen = false; + continue; + } + d += `${pen ? "L" : "M"}${f1(p[0])},${f1(p[1])}`; + pen = true; + } + return d; +} + +export function PlanView(props: PlanViewProps): JSX.Element { + const { s, view, cursor, mode, model, classical, trueBearings, svgRef } = props; + const [vp, setVp] = useState(() => initial(view)); + const vpRef = useRef(vp); + vpRef.current = vp; + const drag = useRef<{ x: number; y: number; vp: Viewport } | null>(null); + + // Wheel zoom needs a non-passive listener to stop the page scrolling. + useEffect(() => { + const el = svgRef.current; + if (el === null) return; + const onWheel = (e: WheelEvent): void => { + e.preventDefault(); + const r = el.getBoundingClientRect(); + const sx = ((e.clientX - r.left) / r.width) * S; + const sy = ((e.clientY - r.top) / r.height) * S; + const v = vpRef.current; + const k = HALF / v.half; + const wx = v.cx + (sx - HALF) / k; + const wy = v.cy - (sy - HALF) / k; + const half = Math.min(2e5, Math.max(200, v.half * (e.deltaY > 0 ? 1.15 : 1 / 1.15))); + const k2 = HALF / half; + setVp({ cx: wx - (sx - HALF) / k2, cy: wy + (sy - HALF) / k2, half }); + }; + el.addEventListener("wheel", onWheel, { passive: false }); + return () => { + el.removeEventListener("wheel", onWheel); + }; + }, [svgRef]); + + const k = HALF / vp.half; + const px = (x: number): number => HALF + (x - vp.cx) * k; + const py = (y: number): number => HALF - (y - vp.cy) * k; + const pt = (x: number, y: number): [number, number] => [px(x), py(y)]; + + const own = s.ownship.states; + const tgt = s.target.states; + const n = s.metadata.samples; + const zig = s.metadata.zig_index; + const { leg1, turn, leg2 } = view.legs; + const ownAt = (i: number): [number, number] | null => { + const o = own[i]; + return o === undefined ? null : pt(o.x_m, o.y_m); + }; + const seg = (a: number, b: number): string => { + const pts: [number, number][] = []; + for (let i = Math.max(0, a); i < Math.min(n, b); i++) { + const p = ownAt(i); + if (p !== null) pts.push(p); + } + return pathOf(pts); + }; + + // Grid at the current zoom. + const step = gridStep(2 * vp.half); + const xs = gridLines(vp.cx - vp.half, vp.cx + vp.half, step); + const ys = gridLines(vp.cy - vp.half, vp.cy + vp.half, step); + + // Observed bearing rays up to the cursor (dropouts omitted). + const rayLen = 4 * Math.max(view.bounds.maxX - view.bounds.minX, 2 * vp.half); + let lastRay = -1; + const rays: string[] = []; + for (let i = 0; i <= cursor && i < n; i++) { + const o = own[i]; + const b = s.observations[i]; + if (o === undefined || b === undefined || !b.valid) continue; + lastRay = i; + rays.push( + `M${f1(px(o.x_m))},${f1(py(o.y_m))}L${f1(px(o.x_m + rayLen * Math.sin(b.bearing_rad)))},${f1(py(o.y_m + rayLen * Math.cos(b.bearing_rad)))}`, + ); + } + const curRay = lastRay >= 0 ? rays[rays.length - 1] : undefined; + + const oc = own[cursor]; + const tc = tgt[cursor]; + const heading = (x: number, y: number, c: number, color: string, key: string): JSX.Element => { + const [hx, hy] = pt(x, y); + const dx = Math.sin(c); + const dy = -Math.cos(c); + const tip = `${f1(hx + dx * 11)},${f1(hy + dy * 11)}`; + const l = `${f1(hx - dx * 5 + dy * 5)},${f1(hy - dy * 5 - dx * 5)}`; + const r = `${f1(hx - dx * 5 - dy * 5)},${f1(hy - dy * 5 + dx * 5)}`; + return ( + + ); + }; + + const estPoint = (r: PredictionRow | null): [number, number] | null => + r === null || !r.converged ? null : pt(r.x_m, r.y_m); + const vector = (r: PredictionRow, color: string, key: string): JSX.Element => { + const a = pt(r.x_m, r.y_m); + const b = pt( + r.x_m + r.speed_mps * VECTOR_S * Math.sin(r.course_rad), + r.y_m + r.speed_mps * VECTOR_S * Math.cos(r.course_rad), + ); + return ( + + ); + }; + + const modelRow = rowAtSample(model, s, cursor); + const classRow = rowAtSample(classical, s, cursor); + const estimates: JSX.Element[] = []; + if (mode !== "truth") { + // Trail (instantaneous, up to the cursor) or track (time-late, the whole window). + const trail = (p: Prediction | null, color: string, key: string, dots: boolean): void => { + if (p === null) return; + const pre: ([number, number] | null)[] = []; + const post: ([number, number] | null)[] = []; + const dt = s.metadata.parameters.dt_s; + for (const r of p.rows) { + const i = Math.round(r.timestamp_s / dt); + if (mode === "instantaneous" && i > cursor) break; + const q = estPoint(r); + (i <= zig ? pre : post).push(q); + } + if (dots) { + estimates.push( + + {pre.map((q, j) => + q === null ? null : ( + + ), + )} + {post.map((q, j) => + q === null ? null : ( + + ), + )} + , + ); + } else { + estimates.push( + , + , + ); + } + }; + if (mode === "instantaneous") { + trail(model, C.model, "model", true); + if (classical !== null) trail(classical, C.classical, "classical", true); + } else { + trail(model, C.model, "model", false); + if (classical !== null) trail(classical, C.classical, "classical", false); + } + if (modelRow?.converged === true && oc !== undefined) { + const [ox, oy] = pt(oc.x_m, oc.y_m); + if (mode === "instantaneous") { + estimates.push( + , + ); + } + estimates.push(vector(modelRow, C.model, "model")); + const q = estPoint(modelRow); + if (q !== null) + estimates.push( + , + ); + } + if (classRow?.converged === true) { + estimates.push(vector(classRow, C.classical, "classical")); + const q = estPoint(classRow); + if (q !== null) + estimates.push( + , + ); + } + } + + const zigP = ownAt(zig); + const ownStart = ownAt(0); + const tgtStart = tgt[0] === undefined ? null : pt(tgt[0].x_m, tgt[0].y_m); + const tgtPath = pathOf(tgt.map((p) => pt(p.x_m, p.y_m))); + + const legend: [string, string, string?][] = [ + ["ownship", C.own], + ["turn", C.turn], + ["target", C.target], + ["bearings", C.bearing], + ]; + if (trueBearings) legend.push(["true bearing", C.trueBearing, "4 3"]); + if (mode !== "truth") { + legend.push(["model", C.model]); + if (classical !== null) legend.push(["classical", C.classical]); + } + + return ( + { + e.currentTarget.setPointerCapture(e.pointerId); + drag.current = { x: e.clientX, y: e.clientY, vp }; + }} + onPointerMove={(e) => { + const d = drag.current; + if (d === null || e.buttons !== 1) return; + const r = e.currentTarget.getBoundingClientRect(); + const scale = S / r.width / (HALF / d.vp.half); + setVp({ + ...d.vp, + cx: d.vp.cx - (e.clientX - d.x) * scale, + cy: d.vp.cy + (e.clientY - d.y) * scale, + }); + }} + onPointerUp={() => { + drag.current = null; + }} + onDoubleClick={() => { + setVp(initial(view)); + }} + > + + + + + {[ + ["model", C.model], + ["classical", C.classical], + ].map(([id, color]) => ( + + + + ))} + + + + + {xs.map((x) => ( + + ))} + {ys.map((y) => ( + + ))} + + + {curRay !== undefined && ( + + )} + {trueBearings && oc !== undefined && tc !== undefined && ( + + )} + + + + + {estimates} + {ownStart !== null && ( + + )} + {tgtStart !== null && ( + + )} + {zigP !== null && ( + + + + zig + + + )} + {oc !== undefined && heading(oc.x_m, oc.y_m, oc.course_rad, C.own, "own")} + {tc !== undefined && heading(tc.x_m, tc.y_m, tc.course_rad, C.target, "tgt")} + + + {xs.map((x) => ( + + {formatDistance(x)} + + ))} + {ys.map((y) => ( + + {formatDistance(y)} + + ))} + + + + + N + + + + + + grid {formatDistance(step)} + + {legend.map(([label, color, dash], j) => ( + + + + {label} + + + ))} + + + ); +} diff --git a/packages/demos/src/viewer/TimePlot.tsx b/packages/demos/src/viewer/TimePlot.tsx new file mode 100644 index 00000000..36b30039 --- /dev/null +++ b/packages/demos/src/viewer/TimePlot.tsx @@ -0,0 +1,232 @@ +// One time-series plot (spec 006 FR-004): shared time axis, cursor, gate markers, a shaded turn. +import { type JSX, type PointerEvent, type Ref, useCallback } from "react"; +import { C, FONT } from "./style.js"; + +export interface Line { + label: string; + values: readonly (number | null)[]; + color: string; + dash?: string | undefined; + /** Draw as dots (observed bearings) rather than a line. */ + dots?: boolean; + /** Break the line where it jumps by more than half this period (angles: 360). */ + wrap?: number | undefined; + width?: number | undefined; +} + +export interface Marker { + t: number; + label: string; +} + +export interface TimePlotProps { + title: string; + unit: string; + t: readonly number[]; + lines: readonly Line[]; + cursorT: number; + windowS: number; + markers?: readonly Marker[]; + shade?: readonly [number, number] | null; + /** Grey band from 0 to this time, labelled unobservable (estimates before the zig). */ + unobservableUntil?: number | null; + /** Fixed y range (angles); otherwise from the data. */ + yRange?: readonly [number, number]; + yTicks?: readonly number[]; + onCursor?: (t: number) => void; + svgRef?: Ref; +} + +const W = 640; +const H = 150; +const L = 48; +const R = 10; +const T = 22; +const B = 22; + +function niceTicks(lo: number, hi: number, n = 4): number[] { + const span = hi - lo; + if (!(span > 0)) return [lo]; + const raw = span / n; + const p = 10 ** Math.floor(Math.log10(raw)); + const m = raw / p; + const step = (m < 1.5 ? 1 : m < 3.5 ? 2 : m < 7.5 ? 5 : 10) * p; + const out: number[] = []; + for (let v = Math.ceil(lo / step) * step; v <= hi + step * 1e-9; v += step) + out.push(Number(v.toPrecision(12))); + return out; +} + +const fmt = (v: number): string => + Math.abs(v) >= 100 || Number.isInteger(v) ? v.toFixed(0) : v.toPrecision(2); + +export function TimePlot(props: TimePlotProps): JSX.Element { + const { t, lines, cursorT, windowS, onCursor } = props; + let lo = Infinity; + let hi = -Infinity; + if (props.yRange !== undefined) [lo, hi] = props.yRange; + else { + for (const l of lines) + for (const v of l.values) + if (v !== null && Number.isFinite(v)) { + lo = Math.min(lo, v); + hi = Math.max(hi, v); + } + if (!Number.isFinite(lo)) [lo, hi] = [0, 1]; + if (hi - lo < 1e-9) [lo, hi] = [lo - 1, hi + 1]; + const pad = (hi - lo) * 0.08; + lo -= pad; + hi += pad; + } + const x = (s: number): number => L + ((W - L - R) * s) / Math.max(windowS, 1); + const y = (v: number): number => + T + ((H - T - B) * (hi - Math.min(hi, Math.max(lo, v)))) / (hi - lo); + const ticks = props.yTicks ?? niceTicks(lo, hi); + const tTicks = niceTicks(0, windowS / 60, 6).filter((m) => m * 60 <= windowS); + + const pick = useCallback( + (e: PointerEvent): void => { + if (onCursor === undefined) return; + const r = e.currentTarget.getBoundingClientRect(); + const px = ((e.clientX - r.left) / r.width) * W; + const s = ((px - L) / (W - L - R)) * windowS; + onCursor(Math.min(windowS, Math.max(0, s))); + }, + [onCursor, windowS], + ); + + const paths = lines.map((l) => { + if (l.dots === true) { + return ( + + {l.values.map((v, i) => + v === null || !Number.isFinite(v) ? null : ( + + ), + )} + + ); + } + const segs: string[] = []; + let cur = ""; + let prev: number | null = null; + l.values.forEach((v, i) => { + if (v === null || !Number.isFinite(v)) { + if (cur !== "") segs.push(cur); + cur = ""; + prev = null; + return; + } + const brk = prev !== null && l.wrap !== undefined && Math.abs(v - prev) > l.wrap / 2; + if (brk && cur !== "") { + segs.push(cur); + cur = ""; + } + cur += `${cur === "" ? "M" : "L"}${x(t[i] ?? 0).toFixed(1)},${y(v).toFixed(1)}`; + prev = v; + }); + if (cur !== "") segs.push(cur); + return ( + + ); + }); + + return ( + { + e.currentTarget.setPointerCapture(e.pointerId); + pick(e); + }} + onPointerMove={(e) => { + if (e.buttons === 1) pick(e); + }} + style={{ fontFamily: FONT }} + > + + {props.unobservableUntil != null && ( + + + + unobservable + + + )} + {props.shade != null && ( + + )} + {ticks.map((v) => ( + + + + {fmt(v)} + + + ))} + {tTicks.map((m) => ( + + + + {m} + + + ))} + {(props.markers ?? []).map((mk) => ( + + + + {mk.label} + + + ))} + {paths} + + + {props.title}{" "} + + ({props.unit}; time in min) + + + + {lines.map((l, k) => ( + + {k === 0 ? "" : " · "} + {l.label} + + ))} + + + ); +} diff --git a/packages/demos/src/viewer/download.ts b/packages/demos/src/viewer/download.ts new file mode 100644 index 00000000..5dd026ac --- /dev/null +++ b/packages/demos/src/viewer/download.ts @@ -0,0 +1,67 @@ +// Exports (spec 006 FR-008): SVGs to one PNG, text to a file. Browser only. + +function save(blob: Blob, name: string): void { + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = name; + a.click(); + setTimeout(() => { + URL.revokeObjectURL(a.href); + }, 1000); +} + +export function saveText(text: string, name: string, type = "application/x-ndjson"): void { + save(new Blob([text], { type }), name); +} + +const size = (svg: SVGSVGElement): { w: number; h: number } => { + const vb = svg.viewBox.baseVal; + return { w: vb.width || svg.clientWidth, h: vb.height || svg.clientHeight }; +}; + +/** Stack the SVGs vertically into one PNG at `scale` × their viewBox size. */ +export async function savePng(svgs: SVGSVGElement[], name: string, scale = 2): Promise { + const parts = svgs.map((s) => ({ s, ...size(s) })); + const width = Math.max(...parts.map((p) => p.w)); + const height = parts.reduce((h, p) => h + p.h, 0); + let y = 0; + const body = parts + .map((p) => { + const clone = p.s.cloneNode(true) as SVGSVGElement; + clone.setAttribute("x", "0"); + clone.setAttribute("y", String(y)); + clone.setAttribute("width", String(p.w)); + clone.setAttribute("height", String(p.h)); + y += p.h; + return new XMLSerializer().serializeToString(clone); + }) + .join(""); + const svg = `${body}`; + const img = new Image(); + const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml" })); + try { + await new Promise((resolve, reject) => { + img.onload = () => { + resolve(); + }; + img.onerror = () => { + reject(new Error("could not render the SVG")); + }; + img.src = url; + }); + const canvas = document.createElement("canvas"); + canvas.width = Math.round(width * scale); + canvas.height = Math.round(height * scale); + const ctx = canvas.getContext("2d"); + if (ctx === null) throw new Error("no 2D canvas"); + ctx.scale(scale, scale); + ctx.drawImage(img, 0, 0); + const blob = await new Promise((resolve) => { + canvas.toBlob(resolve, "image/png"); + }); + if (blob === null) throw new Error("PNG encoding failed"); + save(blob, name); + } finally { + URL.revokeObjectURL(url); + } +} diff --git a/packages/demos/src/viewer/index.ts b/packages/demos/src/viewer/index.ts new file mode 100644 index 00000000..50cdb575 --- /dev/null +++ b/packages/demos/src/viewer/index.ts @@ -0,0 +1,4 @@ +// @tma/demos/viewer: the React engagement and model viewer (spec 006). Browser only. +export { EngagementViewer, type EngagementViewerProps } from "./EngagementViewer.js"; +export { PlanView, type PlanViewProps } from "./PlanView.js"; +export { TimePlot, type TimePlotProps } from "./TimePlot.js"; diff --git a/packages/demos/src/viewer/runtime.ts b/packages/demos/src/viewer/runtime.ts new file mode 100644 index 00000000..86f4dbf1 --- /dev/null +++ b/packages/demos/src/viewer/runtime.ts @@ -0,0 +1,36 @@ +// tfjs, loaded once and lazily (spec 006 FR-007): core, the CPU backend and layers only. The CPU +// backend is chosen because it gives the same numbers on every machine (WebGL does not). +import type { Tensor } from "@tensorflow/tfjs-core"; +import type { LayersModel } from "@tensorflow/tfjs-layers"; +import type { TfLike } from "../core/estimates.js"; + +export interface Runtime { + tf: TfLike; + load(url: string): Promise; +} + +let ready: Promise | null = null; +const models = new Map>(); + +export function runtime(): Promise { + ready ??= (async () => { + const tf = await import("@tensorflow/tfjs-core"); + await import("@tensorflow/tfjs-backend-cpu"); + const layers = await import("@tensorflow/tfjs-layers"); + await tf.setBackend("cpu"); + await tf.ready(); + return { + tf, + load(url: string): Promise { + let m = models.get(url); + if (m === undefined) { + m = layers.loadLayersModel(url); + models.set(url, m); + m.catch(() => models.delete(url)); + } + return m; + }, + }; + })(); + return ready; +} diff --git a/packages/demos/src/viewer/style.ts b/packages/demos/src/viewer/style.ts new file mode 100644 index 00000000..e2c6e715 --- /dev/null +++ b/packages/demos/src/viewer/style.ts @@ -0,0 +1,41 @@ +// Colours and layout for the viewer. Plots draw on their own light background so they read the +// same in light and dark site themes and in PNG exports. +export const C = { + paper: "#ffffff", + ink: "#1f2328", + muted: "#6e7781", + grid: "#d8dee4", + gridMajor: "#afb8c1", + own: "#0969da", + turn: "#bf8700", + target: "#cf222e", + bearing: "#57606a", + trueBearing: "#8250df", + model: "#8250df", + classical: "#1a7f37", + naive: "#9a6700", + cursor: "#24292f", + gate: "#bc4c00", +} as const; + +export const FONT = "system-ui, -apple-system, 'Segoe UI', sans-serif"; + +/** Scoped CSS for the viewer's layout (one class prefix, no global rules). */ +export const CSS = ` +.tmav { font-family: ${FONT}; font-size: 14px; } +.tmav-bar { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; margin: 8px 0 12px; } +.tmav-bar label { display: inline-flex; gap: 6px; align-items: center; } +.tmav-bar input[type=number] { width: 7.5em; } +.tmav-grid { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); gap: 16px; align-items: start; } +@media (max-width: 960px) { .tmav-grid { grid-template-columns: minmax(0, 1fr); } } +.tmav-panel { border: 1px solid var(--ifm-color-emphasis-300, #d0d7de); border-radius: 6px; padding: 8px 12px; margin-bottom: 12px; overflow-x: auto; } +.tmav-panel h3 { font-size: 15px; margin: 4px 0 8px; } +.tmav table { font-size: 13px; border-collapse: collapse; margin: 0; display: table; width: 100%; } +.tmav td, .tmav th { padding: 2px 8px; border: none; text-align: right; white-space: nowrap; } +.tmav td:first-child, .tmav th:first-child { text-align: left; } +.tmav tr:nth-child(2n) { background: transparent; } +.tmav-note { color: var(--ifm-color-emphasis-700, #57606a); font-size: 13px; } +.tmav-warn { color: #bc4c00; font-size: 13px; } +.tmav-svg { width: 100%; height: auto; display: block; border-radius: 6px; touch-action: none; } +.tmav-slider { width: 100%; } +`; diff --git a/packages/demos/test/agreement.test.ts b/packages/demos/test/agreement.test.ts new file mode 100644 index 00000000..845ca669 --- /dev/null +++ b/packages/demos/test/agreement.test.ts @@ -0,0 +1,93 @@ +// FR-007: the viewer's inference (tfjs-core on the CPU backend, as the browser runs it) agrees +// with the pipeline's (tfjs-node, `tma-model-predict`'s backend) to float32 tolerance, on the +// committed 2c and iteration-1 carried models. +import { readFileSync } from "node:fs"; +import * as tfc from "@tensorflow/tfjs-core"; +import "@tensorflow/tfjs-backend-cpu"; +import { buildWindow, inputWidth, outputsToPrediction, ROWS } from "@tma/features"; +import { loadModel } from "@tma/models"; +import type { FeatureSet, NormalisationConstants, Prediction } from "@tma/types"; +import { afterAll, describe, expect, it } from "vitest"; +import { modelPrediction, scenarioAt } from "../src/index.js"; +import { I1_TEST, I2_TEST, repoPath } from "./helpers.js"; + +interface Entry { + id: string; + featureSet: FeatureSet; + normalisation: NormalisationConstants; +} +const entry = (dir: string): Entry => { + const m = JSON.parse(readFileSync(`${dir}/manifest.json`, "utf8")) as { + id: string; + config: { feature_set?: FeatureSet }; + normalisation: NormalisationConstants; + }; + return { id: m.id, featureSet: m.config.feature_set ?? "raw", normalisation: m.normalisation }; +}; + +type Loaded = Awaited>; + +/** + * The pipeline's path: the "tensorflow" backend `@tma/models` registers (tfjs-node, as + * `tma-model-predict` runs), over the same window, decoded from row 0. + */ +function nodePrediction(model: Loaded, s: ReturnType, e: Entry): Prediction { + const w = buildWindow(s, e.normalisation, e.featureSet); + const y = tfc.tidy(() => { + const p = model.predict(tfc.tensor3d(w.inputs, [1, ROWS, inputWidth(e.featureSet)])); + return (Array.isArray(p) ? p[0] : p)?.dataSync() as Float32Array; + }); + return outputsToPrediction(s, y, e.normalisation, e.id, 0); +} + +describe("agreement: browser (CPU) inference vs tfjs-node (FR-007)", () => { + afterAll(async () => { + await tfc.setBackend("tensorflow"); + }); + for (const [dir, source] of [ + ["experiments/20261016-i2d-val/models/lstm-instantaneous", I2_TEST], + ["experiments/20261016-i2d-val/models/bilstm-time-late", I2_TEST], + ["experiments/20261012-g1-r2/models/lstm-instantaneous", I1_TEST], + ] as const) { + it( + dir, + async () => { + const path = repoPath(dir); + const e = entry(path); + const scenarios = [90000, 93210].map((i) => scenarioAt(source, i)); + await tfc.setBackend("tensorflow"); + expect(tfc.getBackend()).toBe("tensorflow"); + const nodeModel = await loadModel(path); + const ref = scenarios.map((s) => nodePrediction(nodeModel, s, e)); + nodeModel.dispose(); + await tfc.setBackend("cpu"); + expect(tfc.getBackend()).toBe("cpu"); + const cpuModel = await loadModel(path); + const got = scenarios.map((s) => modelPrediction(tfc, cpuModel, s, e)); + cpuModel.dispose(); + let worst = 0; + got.forEach((p, k) => { + const r = ref[k]; + expect(p.rows).toHaveLength(r?.rows.length ?? -1); + p.rows.forEach((row, j) => { + const o = r?.rows[j]; + if (o === undefined) throw new Error("row missing"); + expect(row.converged).toBe(o.converged); + const rel = Math.abs(row.range_m - o.range_m) / Math.max(1, o.range_m); + const dc = Math.abs( + Math.atan2( + Math.sin(row.course_rad - o.course_rad), + Math.cos(row.course_rad - o.course_rad), + ), + ); + const ds = Math.abs(row.speed_mps - o.speed_mps); + worst = Math.max(worst, rel, dc, ds); + }); + }); + // float32 accumulation over 240 recurrent steps on two backends. + expect(worst).toBeLessThan(1e-3); + }, + 300_000, + ); + } +}); diff --git a/packages/demos/test/browser-bundle.test.ts b/packages/demos/test/browser-bundle.test.ts new file mode 100644 index 00000000..2aa7f1d1 --- /dev/null +++ b/packages/demos/test/browser-bundle.test.ts @@ -0,0 +1,52 @@ +// FR-001: the viewer's core bundles for the browser (no Node built-ins) and, run in a bare +// context, regenerates scenarios and classical estimates byte-identically with Node. +import { runInNewContext } from "node:vm"; +import { canonicalJson } from "@tma/data"; +import { classicalPredict } from "@tma/classical"; +import { generateScenario, parseSeed } from "@tma/sim"; +import { build } from "esbuild"; +import { describe, expect, it } from "vitest"; +import { I1_TEST, I2_TEST, repoPath } from "./helpers.js"; + +const src = (p: string): string => repoPath(`packages/${p}`); +const ALIAS: Record = { + "@tma/types": src("types/src/index.ts"), + "@tma/sim": src("sim/src/index.ts"), + "@tma/features/browser": src("features/src/browser.ts"), + "@tma/classical/browser": src("classical/src/browser.ts"), + "@tma/eval/browser": src("eval/src/browser.ts"), + "@tma/data/canonical": src("data/src/canonical.ts"), +}; + +describe("browser bundle (FR-001)", () => { + it("bundles with platform browser and regenerates scenarios byte-identically", async () => { + const out = await build({ + entryPoints: [src("demos/src/index.ts")], + bundle: true, + platform: "browser", + format: "iife", + globalName: "TMA", + write: false, + alias: ALIAS, + logLevel: "silent", + }); + const code = out.outputFiles[0]?.text ?? ""; + expect(code).not.toMatch(/require\(["']node:/); + const ctx: { TMA?: Record unknown> } = {}; + runInNewContext(code, ctx); + const tma = ctx.TMA; + if (tma === undefined) throw new Error("bundle did not define TMA"); + for (const [source, index] of [ + [I2_TEST, 90000], + [I2_TEST, 95555], + [I1_TEST, 90042], + ] as const) { + const browser = tma.scenarioAt?.(source, index); + const node = generateScenario(parseSeed(source.seed), index, source.ranges).scenario; + expect(canonicalJson(browser)).toBe(canonicalJson(node)); + expect(canonicalJson(tma.classicalRows?.(browser))).toBe( + canonicalJson(classicalPredict(node, "per_timestep")), + ); + } + }, 120_000); +}); diff --git a/packages/demos/test/core.test.ts b/packages/demos/test/core.test.ts new file mode 100644 index 00000000..03961c91 --- /dev/null +++ b/packages/demos/test/core.test.ts @@ -0,0 +1,147 @@ +// @tma/demos core (spec 006): geometry, URL state, scenario regeneration, estimates and errors. +import { canonicalJson } from "@tma/data"; +import { generateScenario, parseSeed } from "@tma/sim"; +import { describe, expect, it } from "vitest"; +import { + boundsOf, + clampIndex, + classicalRows, + decodeView, + encodeView, + formatDistance, + gridLines, + gridStep, + legsOf, + naiveRows, + rowAtSample, + sampleAt, + scenarioAt, + scenarioErrors, + seriesOf, + type ViewerCatalogue, + viewNdjson, +} from "../src/index.js"; +import { I1_TEST, I2_TEST } from "./helpers.js"; + +const MEANS = { log_range_km: 2, course_sin: 0, course_cos: 1, speed_mps: 5.9, n: 1 }; + +describe("geometry", () => { + it("picks 1–2–5 grid steps between 100 m and 10 km", () => { + expect(gridStep(8000)).toBe(1000); + expect(gridStep(16000)).toBe(2000); + expect(gridStep(40000)).toBe(5000); + expect(gridStep(10)).toBe(100); + expect(gridStep(1e7)).toBe(10000); + expect(gridLines(-2500, 2500, 1000)).toEqual([-2000, -1000, 0, 1000, 2000]); + }); + it("bounds are square, centred and at least the minimum span", () => { + const b = boundsOf([ + [0, 0], + [10000, 2000], + ]); + expect(b.maxX - b.minX).toBeCloseTo(b.maxY - b.minY); + expect((b.minX + b.maxX) / 2).toBeCloseTo(5000); + expect(b.maxX - b.minX).toBeGreaterThan(10000); + expect(boundsOf([]).maxX - boundsOf([]).minX).toBe(2000); + }); + it("formats distances", () => { + expect(formatDistance(500)).toBe("500 m"); + expect(formatDistance(2000)).toBe("2 km"); + expect(formatDistance(1500)).toBe("1.5 km"); + }); +}); + +describe("scenarios", () => { + it("regenerates the pipeline's scenario byte for byte and refuses indices outside the split", () => { + const s = scenarioAt(I2_TEST, 90007); + const node = generateScenario(parseSeed("201"), 90007, I2_TEST.ranges).scenario; + expect(canonicalJson(s)).toBe(canonicalJson(node)); + expect(() => scenarioAt(I2_TEST, 89999)).toThrow(RangeError); + expect(() => scenarioAt(I2_TEST, 100000)).toThrow(RangeError); + expect(clampIndex(I2_TEST, 5)).toBe(90000); + expect(clampIndex(I2_TEST, 1e9)).toBe(99999); + }); + it("legs and series follow the scenario", () => { + const s = scenarioAt(I2_TEST, 90001); + const legs = legsOf(s); + expect(legs.leg1[0]).toBe(0); + expect(legs.turn[1]).toBe(s.metadata.zig_index); + expect(legs.leg2[1]).toBe(s.metadata.samples); + const ser = seriesOf(s); + expect(ser.t).toHaveLength(s.metadata.samples); + expect(ser.bearingObsDeg.filter((b) => b === null).length).toBe( + s.observations.filter((o) => !o.valid).length, + ); + // Iteration 1 has an instantaneous zig: no turn samples. + const i1 = scenarioAt(I1_TEST, 90001); + expect(legsOf(i1).turn[0]).toBe(legsOf(i1).turn[1]); + expect(sampleAt(s, 1e9)).toBe(s.metadata.samples - 1); + }); +}); + +describe("estimates and errors", () => { + it("classical and naive rows start after the zig, and errors use the SRD §5 definitions", () => { + const s = scenarioAt(I2_TEST, 90002); + const cl = classicalRows(s); + const nv = naiveRows(s, MEANS); + expect(cl.rows).toHaveLength(s.metadata.samples - s.metadata.zig_index - 1); + expect(nv.rows[0]?.speed_mps).toBeCloseTo(5.9, 5); + expect(rowAtSample(cl, s, s.metadata.zig_index)).toBeNull(); + expect(rowAtSample(cl, s, s.metadata.zig_index + 1)).not.toBeNull(); + const e = scenarioErrors(s, nv); + expect(e.rows).toBe(nv.rows.length); + expect(e.speedKtMedian).toBeGreaterThanOrEqual(0); + const nd = viewNdjson(s, [cl, nv]).trimEnd().split("\n"); + expect(nd).toHaveLength(3); + expect(nd[0]).toBe(canonicalJson(s)); + }); +}); + +describe("view state", () => { + const cat: ViewerCatalogue = { + generatorVersion: "0.2.0", + sources: [I2_TEST, I1_TEST], + pairs: [ + { + id: "i1", + label: "", + experiment: "", + gate: "", + instantaneous: {} as never, + timeLate: {} as never, + naive: MEANS, + }, + { + id: "2c", + label: "", + experiment: "", + gate: "", + instantaneous: {} as never, + timeLate: {} as never, + naive: MEANS, + }, + ], + bookmarks: [], + }; + it("round-trips and falls back to defaults", () => { + const v = { + source: "i1-test", + index: 90123, + mode: "time-late" as const, + t: 720, + pair: "i1", + classical: false, + trueBearings: true, + }; + expect(decodeView(encodeView(v), cat)).toEqual(v); + expect(decodeView("src=nope&i=-4&mode=bad&t=x", cat)).toEqual({ + source: "i2-test", + index: 90000, + mode: "truth", + t: null, + pair: "2c", + classical: true, + trueBearings: false, + }); + }); +}); diff --git a/packages/demos/test/golden.test.ts b/packages/demos/test/golden.test.ts new file mode 100644 index 00000000..319cf6e0 --- /dev/null +++ b/packages/demos/test/golden.test.ts @@ -0,0 +1,20 @@ +// Golden (constitution: every package has a fixed-seed, byte-identical golden test): the view +// model of two fixed scenarios, including the classical estimates the viewer overlays. +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { canonicalJson } from "@tma/data"; +import { describe, expect, it } from "vitest"; +import { engagementView, scenarioAt } from "../src/index.js"; +import { I1_TEST, I2_TEST } from "./helpers.js"; + +const GOLDEN = new URL("./golden/views.ndjson", import.meta.url); + +describe("golden: demos view model", () => { + it("iteration-1 and iteration-2 test scenarios 90000, byte-identical", () => { + const text = `${[scenarioAt(I1_TEST, 90000), scenarioAt(I2_TEST, 90000)] + .map((s) => canonicalJson(engagementView(s))) + .join("\n")}\n`; + if (process.env.UPDATE_GOLDEN === "1") writeFileSync(GOLDEN, text); + expect(existsSync(GOLDEN), "golden missing: run with UPDATE_GOLDEN=1").toBe(true); + expect(text === readFileSync(GOLDEN, "utf8")).toBe(true); + }); +}); diff --git a/packages/demos/test/golden/views.ndjson b/packages/demos/test/golden/views.ndjson new file mode 100644 index 00000000..55c5b40d --- /dev/null +++ b/packages/demos/test/golden/views.ndjson @@ -0,0 +1,2 @@ +{"bounds":{"maxX":6057.17205078125,"maxY":4121.22869140625,"minX":-2837.4830859374997,"minY":-4773.4264453125},"classical":{"estimator":"classical","mode":"per_timestep","record":"prediction","rows":[{"converged":true,"course_rad":3.272043466567993,"iterations":8,"range_m":1858.0394287109375,"speed_mps":10.987236976623535,"timestamp_s":390,"x_m":113.46009063720703,"y_m":-217.42453002929688},{"converged":true,"course_rad":3.2628495693206787,"iterations":6,"range_m":1836.872314453125,"speed_mps":10.797653198242188,"timestamp_s":400,"x_m":135.49462890625,"y_m":-334.22930908203125},{"converged":true,"course_rad":3.253389835357666,"iterations":6,"range_m":1827.2939453125,"speed_mps":10.607954025268555,"timestamp_s":410,"x_m":159.826904296875,"y_m":-447.25726318359375},{"converged":true,"course_rad":3.1660778522491455,"iterations":6,"range_m":1565.2686767578125,"speed_mps":9.143492698669434,"timestamp_s":420,"x_m":450.2077331542969,"y_m":-582.047607421875},{"converged":true,"course_rad":3.191335678100586,"iterations":6,"range_m":1689.5439453125,"speed_mps":9.523296356201172,"timestamp_s":430,"x_m":366.8604431152344,"y_m":-670.3785400390625},{"converged":true,"course_rad":3.1897642612457275,"iterations":5,"range_m":1740.4168701171875,"speed_mps":9.49814510345459,"timestamp_s":440,"x_m":367.5347900390625,"y_m":-765.3914794921875},{"converged":true,"course_rad":3.2169384956359863,"iterations":6,"range_m":1905.556396484375,"speed_mps":9.964411735534668,"timestamp_s":450,"x_m":262.0672912597656,"y_m":-867.7139892578125},{"converged":true,"course_rad":3.2212371826171875,"iterations":5,"range_m":1999.504150390625,"speed_mps":10.044371604919434,"timestamp_s":460,"x_m":237.1678924560547,"y_m":-969.2708740234375},{"converged":true,"course_rad":3.230025291442871,"iterations":6,"range_m":2121.765625,"speed_mps":10.215141296386719,"timestamp_s":470,"x_m":191.62203979492188,"y_m":-1076.0257568359375},{"converged":true,"course_rad":3.228652000427246,"iterations":5,"range_m":2208.2666015625,"speed_mps":10.187627792358398,"timestamp_s":480,"x_m":188.72601318359375,"y_m":-1176.4013671875},{"converged":true,"course_rad":3.2300753593444824,"iterations":5,"range_m":2313.3125,"speed_mps":10.216541290283203,"timestamp_s":490,"x_m":173.33827209472656,"y_m":-1279.66162109375},{"converged":true,"course_rad":3.2232518196105957,"iterations":5,"range_m":2384.483642578125,"speed_mps":10.077824592590332,"timestamp_s":500,"x_m":196.1309051513672,"y_m":-1371.389404296875},{"converged":true,"course_rad":3.227112293243408,"iterations":5,"range_m":2510.924072265625,"speed_mps":10.156734466552734,"timestamp_s":510,"x_m":169.5366668701172,"y_m":-1478.41259765625},{"converged":true,"course_rad":3.226086378097534,"iterations":5,"range_m":2617.577880859375,"speed_mps":10.13538932800293,"timestamp_s":520,"x_m":165.8999786376953,"y_m":-1577.598388671875},{"converged":true,"course_rad":3.2181308269500732,"iterations":5,"range_m":2690.9169921875,"speed_mps":9.971550941467285,"timestamp_s":530,"x_m":196.66925048828125,"y_m":-1661.3585205078125},{"converged":true,"course_rad":3.2219250202178955,"iterations":5,"range_m":2828.516845703125,"speed_mps":10.049713134765625,"timestamp_s":540,"x_m":169.99505615234375,"y_m":-1769.849365234375},{"converged":true,"course_rad":3.221367597579956,"iterations":5,"range_m":2945.703125,"speed_mps":10.038039207458496,"timestamp_s":550,"x_m":164.81484985351562,"y_m":-1868.5452880859375},{"converged":true,"course_rad":3.218043565750122,"iterations":5,"range_m":3048.89794921875,"speed_mps":9.968442916870117,"timestamp_s":560,"x_m":174.2761688232422,"y_m":-1959.0579833984375},{"converged":true,"course_rad":3.2166261672973633,"iterations":6,"range_m":3164.500244140625,"speed_mps":9.938855171203613,"timestamp_s":570,"x_m":174.201904296875,"y_m":-2054.076416015625},{"converged":true,"course_rad":3.217528820037842,"iterations":5,"range_m":3295.81787109375,"speed_mps":9.957818031311035,"timestamp_s":580,"x_m":161.84925842285156,"y_m":-2156.1875},{"converged":true,"course_rad":3.2157092094421387,"iterations":5,"range_m":3411.648193359375,"speed_mps":9.919404983520508,"timestamp_s":590,"x_m":164.37062072753906,"y_m":-2248.99658203125},{"converged":true,"course_rad":3.2152657508850098,"iterations":5,"range_m":3537.35791015625,"speed_mps":9.910028457641602,"timestamp_s":600,"x_m":159.52146911621094,"y_m":-2346.23828125},{"converged":true,"course_rad":3.216888666152954,"iterations":5,"range_m":3678.388671875,"speed_mps":9.944652557373047,"timestamp_s":610,"x_m":142.88131713867188,"y_m":-2451.628662109375},{"converged":true,"course_rad":3.2165496349334717,"iterations":5,"range_m":3807.360595703125,"speed_mps":9.937355995178223,"timestamp_s":620,"x_m":137.39739990234375,"y_m":-2549.3369140625},{"converged":true,"course_rad":3.2184672355651855,"iterations":6,"range_m":3953.913818359375,"speed_mps":9.97903823852539,"timestamp_s":630,"x_m":118.39209747314453,"y_m":-2657.175048828125},{"converged":true,"course_rad":3.2175521850585938,"iterations":5,"range_m":4080.663330078125,"speed_mps":9.958992958068848,"timestamp_s":640,"x_m":116.3660659790039,"y_m":-2752.2626953125},{"converged":true,"course_rad":3.2170090675354004,"iterations":5,"range_m":4210.8154296875,"speed_mps":9.947073936462402,"timestamp_s":650,"x_m":112.20858764648438,"y_m":-2848.82275390625},{"converged":true,"course_rad":3.2166810035705566,"iterations":5,"range_m":4343.25927734375,"speed_mps":9.939858436584473,"timestamp_s":660,"x_m":106.80117797851562,"y_m":-2946.27685546875},{"converged":true,"course_rad":3.217958927154541,"iterations":5,"range_m":4489.68115234375,"speed_mps":9.96816635131836,"timestamp_s":670,"x_m":91.04541778564453,"y_m":-3052.482177734375},{"converged":true,"course_rad":3.21759033203125,"iterations":5,"range_m":4623.1826171875,"speed_mps":9.959946632385254,"timestamp_s":680,"x_m":85.88263702392578,"y_m":-3149.733154296875},{"converged":true,"course_rad":3.215147018432617,"iterations":5,"range_m":4738.84130859375,"speed_mps":9.905604362487793,"timestamp_s":690,"x_m":94.69388580322266,"y_m":-3234.3408203125},{"converged":true,"course_rad":3.2163450717926025,"iterations":5,"range_m":4886.8759765625,"speed_mps":9.932276725769043,"timestamp_s":700,"x_m":79.26830291748047,"y_m":-3340.6162109375},{"converged":true,"course_rad":3.2153658866882324,"iterations":5,"range_m":5015.74609375,"speed_mps":9.910380363464355,"timestamp_s":710,"x_m":78.62601470947266,"y_m":-3433.2958984375},{"converged":true,"course_rad":3.215390205383301,"iterations":4,"range_m":5154.19287109375,"speed_mps":9.910923957824707,"timestamp_s":720,"x_m":71.15090942382812,"y_m":-3532.293701171875},{"converged":true,"course_rad":3.2146518230438232,"iterations":5,"range_m":5285.63134765625,"speed_mps":9.894367218017578,"timestamp_s":730,"x_m":69.1032943725586,"y_m":-3625.98681640625},{"converged":true,"course_rad":3.21474289894104,"iterations":4,"range_m":5425.4130859375,"speed_mps":9.896411895751953,"timestamp_s":740,"x_m":61.22307205200195,"y_m":-3725.322509765625},{"converged":true,"course_rad":3.215463161468506,"iterations":5,"range_m":5572.07177734375,"speed_mps":9.912684440612793,"timestamp_s":750,"x_m":48.68832015991211,"y_m":-3829.404296875},{"converged":true,"course_rad":3.215533971786499,"iterations":4,"range_m":5712.5771484375,"speed_mps":9.914292335510254,"timestamp_s":760,"x_m":40.841957092285156,"y_m":-3928.80859375},{"converged":true,"course_rad":3.2154080867767334,"iterations":5,"range_m":5851.30322265625,"speed_mps":9.911425590515137,"timestamp_s":770,"x_m":34.47512435913086,"y_m":-4026.67578125},{"converged":true,"course_rad":3.215876817703247,"iterations":5,"range_m":5996.8447265625,"speed_mps":9.922142028808594,"timestamp_s":780,"x_m":23.542783737182617,"y_m":-4129.38330078125},{"converged":true,"course_rad":3.217939615249634,"iterations":5,"range_m":6161.1044921875,"speed_mps":9.969743728637695,"timestamp_s":790,"x_m":-0.10011020302772522,"y_m":-4245.95458984375}],"scenario_index":90000},"gridStepM":1000,"index":90000,"legs":{"leg1":[0,38],"leg2":[38,80],"turn":[38,38]},"seed":"3341046427358573995","series":{"bearingObsDeg":[9.481605426561877,8.640569229338713,7.463016441898187,7.2115100936381005,6.713795062592918,5.749460290472681,5.644043150822704,3.120291881040989,2.4757013158574983,2.021180401123476,359.8770119713391,359.83715098734285,358.96026398093994,356.7297974055806,355.8480473044886,353.7802485144023,352.2193863645143,350.7233290494714,347.8429016726936,346.99054870578937,345.07052788858545,342.95390690631183,339.7387182979334,336.29184967290814,335.1592403839799,332.75299206014114,328.43541091972264,326.4675514575719,324.31172518946244,320.630389830609,317.1165307102042,313.4500032014713,309.61252971819135,306.0973045599526,302.81829466396954,297.5397605488436,296.3598043885889,291.2702206266622,289.2631558786756,283.88935428500633,278.98943121595255,274.1175119224958,267.509686391102,264.5220523651117,259.6755960168808,257.1249574939879,252.78527386141522,249.88580391723144,246.17384199013964,243.57020119928745,240.0490747576058,238.80641746077038,236.0519660930443,232.88775801663084,232.79861038758384,230.41858999015787,228.26149330686277,227.11109820533588,226.27383995649737,224.38303868856275,223.5363411182913,223.044471875402,221.52390784161707,221.30332005218233,219.56623902170801,218.8507084042575,218.14856495758016,218.03735581751403,216.75725542364856,215.24678640945692,216.128741416224,214.6331348936663,214.52736258417949,213.65460101203527,213.55804945792795,213.42796167499588,212.67638131910604,212.15783335731865,212.09332905079705,212.6580081102387],"bearingRateDegMin":[null,null,-6.055766953991073,-4.792703271727116,-4.17880791818312,-4.233009347818879,-3.86659844662604,-4.653586335467712,-4.968444487302707,-5.006482146478783,-5.38443579840335,-5.404590986266516,-5.412049541089894,-5.644473487295053,-5.78669973030982,-6.027897282846359,-6.258861791773029,-6.46861727344937,-6.960957745290768,-7.373993453521482,-7.859560528976588,-8.320212435446974,-8.877456202955365,-9.576922718243305,-10.043213148887274,-10.68218401357104,-11.47540565524428,-12.107691863135724,-12.778869642771198,-13.401808947067252,-14.012258690094376,-14.752877882278256,-15.465204274018099,-16.21824874333754,-16.909582136314466,-17.720779216857135,-18.33643597468681,-18.950553635028715,-19.31179104821516,-19.796593054192662,-20.44398435113298,-21.257410061078136,-22.129683665899492,-22.76176772650424,-23.566990611863275,-24.02069990505116,-24.326112807654816,-24.484054863281667,-24.570031000775362,-24.48288353870425,-24.352463888933688,-23.904939236881816,-23.29979902493308,-22.82190976248633,-21.691578838579606,-20.62341540836228,-19.264859486223923,-17.904171191714344,-16.50494058378641,-15.225084004413624,-14.195837934782208,-13.025292379561392,-12.071323283853346,-10.968182349165904,-10.114831455675569,-9.266532449223849,-8.544509134303468,-7.79344047165675,-7.275173160862151,-6.7736539814187395,-6.211590398069,-5.932491613367533,-5.448951268091702,-5.111013639043341,-4.829411833191174,-4.514758086416324,-4.208268424578808,-4.010420386062409,-3.7675780217474775,-3.381453598419544],"bearingTrueDeg":[9.8201220462513,9.117198763375592,8.378364389874415,7.601061584591331,6.782515941712564,5.919716700712148,5.009400934072264,4.04803176473348,3.0317809867838994,1.956508642348718,0.8177448349029462,359.6106797062583,358.3301536199402,356.9706510112713,355.5263172452405,353.99097291120273,352.3581614949902,350.62121545817644,348.77336322529,346.8078615088475,344.7181966179245,342.4983252065539,340.1430007652611,337.6481488341984,335.0113122731081,332.2321468039765,329.312946682377,326.25914027616705,323.0797041089374,319.7874369757881,316.3990164426858,312.93476682726407,309.4181227142676,305.874780490072,302.3316514067584,298.81562147343453,295.35236549555475,291.9652775005164,288.6746274135805,283.623127734303,278.6081989689306,273.7043209847432,268.97638436075255,264.47536022190974,260.2364693930139,256.27963150326445,252.61155535161004,249.22858119858032,246.11964822288166,243.26895014635565,240.65804886754887,238.2674781229684,236.07781276764644,234.0704060071119,232.22776641954073,230.53378060054013,228.97379708887837,227.5345755993521,226.20423455638593,224.97215501928588,223.82886383058894,222.76591060091053,221.77578088889285,220.8517703354934,219.9879041163666,219.1788543053746,218.4198598127813,217.7066559155956,217.03541978220193,216.40272693652244,215.80548974749627,215.2409246919928,214.70653064047812,214.20003449595782,213.7193783770446,213.26270085501721,212.8283019407723,212.41463684779507,212.02029918507125,211.6439969083724],"ownCourseDeg":[110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,110.51530212950104,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884,37.947246953026884],"ownSpeedKt":[10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162,10.29331019836162],"rangeKm":[3.5599645837581595,3.473180478201001,3.3869458605275793,3.301303800502514,3.2163013968436314,3.131991223278276,3.0484304689040185,2.965682471770457,2.883817199603971,2.8029117381987168,2.7230521790344735,2.6443330028473304,2.5668591301466175,2.490746770679202,2.416124593154141,2.3431347168326058,2.271935030802346,2.2026988162304315,2.1356170619435915,2.0708990638381657,2.0087734783888282,1.949488128337514,1.8933096223845747,1.8405227936642918,1.7914272893952603,1.746334413603304,1.705561971848292,1.6694262006369809,1.6382341054494356,1.6122726511376333,1.591797768192813,1.577023171646332,1.5681099883931089,1.5651584283796969,1.5682020416426947,1.5772061702813411,1.5920696262683065,1.6126305143527229,1.6386743304674405,1.6382922678638903,1.6505932766411584,1.6752981840881094,1.7118701399624816,1.7595691165015435,1.8175194609308878,1.8847755531964077,1.9603802788662559,2.0434069366466696,2.1329887363340125,2.228335735915829,2.328739740832604,2.433574774517792,2.542292685399047,2.654416647560078,2.769532917702912,2.8872834693363205,3.00735912540326,3.1294921358626735,3.2534506660545937,3.379034287205067,3.506067922402276,3.634399503708482,3.763896576410701,3.8944427610735164,4.025935811099784,4.158286394782159,4.291414686591313,4.4252506544572965,4.559731865990032,4.694803110233733,4.830414776114155,4.96652265346471,5.103086928887433,5.240071970067935,5.377445485814873,5.515178710051027,5.653245429444464,5.791621319436677,5.930285082342726,6.069217119378155],"t":[0,10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170,180,190,200,210,220,230,240,250,260,270,280,290,300,310,320,330,340,350,360,370,380,390,400,410,420,430,440,450,460,470,480,490,500,510,520,530,540,550,560,570,580,590,600,610,620,630,640,650,660,670,680,690,700,710,720,730,740,750,760,770,780,790],"tgtCourseDeg":[184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905,184.263518371905],"tgtSpeedKt":[18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916,18.919473281431916]}} +{"bounds":{"maxX":4762.8356420898435,"maxY":10479.3718359375,"minX":-6492.785959472656,"minY":-776.2497656249998},"classical":{"estimator":"classical","mode":"per_timestep","record":"prediction","rows":[{"converged":true,"course_rad":5.955951690673828,"iterations":10,"range_m":3824.251708984375,"speed_mps":5.918530464172363,"timestamp_s":510,"x_m":-1921.3658447265625,"y_m":5758.30908203125},{"converged":true,"course_rad":5.955951690673828,"iterations":1,"range_m":3883.76513671875,"speed_mps":5.918530464172363,"timestamp_s":520,"x_m":-1940.3895263671875,"y_m":5814.353515625},{"converged":true,"course_rad":5.9229607582092285,"iterations":7,"range_m":4661.3173828125,"speed_mps":5.7971882820129395,"timestamp_s":530,"x_m":-2277.27783203125,"y_m":6515.1845703125},{"converged":true,"course_rad":5.90966272354126,"iterations":7,"range_m":4994.9755859375,"speed_mps":5.741173267364502,"timestamp_s":540,"x_m":-2420.53662109375,"y_m":6815.560546875},{"converged":true,"course_rad":5.906420707702637,"iterations":6,"range_m":5120.12255859375,"speed_mps":5.731146335601807,"timestamp_s":550,"x_m":-2471.346435546875,"y_m":6928.39404296875},{"converged":true,"course_rad":5.8858113288879395,"iterations":7,"range_m":5601.65234375,"speed_mps":5.699608325958252,"timestamp_s":560,"x_m":-2683.60595703125,"y_m":7359.3642578125},{"converged":true,"course_rad":5.8858113288879395,"iterations":1,"range_m":5660.65625,"speed_mps":5.699608325958252,"timestamp_s":570,"x_m":-2705.663330078125,"y_m":7411.91943359375},{"converged":true,"course_rad":5.88231897354126,"iterations":6,"range_m":5799.3212890625,"speed_mps":5.71899938583374,"timestamp_s":580,"x_m":-2763.67529296875,"y_m":7535.4521484375},{"converged":true,"course_rad":5.878798007965088,"iterations":6,"range_m":5949.83203125,"speed_mps":5.764894008636475,"timestamp_s":590,"x_m":-2826.863525390625,"y_m":7669.44482421875},{"converged":true,"course_rad":5.876940727233887,"iterations":6,"range_m":6070.82470703125,"speed_mps":5.818641185760498,"timestamp_s":600,"x_m":-2876.426513671875,"y_m":7776.931640625},{"converged":true,"course_rad":5.875898361206055,"iterations":5,"range_m":6188.7607421875,"speed_mps":5.901246547698975,"timestamp_s":610,"x_m":-2923.92626953125,"y_m":7881.861328125},{"converged":true,"course_rad":5.873448371887207,"iterations":7,"range_m":6143.76513671875,"speed_mps":5.540668964385986,"timestamp_s":620,"x_m":-2905.90234375,"y_m":7835.1416015625},{"converged":true,"course_rad":5.861954689025879,"iterations":9,"range_m":6194.0302734375,"speed_mps":5.058913230895996,"timestamp_s":630,"x_m":-2936.544677734375,"y_m":7869.7236328125},{"converged":true,"course_rad":5.878547668457031,"iterations":7,"range_m":6221.82666015625,"speed_mps":5.676295280456543,"timestamp_s":640,"x_m":-2926.890380859375,"y_m":7903.40234375},{"converged":true,"course_rad":5.881758689880371,"iterations":6,"range_m":6276.5439453125,"speed_mps":5.8094801902771,"timestamp_s":650,"x_m":-2942.220703125,"y_m":7952.70263671875},{"converged":true,"course_rad":5.884981155395508,"iterations":6,"range_m":6328.75,"speed_mps":5.937793731689453,"timestamp_s":660,"x_m":-2955.872802734375,"y_m":7999.87109375},{"converged":true,"course_rad":5.900796413421631,"iterations":9,"range_m":6347.31396484375,"speed_mps":6.6693315505981445,"timestamp_s":670,"x_m":-2932.103271484375,"y_m":8031.55517578125},{"converged":true,"course_rad":5.909267902374268,"iterations":11,"range_m":6419.1572265625,"speed_mps":7.203047275543213,"timestamp_s":680,"x_m":-2938.093505859375,"y_m":8107.40478515625},{"converged":true,"course_rad":5.905756950378418,"iterations":13,"range_m":6499.798828125,"speed_mps":6.990785121917725,"timestamp_s":690,"x_m":-2974.768310546875,"y_m":8172.8681640625},{"converged":true,"course_rad":5.908520698547363,"iterations":9,"range_m":6563.31591796875,"speed_mps":7.134658336639404,"timestamp_s":700,"x_m":-2989.43408203125,"y_m":8232.4560546875},{"converged":true,"course_rad":5.917383670806885,"iterations":12,"range_m":6612.37060546875,"speed_mps":7.650908470153809,"timestamp_s":710,"x_m":-2980.325439453125,"y_m":8290.876953125},{"converged":true,"course_rad":5.900199890136719,"iterations":7,"range_m":6787.98486328125,"speed_mps":6.831683158874512,"timestamp_s":720,"x_m":-3091.483154296875,"y_m":8419.333984375},{"converged":true,"course_rad":5.895223140716553,"iterations":6,"range_m":6897.310546875,"speed_mps":6.654847621917725,"timestamp_s":730,"x_m":-3143.218505859375,"y_m":8508.1787109375},{"converged":true,"course_rad":5.887572288513184,"iterations":7,"range_m":7025.853515625,"speed_mps":6.414228916168213,"timestamp_s":740,"x_m":-3208.541015625,"y_m":8610.623046875},{"converged":true,"course_rad":5.876765251159668,"iterations":10,"range_m":7173.8515625,"speed_mps":6.116390705108643,"timestamp_s":750,"x_m":-3287.81298828125,"y_m":8726.642578125},{"converged":true,"course_rad":5.895054817199707,"iterations":7,"range_m":7104.9736328125,"speed_mps":6.6288042068481445,"timestamp_s":760,"x_m":-3215.71875,"y_m":8684.6640625},{"converged":true,"course_rad":5.900163173675537,"iterations":6,"range_m":7136.216796875,"speed_mps":6.786989688873291,"timestamp_s":770,"x_m":-3211.208984375,"y_m":8717.1201171875},{"converged":true,"course_rad":5.8832292556762695,"iterations":7,"range_m":7342.85400390625,"speed_mps":6.305893898010254,"timestamp_s":780,"x_m":-3332.44189453125,"y_m":8874.2529296875},{"converged":true,"course_rad":5.874904632568359,"iterations":7,"range_m":7473.470703125,"speed_mps":6.101073265075684,"timestamp_s":790,"x_m":-3400.742431640625,"y_m":8975.75390625},{"converged":true,"course_rad":5.900712013244629,"iterations":7,"range_m":7341.64013671875,"speed_mps":6.7794108390808105,"timestamp_s":800,"x_m":-3279.318603515625,"y_m":8891.916015625},{"converged":true,"course_rad":5.903668403625488,"iterations":6,"range_m":7390.7451171875,"speed_mps":6.86683464050293,"timestamp_s":810,"x_m":-3286.009765625,"y_m":8937.7392578125},{"converged":true,"course_rad":5.904238700866699,"iterations":5,"range_m":7460.76513671875,"speed_mps":6.883651256561279,"timestamp_s":820,"x_m":-3307.7724609375,"y_m":8998.2119140625},{"converged":true,"course_rad":5.902276992797852,"iterations":6,"range_m":7552.68798828125,"speed_mps":6.827392101287842,"timestamp_s":830,"x_m":-3345.9658203125,"y_m":9073.48046875},{"converged":true,"course_rad":5.902276992797852,"iterations":1,"range_m":7627.47119140625,"speed_mps":6.827392101287842,"timestamp_s":840,"x_m":-3371.34765625,"y_m":9136.8603515625},{"converged":true,"course_rad":5.896102428436279,"iterations":6,"range_m":7754.07958984375,"speed_mps":6.663031101226807,"timestamp_s":850,"x_m":-3436.448974609375,"y_m":9234.6748046875},{"converged":true,"course_rad":5.920920372009277,"iterations":8,"range_m":7610.21337890625,"speed_mps":7.377430438995361,"timestamp_s":860,"x_m":-3290.4658203125,"y_m":9154.9619140625},{"converged":true,"course_rad":5.892937660217285,"iterations":8,"range_m":7933.93798828125,"speed_mps":6.597395420074463,"timestamp_s":870,"x_m":-3509.617431640625,"y_m":9381.453125},{"converged":true,"course_rad":5.877099990844727,"iterations":7,"range_m":8130.20947265625,"speed_mps":6.241074085235596,"timestamp_s":880,"x_m":-3630.47802734375,"y_m":9522.9462890625},{"converged":true,"course_rad":5.88240909576416,"iterations":6,"range_m":8160.515625,"speed_mps":6.353847503662109,"timestamp_s":890,"x_m":-3623.810791015625,"y_m":9554.2744140625},{"converged":true,"course_rad":5.9005842208862305,"iterations":6,"range_m":8086.42529296875,"speed_mps":6.773159980773926,"timestamp_s":900,"x_m":-3532.58154296875,"y_m":9520.1064453125},{"converged":true,"course_rad":5.898332118988037,"iterations":6,"range_m":8180.2451171875,"speed_mps":6.7184834480285645,"timestamp_s":910,"x_m":-3573.076416015625,"y_m":9594.6904296875},{"converged":true,"course_rad":5.884181499481201,"iterations":7,"range_m":8368.3408203125,"speed_mps":6.402552127838135,"timestamp_s":920,"x_m":-3690.13525390625,"y_m":9727.8076171875},{"converged":true,"course_rad":5.868750095367432,"iterations":7,"range_m":8555.7392578125,"speed_mps":6.100251197814941,"timestamp_s":930,"x_m":-3807.9345703125,"y_m":9858.9482421875},{"converged":true,"course_rad":5.864589691162109,"iterations":6,"range_m":8655.1669921875,"speed_mps":6.025496959686279,"timestamp_s":940,"x_m":-3856.470458984375,"y_m":9933.3115234375},{"converged":true,"course_rad":5.872250080108643,"iterations":6,"range_m":8669.21484375,"speed_mps":6.163041591644287,"timestamp_s":950,"x_m":-3836.032470703125,"y_m":9954.3515625}],"scenario_index":90000},"gridStepM":1000,"index":90000,"legs":{"leg1":[0,42],"leg2":[50,96],"turn":[42,50]},"seed":"8605880571600194331","series":{"bearingObsDeg":[340.9190023072683,342.12912058289777,342.22354111798404,341.7308112712593,341.13625696438805,null,341.189887609751,339.8091238878979,341.45178438328594,338.7580124161463,339.8981622339182,340.93667883684026,339.7416142981415,342.30214293495254,null,339.75972795982034,339.2689925283333,339.44586710707995,338.2301753970877,339.71265429606063,335.5156123341145,337.6650728659175,339.7578428276094,null,339.89368162982265,339.03346028499453,337.40148220546837,339.8600770991062,337.4568340585022,336.20040710029997,337.68515362207734,337.2837297441774,336.90358873573115,337.5352719509306,338.01975092913796,337.04918104807916,336.5044324806356,336.7140919673983,335.8605642079568,335.7957047315984,337.28796446146276,335.5800346783661,335.2445631070917,336.017412672057,null,334.74587733541074,335.1418097412181,334.7150048803623,333.1376683141962,331.726769797725,330.66306345714395,332.1204072599712,null,331.7231088163299,330.78163554113536,330.0567885456569,330.66246240049696,null,329.15370093359786,328.90590167050976,328.52450390725625,328.25391913309704,326.51211161171705,326.14371852864326,327.5473770445942,326.5666165212938,326.2418820073947,327.358426691395,327.28974230910137,324.8392616801975,325.58091094103423,326.7680524602958,322.2929944783735,323.88751580049126,323.4731965254383,323.07800178006136,324.2297082779089,323.4963372063463,321.87086146691,322.10879793683654,323.7615944329529,322.43161999776805,322.0396217809227,321.5973260510293,null,320.8331644866883,323.49338656462487,318.3256654385932,319.4385491411984,320.5636452220396,321.62907277029154,319.81240637560825,318.468033901653,318.1890343344363,318.77563830111364,319.47794567233103],"bearingRateDegMin":[null,null,3.9136164321472657,1.5179084562356178,0.021720001560640547,null,-0.36599900419558723,-1.2032631414207786,-0.7442842347070425,-1.385857492336386,-1.3469262195933087,-1.0353004104064254,-1.049099690026525,-0.5921988019882637,null,-0.6490210883661399,-0.73547250817493,-0.7561762962527683,-1.000334922463191,-0.8821990613702448,-1.193978491516006,-1.2345685637112471,-1.0579496964382855,null,-0.7738081608598408,-0.7729457528706977,-0.7488383306423879,-0.7278077901837801,-0.8572241789658084,-0.9594042646416981,-0.9885712616810288,-0.6634472108225203,-0.7145518493511642,-0.6232240609133679,-0.5108588838959055,-0.461087011776823,-0.5890656668082356,-0.4837677860919571,-0.989922668421075,-1.2449152756850759,-1.0121765810516947,-1.0481783389980601,-0.9676414850308862,-0.8493803332424241,null,-0.7949457261467583,-0.833719289924054,-1.0425520193366238,-1.2255978998434547,-1.5458480035148698,-1.9451598480228205,-2.0276928593060606,null,-2.053677402608077,-2.2019180554205304,-2.3271838018635065,-2.399490851482942,null,-2.453815664714124,-2.5108082678765347,-2.56817905336623,-2.4470782149568984,-2.567144447462451,-2.6390644093232702,-2.4000970096711365,-2.21557283892917,-2.152407227360931,-2.0601630145679817,-2.0666629168952046,-2.095191656794253,-2.0443641403353094,-1.7427804344101698,-1.9806281883475656,-2.030105040997952,-1.9522464532597317,-1.9970236509393873,-1.8670565276339626,-1.7824609764886594,-1.8468469494542372,-1.8356224322624226,-1.7792372963691854,-1.8721276999125869,-1.79517451721173,-1.8119548752139856,null,-1.725353482211119,-1.197697351874428,-1.554320277814022,-1.5906473762573188,-1.2802639707611712,-1.3079531788995502,-1.3389457636562323,-1.5096527368900154,-1.692847371319339,-1.6174426969518838,-1.4868424919399568],"bearingTrueDeg":[341.75469052152596,341.60218714579344,341.4506021156785,341.2999272682493,341.1501531129358,341.0012804114668,340.85329968169293,340.70620294337124,340.5599881306007,340.4146472592306,340.2701721606419,340.12656131508544,339.9838043892536,339.8419000850187,339.70084002936636,339.56061663044767,339.4212278901293,339.2826647867793,339.14492254124417,339.00799777649337,338.8718829517245,338.7365705320767,338.60205868478033,338.4683399423171,338.33540657254235,338.2032579620803,338.07188535516326,337.941281811043,337.81144596091457,337.6823703888279,337.55404673003,337.42647700489727,337.2996466737021,337.17356009590947,337.0482049994831,336.9235791382302,336.79967609240435,336.67649136340026,336.5540198715033,336.4322564971571,336.3111984996564,336.1908346462781,336.03130579339046,335.79636121925336,335.4929111488408,335.1297652024502,334.71745667780175,334.26794507815237,333.7942529728642,333.30998158282614,332.8286948067966,332.35807718120753,331.89828836856213,331.44902524917427,331.0099784027788,330.58085265298723,330.1613636042626,329.7512356587423,329.35019424309564,328.9579852658946,328.57435030463193,328.19904613053984,327.83183760279564,327.47249198275154,327.12078737634835,326.7765108228102,326.43945183244017,326.10940441341376,325.7861806686349,325.4695849557767,325.1594391721147,324.85556213426446,324.5577847916195,324.2659396426371,323.9798611842496,323.69940206502065,323.4244076250641,323.15473555222934,322.89023823401544,322.63078442747417,322.3762323526993,322.1264682115481,321.881357727229,321.6407855154438,321.40463063628636,321.17278042306936,320.9451295638,320.7215721698925,320.5020013414562,320.28632080665136,320.07443317312254,319.8662395240466,319.66165951890383,319.46059986131166,319.2629756637877,319.06870338121496],"ownCourseDeg":[350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,350.82747577393576,359.21005765926566,7.592641479513982,15.975225072391202,24.35780695772113,32.74038884305106,41.122974143475574,49.5055560288055,57.88814132923002,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332,62.44005259969332],"ownSpeedKt":[9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616,9.679694000639616],"rangeKm":[4.405633437762,4.4189337869560426,4.432265717305135,4.445628478202873,4.45902139051688,4.472444998051644,4.485898619400754,4.499381537614519,4.512894396298335,4.526436468927309,4.540007005738004,4.553606724996459,4.567234394137835,4.580890763370796,4.594575004893951,4.608286532150585,4.622025897189684,4.635792498532552,4.649585609291002,4.663405832092306,4.677252530529874,4.691125072000521,4.705024021070536,4.718948749397331,4.73289857565488,4.74687429590543,4.760875006113744,4.7749001406372376,4.7889503977754,4.803025102426369,4.817123748438477,4.831246661075869,4.845393027990596,4.859563360548611,4.873757173169033,4.887973758945384,4.902213757248076,4.916476547773267,4.930761399769801,4.945069183349639,4.959399085867743,4.9737507515989465,4.989223396897235,5.007620103216135,5.029960653410442,5.057194856988651,5.090169197928788,5.129601077970814,5.176053768246946,5.229916409263074,5.290629386114576,5.352781018672698,5.415285167764779,5.478130389496233,5.541305105665405,5.6047970945255825,5.668596978065229,5.732693824505775,5.7970778138902554,5.861739647230158,5.926670288086359,5.991860326111514,6.057302662886812,6.122988274727427,6.188909073964555,6.255058880699409,6.32142949160552,6.388014016033335,6.454806433545071,6.521799963182306,6.588988663789834,6.656366634726137,6.7239279793204165,6.791667631747759,6.859578918542683,6.927658755905419,6.995901386129211,7.064302068898973,7.132856231074529,7.201559281120986,7.270406661673742,7.3393956668872775,7.408521499920376,7.477780182028117,7.54716850291565,7.616681976001789,7.686318634453006,7.7560742232084445,7.825946018451594,7.895930898411743,7.966025354682429,8.036226186732643,8.106531952073462,8.176939634995449,8.24744594579269,8.318048791814881],"t":[0,10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170,180,190,200,210,220,230,240,250,260,270,280,290,300,310,320,330,340,350,360,370,380,390,400,410,420,430,440,450,460,470,480,490,500,510,520,530,540,550,560,570,580,590,600,610,620,630,640,650,660,670,680,690,700,710,720,730,740,750,760,770,780,790,800,810,820,830,840,850,860,870,880,890,900,910,920,930,940,950],"tgtCourseDeg":[338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531,338.1731842986531],"tgtSpeedKt":[12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568,12.164745474994568]}} diff --git a/packages/demos/test/helpers.ts b/packages/demos/test/helpers.ts new file mode 100644 index 00000000..63b340ed --- /dev/null +++ b/packages/demos/test/helpers.ts @@ -0,0 +1,26 @@ +// Test fixtures: scenario sources built from the committed ranges files, as the site's prebuild does. +import { readFileSync } from "node:fs"; +import type { ParameterRanges } from "@tma/types"; +import type { ScenarioSource } from "../src/index.js"; + +const ROOT = new URL("../../../", import.meta.url); +export const repoPath = (p: string): string => new URL(p, ROOT).pathname; +export const ranges = (file: string): ParameterRanges => + JSON.parse(readFileSync(repoPath(`configs/${file}`), "utf8")) as ParameterRanges; + +export const I2_TEST: ScenarioSource = { + id: "i2-test", + label: "Iteration 2 test", + seed: "201", + start: 90000, + end: 100000, + ranges: ranges("iter2-ranges.json"), +}; +export const I1_TEST: ScenarioSource = { + id: "i1-test", + label: "Iteration 1 test", + seed: "101", + start: 90000, + end: 100000, + ranges: ranges("iter1-ranges.json"), +}; diff --git a/packages/demos/test/render.test.tsx b/packages/demos/test/render.test.tsx new file mode 100644 index 00000000..73232954 --- /dev/null +++ b/packages/demos/test/render.test.tsx @@ -0,0 +1,80 @@ +// T005: the viewer renders on the server (as Docusaurus pre-renders pages) in every mode, from a +// URL, without a browser or tfjs. Inference runs in an effect, so the model status reads "Running". +import { readFileSync } from "node:fs"; +import { renderToString } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import type { ModelEntry, ModelPair, ViewerCatalogue } from "../src/index.js"; +import { EngagementViewer } from "../src/viewer/index.js"; +import { I1_TEST, I2_TEST, repoPath } from "./helpers.js"; + +const NORM = ( + JSON.parse( + readFileSync( + repoPath("experiments/20261016-i2d-val/models/lstm-instantaneous/manifest.json"), + "utf8", + ), + ) as { normalisation: ModelEntry["normalisation"] } +).normalisation; +const entry = (id: string, variant: ModelEntry["variant"]): ModelEntry => ({ + id, + variant, + path: `x/${id}`, + featureSet: "raw", + normalisation: NORM, + parameterCount: 1234, + architecture: "LSTM 2 × 128", + trainedOnSeed: "201", + bestEpoch: 7, +}); +const pair: ModelPair = { + id: "2c", + label: "Iteration 2c", + experiment: "experiments/x", + gate: "G2", + instantaneous: entry("lstm-instantaneous", "instantaneous"), + timeLate: entry("bilstm-time-late", "time_late"), + naive: { log_range_km: 2, course_sin: 0, course_cos: 1, speed_mps: 5.9, n: 1 }, +}; +const cat: ViewerCatalogue = { + generatorVersion: "0.2.0", + sources: [I2_TEST, I1_TEST], + pairs: [pair], + bookmarks: [{ label: "typical", source: "i2-test", index: 90003 }], +}; +const render = (search: string): string => + renderToString( + , + ).replaceAll("", ""); + +describe("server render", () => { + it("truth mode: plan, plots and metadata, no estimates", () => { + const html = render("src=i2-test&i=90001"); + expect(html).toContain('aria-label="Plan view"'); + for (const t of ["Course", "Speed", "Bearing", "Bearing rate", "Range"]) + expect(html).toContain(`aria-label="${t}"`); + expect(html).toContain("index 90001"); + expect(html).toContain("generator"); + expect(html).not.toContain("At the cursor"); + expect(html).toContain("typical"); + }); + it("instantaneous mode: model status, readout with classical and naive", () => { + const html = render("src=i2-test&i=90002&mode=instantaneous&t=1200"); + expect(html).toContain("Running lstm-instantaneous"); + expect(html).toContain("At the cursor"); + expect(html).toContain("classical (error)"); + expect(html).toContain("naive (error)"); + expect(html).toContain("post-zig medians"); + expect(html).toContain("unobservable"); + }); + it("time-late mode on another dataset notes the seed difference", () => { + const html = render("src=i1-test&i=90005&mode=time-late&cl=0"); + expect(html).toContain("Running bilstm-time-late"); + expect(html).not.toContain("classical (error)"); + expect(html).toContain("different dataset (seed 101)"); + }); + it("falls back to defaults on a bad URL", () => { + const html = render("src=nope&i=-4&mode=zz"); + expect(html).toContain("index 90000"); + expect(html).not.toContain("At the cursor"); + }); +}); diff --git a/packages/demos/tsconfig.json b/packages/demos/tsconfig.json new file mode 100644 index 00000000..1b35d6e5 --- /dev/null +++ b/packages/demos/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "types": [] + }, + "include": ["src"], + "references": [ + { "path": "../types" }, + { "path": "../sim" }, + { "path": "../data" }, + { "path": "../features" }, + { "path": "../classical" }, + { "path": "../eval" } + ] +} diff --git a/packages/eval/package.json b/packages/eval/package.json index 7d6435b3..b4a4ab4e 100644 --- a/packages/eval/package.json +++ b/packages/eval/package.json @@ -7,6 +7,10 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" + }, + "./browser": { + "types": "./dist/browser.d.ts", + "default": "./dist/browser.js" } }, "bin": { diff --git a/packages/eval/src/browser.ts b/packages/eval/src/browser.ts new file mode 100644 index 00000000..fc40ae5a --- /dev/null +++ b/packages/eval/src/browser.ts @@ -0,0 +1,3 @@ +// Browser-safe entry point (spec 006 FR-009): per-row errors and order statistics, no file I/O. +export * from "./errors.js"; +export * from "./stats.js"; diff --git a/packages/features/package.json b/packages/features/package.json index 72db665e..5466e7b7 100644 --- a/packages/features/package.json +++ b/packages/features/package.json @@ -7,6 +7,10 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" + }, + "./browser": { + "types": "./dist/browser.d.ts", + "default": "./dist/browser.js" } }, "bin": { diff --git a/packages/features/src/browser.ts b/packages/features/src/browser.ts new file mode 100644 index 00000000..1b904652 --- /dev/null +++ b/packages/features/src/browser.ts @@ -0,0 +1,5 @@ +// Browser-safe entry point (spec 006 FR-009): the pure modules only, no file I/O. +export * from "./decode.js"; +export * from "./derived.js"; +export * from "./layout.js"; +export * from "./normalise.js"; diff --git a/packages/features/src/decode.ts b/packages/features/src/decode.ts new file mode 100644 index 00000000..eb93e124 --- /dev/null +++ b/packages/features/src/decode.ts @@ -0,0 +1,82 @@ +// Model outputs → Prediction rows (spec 002 research R7): the inverse of the target encoding in +// layout.ts. Pure, so the CLI (`@tma/models`) and the browser viewer (`@tma/demos`) share it. +import { bearingOf, f32, f32Angle } from "@tma/sim"; +import type { NormalisationConstants, Prediction, PredictionRow, Scenario } from "@tma/types"; +import { ROWS } from "./layout.js"; + +/** Model outputs per row: log_range_km, course_sin, course_cos, speed_scaled. */ +export const NO = 4; + +/** Last valid bearing at or before sample i (the derived position uses it, as naive does). */ +function lastBearing(s: Scenario, i: number): number | null { + for (let k = i; k >= 0; k--) { + const o = s.observations[k]; + if (o?.valid === true) return o.bearing_rad; + } + return null; +} + +/** + * One scenario's per-timestep prediction from the model's [ROWS × NO] output (row-major), rows from + * `first` (default zig_index + 1, as scored) to the last sample. Uses only ownship and bearings + * from `s`, never target truth. Non-finite outputs or no bearing yet → `converged = false` + * (scored as gross). The viewer passes `first` = 0 to show the pre-zig (unobservable) rows too. + */ +export function outputsToPrediction( + s: Scenario, + out: Float32Array, + norm: NormalisationConstants, + estimator: string, + first: number = s.metadata.zig_index + 1, +): Prediction { + const n = s.metadata.samples; + if (out.length !== ROWS * NO) throw new Error(`expected ${String(ROWS * NO)} outputs`); + const rows: PredictionRow[] = []; + for (let i = first; i < n; i++) { + const own = s.ownship.states[i]; + if (own === undefined) throw new Error(`no sample ${String(i)}`); + const a = i * NO; + const logR = out[a] ?? NaN; + const sin = out[a + 1] ?? NaN; + const cos = out[a + 2] ?? NaN; + const spd = out[a + 3] ?? NaN; + const range = 1000 * Math.exp(logR); + const speed = spd * norm.speed_scale_mps; + const b = lastBearing(s, i); + const ok = + b !== null && + Number.isFinite(range) && + Number.isFinite(speed) && + Number.isFinite(sin) && + Number.isFinite(cos) && + (sin !== 0 || cos !== 0); + rows.push( + ok + ? { + timestamp_s: own.timestamp_s, + range_m: f32(range), + course_rad: f32Angle(bearingOf(sin, cos)), + speed_mps: f32(speed), + x_m: f32(own.x_m + range * Math.sin(b)), + y_m: f32(own.y_m + range * Math.cos(b)), + converged: true, + } + : { + timestamp_s: own.timestamp_s, + range_m: 0, + course_rad: 0, + speed_mps: 0, + x_m: 0, + y_m: 0, + converged: false, + }, + ); + } + return { + record: "prediction", + scenario_index: s.metadata.index, + estimator, + mode: "per_timestep", + rows, + }; +} diff --git a/packages/features/src/index.ts b/packages/features/src/index.ts index 4adade87..ad48b1e6 100644 --- a/packages/features/src/index.ts +++ b/packages/features/src/index.ts @@ -1,5 +1,6 @@ export * from "./derived.js"; export * from "./layout.js"; +export * from "./decode.js"; export * from "./normalise.js"; export * from "./features-file.js"; export * from "./reader.js"; diff --git a/packages/models/src/architectures.ts b/packages/models/src/architectures.ts index 13ba52b5..36288437 100644 --- a/packages/models/src/architectures.ts +++ b/packages/models/src/architectures.ts @@ -1,12 +1,12 @@ // The four SRD §4 models, built only from layers with direct Keras equivalents (constitution III). // Every initializer is seeded from the run's train seed, so training is reproducible (research R2). import * as tf from "@tensorflow/tfjs-node"; -import { inputWidth, ROWS } from "@tma/features"; +import { inputWidth, NO, ROWS } from "@tma/features"; import type { TrainingConfig } from "@tma/types"; import { initSeed } from "./seeds.js"; -/** Model outputs per row: log_range_km, course_sin, course_cos, speed_scaled. */ -export const NO = 4; +/** Model outputs per row (defined with the decoding in `@tma/features`). */ +export { NO }; /** Keras layer classes the models may use (checked by tests). */ export const ALLOWED_LAYERS = new Set([ diff --git a/packages/models/src/predict.ts b/packages/models/src/predict.ts index f34737f0..19202c82 100644 --- a/packages/models/src/predict.ts +++ b/packages/models/src/predict.ts @@ -3,89 +3,18 @@ import { createHash } from "node:crypto"; import { resolve } from "node:path"; import * as tf from "@tensorflow/tfjs-node"; import { canonicalJson } from "@tma/data"; -import { type FeatureFile, ROWS } from "@tma/features"; -import { bearingOf, f32, f32Angle } from "@tma/sim"; +import { type FeatureFile, outputsToPrediction, ROWS } from "@tma/features"; import type { DatasetHeader, FeatureSet, NormalisationConstants, Prediction, - PredictionRow, Scenario, } from "@tma/types"; import { modelInputWidth, NO } from "./architectures.js"; -/** Last valid bearing at or before sample i (the derived position uses it, as naive does). */ -function lastBearing(s: Scenario, i: number): number | null { - for (let k = i; k >= 0; k--) { - const o = s.observations[k]; - if (o?.valid === true) return o.bearing_rad; - } - return null; -} - -/** - * One scenario's per-timestep prediction from the model's [ROWS × NO] output (row-major), rows from - * zig_index + 1 to the last sample. Uses only ownship and bearings from `s`, never target truth. - * Non-finite outputs or no bearing yet → `converged = false` (scored as gross). - */ -export function outputsToPrediction( - s: Scenario, - out: Float32Array, - norm: NormalisationConstants, - estimator: string, -): Prediction { - const n = s.metadata.samples; - if (out.length !== ROWS * NO) throw new Error(`expected ${String(ROWS * NO)} outputs`); - const rows: PredictionRow[] = []; - for (let i = s.metadata.zig_index + 1; i < n; i++) { - const own = s.ownship.states[i]; - if (own === undefined) throw new Error(`no sample ${String(i)}`); - const a = i * NO; - const logR = out[a] ?? NaN; - const sin = out[a + 1] ?? NaN; - const cos = out[a + 2] ?? NaN; - const spd = out[a + 3] ?? NaN; - const range = 1000 * Math.exp(logR); - const speed = spd * norm.speed_scale_mps; - const b = lastBearing(s, i); - const ok = - b !== null && - Number.isFinite(range) && - Number.isFinite(speed) && - Number.isFinite(sin) && - Number.isFinite(cos) && - (sin !== 0 || cos !== 0); - rows.push( - ok - ? { - timestamp_s: own.timestamp_s, - range_m: f32(range), - course_rad: f32Angle(bearingOf(sin, cos)), - speed_mps: f32(speed), - x_m: f32(own.x_m + range * Math.sin(b)), - y_m: f32(own.y_m + range * Math.cos(b)), - converged: true, - } - : { - timestamp_s: own.timestamp_s, - range_m: 0, - course_rad: 0, - speed_mps: 0, - x_m: 0, - y_m: 0, - converged: false, - }, - ); - } - return { - record: "prediction", - scenario_index: s.metadata.index, - estimator, - mode: "per_timestep", - rows, - }; -} +/** Moved to `@tma/features` (decode.ts) so the browser viewer shares it; re-exported here. */ +export { outputsToPrediction }; export async function loadModel(dir: string): Promise { return tf.loadLayersModel(`file://${resolve(dir)}/model.json`); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67bbd5da..724a74dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,55 @@ importers: specifier: workspace:* version: link:../types + packages/demos: + dependencies: + '@tensorflow/tfjs-backend-cpu': + specifier: 4.22.0 + version: 4.22.0(@tensorflow/tfjs-core@4.22.0) + '@tensorflow/tfjs-core': + specifier: 4.22.0 + version: 4.22.0 + '@tensorflow/tfjs-layers': + specifier: 4.22.0 + version: 4.22.0(@tensorflow/tfjs-core@4.22.0) + '@tma/classical': + specifier: workspace:* + version: link:../classical + '@tma/data': + specifier: workspace:* + version: link:../data + '@tma/eval': + specifier: workspace:* + version: link:../eval + '@tma/features': + specifier: workspace:* + version: link:../features + '@tma/sim': + specifier: workspace:* + version: link:../sim + '@tma/types': + specifier: workspace:* + version: link:../types + devDependencies: + '@tma/models': + specifier: workspace:* + version: link:../models + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.3.0) + esbuild: + specifier: 0.28.2 + version: 0.28.2 + react: + specifier: 19.3.0 + version: 19.3.0 + react-dom: + specifier: 19.3.0 + version: 19.3.0(react@19.3.0) + packages/eval: dependencies: '@tma/classical': @@ -2332,6 +2381,11 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + '@types/react-router-config@5.0.11': resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} @@ -10031,6 +10085,10 @@ snapshots: '@types/range-parser@1.2.7': {} + '@types/react-dom@19.2.3(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 diff --git a/tsconfig.check.json b/tsconfig.check.json index ab13247d..9fd2c46f 100644 --- a/tsconfig.check.json +++ b/tsconfig.check.json @@ -13,8 +13,16 @@ "@tma/features": ["./packages/features/src/index.ts"], "@tma/classical": ["./packages/classical/src/index.ts"], "@tma/eval": ["./packages/eval/src/index.ts"], - "@tma/models": ["./packages/models/src/index.ts"] - } + "@tma/models": ["./packages/models/src/index.ts"], + "@tma/features/browser": ["./packages/features/src/browser.ts"], + "@tma/classical/browser": ["./packages/classical/src/browser.ts"], + "@tma/eval/browser": ["./packages/eval/src/browser.ts"], + "@tma/data/canonical": ["./packages/data/src/canonical.ts"], + "@tma/demos": ["./packages/demos/src/index.ts"], + "@tma/demos/viewer": ["./packages/demos/src/viewer/index.ts"] + }, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "jsx": "react-jsx" }, "include": [ "packages/*/src", diff --git a/tsconfig.json b/tsconfig.json index be935fe0..69d59828 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,9 @@ }, { "path": "packages/models" + }, + { + "path": "packages/demos" } ] } diff --git a/vitest.config.ts b/vitest.config.ts index 6fdce6e2..f70d5e2f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,23 +1,28 @@ import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; -const pkg = (name: string): string => - fileURLToPath(new URL(`./packages/${name}/src/index.ts`, import.meta.url)); +const src = (path: string): string => fileURLToPath(new URL(`./packages/${path}`, import.meta.url)); +const pkg = (name: string): string => src(`${name}/src/index.ts`); export default defineConfig({ resolve: { - alias: { - "@tma/types": pkg("types"), - "@tma/sim": pkg("sim"), - "@tma/data": pkg("data"), - "@tma/features": pkg("features"), - "@tma/classical": pkg("classical"), - "@tma/eval": pkg("eval"), - "@tma/models": pkg("models"), - }, + // Exact-match aliases, so subpath exports (`@tma/features/browser`) resolve to their own sources. + alias: [ + ...["types", "sim", "data", "features", "classical", "eval", "models", "demos"].map( + (name) => ({ + find: new RegExp(`^@tma/${name}$`), + replacement: pkg(name), + }), + ), + { find: /^@tma\/features\/browser$/, replacement: src("features/src/browser.ts") }, + { find: /^@tma\/classical\/browser$/, replacement: src("classical/src/browser.ts") }, + { find: /^@tma\/eval\/browser$/, replacement: src("eval/src/browser.ts") }, + { find: /^@tma\/data\/canonical$/, replacement: src("data/src/canonical.ts") }, + { find: /^@tma\/demos\/viewer$/, replacement: src("demos/src/viewer/index.ts") }, + ], }, test: { - include: ["packages/*/test/**/*.test.ts"], + include: ["packages/*/test/**/*.test.ts", "packages/*/test/**/*.test.tsx"], passWithNoTests: true, // CPU-heavy solver tests share 2–4 cores with tfjs training tests; 5 s is too tight on CI. testTimeout: 60_000, From 85afe736fc795802092bfe0da09be501c0a8932d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 07:37:59 +0000 Subject: [PATCH 03/21] Viewer: site page, catalogue from gate experiments, bookmarks, blog post, smoke script - /viewer page (BrowserOnly, lazy) with navbar link; prebuild writes the catalogue from the gate experiments' dataset headers and manifests and copies the carried models (4.5 MB) into site/static/viewer (ignored) - Featured bookmarks from the 2c predictions on seeds 203 and 201 held-out - Algorithm note and blog post with linked views (SC-003) - scripts/viewer-smoke.mjs: Playwright smoke of the served page - PNG export: Array.from, not a NodeList spread (Babel loose mode) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- .gitignore | 3 + AGENTS.md | 11 +- package.json | 4 +- .../demos/src/viewer/EngagementViewer.tsx | 25 +++- packages/demos/src/viewer/Panels.tsx | 14 +- packages/demos/src/viewer/PlanView.tsx | 4 +- packages/demos/src/viewer/TimePlot.tsx | 8 +- packages/demos/src/viewer/style.ts | 1 + pnpm-lock.yaml | 3 + scripts/viewer-smoke.mjs | 71 ++++++++++ site/blog/2026-10-17-engagement-viewer.md | 94 +++++++++++++ site/docs/algorithms/viewer.md | 56 ++++++++ site/docusaurus.config.js | 1 + site/package.json | 1 + site/scripts/prebuild.mjs | 127 +++++++++++++++++- site/sidebars.js | 1 + site/src/data/viewer-bookmarks.json | 33 +++++ site/src/pages/viewer.jsx | 49 +++++++ site/static/img/blog/viewer-far.jpg | Bin 0 -> 78007 bytes site/static/img/blog/viewer-typical-tl.jpg | Bin 0 -> 94712 bytes site/static/img/blog/viewer-typical.jpg | Bin 0 -> 95791 bytes site/static/img/blog/viewer-worst.jpg | Bin 0 -> 86040 bytes specs/006-engagement-viewer/tasks.md | 14 +- 23 files changed, 487 insertions(+), 33 deletions(-) create mode 100644 scripts/viewer-smoke.mjs create mode 100644 site/blog/2026-10-17-engagement-viewer.md create mode 100644 site/docs/algorithms/viewer.md create mode 100644 site/src/data/viewer-bookmarks.json create mode 100644 site/src/pages/viewer.jsx create mode 100644 site/static/img/blog/viewer-far.jpg create mode 100644 site/static/img/blog/viewer-typical-tl.jpg create mode 100644 site/static/img/blog/viewer-typical.jpg create mode 100644 site/static/img/blog/viewer-worst.jpg diff --git a/.gitignore b/.gitignore index 2dac71b5..c674263c 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,6 @@ site/docs/data-dictionary.md site/src/generated/ /models/ .claude/worktrees/ + +# Viewer model copies (site prebuild, from experiments/) +site/static/viewer/ diff --git a/AGENTS.md b/AGENTS.md index 9e2b31b5..b22acf8f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,8 @@ pnpm test # vitest: unit, golden, e2e (~30 s) UPDATE_GOLDEN=1 pnpm test # regenerate golden files; review the diff, say why in the PR pnpm schema:gen # after editing schema/tma.yaml; bump its version pnpm schema:check # fails if generated types are stale -pnpm site:build # data dictionary + gate status + site; fails on broken links +pnpm site:build # tsc -b, then data dictionary + gate status + viewer catalogue + site +node scripts/viewer-smoke.mjs [url] [dir] # Playwright smoke of a served /viewer (PLAYWRIGHT=) pnpm tma-generate --seed 42 --count 100000 --noise-free --out data/g0 pnpm tma-g0 --data data/g0 --experiment experiments/ pnpm tma-train --config configs/iter1/.json --train /train.features.bin \ @@ -167,6 +168,9 @@ sessions (containers are reclaimed when idle). - A seeded continuation that changes the loss re-scores the best epoch under the new loss and persists it as `best_val_loss` (schema 0.8.2); a chained job resumes against that value. Without it the old best, on the old scale, is never beaten and the old weights are kept silently. +- The viewer page imports `@tma/demos` from `dist/`, so `site:build` runs `tsc -b` first. The + site's Babel compiles `[...iterable]` to `[].concat(iterable)`: in browser code spread arrays + only, and use `Array.from` for NodeLists, Maps and Sets. - Gate checks: pass needs the whole 95% CI and the point below the threshold; fail needs the CI's lower bound at or above it; anything else is inconclusive (FR-015). @@ -191,8 +195,9 @@ decision `experiments/20261015-i2d-decision/`. **G2 failed a third time** (panel `experiments/20261016-i2d-g2/`): range 13.5% / 12.1% and course 7.4° / 6.1° pass, speed 1.47 / 1.53 kt fails (classical 0.61 kt), no-go on speed and pass on range and course (write-up: `site/blog/2026-10-17-iteration-2c.md`). Seeds 201–203 are reserved. **SRD Annex A** (accepted -2026-10-17) lifts the effort budget: next is the engagement and model viewer (spec 006, -`packages/demos`), then the owner's realism check, then pre-registered levers (Annex A Part B), +2026-10-17) lifts the effort budget. The engagement and model viewer (spec 006, `packages/demos`, +site page `/viewer`) is built; next is the owner's realism check (T010), then pre-registered +levers (Annex A Part B), each with a decision record committed before training and a fresh test seed (204 onward). Gate decisions come from an **agent review panel**, never from waiting on humans (constitution diff --git a/package.json b/package.json index 5096be75..81d9eac5 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "tma-gate": "node packages/eval/dist/cli-gate.js", "tma-g0": "tsc -b && node packages/eval/dist/cli-g0.js", "tma-features": "node packages/features/dist/cli.js", - "site:build": "pnpm --filter @tma/site build", - "site:start": "pnpm --filter @tma/site start", + "site:build": "pnpm build && pnpm --filter @tma/site build", + "site:start": "pnpm build && pnpm --filter @tma/site start", "tma-train": "tsc -b && node packages/models/dist/cli-train.js", "tma-model-predict": "node packages/models/dist/cli-predict.js", "tma-g1": "tsc -b && node packages/models/dist/cli-g1.js", diff --git a/packages/demos/src/viewer/EngagementViewer.tsx b/packages/demos/src/viewer/EngagementViewer.tsx index 01d31420..3013ccfe 100644 --- a/packages/demos/src/viewer/EngagementViewer.tsx +++ b/packages/demos/src/viewer/EngagementViewer.tsx @@ -203,6 +203,7 @@ export function EngagementViewer(props: EngagementViewerProps): JSX.Element { }; const root = useRef(null); + const [exportError, setExportError] = useState(null); const planRef = useRef(null); const fileStem = `tma-${source.id}-${String(v.index)}-${v.mode}`; @@ -360,6 +361,13 @@ export function EngagementViewer(props: EngagementViewerProps): JSX.Element { "time-late": "time-late", }; + const scenarioPanel = ( +
+

Scenario

+ +
+ ); + return (
@@ -531,11 +539,15 @@ export function EngagementViewer(props: EngagementViewerProps): JSX.Element {
+ {exportError !== null &&

PNG export failed: {exportError}

} {entry !== null && (

+ {v.mode === "truth" && scenarioPanel} {v.mode !== "truth" && (

At the cursor

@@ -625,10 +639,7 @@ export function EngagementViewer(props: EngagementViewerProps): JSX.Element {
{plots} -
-

Scenario

- -
+ {v.mode !== "truth" && scenarioPanel}

Models

diff --git a/packages/demos/src/viewer/Panels.tsx b/packages/demos/src/viewer/Panels.tsx index a6a2ed61..951051ad 100644 --- a/packages/demos/src/viewer/Panels.tsx +++ b/packages/demos/src/viewer/Panels.tsx @@ -14,7 +14,11 @@ const deg = (rad: number): number => { }; const dash = "—"; const fx = (v: number | null | undefined, digits: number): string => - v === null || v === undefined || !Number.isFinite(v) ? dash : v.toFixed(digits); + v === null || v === undefined || Number.isNaN(v) + ? dash + : v === Infinity + ? "∞" + : v.toFixed(digits); export interface Estimate { label: string; @@ -208,7 +212,7 @@ export function Metadata({ s, source, generatorVersion }: MetadataProps): JSX.El ["generator", generatorVersion], [ "window", - `${(p.window_s / 60).toFixed(0)} min, ${String(m.samples)} samples at ${String(p.dt_s)} s`, + `${String(m.samples)} samples at ${String(p.dt_s)} s (${(((m.samples - 1) * p.dt_s) / 60).toFixed(1)} min)`, ], ["noise σ", `${radToDeg(p.noise_sigma_rad).toFixed(2)}°`], ["dropouts", `${(p.dropout_fraction * 100).toFixed(1)}%`], @@ -270,16 +274,16 @@ export function ModelInfo({ pair, source }: ModelInfoProps): JSX.Element {

The manifest does not record the training dataset's seed.

) : trained === source.seed ? (

- Trained on seed {trained}; this scenario is from the same dataset's {source.label}{" "} - split, which training never saw. + Trained on seed {trained}'s training split; this scenario is from a split of the same + seed that training never saw.

) : (

Trained on seed {trained}; this scenario comes from a different dataset (seed{" "} {source.seed}). - {source.note === undefined ? "" : ` ${source.note}`}

)} + {source.note !== undefined &&

{source.note}

}
); } diff --git a/packages/demos/src/viewer/PlanView.tsx b/packages/demos/src/viewer/PlanView.tsx index 99934eeb..2a36babd 100644 --- a/packages/demos/src/viewer/PlanView.tsx +++ b/packages/demos/src/viewer/PlanView.tsx @@ -437,9 +437,9 @@ export function PlanView(props: PlanViewProps): JSX.Element { - Math.abs(v) >= 100 || Number.isInteger(v) ? v.toFixed(0) : v.toPrecision(2); +/** Tick label with as many decimals as the tick step needs. */ +const fmt = (v: number, step: number): string => + v.toFixed(step > 0 && step < 1 ? Math.min(3, Math.ceil(-Math.log10(step) - 1e-9)) : 0); export function TimePlot(props: TimePlotProps): JSX.Element { const { t, lines, cursorT, windowS, onCursor } = props; @@ -82,6 +83,7 @@ export function TimePlot(props: TimePlotProps): JSX.Element { const y = (v: number): number => T + ((H - T - B) * (hi - Math.min(hi, Math.max(lo, v)))) / (hi - lo); const ticks = props.yTicks ?? niceTicks(lo, hi); + const tickStep = ticks.length > 1 ? (ticks[1] ?? 0) - (ticks[0] ?? 0) : 1; const tTicks = niceTicks(0, windowS / 60, 6).filter((m) => m * 60 <= windowS); const pick = useCallback( @@ -184,7 +186,7 @@ export function TimePlot(props: TimePlotProps): JSX.Element { - {fmt(v)} + {fmt(v, tickStep)} ))} diff --git a/packages/demos/src/viewer/style.ts b/packages/demos/src/viewer/style.ts index e2c6e715..685cd944 100644 --- a/packages/demos/src/viewer/style.ts +++ b/packages/demos/src/viewer/style.ts @@ -26,6 +26,7 @@ export const CSS = ` .tmav-bar { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; margin: 8px 0 12px; } .tmav-bar label { display: inline-flex; gap: 6px; align-items: center; } .tmav-bar input[type=number] { width: 7.5em; } +.tmav-bar select { max-width: 24em; } .tmav-grid { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); gap: 16px; align-items: start; } @media (max-width: 960px) { .tmav-grid { grid-template-columns: minmax(0, 1fr); } } .tmav-panel { border: 1px solid var(--ifm-color-emphasis-300, #d0d7de); border-radius: 6px; padding: 8px 12px; margin-bottom: 12px; overflow-x: auto; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 724a74dc..a2c75050 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,6 +183,9 @@ importers: '@mermaid-js/layout-elk': specifier: ^0.1.9 version: 0.1.9(mermaid@12.1.0) + '@tma/demos': + specifier: workspace:* + version: link:../packages/demos clsx: specifier: ^2.1.1 version: 2.1.1 diff --git a/scripts/viewer-smoke.mjs b/scripts/viewer-smoke.mjs new file mode 100644 index 00000000..1a8627a0 --- /dev/null +++ b/scripts/viewer-smoke.mjs @@ -0,0 +1,71 @@ +// Smoke test of the built viewer page (spec 006 T008, SC-001, SC-002). Not part of `pnpm test`: +// it needs a served site and Playwright with Chromium, which the repo does not install. +// +// pnpm site:build && (cd site && npx docusaurus serve --port 3123 --no-open) & +// node scripts/viewer-smoke.mjs [http://localhost:3123/Gitma/viewer] [screenshot dir] +// +// Playwright is loaded from $PLAYWRIGHT (a path to the package) or as `playwright`. Fails on any +// console error, a missing plan view, or a model that does not run; reports the inference times +// against SC-001 (2c BiLSTM under 2 s). +import { mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +const base = process.argv[2] ?? "http://localhost:3123/Gitma/viewer"; +const shots = process.argv[3] ?? null; +const pw = process.env.PLAYWRIGHT; +let chromium; +try { + ({ chromium } = await import( + pw === undefined ? "playwright" : pathToFileURL(join(pw, "index.mjs")).href + )); +} catch { + console.error("viewer-smoke: Playwright not found; set PLAYWRIGHT to its package directory"); + process.exit(2); +} +if (shots !== null) mkdirSync(shots, { recursive: true }); + +const views = [ + ["truth", "src=s203-test&i=90000"], + ["instantaneous", "src=s203-test&i=97612&mode=instantaneous&t=1690&pair=i2c"], + ["time-late", "src=s203-test&i=97612&mode=time-late&t=1690&pair=i2c"], + ["held-out", "src=s201-heldout&i=102983&mode=time-late&t=1300&pair=i2c"], + ["i1", "src=s101-test&i=90000&mode=instantaneous&pair=i1"], + ["i2", "src=s201-test&i=90000&mode=time-late&pair=i2"], + ["i2b", "src=s202-test&i=90000&mode=time-late&pair=i2b"], +]; + +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: 1400, height: 1000 } }); +const errors = []; +page.on("console", (m) => { + if (m.type() === "error") errors.push(m.text()); +}); +page.on("pageerror", (e) => errors.push(String(e))); +const failures = []; +let bilstmMs = null; +for (const [name, query] of views) { + await page.goto(`${base}?${query}`); + await page.waitForSelector('svg[aria-label="Plan view"]', { timeout: 60_000 }); + let line = ""; + if (query.includes("mode=") && !query.includes("mode=truth")) { + const status = page.locator('[data-testid="tmav-status"]'); + await status.filter({ hasText: /ms on the CPU|Could not run/ }).waitFor({ timeout: 120_000 }); + line = (await status.textContent()) ?? ""; + if (line.includes("Could not run")) failures.push(`${name}: ${line}`); + const ms = /(\d+) ms/.exec(line); + if (name === "time-late" && ms !== null) bilstmMs = Number(ms[1]); + } + if (shots !== null) await page.screenshot({ path: join(shots, `${name}.png`), fullPage: true }); + console.log(`${name}: ok ${line}`); +} +await browser.close(); +if (errors.length > 0) failures.push(...errors.map((e) => `console: ${e}`)); +console.log( + `SC-001: 2c BiLSTM inference ${bilstmMs === null ? "not measured" : `${String(bilstmMs)} ms`} (target < 2000 ms)`, +); +if (failures.length > 0) { + console.error(failures.join("\n")); + process.exit(1); +} +console.log("viewer-smoke: pass"); diff --git a/site/blog/2026-10-17-engagement-viewer.md b/site/blog/2026-10-17-engagement-viewer.md new file mode 100644 index 00000000..dd48cda3 --- /dev/null +++ b/site/blog/2026-10-17-engagement-viewer.md @@ -0,0 +1,94 @@ +--- +title: "The engagement viewer: single cases behind the G2 numbers" +date: 2026-10-17 +--- + +The gates judged the models on medians over thousands of scenarios. The new +[engagement viewer](/viewer) shows one scenario at a time: the plan, both platforms' course and +speed, the bearings, and each gate's carried models beside the classical solver and the naive +baseline. It is the first step of SRD Annex A. Its first use is the owner's realism check of the +generator, which comes before any change to the training data. + + + +## What it does + +- **Regenerates scenarios in the browser** from their seed with the pipeline's own generator. No + scenario files ship. A test checks the browser path is byte-identical to Node. +- **Runs the models in the browser** with tfjs on the CPU backend. The same encoding and decoding + the pipeline uses feed it, and a test finds it within float32 tolerance of `tfjs-node`. +- **Three modes.** Truth shows the engagement. Instantaneous shows the causal LSTM's estimate at + a "now" cursor, from the bearings up to then. Time-late shows the bidirectional LSTM's estimate + at every sample of the window. +- **Every gate's carried pair**: iteration 1 (G1 pass) and iterations 2, 2b and 2c (G2 fail). +- **The URL holds the view**, so every link below reopens exactly what is described. + +The 2c BiLSTM runs in about 1 s for one scenario in headless Chromium; the LSTM in about 0.5 s. +The eight models total 4.5 MB. + +How it works: [the viewer](/docs/algorithms/viewer). + +## A typical engagement + +[Seed 203, scenario 97612, instantaneous, zig + 10 min](/viewer?src=s203-test&i=97612&mode=instantaneous&t=1690&pair=i2c). +The 2c LSTM sits near its own median errors here. + +![The typical engagement in instantaneous mode](/img/blog/viewer-typical.jpg) + +| At zig + 10 min | Range error | Course error | Speed error | +| --------------- | ----------- | ------------ | ----------- | +| 2c LSTM | 11% | 6° | 1.6 kt | +| Classical | 3% | 0° | 0.9 kt | +| Naive | 41% | 51° | 6.6 kt | + +Before the zig the model still answers, but range is unobservable from bearings alone. The viewer +greys that period out. Here the LSTM's range sits well short of the truth before the zig and +reaches it only after the turn. + +[The same scenario in time-late mode](/viewer?src=s203-test&i=97612&mode=time-late&t=1690&pair=i2c) +shows something the medians hide. The BiLSTM's estimated positions form a jagged path rather than +a straight track. Each sample's estimate comes from its own output row, and nothing ties them to +one constant-velocity solution. The classical batch solution is one straight track by +construction. In this case the BiLSTM's post-zig median speed error, 3.0 kt, is worse than the +instantaneous LSTM's 1.8 kt. + +![The typical engagement in time-late mode](/img/blog/viewer-typical-tl.jpg) + +## The largest speed error + +[Seed 203, scenario 95484, instantaneous, zig + 10 min](/viewer?src=s203-test&i=95484&mode=instantaneous&t=1980&pair=i2c) +has the 2c LSTM's largest speed error at zig + 10 min among the σ = 1° scenarios: 10.9 kt. + +![The largest speed error](/img/blog/viewer-worst.jpg) + +The geometry is hard, not broken. The target starts 2.5 km away and opens at 17.3 kt on a course +within about 10° of the line of sight. The bearing barely moves, so speed along the line of sight +is weakly observable. The LSTM answers 6.5 kt, pulled towards typical training speeds. The +classical solver struggles too: 13 of its 101 post-zig solutions fail to converge, and its +median speed error is 6.5 kt. One case cannot say how often this happens; it shows one way the +models fail. + +## A held-out engagement beyond the training ranges + +[Seed 201 held-out, scenario 102983, time-late, zig + 5 min](/viewer?src=s201-heldout&i=102983&mode=time-late&t=1300&pair=i2c) +starts at 25.8 km. Training ranges ran from 2 to 20 km. The BiLSTM's range estimate starts near +18 km against a true 25 km: in this case the model does not reach ranges beyond those it was +trained on. The classical solver has no such limit. + +![The held-out engagement in time-late mode](/img/blog/viewer-far.jpg) + +| Post-zig medians | Range error | Course error | Speed error | +| ---------------- | ----------- | ------------ | ----------- | +| 2c BiLSTM | 31% | 34° | 6.3 kt | +| Classical | 12% | 3.5° | 0.85 kt | +| Naive | 55% | 53° | 7.7 kt | + +## What comes next + +The owner now runs the realism check in Annex A §A.5: do the tracks, turns, speeds and bearing +noise look like real engagements? Its record (§A.7) decides which Part B levers, such as larger +or more ownship course changes, are worth pre-registering on fresh seeds from 204 onward. Nothing +is trained until then. + +These three cases illustrate; they do not measure. Frequencies come from the gate experiments, +not from cases picked in a viewer. diff --git a/site/docs/algorithms/viewer.md b/site/docs/algorithms/viewer.md new file mode 100644 index 00000000..61a821ab --- /dev/null +++ b/site/docs/algorithms/viewer.md @@ -0,0 +1,56 @@ +# Engagement viewer + +Source: `packages/demos/`, page `site/src/pages/viewer.jsx`. Requirements: SRD Annex A Part A, +spec 006. + +The [viewer](/viewer) shows one engagement at a time: the plan, the platforms' +course and speed, the bearings, and the carried models' estimates beside the classical solver and +the naive baseline. Its first job is the owner's realism check (Annex A §A.5); its second is to +show where and why the models fail, case by case, which the gate statistics cannot. + +## Scenarios + +No scenario files ship. The page holds a catalogue of datasets (seed, index range and parameter +ranges, copied from the gate experiments' dataset headers) and regenerates a scenario on demand +with `generateScenario` from `@tma/sim`, the function `tma-generate` calls. A test bundles the +viewer's core for the browser and checks the regenerated scenarios are byte-identical to the +Node generator's. Generator 0.2.0 reproduces 0.1.0's instantaneous-zig scenarios byte for byte, +so the iteration-1 datasets regenerate too. + +## Models + +Each gate's carried pair (instantaneous LSTM and time-late BiLSTM) is copied from its experiment +folder into the site at build time. The browser runs them with tfjs on the CPU backend, chosen +because it gives the same numbers on every machine; WebGL does not. Inputs and outputs go through +the pipeline's own `buildWindow` and `outputsToPrediction` from `@tma/features`. A test runs three +committed models on both backends and finds the browser path within float32 tolerance of +`tfjs-node`. + +One forward pass serves the whole window. The instantaneous model is causal, so its estimate at +time t depends only on bearings up to t: moving the cursor reads a different row, and the page +draws only the rows up to the cursor. Rows before the zig are shown but marked unobservable; the +gates never score them. + +## Baselines + +- **Classical, instantaneous mode:** the per-timestep least-squares solution, as `tma-eval` scores it. +- **Classical, time-late mode:** one batch solution over the whole window, drawn as a + constant-velocity track. +- **Naive:** the training-set means recorded in the gate experiment's manifest. + +The readout gives each estimator's range, bearing, course and speed at the cursor, with the error +against truth using the same `rowError` the evaluation uses. The summary gives the medians over +the post-zig rows, with a non-converged row counted as infinite error, as the gates count it. + +## State and export + +The dataset, index, mode, cursor time, model pair and overlays live in the URL query, so a link +reopens the same view. The PNG export stacks the plan and plots. The NDJSON export writes the +scenario and the shown predictions as canonical JSON in the schema's types, ready for the CLIs. + +## Limits + +- The viewer shows single cases. A striking case says nothing about frequency: use the gate + experiments for that. +- The browser regenerates scenarios, so a later generator change would show different scenarios + for the old seeds. The catalogue records the generator version it was built with. diff --git a/site/docusaurus.config.js b/site/docusaurus.config.js index 0afbdd07..be661bb2 100644 --- a/site/docusaurus.config.js +++ b/site/docusaurus.config.js @@ -43,6 +43,7 @@ const config = { { to: "/docs/data-dictionary", label: "Data dictionary", position: "left" }, { to: "/docs/algorithms/generator", label: "Algorithms", position: "left" }, { to: "/docs/results/iteration-1", label: "Results", position: "left" }, + { to: "/viewer", label: "Viewer", position: "left" }, { to: "/blog", label: "Blog", position: "left" }, { href: "https://github.com/debrief/Gitma", label: "GitHub", position: "right" }, ], diff --git a/site/package.json b/site/package.json index 9f7350eb..ba1d540f 100644 --- a/site/package.json +++ b/site/package.json @@ -15,6 +15,7 @@ "@docusaurus/theme-mermaid": "3.10.2", "@mdx-js/react": "^3.1.0", "@mermaid-js/layout-elk": "^0.1.9", + "@tma/demos": "workspace:*", "clsx": "^2.1.1", "prism-react-renderer": "^2.4.1", "react": "^19.2.0", diff --git a/site/scripts/prebuild.mjs b/site/scripts/prebuild.mjs index 8cc17737..7ac5ea82 100644 --- a/site/scripts/prebuild.mjs +++ b/site/scripts/prebuild.mjs @@ -1,7 +1,16 @@ -// Generated site inputs (not committed): gate status from experiments/*/verdict.json and the -// data dictionary from schema/tma.yaml (SRD §9: generated, never hand-edited). +// Generated site inputs (not committed): gate status from experiments/*/verdict.json, the +// data dictionary from schema/tma.yaml (SRD §9: generated, never hand-edited), and the viewer's +// catalogue and model copies from the gate experiments (spec 006 FR-007). import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { + copyFileSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -111,7 +120,117 @@ function dataDictionary() { writeFileSync(join(site, "docs", "data-dictionary.md"), `${front}\n${md}`); } +/** + * Each gate's carried pair (Annex A decision D3), oldest first: the viewer defaults to the last. + * The scenario sources are those experiments' test and held-out datasets, from their headers. + */ +const VIEWER_PAIRS = [ + { id: "i1", label: "Iteration 1", experiment: "20261012-g1-r2" }, + { id: "i2", label: "Iteration 2", experiment: "20261014-i2-g2" }, + { id: "i2b", label: "Iteration 2b", experiment: "20261015-i2c-g2" }, + { id: "i2c", label: "Iteration 2c", experiment: "20261016-i2d-g2" }, +]; +const VIEWER_MODELS = { instantaneous: "lstm-instantaneous", timeLate: "bilstm-time-late" }; + +/** The panel's decision where it overrides the machine result (e.g. G1 passed under rule 1). */ +function decision(verdict) { + const m = /\b(G\d) (PASS|FAIL)\b/.exec(verdict.decided_by ?? ""); + return m === null ? `${verdict.gate} ${verdict.result}` : `${m[1]} ${m[2].toLowerCase()}`; +} + +function viewer() { + const read = (path) => JSON.parse(readFileSync(path, "utf8")); + const out = join(site, "static", "viewer", "models"); + rmSync(out, { recursive: true, force: true }); + const sources = []; + const pairs = []; + let bytes = 0; + for (const p of [...VIEWER_PAIRS].reverse()) { + const base = join(root, "experiments", p.experiment); + const exp = read(join(base, "manifest.json")); + const verdict = read(join(base, "verdict.json")); + const train = exp.datasets.find((d) => d.header.split === "train")?.header; + for (const split of ["test", "heldout"]) { + const h = exp.datasets.find((d) => d.header.split === split)?.header; + if (h === undefined) continue; + const id = `s${h.dataset_seed}-${split}`; + if (sources.some((x) => x.id === id)) continue; + const fresh = h.dataset_seed !== train?.dataset_seed; + const r0 = h.parameter_ranges.target_range0_m; + const t0 = train?.parameter_ranges.target_range0_m; + const km = (r) => `${String(r.min / 1000)}–${String(r.max / 1000)} km`; + const notes = []; + if (fresh) + notes.push(`A fresh test split: seed ${h.dataset_seed}'s test split, never trained on.`); + if (t0 !== undefined && (r0.min !== t0.min || r0.max !== t0.max)) + notes.push(`Start ranges ${km(r0)}, outside the training ranges (${km(t0)}).`); + sources.push({ + id, + label: + split === "test" + ? `Seed ${h.dataset_seed} test (${p.label} ${verdict.gate})` + : `Seed ${h.dataset_seed} held-out (${km(r0)})`, + seed: h.dataset_seed, + start: h.index_start, + end: h.index_end, + ranges: h.parameter_ranges, + ...(notes.length > 0 ? { note: notes.join(" ") } : {}), + }); + } + const naive = JSON.parse(exp.estimators.find((e) => e.name === "naive").settings_json).means; + const entry = (variant, id) => { + const dir = join(base, "models", id); + const m = read(join(dir, "manifest.json")); + const c = m.config; + const kind = c.model_kind === "time_late" ? "BiLSTM" : "LSTM"; + const layers = c.layers ?? 1; + const dest = join(out, p.experiment, id); + mkdirSync(dest, { recursive: true }); + for (const f of ["model.json", "weights.bin"]) { + copyFileSync(join(dir, f), join(dest, f)); + bytes += readFileSync(join(dir, f)).length; + } + return { + id, + variant, + path: `${p.experiment}/${id}`, + featureSet: c.feature_set ?? "raw", + normalisation: m.normalisation, + parameterCount: m.parameter_count, + architecture: `${kind} ${layers > 1 ? `${String(layers)} × ` : ""}${String(c.units)} units`, + trainedOnSeed: m.source_dataset_header?.dataset_seed ?? train?.dataset_seed ?? null, + bestEpoch: m.best_epoch, + }; + }; + pairs.unshift({ + id: p.id, + label: p.label, + experiment: `experiments/${p.experiment}`, + gate: decision(verdict), + instantaneous: entry("instantaneous", VIEWER_MODELS.instantaneous), + timeLate: entry("time_late", VIEWER_MODELS.timeLate), + naive, + }); + } + const sim = readFileSync(join(root, "packages", "sim", "src", "scenario.ts"), "utf8"); + const generatorVersion = /GENERATOR_VERSION = "([^"]+)"/.exec(sim)?.[1] ?? "unknown"; + const bookmarks = read(join(site, "src", "data", "viewer-bookmarks.json")); + for (const b of bookmarks) { + if (!sources.some((x) => x.id === b.source)) + throw new Error(`viewer bookmark: no source ${b.source}`); + } + writeFileSync( + join(site, "src", "generated", "viewer.json"), + `${JSON.stringify({ generatorVersion, sources, pairs, bookmarks })}\n`, + ); + return { sources: sources.length, pairs: pairs.length, mb: bytes / 1e6 }; +} + const n = gates(); const r = results(); +const v = viewer(); dataDictionary(); -console.log(`prebuild: ${n} gate verdict(s), ${r} result set(s); data dictionary generated`); +console.log( + `prebuild: ${n} gate verdict(s), ${r} result set(s); viewer: ${v.sources} sources, ` + + `${v.pairs} model pairs, ${v.mb.toFixed(1)} MB of weights; data dictionary generated`, +); diff --git a/site/sidebars.js b/site/sidebars.js index c3fd9f3d..ac99f85a 100644 --- a/site/sidebars.js +++ b/site/sidebars.js @@ -13,6 +13,7 @@ const sidebars = { "algorithms/naive-baseline", "algorithms/least-squares-solver", "algorithms/models", + "algorithms/viewer", ], }, { diff --git a/site/src/data/viewer-bookmarks.json b/site/src/data/viewer-bookmarks.json new file mode 100644 index 00000000..0dc82b16 --- /dev/null +++ b/site/src/data/viewer-bookmarks.json @@ -0,0 +1,33 @@ +[ + { + "label": "Typical engagement: 2c instantaneous near its median errors at zig + 10 min", + "source": "s203-test", + "index": 97612, + "mode": "instantaneous", + "t": 1690, + "pair": "i2c" + }, + { + "label": "Largest 2c instantaneous speed error at zig + 10 min, σ = 1° (10.9 kt)", + "source": "s203-test", + "index": 95484, + "mode": "instantaneous", + "t": 1980, + "pair": "i2c" + }, + { + "label": "Held-out engagement starting at 26 km: 2c time-late, median range error at zig + 5 min", + "source": "s201-heldout", + "index": 102983, + "mode": "time-late", + "t": 1300, + "pair": "i2c" + }, + { + "label": "Iteration 1 engagement: σ = 0.5°, no dropouts, instantaneous zig", + "source": "s101-test", + "index": 90000, + "mode": "truth", + "pair": "i1" + } +] diff --git a/site/src/pages/viewer.jsx b/site/src/pages/viewer.jsx new file mode 100644 index 00000000..dcfff033 --- /dev/null +++ b/site/src/pages/viewer.jsx @@ -0,0 +1,49 @@ +// The engagement and model viewer (SRD Annex A Part A, spec 006). Browser only: scenarios are +// regenerated and the models run in the page, so the server render shows a placeholder. +import BrowserOnly from "@docusaurus/BrowserOnly"; +import Link from "@docusaurus/Link"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Layout from "@theme/Layout"; +import { lazy, Suspense } from "react"; +import catalogue from "../generated/viewer.json"; + +const EngagementViewer = lazy(() => + import("@tma/demos/viewer").then((m) => ({ default: m.EngagementViewer })), +); + +export default function Viewer() { + const models = useBaseUrl("/viewer/models"); + const loading =

Loading the viewer…

; + return ( + +
+

Engagement viewer

+

+ Pick a dataset and a scenario: the page regenerates it from its seed with the + pipeline's own generator and runs each gate's carried models in your browser.{" "} + Truth shows the engagement. Instantaneous shows the + causal model's estimate at the cursor, from the bearings up to then.{" "} + Time-late shows the bidirectional model's track over the whole + window. The classical solver and the naive baseline sit beside every model figure. The + link in the address bar reopens this view. How it works:{" "} + the viewer. +

+ + {() => ( + + + + )} + +
+
+ ); +} diff --git a/site/static/img/blog/viewer-far.jpg b/site/static/img/blog/viewer-far.jpg new file mode 100644 index 0000000000000000000000000000000000000000..07e702a409cd23345995589ec2874e532ced607f GIT binary patch literal 78007 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%0DF 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

Y1VGOsKG8=vYwH#1Rd7upA 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 literal 0 HcmV?d00001 diff --git a/site/static/img/blog/viewer-typical-tl.jpg b/site/static/img/blog/viewer-typical-tl.jpg new file mode 100644 index 0000000000000000000000000000000000000000..854e66d4200719b6b3f6a95e7c6942ea98987382 GIT binary patch literal 94712 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 literal 0 HcmV?d00001 diff --git a/site/static/img/blog/viewer-typical.jpg b/site/static/img/blog/viewer-typical.jpg new file mode 100644 index 0000000000000000000000000000000000000000..16c50c3da8a0b21906b4ca5d9298f60e9d8353a4 GIT binary patch literal 95791 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

9|nr@yFjn^ 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 s&+b^4zo;JzA02ykqZ2$lO literal 0 HcmV?d00001 diff --git a/site/static/img/blog/viewer-worst.jpg b/site/static/img/blog/viewer-worst.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2c7d9d2c21b44319d5170cb469084ef63208c7a3 GIT binary patch literal 86040 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

p)NNIt%|vf??f11KitG<01KCLb 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 Date: Sat, 17 Oct 2026 08:36:29 +0000 Subject: [PATCH 04/21] Iteration 3 setup: generator 0.3.0 (second zig, across-LOS), schema 0.9.0, spec 007 - Generator 0.3.0: zig_rule across_los (target ends on the other bow at a drawn offset, shorter turn) and an optional second zig (zig2_delay_s); new draws after every 0.2.0 draw, so earlier ranges are byte-identical - Schema 0.9.0: ZigRule, los_offset_rad, zig2_delay_s, zig2_angle_rad, zig2_index, turn2_start_index; 0.1.0 and 0.2.0 data stay readable - Goldens: data and features headers carry the new version strings only; new three-leg sim golden; known-answer and generated-scenario tests - G0 guard test: generator 0.3.0 refuses the 0.2.0 G0 until G0 is re-run - Viewer: second turn, leg 3, zig 2 marker and readout - configs/iter3-ranges.json, iter3-heldout-ranges.json - Spec 007 (spec, research R1-R4, tasks); Annex A A.7 record and D4; SRD section 3 row and decision 22; glossary and generator note Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- AGENTS.md | 10 +- configs/iter3-heldout-ranges.json | 47 +++++ configs/iter3-ranges.json | 47 +++++ docs/SRD-annex-A.md | 29 ++- docs/SRD.md | 2 + packages/data/src/versions.ts | 5 +- packages/data/test/golden/digests.json | 16 +- .../test/golden/seed1-count10-test.ndjson | 2 +- packages/demos/src/core/series.ts | 21 ++- .../demos/src/viewer/EngagementViewer.tsx | 7 +- packages/demos/src/viewer/Panels.tsx | 26 ++- packages/demos/src/viewer/PlanView.tsx | 36 ++-- packages/demos/src/viewer/TimePlot.tsx | 12 +- packages/demos/test/core.test.ts | 19 +- .../features/test/golden/seed1-2.features.bin | Bin 34020 -> 34020 bytes .../test/golden/seed1-4.derived.features.bin | Bin 78296 -> 78296 bytes packages/models/test/g0-guard.test.ts | 23 ++- packages/sim/package.json | 2 +- packages/sim/src/scenario.ts | 176 +++++++++++++++++- packages/sim/test/golden.test.ts | 18 ++ .../test/golden/seed1-3-zig2-across.ndjson | 3 + packages/sim/test/unit/second-zig.test.ts | 159 ++++++++++++++++ packages/types/src/generated/tma.schema.json | 72 ++++++- packages/types/src/generated/version.ts | 2 +- packages/types/src/generated/zod.ts | 18 ++ schema/tma.yaml | 48 ++++- site/docs/algorithms/generator.md | 43 ++++- site/docs/glossary.md | 12 +- specs/007-iteration-3-second-zig/research.md | 116 ++++++++++++ specs/007-iteration-3-second-zig/spec.md | 120 ++++++++++++ specs/007-iteration-3-second-zig/tasks.md | 28 +++ 31 files changed, 1044 insertions(+), 75 deletions(-) create mode 100644 configs/iter3-heldout-ranges.json create mode 100644 configs/iter3-ranges.json create mode 100644 packages/sim/test/golden/seed1-3-zig2-across.ndjson create mode 100644 packages/sim/test/unit/second-zig.test.ts create mode 100644 specs/007-iteration-3-second-zig/research.md create mode 100644 specs/007-iteration-3-second-zig/spec.md create mode 100644 specs/007-iteration-3-second-zig/tasks.md diff --git a/AGENTS.md b/AGENTS.md index b22acf8f..8b21e1ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -196,9 +196,13 @@ decision `experiments/20261015-i2d-decision/`. **G2 failed a third time** (panel 1.53 kt fails (classical 0.61 kt), no-go on speed and pass on range and course (write-up: `site/blog/2026-10-17-iteration-2c.md`). Seeds 201–203 are reserved. **SRD Annex A** (accepted 2026-10-17) lifts the effort budget. The engagement and model viewer (spec 006, `packages/demos`, -site page `/viewer`) is built; next is the owner's realism check (T010), then pre-registered -levers (Annex A Part B), -each with a decision record committed before training and a fresh test seed (204 onward). +site page `/viewer`) is built, and the owner's realism check is recorded (Annex A §A.7). Part B +levers are pre-registered one at a time, each with a decision record committed before training +and a fresh test seed (204 onward). **Iteration 3** (spec 007, decision 22) is lever B.2.1: +generator 0.3.0 adds a second zig 3–5 min into leg 2, and both turns cross the line of sight +(`zig_rule: across_los`). Its dataset is seed 204, and G0 must be re-run on 0.3.0 before +training. The classical forecast is 0.36 kt at zig + 600 s, and the model forecast is 1.1–1.3 kt +(spec 007 research R2). Gate decisions come from an **agent review panel**, never from waiting on humans (constitution 1.2.0). For each gate experiment: diff --git a/configs/iter3-heldout-ranges.json b/configs/iter3-heldout-ranges.json new file mode 100644 index 00000000..6b6227ed --- /dev/null +++ b/configs/iter3-heldout-ranges.json @@ -0,0 +1,47 @@ +{ + "window_s": { + "min": 600, + "max": 2400 + }, + "dt_s": 10, + "ownship_speed_mps": { + "min": 2.5722222222222224, + "max": 7.716666666666667 + }, + "zig_angle_abs_rad": { + "min": 0.5235987755982988, + "max": 1.5707963267948966 + }, + "zig_fraction": { + "min": 0.4, + "max": 0.6 + }, + "target_range0_m": { + "min": 20000, + "max": 30000 + }, + "range_distribution": "log_uniform", + "target_speed_mps": { + "min": 1.5433333333333334, + "max": 10.28888888888889 + }, + "noise_sigma_rad_values": [0.017453292519943295, 0.03490658503988659], + "dropout_fraction": { + "min": 0, + "max": 0.1 + }, + "ownship_turn_rate_rad_s": { + "min": 0.008726646259971648, + "max": 0.05235987755982989 + }, + "min_range_m": 100, + "zig_rule": "across_los", + "los_offset_rad": { + "min": 0.5235987755982988, + "max": 1.5707963267948966 + }, + "zig2_delay_s": { + "min": 180, + "max": 300 + } +} diff --git a/configs/iter3-ranges.json b/configs/iter3-ranges.json new file mode 100644 index 00000000..b7b05be9 --- /dev/null +++ b/configs/iter3-ranges.json @@ -0,0 +1,47 @@ +{ + "window_s": { + "min": 600, + "max": 2400 + }, + "dt_s": 10, + "ownship_speed_mps": { + "min": 2.5722222222222224, + "max": 7.716666666666667 + }, + "zig_angle_abs_rad": { + "min": 0.5235987755982988, + "max": 1.5707963267948966 + }, + "zig_fraction": { + "min": 0.4, + "max": 0.6 + }, + "target_range0_m": { + "min": 2000, + "max": 20000 + }, + "range_distribution": "log_uniform", + "target_speed_mps": { + "min": 1.5433333333333334, + "max": 10.28888888888889 + }, + "noise_sigma_rad_values": [0.017453292519943295, 0.03490658503988659], + "dropout_fraction": { + "min": 0, + "max": 0.1 + }, + "ownship_turn_rate_rad_s": { + "min": 0.008726646259971648, + "max": 0.05235987755982989 + }, + "min_range_m": 100, + "zig_rule": "across_los", + "los_offset_rad": { + "min": 0.5235987755982988, + "max": 1.5707963267948966 + }, + "zig2_delay_s": { + "min": 180, + "max": 300 + } +} diff --git a/docs/SRD-annex-A.md b/docs/SRD-annex-A.md index d4f73f65..8c68422e 100644 --- a/docs/SRD-annex-A.md +++ b/docs/SRD-annex-A.md @@ -173,8 +173,30 @@ training-progress demo (§9, PR preview apps) remains a separate item. ## A.7 Record of the realism check -To be filled in when A.5 has been done: date, datasets and indices looked at, findings, and the -decision whether Part B starts on the current generator or after generator changes. +**2026-10-17, the owner, in the viewer's PR preview (PR #51).** The owner looked at engagements +in the viewer; the record does not list the indices viewed. + +**Findings.** + +- **Platform motion looks right.** Tracks, turns and bearings are realistic. +- **The zig comes too late.** With both platforms on steady legs there is not enough information + for TMA, unless there is other context such as a predicted initial detection range or a speed + estimate from acoustic cues. +- **Operational practice.** An operator with no steady solution zigs again, and turning towards + the contact traditionally gives a better solution. + +**What the gate data says.** Spec 007 research R1 tested these ideas against the seed-203 test +predictions. + +- **Shorter first legs are worse at a fixed time after the zig.** The gates and the training loss + already exclude the pre-zig period. +- **Turning towards the contact helps only when the turn crosses the line of sight.** +- **Larger turns help every estimator.** + +**Decision (decision 22, D4).** Part B starts after a generator change: lever B.2.1. There is a +second zig, both turns cross the line of sight, and the first zig keeps its timing. This is +iteration 3, spec 007, judged on fresh seed 204. The new engagements are shown in the viewer +before training. ## B. Phase 2: a further attempt at G2 @@ -261,4 +283,5 @@ Each is a separate, pre-registered experiment (B.3). None is chosen until A.7 is - **D3** Which model folders ship by default (the 2c pair, or every gate's carried pair). _Decided 2026-10-17: every gate's carried pair (iterations 1, 2, 2b and 2c), so one engagement can be stepped through the project's history; controls remain selectable by path._ -- **D4** After A.7: the first lever of B.2. +- **D4** After A.7: the first lever of B.2. _Decided 2026-10-17: B.2.1, a second zig with both + turns across the line of sight (spec 007)._ diff --git a/docs/SRD.md b/docs/SRD.md index 1b6cc273..2a274418 100644 --- a/docs/SRD.md +++ b/docs/SRD.md @@ -49,6 +49,7 @@ The generator must produce scenarios fast enough that data volume is never the b | Window length | 10–40 min, sampled per scenario; 1 bearing / 10 s (60–240 samples) | Variable length from the outset; inputs padded to 240 and masked | | Ownship speed | 5–15 kt | Constant within a leg | | Ownship zig | Course change 30–90°, at 40–60% of window | Turn modelled as instantaneous in iteration 1, then rate-limited | +| Ownship zig, from iteration 3 | First zig at 40–60% of window, then a second zig 3–5 min after leg 2 starts; each turn crosses the line of sight, putting the target 30–90° on the other bow (shorter turn; median 143°) | Generator 0.3.0, Annex A lever B.2.1, spec 007; same turn rate (0.5–3°/s) for both turns; gate times still count from the end of the first turn | | Target initial range | 2–20 km | Log-uniform so short ranges aren't under-represented | | Target initial bearing | Uniform 0–360° | Relative to ownship at window start | | Target speed | 3–20 kt | Constant | @@ -239,6 +240,7 @@ Four one-week iterations, each ending at a gate. Iteration 0 is infrastructure; - Decision 21: the effort budget is lifted; failing G2 no longer ends the project. Further work follows Annex A: the engagement and model viewer first (Part A), then pre-registered levers (Part B). - Viewer: inference in the browser on the committed weights (D1); bearing-window trimming deferred (D2); every gate's carried pair offered by default (D3). +- Decision 22 (Annex A D4): the realism check is recorded (Annex A §A.7), and the first Part B lever is observability in the data. Iteration 3 (spec 007) adds a second zig, with both turns across the line of sight, keeps the first zig's timing, and is judged on fresh seed 204. ## 9. Development practice and public site diff --git a/packages/data/src/versions.ts b/packages/data/src/versions.ts index be6da66b..95e0f8fc 100644 --- a/packages/data/src/versions.ts +++ b/packages/data/src/versions.ts @@ -15,9 +15,10 @@ function compatible(file: string, build: string): boolean { /** * Generator versions whose files this build reads. 0.2.0 (rate-limited turn) reproduces 0.1.0 - * byte for byte when the ranges have no turn rate, so iteration-1 datasets stay readable. + * byte for byte when the ranges have no turn rate, and 0.3.0 (second zig, across-LOS rule) + * reproduces 0.2.0 when the ranges have neither, so earlier datasets stay readable. */ -export const READABLE_GENERATOR_VERSIONS: readonly string[] = ["0.1.0", GENERATOR_VERSION]; +export const READABLE_GENERATOR_VERSIONS: readonly string[] = ["0.1.0", "0.2.0", GENERATOR_VERSION]; /** Reject files from an incompatible schema version or an unknown generator version (FR-007). */ export function checkVersions( diff --git a/packages/data/test/golden/digests.json b/packages/data/test/golden/digests.json index d5b35adc..6d764f0c 100644 --- a/packages/data/test/golden/digests.json +++ b/packages/data/test/golden/digests.json @@ -1,10 +1,10 @@ { - "train.ndjson": "3bd7a7c8cc3ec52141b25836ecc25cfdddae8a71bcccec549587e8b993714418", - "train.f32pack": "c54345b1c8052f3713a88c074692fb62da1e20d345539c0e5f063d8f02729a11", - "validation.ndjson": "620619a22bf57cc567f0b55cd67198242fd684e5fc5242b53f00b17e16d5d898", - "validation.f32pack": "7d5075c8819748d240fe4c980a94b134e737eeb6893f4756b30eeccb05c68513", - "test.ndjson": "a3b0230c4f415913c7d7036dc9621996b90ed49e07c5df4e6955598fee3db95f", - "test.f32pack": "e229ddeaaf21c40f32028c99922f4421da5a60c9a68c7a438cbc52d9cab9b685", - "heldout.ndjson": "89376fd645ae3ad96e4700dffeda4f1a30a5041983772bed9082b7acb9b647cb", - "heldout.f32pack": "0b62e4f74cd48e5602e51a3d1fc3aa4d9cdab4efea848a570924cea7f4d9063f" + "train.ndjson": "9e1ed453f0fb5511979e04b2fdfdfa6d3d5e18756092645233d4f133eaffb6e3", + "train.f32pack": "2d5919fc4a619cc50e5d324e0ffbc9805ed3bb98ae565f31c90bcc40420a3a30", + "validation.ndjson": "fbad0af145c206ad1127cd1266d586b48aaaba4b9002a2d6ad8aa1413fdfcd59", + "validation.f32pack": "006d3e71f7b3b16ba96630bbad261e9fefae005be0f3e3784a194a064d15a5f8", + "test.ndjson": "c746ace467b192676c5f8667ae541415ff8b3a3e80034e03bd7f771098a6bccb", + "test.f32pack": "e53663577ea827c98cd273e3a86b770f68ea49158dfb34886bfde970ab4b47ea", + "heldout.ndjson": "514403783aaf1d79d9018658992d9201e3130e665b3909fddcbb5481a62813d3", + "heldout.f32pack": "6cae688cada64acd7de81086612622444b480866b9df9709e0c9686cdfe35aae" } diff --git a/packages/data/test/golden/seed1-count10-test.ndjson b/packages/data/test/golden/seed1-count10-test.ndjson index 1192e748..3c11e6b3 100644 --- a/packages/data/test/golden/seed1-count10-test.ndjson +++ b/packages/data/test/golden/seed1-count10-test.ndjson @@ -1,2 +1,2 @@ -{"created_by":"tma-generate --seed 1 --count 10","dataset_seed":"1","generator_git_commit":"0123456789abcdef0123456789abcdef01234567","generator_git_dirty":false,"generator_version":"0.2.0","index_end":10,"index_start":9,"parameter_ranges":{"dropout_fraction":{"max":0.1,"min":0},"dt_s":10,"min_range_m":100,"noise_sigma_rad_values":[0.008726646259971648,0.017453292519943295,0.03490658503988659],"ownship_speed_mps":{"max":7.716666666666667,"min":2.5722222222222224},"range_distribution":"log_uniform","target_range0_m":{"max":20000,"min":2000},"target_speed_mps":{"max":10.28888888888889,"min":1.5433333333333334},"window_s":{"max":2400,"min":600},"zig_angle_abs_rad":{"max":1.5707963267948966,"min":0.5235987755982988},"zig_fraction":{"max":0.6,"min":0.4}},"record":"header","redraw_count":0,"scenario_count":1,"schema_version":"0.8.2","split":"test"} +{"created_by":"tma-generate --seed 1 --count 10","dataset_seed":"1","generator_git_commit":"0123456789abcdef0123456789abcdef01234567","generator_git_dirty":false,"generator_version":"0.3.0","index_end":10,"index_start":9,"parameter_ranges":{"dropout_fraction":{"max":0.1,"min":0},"dt_s":10,"min_range_m":100,"noise_sigma_rad_values":[0.008726646259971648,0.017453292519943295,0.03490658503988659],"ownship_speed_mps":{"max":7.716666666666667,"min":2.5722222222222224},"range_distribution":"log_uniform","target_range0_m":{"max":20000,"min":2000},"target_speed_mps":{"max":10.28888888888889,"min":1.5433333333333334},"window_s":{"max":2400,"min":600},"zig_angle_abs_rad":{"max":1.5707963267948966,"min":0.5235987755982988},"zig_fraction":{"max":0.6,"min":0.4}},"record":"header","redraw_count":0,"scenario_count":1,"schema_version":"0.9.0","split":"test"} {"metadata":{"index":9,"mean_range_m":5958.755859375,"min_range_m":991.6001586914062,"parameters":{"dropout_fraction":0.03438697010278702,"dt_s":10,"noise_sigma_rad":0.03490658476948738,"ownship_course0_rad":2.5504064559936523,"ownship_speed_mps":5.665868282318115,"target_bearing0_rad":2.595902442932129,"target_course_rad":6.078586101531982,"target_range0_m":5175.45263671875,"target_speed_mps":9.046577453613281,"window_s":1390,"zig_angle_rad":1.5534991025924683,"zig_fraction":0.4844420850276947},"samples":139,"seed":"14646652180046636950","zig_index":67,"zig_time_s":670},"observations":[{"bearing_rad":2.6220357418060303,"timestamp_s":0,"valid":true},{"bearing_rad":0,"timestamp_s":10,"valid":false},{"bearing_rad":2.615083694458008,"timestamp_s":20,"valid":true},{"bearing_rad":2.58514404296875,"timestamp_s":30,"valid":true},{"bearing_rad":2.574988842010498,"timestamp_s":40,"valid":true},{"bearing_rad":2.527792453765869,"timestamp_s":50,"valid":true},{"bearing_rad":2.5440268516540527,"timestamp_s":60,"valid":true},{"bearing_rad":2.596759557723999,"timestamp_s":70,"valid":true},{"bearing_rad":2.575688600540161,"timestamp_s":80,"valid":true},{"bearing_rad":2.616668224334717,"timestamp_s":90,"valid":true},{"bearing_rad":2.5857462882995605,"timestamp_s":100,"valid":true},{"bearing_rad":2.500842332839966,"timestamp_s":110,"valid":true},{"bearing_rad":2.5180552005767822,"timestamp_s":120,"valid":true},{"bearing_rad":2.519590377807617,"timestamp_s":130,"valid":true},{"bearing_rad":2.478708505630493,"timestamp_s":140,"valid":true},{"bearing_rad":2.4860692024230957,"timestamp_s":150,"valid":true},{"bearing_rad":2.455496072769165,"timestamp_s":160,"valid":true},{"bearing_rad":2.447126865386963,"timestamp_s":170,"valid":true},{"bearing_rad":2.408547878265381,"timestamp_s":180,"valid":true},{"bearing_rad":2.3457624912261963,"timestamp_s":190,"valid":true},{"bearing_rad":2.3567445278167725,"timestamp_s":200,"valid":true},{"bearing_rad":2.330347776412964,"timestamp_s":210,"valid":true},{"bearing_rad":2.3350954055786133,"timestamp_s":220,"valid":true},{"bearing_rad":2.2078351974487305,"timestamp_s":230,"valid":true},{"bearing_rad":2.1612424850463867,"timestamp_s":240,"valid":true},{"bearing_rad":2.199679374694824,"timestamp_s":250,"valid":true},{"bearing_rad":2.1880311965942383,"timestamp_s":260,"valid":true},{"bearing_rad":2.0965542793273926,"timestamp_s":270,"valid":true},{"bearing_rad":2.030519723892212,"timestamp_s":280,"valid":true},{"bearing_rad":1.982517957687378,"timestamp_s":290,"valid":true},{"bearing_rad":1.8200387954711914,"timestamp_s":300,"valid":true},{"bearing_rad":1.7571829557418823,"timestamp_s":310,"valid":true},{"bearing_rad":0,"timestamp_s":320,"valid":false},{"bearing_rad":1.5004644393920898,"timestamp_s":330,"valid":true},{"bearing_rad":1.3480144739151,"timestamp_s":340,"valid":true},{"bearing_rad":1.2327461242675781,"timestamp_s":350,"valid":true},{"bearing_rad":1.1466983556747437,"timestamp_s":360,"valid":true},{"bearing_rad":1.007601261138916,"timestamp_s":370,"valid":true},{"bearing_rad":0.8127108812332153,"timestamp_s":380,"valid":true},{"bearing_rad":0.7107616066932678,"timestamp_s":390,"valid":true},{"bearing_rad":0.628229022026062,"timestamp_s":400,"valid":true},{"bearing_rad":0.5733001828193665,"timestamp_s":410,"valid":true},{"bearing_rad":0.47830432653427124,"timestamp_s":420,"valid":true},{"bearing_rad":0.34476739168167114,"timestamp_s":430,"valid":true},{"bearing_rad":0.3072022497653961,"timestamp_s":440,"valid":true},{"bearing_rad":0.21448193490505219,"timestamp_s":450,"valid":true},{"bearing_rad":0.2762027382850647,"timestamp_s":460,"valid":true},{"bearing_rad":0.1993727833032608,"timestamp_s":470,"valid":true},{"bearing_rad":0.12604738771915436,"timestamp_s":480,"valid":true},{"bearing_rad":0.1762598752975464,"timestamp_s":490,"valid":true},{"bearing_rad":0.11149193346500397,"timestamp_s":500,"valid":true},{"bearing_rad":0.035279180854558945,"timestamp_s":510,"valid":true},{"bearing_rad":6.274420738220215,"timestamp_s":520,"valid":true},{"bearing_rad":0.01893194206058979,"timestamp_s":530,"valid":true},{"bearing_rad":6.234099388122559,"timestamp_s":540,"valid":true},{"bearing_rad":6.275217533111572,"timestamp_s":550,"valid":true},{"bearing_rad":6.24389123916626,"timestamp_s":560,"valid":true},{"bearing_rad":6.210912704467773,"timestamp_s":570,"valid":true},{"bearing_rad":6.221532821655273,"timestamp_s":580,"valid":true},{"bearing_rad":6.1376118659973145,"timestamp_s":590,"valid":true},{"bearing_rad":6.246764659881592,"timestamp_s":600,"valid":true},{"bearing_rad":6.193379878997803,"timestamp_s":610,"valid":true},{"bearing_rad":6.177923202514648,"timestamp_s":620,"valid":true},{"bearing_rad":6.193901062011719,"timestamp_s":630,"valid":true},{"bearing_rad":6.178808689117432,"timestamp_s":640,"valid":true},{"bearing_rad":6.147686004638672,"timestamp_s":650,"valid":true},{"bearing_rad":6.212806224822998,"timestamp_s":660,"valid":true},{"bearing_rad":6.125346660614014,"timestamp_s":670,"valid":true},{"bearing_rad":6.2379584312438965,"timestamp_s":680,"valid":true},{"bearing_rad":6.065344333648682,"timestamp_s":690,"valid":true},{"bearing_rad":6.137896537780762,"timestamp_s":700,"valid":true},{"bearing_rad":6.209718704223633,"timestamp_s":710,"valid":true},{"bearing_rad":6.189711570739746,"timestamp_s":720,"valid":true},{"bearing_rad":6.182713508605957,"timestamp_s":730,"valid":true},{"bearing_rad":6.21376371383667,"timestamp_s":740,"valid":true},{"bearing_rad":6.191138744354248,"timestamp_s":750,"valid":true},{"bearing_rad":6.180636882781982,"timestamp_s":760,"valid":true},{"bearing_rad":6.251753330230713,"timestamp_s":770,"valid":true},{"bearing_rad":6.239569187164307,"timestamp_s":780,"valid":true},{"bearing_rad":0,"timestamp_s":790,"valid":false},{"bearing_rad":6.198854446411133,"timestamp_s":800,"valid":true},{"bearing_rad":0.002407611347734928,"timestamp_s":810,"valid":true},{"bearing_rad":6.25775146484375,"timestamp_s":820,"valid":true},{"bearing_rad":6.27123498916626,"timestamp_s":830,"valid":true},{"bearing_rad":0.03430521488189697,"timestamp_s":840,"valid":true},{"bearing_rad":6.239800453186035,"timestamp_s":850,"valid":true},{"bearing_rad":6.269564151763916,"timestamp_s":860,"valid":true},{"bearing_rad":0.043648820370435715,"timestamp_s":870,"valid":true},{"bearing_rad":6.223535060882568,"timestamp_s":880,"valid":true},{"bearing_rad":0.005696240346878767,"timestamp_s":890,"valid":true},{"bearing_rad":0.020358096808195114,"timestamp_s":900,"valid":true},{"bearing_rad":0.012834550812840462,"timestamp_s":910,"valid":true},{"bearing_rad":0.041951797902584076,"timestamp_s":920,"valid":true},{"bearing_rad":0.00724956626072526,"timestamp_s":930,"valid":true},{"bearing_rad":6.24912691116333,"timestamp_s":940,"valid":true},{"bearing_rad":0.036373239010572433,"timestamp_s":950,"valid":true},{"bearing_rad":0.02393398806452751,"timestamp_s":960,"valid":true},{"bearing_rad":0.04807974770665169,"timestamp_s":970,"valid":true},{"bearing_rad":0.05394063889980316,"timestamp_s":980,"valid":true},{"bearing_rad":0.00478453328832984,"timestamp_s":990,"valid":true},{"bearing_rad":0.01687886379659176,"timestamp_s":1000,"valid":true},{"bearing_rad":0.05434570461511612,"timestamp_s":1010,"valid":true},{"bearing_rad":0.02123403735458851,"timestamp_s":1020,"valid":true},{"bearing_rad":0,"timestamp_s":1030,"valid":false},{"bearing_rad":0.02340744249522686,"timestamp_s":1040,"valid":true},{"bearing_rad":0.06245235726237297,"timestamp_s":1050,"valid":true},{"bearing_rad":0.09659411758184433,"timestamp_s":1060,"valid":true},{"bearing_rad":0.05827644094824791,"timestamp_s":1070,"valid":true},{"bearing_rad":0.05699284374713898,"timestamp_s":1080,"valid":true},{"bearing_rad":0.079716257750988,"timestamp_s":1090,"valid":true},{"bearing_rad":6.27664041519165,"timestamp_s":1100,"valid":true},{"bearing_rad":0.08928501605987549,"timestamp_s":1110,"valid":true},{"bearing_rad":0.05191955342888832,"timestamp_s":1120,"valid":true},{"bearing_rad":0.040906764566898346,"timestamp_s":1130,"valid":true},{"bearing_rad":0.07411102205514908,"timestamp_s":1140,"valid":true},{"bearing_rad":0.07693947851657867,"timestamp_s":1150,"valid":true},{"bearing_rad":0.07902813702821732,"timestamp_s":1160,"valid":true},{"bearing_rad":0.06636165827512741,"timestamp_s":1170,"valid":true},{"bearing_rad":0.0027758220676332712,"timestamp_s":1180,"valid":true},{"bearing_rad":0.12958376109600067,"timestamp_s":1190,"valid":true},{"bearing_rad":0.1077728122472763,"timestamp_s":1200,"valid":true},{"bearing_rad":0.046574659645557404,"timestamp_s":1210,"valid":true},{"bearing_rad":0.06341735273599625,"timestamp_s":1220,"valid":true},{"bearing_rad":0.0646064430475235,"timestamp_s":1230,"valid":true},{"bearing_rad":0.07916583865880966,"timestamp_s":1240,"valid":true},{"bearing_rad":0.07559680193662643,"timestamp_s":1250,"valid":true},{"bearing_rad":0.08714760839939117,"timestamp_s":1260,"valid":true},{"bearing_rad":0.030894936993718147,"timestamp_s":1270,"valid":true},{"bearing_rad":0.06951107084751129,"timestamp_s":1280,"valid":true},{"bearing_rad":0.048993512988090515,"timestamp_s":1290,"valid":true},{"bearing_rad":0.0934167131781578,"timestamp_s":1300,"valid":true},{"bearing_rad":0.08700275421142578,"timestamp_s":1310,"valid":true},{"bearing_rad":0.11747071892023087,"timestamp_s":1320,"valid":true},{"bearing_rad":0.04383845254778862,"timestamp_s":1330,"valid":true},{"bearing_rad":0.1259775459766388,"timestamp_s":1340,"valid":true},{"bearing_rad":0.10197585821151733,"timestamp_s":1350,"valid":true},{"bearing_rad":0.04319304972887039,"timestamp_s":1360,"valid":true},{"bearing_rad":0.1367194503545761,"timestamp_s":1370,"valid":true},{"bearing_rad":0,"timestamp_s":1380,"valid":false}],"ownship":{"id":"ownship","states":[{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":0,"x_m":0,"y_m":0},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":10,"x_m":31.578506469726562,"y_m":-47.042579650878906},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":20,"x_m":63.157012939453125,"y_m":-94.08515930175781},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":30,"x_m":94.73551940917969,"y_m":-141.1277313232422},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":40,"x_m":126.31402587890625,"y_m":-188.17031860351562},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":50,"x_m":157.8925323486328,"y_m":-235.212890625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":60,"x_m":189.47103881835938,"y_m":-282.2554626464844},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":70,"x_m":221.04954528808594,"y_m":-329.2980651855469},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":80,"x_m":252.6280517578125,"y_m":-376.34063720703125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":90,"x_m":284.2065734863281,"y_m":-423.3832092285156},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":100,"x_m":315.7850646972656,"y_m":-470.42578125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":110,"x_m":347.36358642578125,"y_m":-517.4683837890625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":120,"x_m":378.94207763671875,"y_m":-564.5109252929688},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":130,"x_m":410.5205993652344,"y_m":-611.5535278320312},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":140,"x_m":442.0990905761719,"y_m":-658.5961303710938},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":150,"x_m":473.6776123046875,"y_m":-705.638671875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":160,"x_m":505.256103515625,"y_m":-752.6812744140625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":170,"x_m":536.8345947265625,"y_m":-799.7238159179688},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":180,"x_m":568.4131469726562,"y_m":-846.7664184570312},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":190,"x_m":599.9916381835938,"y_m":-893.8090209960938},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":200,"x_m":631.5701293945312,"y_m":-940.8515625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":210,"x_m":663.1486206054688,"y_m":-987.8941650390625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":220,"x_m":694.7271728515625,"y_m":-1034.936767578125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":230,"x_m":726.3056640625,"y_m":-1081.9793701171875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":240,"x_m":757.8841552734375,"y_m":-1129.0218505859375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":250,"x_m":789.462646484375,"y_m":-1176.064453125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":260,"x_m":821.0411987304688,"y_m":-1223.1070556640625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":270,"x_m":852.6196899414062,"y_m":-1270.149658203125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":280,"x_m":884.1981811523438,"y_m":-1317.1922607421875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":290,"x_m":915.7766723632812,"y_m":-1364.2347412109375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":300,"x_m":947.355224609375,"y_m":-1411.27734375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":310,"x_m":978.9337158203125,"y_m":-1458.3199462890625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":320,"x_m":1010.51220703125,"y_m":-1505.362548828125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":330,"x_m":1042.0906982421875,"y_m":-1552.4051513671875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":340,"x_m":1073.669189453125,"y_m":-1599.4476318359375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":350,"x_m":1105.2476806640625,"y_m":-1646.490234375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":360,"x_m":1136.8262939453125,"y_m":-1693.5328369140625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":370,"x_m":1168.40478515625,"y_m":-1740.575439453125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":380,"x_m":1199.9832763671875,"y_m":-1787.6180419921875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":390,"x_m":1231.561767578125,"y_m":-1834.66064453125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":400,"x_m":1263.1402587890625,"y_m":-1881.703125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":410,"x_m":1294.71875,"y_m":-1928.7457275390625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":420,"x_m":1326.2972412109375,"y_m":-1975.788330078125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":430,"x_m":1357.875732421875,"y_m":-2022.8309326171875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":440,"x_m":1389.454345703125,"y_m":-2069.87353515625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":450,"x_m":1421.0328369140625,"y_m":-2116.916015625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":460,"x_m":1452.611328125,"y_m":-2163.958740234375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":470,"x_m":1484.1898193359375,"y_m":-2211.001220703125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":480,"x_m":1515.768310546875,"y_m":-2258.043701171875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":490,"x_m":1547.3468017578125,"y_m":-2305.08642578125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":500,"x_m":1578.92529296875,"y_m":-2352.12890625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":510,"x_m":1610.5037841796875,"y_m":-2399.171630859375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":520,"x_m":1642.0823974609375,"y_m":-2446.214111328125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":530,"x_m":1673.660888671875,"y_m":-2493.256591796875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":540,"x_m":1705.2393798828125,"y_m":-2540.29931640625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":550,"x_m":1736.81787109375,"y_m":-2587.341796875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":560,"x_m":1768.3963623046875,"y_m":-2634.384521484375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":570,"x_m":1799.974853515625,"y_m":-2681.427001953125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":580,"x_m":1831.5533447265625,"y_m":-2728.469482421875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":590,"x_m":1863.1319580078125,"y_m":-2775.51220703125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":600,"x_m":1894.71044921875,"y_m":-2822.5546875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":610,"x_m":1926.2889404296875,"y_m":-2869.597412109375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":620,"x_m":1957.867431640625,"y_m":-2916.639892578125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":630,"x_m":1989.4459228515625,"y_m":-2963.682373046875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":640,"x_m":2021.0244140625,"y_m":-3010.72509765625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":650,"x_m":2052.60302734375,"y_m":-3057.767578125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":660,"x_m":2084.181396484375,"y_m":-3104.810302734375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":670,"x_m":2115.760009765625,"y_m":-3151.852783203125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":680,"x_m":2069.2705078125,"y_m":-3184.240234375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":690,"x_m":2022.78125,"y_m":-3216.627685546875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":700,"x_m":1976.2918701171875,"y_m":-3249.01513671875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":710,"x_m":1929.8026123046875,"y_m":-3281.402587890625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":720,"x_m":1883.313232421875,"y_m":-3313.7900390625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":730,"x_m":1836.8238525390625,"y_m":-3346.177490234375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":740,"x_m":1790.33447265625,"y_m":-3378.56494140625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":750,"x_m":1743.84521484375,"y_m":-3410.952392578125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":760,"x_m":1697.3558349609375,"y_m":-3443.33984375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":770,"x_m":1650.866455078125,"y_m":-3475.727294921875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":780,"x_m":1604.3770751953125,"y_m":-3508.11474609375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":790,"x_m":1557.8878173828125,"y_m":-3540.502197265625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":800,"x_m":1511.3984375,"y_m":-3572.8896484375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":810,"x_m":1464.9090576171875,"y_m":-3605.277099609375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":820,"x_m":1418.419677734375,"y_m":-3637.66455078125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":830,"x_m":1371.930419921875,"y_m":-3670.052001953125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":840,"x_m":1325.4410400390625,"y_m":-3702.439453125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":850,"x_m":1278.95166015625,"y_m":-3734.826904296875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":860,"x_m":1232.4622802734375,"y_m":-3767.21435546875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":870,"x_m":1185.9730224609375,"y_m":-3799.601806640625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":880,"x_m":1139.483642578125,"y_m":-3831.9892578125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":890,"x_m":1092.9942626953125,"y_m":-3864.376708984375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":900,"x_m":1046.5048828125,"y_m":-3896.76416015625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":910,"x_m":1000.0155639648438,"y_m":-3929.151611328125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":920,"x_m":953.5262451171875,"y_m":-3961.5390625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":930,"x_m":907.036865234375,"y_m":-3993.926513671875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":940,"x_m":860.5475463867188,"y_m":-4026.31396484375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":950,"x_m":814.0581665039062,"y_m":-4058.701416015625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":960,"x_m":767.56884765625,"y_m":-4091.0888671875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":970,"x_m":721.0794677734375,"y_m":-4123.47607421875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":980,"x_m":674.5901489257812,"y_m":-4155.86376953125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":990,"x_m":628.1007690429688,"y_m":-4188.2509765625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1000,"x_m":581.6114501953125,"y_m":-4220.638671875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1010,"x_m":535.1220703125,"y_m":-4253.02587890625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1020,"x_m":488.63275146484375,"y_m":-4285.41357421875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1030,"x_m":442.1434020996094,"y_m":-4317.80078125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1040,"x_m":395.654052734375,"y_m":-4350.1884765625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1050,"x_m":349.1647033691406,"y_m":-4382.57568359375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1060,"x_m":302.67535400390625,"y_m":-4414.96337890625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1070,"x_m":256.1860046386719,"y_m":-4447.3505859375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1080,"x_m":209.6966552734375,"y_m":-4479.73828125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1090,"x_m":163.20730590820312,"y_m":-4512.12548828125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1100,"x_m":116.71796417236328,"y_m":-4544.51318359375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1110,"x_m":70.2286148071289,"y_m":-4576.900390625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1120,"x_m":23.7392635345459,"y_m":-4609.2880859375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1130,"x_m":-22.750085830688477,"y_m":-4641.67529296875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1140,"x_m":-69.23943328857422,"y_m":-4674.06298828125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1150,"x_m":-115.7287826538086,"y_m":-4706.4501953125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1160,"x_m":-162.21812438964844,"y_m":-4738.837890625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1170,"x_m":-208.7074737548828,"y_m":-4771.22509765625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1180,"x_m":-255.1968231201172,"y_m":-4803.61279296875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1190,"x_m":-301.6861877441406,"y_m":-4836},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1200,"x_m":-348.175537109375,"y_m":-4868.3876953125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1210,"x_m":-394.6648864746094,"y_m":-4900.77490234375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1220,"x_m":-441.15423583984375,"y_m":-4933.16259765625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1230,"x_m":-487.6435852050781,"y_m":-4965.5498046875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1240,"x_m":-534.1329345703125,"y_m":-4997.9375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1250,"x_m":-580.6222534179688,"y_m":-5030.32470703125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1260,"x_m":-627.1116333007812,"y_m":-5062.71240234375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1270,"x_m":-673.6009521484375,"y_m":-5095.099609375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1280,"x_m":-720.09033203125,"y_m":-5127.4873046875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1290,"x_m":-766.5796508789062,"y_m":-5159.87451171875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1300,"x_m":-813.0690307617188,"y_m":-5192.26220703125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1310,"x_m":-859.558349609375,"y_m":-5224.6494140625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1320,"x_m":-906.0477294921875,"y_m":-5257.037109375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1330,"x_m":-952.5370483398438,"y_m":-5289.42431640625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1340,"x_m":-999.0264282226562,"y_m":-5321.81201171875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1350,"x_m":-1045.5157470703125,"y_m":-5354.19921875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1360,"x_m":-1092.005126953125,"y_m":-5386.5869140625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1370,"x_m":-1138.4945068359375,"y_m":-5418.97412109375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1380,"x_m":-1184.9837646484375,"y_m":-5451.36181640625}]},"record":"scenario","target":{"id":"target","states":[{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":0,"x_m":2686.102294921875,"y_m":-4423.81787109375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":10,"x_m":2667.721923828125,"y_m":-4335.23876953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":20,"x_m":2649.341552734375,"y_m":-4246.66015625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":30,"x_m":2630.961181640625,"y_m":-4158.0810546875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":40,"x_m":2612.580810546875,"y_m":-4069.50244140625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":50,"x_m":2594.200439453125,"y_m":-3980.92333984375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":60,"x_m":2575.820068359375,"y_m":-3892.344482421875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":70,"x_m":2557.439697265625,"y_m":-3803.765625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":80,"x_m":2539.059326171875,"y_m":-3715.186767578125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":90,"x_m":2520.678955078125,"y_m":-3626.60791015625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":100,"x_m":2502.298583984375,"y_m":-3538.029052734375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":110,"x_m":2483.918212890625,"y_m":-3449.4501953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":120,"x_m":2465.537841796875,"y_m":-3360.871337890625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":130,"x_m":2447.15771484375,"y_m":-3272.29248046875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":140,"x_m":2428.77734375,"y_m":-3183.71337890625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":150,"x_m":2410.39697265625,"y_m":-3095.134521484375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":160,"x_m":2392.0166015625,"y_m":-3006.5556640625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":170,"x_m":2373.63623046875,"y_m":-2917.976806640625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":180,"x_m":2355.255859375,"y_m":-2829.39794921875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":190,"x_m":2336.87548828125,"y_m":-2740.819091796875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":200,"x_m":2318.4951171875,"y_m":-2652.240234375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":210,"x_m":2300.11474609375,"y_m":-2563.661376953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":220,"x_m":2281.734375,"y_m":-2475.08251953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":230,"x_m":2263.35400390625,"y_m":-2386.50341796875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":240,"x_m":2244.9736328125,"y_m":-2297.924560546875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":250,"x_m":2226.59326171875,"y_m":-2209.345703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":260,"x_m":2208.212890625,"y_m":-2120.766845703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":270,"x_m":2189.83251953125,"y_m":-2032.18798828125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":280,"x_m":2171.4521484375,"y_m":-1943.609130859375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":290,"x_m":2153.07177734375,"y_m":-1855.0302734375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":300,"x_m":2134.69140625,"y_m":-1766.4512939453125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":310,"x_m":2116.31103515625,"y_m":-1677.8724365234375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":320,"x_m":2097.9306640625,"y_m":-1589.2935791015625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":330,"x_m":2079.55029296875,"y_m":-1500.7147216796875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":340,"x_m":2061.169921875,"y_m":-1412.1358642578125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":350,"x_m":2042.7896728515625,"y_m":-1323.556884765625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":360,"x_m":2024.4093017578125,"y_m":-1234.97802734375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":370,"x_m":2006.0289306640625,"y_m":-1146.399169921875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":380,"x_m":1987.6485595703125,"y_m":-1057.8203125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":390,"x_m":1969.2681884765625,"y_m":-969.2413940429688},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":400,"x_m":1950.8878173828125,"y_m":-880.6624755859375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":410,"x_m":1932.5074462890625,"y_m":-792.0836181640625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":420,"x_m":1914.127197265625,"y_m":-703.5047607421875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":430,"x_m":1895.746826171875,"y_m":-614.9258422851562},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":440,"x_m":1877.366455078125,"y_m":-526.3469848632812},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":450,"x_m":1858.986083984375,"y_m":-437.7680969238281},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":460,"x_m":1840.605712890625,"y_m":-349.189208984375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":470,"x_m":1822.225341796875,"y_m":-260.6103210449219},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":480,"x_m":1803.844970703125,"y_m":-172.03143310546875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":490,"x_m":1785.464599609375,"y_m":-83.45254516601562},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":500,"x_m":1767.084228515625,"y_m":5.126338005065918},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":510,"x_m":1748.703857421875,"y_m":93.7052230834961},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":520,"x_m":1730.323486328125,"y_m":182.2841033935547},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":530,"x_m":1711.9432373046875,"y_m":270.86297607421875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":540,"x_m":1693.5628662109375,"y_m":359.4418640136719},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":550,"x_m":1675.1824951171875,"y_m":448.020751953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":560,"x_m":1656.8021240234375,"y_m":536.5996704101562},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":570,"x_m":1638.4217529296875,"y_m":625.1785278320312},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":580,"x_m":1620.0413818359375,"y_m":713.7573852539062},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":590,"x_m":1601.6610107421875,"y_m":802.3363037109375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":600,"x_m":1583.2806396484375,"y_m":890.9151611328125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":610,"x_m":1564.9002685546875,"y_m":979.4940795898438},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":620,"x_m":1546.5198974609375,"y_m":1068.072998046875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":630,"x_m":1528.1395263671875,"y_m":1156.65185546875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":640,"x_m":1509.75927734375,"y_m":1245.230712890625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":650,"x_m":1491.37890625,"y_m":1333.8095703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":660,"x_m":1472.99853515625,"y_m":1422.388427734375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":670,"x_m":1454.6181640625,"y_m":1510.9674072265625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":680,"x_m":1436.23779296875,"y_m":1599.5462646484375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":690,"x_m":1417.857421875,"y_m":1688.1251220703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":700,"x_m":1399.47705078125,"y_m":1776.7039794921875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":710,"x_m":1381.0966796875,"y_m":1865.282958984375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":720,"x_m":1362.71630859375,"y_m":1953.86181640625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":730,"x_m":1344.3359375,"y_m":2042.440673828125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":740,"x_m":1325.95556640625,"y_m":2131.01953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":750,"x_m":1307.5751953125,"y_m":2219.598388671875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":760,"x_m":1289.1949462890625,"y_m":2308.17724609375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":770,"x_m":1270.8145751953125,"y_m":2396.756103515625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":780,"x_m":1252.4342041015625,"y_m":2485.335205078125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":790,"x_m":1234.0538330078125,"y_m":2573.9140625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":800,"x_m":1215.6734619140625,"y_m":2662.492919921875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":810,"x_m":1197.2930908203125,"y_m":2751.07177734375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":820,"x_m":1178.9127197265625,"y_m":2839.650634765625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":830,"x_m":1160.5323486328125,"y_m":2928.2294921875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":840,"x_m":1142.1519775390625,"y_m":3016.808349609375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":850,"x_m":1123.7716064453125,"y_m":3105.38720703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":860,"x_m":1105.3912353515625,"y_m":3193.966064453125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":870,"x_m":1087.010986328125,"y_m":3282.545166015625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":880,"x_m":1068.630615234375,"y_m":3371.1240234375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":890,"x_m":1050.250244140625,"y_m":3459.702880859375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":900,"x_m":1031.869873046875,"y_m":3548.28173828125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":910,"x_m":1013.489501953125,"y_m":3636.860595703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":920,"x_m":995.109130859375,"y_m":3725.439453125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":930,"x_m":976.728759765625,"y_m":3814.018310546875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":940,"x_m":958.348388671875,"y_m":3902.59716796875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":950,"x_m":939.968017578125,"y_m":3991.176025390625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":960,"x_m":921.5877075195312,"y_m":4079.755126953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":970,"x_m":903.2073364257812,"y_m":4168.333984375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":980,"x_m":884.8269653320312,"y_m":4256.91259765625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":990,"x_m":866.4465942382812,"y_m":4345.49169921875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1000,"x_m":848.0662231445312,"y_m":4434.0703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1010,"x_m":829.6859130859375,"y_m":4522.6494140625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1020,"x_m":811.3055419921875,"y_m":4611.228515625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1030,"x_m":792.9251708984375,"y_m":4699.80712890625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1040,"x_m":774.5447998046875,"y_m":4788.38623046875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1050,"x_m":756.1644287109375,"y_m":4876.96484375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1060,"x_m":737.7840576171875,"y_m":4965.5439453125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1070,"x_m":719.4037475585938,"y_m":5054.12255859375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1080,"x_m":701.0233764648438,"y_m":5142.70166015625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1090,"x_m":682.6430053710938,"y_m":5231.2802734375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1100,"x_m":664.2626342773438,"y_m":5319.859375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1110,"x_m":645.8822631835938,"y_m":5408.4384765625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1120,"x_m":627.5018920898438,"y_m":5497.01708984375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1130,"x_m":609.12158203125,"y_m":5585.59619140625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1140,"x_m":590.7412109375,"y_m":5674.1748046875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1150,"x_m":572.36083984375,"y_m":5762.75390625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1160,"x_m":553.98046875,"y_m":5851.33251953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1170,"x_m":535.60009765625,"y_m":5939.91162109375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1180,"x_m":517.2197875976562,"y_m":6028.490234375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1190,"x_m":498.8393859863281,"y_m":6117.0693359375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1200,"x_m":480.45904541015625,"y_m":6205.6484375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1210,"x_m":462.07867431640625,"y_m":6294.22705078125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1220,"x_m":443.69830322265625,"y_m":6382.80615234375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1230,"x_m":425.3179626464844,"y_m":6471.384765625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1240,"x_m":406.9375915527344,"y_m":6559.9638671875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1250,"x_m":388.5572204589844,"y_m":6648.54248046875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1260,"x_m":370.1768798828125,"y_m":6737.12158203125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1270,"x_m":351.7965087890625,"y_m":6825.7001953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1280,"x_m":333.4161682128906,"y_m":6914.279296875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1290,"x_m":315.0357971191406,"y_m":7002.8583984375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1300,"x_m":296.6554260253906,"y_m":7091.43701171875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1310,"x_m":278.27508544921875,"y_m":7180.01611328125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1320,"x_m":259.89471435546875,"y_m":7268.5947265625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1330,"x_m":241.5143585205078,"y_m":7357.173828125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1340,"x_m":223.1339874267578,"y_m":7445.75244140625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1350,"x_m":204.75363159179688,"y_m":7534.33154296875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1360,"x_m":186.37327575683594,"y_m":7622.91015625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1370,"x_m":167.99290466308594,"y_m":7711.4892578125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1380,"x_m":149.612548828125,"y_m":7800.068359375}]}} diff --git a/packages/demos/src/core/series.ts b/packages/demos/src/core/series.ts index 77d8f2c3..ce247f5e 100644 --- a/packages/demos/src/core/series.ts +++ b/packages/demos/src/core/series.ts @@ -65,17 +65,34 @@ export function seriesOf(s: Scenario): Series { return out; } -/** Ownship's legs as sample index ranges: leg 1, the turn (empty for an instantaneous zig), leg 2. */ +/** + * Ownship's legs as sample index ranges: leg 1, the turn (empty for an instantaneous zig), leg 2, + * and with a second zig (generator 0.3.0) the second turn and leg 3; leg 2 then ends where the + * second turn starts. The second-zig keys are absent with one zig. + */ export interface Legs { leg1: [number, number]; turn: [number, number]; leg2: [number, number]; + turn2?: [number, number]; + leg3?: [number, number]; } export function legsOf(s: Scenario): Legs { const m = s.metadata; const turnStart = m.turn_start_index ?? m.zig_index; - return { leg1: [0, turnStart], turn: [turnStart, m.zig_index], leg2: [m.zig_index, m.samples] }; + const z2 = m.zig2_index; + if (z2 == null) { + return { leg1: [0, turnStart], turn: [turnStart, m.zig_index], leg2: [m.zig_index, m.samples] }; + } + const t2 = m.turn2_start_index ?? z2; + return { + leg1: [0, turnStart], + turn: [turnStart, m.zig_index], + leg2: [m.zig_index, t2], + turn2: [t2, z2], + leg3: [z2, m.samples], + }; } /** Sample index nearest a time, within the window. */ diff --git a/packages/demos/src/viewer/EngagementViewer.tsx b/packages/demos/src/viewer/EngagementViewer.tsx index 3013ccfe..9d21c183 100644 --- a/packages/demos/src/viewer/EngagementViewer.tsx +++ b/packages/demos/src/viewer/EngagementViewer.tsx @@ -237,6 +237,8 @@ export function EngagementViewer(props: EngagementViewerProps): JSX.Element { return out; }; const markers: Marker[] = [{ t: zigT, label: "zig" }]; + const zig2 = s.metadata.zig2_index; + if (zig2 != null) markers.push({ t: zig2 * dt, label: "zig 2" }); if (v.mode !== "truth") { for (const [m, label] of [ [300, "zig+5"], @@ -245,8 +247,9 @@ export function EngagementViewer(props: EngagementViewerProps): JSX.Element { if (zigT + m <= windowS) markers.push({ t: zigT + m, label }); } } - const turnShade: [number, number] | null = - view.legs.turn[1] > view.legs.turn[0] ? [view.legs.turn[0] * dt, view.legs.turn[1] * dt] : null; + const turnShade: [number, number][] = [view.legs.turn, view.legs.turn2] + .filter((t): t is [number, number] => t !== undefined && t[1] > t[0]) + .map(([a, b]) => [a * dt, b * dt]); const common = { t: ser.t, cursorT: tCur, diff --git a/packages/demos/src/viewer/Panels.tsx b/packages/demos/src/viewer/Panels.tsx index 951051ad..3a0c16d3 100644 --- a/packages/demos/src/viewer/Panels.tsx +++ b/packages/demos/src/viewer/Panels.tsx @@ -20,6 +20,15 @@ const fx = (v: number | null | undefined, digits: number): string => ? "∞" : v.toFixed(digits); +/** Which leg (or turn) ownship is on at sample i. */ +function legName(s: Scenario, i: number): string { + const m = s.metadata; + if (i < (m.turn_start_index ?? m.zig_index)) return "leg 1"; + if (i < m.zig_index) return "turning"; + if (m.zig2_index == null || i < (m.turn2_start_index ?? m.zig2_index)) return "leg 2"; + return i < m.zig2_index ? "second turn" : "leg 3"; +} + export interface Estimate { label: string; color: string; @@ -140,13 +149,8 @@ export function Readout({ s, cursor, estimates }: ReadoutProps): JSX.Element {

Ownship ({(o.x_m / 1000).toFixed(2)}, {(o.y_m / 1000).toFixed(2)}) km, course{" "} - {deg(o.course_rad).toFixed(0)}°, speed {mpsToKt(o.speed_mps).toFixed(1)} kt - {cursor < (s.metadata.turn_start_index ?? zig) - ? ", leg 1" - : cursor < zig - ? ", turning" - : ", leg 2"} - . + {deg(o.course_rad).toFixed(0)}°, speed {mpsToKt(o.speed_mps).toFixed(1)} kt ,{" "} + {legName(s, cursor)}.

{cursor <= zig && (

@@ -226,6 +230,14 @@ export function Metadata({ s, source, generatorVersion }: MetadataProps): JSX.El (rate > 0 ? `, turn rate ${radToDeg(rate).toFixed(2)}°/s` : ", instantaneous") + `, zig index ${String(m.zig_index)}`, ], + ...(p.zig2_angle_rad == null || m.zig2_index == null + ? [] + : [ + [ + "zig 2", + `${radToDeg(p.zig2_angle_rad).toFixed(0)}°, ${String(p.zig2_delay_s ?? 0)} s after leg 2 starts, leg 3 from ${((m.zig2_index * p.dt_s) / 60).toFixed(1)} min`, + ] as [string, string], + ]), [ "target", `${(p.target_range0_m / 1000).toFixed(1)} km on ${deg(p.target_bearing0_rad).toFixed(0)}°, course ${deg(p.target_course_rad).toFixed(0)}°, ${mpsToKt(p.target_speed_mps).toFixed(1)} kt`, diff --git a/packages/demos/src/viewer/PlanView.tsx b/packages/demos/src/viewer/PlanView.tsx index 2a36babd..d9af5700 100644 --- a/packages/demos/src/viewer/PlanView.tsx +++ b/packages/demos/src/viewer/PlanView.tsx @@ -96,7 +96,7 @@ export function PlanView(props: PlanViewProps): JSX.Element { const tgt = s.target.states; const n = s.metadata.samples; const zig = s.metadata.zig_index; - const { leg1, turn, leg2 } = view.legs; + const { leg1, turn, leg2, turn2, leg3 } = view.legs; const ownAt = (i: number): [number, number] | null => { const o = own[i]; return o === undefined ? null : pt(o.x_m, o.y_m); @@ -265,7 +265,15 @@ export function PlanView(props: PlanViewProps): JSX.Element { } } - const zigP = ownAt(zig); + // Zig markers at the first sample on each new leg. + const zigMarks: [string, [number, number]][] = []; + for (const [label, k] of [ + ["zig", zig], + ["zig 2", s.metadata.zig2_index], + ] as const) { + const q = k == null ? null : ownAt(k); + if (q !== null) zigMarks.push([label, q]); + } const ownStart = ownAt(0); const tgtStart = tgt[0] === undefined ? null : pt(tgt[0].x_m, tgt[0].y_m); const tgtPath = pathOf(tgt.map((p) => pt(p.x_m, p.y_m))); @@ -376,7 +384,13 @@ export function PlanView(props: PlanViewProps): JSX.Element { - + + {turn2 !== undefined && ( + + )} + {leg3 !== undefined && ( + + )} {estimates} {ownStart !== null && ( )} - {zigP !== null && ( - + {zigMarks.map(([label, q]) => ( + - - zig + + {label} - )} + ))} {oc !== undefined && heading(oc.x_m, oc.y_m, oc.course_rad, C.own, "own")} {tc !== undefined && heading(tc.x_m, tc.y_m, tc.course_rad, C.target, "tgt")} diff --git a/packages/demos/src/viewer/TimePlot.tsx b/packages/demos/src/viewer/TimePlot.tsx index 460cba53..d96fbc2f 100644 --- a/packages/demos/src/viewer/TimePlot.tsx +++ b/packages/demos/src/viewer/TimePlot.tsx @@ -27,7 +27,8 @@ export interface TimePlotProps { cursorT: number; windowS: number; markers?: readonly Marker[]; - shade?: readonly [number, number] | null; + /** Shaded spans (ownship turns), seconds. */ + shade?: readonly (readonly [number, number])[]; /** Grey band from 0 to this time, labelled unobservable (estimates before the zig). */ unobservableUntil?: number | null; /** Fixed y range (angles); otherwise from the data. */ @@ -172,16 +173,17 @@ export function TimePlot(props: TimePlotProps): JSX.Element { )} - {props.shade != null && ( + {(props.shade ?? []).map(([a, b]) => ( - )} + ))} {ticks.map((v) => ( diff --git a/packages/demos/test/core.test.ts b/packages/demos/test/core.test.ts index 03961c91..5a5f3d0b 100644 --- a/packages/demos/test/core.test.ts +++ b/packages/demos/test/core.test.ts @@ -21,7 +21,7 @@ import { type ViewerCatalogue, viewNdjson, } from "../src/index.js"; -import { I1_TEST, I2_TEST } from "./helpers.js"; +import { I1_TEST, I2_TEST, ranges } from "./helpers.js"; const MEANS = { log_range_km: 2, course_sin: 0, course_cos: 1, speed_mps: 5.9, n: 1 }; @@ -145,3 +145,20 @@ describe("view state", () => { }); }); }); + +describe("three legs (generator 0.3.0)", () => { + it("legs cover the window contiguously, with the second turn and leg 3", () => { + const src = { ...I2_TEST, id: "i3", ranges: ranges("iter3-ranges.json") }; + for (const i of [90000, 90001, 90002]) { + const s = scenarioAt(src, i); + const l = legsOf(s); + if (l.turn2 === undefined || l.leg3 === undefined) throw new Error("no second zig"); + expect(l.leg1[1]).toBe(l.turn[0]); + expect(l.turn[1]).toBe(l.leg2[0]); + expect(l.leg2[1]).toBe(l.turn2[0]); + expect(l.turn2[1]).toBe(l.leg3[0]); + expect(l.leg3[1]).toBe(s.metadata.samples); + expect(l.turn2[1]).toBeGreaterThan(l.turn2[0]); + } + }); +}); diff --git a/packages/features/test/golden/seed1-2.features.bin b/packages/features/test/golden/seed1-2.features.bin index ef3a65e92e6d44acd336be723c8507c7ab813dc0..ed0917db221bc7be4aa02f080b5eebe94b40962a 100644 GIT binary patch delta 24 fcmaFT$@HX?X+tR!qw(f4rgkP~OFe^4tWW9zb9D%! delta 24 fcmaFT$@HX?X+tR!qtWIvrgkP~3q7MvtWW9zb7}~p diff --git a/packages/features/test/golden/seed1-4.derived.features.bin b/packages/features/test/golden/seed1-4.derived.features.bin index a04930ffd366c225ae0a4046edf1179ee1464259..958d147b186a6f402d2681bf9f354e02244978b6 100644 GIT binary patch delta 29 lcmccdnB~S}mJJV?7>zeSVrpk%w$w9d&SBl2!^)VW0|38<3bgzbRVrpk%w$L+b&SBl2!^)VW0|38!3bg { ).not.toBeNull(); }); - it("the committed iteration-2 G0 re-run is the latest, and needs its panel review", () => { - // Round-1 fixes (24bbdab) touched packages/data/src, so G0 was re-run there (T019-T020). + it("generator 0.3.0 does not accept the iteration-2 G0 (0.2.0): G0 must be re-run (spec 007)", () => { + // The latest committed G0 (20261013-g0-i2-r2) verified generator 0.2.0. Generator 0.3.0 adds + // the second zig and the across-LOS rule, so training refuses until a G0 on 0.3.0 passes its + // panel; this test moves to that experiment when it is committed. const dir = new URL("../../../experiments", import.meta.url).pathname; - const want = { id: "20261013-g0-i2-r2", commit: "24bbdab5d79f3c872f88ac6494c2d73068016225" }; - expect(g0Accepted(dir, { requireReview: false })).toEqual(want); - const verdict = JSON.parse(readFileSync(join(dir, want.id, "verdict.json"), "utf8")) as { - decided_by: string; - }; - const reviewed = - existsSync(join(dir, want.id, "review.md")) && - /^agent review panel, rounds? \d+(-\d+)? \(no rejections/.test(verdict.decided_by); - expect(g0Accepted(dir)).toEqual(reviewed ? want : null); + const latest = JSON.parse( + readFileSync(join(dir, "20261013-g0-i2-r2", "manifest.json"), "utf8"), + ) as { generator_version: string }; + expect(latest.generator_version).toBe("0.2.0"); + expect(g0Accepted(dir, { requireReview: false })).toBeNull(); + expect(g0Accepted(dir)).toBeNull(); }); it("returns the accepted experiment's id and commit", () => { diff --git a/packages/sim/package.json b/packages/sim/package.json index 4de91c9f..18d87a30 100644 --- a/packages/sim/package.json +++ b/packages/sim/package.json @@ -1,6 +1,6 @@ { "name": "@tma/sim", - "version": "0.2.0", + "version": "0.3.0", "private": true, "type": "module", "exports": { diff --git a/packages/sim/src/scenario.ts b/packages/sim/src/scenario.ts index 53350bbc..ca2f0561 100644 --- a/packages/sim/src/scenario.ts +++ b/packages/sim/src/scenario.ts @@ -1,5 +1,7 @@ -// Scenario generation (SRD §2–3): two-leg ownship, constant-velocity target, noisy bearings. -// The zig is instantaneous, or a constant-rate turn when the ranges give a turn rate (spec 003 US6). +// Scenario generation (SRD §2–3): two- or three-leg ownship, constant-velocity target, noisy +// bearings. The zig is instantaneous, or a constant-rate turn when the ranges give a turn rate +// (spec 003 US6). Generator 0.3.0 (spec 007) adds an optional second zig and the across-the-line- +// of-sight rule; ranges without them reproduce 0.2.0's scenarios byte for byte. // Pure functions: every output is determined by (datasetSeed, index, ranges). import type { FloatRange, @@ -21,7 +23,7 @@ import { uniformInt, } from "./prng.js"; -export const GENERATOR_VERSION = "0.2.0"; +export const GENERATOR_VERSION = "0.3.0"; const MAX_REDRAWS = 1000; const draw = (s: RngState, r: FloatRange): number => uniform(s, r.min, r.max); @@ -50,7 +52,7 @@ export function drawParameters(s: RngState, ranges: ParameterRanges): ScenarioPa // exactly as generator 0.1.0 did and reproduce its scenarios byte for byte. const turnRange = ranges.ownship_turn_rate_rad_s; const turn = turnRange == null ? {} : { ownship_turn_rate_rad_s: f32(draw(s, turnRange)) }; - return { + const base: ScenarioParameters = { window_s: f32(samples * dt), dt_s: f32(dt), ownship_speed_mps: f32(ownshipSpeed), @@ -65,6 +67,100 @@ export function drawParameters(s: RngState, ranges: ParameterRanges): ScenarioPa dropout_fraction: f32(dropout), ...turn, }; + return planManoeuvres(s, ranges, base); +} + +/** Angle in (−π, π]. */ +const wrapPi = (a: number): number => Math.atan2(Math.sin(a), Math.cos(a)); + +/** + * The across-the-line-of-sight turn from course `course` at (ox, oy) with the target at (tx, ty): + * the new course puts the target on the other bow at angle off `offset`, by the shorter turn. + */ +export function acrossLosTurn( + ox: number, + oy: number, + course: number, + tx: number, + ty: number, + offset: number, +): number { + const brg = bearingOf(tx - ox, ty - oy); + const rel = wrapPi(brg - course); + const side = rel < 0 ? -1 : 1; + return wrapPi(brg + side * offset - course); +} + +/** + * Generator 0.3.0 draws, after every 0.2.0 draw so older ranges consume the PRNG unchanged: the + * across-LOS offsets (zig_rule across_los) and the second zig (zig2_delay_s). Across-LOS angles + * depend on the geometry at each turn's start, so they are computed from the simulated truth. + */ +function planManoeuvres( + s: RngState, + ranges: ParameterRanges, + base: ScenarioParameters, +): ScenarioParameters { + const across = ranges.zig_rule === "across_los"; + const delayRange = ranges.zig2_delay_s; + if (!across && delayRange == null) return base; + const los = ranges.los_offset_rad; + if (across && los == null) throw new Error("zig_rule across_los needs los_offset_rad"); + const offset1 = across && los != null ? draw(s, los) : 0; + const delay = delayRange == null ? null : f32(draw(s, delayRange)); + let zig2Drawn = 0; + if (delay !== null) { + if (across && los != null) zig2Drawn = draw(s, los); + else { + const abs = draw(s, ranges.zig_angle_abs_rad); + zig2Drawn = (uniform01(s) < 0.5 ? -1 : 1) * abs; + } + } + let p = base; + if (across) { + const k = Math.round(p.zig_fraction * Math.round(p.window_s / p.dt_s)); + const t = k * p.dt_s; + const c0 = p.ownship_course0_rad; + const v = p.ownship_speed_mps; + const tg = targetAt(p, t); + p = { + ...p, + zig_angle_rad: f32( + acrossLosTurn(v * Math.sin(c0) * t, v * Math.cos(c0) * t, c0, tg.x, tg.y, offset1), + ), + }; + } + if (delay === null) return p; + let zig2 = zig2Drawn; + if (across) { + // Geometry at the second turn's start, on the two-leg path (unchanged up to that sample). + const two = simulateTruth(p); + const k2 = two.zigIndex + Math.round(delay / p.dt_s); + if (k2 < two.samples) { + const tg = targetAt(p, k2 * p.dt_s); + zig2 = acrossLosTurn( + two.ownX[k2] ?? 0, + two.ownY[k2] ?? 0, + two.ownCourse[k2] ?? 0, + tg.x, + tg.y, + zig2Drawn, + ); + } + } + return { ...p, zig2_delay_s: delay, zig2_angle_rad: f32(zig2) }; +} + +/** Target position at time t (constant velocity from its start). */ +function targetAt(p: ScenarioParameters, t: number): { x: number; y: number } { + return { + x: + p.target_range0_m * Math.sin(p.target_bearing0_rad) + + p.target_speed_mps * Math.sin(p.target_course_rad) * t, + y: + p.target_range0_m * Math.cos(p.target_bearing0_rad) + + p.target_speed_mps * Math.cos(p.target_course_rad) * t, + }; } /** Truth positions (float64) for ownship and target at each sample. */ @@ -74,6 +170,10 @@ export interface Truth { zigIndex: number; /** First sample after the turn starts; null for an instantaneous zig. */ turnStartIndex: number | null; + /** First sample on leg 3 (second zig); null with one zig. */ + zig2Index: number | null; + /** First sample after the second turn starts; null with one zig or an instantaneous zig. */ + turn2StartIndex: number | null; ownX: Float64Array; ownY: Float64Array; ownCourse: Float64Array; @@ -108,12 +208,12 @@ export function arcPosition( const turnRate = (p: ScenarioParameters): number => p.ownship_turn_rate_rad_s ?? 0; /** - * Ownship two-leg motion and constant-velocity target motion. The zig (or the start of the turn) + * Ownship two-leg motion (generator 0.2.0, unchanged) and constant-velocity target motion. The zig (or the start of the turn) * is at sample k = round(zig_fraction × samples). With a turn rate ω the course changes at ω * towards the zig side for |zig| / ω seconds; leg 2 starts where the arc ends. A turn that does * not complete inside the window gives zigIndex ≥ samples (the caller redraws). */ -export function simulateTruth(p: ScenarioParameters): Truth { +function simulateTwoLegs(p: ScenarioParameters): Truth { const samples = Math.round(p.window_s / p.dt_s); const zigIndex = Math.round(p.zig_fraction * samples); const tZig = zigIndex * p.dt_s; @@ -130,6 +230,8 @@ export function simulateTruth(p: ScenarioParameters): Truth { samples, zigIndex, turnStartIndex: null, + zig2Index: null, + turn2StartIndex: null, ownX: new Float64Array(samples), ownY: new Float64Array(samples), ownCourse: new Float64Array(samples), @@ -220,14 +322,69 @@ export function observe(s: RngState, p: ScenarioParameters, truth: Truth): Obser return out; } +/** + * Ownship motion with an optional second zig (generator 0.3.0): the two-leg path up to the + * second turn's start, sample k2 = zig_index + round(zig2_delay_s / dt), then the same + * conventions as the first zig (instantaneous at k2, or an arc at the same turn rate with leg 3 + * from the first sample after it ends). A second turn that does not complete inside the window + * gives zig2Index ≥ samples (the caller redraws). + */ +export function simulateTruth(p: ScenarioParameters): Truth { + const truth = simulateTwoLegs(p); + const zig2 = p.zig2_angle_rad; + const delay = p.zig2_delay_s; + if (zig2 == null || delay == null) return truth; + const dt = p.dt_s; + const v = p.ownship_speed_mps; + const k2 = truth.zigIndex + Math.round(delay / dt); + truth.zig2Index = k2; + if (k2 >= truth.samples) return truth; + const t2 = k2 * dt; + const x2 = truth.ownX[k2] ?? 0; + const y2 = truth.ownY[k2] ?? 0; + const c1 = truth.ownCourse[k2] ?? 0; + const c2 = wrap2pi(c1 + zig2); + const rate = turnRate(p); + if (!(rate > 0)) { + for (let i = k2; i < truth.samples; i++) { + const tau = i * dt - t2; + truth.ownX[i] = x2 + v * Math.sin(c2) * tau; + truth.ownY[i] = y2 + v * Math.cos(c2) * tau; + truth.ownCourse[i] = c2; + } + return truth; + } + const omega = (zig2 < 0 ? -1 : 1) * rate; + const turnS = Math.abs(zig2) / rate; + const end = arcPosition(x2, y2, c1, v, omega, turnS); + let leg3 = k2 + 1; + while ((leg3 - k2) * dt < turnS) leg3++; + truth.turn2StartIndex = k2 + 1; + truth.zig2Index = leg3; + for (let i = k2 + 1; i < truth.samples; i++) { + const tau = i * dt - t2; + if (i < leg3) { + const a = arcPosition(x2, y2, c1, v, omega, tau); + truth.ownX[i] = a.x; + truth.ownY[i] = a.y; + truth.ownCourse[i] = a.course; + } else { + truth.ownX[i] = end.x + v * Math.sin(c2) * (tau - turnS); + truth.ownY[i] = end.y + v * Math.cos(c2) * (tau - turnS); + truth.ownCourse[i] = c2; + } + } + return truth; +} + export interface GeneratedScenario { scenario: Scenario; redraws: number; } /** - * Generate scenario `index` of a dataset. Geometries closer than `min_range_m`, and turns that do - * not complete inside the window, are redrawn. + * Generate scenario `index` of a dataset. Geometries closer than `min_range_m`, and turns (first + * or second) that do not complete inside the window, are redrawn. */ export function generateScenario( datasetSeed: bigint, @@ -240,6 +397,7 @@ export function generateScenario( const p = drawParameters(s, r); const truth = simulateTruth(p); if (truth.turnStartIndex !== null && truth.zigIndex >= truth.samples) continue; + if (truth.zig2Index !== null && truth.zig2Index >= truth.samples) continue; const rg = ranges(truth); if (rg.min < r.min_range_m) continue; const observations = observe(s, p, truth); @@ -271,6 +429,8 @@ export function generateScenario( samples: truth.samples, zig_index: truth.zigIndex, ...(truth.turnStartIndex === null ? {} : { turn_start_index: truth.turnStartIndex }), + ...(truth.zig2Index === null ? {} : { zig2_index: truth.zig2Index }), + ...(truth.turn2StartIndex === null ? {} : { turn2_start_index: truth.turn2StartIndex }), zig_time_s: f32(truth.zigIndex * p.dt_s), mean_range_m: f32(rg.mean), min_range_m: f32(rg.min), diff --git a/packages/sim/test/golden.test.ts b/packages/sim/test/golden.test.ts index b7ec6874..67562819 100644 --- a/packages/sim/test/golden.test.ts +++ b/packages/sim/test/golden.test.ts @@ -6,6 +6,7 @@ import { generateScenario } from "../src/scenario.js"; const GOLDEN = new URL("./golden/seed1-3.ndjson", import.meta.url); const GOLDEN_TURN = new URL("./golden/seed1-3-turn.ndjson", import.meta.url); +const GOLDEN_ZIG2 = new URL("./golden/seed1-3-zig2-across.ndjson", import.meta.url); describe("golden: sim", () => { it("seed 1, scenarios 0–2 are byte-identical to the committed golden file", () => { @@ -31,4 +32,21 @@ describe("golden: sim", () => { expect(existsSync(GOLDEN_TURN), "golden file missing: run with UPDATE_GOLDEN=1").toBe(true); expect(text === readFileSync(GOLDEN_TURN, "utf8")).toBe(true); }); + + it("generator 0.3.0, two zigs across the line of sight: seed 1, scenarios 0–2 are byte-identical", () => { + const ranges = { + ...DEFAULT_RANGES, + ownship_turn_rate_rad_s: { min: degToRad(0.5), max: degToRad(3) }, + zig_rule: "across_los" as const, + los_offset_rad: { min: degToRad(30), max: degToRad(90) }, + zig2_delay_s: { min: 180, max: 300 }, + }; + const text = [0, 1, 2] + .map((i) => JSON.stringify(generateScenario(1n, i, ranges).scenario)) + .join("\n") + .concat("\n"); + if (process.env.UPDATE_GOLDEN === "1") writeFileSync(GOLDEN_ZIG2, text); + expect(existsSync(GOLDEN_ZIG2), "golden file missing: run with UPDATE_GOLDEN=1").toBe(true); + expect(text === readFileSync(GOLDEN_ZIG2, "utf8")).toBe(true); + }); }); diff --git a/packages/sim/test/golden/seed1-3-zig2-across.ndjson b/packages/sim/test/golden/seed1-3-zig2-across.ndjson new file mode 100644 index 00000000..018ec7bb --- /dev/null +++ b/packages/sim/test/golden/seed1-3-zig2-across.ndjson @@ -0,0 +1,3 @@ +{"record":"scenario","metadata":{"seed":"10451216379200822465","index":0,"parameters":{"window_s":1990,"dt_s":10,"ownship_speed_mps":7.066248893737793,"ownship_course0_rad":4.0582990646362305,"zig_angle_rad":-1.4848365783691406,"zig_fraction":0.599398136138916,"target_range0_m":19707.986328125,"target_bearing0_rad":3.49949312210083,"target_speed_mps":6.48574161529541,"target_course_rad":6.162376403808594,"noise_sigma_rad":0.01745329238474369,"dropout_fraction":0.005200065206736326,"ownship_turn_rate_rad_s":0.044397737830877304,"zig2_delay_s":221.35269165039062,"zig2_angle_rad":1.955519676208496},"samples":199,"zig_index":123,"turn_start_index":120,"zig2_index":150,"turn2_start_index":146,"zig_time_s":1230,"mean_range_m":8646.6748046875,"min_range_m":457.4162902832031},"ownship":{"id":"ownship","states":[{"timestamp_s":0,"x_m":0,"y_m":0,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":10,"x_m":-56.0778923034668,"y_m":-42.993690490722656,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":20,"x_m":-112.1557846069336,"y_m":-85.98738098144531,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":30,"x_m":-168.23367309570312,"y_m":-128.9810791015625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":40,"x_m":-224.3115692138672,"y_m":-171.97476196289062,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":50,"x_m":-280.38946533203125,"y_m":-214.9684600830078,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":60,"x_m":-336.46734619140625,"y_m":-257.962158203125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":70,"x_m":-392.54522705078125,"y_m":-300.9558410644531,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":80,"x_m":-448.6231384277344,"y_m":-343.94952392578125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":90,"x_m":-504.7010192871094,"y_m":-386.9432067871094,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":100,"x_m":-560.7789306640625,"y_m":-429.9369201660156,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":110,"x_m":-616.8568115234375,"y_m":-472.93060302734375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":120,"x_m":-672.9346923828125,"y_m":-515.92431640625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":130,"x_m":-729.0125732421875,"y_m":-558.91796875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":140,"x_m":-785.0904541015625,"y_m":-601.9116821289062,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":150,"x_m":-841.1683959960938,"y_m":-644.9053344726562,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":160,"x_m":-897.2462768554688,"y_m":-687.8990478515625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":170,"x_m":-953.3241577148438,"y_m":-730.8927612304688,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":180,"x_m":-1009.4020385742188,"y_m":-773.8864135742188,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":190,"x_m":-1065.47998046875,"y_m":-816.880126953125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":200,"x_m":-1121.557861328125,"y_m":-859.8738403320312,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":210,"x_m":-1177.6357421875,"y_m":-902.8674926757812,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":220,"x_m":-1233.713623046875,"y_m":-945.8612060546875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":230,"x_m":-1289.79150390625,"y_m":-988.8548583984375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":240,"x_m":-1345.869384765625,"y_m":-1031.8486328125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":250,"x_m":-1401.947265625,"y_m":-1074.84228515625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":260,"x_m":-1458.025146484375,"y_m":-1117.8359375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":270,"x_m":-1514.10302734375,"y_m":-1160.82958984375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":280,"x_m":-1570.180908203125,"y_m":-1203.8233642578125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":290,"x_m":-1626.2589111328125,"y_m":-1246.8170166015625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":300,"x_m":-1682.3367919921875,"y_m":-1289.8106689453125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":310,"x_m":-1738.4146728515625,"y_m":-1332.804443359375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":320,"x_m":-1794.4925537109375,"y_m":-1375.798095703125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":330,"x_m":-1850.5704345703125,"y_m":-1418.791748046875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":340,"x_m":-1906.6483154296875,"y_m":-1461.7855224609375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":350,"x_m":-1962.7261962890625,"y_m":-1504.7791748046875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":360,"x_m":-2018.8040771484375,"y_m":-1547.7728271484375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":370,"x_m":-2074.882080078125,"y_m":-1590.7666015625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":380,"x_m":-2130.9599609375,"y_m":-1633.76025390625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":390,"x_m":-2187.037841796875,"y_m":-1676.75390625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":400,"x_m":-2243.11572265625,"y_m":-1719.7476806640625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":410,"x_m":-2299.193603515625,"y_m":-1762.7413330078125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":420,"x_m":-2355.271484375,"y_m":-1805.7349853515625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":430,"x_m":-2411.349365234375,"y_m":-1848.7286376953125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":440,"x_m":-2467.42724609375,"y_m":-1891.722412109375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":450,"x_m":-2523.505126953125,"y_m":-1934.716064453125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":460,"x_m":-2579.5830078125,"y_m":-1977.709716796875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":470,"x_m":-2635.660888671875,"y_m":-2020.7034912109375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":480,"x_m":-2691.73876953125,"y_m":-2063.697265625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":490,"x_m":-2747.816650390625,"y_m":-2106.69091796875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":500,"x_m":-2803.89453125,"y_m":-2149.6845703125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":510,"x_m":-2859.972412109375,"y_m":-2192.67822265625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":520,"x_m":-2916.05029296875,"y_m":-2235.671875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":530,"x_m":-2972.128173828125,"y_m":-2278.66552734375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":540,"x_m":-3028.2060546875,"y_m":-2321.6591796875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":550,"x_m":-3084.283935546875,"y_m":-2364.653076171875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":560,"x_m":-3140.36181640625,"y_m":-2407.646728515625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":570,"x_m":-3196.439697265625,"y_m":-2450.640380859375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":580,"x_m":-3252.517822265625,"y_m":-2493.634033203125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":590,"x_m":-3308.595703125,"y_m":-2536.627685546875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":600,"x_m":-3364.673583984375,"y_m":-2579.621337890625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":610,"x_m":-3420.75146484375,"y_m":-2622.615234375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":620,"x_m":-3476.829345703125,"y_m":-2665.60888671875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":630,"x_m":-3532.9072265625,"y_m":-2708.6025390625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":640,"x_m":-3588.985107421875,"y_m":-2751.59619140625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":650,"x_m":-3645.06298828125,"y_m":-2794.58984375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":660,"x_m":-3701.140869140625,"y_m":-2837.58349609375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":670,"x_m":-3757.21875,"y_m":-2880.5771484375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":680,"x_m":-3813.296630859375,"y_m":-2923.571044921875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":690,"x_m":-3869.37451171875,"y_m":-2966.564697265625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":700,"x_m":-3925.452392578125,"y_m":-3009.558349609375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":710,"x_m":-3981.5302734375,"y_m":-3052.552001953125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":720,"x_m":-4037.608154296875,"y_m":-3095.545654296875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":730,"x_m":-4093.68603515625,"y_m":-3138.539306640625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":740,"x_m":-4149.76416015625,"y_m":-3181.533203125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":750,"x_m":-4205.841796875,"y_m":-3224.52685546875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":760,"x_m":-4261.919921875,"y_m":-3267.5205078125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":770,"x_m":-4317.99755859375,"y_m":-3310.51416015625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":780,"x_m":-4374.07568359375,"y_m":-3353.5078125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":790,"x_m":-4430.1533203125,"y_m":-3396.50146484375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":800,"x_m":-4486.2314453125,"y_m":-3439.495361328125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":810,"x_m":-4542.30908203125,"y_m":-3482.489013671875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":820,"x_m":-4598.38720703125,"y_m":-3525.482666015625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":830,"x_m":-4654.46484375,"y_m":-3568.476318359375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":840,"x_m":-4710.54296875,"y_m":-3611.469970703125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":850,"x_m":-4766.62060546875,"y_m":-3654.463623046875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":860,"x_m":-4822.69873046875,"y_m":-3697.457275390625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":870,"x_m":-4878.7763671875,"y_m":-3740.451171875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":880,"x_m":-4934.8544921875,"y_m":-3783.44482421875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":890,"x_m":-4990.93212890625,"y_m":-3826.4384765625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":900,"x_m":-5047.01025390625,"y_m":-3869.43212890625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":910,"x_m":-5103.087890625,"y_m":-3912.42578125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":920,"x_m":-5159.166015625,"y_m":-3955.41943359375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":930,"x_m":-5215.244140625,"y_m":-3998.413330078125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":940,"x_m":-5271.32177734375,"y_m":-4041.406982421875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":950,"x_m":-5327.39990234375,"y_m":-4084.400634765625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":960,"x_m":-5383.4775390625,"y_m":-4127.39453125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":970,"x_m":-5439.5556640625,"y_m":-4170.38818359375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":980,"x_m":-5495.63330078125,"y_m":-4213.3818359375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":990,"x_m":-5551.71142578125,"y_m":-4256.37548828125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1000,"x_m":-5607.7890625,"y_m":-4299.369140625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1010,"x_m":-5663.8671875,"y_m":-4342.36279296875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1020,"x_m":-5719.94482421875,"y_m":-4385.3564453125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1030,"x_m":-5776.02294921875,"y_m":-4428.35009765625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1040,"x_m":-5832.1005859375,"y_m":-4471.34375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1050,"x_m":-5888.1787109375,"y_m":-4514.33740234375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1060,"x_m":-5944.25634765625,"y_m":-4557.3310546875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1070,"x_m":-6000.33447265625,"y_m":-4600.32470703125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1080,"x_m":-6056.412109375,"y_m":-4643.318359375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1090,"x_m":-6112.490234375,"y_m":-4686.3125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1100,"x_m":-6168.56787109375,"y_m":-4729.30615234375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1110,"x_m":-6224.64599609375,"y_m":-4772.2998046875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1120,"x_m":-6280.7236328125,"y_m":-4815.29345703125,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1130,"x_m":-6336.8017578125,"y_m":-4858.287109375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1140,"x_m":-6392.87939453125,"y_m":-4901.28076171875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1150,"x_m":-6448.95751953125,"y_m":-4944.2744140625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1160,"x_m":-6505.03564453125,"y_m":-4987.26806640625,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1170,"x_m":-6561.11328125,"y_m":-5030.26171875,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1180,"x_m":-6617.19140625,"y_m":-5073.25537109375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1190,"x_m":-6673.26904296875,"y_m":-5116.2490234375,"course_rad":4.0582990646362305,"speed_mps":7.066248893737793},{"timestamp_s":1200,"x_m":-6718.13427734375,"y_m":-5170.08984375,"course_rad":3.614321708679199,"speed_mps":7.066248893737793},{"timestamp_s":1210,"x_m":-6735.5234375,"y_m":-5237.98193359375,"course_rad":3.170344352722168,"speed_mps":7.066248893737793},{"timestamp_s":1220,"x_m":-6722.06494140625,"y_m":-5306.7607421875,"course_rad":2.7263669967651367,"speed_mps":7.066248893737793},{"timestamp_s":1230,"x_m":-6685.66064453125,"y_m":-5367.2421875,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1240,"x_m":-6647.64013671875,"y_m":-5426.80419921875,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1250,"x_m":-6609.61962890625,"y_m":-5486.3662109375,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1260,"x_m":-6571.599609375,"y_m":-5545.92822265625,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1270,"x_m":-6533.5791015625,"y_m":-5605.490234375,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1280,"x_m":-6495.55859375,"y_m":-5665.05224609375,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1290,"x_m":-6457.5380859375,"y_m":-5724.6142578125,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1300,"x_m":-6419.517578125,"y_m":-5784.17626953125,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1310,"x_m":-6381.4970703125,"y_m":-5843.73828125,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1320,"x_m":-6343.4765625,"y_m":-5903.30029296875,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1330,"x_m":-6305.45654296875,"y_m":-5962.8623046875,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1340,"x_m":-6267.43603515625,"y_m":-6022.42431640625,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1350,"x_m":-6229.41552734375,"y_m":-6081.986328125,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1360,"x_m":-6191.39501953125,"y_m":-6141.54833984375,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1370,"x_m":-6153.37451171875,"y_m":-6201.1103515625,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1380,"x_m":-6115.35400390625,"y_m":-6260.67236328125,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1390,"x_m":-6077.33349609375,"y_m":-6320.234375,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1400,"x_m":-6039.3134765625,"y_m":-6379.79638671875,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1410,"x_m":-6001.29296875,"y_m":-6439.3583984375,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1420,"x_m":-5963.2724609375,"y_m":-6498.92041015625,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1430,"x_m":-5925.251953125,"y_m":-6558.482421875,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1440,"x_m":-5887.2314453125,"y_m":-6618.04443359375,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1450,"x_m":-5849.2109375,"y_m":-6677.6064453125,"course_rad":2.57346248626709,"speed_mps":7.066248893737793},{"timestamp_s":1460,"x_m":-5825.43359375,"y_m":-6743.533203125,"course_rad":3.017439842224121,"speed_mps":7.066248893737793},{"timestamp_s":1470,"x_m":-5832.279296875,"y_m":-6813.28173828125,"course_rad":3.4614171981811523,"speed_mps":7.066248893737793},{"timestamp_s":1480,"x_m":-5868.4208984375,"y_m":-6873.32763671875,"course_rad":3.9053945541381836,"speed_mps":7.066248893737793},{"timestamp_s":1490,"x_m":-5926.85009765625,"y_m":-6912.02783203125,"course_rad":4.349371910095215,"speed_mps":7.066248893737793},{"timestamp_s":1500,"x_m":-5995.7099609375,"y_m":-6927.40478515625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1510,"x_m":-6065.18701171875,"y_m":-6940.29248046875,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1520,"x_m":-6134.66455078125,"y_m":-6953.1796875,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1530,"x_m":-6204.1416015625,"y_m":-6966.0673828125,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1540,"x_m":-6273.619140625,"y_m":-6978.95458984375,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1550,"x_m":-6343.0966796875,"y_m":-6991.84228515625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1560,"x_m":-6412.57373046875,"y_m":-7004.7294921875,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1570,"x_m":-6482.05126953125,"y_m":-7017.61669921875,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1580,"x_m":-6551.5283203125,"y_m":-7030.50439453125,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1590,"x_m":-6621.005859375,"y_m":-7043.3916015625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1600,"x_m":-6690.4833984375,"y_m":-7056.279296875,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1610,"x_m":-6759.96044921875,"y_m":-7069.16650390625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1620,"x_m":-6829.43798828125,"y_m":-7082.05419921875,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1630,"x_m":-6898.9150390625,"y_m":-7094.94140625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1640,"x_m":-6968.392578125,"y_m":-7107.8291015625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1650,"x_m":-7037.86962890625,"y_m":-7120.71630859375,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1660,"x_m":-7107.34716796875,"y_m":-7133.60400390625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1670,"x_m":-7176.82470703125,"y_m":-7146.4912109375,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1680,"x_m":-7246.3017578125,"y_m":-7159.37890625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1690,"x_m":-7315.779296875,"y_m":-7172.26611328125,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1700,"x_m":-7385.25634765625,"y_m":-7185.15380859375,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1710,"x_m":-7454.73388671875,"y_m":-7198.041015625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1720,"x_m":-7524.21142578125,"y_m":-7210.9287109375,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1730,"x_m":-7593.6884765625,"y_m":-7223.81591796875,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1740,"x_m":-7663.166015625,"y_m":-7236.70361328125,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1750,"x_m":-7732.64306640625,"y_m":-7249.5908203125,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1760,"x_m":-7802.12060546875,"y_m":-7262.478515625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1770,"x_m":-7871.59814453125,"y_m":-7275.36572265625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1780,"x_m":-7941.0751953125,"y_m":-7288.25341796875,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1790,"x_m":-8010.552734375,"y_m":-7301.140625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1800,"x_m":-8080.02978515625,"y_m":-7314.0283203125,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1810,"x_m":-8149.50732421875,"y_m":-7326.91552734375,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1820,"x_m":-8218.984375,"y_m":-7339.80322265625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1830,"x_m":-8288.4619140625,"y_m":-7352.6904296875,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1840,"x_m":-8357.939453125,"y_m":-7365.578125,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1850,"x_m":-8427.4169921875,"y_m":-7378.46533203125,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1860,"x_m":-8496.89453125,"y_m":-7391.35302734375,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1870,"x_m":-8566.37109375,"y_m":-7404.240234375,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1880,"x_m":-8635.8486328125,"y_m":-7417.1279296875,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1890,"x_m":-8705.326171875,"y_m":-7430.01513671875,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1900,"x_m":-8774.8037109375,"y_m":-7442.90283203125,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1910,"x_m":-8844.28125,"y_m":-7455.7900390625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1920,"x_m":-8913.7578125,"y_m":-7468.677734375,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1930,"x_m":-8983.2353515625,"y_m":-7481.56494140625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1940,"x_m":-9052.712890625,"y_m":-7494.45263671875,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1950,"x_m":-9122.1904296875,"y_m":-7507.33984375,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1960,"x_m":-9191.6669921875,"y_m":-7520.2275390625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1970,"x_m":-9261.14453125,"y_m":-7533.11474609375,"course_rad":4.528982162475586,"speed_mps":7.066248893737793},{"timestamp_s":1980,"x_m":-9330.6220703125,"y_m":-7546.00244140625,"course_rad":4.528982162475586,"speed_mps":7.066248893737793}]},"target":{"id":"target","states":[{"timestamp_s":0,"x_m":-6903.87548828125,"y_m":-18459.177734375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":10,"x_m":-6911.69189453125,"y_m":-18394.79296875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":20,"x_m":-6919.5078125,"y_m":-18330.408203125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":30,"x_m":-6927.32421875,"y_m":-18266.0234375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":40,"x_m":-6935.140625,"y_m":-18201.638671875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":50,"x_m":-6942.95703125,"y_m":-18137.25390625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":60,"x_m":-6950.7734375,"y_m":-18072.869140625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":70,"x_m":-6958.58935546875,"y_m":-18008.484375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":80,"x_m":-6966.40576171875,"y_m":-17944.099609375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":90,"x_m":-6974.22216796875,"y_m":-17879.71484375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":100,"x_m":-6982.03857421875,"y_m":-17815.330078125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":110,"x_m":-6989.85498046875,"y_m":-17750.9453125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":120,"x_m":-6997.6708984375,"y_m":-17686.560546875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":130,"x_m":-7005.4873046875,"y_m":-17622.17578125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":140,"x_m":-7013.3037109375,"y_m":-17557.791015625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":150,"x_m":-7021.1201171875,"y_m":-17493.40625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":160,"x_m":-7028.9365234375,"y_m":-17429.021484375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":170,"x_m":-7036.75244140625,"y_m":-17364.63671875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":180,"x_m":-7044.56884765625,"y_m":-17300.251953125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":190,"x_m":-7052.38525390625,"y_m":-17235.8671875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":200,"x_m":-7060.20166015625,"y_m":-17171.482421875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":210,"x_m":-7068.01806640625,"y_m":-17107.099609375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":220,"x_m":-7075.833984375,"y_m":-17042.71484375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":230,"x_m":-7083.650390625,"y_m":-16978.330078125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":240,"x_m":-7091.466796875,"y_m":-16913.9453125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":250,"x_m":-7099.283203125,"y_m":-16849.560546875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":260,"x_m":-7107.099609375,"y_m":-16785.17578125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":270,"x_m":-7114.91552734375,"y_m":-16720.791015625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":280,"x_m":-7122.73193359375,"y_m":-16656.40625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":290,"x_m":-7130.54833984375,"y_m":-16592.021484375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":300,"x_m":-7138.36474609375,"y_m":-16527.63671875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":310,"x_m":-7146.18115234375,"y_m":-16463.251953125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":320,"x_m":-7153.9970703125,"y_m":-16398.8671875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":330,"x_m":-7161.8134765625,"y_m":-16334.482421875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":340,"x_m":-7169.6298828125,"y_m":-16270.09765625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":350,"x_m":-7177.4462890625,"y_m":-16205.712890625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":360,"x_m":-7185.2626953125,"y_m":-16141.328125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":370,"x_m":-7193.07861328125,"y_m":-16076.943359375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":380,"x_m":-7200.89501953125,"y_m":-16012.55859375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":390,"x_m":-7208.71142578125,"y_m":-15948.173828125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":400,"x_m":-7216.52783203125,"y_m":-15883.7890625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":410,"x_m":-7224.34423828125,"y_m":-15819.404296875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":420,"x_m":-7232.16015625,"y_m":-15755.01953125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":430,"x_m":-7239.9765625,"y_m":-15690.634765625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":440,"x_m":-7247.79296875,"y_m":-15626.2509765625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":450,"x_m":-7255.609375,"y_m":-15561.8662109375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":460,"x_m":-7263.42578125,"y_m":-15497.4814453125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":470,"x_m":-7271.24169921875,"y_m":-15433.0966796875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":480,"x_m":-7279.05810546875,"y_m":-15368.7119140625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":490,"x_m":-7286.87451171875,"y_m":-15304.3271484375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":500,"x_m":-7294.69091796875,"y_m":-15239.9423828125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":510,"x_m":-7302.50732421875,"y_m":-15175.5576171875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":520,"x_m":-7310.3232421875,"y_m":-15111.1728515625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":530,"x_m":-7318.1396484375,"y_m":-15046.7880859375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":540,"x_m":-7325.9560546875,"y_m":-14982.4033203125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":550,"x_m":-7333.7724609375,"y_m":-14918.0185546875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":560,"x_m":-7341.5888671875,"y_m":-14853.6337890625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":570,"x_m":-7349.40478515625,"y_m":-14789.2490234375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":580,"x_m":-7357.22119140625,"y_m":-14724.8642578125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":590,"x_m":-7365.03759765625,"y_m":-14660.48046875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":600,"x_m":-7372.85400390625,"y_m":-14596.095703125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":610,"x_m":-7380.67041015625,"y_m":-14531.7109375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":620,"x_m":-7388.486328125,"y_m":-14467.326171875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":630,"x_m":-7396.302734375,"y_m":-14402.94140625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":640,"x_m":-7404.119140625,"y_m":-14338.556640625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":650,"x_m":-7411.935546875,"y_m":-14274.171875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":660,"x_m":-7419.751953125,"y_m":-14209.787109375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":670,"x_m":-7427.56787109375,"y_m":-14145.40234375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":680,"x_m":-7435.38427734375,"y_m":-14081.017578125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":690,"x_m":-7443.20068359375,"y_m":-14016.6328125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":700,"x_m":-7451.01708984375,"y_m":-13952.248046875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":710,"x_m":-7458.83349609375,"y_m":-13887.86328125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":720,"x_m":-7466.6494140625,"y_m":-13823.478515625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":730,"x_m":-7474.4658203125,"y_m":-13759.09375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":740,"x_m":-7482.2822265625,"y_m":-13694.7099609375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":750,"x_m":-7490.0986328125,"y_m":-13630.3251953125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":760,"x_m":-7497.9150390625,"y_m":-13565.9404296875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":770,"x_m":-7505.73095703125,"y_m":-13501.5556640625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":780,"x_m":-7513.54736328125,"y_m":-13437.1708984375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":790,"x_m":-7521.36376953125,"y_m":-13372.7861328125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":800,"x_m":-7529.18017578125,"y_m":-13308.4013671875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":810,"x_m":-7536.99658203125,"y_m":-13244.0166015625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":820,"x_m":-7544.8125,"y_m":-13179.6318359375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":830,"x_m":-7552.62890625,"y_m":-13115.2470703125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":840,"x_m":-7560.4453125,"y_m":-13050.8623046875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":850,"x_m":-7568.26171875,"y_m":-12986.4775390625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":860,"x_m":-7576.078125,"y_m":-12922.0927734375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":870,"x_m":-7583.89404296875,"y_m":-12857.7080078125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":880,"x_m":-7591.71044921875,"y_m":-12793.3232421875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":890,"x_m":-7599.52685546875,"y_m":-12728.939453125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":900,"x_m":-7607.34326171875,"y_m":-12664.5546875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":910,"x_m":-7615.1591796875,"y_m":-12600.169921875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":920,"x_m":-7622.9755859375,"y_m":-12535.78515625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":930,"x_m":-7630.7919921875,"y_m":-12471.400390625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":940,"x_m":-7638.6083984375,"y_m":-12407.015625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":950,"x_m":-7646.4248046875,"y_m":-12342.630859375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":960,"x_m":-7654.24072265625,"y_m":-12278.24609375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":970,"x_m":-7662.05712890625,"y_m":-12213.861328125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":980,"x_m":-7669.87353515625,"y_m":-12149.4765625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":990,"x_m":-7677.68994140625,"y_m":-12085.091796875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1000,"x_m":-7685.50634765625,"y_m":-12020.70703125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1010,"x_m":-7693.322265625,"y_m":-11956.322265625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1020,"x_m":-7701.138671875,"y_m":-11891.9375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1030,"x_m":-7708.955078125,"y_m":-11827.552734375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1040,"x_m":-7716.771484375,"y_m":-11763.16796875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1050,"x_m":-7724.587890625,"y_m":-11698.7841796875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1060,"x_m":-7732.40380859375,"y_m":-11634.3994140625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1070,"x_m":-7740.22021484375,"y_m":-11570.0146484375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1080,"x_m":-7748.03662109375,"y_m":-11505.6298828125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1090,"x_m":-7755.85302734375,"y_m":-11441.2451171875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1100,"x_m":-7763.66943359375,"y_m":-11376.8603515625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1110,"x_m":-7771.4853515625,"y_m":-11312.4755859375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1120,"x_m":-7779.3017578125,"y_m":-11248.0908203125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1130,"x_m":-7787.1181640625,"y_m":-11183.7060546875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1140,"x_m":-7794.9345703125,"y_m":-11119.3212890625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1150,"x_m":-7802.7509765625,"y_m":-11054.9365234375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1160,"x_m":-7810.56689453125,"y_m":-10990.5517578125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1170,"x_m":-7818.38330078125,"y_m":-10926.1669921875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1180,"x_m":-7826.19970703125,"y_m":-10861.7822265625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1190,"x_m":-7834.01611328125,"y_m":-10797.3974609375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1200,"x_m":-7841.83251953125,"y_m":-10733.013671875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1210,"x_m":-7849.6484375,"y_m":-10668.62890625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1220,"x_m":-7857.46484375,"y_m":-10604.244140625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1230,"x_m":-7865.28125,"y_m":-10539.859375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1240,"x_m":-7873.09765625,"y_m":-10475.474609375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1250,"x_m":-7880.9140625,"y_m":-10411.08984375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1260,"x_m":-7888.72998046875,"y_m":-10346.705078125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1270,"x_m":-7896.54638671875,"y_m":-10282.3203125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1280,"x_m":-7904.36279296875,"y_m":-10217.935546875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1290,"x_m":-7912.17919921875,"y_m":-10153.55078125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1300,"x_m":-7919.99560546875,"y_m":-10089.166015625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1310,"x_m":-7927.8115234375,"y_m":-10024.78125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1320,"x_m":-7935.6279296875,"y_m":-9960.396484375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1330,"x_m":-7943.4443359375,"y_m":-9896.01171875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1340,"x_m":-7951.2607421875,"y_m":-9831.626953125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1350,"x_m":-7959.0771484375,"y_m":-9767.2431640625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1360,"x_m":-7966.89306640625,"y_m":-9702.8583984375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1370,"x_m":-7974.70947265625,"y_m":-9638.4736328125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1380,"x_m":-7982.52587890625,"y_m":-9574.0888671875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1390,"x_m":-7990.34228515625,"y_m":-9509.7041015625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1400,"x_m":-7998.15869140625,"y_m":-9445.3193359375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1410,"x_m":-8005.974609375,"y_m":-9380.9345703125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1420,"x_m":-8013.791015625,"y_m":-9316.5498046875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1430,"x_m":-8021.607421875,"y_m":-9252.1650390625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1440,"x_m":-8029.423828125,"y_m":-9187.7802734375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1450,"x_m":-8037.240234375,"y_m":-9123.3955078125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1460,"x_m":-8045.05615234375,"y_m":-9059.0107421875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1470,"x_m":-8052.87255859375,"y_m":-8994.6259765625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1480,"x_m":-8060.68896484375,"y_m":-8930.2412109375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1490,"x_m":-8068.50537109375,"y_m":-8865.8564453125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1500,"x_m":-8076.32177734375,"y_m":-8801.47265625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1510,"x_m":-8084.1376953125,"y_m":-8737.087890625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1520,"x_m":-8091.9541015625,"y_m":-8672.703125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1530,"x_m":-8099.7705078125,"y_m":-8608.318359375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1540,"x_m":-8107.5869140625,"y_m":-8543.93359375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1550,"x_m":-8115.4033203125,"y_m":-8479.548828125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1560,"x_m":-8123.21923828125,"y_m":-8415.1640625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1570,"x_m":-8131.03564453125,"y_m":-8350.779296875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1580,"x_m":-8138.85205078125,"y_m":-8286.39453125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1590,"x_m":-8146.66845703125,"y_m":-8222.009765625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1600,"x_m":-8154.48486328125,"y_m":-8157.625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1610,"x_m":-8162.30078125,"y_m":-8093.240234375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1620,"x_m":-8170.1171875,"y_m":-8028.85595703125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1630,"x_m":-8177.93359375,"y_m":-7964.47119140625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1640,"x_m":-8185.75,"y_m":-7900.08642578125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1650,"x_m":-8193.56640625,"y_m":-7835.70166015625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1660,"x_m":-8201.3828125,"y_m":-7771.31689453125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1670,"x_m":-8209.19921875,"y_m":-7706.93212890625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1680,"x_m":-8217.015625,"y_m":-7642.54736328125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1690,"x_m":-8224.8310546875,"y_m":-7578.1630859375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1700,"x_m":-8232.6474609375,"y_m":-7513.7783203125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1710,"x_m":-8240.4638671875,"y_m":-7449.3935546875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1720,"x_m":-8248.2802734375,"y_m":-7385.0087890625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1730,"x_m":-8256.0966796875,"y_m":-7320.6240234375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1740,"x_m":-8263.9130859375,"y_m":-7256.2392578125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1750,"x_m":-8271.7294921875,"y_m":-7191.8544921875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1760,"x_m":-8279.5458984375,"y_m":-7127.4697265625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1770,"x_m":-8287.3623046875,"y_m":-7063.08544921875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1780,"x_m":-8295.177734375,"y_m":-6998.70068359375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1790,"x_m":-8302.994140625,"y_m":-6934.31591796875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1800,"x_m":-8310.810546875,"y_m":-6869.93115234375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1810,"x_m":-8318.626953125,"y_m":-6805.54638671875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1820,"x_m":-8326.443359375,"y_m":-6741.16162109375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1830,"x_m":-8334.259765625,"y_m":-6676.77685546875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1840,"x_m":-8342.076171875,"y_m":-6612.392578125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1850,"x_m":-8349.892578125,"y_m":-6548.0078125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1860,"x_m":-8357.708984375,"y_m":-6483.623046875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1870,"x_m":-8365.525390625,"y_m":-6419.23828125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1880,"x_m":-8373.3408203125,"y_m":-6354.853515625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1890,"x_m":-8381.1572265625,"y_m":-6290.46875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1900,"x_m":-8388.9736328125,"y_m":-6226.083984375,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1910,"x_m":-8396.7900390625,"y_m":-6161.69921875,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1920,"x_m":-8404.6064453125,"y_m":-6097.31494140625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1930,"x_m":-8412.4228515625,"y_m":-6032.93017578125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1940,"x_m":-8420.2392578125,"y_m":-5968.54541015625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1950,"x_m":-8428.0556640625,"y_m":-5904.16064453125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1960,"x_m":-8435.8720703125,"y_m":-5839.77587890625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1970,"x_m":-8443.6884765625,"y_m":-5775.39111328125,"course_rad":6.162376403808594,"speed_mps":6.48574161529541},{"timestamp_s":1980,"x_m":-8451.50390625,"y_m":-5711.00634765625,"course_rad":6.162376403808594,"speed_mps":6.48574161529541}]},"observations":[{"timestamp_s":0,"bearing_rad":3.4743990898132324,"valid":true},{"timestamp_s":10,"bearing_rad":3.522998332977295,"valid":true},{"timestamp_s":20,"bearing_rad":3.4964778423309326,"valid":true},{"timestamp_s":30,"bearing_rad":3.5268783569335938,"valid":true},{"timestamp_s":40,"bearing_rad":3.501065492630005,"valid":true},{"timestamp_s":50,"bearing_rad":3.50003719329834,"valid":true},{"timestamp_s":60,"bearing_rad":3.519585132598877,"valid":true},{"timestamp_s":70,"bearing_rad":3.481520175933838,"valid":true},{"timestamp_s":80,"bearing_rad":3.5051040649414062,"valid":true},{"timestamp_s":90,"bearing_rad":3.487212657928467,"valid":true},{"timestamp_s":100,"bearing_rad":3.5053043365478516,"valid":true},{"timestamp_s":110,"bearing_rad":3.4955029487609863,"valid":true},{"timestamp_s":120,"bearing_rad":3.4910595417022705,"valid":true},{"timestamp_s":130,"bearing_rad":3.4647088050842285,"valid":true},{"timestamp_s":140,"bearing_rad":3.4586875438690186,"valid":true},{"timestamp_s":150,"bearing_rad":3.4773569107055664,"valid":true},{"timestamp_s":160,"bearing_rad":3.4847805500030518,"valid":true},{"timestamp_s":170,"bearing_rad":3.4763667583465576,"valid":true},{"timestamp_s":180,"bearing_rad":3.4527740478515625,"valid":true},{"timestamp_s":190,"bearing_rad":3.5102949142456055,"valid":true},{"timestamp_s":200,"bearing_rad":3.5033764839172363,"valid":true},{"timestamp_s":210,"bearing_rad":3.497830629348755,"valid":true},{"timestamp_s":220,"bearing_rad":3.5074613094329834,"valid":true},{"timestamp_s":230,"bearing_rad":3.4813287258148193,"valid":true},{"timestamp_s":240,"bearing_rad":3.4875259399414062,"valid":true},{"timestamp_s":250,"bearing_rad":3.538048505783081,"valid":true},{"timestamp_s":260,"bearing_rad":3.5140771865844727,"valid":true},{"timestamp_s":270,"bearing_rad":3.5049381256103516,"valid":true},{"timestamp_s":280,"bearing_rad":3.480151653289795,"valid":true},{"timestamp_s":290,"bearing_rad":3.4810032844543457,"valid":true},{"timestamp_s":300,"bearing_rad":3.4874427318573,"valid":true},{"timestamp_s":310,"bearing_rad":3.492652416229248,"valid":true},{"timestamp_s":320,"bearing_rad":3.483689069747925,"valid":true},{"timestamp_s":330,"bearing_rad":3.5140891075134277,"valid":true},{"timestamp_s":340,"bearing_rad":3.5012829303741455,"valid":true},{"timestamp_s":350,"bearing_rad":3.4482967853546143,"valid":true},{"timestamp_s":360,"bearing_rad":3.48399019241333,"valid":true},{"timestamp_s":370,"bearing_rad":3.490920066833496,"valid":true},{"timestamp_s":380,"bearing_rad":3.4929752349853516,"valid":true},{"timestamp_s":390,"bearing_rad":3.504429340362549,"valid":true},{"timestamp_s":400,"bearing_rad":3.4701740741729736,"valid":true},{"timestamp_s":410,"bearing_rad":3.4303834438323975,"valid":true},{"timestamp_s":420,"bearing_rad":3.4985616207122803,"valid":true},{"timestamp_s":430,"bearing_rad":3.4944372177124023,"valid":true},{"timestamp_s":440,"bearing_rad":3.489922046661377,"valid":true},{"timestamp_s":450,"bearing_rad":3.4552927017211914,"valid":true},{"timestamp_s":460,"bearing_rad":3.477231740951538,"valid":true},{"timestamp_s":470,"bearing_rad":3.470418930053711,"valid":true},{"timestamp_s":480,"bearing_rad":3.4716548919677734,"valid":true},{"timestamp_s":490,"bearing_rad":3.4798669815063477,"valid":true},{"timestamp_s":500,"bearing_rad":3.4711616039276123,"valid":true},{"timestamp_s":510,"bearing_rad":3.4800384044647217,"valid":true},{"timestamp_s":520,"bearing_rad":3.5011003017425537,"valid":true},{"timestamp_s":530,"bearing_rad":3.5010197162628174,"valid":true},{"timestamp_s":540,"bearing_rad":3.4768476486206055,"valid":true},{"timestamp_s":550,"bearing_rad":3.465092182159424,"valid":true},{"timestamp_s":560,"bearing_rad":3.461470127105713,"valid":true},{"timestamp_s":570,"bearing_rad":3.479999303817749,"valid":true},{"timestamp_s":580,"bearing_rad":3.4619224071502686,"valid":true},{"timestamp_s":590,"bearing_rad":3.4603798389434814,"valid":true},{"timestamp_s":600,"bearing_rad":3.469188928604126,"valid":true},{"timestamp_s":610,"bearing_rad":3.460798978805542,"valid":true},{"timestamp_s":620,"bearing_rad":3.446106433868408,"valid":true},{"timestamp_s":630,"bearing_rad":3.4397976398468018,"valid":true},{"timestamp_s":640,"bearing_rad":3.4567227363586426,"valid":true},{"timestamp_s":650,"bearing_rad":3.4637370109558105,"valid":true},{"timestamp_s":660,"bearing_rad":3.477142572402954,"valid":true},{"timestamp_s":670,"bearing_rad":3.4418492317199707,"valid":true},{"timestamp_s":680,"bearing_rad":3.4697766304016113,"valid":true},{"timestamp_s":690,"bearing_rad":3.4217240810394287,"valid":true},{"timestamp_s":700,"bearing_rad":3.463090658187866,"valid":true},{"timestamp_s":710,"bearing_rad":3.4443678855895996,"valid":true},{"timestamp_s":720,"bearing_rad":3.450188159942627,"valid":true},{"timestamp_s":730,"bearing_rad":3.470649003982544,"valid":true},{"timestamp_s":740,"bearing_rad":3.426988124847412,"valid":true},{"timestamp_s":750,"bearing_rad":3.4421613216400146,"valid":true},{"timestamp_s":760,"bearing_rad":3.466487407684326,"valid":true},{"timestamp_s":770,"bearing_rad":3.4443767070770264,"valid":true},{"timestamp_s":780,"bearing_rad":3.437631368637085,"valid":true},{"timestamp_s":790,"bearing_rad":3.478949785232544,"valid":true},{"timestamp_s":800,"bearing_rad":3.4423208236694336,"valid":true},{"timestamp_s":810,"bearing_rad":3.4722721576690674,"valid":true},{"timestamp_s":820,"bearing_rad":3.440028667449951,"valid":true},{"timestamp_s":830,"bearing_rad":3.435957431793213,"valid":true},{"timestamp_s":840,"bearing_rad":3.4388656616210938,"valid":true},{"timestamp_s":850,"bearing_rad":3.4167721271514893,"valid":true},{"timestamp_s":860,"bearing_rad":3.4321765899658203,"valid":true},{"timestamp_s":870,"bearing_rad":3.4211313724517822,"valid":true},{"timestamp_s":880,"bearing_rad":3.4416134357452393,"valid":true},{"timestamp_s":890,"bearing_rad":3.4376134872436523,"valid":true},{"timestamp_s":900,"bearing_rad":3.415898561477661,"valid":true},{"timestamp_s":910,"bearing_rad":3.4549663066864014,"valid":true},{"timestamp_s":920,"bearing_rad":3.432187080383301,"valid":true},{"timestamp_s":930,"bearing_rad":3.4113569259643555,"valid":true},{"timestamp_s":940,"bearing_rad":3.390424966812134,"valid":true},{"timestamp_s":950,"bearing_rad":3.418825387954712,"valid":true},{"timestamp_s":960,"bearing_rad":3.4203948974609375,"valid":true},{"timestamp_s":970,"bearing_rad":3.4134488105773926,"valid":true},{"timestamp_s":980,"bearing_rad":3.4012906551361084,"valid":true},{"timestamp_s":990,"bearing_rad":3.4377245903015137,"valid":true},{"timestamp_s":1000,"bearing_rad":0,"valid":false},{"timestamp_s":1010,"bearing_rad":3.397705554962158,"valid":true},{"timestamp_s":1020,"bearing_rad":3.4153246879577637,"valid":true},{"timestamp_s":1030,"bearing_rad":3.4059853553771973,"valid":true},{"timestamp_s":1040,"bearing_rad":3.3687729835510254,"valid":true},{"timestamp_s":1050,"bearing_rad":3.420656204223633,"valid":true},{"timestamp_s":1060,"bearing_rad":3.3456735610961914,"valid":true},{"timestamp_s":1070,"bearing_rad":3.395310878753662,"valid":true},{"timestamp_s":1080,"bearing_rad":3.3311855792999268,"valid":true},{"timestamp_s":1090,"bearing_rad":3.391245126724243,"valid":true},{"timestamp_s":1100,"bearing_rad":3.404555559158325,"valid":true},{"timestamp_s":1110,"bearing_rad":3.398926258087158,"valid":true},{"timestamp_s":1120,"bearing_rad":3.3654613494873047,"valid":true},{"timestamp_s":1130,"bearing_rad":3.3843438625335693,"valid":true},{"timestamp_s":1140,"bearing_rad":3.328619956970215,"valid":true},{"timestamp_s":1150,"bearing_rad":3.319823980331421,"valid":true},{"timestamp_s":1160,"bearing_rad":3.355729579925537,"valid":true},{"timestamp_s":1170,"bearing_rad":3.3590474128723145,"valid":true},{"timestamp_s":1180,"bearing_rad":3.3392159938812256,"valid":true},{"timestamp_s":1190,"bearing_rad":3.3596317768096924,"valid":true},{"timestamp_s":1200,"bearing_rad":3.3527932167053223,"valid":true},{"timestamp_s":1210,"bearing_rad":3.3632779121398926,"valid":true},{"timestamp_s":1220,"bearing_rad":3.343010187149048,"valid":true},{"timestamp_s":1230,"bearing_rad":3.3612680435180664,"valid":true},{"timestamp_s":1240,"bearing_rad":3.3740463256835938,"valid":true},{"timestamp_s":1250,"bearing_rad":3.385761022567749,"valid":true},{"timestamp_s":1260,"bearing_rad":3.3840713500976562,"valid":true},{"timestamp_s":1270,"bearing_rad":3.4361801147460938,"valid":true},{"timestamp_s":1280,"bearing_rad":3.4539077281951904,"valid":true},{"timestamp_s":1290,"bearing_rad":3.4634950160980225,"valid":true},{"timestamp_s":1300,"bearing_rad":3.4754092693328857,"valid":true},{"timestamp_s":1310,"bearing_rad":3.4997804164886475,"valid":true},{"timestamp_s":1320,"bearing_rad":3.4935951232910156,"valid":true},{"timestamp_s":1330,"bearing_rad":3.5370850563049316,"valid":true},{"timestamp_s":1340,"bearing_rad":3.5388193130493164,"valid":true},{"timestamp_s":1350,"bearing_rad":3.57324481010437,"valid":true},{"timestamp_s":1360,"bearing_rad":3.594599962234497,"valid":true},{"timestamp_s":1370,"bearing_rad":3.641897678375244,"valid":true},{"timestamp_s":1380,"bearing_rad":3.634690046310425,"valid":true},{"timestamp_s":1390,"bearing_rad":3.6761462688446045,"valid":true},{"timestamp_s":1400,"bearing_rad":3.713228225708008,"valid":true},{"timestamp_s":1410,"bearing_rad":3.7051994800567627,"valid":true},{"timestamp_s":1420,"bearing_rad":3.772737979888916,"valid":true},{"timestamp_s":1430,"bearing_rad":3.773449182510376,"valid":true},{"timestamp_s":1440,"bearing_rad":3.83038330078125,"valid":true},{"timestamp_s":1450,"bearing_rad":3.8584744930267334,"valid":true},{"timestamp_s":1460,"bearing_rad":3.8836324214935303,"valid":true},{"timestamp_s":1470,"bearing_rad":3.9481334686279297,"valid":true},{"timestamp_s":1480,"bearing_rad":3.9644930362701416,"valid":true},{"timestamp_s":1490,"bearing_rad":3.9746828079223633,"valid":true},{"timestamp_s":1500,"bearing_rad":3.9703783988952637,"valid":true},{"timestamp_s":1510,"bearing_rad":3.9893267154693604,"valid":true},{"timestamp_s":1520,"bearing_rad":4.0082502365112305,"valid":true},{"timestamp_s":1530,"bearing_rad":3.998185157775879,"valid":true},{"timestamp_s":1540,"bearing_rad":4.018978595733643,"valid":true},{"timestamp_s":1550,"bearing_rad":4.0402727127075195,"valid":true},{"timestamp_s":1560,"bearing_rad":4.0371294021606445,"valid":true},{"timestamp_s":1570,"bearing_rad":4.046125411987305,"valid":true},{"timestamp_s":1580,"bearing_rad":4.026165008544922,"valid":true},{"timestamp_s":1590,"bearing_rad":4.073364734649658,"valid":true},{"timestamp_s":1600,"bearing_rad":4.0634965896606445,"valid":true},{"timestamp_s":1610,"bearing_rad":4.068263053894043,"valid":true},{"timestamp_s":1620,"bearing_rad":4.080294132232666,"valid":true},{"timestamp_s":1630,"bearing_rad":4.081234455108643,"valid":true},{"timestamp_s":1640,"bearing_rad":4.152510643005371,"valid":true},{"timestamp_s":1650,"bearing_rad":4.158636569976807,"valid":true},{"timestamp_s":1660,"bearing_rad":4.179069995880127,"valid":true},{"timestamp_s":1670,"bearing_rad":4.2137908935546875,"valid":true},{"timestamp_s":1680,"bearing_rad":4.246204376220703,"valid":true},{"timestamp_s":1690,"bearing_rad":4.28160285949707,"valid":true},{"timestamp_s":1700,"bearing_rad":4.375707626342773,"valid":true},{"timestamp_s":1710,"bearing_rad":4.425714492797852,"valid":true},{"timestamp_s":1720,"bearing_rad":4.477023124694824,"valid":true},{"timestamp_s":1730,"bearing_rad":4.597766399383545,"valid":true},{"timestamp_s":1740,"bearing_rad":4.647132396697998,"valid":true},{"timestamp_s":1750,"bearing_rad":4.792215347290039,"valid":true},{"timestamp_s":1760,"bearing_rad":4.972593307495117,"valid":true},{"timestamp_s":1770,"bearing_rad":5.189088344573975,"valid":true},{"timestamp_s":1780,"bearing_rad":5.394287109375,"valid":true},{"timestamp_s":1790,"bearing_rad":5.621058464050293,"valid":true},{"timestamp_s":1800,"bearing_rad":5.800135135650635,"valid":true},{"timestamp_s":1810,"bearing_rad":5.9914045333862305,"valid":true},{"timestamp_s":1820,"bearing_rad":6.1234893798828125,"valid":true},{"timestamp_s":1830,"bearing_rad":6.248569011688232,"valid":true},{"timestamp_s":1840,"bearing_rad":0.04469949007034302,"valid":true},{"timestamp_s":1850,"bearing_rad":0.08799152821302414,"valid":true},{"timestamp_s":1860,"bearing_rad":0.18083040416240692,"valid":true},{"timestamp_s":1870,"bearing_rad":0.18170678615570068,"valid":true},{"timestamp_s":1880,"bearing_rad":0.23736202716827393,"valid":true},{"timestamp_s":1890,"bearing_rad":0.27806544303894043,"valid":true},{"timestamp_s":1900,"bearing_rad":0.29457715153694153,"valid":true},{"timestamp_s":1910,"bearing_rad":0.3295366168022156,"valid":true},{"timestamp_s":1920,"bearing_rad":0.36715036630630493,"valid":true},{"timestamp_s":1930,"bearing_rad":0.3950921893119812,"valid":true},{"timestamp_s":1940,"bearing_rad":0.3860781490802765,"valid":true},{"timestamp_s":1950,"bearing_rad":0.40029236674308777,"valid":true},{"timestamp_s":1960,"bearing_rad":0.3909897208213806,"valid":true},{"timestamp_s":1970,"bearing_rad":0.43283456563949585,"valid":true},{"timestamp_s":1980,"bearing_rad":0.45963266491889954,"valid":true}]} +{"record":"scenario","metadata":{"seed":"13757245211066428519","index":1,"parameters":{"window_s":1910,"dt_s":10,"ownship_speed_mps":3.043410301208496,"ownship_course0_rad":4.208929061889648,"zig_angle_rad":2.686316967010498,"zig_fraction":0.4479781985282898,"target_range0_m":3554.22607421875,"target_bearing0_rad":5.889577865600586,"target_speed_mps":2.7981085777282715,"target_course_rad":3.483982801437378,"noise_sigma_rad":0.008726646192371845,"dropout_fraction":0.028548268601298332,"ownship_turn_rate_rad_s":0.03190680965781212,"zig2_delay_s":290.0543518066406,"zig2_angle_rad":-2.7539899349212646},"samples":191,"zig_index":95,"turn_start_index":87,"zig2_index":133,"turn2_start_index":125,"zig_time_s":950,"mean_range_m":1739.6417236328125,"min_range_m":269.03668212890625},"ownship":{"id":"ownship","states":[{"timestamp_s":0,"x_m":0,"y_m":0,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":10,"x_m":-26.657794952392578,"y_m":-14.683207511901855,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":20,"x_m":-53.315589904785156,"y_m":-29.36641502380371,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":30,"x_m":-79.973388671875,"y_m":-44.04962158203125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":40,"x_m":-106.63117980957031,"y_m":-58.73283004760742,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":50,"x_m":-133.28897094726562,"y_m":-73.4160385131836,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":60,"x_m":-159.94677734375,"y_m":-88.0992431640625,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":70,"x_m":-186.6045684814453,"y_m":-102.78245544433594,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":80,"x_m":-213.26235961914062,"y_m":-117.46566009521484,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":90,"x_m":-239.92015075683594,"y_m":-132.14886474609375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":100,"x_m":-266.57794189453125,"y_m":-146.8320770263672,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":110,"x_m":-293.2357482910156,"y_m":-161.51528930664062,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":120,"x_m":-319.8935546875,"y_m":-176.198486328125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":130,"x_m":-346.55133056640625,"y_m":-190.88169860839844,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":140,"x_m":-373.2091369628906,"y_m":-205.56491088867188,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":150,"x_m":-399.8669128417969,"y_m":-220.2481231689453,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":160,"x_m":-426.52471923828125,"y_m":-234.9313201904297,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":170,"x_m":-453.1825256347656,"y_m":-249.61453247070312,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":180,"x_m":-479.8403015136719,"y_m":-264.2977294921875,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":190,"x_m":-506.49810791015625,"y_m":-278.98095703125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":200,"x_m":-533.1558837890625,"y_m":-293.6641540527344,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":210,"x_m":-559.813720703125,"y_m":-308.34735107421875,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":220,"x_m":-586.4714965820312,"y_m":-323.03057861328125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":230,"x_m":-613.1292724609375,"y_m":-337.7137756347656,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":240,"x_m":-639.787109375,"y_m":-352.39697265625,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":250,"x_m":-666.4448852539062,"y_m":-367.0802001953125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":260,"x_m":-693.1026611328125,"y_m":-381.7633972167969,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":270,"x_m":-719.7604370117188,"y_m":-396.44659423828125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":280,"x_m":-746.4182739257812,"y_m":-411.12982177734375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":290,"x_m":-773.0760498046875,"y_m":-425.8130187988281,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":300,"x_m":-799.7338256835938,"y_m":-440.4962463378906,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":310,"x_m":-826.3916625976562,"y_m":-455.179443359375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":320,"x_m":-853.0494384765625,"y_m":-469.8626403808594,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":330,"x_m":-879.7072143554688,"y_m":-484.5458679199219,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":340,"x_m":-906.3650512695312,"y_m":-499.22906494140625,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":350,"x_m":-933.0228271484375,"y_m":-513.9122924804688,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":360,"x_m":-959.6806030273438,"y_m":-528.595458984375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":370,"x_m":-986.3384399414062,"y_m":-543.2786865234375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":380,"x_m":-1012.9962158203125,"y_m":-557.9619140625,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":390,"x_m":-1039.654052734375,"y_m":-572.6450805664062,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":400,"x_m":-1066.311767578125,"y_m":-587.3283081054688,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":410,"x_m":-1092.9696044921875,"y_m":-602.0115356445312,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":420,"x_m":-1119.62744140625,"y_m":-616.6947021484375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":430,"x_m":-1146.28515625,"y_m":-631.3779296875,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":440,"x_m":-1172.9429931640625,"y_m":-646.0611572265625,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":450,"x_m":-1199.600830078125,"y_m":-660.7443237304688,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":460,"x_m":-1226.258544921875,"y_m":-675.4275512695312,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":470,"x_m":-1252.9163818359375,"y_m":-690.1107788085938,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":480,"x_m":-1279.57421875,"y_m":-704.7939453125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":490,"x_m":-1306.23193359375,"y_m":-719.4771728515625,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":500,"x_m":-1332.8897705078125,"y_m":-734.160400390625,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":510,"x_m":-1359.547607421875,"y_m":-748.8435668945312,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":520,"x_m":-1386.205322265625,"y_m":-763.5267944335938,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":530,"x_m":-1412.8631591796875,"y_m":-778.2100219726562,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":540,"x_m":-1439.5208740234375,"y_m":-792.8931884765625,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":550,"x_m":-1466.1787109375,"y_m":-807.576416015625,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":560,"x_m":-1492.8365478515625,"y_m":-822.2596435546875,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":570,"x_m":-1519.4942626953125,"y_m":-836.9428100585938,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":580,"x_m":-1546.152099609375,"y_m":-851.6260375976562,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":590,"x_m":-1572.8099365234375,"y_m":-866.3092651367188,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":600,"x_m":-1599.4676513671875,"y_m":-880.9924926757812,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":610,"x_m":-1626.12548828125,"y_m":-895.6756591796875,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":620,"x_m":-1652.7833251953125,"y_m":-910.35888671875,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":630,"x_m":-1679.4410400390625,"y_m":-925.0421142578125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":640,"x_m":-1706.098876953125,"y_m":-939.7252807617188,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":650,"x_m":-1732.7567138671875,"y_m":-954.4085083007812,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":660,"x_m":-1759.4144287109375,"y_m":-969.0917358398438,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":670,"x_m":-1786.072265625,"y_m":-983.77490234375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":680,"x_m":-1812.7301025390625,"y_m":-998.4581298828125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":690,"x_m":-1839.3878173828125,"y_m":-1013.141357421875,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":700,"x_m":-1866.045654296875,"y_m":-1027.8245849609375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":710,"x_m":-1892.7034912109375,"y_m":-1042.5076904296875,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":720,"x_m":-1919.3612060546875,"y_m":-1057.19091796875,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":730,"x_m":-1946.01904296875,"y_m":-1071.8741455078125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":740,"x_m":-1972.6768798828125,"y_m":-1086.557373046875,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":750,"x_m":-1999.3345947265625,"y_m":-1101.2406005859375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":760,"x_m":-2025.992431640625,"y_m":-1115.923828125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":770,"x_m":-2052.650146484375,"y_m":-1130.6070556640625,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":780,"x_m":-2079.30810546875,"y_m":-1145.2901611328125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":790,"x_m":-2105.9658203125,"y_m":-1159.973388671875,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":800,"x_m":-2132.62353515625,"y_m":-1174.6566162109375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":810,"x_m":-2159.281494140625,"y_m":-1189.33984375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":820,"x_m":-2185.939208984375,"y_m":-1204.0230712890625,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":830,"x_m":-2212.596923828125,"y_m":-1218.706298828125,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":840,"x_m":-2239.2548828125,"y_m":-1233.389404296875,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":850,"x_m":-2265.91259765625,"y_m":-1248.0726318359375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":860,"x_m":-2292.5703125,"y_m":-1262.755859375,"course_rad":4.208929061889648,"speed_mps":3.043410301208496},{"timestamp_s":870,"x_m":-2321.100830078125,"y_m":-1272.974365234375,"course_rad":4.527997016906738,"speed_mps":3.043410301208496},{"timestamp_s":880,"x_m":-2351.396728515625,"y_m":-1273.7275390625,"course_rad":4.847065448760986,"speed_mps":3.043410301208496},{"timestamp_s":890,"x_m":-2380.399658203125,"y_m":-1264.9395751953125,"course_rad":5.166133403778076,"speed_mps":3.043410301208496},{"timestamp_s":900,"x_m":-2405.18212890625,"y_m":-1247.497314453125,"course_rad":5.485201358795166,"speed_mps":3.043410301208496},{"timestamp_s":910,"x_m":-2423.24267578125,"y_m":-1223.1617431640625,"course_rad":5.804269313812256,"speed_mps":3.043410301208496},{"timestamp_s":920,"x_m":-2432.7578125,"y_m":-1194.3890380859375,"course_rad":6.123337745666504,"speed_mps":3.043410301208496},{"timestamp_s":930,"x_m":-2432.767333984375,"y_m":-1164.0838623046875,"course_rad":0.15922042727470398,"speed_mps":3.043410301208496},{"timestamp_s":940,"x_m":-2423.27001953125,"y_m":-1135.3052978515625,"course_rad":0.47828853130340576,"speed_mps":3.043410301208496},{"timestamp_s":950,"x_m":-2406.50341796875,"y_m":-1109.9375,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":960,"x_m":-2389.017333984375,"y_m":-1085.0281982421875,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":970,"x_m":-2371.53125,"y_m":-1060.1190185546875,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":980,"x_m":-2354.045166015625,"y_m":-1035.209716796875,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":990,"x_m":-2336.55908203125,"y_m":-1010.3004150390625,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1000,"x_m":-2319.072998046875,"y_m":-985.3911743164062,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1010,"x_m":-2301.5869140625,"y_m":-960.4818725585938,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1020,"x_m":-2284.100830078125,"y_m":-935.5726318359375,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1030,"x_m":-2266.61474609375,"y_m":-910.663330078125,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1040,"x_m":-2249.128662109375,"y_m":-885.7540893554688,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1050,"x_m":-2231.642578125,"y_m":-860.8447875976562,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1060,"x_m":-2214.156494140625,"y_m":-835.935546875,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1070,"x_m":-2196.67041015625,"y_m":-811.0262451171875,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1080,"x_m":-2179.184326171875,"y_m":-786.1170043945312,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1090,"x_m":-2161.6982421875,"y_m":-761.2077026367188,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1100,"x_m":-2144.212158203125,"y_m":-736.2984619140625,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1110,"x_m":-2126.72607421875,"y_m":-711.38916015625,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1120,"x_m":-2109.239990234375,"y_m":-686.4799194335938,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1130,"x_m":-2091.75390625,"y_m":-661.5706176757812,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1140,"x_m":-2074.267822265625,"y_m":-636.661376953125,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1150,"x_m":-2056.78173828125,"y_m":-611.7520751953125,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1160,"x_m":-2039.2957763671875,"y_m":-586.8428344726562,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1170,"x_m":-2021.8096923828125,"y_m":-561.9335327148438,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1180,"x_m":-2004.3236083984375,"y_m":-537.0242919921875,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1190,"x_m":-1986.8375244140625,"y_m":-512.114990234375,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1200,"x_m":-1969.3514404296875,"y_m":-487.20574951171875,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1210,"x_m":-1951.8653564453125,"y_m":-462.2964782714844,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1220,"x_m":-1934.3792724609375,"y_m":-437.38720703125,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1230,"x_m":-1916.8931884765625,"y_m":-412.4779357910156,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1240,"x_m":-1899.4072265625,"y_m":-387.56866455078125,"course_rad":0.6120607256889343,"speed_mps":3.043410301208496},{"timestamp_s":1250,"x_m":-1886.1566162109375,"y_m":-360.3138427734375,"course_rad":0.29299262166023254,"speed_mps":3.043410301208496},{"timestamp_s":1260,"x_m":-1882.1240234375,"y_m":-330.2781677246094,"course_rad":6.257109642028809,"speed_mps":3.043410301208496},{"timestamp_s":1270,"x_m":-1887.716796875,"y_m":-300.4935302734375,"course_rad":5.938041687011719,"speed_mps":3.043410301208496},{"timestamp_s":1280,"x_m":-1902.3701171875,"y_m":-273.96649169921875,"course_rad":5.618973731994629,"speed_mps":3.043410301208496},{"timestamp_s":1290,"x_m":-1924.6048583984375,"y_m":-253.37478637695312,"course_rad":5.299905776977539,"speed_mps":3.043410301208496},{"timestamp_s":1300,"x_m":-1952.1766357421875,"y_m":-240.79705810546875,"course_rad":4.980837345123291,"speed_mps":3.043410301208496},{"timestamp_s":1310,"x_m":-1982.30224609375,"y_m":-237.50289916992188,"course_rad":4.661769390106201,"speed_mps":3.043410301208496},{"timestamp_s":1320,"x_m":-2011.940673828125,"y_m":-243.8248748779297,"course_rad":4.342701435089111,"speed_mps":3.043410301208496},{"timestamp_s":1330,"x_m":-2038.4781494140625,"y_m":-258.58428955078125,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1340,"x_m":-2064.08203125,"y_m":-275.0365295410156,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1350,"x_m":-2089.685791015625,"y_m":-291.48876953125,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1360,"x_m":-2115.289794921875,"y_m":-307.9410095214844,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1370,"x_m":-2140.8935546875,"y_m":-324.39324951171875,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1380,"x_m":-2166.49755859375,"y_m":-340.845458984375,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1390,"x_m":-2192.101318359375,"y_m":-357.2976989746094,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1400,"x_m":-2217.705322265625,"y_m":-373.74993896484375,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1410,"x_m":-2243.30908203125,"y_m":-390.2021789550781,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1420,"x_m":-2268.9130859375,"y_m":-406.6544189453125,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1430,"x_m":-2294.516845703125,"y_m":-423.10662841796875,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1440,"x_m":-2320.120849609375,"y_m":-439.5588684082031,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1450,"x_m":-2345.724609375,"y_m":-456.0111083984375,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1460,"x_m":-2371.32861328125,"y_m":-472.4633483886719,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1470,"x_m":-2396.932373046875,"y_m":-488.91558837890625,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1480,"x_m":-2422.536376953125,"y_m":-505.3678283691406,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1490,"x_m":-2448.14013671875,"y_m":-521.820068359375,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1500,"x_m":-2473.744140625,"y_m":-538.2722778320312,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1510,"x_m":-2499.347900390625,"y_m":-554.7244873046875,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1520,"x_m":-2524.951904296875,"y_m":-571.1767578125,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1530,"x_m":-2550.5556640625,"y_m":-587.6289672851562,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1540,"x_m":-2576.15966796875,"y_m":-604.0812377929688,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1550,"x_m":-2601.763427734375,"y_m":-620.533447265625,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1560,"x_m":-2627.367431640625,"y_m":-636.9857177734375,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1570,"x_m":-2652.97119140625,"y_m":-653.4379272460938,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1580,"x_m":-2678.5751953125,"y_m":-669.89013671875,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1590,"x_m":-2704.178955078125,"y_m":-686.3424072265625,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1600,"x_m":-2729.782958984375,"y_m":-702.7946166992188,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1610,"x_m":-2755.38671875,"y_m":-719.2468872070312,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1620,"x_m":-2780.99072265625,"y_m":-735.6990966796875,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1630,"x_m":-2806.594482421875,"y_m":-752.1513061523438,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1640,"x_m":-2832.198486328125,"y_m":-768.6035766601562,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1650,"x_m":-2857.80224609375,"y_m":-785.0557861328125,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1660,"x_m":-2883.40625,"y_m":-801.508056640625,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1670,"x_m":-2909.010009765625,"y_m":-817.9602661132812,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1680,"x_m":-2934.614013671875,"y_m":-834.4124755859375,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1690,"x_m":-2960.2177734375,"y_m":-850.86474609375,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1700,"x_m":-2985.82177734375,"y_m":-867.3169555664062,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1710,"x_m":-3011.425537109375,"y_m":-883.7692260742188,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1720,"x_m":-3037.029296875,"y_m":-900.221435546875,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1730,"x_m":-3062.63330078125,"y_m":-916.6736450195312,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1740,"x_m":-3088.237060546875,"y_m":-933.1259155273438,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1750,"x_m":-3113.841064453125,"y_m":-949.578125,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1760,"x_m":-3139.44482421875,"y_m":-966.0303955078125,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1770,"x_m":-3165.048828125,"y_m":-982.4826049804688,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1780,"x_m":-3190.652587890625,"y_m":-998.934814453125,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1790,"x_m":-3216.256591796875,"y_m":-1015.3870849609375,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1800,"x_m":-3241.8603515625,"y_m":-1031.83935546875,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1810,"x_m":-3267.46435546875,"y_m":-1048.29150390625,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1820,"x_m":-3293.068115234375,"y_m":-1064.7437744140625,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1830,"x_m":-3318.672119140625,"y_m":-1081.196044921875,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1840,"x_m":-3344.27587890625,"y_m":-1097.648193359375,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1850,"x_m":-3369.8798828125,"y_m":-1114.1004638671875,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1860,"x_m":-3395.483642578125,"y_m":-1130.552734375,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1870,"x_m":-3421.087646484375,"y_m":-1147.0050048828125,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1880,"x_m":-3446.69140625,"y_m":-1163.4571533203125,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1890,"x_m":-3472.29541015625,"y_m":-1179.909423828125,"course_rad":4.141256332397461,"speed_mps":3.043410301208496},{"timestamp_s":1900,"x_m":-3497.899169921875,"y_m":-1196.3616943359375,"course_rad":4.141256332397461,"speed_mps":3.043410301208496}]},"target":{"id":"target","states":[{"timestamp_s":0,"x_m":-1363.1256103515625,"y_m":3282.43994140625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":10,"x_m":-1372.52001953125,"y_m":3256.0830078125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":20,"x_m":-1381.914306640625,"y_m":3229.72607421875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":30,"x_m":-1391.3087158203125,"y_m":3203.369140625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":40,"x_m":-1400.7030029296875,"y_m":3177.01220703125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":50,"x_m":-1410.097412109375,"y_m":3150.6552734375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":60,"x_m":-1419.49169921875,"y_m":3124.29833984375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":70,"x_m":-1428.8861083984375,"y_m":3097.94140625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":80,"x_m":-1438.280517578125,"y_m":3071.58447265625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":90,"x_m":-1447.6748046875,"y_m":3045.2275390625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":100,"x_m":-1457.0692138671875,"y_m":3018.87060546875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":110,"x_m":-1466.4635009765625,"y_m":2992.513671875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":120,"x_m":-1475.85791015625,"y_m":2966.15673828125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":130,"x_m":-1485.252197265625,"y_m":2939.7998046875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":140,"x_m":-1494.6466064453125,"y_m":2913.44287109375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":150,"x_m":-1504.0408935546875,"y_m":2887.086181640625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":160,"x_m":-1513.435302734375,"y_m":2860.729248046875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":170,"x_m":-1522.8297119140625,"y_m":2834.372314453125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":180,"x_m":-1532.2239990234375,"y_m":2808.015380859375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":190,"x_m":-1541.618408203125,"y_m":2781.658447265625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":200,"x_m":-1551.0126953125,"y_m":2755.301513671875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":210,"x_m":-1560.4071044921875,"y_m":2728.944580078125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":220,"x_m":-1569.8013916015625,"y_m":2702.587646484375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":230,"x_m":-1579.19580078125,"y_m":2676.230712890625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":240,"x_m":-1588.590087890625,"y_m":2649.873779296875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":250,"x_m":-1597.9844970703125,"y_m":2623.516845703125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":260,"x_m":-1607.37890625,"y_m":2597.159912109375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":270,"x_m":-1616.773193359375,"y_m":2570.802978515625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":280,"x_m":-1626.1676025390625,"y_m":2544.446044921875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":290,"x_m":-1635.5618896484375,"y_m":2518.089111328125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":300,"x_m":-1644.956298828125,"y_m":2491.732177734375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":310,"x_m":-1654.3505859375,"y_m":2465.375244140625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":320,"x_m":-1663.7449951171875,"y_m":2439.018310546875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":330,"x_m":-1673.1392822265625,"y_m":2412.661376953125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":340,"x_m":-1682.53369140625,"y_m":2386.3046875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":350,"x_m":-1691.9281005859375,"y_m":2359.94775390625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":360,"x_m":-1701.3223876953125,"y_m":2333.5908203125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":370,"x_m":-1710.716796875,"y_m":2307.23388671875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":380,"x_m":-1720.111083984375,"y_m":2280.876953125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":390,"x_m":-1729.5054931640625,"y_m":2254.52001953125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":400,"x_m":-1738.8997802734375,"y_m":2228.1630859375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":410,"x_m":-1748.294189453125,"y_m":2201.80615234375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":420,"x_m":-1757.6884765625,"y_m":2175.44921875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":430,"x_m":-1767.0828857421875,"y_m":2149.09228515625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":440,"x_m":-1776.477294921875,"y_m":2122.7353515625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":450,"x_m":-1785.87158203125,"y_m":2096.37841796875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":460,"x_m":-1795.2659912109375,"y_m":2070.021484375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":470,"x_m":-1804.6602783203125,"y_m":2043.66455078125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":480,"x_m":-1814.0546875,"y_m":2017.3077392578125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":490,"x_m":-1823.448974609375,"y_m":1990.9508056640625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":500,"x_m":-1832.8433837890625,"y_m":1964.5938720703125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":510,"x_m":-1842.2376708984375,"y_m":1938.2369384765625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":520,"x_m":-1851.632080078125,"y_m":1911.8800048828125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":530,"x_m":-1861.0263671875,"y_m":1885.5230712890625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":540,"x_m":-1870.4207763671875,"y_m":1859.1661376953125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":550,"x_m":-1879.815185546875,"y_m":1832.8092041015625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":560,"x_m":-1889.20947265625,"y_m":1806.4522705078125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":570,"x_m":-1898.6038818359375,"y_m":1780.0953369140625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":580,"x_m":-1907.9981689453125,"y_m":1753.738525390625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":590,"x_m":-1917.392578125,"y_m":1727.381591796875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":600,"x_m":-1926.786865234375,"y_m":1701.024658203125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":610,"x_m":-1936.1812744140625,"y_m":1674.667724609375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":620,"x_m":-1945.5755615234375,"y_m":1648.310791015625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":630,"x_m":-1954.969970703125,"y_m":1621.953857421875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":640,"x_m":-1964.3643798828125,"y_m":1595.596923828125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":650,"x_m":-1973.7586669921875,"y_m":1569.239990234375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":660,"x_m":-1983.153076171875,"y_m":1542.883056640625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":670,"x_m":-1992.54736328125,"y_m":1516.5262451171875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":680,"x_m":-2001.9417724609375,"y_m":1490.1693115234375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":690,"x_m":-2011.3360595703125,"y_m":1463.8123779296875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":700,"x_m":-2020.73046875,"y_m":1437.4554443359375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":710,"x_m":-2030.124755859375,"y_m":1411.0985107421875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":720,"x_m":-2039.5191650390625,"y_m":1384.7415771484375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":730,"x_m":-2048.91357421875,"y_m":1358.3846435546875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":740,"x_m":-2058.307861328125,"y_m":1332.0277099609375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":750,"x_m":-2067.7021484375,"y_m":1305.6707763671875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":760,"x_m":-2077.0966796875,"y_m":1279.31396484375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":770,"x_m":-2086.490966796875,"y_m":1252.95703125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":780,"x_m":-2095.88525390625,"y_m":1226.60009765625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":790,"x_m":-2105.279541015625,"y_m":1200.2431640625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":800,"x_m":-2114.674072265625,"y_m":1173.88623046875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":810,"x_m":-2124.068359375,"y_m":1147.529296875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":820,"x_m":-2133.462646484375,"y_m":1121.17236328125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":830,"x_m":-2142.857177734375,"y_m":1094.8154296875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":840,"x_m":-2152.25146484375,"y_m":1068.45849609375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":850,"x_m":-2161.645751953125,"y_m":1042.1015625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":860,"x_m":-2171.0400390625,"y_m":1015.7446899414062,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":870,"x_m":-2180.4345703125,"y_m":989.3877563476562,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":880,"x_m":-2189.828857421875,"y_m":963.0308837890625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":890,"x_m":-2199.22314453125,"y_m":936.6739501953125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":900,"x_m":-2208.617431640625,"y_m":910.3170166015625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":910,"x_m":-2218.011962890625,"y_m":883.9600830078125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":920,"x_m":-2227.40625,"y_m":857.6031494140625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":930,"x_m":-2236.800537109375,"y_m":831.2462768554688,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":940,"x_m":-2246.195068359375,"y_m":804.8893432617188,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":950,"x_m":-2255.58935546875,"y_m":778.5324096679688,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":960,"x_m":-2264.983642578125,"y_m":752.1754760742188,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":970,"x_m":-2274.3779296875,"y_m":725.818603515625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":980,"x_m":-2283.7724609375,"y_m":699.461669921875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":990,"x_m":-2293.166748046875,"y_m":673.104736328125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1000,"x_m":-2302.56103515625,"y_m":646.747802734375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1010,"x_m":-2311.95556640625,"y_m":620.390869140625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1020,"x_m":-2321.349853515625,"y_m":594.0339965820312,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1030,"x_m":-2330.744140625,"y_m":567.6770629882812,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1040,"x_m":-2340.138427734375,"y_m":541.3201293945312,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1050,"x_m":-2349.532958984375,"y_m":514.9631958007812,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1060,"x_m":-2358.92724609375,"y_m":488.6062927246094,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1070,"x_m":-2368.321533203125,"y_m":462.2493591308594,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1080,"x_m":-2377.7158203125,"y_m":435.8924560546875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1090,"x_m":-2387.1103515625,"y_m":409.5355224609375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1100,"x_m":-2396.504638671875,"y_m":383.1786193847656,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1110,"x_m":-2405.89892578125,"y_m":356.8216857910156,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1120,"x_m":-2415.29345703125,"y_m":330.4647521972656,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1130,"x_m":-2424.687744140625,"y_m":304.10784912109375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1140,"x_m":-2434.08203125,"y_m":277.75091552734375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1150,"x_m":-2443.476318359375,"y_m":251.3939971923828,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1160,"x_m":-2452.870849609375,"y_m":225.03707885742188,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1170,"x_m":-2462.26513671875,"y_m":198.68016052246094,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1180,"x_m":-2471.659423828125,"y_m":172.3232421875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1190,"x_m":-2481.053955078125,"y_m":145.96632385253906,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1200,"x_m":-2490.4482421875,"y_m":119.6093978881836,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1210,"x_m":-2499.842529296875,"y_m":93.25247955322266,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1220,"x_m":-2509.23681640625,"y_m":66.89556121826172,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1230,"x_m":-2518.63134765625,"y_m":40.538639068603516,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1240,"x_m":-2528.025634765625,"y_m":14.181718826293945,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1250,"x_m":-2537.419921875,"y_m":-12.175202369689941,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1260,"x_m":-2546.814208984375,"y_m":-38.53212356567383,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1270,"x_m":-2556.208740234375,"y_m":-64.88904571533203,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1280,"x_m":-2565.60302734375,"y_m":-91.24596405029297,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1290,"x_m":-2574.997314453125,"y_m":-117.6028823852539,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1300,"x_m":-2584.391845703125,"y_m":-143.95980834960938,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1310,"x_m":-2593.7861328125,"y_m":-170.3167266845703,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1320,"x_m":-2603.180419921875,"y_m":-196.67364501953125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1330,"x_m":-2612.57470703125,"y_m":-223.0305633544922,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1340,"x_m":-2621.96923828125,"y_m":-249.38748168945312,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1350,"x_m":-2631.363525390625,"y_m":-275.7444152832031,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1360,"x_m":-2640.7578125,"y_m":-302.101318359375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1370,"x_m":-2650.152099609375,"y_m":-328.458251953125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1380,"x_m":-2659.546630859375,"y_m":-354.8151550292969,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1390,"x_m":-2668.94091796875,"y_m":-381.1720886230469,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1400,"x_m":-2678.335205078125,"y_m":-407.5290222167969,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1410,"x_m":-2687.729736328125,"y_m":-433.88592529296875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1420,"x_m":-2697.1240234375,"y_m":-460.24285888671875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1430,"x_m":-2706.518310546875,"y_m":-486.5997619628906,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1440,"x_m":-2715.91259765625,"y_m":-512.9566650390625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1450,"x_m":-2725.30712890625,"y_m":-539.3135986328125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1460,"x_m":-2734.701416015625,"y_m":-565.6705322265625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1470,"x_m":-2744.095703125,"y_m":-592.0274658203125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1480,"x_m":-2753.490234375,"y_m":-618.3843994140625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1490,"x_m":-2762.884521484375,"y_m":-644.7412719726562,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1500,"x_m":-2772.27880859375,"y_m":-671.0982055664062,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1510,"x_m":-2781.673095703125,"y_m":-697.4551391601562,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1520,"x_m":-2791.067626953125,"y_m":-723.8120727539062,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1530,"x_m":-2800.4619140625,"y_m":-750.1690063476562,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1540,"x_m":-2809.856201171875,"y_m":-776.52587890625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1550,"x_m":-2819.25048828125,"y_m":-802.8828125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1560,"x_m":-2828.64501953125,"y_m":-829.23974609375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1570,"x_m":-2838.039306640625,"y_m":-855.5966796875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1580,"x_m":-2847.43359375,"y_m":-881.9535522460938,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1590,"x_m":-2856.828125,"y_m":-908.3104858398438,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1600,"x_m":-2866.222412109375,"y_m":-934.6674194335938,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1610,"x_m":-2875.61669921875,"y_m":-961.0243530273438,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1620,"x_m":-2885.010986328125,"y_m":-987.3812866210938,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1630,"x_m":-2894.405517578125,"y_m":-1013.7381591796875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1640,"x_m":-2903.7998046875,"y_m":-1040.0950927734375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1650,"x_m":-2913.194091796875,"y_m":-1066.4520263671875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1660,"x_m":-2922.588623046875,"y_m":-1092.8089599609375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1670,"x_m":-2931.98291015625,"y_m":-1119.1658935546875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1680,"x_m":-2941.377197265625,"y_m":-1145.5228271484375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1690,"x_m":-2950.771484375,"y_m":-1171.8797607421875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1700,"x_m":-2960.166015625,"y_m":-1198.236572265625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1710,"x_m":-2969.560302734375,"y_m":-1224.593505859375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1720,"x_m":-2978.95458984375,"y_m":-1250.950439453125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1730,"x_m":-2988.348876953125,"y_m":-1277.307373046875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1740,"x_m":-2997.743408203125,"y_m":-1303.664306640625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1750,"x_m":-3007.1376953125,"y_m":-1330.021240234375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1760,"x_m":-3016.531982421875,"y_m":-1356.378173828125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1770,"x_m":-3025.926513671875,"y_m":-1382.735107421875,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1780,"x_m":-3035.32080078125,"y_m":-1409.092041015625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1790,"x_m":-3044.715087890625,"y_m":-1435.4488525390625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1800,"x_m":-3054.109375,"y_m":-1461.8057861328125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1810,"x_m":-3063.50390625,"y_m":-1488.1627197265625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1820,"x_m":-3072.898193359375,"y_m":-1514.5196533203125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1830,"x_m":-3082.29248046875,"y_m":-1540.8765869140625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1840,"x_m":-3091.686767578125,"y_m":-1567.2335205078125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1850,"x_m":-3101.081298828125,"y_m":-1593.5904541015625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1860,"x_m":-3110.4755859375,"y_m":-1619.9473876953125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1870,"x_m":-3119.869873046875,"y_m":-1646.3043212890625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1880,"x_m":-3129.264404296875,"y_m":-1672.6612548828125,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1890,"x_m":-3138.65869140625,"y_m":-1699.01806640625,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715},{"timestamp_s":1900,"x_m":-3148.052978515625,"y_m":-1725.375,"course_rad":3.483982801437378,"speed_mps":2.7981085777282715}]},"observations":[{"timestamp_s":0,"bearing_rad":5.906383037567139,"valid":true},{"timestamp_s":10,"bearing_rad":5.895278453826904,"valid":true},{"timestamp_s":20,"bearing_rad":5.8939971923828125,"valid":true},{"timestamp_s":30,"bearing_rad":5.900091648101807,"valid":true},{"timestamp_s":40,"bearing_rad":5.909576416015625,"valid":true},{"timestamp_s":50,"bearing_rad":5.913343906402588,"valid":true},{"timestamp_s":60,"bearing_rad":5.902440071105957,"valid":true},{"timestamp_s":70,"bearing_rad":5.906706809997559,"valid":true},{"timestamp_s":80,"bearing_rad":5.929105758666992,"valid":true},{"timestamp_s":90,"bearing_rad":5.926067352294922,"valid":true},{"timestamp_s":100,"bearing_rad":5.9332075119018555,"valid":true},{"timestamp_s":110,"bearing_rad":5.942553520202637,"valid":true},{"timestamp_s":120,"bearing_rad":5.930111408233643,"valid":true},{"timestamp_s":130,"bearing_rad":5.935650825500488,"valid":true},{"timestamp_s":140,"bearing_rad":5.942219257354736,"valid":true},{"timestamp_s":150,"bearing_rad":5.9420952796936035,"valid":true},{"timestamp_s":160,"bearing_rad":5.96801233291626,"valid":true},{"timestamp_s":170,"bearing_rad":5.945636749267578,"valid":true},{"timestamp_s":180,"bearing_rad":5.945351600646973,"valid":true},{"timestamp_s":190,"bearing_rad":5.954253673553467,"valid":true},{"timestamp_s":200,"bearing_rad":5.9588470458984375,"valid":true},{"timestamp_s":210,"bearing_rad":5.954052925109863,"valid":true},{"timestamp_s":220,"bearing_rad":5.971007347106934,"valid":true},{"timestamp_s":230,"bearing_rad":5.985423564910889,"valid":true},{"timestamp_s":240,"bearing_rad":5.969524383544922,"valid":true},{"timestamp_s":250,"bearing_rad":5.976949691772461,"valid":true},{"timestamp_s":260,"bearing_rad":5.978240489959717,"valid":true},{"timestamp_s":270,"bearing_rad":5.99212121963501,"valid":true},{"timestamp_s":280,"bearing_rad":5.999427795410156,"valid":true},{"timestamp_s":290,"bearing_rad":5.981324672698975,"valid":true},{"timestamp_s":300,"bearing_rad":6.010334014892578,"valid":true},{"timestamp_s":310,"bearing_rad":6.016106128692627,"valid":true},{"timestamp_s":320,"bearing_rad":6.016801357269287,"valid":true},{"timestamp_s":330,"bearing_rad":6.00852632522583,"valid":true},{"timestamp_s":340,"bearing_rad":6.008671283721924,"valid":true},{"timestamp_s":350,"bearing_rad":6.022615909576416,"valid":true},{"timestamp_s":360,"bearing_rad":6.027061462402344,"valid":true},{"timestamp_s":370,"bearing_rad":6.037876605987549,"valid":true},{"timestamp_s":380,"bearing_rad":6.03715181350708,"valid":true},{"timestamp_s":390,"bearing_rad":6.047501564025879,"valid":true},{"timestamp_s":400,"bearing_rad":6.0304789543151855,"valid":true},{"timestamp_s":410,"bearing_rad":6.0523834228515625,"valid":true},{"timestamp_s":420,"bearing_rad":6.04565954208374,"valid":true},{"timestamp_s":430,"bearing_rad":6.0761542320251465,"valid":true},{"timestamp_s":440,"bearing_rad":6.079933166503906,"valid":true},{"timestamp_s":450,"bearing_rad":6.0759196281433105,"valid":true},{"timestamp_s":460,"bearing_rad":6.074718475341797,"valid":true},{"timestamp_s":470,"bearing_rad":6.08676815032959,"valid":true},{"timestamp_s":480,"bearing_rad":6.092705249786377,"valid":true},{"timestamp_s":490,"bearing_rad":6.091542720794678,"valid":true},{"timestamp_s":500,"bearing_rad":6.099732398986816,"valid":true},{"timestamp_s":510,"bearing_rad":6.111118793487549,"valid":true},{"timestamp_s":520,"bearing_rad":6.1145124435424805,"valid":true},{"timestamp_s":530,"bearing_rad":0,"valid":false},{"timestamp_s":540,"bearing_rad":6.124003887176514,"valid":true},{"timestamp_s":550,"bearing_rad":6.127457141876221,"valid":true},{"timestamp_s":560,"bearing_rad":6.1257004737854,"valid":true},{"timestamp_s":570,"bearing_rad":6.137890338897705,"valid":true},{"timestamp_s":580,"bearing_rad":6.141045093536377,"valid":true},{"timestamp_s":590,"bearing_rad":6.149081230163574,"valid":true},{"timestamp_s":600,"bearing_rad":6.146703720092773,"valid":true},{"timestamp_s":610,"bearing_rad":6.157718658447266,"valid":true},{"timestamp_s":620,"bearing_rad":6.16719388961792,"valid":true},{"timestamp_s":630,"bearing_rad":6.1772379875183105,"valid":true},{"timestamp_s":640,"bearing_rad":6.176623821258545,"valid":true},{"timestamp_s":650,"bearing_rad":6.187121391296387,"valid":true},{"timestamp_s":660,"bearing_rad":6.204688549041748,"valid":true},{"timestamp_s":670,"bearing_rad":6.202735900878906,"valid":true},{"timestamp_s":680,"bearing_rad":6.206160545349121,"valid":true},{"timestamp_s":690,"bearing_rad":6.204446792602539,"valid":true},{"timestamp_s":700,"bearing_rad":6.212564945220947,"valid":true},{"timestamp_s":710,"bearing_rad":6.222342491149902,"valid":true},{"timestamp_s":720,"bearing_rad":6.230867385864258,"valid":true},{"timestamp_s":730,"bearing_rad":6.2205891609191895,"valid":true},{"timestamp_s":740,"bearing_rad":6.247655391693115,"valid":true},{"timestamp_s":750,"bearing_rad":6.250401496887207,"valid":true},{"timestamp_s":760,"bearing_rad":6.2642717361450195,"valid":true},{"timestamp_s":770,"bearing_rad":6.260547637939453,"valid":true},{"timestamp_s":780,"bearing_rad":6.2777862548828125,"valid":true},{"timestamp_s":790,"bearing_rad":6.280605792999268,"valid":true},{"timestamp_s":800,"bearing_rad":0.015164289623498917,"valid":true},{"timestamp_s":810,"bearing_rad":0.010819771327078342,"valid":true},{"timestamp_s":820,"bearing_rad":0.0277843177318573,"valid":true},{"timestamp_s":830,"bearing_rad":0.031920693814754486,"valid":true},{"timestamp_s":840,"bearing_rad":0.03140922263264656,"valid":true},{"timestamp_s":850,"bearing_rad":0.04623942822217941,"valid":true},{"timestamp_s":860,"bearing_rad":0.06750548630952835,"valid":true},{"timestamp_s":870,"bearing_rad":0.06365349888801575,"valid":true},{"timestamp_s":880,"bearing_rad":0.06338626146316528,"valid":true},{"timestamp_s":890,"bearing_rad":0.08107762038707733,"valid":true},{"timestamp_s":900,"bearing_rad":0.09465578943490982,"valid":true},{"timestamp_s":910,"bearing_rad":0.10318737477064133,"valid":true},{"timestamp_s":920,"bearing_rad":0.11403152346611023,"valid":true},{"timestamp_s":930,"bearing_rad":0.09583351016044617,"valid":true},{"timestamp_s":940,"bearing_rad":0.09876561909914017,"valid":true},{"timestamp_s":950,"bearing_rad":0.07580819725990295,"valid":true},{"timestamp_s":960,"bearing_rad":0.06487292796373367,"valid":true},{"timestamp_s":970,"bearing_rad":0.049880288541316986,"valid":true},{"timestamp_s":980,"bearing_rad":0.0487167164683342,"valid":true},{"timestamp_s":990,"bearing_rad":0.02218852937221527,"valid":true},{"timestamp_s":1000,"bearing_rad":0.010217385366559029,"valid":true},{"timestamp_s":1010,"bearing_rad":6.276690483093262,"valid":true},{"timestamp_s":1020,"bearing_rad":6.250349998474121,"valid":true},{"timestamp_s":1030,"bearing_rad":6.235418796539307,"valid":true},{"timestamp_s":1040,"bearing_rad":6.221165180206299,"valid":true},{"timestamp_s":1050,"bearing_rad":6.198423385620117,"valid":true},{"timestamp_s":1060,"bearing_rad":6.162170886993408,"valid":true},{"timestamp_s":1070,"bearing_rad":6.14052152633667,"valid":true},{"timestamp_s":1080,"bearing_rad":6.136379718780518,"valid":true},{"timestamp_s":1090,"bearing_rad":6.0858917236328125,"valid":true},{"timestamp_s":1100,"bearing_rad":6.051519393920898,"valid":true},{"timestamp_s":1110,"bearing_rad":6.0286993980407715,"valid":true},{"timestamp_s":1120,"bearing_rad":5.988953113555908,"valid":true},{"timestamp_s":1130,"bearing_rad":5.954070091247559,"valid":true},{"timestamp_s":1140,"bearing_rad":5.9128899574279785,"valid":true},{"timestamp_s":1150,"bearing_rad":5.8641839027404785,"valid":true},{"timestamp_s":1160,"bearing_rad":5.8080854415893555,"valid":true},{"timestamp_s":1170,"bearing_rad":5.752233028411865,"valid":true},{"timestamp_s":1180,"bearing_rad":5.701921463012695,"valid":true},{"timestamp_s":1190,"bearing_rad":5.636415481567383,"valid":true},{"timestamp_s":1200,"bearing_rad":5.5772881507873535,"valid":true},{"timestamp_s":1210,"bearing_rad":5.49898624420166,"valid":true},{"timestamp_s":1220,"bearing_rad":5.443336486816406,"valid":true},{"timestamp_s":1230,"bearing_rad":5.3500494956970215,"valid":true},{"timestamp_s":1240,"bearing_rad":5.280003547668457,"valid":true},{"timestamp_s":1250,"bearing_rad":5.19453763961792,"valid":true},{"timestamp_s":1260,"bearing_rad":5.132321357727051,"valid":true},{"timestamp_s":1270,"bearing_rad":5.051031589508057,"valid":true},{"timestamp_s":1280,"bearing_rad":4.983000755310059,"valid":true},{"timestamp_s":1290,"bearing_rad":4.903331756591797,"valid":true},{"timestamp_s":1300,"bearing_rad":4.8715667724609375,"valid":true},{"timestamp_s":1310,"bearing_rad":4.831680774688721,"valid":true},{"timestamp_s":1320,"bearing_rad":4.792198657989502,"valid":true},{"timestamp_s":1330,"bearing_rad":4.771928310394287,"valid":true},{"timestamp_s":1340,"bearing_rad":4.762206077575684,"valid":true},{"timestamp_s":1350,"bearing_rad":4.733325481414795,"valid":true},{"timestamp_s":1360,"bearing_rad":4.721972465515137,"valid":true},{"timestamp_s":1370,"bearing_rad":4.712935447692871,"valid":true},{"timestamp_s":1380,"bearing_rad":4.684051990509033,"valid":true},{"timestamp_s":1390,"bearing_rad":4.671960353851318,"valid":true},{"timestamp_s":1400,"bearing_rad":4.639034271240234,"valid":true},{"timestamp_s":1410,"bearing_rad":4.604732513427734,"valid":true},{"timestamp_s":1420,"bearing_rad":4.584641933441162,"valid":true},{"timestamp_s":1430,"bearing_rad":4.5550665855407715,"valid":true},{"timestamp_s":1440,"bearing_rad":4.539829254150391,"valid":true},{"timestamp_s":1450,"bearing_rad":0,"valid":false},{"timestamp_s":1460,"bearing_rad":4.465482711791992,"valid":true},{"timestamp_s":1470,"bearing_rad":4.435017108917236,"valid":true},{"timestamp_s":1480,"bearing_rad":4.375346660614014,"valid":true},{"timestamp_s":1490,"bearing_rad":4.325811862945557,"valid":true},{"timestamp_s":1500,"bearing_rad":4.294029235839844,"valid":true},{"timestamp_s":1510,"bearing_rad":4.246122360229492,"valid":true},{"timestamp_s":1520,"bearing_rad":4.197150230407715,"valid":true},{"timestamp_s":1530,"bearing_rad":4.128900527954102,"valid":true},{"timestamp_s":1540,"bearing_rad":4.07097053527832,"valid":true},{"timestamp_s":1550,"bearing_rad":4.001125335693359,"valid":true},{"timestamp_s":1560,"bearing_rad":3.935152530670166,"valid":true},{"timestamp_s":1570,"bearing_rad":3.8685665130615234,"valid":true},{"timestamp_s":1580,"bearing_rad":3.8123066425323486,"valid":true},{"timestamp_s":1590,"bearing_rad":3.7524290084838867,"valid":true},{"timestamp_s":1600,"bearing_rad":3.667816638946533,"valid":true},{"timestamp_s":1610,"bearing_rad":3.5926198959350586,"valid":true},{"timestamp_s":1620,"bearing_rad":3.536897659301758,"valid":true},{"timestamp_s":1630,"bearing_rad":3.4653754234313965,"valid":true},{"timestamp_s":1640,"bearing_rad":3.412123680114746,"valid":true},{"timestamp_s":1650,"bearing_rad":3.3304245471954346,"valid":true},{"timestamp_s":1660,"bearing_rad":3.274794101715088,"valid":true},{"timestamp_s":1670,"bearing_rad":3.209526300430298,"valid":true},{"timestamp_s":1680,"bearing_rad":3.1556882858276367,"valid":true},{"timestamp_s":1690,"bearing_rad":3.1050310134887695,"valid":true},{"timestamp_s":1700,"bearing_rad":3.064903974533081,"valid":true},{"timestamp_s":1710,"bearing_rad":3.0131306648254395,"valid":true},{"timestamp_s":1720,"bearing_rad":2.962817668914795,"valid":true},{"timestamp_s":1730,"bearing_rad":2.9363975524902344,"valid":true},{"timestamp_s":1740,"bearing_rad":2.882406711578369,"valid":true},{"timestamp_s":1750,"bearing_rad":2.8614420890808105,"valid":true},{"timestamp_s":1760,"bearing_rad":2.8418304920196533,"valid":true},{"timestamp_s":1770,"bearing_rad":2.816624879837036,"valid":true},{"timestamp_s":1780,"bearing_rad":2.7845375537872314,"valid":true},{"timestamp_s":1790,"bearing_rad":2.7495028972625732,"valid":true},{"timestamp_s":1800,"bearing_rad":2.740346908569336,"valid":true},{"timestamp_s":1810,"bearing_rad":2.7051634788513184,"valid":true},{"timestamp_s":1820,"bearing_rad":2.6906163692474365,"valid":true},{"timestamp_s":1830,"bearing_rad":2.6648900508880615,"valid":true},{"timestamp_s":1840,"bearing_rad":2.664102792739868,"valid":true},{"timestamp_s":1850,"bearing_rad":2.6268198490142822,"valid":true},{"timestamp_s":1860,"bearing_rad":2.602678060531616,"valid":true},{"timestamp_s":1870,"bearing_rad":2.6012277603149414,"valid":true},{"timestamp_s":1880,"bearing_rad":2.5853309631347656,"valid":true},{"timestamp_s":1890,"bearing_rad":2.576145648956299,"valid":true},{"timestamp_s":1900,"bearing_rad":2.5620815753936768,"valid":true}]} +{"record":"scenario","metadata":{"seed":"17911839290282890590","index":2,"parameters":{"window_s":1500,"dt_s":10,"ownship_speed_mps":3.7463133335113525,"ownship_course0_rad":1.0907975435256958,"zig_angle_rad":-0.7997997403144836,"zig_fraction":0.42195966839790344,"target_range0_m":14055.9228515625,"target_bearing0_rad":1.1028945446014404,"target_speed_mps":3.778413772583008,"target_course_rad":4.862672805786133,"noise_sigma_rad":0.03490658476948738,"dropout_fraction":0.009396178647875786,"ownship_turn_rate_rad_s":0.04983744025230408,"zig2_delay_s":272.3873596191406,"zig2_angle_rad":1.433792233467102},"samples":150,"zig_index":65,"turn_start_index":64,"zig2_index":95,"turn2_start_index":93,"zig_time_s":650,"mean_range_m":9429.6455078125,"min_range_m":5866.14306640625},"ownship":{"id":"ownship","states":[{"timestamp_s":0,"x_m":0,"y_m":0,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":10,"x_m":33.22962951660156,"y_m":17.299654006958008,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":20,"x_m":66.45925903320312,"y_m":34.599308013916016,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":30,"x_m":99.68888854980469,"y_m":51.898963928222656,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":40,"x_m":132.91851806640625,"y_m":69.19861602783203,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":50,"x_m":166.1481475830078,"y_m":86.49827575683594,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":60,"x_m":199.37777709960938,"y_m":103.79792785644531,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":70,"x_m":232.60740661621094,"y_m":121.09757995605469,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":80,"x_m":265.8370361328125,"y_m":138.39723205566406,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":90,"x_m":299.0666809082031,"y_m":155.69688415527344,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":100,"x_m":332.2962951660156,"y_m":172.99655151367188,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":110,"x_m":365.52593994140625,"y_m":190.29620361328125,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":120,"x_m":398.75555419921875,"y_m":207.59585571289062,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":130,"x_m":431.9851989746094,"y_m":224.8955078125,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":140,"x_m":465.2148132324219,"y_m":242.19515991210938,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":150,"x_m":498.4444580078125,"y_m":259.49481201171875,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":160,"x_m":531.674072265625,"y_m":276.7944641113281,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":170,"x_m":564.9036865234375,"y_m":294.0941162109375,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":180,"x_m":598.1333618164062,"y_m":311.3937683105469,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":190,"x_m":631.3629760742188,"y_m":328.69342041015625,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":200,"x_m":664.5925903320312,"y_m":345.99310302734375,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":210,"x_m":697.8222045898438,"y_m":363.2927551269531,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":220,"x_m":731.0518798828125,"y_m":380.5924072265625,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":230,"x_m":764.281494140625,"y_m":397.8920593261719,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":240,"x_m":797.5111083984375,"y_m":415.19171142578125,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":250,"x_m":830.74072265625,"y_m":432.4913635253906,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":260,"x_m":863.9703979492188,"y_m":449.791015625,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":270,"x_m":897.2000122070312,"y_m":467.0906677246094,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":280,"x_m":930.4296264648438,"y_m":484.39031982421875,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":290,"x_m":963.6593017578125,"y_m":501.6899719238281,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":300,"x_m":996.888916015625,"y_m":518.9896240234375,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":310,"x_m":1030.1185302734375,"y_m":536.289306640625,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":320,"x_m":1063.34814453125,"y_m":553.5889282226562,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":330,"x_m":1096.5777587890625,"y_m":570.8886108398438,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":340,"x_m":1129.807373046875,"y_m":588.188232421875,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":350,"x_m":1163.037109375,"y_m":605.4879150390625,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":360,"x_m":1196.2667236328125,"y_m":622.7875366210938,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":370,"x_m":1229.496337890625,"y_m":640.0872192382812,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":380,"x_m":1262.7259521484375,"y_m":657.3868408203125,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":390,"x_m":1295.95556640625,"y_m":674.6865234375,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":400,"x_m":1329.1851806640625,"y_m":691.9862060546875,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":410,"x_m":1362.414794921875,"y_m":709.2858276367188,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":420,"x_m":1395.6444091796875,"y_m":726.5855102539062,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":430,"x_m":1428.8741455078125,"y_m":743.8851318359375,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":440,"x_m":1462.103759765625,"y_m":761.184814453125,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":450,"x_m":1495.3333740234375,"y_m":778.4844360351562,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":460,"x_m":1528.56298828125,"y_m":795.7841186523438,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":470,"x_m":1561.7926025390625,"y_m":813.083740234375,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":480,"x_m":1595.022216796875,"y_m":830.3834228515625,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":490,"x_m":1628.2518310546875,"y_m":847.6830444335938,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":500,"x_m":1661.4814453125,"y_m":864.9827270507812,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":510,"x_m":1694.711181640625,"y_m":882.2823486328125,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":520,"x_m":1727.9407958984375,"y_m":899.58203125,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":530,"x_m":1761.17041015625,"y_m":916.8816528320312,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":540,"x_m":1794.4000244140625,"y_m":934.1813354492188,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":550,"x_m":1827.629638671875,"y_m":951.4810180664062,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":560,"x_m":1860.8592529296875,"y_m":968.7806396484375,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":570,"x_m":1894.0888671875,"y_m":986.080322265625,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":580,"x_m":1927.318603515625,"y_m":1003.3799438476562,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":590,"x_m":1960.5482177734375,"y_m":1020.6796264648438,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":600,"x_m":1993.77783203125,"y_m":1037.979248046875,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":610,"x_m":2027.0074462890625,"y_m":1055.2789306640625,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":620,"x_m":2060.237060546875,"y_m":1072.57861328125,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":630,"x_m":2093.466796875,"y_m":1089.878173828125,"course_rad":1.0907975435256958,"speed_mps":3.7463133335113525},{"timestamp_s":640,"x_m":2121.115478515625,"y_m":1114.5810546875,"course_rad":0.592423141002655,"speed_mps":3.7463133335113525},{"timestamp_s":650,"x_m":2135.012451171875,"y_m":1149.1695556640625,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":660,"x_m":2145.760986328125,"y_m":1185.0576171875,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":670,"x_m":2156.509521484375,"y_m":1220.94580078125,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":680,"x_m":2167.2578125,"y_m":1256.8338623046875,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":690,"x_m":2178.00634765625,"y_m":1292.7220458984375,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":700,"x_m":2188.7548828125,"y_m":1328.610107421875,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":710,"x_m":2199.50341796875,"y_m":1364.4981689453125,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":720,"x_m":2210.251953125,"y_m":1400.3863525390625,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":730,"x_m":2221.000244140625,"y_m":1436.2744140625,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":740,"x_m":2231.748779296875,"y_m":1472.16259765625,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":750,"x_m":2242.497314453125,"y_m":1508.0506591796875,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":760,"x_m":2253.245849609375,"y_m":1543.938720703125,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":770,"x_m":2263.994140625,"y_m":1579.826904296875,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":780,"x_m":2274.74267578125,"y_m":1615.7149658203125,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":790,"x_m":2285.4912109375,"y_m":1651.6031494140625,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":800,"x_m":2296.23974609375,"y_m":1687.4912109375,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":810,"x_m":2306.98828125,"y_m":1723.3792724609375,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":820,"x_m":2317.736572265625,"y_m":1759.2674560546875,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":830,"x_m":2328.485107421875,"y_m":1795.155517578125,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":840,"x_m":2339.233642578125,"y_m":1831.043701171875,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":850,"x_m":2349.982177734375,"y_m":1866.9317626953125,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":860,"x_m":2360.73046875,"y_m":1902.81982421875,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":870,"x_m":2371.47900390625,"y_m":1938.7080078125,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":880,"x_m":2382.2275390625,"y_m":1974.5960693359375,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":890,"x_m":2392.97607421875,"y_m":2010.4842529296875,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":900,"x_m":2403.724609375,"y_m":2046.372314453125,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":910,"x_m":2414.472900390625,"y_m":2082.260498046875,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":920,"x_m":2425.221435546875,"y_m":2118.1484375,"course_rad":0.29099780321121216,"speed_mps":3.7463133335113525},{"timestamp_s":930,"x_m":2444.289794921875,"y_m":2149.946044921875,"course_rad":0.7893722057342529,"speed_mps":3.7463133335113525},{"timestamp_s":940,"x_m":2476.23779296875,"y_m":2168.760986328125,"course_rad":1.2877466678619385,"speed_mps":3.7463133335113525},{"timestamp_s":950,"x_m":2513.317626953125,"y_m":2170.155517578125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":960,"x_m":2550.33740234375,"y_m":2164.4091796875,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":970,"x_m":2587.357177734375,"y_m":2158.662841796875,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":980,"x_m":2624.377197265625,"y_m":2152.91650390625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":990,"x_m":2661.39697265625,"y_m":2147.170166015625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1000,"x_m":2698.416748046875,"y_m":2141.423828125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1010,"x_m":2735.4365234375,"y_m":2135.677490234375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1020,"x_m":2772.456298828125,"y_m":2129.93115234375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1030,"x_m":2809.47607421875,"y_m":2124.18505859375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1040,"x_m":2846.495849609375,"y_m":2118.438720703125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1050,"x_m":2883.515869140625,"y_m":2112.6923828125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1060,"x_m":2920.53564453125,"y_m":2106.946044921875,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1070,"x_m":2957.555419921875,"y_m":2101.19970703125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1080,"x_m":2994.5751953125,"y_m":2095.453369140625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1090,"x_m":3031.594970703125,"y_m":2089.70703125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1100,"x_m":3068.61474609375,"y_m":2083.960693359375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1110,"x_m":3105.634521484375,"y_m":2078.21435546875,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1120,"x_m":3142.654296875,"y_m":2072.468017578125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1130,"x_m":3179.67431640625,"y_m":2066.721923828125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1140,"x_m":3216.694091796875,"y_m":2060.9755859375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1150,"x_m":3253.7138671875,"y_m":2055.229248046875,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1160,"x_m":3290.733642578125,"y_m":2049.48291015625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1170,"x_m":3327.75341796875,"y_m":2043.736572265625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1180,"x_m":3364.773193359375,"y_m":2037.990234375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1190,"x_m":3401.79296875,"y_m":2032.243896484375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1200,"x_m":3438.81298828125,"y_m":2026.4976806640625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1210,"x_m":3475.832763671875,"y_m":2020.7513427734375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1220,"x_m":3512.8525390625,"y_m":2015.0050048828125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1230,"x_m":3549.872314453125,"y_m":2009.2586669921875,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1240,"x_m":3586.89208984375,"y_m":2003.5123291015625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1250,"x_m":3623.911865234375,"y_m":1997.76611328125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1260,"x_m":3660.931640625,"y_m":1992.019775390625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1270,"x_m":3697.95166015625,"y_m":1986.2734375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1280,"x_m":3734.971435546875,"y_m":1980.527099609375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1290,"x_m":3771.9912109375,"y_m":1974.78076171875,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1300,"x_m":3809.010986328125,"y_m":1969.0345458984375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1310,"x_m":3846.03076171875,"y_m":1963.2882080078125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1320,"x_m":3883.050537109375,"y_m":1957.5418701171875,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1330,"x_m":3920.0703125,"y_m":1951.7955322265625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1340,"x_m":3957.09033203125,"y_m":1946.0491943359375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1350,"x_m":3994.110107421875,"y_m":1940.302978515625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1360,"x_m":4031.1298828125,"y_m":1934.556640625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1370,"x_m":4068.149658203125,"y_m":1928.810302734375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1380,"x_m":4105.16943359375,"y_m":1923.06396484375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1390,"x_m":4142.189453125,"y_m":1917.3177490234375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1400,"x_m":4179.208984375,"y_m":1911.5714111328125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1410,"x_m":4216.22900390625,"y_m":1905.8250732421875,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1420,"x_m":4253.24853515625,"y_m":1900.0787353515625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1430,"x_m":4290.2685546875,"y_m":1894.3323974609375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1440,"x_m":4327.2880859375,"y_m":1888.586181640625,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1450,"x_m":4364.30810546875,"y_m":1882.83984375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1460,"x_m":4401.328125,"y_m":1877.093505859375,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1470,"x_m":4438.34765625,"y_m":1871.34716796875,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1480,"x_m":4475.36767578125,"y_m":1865.600830078125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525},{"timestamp_s":1490,"x_m":4512.38720703125,"y_m":1859.8546142578125,"course_rad":1.724790096282959,"speed_mps":3.7463133335113525}]},"target":{"id":"target","states":[{"timestamp_s":0,"x_m":12545.14453125,"y_m":6339.42626953125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":10,"x_m":12507.7861328125,"y_m":6345.0830078125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":20,"x_m":12470.427734375,"y_m":6350.740234375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":30,"x_m":12433.0693359375,"y_m":6356.39697265625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":40,"x_m":12395.7109375,"y_m":6362.05419921875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":50,"x_m":12358.3525390625,"y_m":6367.71142578125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":60,"x_m":12320.994140625,"y_m":6373.3681640625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":70,"x_m":12283.63671875,"y_m":6379.025390625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":80,"x_m":12246.2783203125,"y_m":6384.68212890625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":90,"x_m":12208.919921875,"y_m":6390.33935546875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":100,"x_m":12171.5615234375,"y_m":6395.99609375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":110,"x_m":12134.203125,"y_m":6401.6533203125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":120,"x_m":12096.8447265625,"y_m":6407.31005859375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":130,"x_m":12059.4873046875,"y_m":6412.96728515625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":140,"x_m":12022.12890625,"y_m":6418.6240234375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":150,"x_m":11984.7705078125,"y_m":6424.28125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":160,"x_m":11947.412109375,"y_m":6429.93798828125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":170,"x_m":11910.0537109375,"y_m":6435.59521484375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":180,"x_m":11872.6953125,"y_m":6441.251953125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":190,"x_m":11835.3369140625,"y_m":6446.9091796875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":200,"x_m":11797.9794921875,"y_m":6452.56591796875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":210,"x_m":11760.62109375,"y_m":6458.22314453125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":220,"x_m":11723.2626953125,"y_m":6463.8798828125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":230,"x_m":11685.904296875,"y_m":6469.537109375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":240,"x_m":11648.5458984375,"y_m":6475.19384765625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":250,"x_m":11611.1875,"y_m":6480.85107421875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":260,"x_m":11573.8291015625,"y_m":6486.50830078125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":270,"x_m":11536.4716796875,"y_m":6492.1650390625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":280,"x_m":11499.11328125,"y_m":6497.822265625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":290,"x_m":11461.7548828125,"y_m":6503.47900390625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":300,"x_m":11424.396484375,"y_m":6509.13623046875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":310,"x_m":11387.0380859375,"y_m":6514.79296875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":320,"x_m":11349.6796875,"y_m":6520.4501953125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":330,"x_m":11312.3212890625,"y_m":6526.10693359375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":340,"x_m":11274.9638671875,"y_m":6531.76416015625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":350,"x_m":11237.60546875,"y_m":6537.4208984375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":360,"x_m":11200.2470703125,"y_m":6543.078125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":370,"x_m":11162.888671875,"y_m":6548.73486328125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":380,"x_m":11125.5302734375,"y_m":6554.39208984375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":390,"x_m":11088.171875,"y_m":6560.048828125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":400,"x_m":11050.8134765625,"y_m":6565.7060546875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":410,"x_m":11013.4560546875,"y_m":6571.36279296875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":420,"x_m":10976.09765625,"y_m":6577.02001953125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":430,"x_m":10938.7392578125,"y_m":6582.6767578125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":440,"x_m":10901.380859375,"y_m":6588.333984375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":450,"x_m":10864.0224609375,"y_m":6593.99072265625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":460,"x_m":10826.6640625,"y_m":6599.64794921875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":470,"x_m":10789.3056640625,"y_m":6605.30517578125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":480,"x_m":10751.9482421875,"y_m":6610.9619140625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":490,"x_m":10714.58984375,"y_m":6616.619140625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":500,"x_m":10677.2314453125,"y_m":6622.27587890625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":510,"x_m":10639.873046875,"y_m":6627.93310546875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":520,"x_m":10602.5146484375,"y_m":6633.58984375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":530,"x_m":10565.15625,"y_m":6639.2470703125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":540,"x_m":10527.7978515625,"y_m":6644.90380859375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":550,"x_m":10490.4404296875,"y_m":6650.56103515625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":560,"x_m":10453.08203125,"y_m":6656.2177734375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":570,"x_m":10415.7236328125,"y_m":6661.875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":580,"x_m":10378.365234375,"y_m":6667.53173828125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":590,"x_m":10341.0068359375,"y_m":6673.18896484375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":600,"x_m":10303.6484375,"y_m":6678.845703125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":610,"x_m":10266.2900390625,"y_m":6684.5029296875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":620,"x_m":10228.9326171875,"y_m":6690.15966796875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":630,"x_m":10191.57421875,"y_m":6695.81689453125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":640,"x_m":10154.2158203125,"y_m":6701.4736328125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":650,"x_m":10116.857421875,"y_m":6707.130859375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":660,"x_m":10079.4990234375,"y_m":6712.7880859375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":670,"x_m":10042.140625,"y_m":6718.44482421875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":680,"x_m":10004.7822265625,"y_m":6724.10205078125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":690,"x_m":9967.4248046875,"y_m":6729.7587890625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":700,"x_m":9930.06640625,"y_m":6735.416015625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":710,"x_m":9892.7080078125,"y_m":6741.07275390625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":720,"x_m":9855.349609375,"y_m":6746.72998046875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":730,"x_m":9817.9912109375,"y_m":6752.38671875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":740,"x_m":9780.6328125,"y_m":6758.0439453125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":750,"x_m":9743.2744140625,"y_m":6763.70068359375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":760,"x_m":9705.9169921875,"y_m":6769.35791015625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":770,"x_m":9668.55859375,"y_m":6775.0146484375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":780,"x_m":9631.2001953125,"y_m":6780.671875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":790,"x_m":9593.841796875,"y_m":6786.32861328125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":800,"x_m":9556.4833984375,"y_m":6791.98583984375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":810,"x_m":9519.125,"y_m":6797.642578125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":820,"x_m":9481.7666015625,"y_m":6803.2998046875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":830,"x_m":9444.4091796875,"y_m":6808.95654296875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":840,"x_m":9407.05078125,"y_m":6814.61376953125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":850,"x_m":9369.6923828125,"y_m":6820.2705078125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":860,"x_m":9332.333984375,"y_m":6825.927734375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":870,"x_m":9294.9755859375,"y_m":6831.5849609375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":880,"x_m":9257.6171875,"y_m":6837.24169921875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":890,"x_m":9220.2587890625,"y_m":6842.89892578125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":900,"x_m":9182.9013671875,"y_m":6848.5556640625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":910,"x_m":9145.54296875,"y_m":6854.212890625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":920,"x_m":9108.1845703125,"y_m":6859.86962890625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":930,"x_m":9070.826171875,"y_m":6865.52685546875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":940,"x_m":9033.4677734375,"y_m":6871.18359375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":950,"x_m":8996.109375,"y_m":6876.8408203125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":960,"x_m":8958.7509765625,"y_m":6882.49755859375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":970,"x_m":8921.3935546875,"y_m":6888.15478515625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":980,"x_m":8884.03515625,"y_m":6893.8115234375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":990,"x_m":8846.6767578125,"y_m":6899.46875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1000,"x_m":8809.318359375,"y_m":6905.12548828125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1010,"x_m":8771.9599609375,"y_m":6910.78271484375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1020,"x_m":8734.6015625,"y_m":6916.439453125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1030,"x_m":8697.2431640625,"y_m":6922.0966796875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1040,"x_m":8659.8857421875,"y_m":6927.75341796875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1050,"x_m":8622.52734375,"y_m":6933.41064453125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1060,"x_m":8585.1689453125,"y_m":6939.0673828125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1070,"x_m":8547.810546875,"y_m":6944.724609375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1080,"x_m":8510.4521484375,"y_m":6950.3818359375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1090,"x_m":8473.09375,"y_m":6956.03857421875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1100,"x_m":8435.7353515625,"y_m":6961.69580078125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1110,"x_m":8398.3779296875,"y_m":6967.3525390625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1120,"x_m":8361.01953125,"y_m":6973.009765625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1130,"x_m":8323.6611328125,"y_m":6978.66650390625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1140,"x_m":8286.302734375,"y_m":6984.32373046875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1150,"x_m":8248.9443359375,"y_m":6989.98046875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1160,"x_m":8211.5859375,"y_m":6995.6376953125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1170,"x_m":8174.22802734375,"y_m":7001.29443359375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1180,"x_m":8136.86962890625,"y_m":7006.95166015625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1190,"x_m":8099.51123046875,"y_m":7012.6083984375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1200,"x_m":8062.1533203125,"y_m":7018.265625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1210,"x_m":8024.794921875,"y_m":7023.92236328125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1220,"x_m":7987.4365234375,"y_m":7029.57958984375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1230,"x_m":7950.07861328125,"y_m":7035.236328125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1240,"x_m":7912.72021484375,"y_m":7040.8935546875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1250,"x_m":7875.36181640625,"y_m":7046.55029296875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1260,"x_m":7838.00341796875,"y_m":7052.20751953125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1270,"x_m":7800.6455078125,"y_m":7057.86474609375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1280,"x_m":7763.287109375,"y_m":7063.521484375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1290,"x_m":7725.9287109375,"y_m":7069.1787109375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1300,"x_m":7688.57080078125,"y_m":7074.83544921875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1310,"x_m":7651.21240234375,"y_m":7080.49267578125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1320,"x_m":7613.85400390625,"y_m":7086.1494140625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1330,"x_m":7576.49560546875,"y_m":7091.806640625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1340,"x_m":7539.1376953125,"y_m":7097.46337890625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1350,"x_m":7501.779296875,"y_m":7103.12060546875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1360,"x_m":7464.4208984375,"y_m":7108.77734375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1370,"x_m":7427.06298828125,"y_m":7114.4345703125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1380,"x_m":7389.70458984375,"y_m":7120.09130859375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1390,"x_m":7352.34619140625,"y_m":7125.74853515625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1400,"x_m":7314.98779296875,"y_m":7131.4052734375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1410,"x_m":7277.6298828125,"y_m":7137.0625,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1420,"x_m":7240.271484375,"y_m":7142.71923828125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1430,"x_m":7202.9130859375,"y_m":7148.37646484375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1440,"x_m":7165.55517578125,"y_m":7154.033203125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1450,"x_m":7128.19677734375,"y_m":7159.6904296875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1460,"x_m":7090.83837890625,"y_m":7165.34716796875,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1470,"x_m":7053.47998046875,"y_m":7171.00439453125,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1480,"x_m":7016.1220703125,"y_m":7176.66162109375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008},{"timestamp_s":1490,"x_m":6978.763671875,"y_m":7182.318359375,"course_rad":4.862672805786133,"speed_mps":3.778413772583008}]},"observations":[{"timestamp_s":0,"bearing_rad":1.07805597782135,"valid":true},{"timestamp_s":10,"bearing_rad":1.0734949111938477,"valid":true},{"timestamp_s":20,"bearing_rad":1.072232961654663,"valid":true},{"timestamp_s":30,"bearing_rad":1.0506222248077393,"valid":true},{"timestamp_s":40,"bearing_rad":1.1526650190353394,"valid":true},{"timestamp_s":50,"bearing_rad":1.1080447435379028,"valid":true},{"timestamp_s":60,"bearing_rad":1.1220102310180664,"valid":true},{"timestamp_s":70,"bearing_rad":1.105557918548584,"valid":true},{"timestamp_s":80,"bearing_rad":1.1586718559265137,"valid":true},{"timestamp_s":90,"bearing_rad":1.1116137504577637,"valid":true},{"timestamp_s":100,"bearing_rad":1.03914475440979,"valid":true},{"timestamp_s":110,"bearing_rad":1.0916523933410645,"valid":true},{"timestamp_s":120,"bearing_rad":1.1200560331344604,"valid":true},{"timestamp_s":130,"bearing_rad":1.0436097383499146,"valid":true},{"timestamp_s":140,"bearing_rad":1.0882441997528076,"valid":true},{"timestamp_s":150,"bearing_rad":1.0579177141189575,"valid":true},{"timestamp_s":160,"bearing_rad":1.0306917428970337,"valid":true},{"timestamp_s":170,"bearing_rad":1.0713303089141846,"valid":true},{"timestamp_s":180,"bearing_rad":0.9585126042366028,"valid":true},{"timestamp_s":190,"bearing_rad":1.0273325443267822,"valid":true},{"timestamp_s":200,"bearing_rad":1.035136103630066,"valid":true},{"timestamp_s":210,"bearing_rad":1.0788922309875488,"valid":true},{"timestamp_s":220,"bearing_rad":1.0981566905975342,"valid":true},{"timestamp_s":230,"bearing_rad":1.052037000656128,"valid":true},{"timestamp_s":240,"bearing_rad":1.122952938079834,"valid":true},{"timestamp_s":250,"bearing_rad":1.1335420608520508,"valid":true},{"timestamp_s":260,"bearing_rad":1.0808552503585815,"valid":true},{"timestamp_s":270,"bearing_rad":1.0475733280181885,"valid":true},{"timestamp_s":280,"bearing_rad":1.0292408466339111,"valid":true},{"timestamp_s":290,"bearing_rad":0,"valid":false},{"timestamp_s":300,"bearing_rad":1.0136140584945679,"valid":true},{"timestamp_s":310,"bearing_rad":1.0546202659606934,"valid":true},{"timestamp_s":320,"bearing_rad":1.0235497951507568,"valid":true},{"timestamp_s":330,"bearing_rad":1.0823923349380493,"valid":true},{"timestamp_s":340,"bearing_rad":1.0430229902267456,"valid":true},{"timestamp_s":350,"bearing_rad":1.0192588567733765,"valid":true},{"timestamp_s":360,"bearing_rad":1.046432614326477,"valid":true},{"timestamp_s":370,"bearing_rad":0.9979828000068665,"valid":true},{"timestamp_s":380,"bearing_rad":1.0330370664596558,"valid":true},{"timestamp_s":390,"bearing_rad":0.9522155523300171,"valid":true},{"timestamp_s":400,"bearing_rad":1.075424313545227,"valid":true},{"timestamp_s":410,"bearing_rad":0.961553156375885,"valid":true},{"timestamp_s":420,"bearing_rad":1.0268341302871704,"valid":true},{"timestamp_s":430,"bearing_rad":1.0623743534088135,"valid":true},{"timestamp_s":440,"bearing_rad":1.0529552698135376,"valid":true},{"timestamp_s":450,"bearing_rad":1.032058835029602,"valid":true},{"timestamp_s":460,"bearing_rad":0.9804391264915466,"valid":true},{"timestamp_s":470,"bearing_rad":1.0115212202072144,"valid":true},{"timestamp_s":480,"bearing_rad":0.9518004655838013,"valid":true},{"timestamp_s":490,"bearing_rad":1.0317883491516113,"valid":true},{"timestamp_s":500,"bearing_rad":0.9754195809364319,"valid":true},{"timestamp_s":510,"bearing_rad":0.9663223028182983,"valid":true},{"timestamp_s":520,"bearing_rad":1.0016543865203857,"valid":true},{"timestamp_s":530,"bearing_rad":0.9797402024269104,"valid":true},{"timestamp_s":540,"bearing_rad":0.966889500617981,"valid":true},{"timestamp_s":550,"bearing_rad":0.9811877608299255,"valid":true},{"timestamp_s":560,"bearing_rad":1.0258958339691162,"valid":true},{"timestamp_s":570,"bearing_rad":1.0052769184112549,"valid":true},{"timestamp_s":580,"bearing_rad":0.9333292245864868,"valid":true},{"timestamp_s":590,"bearing_rad":0.9466297030448914,"valid":true},{"timestamp_s":600,"bearing_rad":0.9648313522338867,"valid":true},{"timestamp_s":610,"bearing_rad":0.9460103511810303,"valid":true},{"timestamp_s":620,"bearing_rad":0.926662802696228,"valid":true},{"timestamp_s":630,"bearing_rad":0.9518508315086365,"valid":true},{"timestamp_s":640,"bearing_rad":1.0173954963684082,"valid":true},{"timestamp_s":650,"bearing_rad":0,"valid":false},{"timestamp_s":660,"bearing_rad":0.9560616612434387,"valid":true},{"timestamp_s":670,"bearing_rad":0.9625765085220337,"valid":true},{"timestamp_s":680,"bearing_rad":0.9475531578063965,"valid":true},{"timestamp_s":690,"bearing_rad":1.0066072940826416,"valid":true},{"timestamp_s":700,"bearing_rad":0.9403138160705566,"valid":true},{"timestamp_s":710,"bearing_rad":0.9100679755210876,"valid":true},{"timestamp_s":720,"bearing_rad":0.9452468156814575,"valid":true},{"timestamp_s":730,"bearing_rad":0.9511740207672119,"valid":true},{"timestamp_s":740,"bearing_rad":0.9591168761253357,"valid":true},{"timestamp_s":750,"bearing_rad":0.9733223915100098,"valid":true},{"timestamp_s":760,"bearing_rad":0.9437837600708008,"valid":true},{"timestamp_s":770,"bearing_rad":0.9672721028327942,"valid":true},{"timestamp_s":780,"bearing_rad":0.9361018538475037,"valid":true},{"timestamp_s":790,"bearing_rad":0.9797233939170837,"valid":true},{"timestamp_s":800,"bearing_rad":0.9659526944160461,"valid":true},{"timestamp_s":810,"bearing_rad":0.9624016880989075,"valid":true},{"timestamp_s":820,"bearing_rad":0.9263776540756226,"valid":true},{"timestamp_s":830,"bearing_rad":0.9750717282295227,"valid":true},{"timestamp_s":840,"bearing_rad":0.9965587258338928,"valid":true},{"timestamp_s":850,"bearing_rad":0.9687344431877136,"valid":true},{"timestamp_s":860,"bearing_rad":0.9131878018379211,"valid":true},{"timestamp_s":870,"bearing_rad":0.9953881502151489,"valid":true},{"timestamp_s":880,"bearing_rad":0.9945214986801147,"valid":true},{"timestamp_s":890,"bearing_rad":0.9322510957717896,"valid":true},{"timestamp_s":900,"bearing_rad":0.9528234004974365,"valid":true},{"timestamp_s":910,"bearing_rad":0.9534602761268616,"valid":true},{"timestamp_s":920,"bearing_rad":1.0080870389938354,"valid":true},{"timestamp_s":930,"bearing_rad":0.9508976936340332,"valid":true},{"timestamp_s":940,"bearing_rad":0.9454882740974426,"valid":true},{"timestamp_s":950,"bearing_rad":0.9717373251914978,"valid":true},{"timestamp_s":960,"bearing_rad":0.8995761275291443,"valid":true},{"timestamp_s":970,"bearing_rad":0.9556267857551575,"valid":true},{"timestamp_s":980,"bearing_rad":0.8883545398712158,"valid":true},{"timestamp_s":990,"bearing_rad":0.8682727217674255,"valid":true},{"timestamp_s":1000,"bearing_rad":0.9140920639038086,"valid":true},{"timestamp_s":1010,"bearing_rad":0.9350923299789429,"valid":true},{"timestamp_s":1020,"bearing_rad":0.9515960812568665,"valid":true},{"timestamp_s":1030,"bearing_rad":0.910459041595459,"valid":true},{"timestamp_s":1040,"bearing_rad":0.8305002450942993,"valid":true},{"timestamp_s":1050,"bearing_rad":0.8179551959037781,"valid":true},{"timestamp_s":1060,"bearing_rad":0.8475106358528137,"valid":true},{"timestamp_s":1070,"bearing_rad":0.8652806878089905,"valid":true},{"timestamp_s":1080,"bearing_rad":0.8183279037475586,"valid":true},{"timestamp_s":1090,"bearing_rad":0.8511764407157898,"valid":true},{"timestamp_s":1100,"bearing_rad":0.8486273288726807,"valid":true},{"timestamp_s":1110,"bearing_rad":0.734054684638977,"valid":true},{"timestamp_s":1120,"bearing_rad":0.7612407207489014,"valid":true},{"timestamp_s":1130,"bearing_rad":0.7659303545951843,"valid":true},{"timestamp_s":1140,"bearing_rad":0.7801884412765503,"valid":true},{"timestamp_s":1150,"bearing_rad":0.864984393119812,"valid":true},{"timestamp_s":1160,"bearing_rad":0.8461356163024902,"valid":true},{"timestamp_s":1170,"bearing_rad":0.7681589126586914,"valid":true},{"timestamp_s":1180,"bearing_rad":0.7597492337226868,"valid":true},{"timestamp_s":1190,"bearing_rad":0.7404963374137878,"valid":true},{"timestamp_s":1200,"bearing_rad":0.7502588033676147,"valid":true},{"timestamp_s":1210,"bearing_rad":0.7346709370613098,"valid":true},{"timestamp_s":1220,"bearing_rad":0.7109792232513428,"valid":true},{"timestamp_s":1230,"bearing_rad":0.6931037902832031,"valid":true},{"timestamp_s":1240,"bearing_rad":0.667599081993103,"valid":true},{"timestamp_s":1250,"bearing_rad":0.6546182036399841,"valid":true},{"timestamp_s":1260,"bearing_rad":0.6650578379631042,"valid":true},{"timestamp_s":1270,"bearing_rad":0.6461337804794312,"valid":true},{"timestamp_s":1280,"bearing_rad":0.5960041880607605,"valid":true},{"timestamp_s":1290,"bearing_rad":0.6413946747779846,"valid":true},{"timestamp_s":1300,"bearing_rad":0.6463676691055298,"valid":true},{"timestamp_s":1310,"bearing_rad":0.6638833284378052,"valid":true},{"timestamp_s":1320,"bearing_rad":0.65494704246521,"valid":true},{"timestamp_s":1330,"bearing_rad":0.6550017595291138,"valid":true},{"timestamp_s":1340,"bearing_rad":0.5684303641319275,"valid":true},{"timestamp_s":1350,"bearing_rad":0.5860951542854309,"valid":true},{"timestamp_s":1360,"bearing_rad":0.5510397553443909,"valid":true},{"timestamp_s":1370,"bearing_rad":0.6005047559738159,"valid":true},{"timestamp_s":1380,"bearing_rad":0.602243959903717,"valid":true},{"timestamp_s":1390,"bearing_rad":0.544645369052887,"valid":true},{"timestamp_s":1400,"bearing_rad":0.562533438205719,"valid":true},{"timestamp_s":1410,"bearing_rad":0.5199823975563049,"valid":true},{"timestamp_s":1420,"bearing_rad":0.5284174084663391,"valid":true},{"timestamp_s":1430,"bearing_rad":0.5315268039703369,"valid":true},{"timestamp_s":1440,"bearing_rad":0.49537450075149536,"valid":true},{"timestamp_s":1450,"bearing_rad":0.5552708506584167,"valid":true},{"timestamp_s":1460,"bearing_rad":0.48948007822036743,"valid":true},{"timestamp_s":1470,"bearing_rad":0.4212539494037628,"valid":true},{"timestamp_s":1480,"bearing_rad":0.5072190165519714,"valid":true},{"timestamp_s":1490,"bearing_rad":0.4705100357532501,"valid":true}]} diff --git a/packages/sim/test/unit/second-zig.test.ts b/packages/sim/test/unit/second-zig.test.ts new file mode 100644 index 00000000..52a5030f --- /dev/null +++ b/packages/sim/test/unit/second-zig.test.ts @@ -0,0 +1,159 @@ +// Generator 0.3.0 (spec 007): the across-the-line-of-sight rule and the second zig. Known answers +// are hand-computed; the generated-scenario checks hold for every scenario the iteration-3 ranges +// produce. +import type { ParameterRanges, ScenarioParameters } from "@tma/types"; +import { describe, expect, it } from "vitest"; +import { angleDiff, bearingOf, degToRad } from "../../src/geometry.js"; +import { DEFAULT_RANGES } from "../../src/ranges.js"; +import { acrossLosTurn, generateScenario, simulateTruth } from "../../src/scenario.js"; + +const deg = (r: number): number => (r * 180) / Math.PI; +const wrapPi = (a: number): number => Math.atan2(Math.sin(a), Math.cos(a)); + +describe("acrossLosTurn: known answers (ownship at the origin heading north)", () => { + const at = (brgDeg: number, offDeg: number): number => + deg( + acrossLosTurn( + 0, + 0, + 0, + 1000 * Math.sin(degToRad(brgDeg)), + 1000 * Math.cos(degToRad(brgDeg)), + degToRad(offDeg), + ), + ); + it("target 40° on the starboard bow, offset 60°: turn 100° to starboard (target to port at 60°)", () => { + expect(at(40, 60)).toBeCloseTo(100, 9); + }); + it("target 20° on the port bow, offset 30°: turn 50° to port", () => { + expect(at(-20, 30)).toBeCloseTo(-50, 9); + }); + it("target on the starboard quarter (150°), offset 60°: the shorter turn is 150° to port", () => { + expect(at(150, 60)).toBeCloseTo(-150, 9); + }); + it("the target always ends on the other bow at the offset", () => { + for (const brg of [-170, -95, -30, -1, 1, 45, 89, 135, 179]) { + for (const off of [30, 60, 90]) { + const zig = at(brg, off); + const relAfter = wrapPi(degToRad(brg - zig)); + expect(Math.sign(relAfter)).toBe(-Math.sign(brg)); + expect(Math.abs(deg(relAfter))).toBeCloseTo(off, 9); + expect(Math.abs(zig)).toBeLessThanOrEqual(180 + 1e-9); + } + } + }); +}); + +const base: ScenarioParameters = { + window_s: 1800, + dt_s: 10, + ownship_speed_mps: 5, + ownship_course0_rad: 0, + zig_angle_rad: degToRad(90), + zig_fraction: 0.5, + target_range0_m: 8000, + target_bearing0_rad: Math.PI / 2, + target_speed_mps: 0, + target_course_rad: 0, + noise_sigma_rad: 0, + dropout_fraction: 0, + zig2_delay_s: 240, + zig2_angle_rad: degToRad(-60), +}; + +describe("second zig: known answers", () => { + it("instantaneous: leg 2 from sample 90, second zig at sample 114 to course 30°", () => { + const tr = simulateTruth(base); + expect(tr.zigIndex).toBe(90); + expect(tr.zig2Index).toBe(114); + expect(tr.turn2StartIndex).toBeNull(); + // Leg 1 north 4500 m, leg 2 east for 240 s (1200 m), then 30° true. + expect(tr.ownX[114]).toBeCloseTo(1200, 9); + expect(tr.ownY[114]).toBeCloseTo(4500, 9); + expect(tr.ownCourse[113]).toBeCloseTo(Math.PI / 2, 12); + expect(tr.ownCourse[114]).toBeCloseTo(degToRad(30), 12); + expect(tr.ownX[124]).toBeCloseTo(1200 + 500 * Math.sin(degToRad(30)), 9); + expect(tr.ownY[124]).toBeCloseTo(4500 + 500 * Math.cos(degToRad(30)), 9); + }); + + it("rate-limited: second arc at the same rate, leg 3 after it, speed v throughout", () => { + const tr = simulateTruth({ ...base, ownship_turn_rate_rad_s: degToRad(1) }); + // First turn 90 s: leg 2 from sample 90 + 1 + 9 - 1 = 99; second turn starts at 99 + 24 = 123. + expect(tr.zigIndex).toBe(99); + expect(tr.turn2StartIndex).toBe(124); + expect(tr.zig2Index).toBe(129); + expect(tr.ownCourse[123]).toBeCloseTo(Math.PI / 2, 12); + expect(tr.ownCourse[129]).toBeCloseTo(degToRad(30), 12); + for (let i = 1; i < tr.samples; i++) { + const step = Math.hypot( + (tr.ownX[i] ?? 0) - (tr.ownX[i - 1] ?? 0), + (tr.ownY[i] ?? 0) - (tr.ownY[i - 1] ?? 0), + ); + // Chords on an arc are a little shorter than the arc length. + expect(step).toBeLessThanOrEqual(50 + 1e-9); + expect(step).toBeGreaterThan(49.9); + } + }); + + it("without zig2 fields the path is the two-leg path", () => { + const one: ScenarioParameters = { ...base }; + delete one.zig2_delay_s; + delete one.zig2_angle_rad; + const tr = simulateTruth(one); + expect(tr.zig2Index).toBeNull(); + expect(tr.ownCourse[150]).toBeCloseTo(Math.PI / 2, 12); + }); +}); + +const ITER3: ParameterRanges = { + ...DEFAULT_RANGES, + noise_sigma_rad_values: [0], + ownship_turn_rate_rad_s: { min: degToRad(0.5), max: degToRad(3) }, + zig_rule: "across_los", + los_offset_rad: { min: degToRad(30), max: degToRad(90) }, + zig2_delay_s: { min: 180, max: 300 }, +}; + +describe("generated scenarios under across_los with a second zig", () => { + it("both turns cross the line of sight to the drawn offset; legs are steady; turns complete", () => { + for (let i = 0; i < 200; i++) { + const sc = generateScenario(7n, i, ITER3).scenario; + const m = sc.metadata; + const p = m.parameters; + expect(p.zig2_delay_s).toBeGreaterThanOrEqual(180); + expect(p.zig2_delay_s).toBeLessThanOrEqual(300); + const z2 = m.zig2_index; + const t2 = m.turn2_start_index; + if (z2 == null || t2 == null || m.turn_start_index == null) throw new Error("no second zig"); + expect(z2).toBeLessThan(m.samples); + expect(t2 - 1).toBe(m.zig_index + Math.round((p.zig2_delay_s ?? 0) / p.dt_s)); + for (const [start, end] of [ + [m.turn_start_index - 1, m.zig_index], + [t2 - 1, z2], + ] as const) { + const o = sc.ownship.states[start]; + const g = sc.target.states[start]; + const after = sc.ownship.states[end]; + if (o === undefined || g === undefined || after === undefined) throw new Error("index"); + const brg = bearingOf(g.x_m - o.x_m, g.y_m - o.y_m); + const before = wrapPi(brg - o.course_rad); + const rel = wrapPi(brg - after.course_rad); + expect(Math.sign(rel)).toBe(-Math.sign(before)); + // float32 storage: offsets within 0.01° of the drawn range. + expect(deg(Math.abs(rel))).toBeGreaterThan(30 - 0.01); + expect(deg(Math.abs(rel))).toBeLessThan(90 + 0.01); + } + // Leg 3 course is constant. + const c = sc.ownship.states[z2]?.course_rad ?? NaN; + for (let k = z2; k < m.samples; k++) { + expect(Math.abs(angleDiff(sc.ownship.states[k]?.course_rad ?? NaN, c))).toBeLessThan(1e-6); + } + } + }); + + it("is deterministic and leaves the 0.2.0 draws untouched up to the new ones", () => { + const a = generateScenario(7n, 3, ITER3).scenario; + const b = generateScenario(7n, 3, ITER3).scenario; + expect(JSON.stringify(a)).toBe(JSON.stringify(b)); + }); +}); diff --git a/packages/types/src/generated/tma.schema.json b/packages/types/src/generated/tma.schema.json index a3a54ad0..1b26c3e6 100644 --- a/packages/types/src/generated/tma.schema.json +++ b/packages/types/src/generated/tma.schema.json @@ -1025,6 +1025,17 @@ "dt_s": { "type": "number" }, + "los_offset_rad": { + "anyOf": [ + { + "$ref": "#/$defs/FloatRange" + }, + { + "type": "null" + } + ], + "description": "Angle off the line of sight, on the other bow, after an across_los turn; required when zig_rule is across_los." + }, "min_range_m": { "description": "Geometries closer than this at any sample are redrawn.", "type": "number" @@ -1062,11 +1073,33 @@ "window_s": { "$ref": "#/$defs/FloatRange" }, + "zig2_delay_s": { + "anyOf": [ + { + "$ref": "#/$defs/FloatRange" + }, + { + "type": "null" + } + ], + "description": "Delay from the start of leg 2 to the second turn; absent means one zig. The second zig follows zig_rule (and uses the same turn rate as the first)." + }, "zig_angle_abs_rad": { "$ref": "#/$defs/FloatRange" }, "zig_fraction": { "$ref": "#/$defs/FloatRange" + }, + "zig_rule": { + "anyOf": [ + { + "$ref": "#/$defs/ZigRule" + }, + { + "type": "null" + } + ], + "description": "How each zig is chosen; absent means random (generators 0.1.0–0.2.0). Under across_los zig_angle_abs_rad is still drawn but not used." } }, "required": [ @@ -1309,6 +1342,13 @@ "description": "Scenario seed as a decimal uint64 string.", "type": "string" }, + "turn2_start_index": { + "description": "First timestep after the second turn starts (absent with one zig or no turn rate).", + "type": [ + "integer", + "null" + ] + }, "turn_start_index": { "description": "First timestep after the turn starts (absent for an instantaneous zig).", "type": [ @@ -1316,6 +1356,13 @@ "null" ] }, + "zig2_index": { + "description": "First timestep on leg 3, after the second turn completes (absent with one zig). Gate times still count from zig_index.", + "type": [ + "integer", + "null" + ] + }, "zig_index": { "description": "First timestep on leg 2 (with a rate-limited turn, the first after it completes).", "type": "integer" @@ -1385,6 +1432,20 @@ "description": "Window length; multiple of dt_s.", "type": "number" }, + "zig2_angle_rad": { + "description": "Signed course change at the second zig; positive is clockwise.", + "type": [ + "number", + "null" + ] + }, + "zig2_delay_s": { + "description": "Second zig (generator 0.3.0, spec 007): time from the first sample on leg 2 to the start of the second turn. Absent: one zig, two legs.", + "type": [ + "number", + "null" + ] + }, "zig_angle_rad": { "description": "Signed course change at the zig; positive is clockwise.", "type": "number" @@ -1585,6 +1646,15 @@ ], "title": "TrainingConfig", "type": "object" + }, + "ZigRule": { + "description": "How each zig's direction and size are chosen (generator 0.3.0, spec 007).", + "enum": [ + "random", + "across_los" + ], + "title": "ZigRule", + "type": "string" } }, "$id": "https://github.com/debrief/gitma/schema/tma", @@ -1621,5 +1691,5 @@ ], "title": "tma", "type": "object", - "version": "0.8.2" + "version": "0.9.0" } diff --git a/packages/types/src/generated/version.ts b/packages/types/src/generated/version.ts index 4b1afeec..9d332f87 100644 --- a/packages/types/src/generated/version.ts +++ b/packages/types/src/generated/version.ts @@ -1,2 +1,2 @@ // GENERATED from schema/tma.yaml by packages/types/scripts/generate.ts. Do not edit. -export const SCHEMA_VERSION = "0.8.2"; +export const SCHEMA_VERSION = "0.9.0"; diff --git a/packages/types/src/generated/zod.ts b/packages/types/src/generated/zod.ts index a076a5f0..daa98ccb 100644 --- a/packages/types/src/generated/zod.ts +++ b/packages/types/src/generated/zod.ts @@ -55,10 +55,16 @@ export type FloatRange = z.infer; export const RangeDistribution = z.enum(["uniform", "log_uniform"]); export type RangeDistribution = z.infer; +/** How each zig's direction and size are chosen (generator 0.3.0, spec 007). */ +export const ZigRule = z.enum(["random", "across_los"]); +export type ZigRule = z.infer; + /** Ranges from which ScenarioParameters are drawn. Initial ownship course, target bearing and target course are always uniform on [0, 2π); zig sign is uniform ±. */ export const ParameterRanges = z.strictObject({ dropout_fraction: FloatRange, dt_s: z.number(), + /** Angle off the line of sight, on the other bow, after an across_los turn; required when zig_rule is across_los. */ + los_offset_rad: FloatRange.nullish(), /** Geometries closer than this at any sample are redrawn. */ min_range_m: z.number(), /** σ drawn uniformly from this set. */ @@ -70,8 +76,12 @@ export const ParameterRanges = z.strictObject({ target_range0_m: FloatRange, target_speed_mps: FloatRange, window_s: FloatRange, + /** Delay from the start of leg 2 to the second turn; absent means one zig. The second zig follows zig_rule (and uses the same turn rate as the first). */ + zig2_delay_s: FloatRange.nullish(), zig_angle_abs_rad: FloatRange, zig_fraction: FloatRange, + /** How each zig is chosen; absent means random (generators 0.1.0–0.2.0). Under across_los zig_angle_abs_rad is still drawn but not used. */ + zig_rule: ZigRule.nullish(), }); export type ParameterRanges = z.infer; @@ -445,6 +455,10 @@ export const ScenarioParameters = z.strictObject({ target_speed_mps: z.number(), /** Window length; multiple of dt_s. */ window_s: z.number(), + /** Signed course change at the second zig; positive is clockwise. */ + zig2_angle_rad: z.number().nullish(), + /** Second zig (generator 0.3.0, spec 007): time from the first sample on leg 2 to the start of the second turn. Absent: one zig, two legs. */ + zig2_delay_s: z.number().nullish(), /** Signed course change at the zig; positive is clockwise. */ zig_angle_rad: z.number(), /** Zig time as a fraction of the window. */ @@ -463,8 +477,12 @@ export const ScenarioMetadata = z.strictObject({ samples: z.number().int(), /** Scenario seed as a decimal uint64 string. */ seed: z.string(), + /** First timestep after the second turn starts (absent with one zig or no turn rate). */ + turn2_start_index: z.number().int().nullish(), /** First timestep after the turn starts (absent for an instantaneous zig). */ turn_start_index: z.number().int().nullish(), + /** First timestep on leg 3, after the second turn completes (absent with one zig). Gate times still count from zig_index. */ + zig2_index: z.number().int().nullish(), /** First timestep on leg 2 (with a rate-limited turn, the first after it completes). */ zig_index: z.number().int(), zig_time_s: z.number(), diff --git a/schema/tma.yaml b/schema/tma.yaml index 29829bb0..3e1a7784 100644 --- a/schema/tma.yaml +++ b/schema/tma.yaml @@ -6,7 +6,7 @@ description: >- principle I). Frame: local flat-earth Cartesian, x east, y north, metres, origin at ownship's window-start position. Bearings and courses are true, clockwise from north, radians in [0, 2π). Speeds in m/s. Times in seconds since window start. All stored floats are float32-rounded. -version: 0.8.2 +version: 0.9.0 license: Apache-2.0 prefixes: linkml: https://w3id.org/linkml/ @@ -39,6 +39,16 @@ enums: permissible_values: uniform: {} log_uniform: {} + ZigRule: + description: How each zig's direction and size are chosen (generator 0.3.0, spec 007). + permissible_values: + random: + description: Size from zig_angle_abs_rad, side uniformly ±, regardless of the target. + across_los: + description: >- + Turn across the line of sight: the new course puts the target on the other bow at an + angle off drawn from los_offset_rad, by the shorter turn. Uses the true bearing at the + last sample before the turn. PredictionMode: permissible_values: final: { description: One row at the last timestep of the window. } @@ -141,6 +151,16 @@ classes: description: >- Ownship turn rate at the zig (rate-limited turn, spec 003 US6). Absent or 0: the course changes instantaneously, as in generator 0.1.0. + zig2_delay_s: + range: float + unit: { ucum_code: s } + description: >- + Second zig (generator 0.3.0, spec 007): time from the first sample on leg 2 to the start + of the second turn. Absent: one zig, two legs. + zig2_angle_rad: + range: float + unit: { ucum_code: rad } + description: Signed course change at the second zig; positive is clockwise. ParameterRanges: description: >- @@ -167,6 +187,24 @@ classes: range: FloatRange inlined: true description: Turn rate drawn uniformly per scenario; absent means an instantaneous zig. + zig_rule: + range: ZigRule + description: >- + How each zig is chosen; absent means random (generators 0.1.0–0.2.0). Under across_los + zig_angle_abs_rad is still drawn but not used. + los_offset_rad: + range: FloatRange + inlined: true + description: >- + Angle off the line of sight, on the other bow, after an across_los turn; required when + zig_rule is across_los. + zig2_delay_s: + range: FloatRange + inlined: true + unit: { ucum_code: s } + description: >- + Delay from the start of leg 2 to the second turn; absent means one zig. The second zig + follows zig_rule (and uses the same turn rate as the first). min_range_m: { range: float, @@ -213,6 +251,14 @@ classes: turn_start_index: range: integer description: First timestep after the turn starts (absent for an instantaneous zig). + zig2_index: + range: integer + description: >- + First timestep on leg 3, after the second turn completes (absent with one zig). Gate + times still count from zig_index. + turn2_start_index: + range: integer + description: First timestep after the second turn starts (absent with one zig or no turn rate). zig_time_s: { range: float, required: true, unit: { ucum_code: s } } mean_range_m: { range: float, required: true, unit: { ucum_code: m } } min_range_m: { range: float, required: true, unit: { ucum_code: m } } diff --git a/site/docs/algorithms/generator.md b/site/docs/algorithms/generator.md index 717d04ad..a0b4bed6 100644 --- a/site/docs/algorithms/generator.md +++ b/site/docs/algorithms/generator.md @@ -21,9 +21,17 @@ For each attempt: sample count `S` (uniform integer, window/dt), ownship speed, |zig angle|, zig sign (±), zig fraction `f`, initial range `r₀` (log-uniform by default), initial bearing `β₀`, target speed `v_t`, target course `θ_t`, noise σ (uniform choice from a set), dropout fraction `d`, then — only when the ranges give `ownship_turn_rate_rad_s` — the turn rate `ω` -(uniform; iteration 2 uses 0.5–3°/s). All parameters are float32-rounded before use, so the -metadata describes the data exactly. Ranges without a turn rate consume the random stream exactly -as generator 0.1.0 did, so they reproduce iteration-1 datasets byte for byte. +(uniform; iteration 2 uses 0.5–3°/s). Generator 0.3.0 (iteration 3) then draws, only when the +ranges ask for them: + +- **Across-the-line-of-sight offset.** The first turn's offset, from `los_offset_rad`, under + `zig_rule: across_los`. +- **Second zig.** Its delay, from `zig2_delay_s`, then its offset (or, under the random rule, its + size and side). + +All parameters are float32-rounded before use, so the metadata describes the data exactly. Ranges +without a turn rate consume the random stream exactly as generator 0.1.0 did, and ranges without +the 0.3.0 options exactly as 0.2.0 did. Both reproduce earlier datasets byte for byte. ## Motion @@ -60,6 +68,35 @@ first sample with `τ ≥ T`, i.e. the first sample on leg 2. Everything downstr leg, as in iteration 1. A turn that would not finish inside the window is redrawn (it cannot happen with the iteration-2 ranges: at most 180 s of turn against at least 240 s after the turn starts). +### Across the line of sight (iteration 3) + +Under `zig_rule: across_los` each turn is chosen from the geometry at its start, not drawn. Take +ownship at `p` on course `c` and the true bearing `b` to the target at the last sample before the +turn. The relative bearing is `ρ = b − c`, wrapped to (−180°, 180°]. + +- **New course.** The turn puts the target on the other bow at the drawn offset `φ` (30–90° in + iteration 3): the new course is `b + sign(ρ) · φ`. +- **Turn direction.** The turn takes the shorter way, so `zig = wrap(b + sign(ρ) φ − c)` and + `|zig| ≤ 180°`. + +A target ahead is crossed by turning towards it; a target on the quarter by turning away, across +the stern line of sight. Both reverse ownship's velocity component across the line of sight, +which is what makes the bearings informative (spec 007 research R4). The realised turn is stored +as `zig_angle_rad`, so the tracks depend only on the parameters. + +Worked check: heading north with the target 40° on the starboard bow and `φ` = 60°, the turn is +100° to starboard. With the target on the starboard quarter (150°) it is 150° to port. + +### Second zig (iteration 3) + +With `zig2_delay_s` the second turn starts at sample `k₂ = zig_index + round(delay / dt)`, still +on the leg-2 course. It follows the first turn's conventions: + +- **Turn.** It is instantaneous at `k₂`, or an arc at the same turn rate. +- **Indices.** `turn2_start_index = k₂ + 1`, and `zig2_index` is the first sample on leg 3. +- **Redraws.** A second turn that does not finish inside the window is redrawn. Gate times still + count from `zig_index`, the end of the first turn. + Target: `q(t) = r₀ (sin β₀, cos β₀) + v_t t (sin θ_t, cos θ_t)`. If the range `|q − p|` falls below `min_range_m` (100 m) at any sample, the parameters are redrawn diff --git a/site/docs/glossary.md b/site/docs/glossary.md index c1547252..832e4efb 100644 --- a/site/docs/glossary.md +++ b/site/docs/glossary.md @@ -5,6 +5,11 @@ title: Glossary TMA and ML terms as used in this project, alphabetical. Algorithm details are linked where they exist. +**Across the line of sight** — A zig that puts the target on ownship's other bow. It reverses +ownship's velocity component across the line of sight, the change bearings respond to most. It +is iteration 3's rule for both turns (`zig_rule: across_los`; see +[generator](algorithms/generator.md)). + **Bearing** — True direction from ownship to the target, clockwise from north, in radians in [0, 2π). The only measurement the estimators receive about the target. @@ -61,8 +66,8 @@ inconclusive; the agent review panel decides it under a pre-registered rule. **Instantaneous variant** — The model that gives the operator's running solution: at each time, range, course and speed from bearings so far. Scored as a curve of error against time since the zig. -**Leg** — A period of constant ownship course and speed. Every scenario has two legs (joined by a -rate-limited turn from iteration 2). +**Leg** — A period of constant ownship course and speed. Scenarios have two legs (joined by a +rate-limited turn from iteration 2), or three from iteration 3 (a second zig). **Naive baseline** — Predicts training-set means and ignores the bearings. Anything that cannot beat it has learned nothing. See [naive baseline](algorithms/naive-baseline.md). @@ -104,4 +109,5 @@ must score at least as well. 240 rows. **Zig** — Ownship's course change part-way through the window (30–90° at 40–60% of the window; -instantaneous in iterations 0–1, a rate-limited turn from iteration 2). +instantaneous in iterations 0–1, a rate-limited turn from iteration 2). Iteration 3 adds a second +zig 3–5 minutes into leg 2, and both turns are across the line of sight. diff --git a/specs/007-iteration-3-second-zig/research.md b/specs/007-iteration-3-second-zig/research.md new file mode 100644 index 00000000..a9a3acb4 --- /dev/null +++ b/specs/007-iteration-3-second-zig/research.md @@ -0,0 +1,116 @@ +# Research: Iteration 3 (spec 007) + +## R1 — What the existing gate data says about the owner's ideas (2026-10-17) + +These are the 2c carried models' and the classical solver's per-timestep predictions on the +seed-203 test split. That split decided iteration 2c's G2 and is reserved, so reading it again +decides nothing. Speed error is at zig + 600 s, σ = 1°, 3,038 scenarios. + +The slices are observational: zig angle, leg-1 length and turn direction are correlated in the +data. They show tendencies, not effects. + +**Direction of the first turn.** "Towards" means the turn is to the target's side; "crosses" means +the target ends on the other bow. + +| Slice | n | LSTM, kt | BiLSTM, kt | Classical, kt | LSTM range | Classical range | +| --- | --- | --- | --- | --- | --- | --- | +| all | 3,038 | 1.47 | 1.53 | 0.61 | 11.5% | 4.6% | +| towards the target | 1,502 | 1.59 | 1.65 | 0.66 | 13.7% | 5.6% | +| away from the target | 1,536 | 1.36 | 1.45 | 0.59 | 9.7% | 3.7% | +| towards, crossing the line of sight | 258 | 1.41 | 1.61 | 0.42 | 12.9% | 3.2% | +| towards, not crossing | 1,244 | 1.62 | 1.67 | 0.75 | 14.0% | 6.3% | + +**Zig size.** + +| Slice | n | LSTM, kt | BiLSTM, kt | Classical, kt | +| --- | --- | --- | --- | --- | +| zig 30–50° | 1,058 | 1.65 | 1.74 | 0.86 | +| zig 50–70° | 997 | 1.36 | 1.51 | 0.60 | +| zig 70–90° | 983 | 1.36 | 1.41 | 0.46 | + +**Leg-1 length.** The leg-1 0–8 min slice is omitted: it has only 6 scenarios. + +| Slice | n | LSTM, kt | BiLSTM, kt | Classical, kt | +| --- | --- | --- | --- | --- | +| leg 1, 8–12 min | 551 | 1.77 | 2.02 | 0.81 | +| leg 1, 12–16 min | 1,133 | 1.42 | 1.44 | 0.63 | +| leg 1, 16 min or more | 1,348 | 1.37 | 1.45 | 0.56 | + +**Combined slices.** + +| Slice | n | LSTM, kt | BiLSTM, kt | Classical, kt | +| --- | --- | --- | --- | --- | +| crossing, zig ≥ 60° | 160 | 1.38 | 1.43 | 0.39 | +| crossing, leg 1 ≥ 12 min | 192 | 1.32 | 1.43 | 0.36 | +| zig ≥ 70°, leg 1 ≥ 16 min | 437 | 1.28 | 1.31 | 0.43 | +| away, zig ≥ 70°, leg 1 ≥ 16 min | 231 | 1.12 | 1.23 | 0.45 | +| towards, not crossing, zig < 50° | 479 | 1.67 | 1.91 | 0.90 | + +**Readings.** + +- **Earlier zig.** A shorter leg 1 is worse for every estimator at a fixed time after the zig. + Gate rows and the training loss already exclude the pre-zig period, so an earlier zig would mainly + shorten leg 1. It was not chosen. +- **Turning towards the contact.** It helps only when the turn crosses the line of sight; a + towards-turn that keeps the target on the same bow is the worst case. The useful rule is + "across the line of sight". +- **Larger turns.** They help every estimator. +- **The gap to classical.** No slice brings the models near classical. Geometry lifts both, but + the ratio widens: from 2.4× overall to 3.7× in the crossing slices. The models still extract + less than the bearings carry (Annex A B.1). + +The script reproduces from the committed predictions' sources: it regenerates the seed-203 +scenarios and reads `data/i2/predictions/test.*.ndjson`. + +## R2 — Classical forecast for the iteration-3 ranges (2026-10-17) + +The same scratch seed (9001, not a gate seed) was drawn under each set of ranges. These are +classical per-timestep estimates at σ = 1°, 1,500 scenarios each. + +| | Iteration-2 ranges | Iteration-3 ranges | +| --- | --- | --- | +| redraws per scenario | 0.00 | 0.21 | +| window, median (p10) | 25.2 (12.7) min | 27.8 (16.5) min | +| first turn, median (p90) | 61° (84°) | 143° (173°) | +| second turn, median | — | 129° | +| reaching zig + 600 s | 61% | 66% | +| classical speed, zig + 300 s, median (p90) | 1.13 (8.98) kt | 0.56 (3.66) kt | +| classical speed, zig + 600 s, median (p90) | 0.67 (4.79) kt | **0.36 (1.92) kt** | +| classical range, zig + 300 s | 7.9% | 3.4% | +| classical range, zig + 600 s | 4.6% | 2.8% | + +The new problem roughly halves the classical solver's speed error at both gate times. + +**Forecast for the models, recorded before training.** Across R1's slices the LSTM's speed tracks +roughly 1.0 + 0.75 × classical (kt). With classical at about 0.36 kt that gives **1.1–1.3 kt**, +probably still above 1 kt. A pass needs the models, trained on an all-favourable distribution, to +use the extra information better than the 2c models did in iteration 2's most favourable slices +(1.12–1.32 kt). Range and course should improve with classical. + +## R3 — Why not an adaptive second zig + +The owner's operational rule is to zig again when the estimate is not steady. That needs the +estimator inside the simulation loop: the generator plans ownship's path in advance, and training +data would depend on the model being trained. A scripted second zig 3–5 minutes into leg 2 is the +nearest open-loop equivalent. The viewer could later mark where the rule would have called for a +zig. That is a display-only follow-up, not in this spec. + +## R4 — The across-the-line-of-sight rule + +Bearings constrain the target through the change in ownship's velocity across the line of sight. +For a turn of angle θ from relative bearing β, the change in that component is +2v |sin(θ/2) cos(β − θ/2)|. It is largest when the turn is symmetric about the line of sight, +which crosses it and reverses the component's sign. That is the standard reason for "turning +across". + +The rule is implemented as follows: + +- **New course.** The new course is the bearing plus the side of the target times the offset, by + the shorter turn. +- **Target ahead.** It turns towards the target and through the line of sight. +- **Target on the quarter.** It turns away, through the stern line of sight. + +Both cases reverse the across-line-of-sight component. + +The rule uses the true bearing, not the observed one: at σ ≤ 2° the difference is small, and it +keeps the parameters a pure function of the draws. diff --git a/specs/007-iteration-3-second-zig/spec.md b/specs/007-iteration-3-second-zig/spec.md new file mode 100644 index 00000000..a26384f7 --- /dev/null +++ b/specs/007-iteration-3-second-zig/spec.md @@ -0,0 +1,120 @@ +# Feature Specification: Iteration 3 — A Second Zig Across the Line of Sight (Gate G2) + +**Feature Branch**: `007-iteration-3-second-zig` + +**Created**: 2026-10-17 + +**Status**: Planned. Owner's realism check recorded (SRD Annex A §A.7) and lever chosen (decision +22, Annex A D4), 2026-10-17. + +**Input**: The owner's realism check in the engagement viewer (spec 006). The engagements look +right, but with both platforms on steady legs there is not enough information for TMA: the zig +comes late, and an operator who has no steady solution zigs again, traditionally across towards +the contact. The diagnostic in [research.md](research.md) R1 tested these ideas on the seed-203 +test predictions. The owner chose a second zig with both turns across the line of sight, keeping +the first zig's timing. + +**SRD trace**: Annex A Part B: lever B.2.1 (observability in the data) under the sequence and +rules of B.3; §3 (new parameter rows); §5 (G2 thresholds, times, slices and intervals unchanged); +§8 (panel, decision 22); §9 (blog, results). + +**Out of scope**: an adaptive manoeuvre that zigs only when the estimate is unsteady (the +generator plans ownship's path in advance; see research R3); output parameterisation, derived +features, training changes and capacity (levers B.2.2–B.2.5); real data. + +## The single change + +The problem changes; the models do not. + +- **Generator 0.3.0.** Two new options in the parameter ranges, both off by default so earlier + ranges reproduce generator 0.2.0's scenarios byte for byte: + - `zig_rule: across_los`. Each zig turns ownship so that the target ends on the **other bow** at + an angle off drawn from `los_offset_rad`, by the shorter turn. It uses the true bearing at the + last sample before the turn. + - `zig2_delay_s`. A second zig starts this long after leg 2 begins, at the first zig's turn + rate, giving three legs. A second turn that does not complete inside the window is redrawn. +- **Iteration-3 ranges** (`configs/iter3-ranges.json`, `configs/iter3-heldout-ranges.json`): + - The iteration-2 ranges, plus `across_los` with an offset of 30–90° for both turns. + - A second zig 180–300 s after leg 2 starts. + - Unchanged: window 10–40 min, first zig at 40–60% of the window, turn rate 0.5–3°/s, speeds, + ranges, σ 1–2°, dropouts. The held-out set keeps its 20–30 km start ranges. +- **Dataset**: seed **204**, count 100,000, splits as `planSplits`. Its own test split decides G2; + 204 is reserved afterwards. Seeds 201–203 are not used. +- **Models**: the 2c configuration, from scratch on the new data (`configs/iter3/a`): + - 2 × 128 stacked recurrent layers for the instantaneous LSTM, the time-late BiLSTM and the + shuffled control. + - CNN and MLP controls at their sizes. + - `loss_weight_speed` 4, `max_epochs` 150, patience 6, raw features, the same training seeds. + +## User stories + +### US1 — Generate and see the new engagements (P1) + +Generator 0.3.0 and the iteration-3 ranges. The viewer draws both turns and all three legs and +lists the second zig. The seed-204 validation split is in the viewer catalogue, so the owner can +check the new engagements before any training (B.2.1). + +**Acceptance**: +- Unit tests show both turns cross the line of sight to an offset inside the drawn range, and + that leg 3 is steady. +- Hand-worked known answers pass for the turn rule and for the second zig, instantaneous and + rate-limited. +- The 0.2.0 golden scenarios are unchanged, and a new golden file pins the three-leg generator. + +### US2 — G0 on generator 0.3.0 (P1) + +G0 re-run on seed 42 (noise-free, iteration-3 ranges), then the agent review panel. `tma-train` +refuses to train until that G0 passes its panel (the G0 guard checks the generator version). + +### US3 — Pre-register, train, validate (P1) + +The decision record (`experiments/-i3-decision/`) is committed before training. It holds +the change, the forecast, the carried-set rule and seed 204. Then plan `configs/iter3/a/plan.json` +(digests pinned), branch `train/i3a`, and a validation diagnostic before any test scoring. + +### US4 — Gate and record (P1) + +`tma-g2 --split test --plan --decision --compare-models` scores the carried set once on the +seed-204 test split, with the 2c models beside it as the other phase. Then the agent review panel, +`review.md`, `decided_by`, a results page and a blog post. + +## Functional requirements + +- **FR-001**: Ranges without `zig_rule` and `zig2_delay_s` MUST reproduce generator 0.2.0 + byte for byte. The new draws come after every 0.2.0 draw. +- **FR-002**: `zig_angle_rad` and `zig2_angle_rad` record the realised signed turns, so + `simulateTruth(parameters)` alone reproduces the tracks. +- **FR-003**: Metadata records `zig2_index` (first sample on leg 3) and `turn2_start_index`. Gate + times still count from `zig_index`, the first sample after the **first** turn. +- **FR-004**: Generator 0.3.0 reads 0.1.0 and 0.2.0 datasets. `tma-train` requires a G0 on 0.3.0. +- **FR-005**: The decision record MUST be committed before any iteration-3 weights are trained. + `tma-g2` checks ancestry. +- **FR-006 (validation rule, B.3.3)**: The carried pair goes to the test split only if: + - the instantaneous model's validation speed (σ = 1°, zig + 600 s) is below 1.36 kt, which is + 2c's validation 1.42 kt less its interval half-width; + - or the time-late model's validation speed is below 1.45 kt, which is 2c's 1.52 kt less its + half-width. + + Otherwise the iteration stops at validation and is written up. +- **FR-007**: The carried set is the iteration-3 LSTM and BiLSTM, unconditionally. The 2c models + are scored beside them on the same split, reported and never gated. +- **FR-008**: Thresholds, slices, bootstrap and the gate rule are spec 003's as amended by the G2 + panels. Classical and naive are re-baselined on the new data, and the improvement is reported + rather than subtracted (B.2.1). + +## Success criteria + +- **SC-001**: One test scoring of seed 204 for the carried set, with 2c beside it. +- **SC-002**: Every reported model figure sits beside naive and classical on the same rows. +- **SC-003**: The write-up separates what the easier problem gave every estimator from what the + models gained against classical. + +## Risks + +- **Large turns.** With an offset of 30–90° on the other bow, the median first turn is 143° (p90 + 173°), against 61° in iteration 2. The owner checks this in the viewer (US1). A cap on turn size + would be a further change to the problem. +- **Longer windows.** Redraws for second turns that do not fit remove short windows: p10 window + 16.5 min, against 12.7. The share reaching zig + 600 s rises from 61% to 66%. +- **An easier problem.** Every estimator gains, so a G2 pass is a pass on the three-leg problem, + and is reported as such. diff --git a/specs/007-iteration-3-second-zig/tasks.md b/specs/007-iteration-3-second-zig/tasks.md new file mode 100644 index 00000000..09c36caa --- /dev/null +++ b/specs/007-iteration-3-second-zig/tasks.md @@ -0,0 +1,28 @@ +# Tasks: Iteration 3 — a second zig across the line of sight + +**Spec**: [spec.md](spec.md) · **Research**: [research.md](research.md) + +## Setup PR (before any training) + +- [x] T001 Research R1 (owner's ideas against the seed-203 gate data) and R2 (classical forecast) +- [x] T002 Schema 0.9.0: `ZigRule`, `los_offset_rad`, `zig2_delay_s` (ranges); `zig2_delay_s`, + `zig2_angle_rad` (parameters); `zig2_index`, `turn2_start_index` (metadata) +- [x] T003 Generator 0.3.0: across-LOS rule and second zig; known-answer, generated-scenario and + golden tests; 0.2.0 goldens unchanged; 0.1.0 and 0.2.0 data readable +- [x] T004 Viewer: second turn, leg 3, "zig 2" marker and readout +- [x] T005 Ranges `configs/iter3-ranges.json`, `configs/iter3-heldout-ranges.json` +- [ ] T006 SRD: Annex A §A.7 record, §3 rows, §8 decision 22; AGENTS.md +- [ ] T007 G0 on generator 0.3.0 (seed 42, noise-free, iteration-3 ranges) and its agent panel; + the G0 guard test moves to it +- [ ] T008 Dataset seed 204 generated at a clean commit; plan `configs/iter3/a/plan.json` with + digests; seed 204 reserved in `scripts/train-plan.mjs`; viewer catalogue shows the validation + split +- [ ] T009 Decision record `experiments/-i3-decision/` (change, forecast, validation rule, + carried set, seed 204), committed before training + +## Run + +- [ ] T010 `train/i3a`: five models, chained jobs; validation diagnostic (`tma-g2 --split validation`) +- [ ] T011 Validation rule (FR-006): carry to test or stop +- [ ] T012 G2 on the seed-204 test split with 2c beside; agent panel; `review.md`; `decided_by` +- [ ] T013 Write-up: results page, blog post, AGENTS.md gate status; viewer catalogue adds the pair From 5d4e2731fd886d56eba68901294d9c0054a68e38 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 08:38:11 +0000 Subject: [PATCH 05/21] Iteration 3 plan configs/iter3/a (seed 204, 2c configuration); reserve seed 204 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- configs/iter3/a/causal-cnn.json | 20 ++++++++++++++++++ configs/iter3/a/instantaneous-shuffled.json | 19 +++++++++++++++++ configs/iter3/a/instantaneous.json | 19 +++++++++++++++++ configs/iter3/a/mlp.json | 18 ++++++++++++++++ configs/iter3/a/plan.json | 23 +++++++++++++++++++++ configs/iter3/a/time-late.json | 19 +++++++++++++++++ packages/models/test/train-plan.test.ts | 15 +++++++++----- scripts/train-plan.mjs | 3 ++- 8 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 configs/iter3/a/causal-cnn.json create mode 100644 configs/iter3/a/instantaneous-shuffled.json create mode 100644 configs/iter3/a/instantaneous.json create mode 100644 configs/iter3/a/mlp.json create mode 100644 configs/iter3/a/plan.json create mode 100644 configs/iter3/a/time-late.json diff --git a/configs/iter3/a/causal-cnn.json b/configs/iter3/a/causal-cnn.json new file mode 100644 index 00000000..3310c54b --- /dev/null +++ b/configs/iter3/a/causal-cnn.json @@ -0,0 +1,20 @@ +{ + "learning_rate": 0.001, + "batch_size": 256, + "max_epochs": 150, + "patience": 6, + "max_wall_s": 172800, + "loss_weight_range": 1, + "loss_weight_course": 1, + "loss_weight_speed": 4, + "shuffle_labels": false, + "id": "cnn-causal-control", + "model_kind": "causal_cnn", + "units": 64, + "kernel_size": 15, + "layers": 3, + "train_seed": "1003", + "length_bucketing": true, + "tfjs_threads": 1, + "feature_set": "raw" +} diff --git a/configs/iter3/a/instantaneous-shuffled.json b/configs/iter3/a/instantaneous-shuffled.json new file mode 100644 index 00000000..91aada21 --- /dev/null +++ b/configs/iter3/a/instantaneous-shuffled.json @@ -0,0 +1,19 @@ +{ + "learning_rate": 0.001, + "batch_size": 256, + "max_epochs": 150, + "patience": 6, + "max_wall_s": 172800, + "loss_weight_range": 1, + "loss_weight_course": 1, + "loss_weight_speed": 4, + "shuffle_labels": true, + "id": "lstm-instantaneous-shuffled", + "model_kind": "instantaneous", + "units": 128, + "train_seed": "1005", + "length_bucketing": true, + "tfjs_threads": 1, + "feature_set": "raw", + "layers": 2 +} diff --git a/configs/iter3/a/instantaneous.json b/configs/iter3/a/instantaneous.json new file mode 100644 index 00000000..872d254e --- /dev/null +++ b/configs/iter3/a/instantaneous.json @@ -0,0 +1,19 @@ +{ + "learning_rate": 0.001, + "batch_size": 256, + "max_epochs": 150, + "patience": 6, + "max_wall_s": 172800, + "loss_weight_range": 1, + "loss_weight_course": 1, + "loss_weight_speed": 4, + "shuffle_labels": false, + "id": "lstm-instantaneous", + "model_kind": "instantaneous", + "units": 128, + "train_seed": "1001", + "length_bucketing": true, + "tfjs_threads": 1, + "feature_set": "raw", + "layers": 2 +} diff --git a/configs/iter3/a/mlp.json b/configs/iter3/a/mlp.json new file mode 100644 index 00000000..78b3ec6f --- /dev/null +++ b/configs/iter3/a/mlp.json @@ -0,0 +1,18 @@ +{ + "learning_rate": 0.001, + "batch_size": 256, + "max_epochs": 150, + "patience": 6, + "max_wall_s": 172800, + "loss_weight_range": 1, + "loss_weight_course": 1, + "loss_weight_speed": 4, + "shuffle_labels": false, + "id": "mlp-control", + "model_kind": "mlp", + "units": 256, + "train_seed": "1004", + "length_bucketing": false, + "tfjs_threads": 1, + "feature_set": "raw" +} diff --git a/configs/iter3/a/plan.json b/configs/iter3/a/plan.json new file mode 100644 index 00000000..15fe376d --- /dev/null +++ b/configs/iter3/a/plan.json @@ -0,0 +1,23 @@ +{ + "id": "i3a", + "description": "Spec 007 (iteration 3, Annex A lever B.2.1, decision 22): the single change is the problem. Generator 0.3.0 with a second zig 180-300 s after leg 2 starts, and both turns across the line of sight (target 30-90 deg on the other bow); first-zig timing, ranges, noise and dropouts as iteration 2 (configs/iter3-ranges.json). Models: the 2c configuration from scratch (2 x 128 stacked recurrent layers, loss_weight_speed 4, max_epochs 150, patience 6, raw features, same seeds). Dataset seed 204: its own test split decides G2. Scored on validation first (diagnostic; validation rule FR-006).", + "dataset": { + "seed": "204", + "count": 100000, + "ranges": "configs/iter3-ranges.json", + "heldout_ranges": "configs/iter3-heldout-ranges.json", + "digests": { + "train.f32pack": "PLACEHOLDER", + "validation.f32pack": "PLACEHOLDER", + "test.f32pack": "PLACEHOLDER", + "heldout.f32pack": "PLACEHOLDER" + } + }, + "feature_set": "raw", + "configs": "configs/iter3/a", + "models": ["instantaneous", "time-late", "causal-cnn", "mlp", "instantaneous-shuffled"], + "evaluate": { + "command": "tma-g2", + "split": "validation" + } +} diff --git a/configs/iter3/a/time-late.json b/configs/iter3/a/time-late.json new file mode 100644 index 00000000..9aab0c40 --- /dev/null +++ b/configs/iter3/a/time-late.json @@ -0,0 +1,19 @@ +{ + "learning_rate": 0.001, + "batch_size": 256, + "max_epochs": 150, + "patience": 6, + "max_wall_s": 172800, + "loss_weight_range": 1, + "loss_weight_course": 1, + "loss_weight_speed": 4, + "shuffle_labels": false, + "id": "bilstm-time-late", + "model_kind": "time_late", + "units": 128, + "train_seed": "1002", + "length_bucketing": true, + "tfjs_threads": 1, + "feature_set": "raw", + "layers": 2 +} diff --git a/packages/models/test/train-plan.test.ts b/packages/models/test/train-plan.test.ts index 25e57e18..9f6875c5 100644 --- a/packages/models/test/train-plan.test.ts +++ b/packages/models/test/train-plan.test.ts @@ -86,6 +86,7 @@ describe("plan resolution", () => { "configs/iter2/b/plan.json", "configs/iter2/c/plan.json", "configs/iter2/s/plan.json", + "configs/iter3/a/plan.json", "configs/ci/plan.json", "configs/ci/derived/plan.json", ]) { @@ -185,7 +186,7 @@ describe("plan validation", () => { it("a fresh test split needs its own unreserved seed and a digest (spec 004 decision 17)", () => { const fresh = (f: unknown) => workspace((p) => (dataset(p).fresh_test = f)); - expect(validate(fresh({ seed: "204", digest: "PLACEHOLDER" })).status).toBe(0); + expect(validate(fresh({ seed: "205", digest: "PLACEHOLDER" })).status).toBe(0); expect(validate(fresh({ seed: "201", digest: "PLACEHOLDER" })).err).toMatch( /must differ from dataset.seed/, ); @@ -194,7 +195,7 @@ describe("plan validation", () => { ); // The iteration-2 dataset seed is only for configs/iter2/ plans (its test split decided G2). const elsewhere = workspace(); - mkdirSync(join(elsewhere, "configs/iter3")); + mkdirSync(join(elsewhere, "configs/iter3"), { recursive: true }); cpSync( join(elsewhere, "configs/iter2/a/plan.json"), join(elsewhere, "configs/iter3/plan.json"), @@ -209,7 +210,11 @@ describe("plan validation", () => { expect(validate(fresh({ seed: "203", digest: "PLACEHOLDER" })).err).toMatch( /reserved for iteration 2c/, ); - expect(validate(fresh({ seed: "204", digest: "abc" })).err).toMatch(/fresh_test.digest/); + // 204 is the iteration-3 dataset (spec 007): its own test split decides iteration 3's G2. + expect(validate(fresh({ seed: "204", digest: "PLACEHOLDER" })).err).toMatch( + /reserved for iteration 3 G2/, + ); + expect(validate(fresh({ seed: "205", digest: "abc" })).err).toMatch(/fresh_test.digest/); expect(validate(fresh({ digest: "PLACEHOLDER" })).err).toMatch(/fresh_test.seed/); }); }); @@ -276,11 +281,11 @@ describe("dataset content digests", () => { // With a fresh test split, test.f32pack is checked against the fresh digest, not the plan's. plan.dataset.digests["validation.f32pack"] = digests["validation.f32pack"] ?? ""; const withFresh = plan as unknown as { dataset: Record }; - withFresh.dataset.fresh_test = { seed: "204", digest: digests["test.f32pack"] }; + withFresh.dataset.fresh_test = { seed: "205", digest: digests["test.f32pack"] }; plan.dataset.digests["test.f32pack"] = "0".repeat(64); writeFileSync(p, JSON.stringify(plan)); expect(check().status).toBe(0); - withFresh.dataset.fresh_test = { seed: "204", digest: "1".repeat(64) }; + withFresh.dataset.fresh_test = { seed: "205", digest: "1".repeat(64) }; writeFileSync(p, JSON.stringify(plan)); expect(check().err).toMatch(/BAD test.f32pack .*\(plan: 1{64}\)/); }); diff --git a/scripts/train-plan.mjs b/scripts/train-plan.mjs index 92d7f60c..268a6f00 100644 --- a/scripts/train-plan.mjs +++ b/scripts/train-plan.mjs @@ -42,7 +42,7 @@ export const RESERVED_SEEDS = { 203: "iteration 2c G2 (fresh test)", }; /** Dataset seeds owned by one iteration's plans: its test split decided that iteration's gate. */ -export const ITERATION_DATASET_SEEDS = { 201: "2" }; +export const ITERATION_DATASET_SEEDS = { 201: "2", 204: "3" }; /** * Seeds whose test split has already decided a gate: a fresh test split may use one only in the * plan that decided it (so the committed plan of record still validates), never in another. @@ -53,6 +53,7 @@ export const RESERVED_TEST_SEEDS = { 201: { for: "iteration 2 G2 (the iteration-2 dataset's own test split)" }, 202: { for: "iteration 2b G2", plan: "configs/iter2/c/plan.json" }, 203: { for: "iteration 2c G2", plan: "configs/iter2/d/plan.json" }, + 204: { for: "iteration 3 G2 (the iteration-3 dataset's own test split)" }, }; class PlanError extends Error {} From a22a0498bdc4b1958c6a03fccefc1c777c555305 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 08:39:52 +0000 Subject: [PATCH 06/21] Iteration 3 plan: pin seed-204 content digests Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- configs/iter3/a/plan.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/configs/iter3/a/plan.json b/configs/iter3/a/plan.json index 15fe376d..688fdd29 100644 --- a/configs/iter3/a/plan.json +++ b/configs/iter3/a/plan.json @@ -7,10 +7,10 @@ "ranges": "configs/iter3-ranges.json", "heldout_ranges": "configs/iter3-heldout-ranges.json", "digests": { - "train.f32pack": "PLACEHOLDER", - "validation.f32pack": "PLACEHOLDER", - "test.f32pack": "PLACEHOLDER", - "heldout.f32pack": "PLACEHOLDER" + "train.f32pack": "f8f0629b7db7333c9f079aefadaf02c254ce0f83402e787cadb429e6cc7b32f8", + "validation.f32pack": "01fe0295953e175309c1fe00a6d8a81dcbeb461f237bb2c0e361ea8d464651d5", + "test.f32pack": "947776b48136e31317b5ebd84be80fc8dd64377aab5740400b6c6ef686d9733f", + "heldout.f32pack": "425f4f0df90edddf9392a92de578ece680a78040ffef5742fbcb3caf65b4ee6b" } }, "feature_set": "raw", From c804757853ce2a82e6aeef0e226a499726049f48 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 08:42:29 +0000 Subject: [PATCH 07/21] G0 checks the second zig and the across-LOS rule; viewer shows seed-204 validation - independent-geometry: hand-computed leg-3 case; with across_los ranges, every turn of every scenario recomputed with literal atan2 (target on the other bow, offset inside the drawn range) - Viewer catalogue: validation splits of unscored plans (seed 204, before training) and a featured three-leg bookmark Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- packages/eval/src/g0-checks.ts | 66 ++++++++++++++++++++++++-- packages/eval/src/g0.ts | 2 +- packages/eval/test/g0-checks.test.ts | 27 +++++++++++ packages/eval/test/golden/verdict.json | 2 +- site/scripts/prebuild.mjs | 17 +++++++ site/src/data/viewer-bookmarks.json | 6 +++ 6 files changed, 115 insertions(+), 5 deletions(-) diff --git a/packages/eval/src/g0-checks.ts b/packages/eval/src/g0-checks.ts index 699cba4f..8bd85372 100644 --- a/packages/eval/src/g0-checks.ts +++ b/packages/eval/src/g0-checks.ts @@ -3,7 +3,13 @@ // geometry checks that do not go through the solver. import { bearingData, solveBatch } from "@tma/classical"; import { angleDiff, createRng, degToRad, observe, simulateTruth } from "@tma/sim"; -import type { GateCheck, Prediction, Scenario, ScenarioParameters } from "@tma/types"; +import type { + GateCheck, + ParameterRanges, + Prediction, + Scenario, + ScenarioParameters, +} from "@tma/types"; import { rowError } from "./errors.js"; import { G0_TOLERANCE } from "./gate.js"; @@ -162,7 +168,10 @@ export function inTurnBearings(): { name: string; got: number; want: number }[] * the shared bearingOf) and, for every truth scenario, the t = 0 bearing and range against the * drawn parameters. Catches frame errors that the solver would share with the generator. */ -export function checkIndependentGeometry(truth: Iterable): GateCheck { +export function checkIndependentGeometry( + truth: Iterable, + ranges?: ParameterRanges, +): GateCheck { const fails: string[] = []; const bearingsOf = (p: ScenarioParameters): number[] => observe(createRng(1n), p, simulateTruth(p)).map((o) => o.bearing_rad); @@ -193,6 +202,24 @@ export function checkIndependentGeometry(truth: Iterable): GateCheck { if (!(Math.abs(angleDiff(c.got, c.want)) < 1e-6)) fails.push(`${c.name} turn case gave ${String(c.got)}, want ${String(c.want)}`); } + // Second zig (generator 0.3.0): as the leg-2 case, then a second instantaneous zig of −120° + // 100 s into leg 2 (t = 400 s, at (866.03, 3500)) onto course 300°. At t = 500 s ownship is at + // (866.03 − 1000 sin 60°, 3500 + 1000 cos 60°) = (0, 4000). + const leg3 = bearingsOf({ + ...KNOWN, + target_bearing0_rad: Math.PI / 2, + target_range0_m: 5000, + zig2_delay_s: 100, + zig2_angle_rad: degToRad(-120), + })[50]; + if (!(Math.abs(angleDiff(leg3 ?? NaN, Math.atan2(5000, -4000))) < 1e-6)) + fails.push(`leg-3 case gave ${String(leg3)}`); + // Across the line of sight (zig_rule across_los): every turn of every scenario puts the target + // on the other bow at an offset inside the drawn range, recomputed with literal atan2 from the + // stored positions at the last sample before the turn and the course after it. + const los = ranges?.zig_rule === "across_los" ? ranges.los_offset_rad : null; + let turns = 0; + const rel = (a: number): number => Math.atan2(Math.sin(a), Math.cos(a)); let n = 0; for (const s of truth) { const p = s.metadata.parameters; @@ -204,6 +231,36 @@ export function checkIndependentGeometry(truth: Iterable): GateCheck { fails.push(`scenario ${String(s.metadata.index)}: empty`); continue; } + if (los != null) { + const m = s.metadata; + const pairs: [number, number][] = [ + [(m.turn_start_index ?? m.zig_index + 1) - 1, m.zig_index], + ]; + if (m.zig2_index != null) + pairs.push([(m.turn2_start_index ?? m.zig2_index + 1) - 1, m.zig2_index]); + for (const [k, after] of pairs) { + const ok = s.ownship.states[k]; + const tk = s.target.states[k]; + const oa = s.ownship.states[after]; + if (ok === undefined || tk === undefined || oa === undefined) continue; + turns++; + const brg = Math.atan2(tk.x_m - ok.x_m, tk.y_m - ok.y_m); + // An instantaneous zig changes course at sample k itself: the old course is at k − 1. + const instant = after === k; + const oldCourse = (instant ? s.ownship.states[k - 1] : ok)?.course_rad ?? NaN; + const before = rel(brg - oldCourse); + const now = rel(brg - oa.course_rad); + const off = Math.abs(now); + const tol = 1e-4; // float32 storage of positions and courses + if (Math.sign(now) === Math.sign(before) || off < los.min - tol || off > los.max + tol) { + if (fails.length < 5) + fails.push( + `scenario ${String(m.index)}: turn at sample ${String(k)} not across the line of sight`, + ); + else fails.push("…"); + } + } + } const b = Math.atan2(t.x_m - o.x_m, t.y_m - o.y_m); const r = Math.hypot(t.x_m - o.x_m, t.y_m - o.y_m); if ( @@ -219,7 +276,10 @@ export function checkIndependentGeometry(truth: Iterable): GateCheck { "independent-geometry", fails.length === 0, fails.length === 0 - ? `hand-computed 315°, abeam, leg-2 and turn cases (starboard and port, mid-turn and leg 2) match; t = 0 bearing and range match the drawn parameters on ${String(n)} scenarios` + ? `hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2) and leg-3 cases match; t = 0 bearing and range match the drawn parameters on ${String(n)} scenarios` + + (los != null + ? `; ${String(turns)} turns put the target on the other bow inside the drawn offset range` + : "") : fails.slice(0, 6).join("; "), ); } diff --git a/packages/eval/src/g0.ts b/packages/eval/src/g0.ts index c185bd7a..ec5cc3fc 100644 --- a/packages/eval/src/g0.ts +++ b/packages/eval/src/g0.ts @@ -77,7 +77,7 @@ export function runG0(dataDir: string, experiment: string, commandLine: string): checkNoiseFreeRecovery(truth.header, scenarios, byIndex(classical)), checkPerTimestepRecovery(sub, byIndex(perTimestep)), checkBearingResiduals(scenarios), - checkIndependentGeometry(scenarios), + checkIndependentGeometry(scenarios, truth.header.parameter_ranges), checkNaiveReproducible(evA.metrics, evB.metrics), ]); writeEvaluation(experiment, evA); diff --git a/packages/eval/test/g0-checks.test.ts b/packages/eval/test/g0-checks.test.ts index 1d94aebe..46312b83 100644 --- a/packages/eval/test/g0-checks.test.ts +++ b/packages/eval/test/g0-checks.test.ts @@ -42,3 +42,30 @@ describe("G0 independent geometry: in-turn case", () => { expect(checkBearingResiduals(scenarios(6n, 50, TURN)).result).toBe("pass"); }); }); + +const ACROSS: ParameterRanges = { + ...TURN, + zig_rule: "across_los", + los_offset_rad: { min: degToRad(30), max: degToRad(90) }, + zig2_delay_s: { min: 180, max: 300 }, +}; + +describe("G0 independent geometry: generator 0.3.0 (spec 007)", () => { + it("passes on across-LOS two-zig scenarios and counts both turns", () => { + const c = checkIndependentGeometry(scenarios(8n, 100, ACROSS), ACROSS); + expect(c.result).toBe("pass"); + expect(c.detail).toContain("leg-3"); + expect(c.detail).toContain("200 turns put the target on the other bow"); + }); + + it("fails when random-rule scenarios are claimed to be across the line of sight", () => { + const random: ParameterRanges = { ...TURN, zig2_delay_s: { min: 180, max: 300 } }; + const c = checkIndependentGeometry(scenarios(8n, 100, random), { + ...random, + zig_rule: "across_los", + los_offset_rad: { min: degToRad(30), max: degToRad(90) }, + }); + expect(c.result).toBe("fail"); + expect(c.detail).toMatch(/not across the line of sight/); + }); +}); diff --git a/packages/eval/test/golden/verdict.json b/packages/eval/test/golden/verdict.json index 6f3d294d..43e80796 100644 --- a/packages/eval/test/golden/verdict.json +++ b/packages/eval/test/golden/verdict.json @@ -16,7 +16,7 @@ "result": "pass" }, { - "detail": "hand-computed 315°, abeam, leg-2 and turn cases (starboard and port, mid-turn and leg 2) match; t = 0 bearing and range match the drawn parameters on 4 scenarios", + "detail": "hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2) and leg-3 cases match; t = 0 bearing and range match the drawn parameters on 4 scenarios", "name": "independent-geometry", "result": "pass" }, diff --git a/site/scripts/prebuild.mjs b/site/scripts/prebuild.mjs index 7ac5ea82..cabfa31c 100644 --- a/site/scripts/prebuild.mjs +++ b/site/scripts/prebuild.mjs @@ -131,6 +131,11 @@ const VIEWER_PAIRS = [ { id: "i2c", label: "Iteration 2c", experiment: "20261016-i2d-g2" }, ]; const VIEWER_MODELS = { instantaneous: "lstm-instantaneous", timeLate: "bilstm-time-late" }; +/** + * Datasets of plans not yet scored, shown before training so the owner can check the engagements + * (Annex A B.2.1). Only the validation split: a test split stays unseen until its gate. + */ +const VIEWER_PLANS = [{ plan: "configs/iter3/a/plan.json", label: "Iteration 3" }]; /** The panel's decision where it overrides the machine result (e.g. G1 passed under rule 1). */ function decision(verdict) { @@ -212,6 +217,18 @@ function viewer() { naive, }); } + for (const v of [...VIEWER_PLANS].reverse()) { + const d = read(join(root, v.plan)).dataset; + sources.unshift({ + id: `s${d.seed}-validation`, + label: `Seed ${d.seed} validation (${v.label}, before training)`, + seed: d.seed, + start: Math.floor(0.8 * d.count), + end: Math.floor(0.9 * d.count), + ranges: read(join(root, d.ranges)), + note: `The ${v.label.toLowerCase()} dataset (${v.plan}); no model has been trained on it yet.`, + }); + } const sim = readFileSync(join(root, "packages", "sim", "src", "scenario.ts"), "utf8"); const generatorVersion = /GENERATOR_VERSION = "([^"]+)"/.exec(sim)?.[1] ?? "unknown"; const bookmarks = read(join(site, "src", "data", "viewer-bookmarks.json")); diff --git a/site/src/data/viewer-bookmarks.json b/site/src/data/viewer-bookmarks.json index 0dc82b16..c04184b7 100644 --- a/site/src/data/viewer-bookmarks.json +++ b/site/src/data/viewer-bookmarks.json @@ -1,4 +1,10 @@ [ + { + "label": "Iteration 3 engagement: two zigs across the line of sight (before training)", + "source": "s204-validation", + "index": 80000, + "mode": "truth" + }, { "label": "Typical engagement: 2c instantaneous near its median errors at zig + 10 min", "source": "s203-test", From 10688314ebe75a5213c66000b4f0bed1d1959ec0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 08:47:20 +0000 Subject: [PATCH 08/21] Slices: zig bands up to 180 degrees for across-LOS turns (spec 007) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- packages/eval/src/slices.ts | 16 +++++++++++++--- packages/eval/test/slices.test.ts | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/eval/src/slices.ts b/packages/eval/src/slices.ts index 3c1cf8fd..cfd35f38 100644 --- a/packages/eval/src/slices.ts +++ b/packages/eval/src/slices.ts @@ -14,6 +14,10 @@ const ZIG_BANDS_DEG: [number, number][] = [ [30, 50], [50, 70], [70, 90], + // Iteration 3 (spec 007): across-the-line-of-sight turns reach 180°. + [90, 120], + [120, 150], + [150, 180], ]; const DROPOUT_BANDS_PCT: [number, number][] = [ @@ -30,10 +34,16 @@ function band(value: number, bands: [number, number][], unit: string): string { return "other"; } -/** Zig angles are drawn in [30°, 90°]; float32 rounding can land exactly on 90°. */ +/** + * Zig angles are drawn in [30°, 90°] (random rule) or fall in [30°, 180°] (across-LOS rule); + * float32 rounding can land exactly on 90° (kept in 70–90°, as before iteration 3) or just + * outside [30°, 180°]. + */ function zigBand(deg: number): string { - const b = band(deg, ZIG_BANDS_DEG, "deg"); - return b === "90+deg" && deg < 90.001 ? "70-90deg" : b; + if (deg >= 90 && deg < 90.001) return "70-90deg"; + if (deg >= 180 && deg < 180.001) return "150-180deg"; + if (deg < 30 && deg > 29.999) return "30-50deg"; + return band(deg, ZIG_BANDS_DEG, "deg"); } /** Dropout fractions are drawn in [0, 0.10]; float32 rounding can land just above 10%. */ diff --git a/packages/eval/test/slices.test.ts b/packages/eval/test/slices.test.ts index 05f6c578..3c7b772a 100644 --- a/packages/eval/test/slices.test.ts +++ b/packages/eval/test/slices.test.ts @@ -25,6 +25,21 @@ describe("slices", () => { expect(sliceKeys(m).slice(1, 3)).toEqual(["range_band=5-10km", "zig_band=70-90deg"]); }); + it("iteration-3 turns beyond 90° get their own bands up to 180° (spec 007)", () => { + const [s] = scenarios(4n, 1); + if (s === undefined) throw new Error(); + const zig = (deg: number) => + sliceKeys({ + ...s.metadata, + parameters: { ...s.metadata.parameters, zig_angle_rad: (deg * Math.PI) / 180 }, + })[2]; + expect(zig(100)).toBe("zig_band=90-120deg"); + expect(zig(-143)).toBe("zig_band=120-150deg"); + expect(zig(179.9)).toBe("zig_band=150-180deg"); + expect(zig(180.0004)).toBe("zig_band=150-180deg"); + expect(zig(29.9995)).toBe("zig_band=30-50deg"); + }); + it("dropout band: 3% → 0-5pct, 7% → 5-10pct, boundary 5% → upper band (T021)", () => { const [s] = scenarios(5n, 1); if (s === undefined) throw new Error(); From eb19125ded65648d9bf45751e97ca83bf79578af Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 08:49:31 +0000 Subject: [PATCH 09/21] G0 on generator 0.3.0 (seed 42, iteration-3 ranges, noise-free): machine pass, pending panel Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- experiments/20261017-g0-i3/manifest.json | 163 ++++++++ experiments/20261017-g0-i3/metrics.csv | 27 ++ experiments/20261017-g0-i3/metrics.json | 470 +++++++++++++++++++++++ experiments/20261017-g0-i3/seeds.txt | 2 + experiments/20261017-g0-i3/verdict.json | 32 ++ 5 files changed, 694 insertions(+) create mode 100644 experiments/20261017-g0-i3/manifest.json create mode 100644 experiments/20261017-g0-i3/metrics.csv create mode 100644 experiments/20261017-g0-i3/metrics.json create mode 100644 experiments/20261017-g0-i3/seeds.txt create mode 100644 experiments/20261017-g0-i3/verdict.json diff --git a/experiments/20261017-g0-i3/manifest.json b/experiments/20261017-g0-i3/manifest.json new file mode 100644 index 00000000..2269d0b9 --- /dev/null +++ b/experiments/20261017-g0-i3/manifest.json @@ -0,0 +1,163 @@ +{ + "command_lines": [ + "tma-generate --seed 42 --count 100000 --noise-free --ranges sha256:bdf46080f66c24c867245c482121a2f1b82d3f13e59ef8c0cabccde08d1172be --heldout-ranges sha256:7d9e444c99d8ac6f759d0226c1724a7ea19cb34237465730f4d1aed51de1aca1", + "tma-g0 --data data/g0-i3 --experiment experiments/20261017-g0-i3" + ], + "created": "2026-10-17T08:49:17.120Z", + "datasets": [ + { + "digest": "f76097e8d6d3ee83aad97ef77722c62298fc33312f595ae5a39d9e1da107c6d0", + "header": { + "created_by": "tma-generate --seed 42 --count 100000 --noise-free --ranges sha256:bdf46080f66c24c867245c482121a2f1b82d3f13e59ef8c0cabccde08d1172be --heldout-ranges sha256:7d9e444c99d8ac6f759d0226c1724a7ea19cb34237465730f4d1aed51de1aca1", + "dataset_seed": "42", + "generator_git_commit": "90f4873b54cce6af72efadec5f1ed4f976c0008c", + "generator_git_dirty": false, + "generator_version": "0.3.0", + "index_end": 80000, + "index_start": 0, + "parameter_ranges": { + "dropout_fraction": { + "max": 0, + "min": 0 + }, + "dt_s": 10, + "los_offset_rad": { + "max": 1.5707963267948966, + "min": 0.5235987755982988 + }, + "min_range_m": 100, + "noise_sigma_rad_values": [ + 0 + ], + "ownship_speed_mps": { + "max": 7.716666666666667, + "min": 2.5722222222222224 + }, + "ownship_turn_rate_rad_s": { + "max": 0.05235987755982989, + "min": 0.008726646259971648 + }, + "range_distribution": "log_uniform", + "target_range0_m": { + "max": 20000, + "min": 2000 + }, + "target_speed_mps": { + "max": 10.28888888888889, + "min": 1.5433333333333334 + }, + "window_s": { + "max": 2400, + "min": 600 + }, + "zig2_delay_s": { + "max": 300, + "min": 180 + }, + "zig_angle_abs_rad": { + "max": 1.5707963267948966, + "min": 0.5235987755982988 + }, + "zig_fraction": { + "max": 0.6, + "min": 0.4 + }, + "zig_rule": "across_los" + }, + "record": "header", + "redraw_count": 17247, + "scenario_count": 80000, + "schema_version": "0.9.0", + "split": "train" + }, + "path": "data/g0-i3/train.f32pack" + }, + { + "digest": "ea38d17219a941d86c95f4323ca669d9f8f264dcc807d597c2229a5ac51c6564", + "header": { + "created_by": "tma-generate --seed 42 --count 100000 --noise-free --ranges sha256:bdf46080f66c24c867245c482121a2f1b82d3f13e59ef8c0cabccde08d1172be --heldout-ranges sha256:7d9e444c99d8ac6f759d0226c1724a7ea19cb34237465730f4d1aed51de1aca1", + "dataset_seed": "42", + "generator_git_commit": "90f4873b54cce6af72efadec5f1ed4f976c0008c", + "generator_git_dirty": false, + "generator_version": "0.3.0", + "index_end": 100000, + "index_start": 90000, + "parameter_ranges": { + "dropout_fraction": { + "max": 0, + "min": 0 + }, + "dt_s": 10, + "los_offset_rad": { + "max": 1.5707963267948966, + "min": 0.5235987755982988 + }, + "min_range_m": 100, + "noise_sigma_rad_values": [ + 0 + ], + "ownship_speed_mps": { + "max": 7.716666666666667, + "min": 2.5722222222222224 + }, + "ownship_turn_rate_rad_s": { + "max": 0.05235987755982989, + "min": 0.008726646259971648 + }, + "range_distribution": "log_uniform", + "target_range0_m": { + "max": 20000, + "min": 2000 + }, + "target_speed_mps": { + "max": 10.28888888888889, + "min": 1.5433333333333334 + }, + "window_s": { + "max": 2400, + "min": 600 + }, + "zig2_delay_s": { + "max": 300, + "min": 180 + }, + "zig_angle_abs_rad": { + "max": 1.5707963267948966, + "min": 0.5235987755982988 + }, + "zig_fraction": { + "max": 0.6, + "min": 0.4 + }, + "zig_rule": "across_los" + }, + "record": "header", + "redraw_count": 2052, + "scenario_count": 10000, + "schema_version": "0.9.0", + "split": "test" + }, + "path": "data/g0-i3/test.f32pack" + } + ], + "estimators": [ + { + "name": "naive", + "settings_json": "{\"fitted_on\":\"train\",\"means\":{\"course_cos\":-0.0010852328029513894,\"course_sin\":-0.0022651612364012388,\"log_range_km\":2.27659913273481,\"n\":80000,\"speed_mps\":5.9123806832298635}}" + }, + { + "name": "classical", + "settings_json": "{\"max_iterations\":50,\"method\":\"levenberg-marquardt\",\"mode\":\"final\"}" + }, + { + "name": "classical:per_timestep", + "settings_json": "{\"grid_every\":6,\"max_iterations\":50,\"method\":\"levenberg-marquardt\",\"mode\":\"per_timestep\",\"note\":\"first scenarios of the test split, for check classical-per-timestep-recovers\",\"scenarios\":1000}" + } + ], + "generator_version": "0.3.0", + "git_commit": "c804757853ce2a82e6aeef0e226a499726049f48", + "git_dirty": false, + "id": "20261017-g0-i3", + "node_version": "v22.22.0", + "schema_version": "0.9.0" +} diff --git a/experiments/20261017-g0-i3/metrics.csv b/experiments/20261017-g0-i3/metrics.csv new file mode 100644 index 00000000..885fc756 --- /dev/null +++ b/experiments/20261017-g0-i3/metrics.csv @@ -0,0 +1,27 @@ +estimator,slice,point,n,excluded,range_rel_median,range_rel_p90,course_deg_median,course_deg_p90,speed_kt_median,speed_kt_p90,gross_error_rate,non_converged_rate,position_m_mean,position_window_m_median,position_window_m_p90 +naive,overall,final,10000,0,0.41296159627504475,1.5327997889522416,89.45474167284688,162.58337096507148,4.243544316961235,7.647355148653232,0.3878,0,5452.883076396702,4401.405811239404,11507.173492769414 +naive,dropout_band=0-5pct,final,10000,0,0.41296159627504475,1.5327997889522416,89.45474167284688,162.58337096507148,4.243544316961235,7.647355148653232,0.3878,0,5452.883076396702,4401.405811239404,11507.173492769414 +naive,range_band=0-2km,final,133,0,3.554155199769074,10.189099437703875,90.76955308807433,167.12415266965337,4.888747783767997,7.68978966492556,1,0,7541.811836895824,7604.0155201139005,8872.685837083234 +naive,range_band=10-20km,final,3634,0,0.4051582349171333,0.5659043446436742,89.08235975929779,161.3485137442682,4.333764790973704,7.650062383894786,0.25949367088607594,0,6745.622183900448,6441.378841890848,12363.699782051417 +naive,range_band=2-5km,final,1863,0,1.090394363945709,3.7852475362152567,91.6602780577357,163.413635327571,4.118415500896281,7.688682949568745,0.775630703166935,0,4947.390624738301,5082.411505237025,7707.336892376554 +naive,range_band=20-30km,final,515,0,0.634768967095896,0.7013337367422446,87.71670441588876,163.07976179319132,4.6361965443094135,7.529825268244897,0.9825242718446602,0,17343.277986636796,16934.112145062863,22879.839699095115 +naive,range_band=30+km,final,9,0,0.7485700238919062,0.7642858167300461,106.55888773201448,157.09385230648385,3.79518092837241,8.285669219674096,1,0,28713.815931989186,29008.802337317957,31592.53709787277 +naive,range_band=5-10km,final,3846,0,0.24299138820601726,1.0084669091137104,89.37135872345937,162.82039583211494,4.159294451802132,7.6459358371874675,0.218928757150286,0,2757.4044547509648,2380.8681528827433,5669.739080810058 +naive,sigma=0deg,final,10000,0,0.41296159627504475,1.5327997889522416,89.45474167284688,162.58337096507148,4.243544316961235,7.647355148653232,0.3878,0,5452.883076396702,4401.405811239404,11507.173492769414 +naive,zig_band=30-50deg,final,84,0,0.4614850895772321,1.6337174892005477,82.8358375746193,162.83246477637834,3.8864580652883705,7.198060923467186,0.4642857142857143,0,4648.201741165161,4436.822024701133,8943.548089216078 +naive,zig_band=50-70deg,final,319,0,0.39053573219917426,1.700969404255579,93.63977616223211,164.3109514297814,4.118990177212214,7.61714084617005,0.3667711598746082,0,4489.542019678022,3594.839080360501,8942.730785294072 +naive,zig_band=70-90deg,final,501,0,0.37249923148578495,1.6897176776549168,88.19526845027487,165.7484057217456,4.185012607286095,7.589079494352731,0.3333333333333333,0,4447.1342740142945,3821.0031499270726,8588.865422100125 +naive,zig_band=90+deg,final,9096,0,0.4158303430447431,1.521811879170088,89.5008591101228,162.38290491293162,4.256870319209912,7.654735799999525,0.3908311345646438,0,5549.494683613761,4479.3602910220725,11767.972691651847 +classical,overall,final,10000,0,9.625246868512277e-8,4.812320236817345e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0027786471935062848,0.0009841623227903502,0.006905339660024878 +classical,dropout_band=0-5pct,final,10000,0,9.625246868512277e-8,4.812320236817345e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0027786471935062848,0.0009841623227903502,0.006905339660024878 +classical,range_band=0-2km,final,133,0,4.794212208460275e-8,1.6569781425015622e-7,0,0,0,9.268972837641738e-7,0,0,0.00013530523967983556,0.00006103515625,0.00048828125 +classical,range_band=10-20km,final,3634,0,1.3944097671445805e-7,6.497986438982158e-7,0,0.00005464151336010202,0.0000018537945675283476,0.000013903459256462607,0,0,0.004558875236756464,0.0021836601342771385,0.010708843847393217 +classical,range_band=2-5km,final,1863,0,6.045721709550033e-8,2.276486079566025e-7,0,0.000005122641877509563,2.3172432094104345e-7,0.0000018537945675283476,0,0,0.00043650168202587875,0.000244140625,0.0010429692071920813 +classical,range_band=20-30km,final,515,0,1.990455683591539e-7,8.591447716087028e-7,0.0000034150945850063763,0.00005464151336010202,0.0000055613837025850425,0.000027806918512925215,0,0,0.009508762893649318,0.005859375,0.02344258571124571 +classical,range_band=30+km,final,9,0,6.851410989829762e-8,0.0000011607263146006765,0.0000034150945850063763,0.000040981135020076506,0.0000037075891350566953,0.00005376004245832208,0,0,0.00874825760899502,0.0021836601342771385,0.0453970285123807 +classical,range_band=5-10km,final,3846,0,8.70445563787829e-8,3.6632019879375324e-7,0,0.000025613209387547814,9.268972837641738e-7,0.0000055613837025850425,0,0,0.0014073255032468878,0.0009765625,0.0034375312110177547 +classical,sigma=0deg,final,10000,0,9.625246868512277e-8,4.812320236817345e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0027786471935062848,0.0009841623227903502,0.006905339660024878 +classical,zig_band=30-50deg,final,84,0,1.1411674223616281e-7,5.385067567064481e-7,0,0.000040981135020076506,0.0000018537945675283476,0.000011122767405170085,0,0,0.005687213290986321,0.0009765625,0.006176323555016366 +classical,zig_band=50-70deg,final,319,0,1.1695404891416201e-7,5.556545196015425e-7,0,0.00002732075668005101,0.0000018537945675283476,0.000009268972837641738,0,0,0.0025582287232674573,0.001078527651798846,0.007814645472616538 +classical,zig_band=70-90deg,final,501,0,1.0593828399940142e-7,5.632217038077209e-7,0,0.000040981135020076506,0.0000013903459256462606,0.000011122767405170085,0,0,0.0029160767709236514,0.0010066175843793117,0.007042092334890605 +classical,zig_band=90+deg,final,9096,0,9.493164012994429e-8,4.714443211630349e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0027519477345717726,0.0009841623227903502,0.006905339660024878 diff --git a/experiments/20261017-g0-i3/metrics.json b/experiments/20261017-g0-i3/metrics.json new file mode 100644 index 00000000..b005158c --- /dev/null +++ b/experiments/20261017-g0-i3/metrics.json @@ -0,0 +1,470 @@ +[ + { + "course_deg_median": 89.45474167284688, + "course_deg_p90": 162.58337096507148, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3878, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5452.883076396702, + "position_window_m_median": 4401.405811239404, + "position_window_m_p90": 11507.173492769414, + "range_rel_median": 0.41296159627504475, + "range_rel_p90": 1.5327997889522416, + "slice": "overall", + "speed_kt_median": 4.243544316961235, + "speed_kt_p90": 7.647355148653232 + }, + { + "course_deg_median": 89.45474167284688, + "course_deg_p90": 162.58337096507148, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3878, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5452.883076396702, + "position_window_m_median": 4401.405811239404, + "position_window_m_p90": 11507.173492769414, + "range_rel_median": 0.41296159627504475, + "range_rel_p90": 1.5327997889522416, + "slice": "dropout_band=0-5pct", + "speed_kt_median": 4.243544316961235, + "speed_kt_p90": 7.647355148653232 + }, + { + "course_deg_median": 90.76955308807433, + "course_deg_p90": 167.12415266965337, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 1, + "n": 133, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 7541.811836895824, + "position_window_m_median": 7604.0155201139005, + "position_window_m_p90": 8872.685837083234, + "range_rel_median": 3.554155199769074, + "range_rel_p90": 10.189099437703875, + "slice": "range_band=0-2km", + "speed_kt_median": 4.888747783767997, + "speed_kt_p90": 7.68978966492556 + }, + { + "course_deg_median": 89.08235975929779, + "course_deg_p90": 161.3485137442682, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.25949367088607594, + "n": 3634, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 6745.622183900448, + "position_window_m_median": 6441.378841890848, + "position_window_m_p90": 12363.699782051417, + "range_rel_median": 0.4051582349171333, + "range_rel_p90": 0.5659043446436742, + "slice": "range_band=10-20km", + "speed_kt_median": 4.333764790973704, + "speed_kt_p90": 7.650062383894786 + }, + { + "course_deg_median": 91.6602780577357, + "course_deg_p90": 163.413635327571, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.775630703166935, + "n": 1863, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 4947.390624738301, + "position_window_m_median": 5082.411505237025, + "position_window_m_p90": 7707.336892376554, + "range_rel_median": 1.090394363945709, + "range_rel_p90": 3.7852475362152567, + "slice": "range_band=2-5km", + "speed_kt_median": 4.118415500896281, + "speed_kt_p90": 7.688682949568745 + }, + { + "course_deg_median": 87.71670441588876, + "course_deg_p90": 163.07976179319132, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.9825242718446602, + "n": 515, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 17343.277986636796, + "position_window_m_median": 16934.112145062863, + "position_window_m_p90": 22879.839699095115, + "range_rel_median": 0.634768967095896, + "range_rel_p90": 0.7013337367422446, + "slice": "range_band=20-30km", + "speed_kt_median": 4.6361965443094135, + "speed_kt_p90": 7.529825268244897 + }, + { + "course_deg_median": 106.55888773201448, + "course_deg_p90": 157.09385230648385, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 1, + "n": 9, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 28713.815931989186, + "position_window_m_median": 29008.802337317957, + "position_window_m_p90": 31592.53709787277, + "range_rel_median": 0.7485700238919062, + "range_rel_p90": 0.7642858167300461, + "slice": "range_band=30+km", + "speed_kt_median": 3.79518092837241, + "speed_kt_p90": 8.285669219674096 + }, + { + "course_deg_median": 89.37135872345937, + "course_deg_p90": 162.82039583211494, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.218928757150286, + "n": 3846, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 2757.4044547509648, + "position_window_m_median": 2380.8681528827433, + "position_window_m_p90": 5669.739080810058, + "range_rel_median": 0.24299138820601726, + "range_rel_p90": 1.0084669091137104, + "slice": "range_band=5-10km", + "speed_kt_median": 4.159294451802132, + "speed_kt_p90": 7.6459358371874675 + }, + { + "course_deg_median": 89.45474167284688, + "course_deg_p90": 162.58337096507148, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3878, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5452.883076396702, + "position_window_m_median": 4401.405811239404, + "position_window_m_p90": 11507.173492769414, + "range_rel_median": 0.41296159627504475, + "range_rel_p90": 1.5327997889522416, + "slice": "sigma=0deg", + "speed_kt_median": 4.243544316961235, + "speed_kt_p90": 7.647355148653232 + }, + { + "course_deg_median": 82.8358375746193, + "course_deg_p90": 162.83246477637834, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.4642857142857143, + "n": 84, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 4648.201741165161, + "position_window_m_median": 4436.822024701133, + "position_window_m_p90": 8943.548089216078, + "range_rel_median": 0.4614850895772321, + "range_rel_p90": 1.6337174892005477, + "slice": "zig_band=30-50deg", + "speed_kt_median": 3.8864580652883705, + "speed_kt_p90": 7.198060923467186 + }, + { + "course_deg_median": 93.63977616223211, + "course_deg_p90": 164.3109514297814, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3667711598746082, + "n": 319, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 4489.542019678022, + "position_window_m_median": 3594.839080360501, + "position_window_m_p90": 8942.730785294072, + "range_rel_median": 0.39053573219917426, + "range_rel_p90": 1.700969404255579, + "slice": "zig_band=50-70deg", + "speed_kt_median": 4.118990177212214, + "speed_kt_p90": 7.61714084617005 + }, + { + "course_deg_median": 88.19526845027487, + "course_deg_p90": 165.7484057217456, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3333333333333333, + "n": 501, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 4447.1342740142945, + "position_window_m_median": 3821.0031499270726, + "position_window_m_p90": 8588.865422100125, + "range_rel_median": 0.37249923148578495, + "range_rel_p90": 1.6897176776549168, + "slice": "zig_band=70-90deg", + "speed_kt_median": 4.185012607286095, + "speed_kt_p90": 7.589079494352731 + }, + { + "course_deg_median": 89.5008591101228, + "course_deg_p90": 162.38290491293162, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3908311345646438, + "n": 9096, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5549.494683613761, + "position_window_m_median": 4479.3602910220725, + "position_window_m_p90": 11767.972691651847, + "range_rel_median": 0.4158303430447431, + "range_rel_p90": 1.521811879170088, + "slice": "zig_band=90+deg", + "speed_kt_median": 4.256870319209912, + "speed_kt_p90": 7.654735799999525 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0027786471935062848, + "position_window_m_median": 0.0009841623227903502, + "position_window_m_p90": 0.006905339660024878, + "range_rel_median": 9.625246868512277e-8, + "range_rel_p90": 4.812320236817345e-7, + "slice": "overall", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0027786471935062848, + "position_window_m_median": 0.0009841623227903502, + "position_window_m_p90": 0.006905339660024878, + "range_rel_median": 9.625246868512277e-8, + "range_rel_p90": 4.812320236817345e-7, + "slice": "dropout_band=0-5pct", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 133, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.00013530523967983556, + "position_window_m_median": 0.00006103515625, + "position_window_m_p90": 0.00048828125, + "range_rel_median": 4.794212208460275e-8, + "range_rel_p90": 1.6569781425015622e-7, + "slice": "range_band=0-2km", + "speed_kt_median": 0, + "speed_kt_p90": 9.268972837641738e-7 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00005464151336010202, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 3634, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.004558875236756464, + "position_window_m_median": 0.0021836601342771385, + "position_window_m_p90": 0.010708843847393217, + "range_rel_median": 1.3944097671445805e-7, + "range_rel_p90": 6.497986438982158e-7, + "slice": "range_band=10-20km", + "speed_kt_median": 0.0000018537945675283476, + "speed_kt_p90": 0.000013903459256462607 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.000005122641877509563, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 1863, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.00043650168202587875, + "position_window_m_median": 0.000244140625, + "position_window_m_p90": 0.0010429692071920813, + "range_rel_median": 6.045721709550033e-8, + "range_rel_p90": 2.276486079566025e-7, + "slice": "range_band=2-5km", + "speed_kt_median": 2.3172432094104345e-7, + "speed_kt_p90": 0.0000018537945675283476 + }, + { + "course_deg_median": 0.0000034150945850063763, + "course_deg_p90": 0.00005464151336010202, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 515, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.009508762893649318, + "position_window_m_median": 0.005859375, + "position_window_m_p90": 0.02344258571124571, + "range_rel_median": 1.990455683591539e-7, + "range_rel_p90": 8.591447716087028e-7, + "slice": "range_band=20-30km", + "speed_kt_median": 0.0000055613837025850425, + "speed_kt_p90": 0.000027806918512925215 + }, + { + "course_deg_median": 0.0000034150945850063763, + "course_deg_p90": 0.000040981135020076506, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 9, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.00874825760899502, + "position_window_m_median": 0.0021836601342771385, + "position_window_m_p90": 0.0453970285123807, + "range_rel_median": 6.851410989829762e-8, + "range_rel_p90": 0.0000011607263146006765, + "slice": "range_band=30+km", + "speed_kt_median": 0.0000037075891350566953, + "speed_kt_p90": 0.00005376004245832208 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.000025613209387547814, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 3846, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0014073255032468878, + "position_window_m_median": 0.0009765625, + "position_window_m_p90": 0.0034375312110177547, + "range_rel_median": 8.70445563787829e-8, + "range_rel_p90": 3.6632019879375324e-7, + "slice": "range_band=5-10km", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.0000055613837025850425 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0027786471935062848, + "position_window_m_median": 0.0009841623227903502, + "position_window_m_p90": 0.006905339660024878, + "range_rel_median": 9.625246868512277e-8, + "range_rel_p90": 4.812320236817345e-7, + "slice": "sigma=0deg", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.000040981135020076506, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 84, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.005687213290986321, + "position_window_m_median": 0.0009765625, + "position_window_m_p90": 0.006176323555016366, + "range_rel_median": 1.1411674223616281e-7, + "range_rel_p90": 5.385067567064481e-7, + "slice": "zig_band=30-50deg", + "speed_kt_median": 0.0000018537945675283476, + "speed_kt_p90": 0.000011122767405170085 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 319, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0025582287232674573, + "position_window_m_median": 0.001078527651798846, + "position_window_m_p90": 0.007814645472616538, + "range_rel_median": 1.1695404891416201e-7, + "range_rel_p90": 5.556545196015425e-7, + "slice": "zig_band=50-70deg", + "speed_kt_median": 0.0000018537945675283476, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.000040981135020076506, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 501, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0029160767709236514, + "position_window_m_median": 0.0010066175843793117, + "position_window_m_p90": 0.007042092334890605, + "range_rel_median": 1.0593828399940142e-7, + "range_rel_p90": 5.632217038077209e-7, + "slice": "zig_band=70-90deg", + "speed_kt_median": 0.0000013903459256462606, + "speed_kt_p90": 0.000011122767405170085 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 9096, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0027519477345717726, + "position_window_m_median": 0.0009841623227903502, + "position_window_m_p90": 0.006905339660024878, + "range_rel_median": 9.493164012994429e-8, + "range_rel_p90": 4.714443211630349e-7, + "slice": "zig_band=90+deg", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + } +] diff --git a/experiments/20261017-g0-i3/seeds.txt b/experiments/20261017-g0-i3/seeds.txt new file mode 100644 index 00000000..feb4bb1f --- /dev/null +++ b/experiments/20261017-g0-i3/seeds.txt @@ -0,0 +1,2 @@ +train seed=42 index=[0, 80000) data/g0-i3/train.f32pack +test seed=42 index=[90000, 100000) data/g0-i3/test.f32pack diff --git a/experiments/20261017-g0-i3/verdict.json b/experiments/20261017-g0-i3/verdict.json new file mode 100644 index 00000000..3564fff1 --- /dev/null +++ b/experiments/20261017-g0-i3/verdict.json @@ -0,0 +1,32 @@ +{ + "checks": [ + { + "detail": "10000/10000 scenarios within tolerance (range 0.001, course 0.1°, speed 0.01 kt); worst range 8.70e-6, course 2.54e-3°, speed 3.43e-4 kt", + "name": "classical-recovers-noise-free-truth", + "result": "pass" + }, + { + "detail": "1000 scenarios, 72208 rows: 0 non-converged; 67208/67208 rows ≥ 60 s after the zig within tolerance (worst range 9.78e-6); worst range in the first 60 s 1.92e-5 (reported, not gated)", + "name": "classical-per-timestep-recovers", + "result": "pass" + }, + { + "detail": "10000/10000 final solutions with RMS bearing residual ≤ 0.000001 rad; worst 2.77e-7 rad", + "name": "bearing-residuals-at-float32-precision", + "result": "pass" + }, + { + "detail": "hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2) and leg-3 cases match; t = 0 bearing and range match the drawn parameters on 10000 scenarios; 20000 turns put the target on the other bow inside the drawn offset range", + "name": "independent-geometry", + "result": "pass" + }, + { + "detail": "two in-process naive fits gave identical metrics (determinism only; cross-process and cross-commit reproduction is shown by re-running G0 from the manifest)", + "name": "naive-baseline-reproduces", + "result": "pass" + } + ], + "decided_by": "harness (pending agent panel review)", + "gate": "G0", + "result": "pass" +} From 007c6bea3a0b57749dc3ca5d73b0e757c1f20382 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 08:52:25 +0000 Subject: [PATCH 10/21] Viewer: round the second-zig delay in the scenario panel Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- packages/demos/src/viewer/Panels.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/demos/src/viewer/Panels.tsx b/packages/demos/src/viewer/Panels.tsx index 3a0c16d3..16717c9a 100644 --- a/packages/demos/src/viewer/Panels.tsx +++ b/packages/demos/src/viewer/Panels.tsx @@ -235,7 +235,7 @@ export function Metadata({ s, source, generatorVersion }: MetadataProps): JSX.El : [ [ "zig 2", - `${radToDeg(p.zig2_angle_rad).toFixed(0)}°, ${String(p.zig2_delay_s ?? 0)} s after leg 2 starts, leg 3 from ${((m.zig2_index * p.dt_s) / 60).toFixed(1)} min`, + `${radToDeg(p.zig2_angle_rad).toFixed(0)}°, ${String(Math.round(p.zig2_delay_s ?? 0))} s after leg 2 starts, leg 3 from ${((m.zig2_index * p.dt_s) / 60).toFixed(1)} min`, ] as [string, string], ]), [ From 7d860de9a42b705971193aec8dbd07a3b03c9157 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 09:06:29 +0000 Subject: [PATCH 11/21] G0 guard test: the committed 0.3.0 G0 is the latest, accepted only after its panel Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- packages/models/test/g0-guard.test.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/models/test/g0-guard.test.ts b/packages/models/test/g0-guard.test.ts index 9fff5eaa..96154396 100644 --- a/packages/models/test/g0-guard.test.ts +++ b/packages/models/test/g0-guard.test.ts @@ -1,4 +1,4 @@ -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { GENERATOR_VERSION } from "@tma/sim"; import { describe, expect, it } from "vitest"; @@ -136,17 +136,19 @@ describe("G0 guard (constitution V)", () => { ).not.toBeNull(); }); - it("generator 0.3.0 does not accept the iteration-2 G0 (0.2.0): G0 must be re-run (spec 007)", () => { - // The latest committed G0 (20261013-g0-i2-r2) verified generator 0.2.0. Generator 0.3.0 adds - // the second zig and the across-LOS rule, so training refuses until a G0 on 0.3.0 passes its - // panel; this test moves to that experiment when it is committed. + it("the committed iteration-3 G0 (generator 0.3.0) is the latest, and needs its panel review", () => { + // Generator 0.3.0 (spec 007) no longer accepts the iteration-2 G0 (20261013-g0-i2-r2, 0.2.0); + // G0 was re-run on 0.3.0 at c804757, whose generator source HEAD still matches. const dir = new URL("../../../experiments", import.meta.url).pathname; - const latest = JSON.parse( - readFileSync(join(dir, "20261013-g0-i2-r2", "manifest.json"), "utf8"), - ) as { generator_version: string }; - expect(latest.generator_version).toBe("0.2.0"); - expect(g0Accepted(dir, { requireReview: false })).toBeNull(); - expect(g0Accepted(dir)).toBeNull(); + const want = { id: "20261017-g0-i3", commit: "c804757853ce2a82e6aeef0e226a499726049f48" }; + expect(g0Accepted(dir, { requireReview: false })).toEqual(want); + const verdict = JSON.parse(readFileSync(join(dir, want.id, "verdict.json"), "utf8")) as { + decided_by: string; + }; + const reviewed = + existsSync(join(dir, want.id, "review.md")) && + /^agent review panel, rounds? \d+(-\d+)? \(no rejections/.test(verdict.decided_by); + expect(g0Accepted(dir)).toEqual(reviewed ? want : null); }); it("returns the accepted experiment's id and commit", () => { From 5989b3edfdf080e8867e5b6987570597837c2acd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 09:16:28 +0000 Subject: [PATCH 12/21] G0 panel round 1 fixes: kinematic and second-arc checks, decision binding, validation rule - G0 independent-geometry: hand-computed second turn in both directions (mid-arc and leg 3); ownship kinematics on every scenario (step <= v dt, chord heading between the stored courses, turn <= omega dt per step, |turn| <= 180 deg); unchecked turns counted; catches the red team's wrong-way second arc and leg-3-on-leg-2 mutations (tests added) - ChangeDecision gains plan and test_seed (schema 0.9.0); from iteration 3 tma-g2 scores a test split only with the decision that names its plan - Spec 007 FR-006 rewritten: paired comparison with 2c on the same seed-204 validation rows, plus a possible-pass condition; FR-009; how the gate reads under three legs; realised window distribution - Research R4 and the generator note: turn direction and the at-turn-start caveat corrected; across-LOS unit test runs on noisy data Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- docs/SRD.md | 2 +- packages/eval/src/g0-checks.ts | 128 ++++++++++++++++++- packages/eval/test/g0-checks.test.ts | 49 ++++++- packages/eval/test/golden/verdict.json | 2 +- packages/models/src/g2.ts | 34 +++++ packages/models/test/g2.test.ts | 43 ++++++- packages/sim/test/unit/second-zig.test.ts | 4 +- packages/types/src/generated/tma.schema.json | 14 ++ packages/types/src/generated/zod.ts | 4 + schema/tma.yaml | 10 ++ site/docs/algorithms/generator.md | 13 +- site/docs/glossary.md | 6 +- specs/007-iteration-3-second-zig/research.md | 20 ++- specs/007-iteration-3-second-zig/spec.md | 64 ++++++++-- 14 files changed, 362 insertions(+), 31 deletions(-) diff --git a/docs/SRD.md b/docs/SRD.md index 2a274418..d4ff9273 100644 --- a/docs/SRD.md +++ b/docs/SRD.md @@ -49,7 +49,7 @@ The generator must produce scenarios fast enough that data volume is never the b | Window length | 10–40 min, sampled per scenario; 1 bearing / 10 s (60–240 samples) | Variable length from the outset; inputs padded to 240 and masked | | Ownship speed | 5–15 kt | Constant within a leg | | Ownship zig | Course change 30–90°, at 40–60% of window | Turn modelled as instantaneous in iteration 1, then rate-limited | -| Ownship zig, from iteration 3 | First zig at 40–60% of window, then a second zig 3–5 min after leg 2 starts; each turn crosses the line of sight, putting the target 30–90° on the other bow (shorter turn; median 143°) | Generator 0.3.0, Annex A lever B.2.1, spec 007; same turn rate (0.5–3°/s) for both turns; gate times still count from the end of the first turn | +| Ownship zig, from iteration 3 | First zig at 40–60% of window, then a second zig 3–5 min after leg 2 starts; each turn crosses the line of sight, putting the target 30–90° on the other bow (shorter turn; median 143°) | Generator 0.3.0, Annex A lever B.2.1, spec 007; same turn rate (0.5–3°/s) for both turns; gate times still count from the end of the first turn, so zig + 5 min falls mostly during the second turn; redraws of second turns that do not fit skew windows long (p10 16.7 min) | | Target initial range | 2–20 km | Log-uniform so short ranges aren't under-represented | | Target initial bearing | Uniform 0–360° | Relative to ownship at window start | | Target speed | 3–20 kt | Constant | diff --git a/packages/eval/src/g0-checks.ts b/packages/eval/src/g0-checks.ts index 8bd85372..ee747b0f 100644 --- a/packages/eval/src/g0-checks.ts +++ b/packages/eval/src/g0-checks.ts @@ -163,6 +163,98 @@ export function inTurnBearings(): { name: string; got: number; want: number }[] return out; } +/** + * Second-turn known answers (generator 0.3.0, spec 007; G0 red team round 1): as `inTurnBearings`' + * first turn (+60° at 1°/s from t = 300 s, so leg 2 starts at t = 360 s from + * p₁ = (r(1 − cos 60°), 3000 + r sin 60°) on course 60°), then 100 s of leg 2 to + * p₂ = p₁ + 1000 (sin 60°, cos 60°) at t = 460 s and a second ±60° turn at 1°/s: + * - mid-arc (t = 490 s, sample 49): heading 60° ± 30°, p = p₂ + (±r)(cos 60° − cos c, sin c − sin 60°); + * - leg 3 (t = 560 s, sample 56): the arc ended at t = 520 s on course 60° ± 60°, then 40 s straight. + * Both directions, so a second arc flown the wrong way, or leg 3 left on the leg-2 track, fails. + */ +export function secondTurnBearings(): { name: string; got: number; want: number }[] { + const omega = Math.PI / 180; + const r = 10 / omega; + const c1 = Math.PI / 3; + const p1x = r * (1 - Math.cos(c1)); + const p1y = 3000 + r * Math.sin(c1); + const p2x = p1x + 1000 * Math.sin(c1); + const p2y = p1y + 1000 * Math.cos(c1); + const out: { name: string; got: number; want: number }[] = []; + for (const side of [1, -1]) { + const p: ScenarioParameters = { + ...KNOWN, + zig_angle_rad: degToRad(60), + target_bearing0_rad: Math.PI / 2, + target_range0_m: 5000, + ownship_turn_rate_rad_s: omega, + zig2_delay_s: 100, + zig2_angle_rad: side * degToRad(60), + }; + const obs = observe(createRng(1n), p, simulateTruth(p)); + const label = side > 0 ? "second turn to starboard" : "second turn to port"; + const c = c1 + side * (Math.PI / 6); + const midX = p2x + side * r * (Math.cos(c1) - Math.cos(c)); + const midY = p2y + side * r * (Math.sin(c) - Math.sin(c1)); + out.push({ + name: `${label}, mid-arc`, + got: obs[49]?.bearing_rad ?? NaN, + want: Math.atan2(5000 - midX, 0 - midY), + }); + const c2 = c1 + side * (Math.PI / 3); + const endX = p2x + side * r * (Math.cos(c1) - Math.cos(c2)); + const endY = p2y + side * r * (Math.sin(c2) - Math.sin(c1)); + const legX = endX + 10 * 40 * Math.sin(c2); + const legY = endY + 10 * 40 * Math.cos(c2); + out.push({ + name: `${label}, leg 3`, + got: obs[56]?.bearing_rad ?? NaN, + want: Math.atan2(5000 - legX, 0 - legY), + }); + } + return out; +} + +/** Wrapped angle difference in (−π, π]. */ +const wrapPi = (a: number): number => Math.atan2(Math.sin(a), Math.cos(a)); + +/** + * Kinematic consistency of one scenario's ownship track (G0 red team round 1): each step is at + * most v·dt; the chord's heading lies between the stored courses at the step's ends (equal to + * them on a leg); and the course changes by at most ω·dt per step (rate-limited turns). + * The stored course is a model input, so a course that disagrees with the positions fails here + * even when the solver, which uses positions only, is unaffected. Returns the first failure. + */ +export function kinematicFailure(s: Scenario): string | null { + const p = s.metadata.parameters; + const v = p.ownship_speed_mps; + const dt = p.dt_s; + const rate = p.ownship_turn_rate_rad_s ?? 0; + const own = s.ownship.states; + for (let i = 1; i < own.length; i++) { + const a = own[i - 1]; + const b = own[i]; + if (a === undefined || b === undefined) return `sample ${String(i)} missing`; + const dx = b.x_m - a.x_m; + const dy = b.y_m - a.y_m; + const step = Math.hypot(dx, dy); + if (step > v * dt + 0.05) return `sample ${String(i)}: step ${step.toFixed(3)} m > v·dt`; + const dc = wrapPi(b.course_rad - a.course_rad); + if (rate > 0 && Math.abs(dc) > rate * dt + 1e-5) + return `sample ${String(i)}: course changed ${dc.toFixed(5)} rad > ω·dt`; + if (rate > 0 && step > 1) { + // On a constant-speed path that turns one way during the step, the chord's heading lies + // between the headings at its ends (equal to them on a straight leg). + const off = wrapPi(Math.atan2(dx, dy) - a.course_rad); + const lo = Math.min(0, dc) - 2e-4; + const hi = Math.max(0, dc) + 2e-4; + if (off < lo || off > hi) + return `sample ${String(i)}: chord heading ${off.toExponential(2)} rad off the course, outside [0, ${dc.toExponential(2)}]`; + } + } + return null; +} + /** * Solver-independent geometry: hand-computed bearings (Math.atan2 with literal coordinates, not * the shared bearingOf) and, for every truth scenario, the t = 0 bearing and range against the @@ -214,6 +306,12 @@ export function checkIndependentGeometry( })[50]; if (!(Math.abs(angleDiff(leg3 ?? NaN, Math.atan2(5000, -4000))) < 1e-6)) fails.push(`leg-3 case gave ${String(leg3)}`); + for (const c of secondTurnBearings()) { + if (!(Math.abs(angleDiff(c.got, c.want)) < 1e-6)) + fails.push(`${c.name} gave ${String(c.got)}, want ${String(c.want)}`); + } + let skipped = 0; + let kinematic = 0; // Across the line of sight (zig_rule across_los): every turn of every scenario puts the target // on the other bow at an offset inside the drawn range, recomputed with literal atan2 from the // stored positions at the last sample before the turn and the course after it. @@ -231,6 +329,19 @@ export function checkIndependentGeometry( fails.push(`scenario ${String(s.metadata.index)}: empty`); continue; } + const pz = s.metadata.parameters; + if ( + Math.abs(pz.zig_angle_rad) > Math.PI + 1e-6 || + Math.abs(pz.zig2_angle_rad ?? 0) > Math.PI + 1e-6 + ) { + if (fails.length < 5) + fails.push(`scenario ${String(s.metadata.index)}: turn larger than 180°`); + } + const kin = kinematicFailure(s); + if (kin !== null) { + kinematic++; + if (fails.length < 5) fails.push(`scenario ${String(s.metadata.index)}: ${kin}`); + } if (los != null) { const m = s.metadata; const pairs: [number, number][] = [ @@ -242,7 +353,10 @@ export function checkIndependentGeometry( const ok = s.ownship.states[k]; const tk = s.target.states[k]; const oa = s.ownship.states[after]; - if (ok === undefined || tk === undefined || oa === undefined) continue; + if (ok === undefined || tk === undefined || oa === undefined) { + skipped++; + continue; + } turns++; const brg = Math.atan2(tk.x_m - ok.x_m, tk.y_m - ok.y_m); // An instantaneous zig changes course at sample k itself: the old course is at k − 1. @@ -274,12 +388,16 @@ export function checkIndependentGeometry( } return check( "independent-geometry", - fails.length === 0, - fails.length === 0 - ? `hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2) and leg-3 cases match; t = 0 bearing and range match the drawn parameters on ${String(n)} scenarios` + + fails.length === 0 && skipped === 0, + fails.length === 0 && skipped === 0 + ? `hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2), leg-3 and second-turn (both directions, mid-arc and leg 3) cases match; t = 0 bearing and range match the drawn parameters, and ownship kinematics (step ≤ v·dt, chord heading between the stored courses, turn ≤ ω·dt per step, |turn| ≤ 180°) hold, on ${String(n)} scenarios` + (los != null ? `; ${String(turns)} turns put the target on the other bow inside the drawn offset range` : "") - : fails.slice(0, 6).join("; "), + : [ + ...fails.slice(0, 6), + ...(skipped > 0 ? [`${String(skipped)} turns could not be checked`] : []), + ...(kinematic > 0 ? [`${String(kinematic)} scenarios fail kinematics`] : []), + ].join("; "), ); } diff --git a/packages/eval/test/g0-checks.test.ts b/packages/eval/test/g0-checks.test.ts index 46312b83..8d961f6a 100644 --- a/packages/eval/test/g0-checks.test.ts +++ b/packages/eval/test/g0-checks.test.ts @@ -1,11 +1,13 @@ // G0 independent geometry with the rate-limited turn (spec 003 US6, T016). -import { DEFAULT_RANGES, degToRad, noiseFree } from "@tma/sim"; +import { DEFAULT_RANGES, degToRad, noiseFree, simulateTruth } from "@tma/sim"; import type { ParameterRanges } from "@tma/types"; import { describe, expect, it } from "vitest"; import { checkBearingResiduals, checkIndependentGeometry, inTurnBearings, + kinematicFailure, + secondTurnBearings, } from "../src/g0-checks.js"; import { scenarios } from "./helpers.js"; @@ -69,3 +71,48 @@ describe("G0 independent geometry: generator 0.3.0 (spec 007)", () => { expect(c.detail).toMatch(/not across the line of sight/); }); }); + +describe("G0 kinematic consistency (red team round 1 mutations)", () => { + const [base] = scenarios(9n, 1, ACROSS); + if (base === undefined) throw new Error("no scenario"); + const z2 = base.metadata.zig2_index ?? NaN; + const t2 = base.metadata.turn2_start_index ?? NaN; + + it("passes the generated three-leg scenario and the hand-computed second turns", () => { + expect(kinematicFailure(base)).toBeNull(); + for (const c of secondTurnBearings()) + expect(Math.abs(c.got - c.want), c.name).toBeLessThan(1e-6); + }); + + it("catches leg 3 left on the leg-2 track while its course is written (M5b)", () => { + const s = structuredClone(base); + const own = s.ownship.states; + const a = own[t2 - 2]; + const b = own[t2 - 1]; + if (a === undefined || b === undefined) throw new Error("index"); + for (let i = t2; i < own.length; i++) { + const o = own[i]; + if (o === undefined) continue; + const k = i - (t2 - 1); + o.x_m = Math.fround(b.x_m + k * (b.x_m - a.x_m)); + o.y_m = Math.fround(b.y_m + k * (b.y_m - a.y_m)); + } + expect(kinematicFailure(s)).toMatch(/chord heading/); + }); + + it("catches the second arc flown the wrong way with the courses unchanged (M1b)", () => { + const p = { + ...base.metadata.parameters, + zig2_angle_rad: -(base.metadata.parameters.zig2_angle_rad ?? 0), + }; + const wrong = simulateTruth(p); + const s = structuredClone(base); + for (let i = t2; i < z2 + 3 && i < s.ownship.states.length; i++) { + const o = s.ownship.states[i]; + if (o === undefined) continue; + o.x_m = Math.fround(wrong.ownX[i] ?? 0); + o.y_m = Math.fround(wrong.ownY[i] ?? 0); + } + expect(kinematicFailure(s)).not.toBeNull(); + }); +}); diff --git a/packages/eval/test/golden/verdict.json b/packages/eval/test/golden/verdict.json index 43e80796..2be8715b 100644 --- a/packages/eval/test/golden/verdict.json +++ b/packages/eval/test/golden/verdict.json @@ -16,7 +16,7 @@ "result": "pass" }, { - "detail": "hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2) and leg-3 cases match; t = 0 bearing and range match the drawn parameters on 4 scenarios", + "detail": "hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2), leg-3 and second-turn (both directions, mid-arc and leg 3) cases match; t = 0 bearing and range match the drawn parameters, and ownship kinematics (step ≤ v·dt, chord heading between the stored courses, turn ≤ ω·dt per step, |turn| ≤ 180°) hold, on 4 scenarios", "name": "independent-geometry", "result": "pass" }, diff --git a/packages/models/src/g2.ts b/packages/models/src/g2.ts index 7e57ad76..656ccd83 100644 --- a/packages/models/src/g2.ts +++ b/packages/models/src/g2.ts @@ -108,6 +108,9 @@ export function checkPlanDigests(plan: string, data: string): string[] { } } +/** A repository-relative path in one spelling: forward slashes, no leading "./". */ +const normPath = (p: string): string => p.replace(/\\/g, "/").replace(/^(\.\/)+/, ""); + /** True when the commit that first added `path` is an ancestor of (or equal to) `commit`. */ export function gitDecisionPrecedes(path: string, commit: string): boolean { try { @@ -169,7 +172,18 @@ export function gitCommitted(path: string): boolean { export function requireDecision(opts: G2RunOptions): ChangeDecision | undefined { if (opts.split !== "test") return undefined; const phaseB = opts.compareModels !== undefined || existsSync(opts.phaseBDir ?? PHASE_B_CONFIGS); + // From iteration 3 (spec 007, G0 red team): a test split is scored only with the decision that + // names its plan, so an earlier iteration's decision cannot unlock a later iteration's test. + const iteration = /(?:^|\/)configs\/iter(\d+)\//.exec(normPath(opts.plan ?? "")); + const bound = iteration !== null && Number(iteration[1]) >= 3; if (opts.decision === undefined) { + if (bound) { + throw new RecordError( + "--decision", + "spec 007", + `scoring the test split of ${String(opts.plan)} needs --decision `, + ); + } if (phaseB) { throw new RecordError( "--decision", @@ -185,6 +199,26 @@ export function requireDecision(opts: G2RunOptions): ChangeDecision | undefined opts.decision, "decision", ); + if (bound && (d.plan == null || normPath(d.plan) !== normPath(opts.plan ?? ""))) { + throw new RecordError( + opts.decision, + "plan", + `the decision pre-registers ${d.plan ?? "no plan"}, not ${String(opts.plan)}`, + ); + } + if (d.test_seed != null && opts.plan !== undefined) { + const p = JSON.parse(readFileSync(opts.plan, "utf8")) as { + dataset?: { seed?: string; fresh_test?: { seed?: string } }; + }; + const seed = p.dataset?.fresh_test?.seed ?? p.dataset?.seed; + if (seed !== d.test_seed) { + throw new RecordError( + opts.decision, + "test_seed", + `the decision commits to test seed ${d.test_seed}, but ${opts.plan} tests seed ${String(seed)}`, + ); + } + } if (!(opts.isCommitted ?? gitCommitted)(opts.decision)) { throw new RecordError( opts.decision, diff --git a/packages/models/test/g2.test.ts b/packages/models/test/g2.test.ts index 8e538fcc..0cf624e4 100644 --- a/packages/models/test/g2.test.ts +++ b/packages/models/test/g2.test.ts @@ -1,6 +1,6 @@ // T038: tma-g2 one-command run, tiny. Validation reads only train + validation and writes a // diagnostic (not a verdict); test needs a committed decision once a phase B exists (SC-005). -import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { fileSha256, generateDataset, readDataset } from "@tma/data"; import { computeNormalisation, writeFeatures } from "@tma/features"; @@ -18,6 +18,7 @@ import { DIAGNOSTIC_FILE, featuresPath, type G2RunOptions, + requireDecision, requireSameRows, runG2, } from "../src/g2.js"; @@ -304,3 +305,43 @@ describe("tma-g2 end to end (tiny)", () => { ); }); }); + +describe("decisions bind to their plan from iteration 3 (spec 007, G0 red team)", () => { + const plan = (dir: string, seed: string): string => { + const p = join(dir, "configs/iter3/a/plan.json"); + mkdirSync(join(dir, "configs/iter3/a"), { recursive: true }); + writeFileSync(p, JSON.stringify({ dataset: { seed } })); + return p; + }; + const opts = (over: Partial): G2RunOptions => ({ + data: "d", + models: "m", + experiment: "e", + split: "test", + commandLine: "test", + phaseBDir: join(tempDir(), "absent"), + isCommitted: () => true, + ...over, + }); + + it("an iteration-3 test needs a decision, and only the one that names its plan and seed", () => { + const dir = tempDir(); + const p = plan(dir, "204"); + expect(() => requireDecision(opts({ plan: p }))).toThrow(/needs --decision/); + // An earlier iteration's decision (no plan) cannot unlock it. + const old = join(dir, "old.json"); + writeFileSync(old, JSON.stringify(DECISION)); + expect(() => requireDecision(opts({ plan: p, decision: old }))).toThrow( + /pre-registers no plan/, + ); + const d = join(dir, "decision.json"); + writeFileSync(d, JSON.stringify({ ...DECISION, plan: "configs/iter3/b/plan.json" })); + expect(() => requireDecision(opts({ plan: p, decision: d }))).toThrow(/pre-registers/); + writeFileSync(d, JSON.stringify({ ...DECISION, plan: p, test_seed: "205" })); + expect(() => requireDecision(opts({ plan: p, decision: d }))).toThrow(/test seed 205/); + writeFileSync(d, JSON.stringify({ ...DECISION, plan: p, test_seed: "204" })); + expect(requireDecision(opts({ plan: p, decision: d }))?.test_seed).toBe("204"); + // Validation needs nothing. + expect(requireDecision(opts({ plan: p, split: "validation" }))).toBeUndefined(); + }); +}); diff --git a/packages/sim/test/unit/second-zig.test.ts b/packages/sim/test/unit/second-zig.test.ts index 52a5030f..bd9593bd 100644 --- a/packages/sim/test/unit/second-zig.test.ts +++ b/packages/sim/test/unit/second-zig.test.ts @@ -105,9 +105,11 @@ describe("second zig: known answers", () => { }); }); +// Noisy (σ = 2°), so a rule planned from the observed bearing rather than the true one fails the +// offset bounds below (G0 red team round 1, mutation M4: G0's noise-free data cannot see it). const ITER3: ParameterRanges = { ...DEFAULT_RANGES, - noise_sigma_rad_values: [0], + noise_sigma_rad_values: [degToRad(2)], ownship_turn_rate_rad_s: { min: degToRad(0.5), max: degToRad(3) }, zig_rule: "across_los", los_offset_rad: { min: degToRad(30), max: degToRad(90) }, diff --git a/packages/types/src/generated/tma.schema.json b/packages/types/src/generated/tma.schema.json index 1b26c3e6..78eaf80b 100644 --- a/packages/types/src/generated/tma.schema.json +++ b/packages/types/src/generated/tma.schema.json @@ -20,6 +20,13 @@ "phase_a_experiment": { "type": "string" }, + "plan": { + "description": "The plan this decision pre-registers (configs/<…>/plan.json). Required from iteration 3 (spec 007): tma-g2 scores a test split only with the decision that names its plan.", + "type": [ + "string", + "null" + ] + }, "rationale": { "description": "Validation evidence cited.", "type": "string" @@ -27,6 +34,13 @@ "stalled": { "description": "Any gated criterion failed or was inconclusive on validation.", "type": "boolean" + }, + "test_seed": { + "description": "Dataset seed of the test split the decision commits to (the plan's fresh_test seed, or its dataset seed); tma-g2 checks it against the plan.", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/packages/types/src/generated/zod.ts b/packages/types/src/generated/zod.ts index daa98ccb..34116089 100644 --- a/packages/types/src/generated/zod.ts +++ b/packages/types/src/generated/zod.ts @@ -9,10 +9,14 @@ export const ChangeDecision = z.strictObject({ decided_at: z.string(), git_commit: z.string(), phase_a_experiment: z.string(), + /** The plan this decision pre-registers (configs/<…>/plan.json). Required from iteration 3 (spec 007): tma-g2 scores a test split only with the decision that names its plan. */ + plan: z.string().nullish(), /** Validation evidence cited. */ rationale: z.string(), /** Any gated criterion failed or was inconclusive on validation. */ stalled: z.boolean(), + /** Dataset seed of the test split the decision commits to (the plan's fresh_test seed, or its dataset seed); tma-g2 checks it against the plan. */ + test_seed: z.string().nullish(), }); export type ChangeDecision = z.infer; diff --git a/schema/tma.yaml b/schema/tma.yaml index 3e1a7784..f1f5a185 100644 --- a/schema/tma.yaml +++ b/schema/tma.yaml @@ -736,3 +736,13 @@ classes: carried_set_rule: { range: string, required: true } decided_at: { range: string, required: true } git_commit: { range: string, required: true } + plan: + range: string + description: >- + The plan this decision pre-registers (configs/<…>/plan.json). Required from iteration 3 + (spec 007): tma-g2 scores a test split only with the decision that names its plan. + test_seed: + range: string + description: >- + Dataset seed of the test split the decision commits to (the plan's fresh_test seed, or + its dataset seed); tma-g2 checks it against the plan. diff --git a/site/docs/algorithms/generator.md b/site/docs/algorithms/generator.md index a0b4bed6..9ff2cbc2 100644 --- a/site/docs/algorithms/generator.md +++ b/site/docs/algorithms/generator.md @@ -79,10 +79,15 @@ turn. The relative bearing is `ρ = b − c`, wrapped to (−180°, 180°]. - **Turn direction.** The turn takes the shorter way, so `zig = wrap(b + sign(ρ) φ − c)` and `|zig| ≤ 180°`. -A target ahead is crossed by turning towards it; a target on the quarter by turning away, across -the stern line of sight. Both reverse ownship's velocity component across the line of sight, -which is what makes the bearings informative (spec 007 research R4). The realised turn is stored -as `zig_angle_rad`, so the tracks depend only on the parameters. +- **Through the bow line of sight.** When `|ρ| + φ < 180°` the turn is towards the target. +- **Through the stern line of sight.** Otherwise the shorter turn is away from the target. + +Either way, ownship's velocity component across the line of sight reverses, which is what makes +the bearings informative (spec 007 research R4). The rule holds at the start of the turn. During +a long turn at short range the bearing keeps drawing, so on the new leg the angle off can fall +outside the drawn range, and in rare cases the target returns to the original bow. Turns near +180° can go either way for a tiny change in geometry. The realised turn is stored as +`zig_angle_rad`, so the tracks depend only on the parameters. Worked check: heading north with the target 40° on the starboard bow and `φ` = 60°, the turn is 100° to starboard. With the target on the starboard quarter (150°) it is 150° to port. diff --git a/site/docs/glossary.md b/site/docs/glossary.md index 832e4efb..aa939ba3 100644 --- a/site/docs/glossary.md +++ b/site/docs/glossary.md @@ -5,9 +5,9 @@ title: Glossary TMA and ML terms as used in this project, alphabetical. Algorithm details are linked where they exist. -**Across the line of sight** — A zig that puts the target on ownship's other bow. It reverses -ownship's velocity component across the line of sight, the change bearings respond to most. It -is iteration 3's rule for both turns (`zig_rule: across_los`; see +**Across the line of sight** — A zig that puts the target on ownship's other bow, judged from the +bearing at the start of the turn. It reverses ownship's velocity component across the line of +sight, the change bearings respond to most. It is iteration 3's rule for both turns (`zig_rule: across_los`; see [generator](algorithms/generator.md)). **Bearing** — True direction from ownship to the target, clockwise from north, in radians in diff --git a/specs/007-iteration-3-second-zig/research.md b/specs/007-iteration-3-second-zig/research.md index a9a3acb4..9f60ecfd 100644 --- a/specs/007-iteration-3-second-zig/research.md +++ b/specs/007-iteration-3-second-zig/research.md @@ -107,10 +107,22 @@ The rule is implemented as follows: - **New course.** The new course is the bearing plus the side of the target times the offset, by the shorter turn. -- **Target ahead.** It turns towards the target and through the line of sight. -- **Target on the quarter.** It turns away, through the stern line of sight. - -Both cases reverse the across-line-of-sight component. +- **Turn direction.** When the relative bearing plus the offset is under 180°, it turns towards + the target, through the bow line of sight. Otherwise the shorter turn is away from the target, + through the stern line of sight. In the G0 data, every target ahead is crossed towards. Of the + targets abaft the beam, 5,793 turns go away and 3,398 towards (G0 panel, geometry). + +Both directions reverse the across-line-of-sight component at the start of the turn. + +**Caveats** (G0 panel round 1): + +- **Defined at the start of the turn.** The rule fixes the course from the bearing at the turn's + start. The bearing keeps drawing during the turn, by median 1.5° and p99 23°. At the first + sample of the new leg, 5.9% of turns have the angle off outside 30–90°. In 26 of 20,000 turns + the target is back on the original bow, all at ranges under about 3 km. +- **Near-reversals.** Turns near 180° (1% of turns over 179°) are near-ambiguous in direction. + A tiny change in geometry flips the arc side and moves the track by up to 2v/ω, about 1.8 km. + The rule is deterministic, so this is a property of the problem, not an error. The rule uses the true bearing, not the observed one: at σ ≤ 2° the difference is small, and it keeps the parameters a pure function of the draws. diff --git a/specs/007-iteration-3-second-zig/spec.md b/specs/007-iteration-3-second-zig/spec.md index a26384f7..617d8576 100644 --- a/specs/007-iteration-3-second-zig/spec.md +++ b/specs/007-iteration-3-second-zig/spec.md @@ -89,18 +89,62 @@ seed-204 test split, with the 2c models beside it as the other phase. Then the a - **FR-004**: Generator 0.3.0 reads 0.1.0 and 0.2.0 datasets. `tma-train` requires a G0 on 0.3.0. - **FR-005**: The decision record MUST be committed before any iteration-3 weights are trained. `tma-g2` checks ancestry. -- **FR-006 (validation rule, B.3.3)**: The carried pair goes to the test split only if: - - the instantaneous model's validation speed (σ = 1°, zig + 600 s) is below 1.36 kt, which is - 2c's validation 1.42 kt less its interval half-width; - - or the time-late model's validation speed is below 1.45 kt, which is 2c's 1.52 kt less its - half-width. - - Otherwise the iteration stops at validation and is written up. +- **FR-006 (validation rule, B.3.3; amended after the G0 panel's statistics finding).** The rule + compares models **on the same rows of the new problem**. Comparing against 2c's iteration-2 + validation figures would credit the easier problem to the lever. The steps: + 1. Score both sets of models on seed 204's validation split, σ = 1°, zig + 600 s, rows common to + both: the iteration-3 models, and the 2c models, whose other-phase predictions + `tma-g2 --compare-models` already writes. + 2. **Primary condition (instantaneous model).** Take the paired bootstrap (eval's + `bootstrapMedianRatio`: 2,000 resamples, seed 20261011) of median speed error, iteration-3 + LSTM / 2c LSTM. Its 95% interval must lie wholly below 1. + 3. **Second condition.** The iteration-3 LSTM's G2 speed check on validation must not be a + fail: its interval's lower bound is under 1 kt. + + The time-late model is reported the same way but is not a condition. The carried pair goes to + the test split only if both conditions hold. Otherwise the iteration stops at validation and is + written up. The model / classical speed ratio on the same rows is reported beside, against 2c's + 2.37 on its own validation. - **FR-007**: The carried set is the iteration-3 LSTM and BiLSTM, unconditionally. The 2c models are scored beside them on the same split, reported and never gated. - **FR-008**: Thresholds, slices, bootstrap and the gate rule are spec 003's as amended by the G2 panels. Classical and naive are re-baselined on the new data, and the improvement is reported - rather than subtracted (B.2.1). + rather than subtracted (B.2.1). Zig bands run to 180°. Every slice is reported with its n, and + slices under 100 rows are marked indicative. Bands are not compared across iterations: a 30–90° + across-LOS turn is not the same population as iteration 2's. +- **FR-009 (decision binding; G0 red team).** From iteration 3, `tma-g2 --split test` requires a + committed decision whose `plan` names the plan being scored and whose `test_seed` matches it. An + earlier iteration's decision cannot unlock seed 204's test split. + +## Reading the gate under three legs (G0 panel, statistics and geometry) + +Gate times still count from the end of the **first** turn. On the G0 test split: + +- **The second turn.** It starts 180–300 s after the zig (median 240 s) and lasts 20–350 s + (median 70 s). +- **At zig + 300 s.** 4% of scenarios are still on leg 2, 59% are mid-way through the second + turn and 37% are on leg 3. Range and course are judged mostly **during** the second manoeuvre. +- **At zig + 600 s.** 99.9% are on leg 3, with a median of 280 s of it (p10 190 s, p90 340 s). + Speed is judged after 4–5 minutes of leg 3. + +So zig + 300 s is not like-for-like with iterations 1–2c, and the report says so. The +re-baselined classical figures, on the same rows, sit beside every model figure. + +**Realised distribution.** Redraws (second turns that do not complete) remove short windows, so +"10–40 min" is no longer uniform: + +| Statistic | Value | +| --- | --- | +| redraws per scenario | 0.21 | +| window p10 | 16.7 min (first draws: 13.0 min) | +| window median | 27.7 min (first draws: 25.2 min) | +| scenarios with 10–15 min windows | 5.6% (16.7% if uniform) | +| mean turn rate | 1.83°/s (first draws: 1.75°/s) | +| reaching zig + 300 s | 98.9% | +| reaching zig + 600 s | 65.4% | + +The drawn delay is rounded to whole samples, so 180 s and 300 s carry half the weight of the +interior values. ## Success criteria @@ -114,7 +158,7 @@ seed-204 test split, with the 2c models beside it as the other phase. Then the a - **Large turns.** With an offset of 30–90° on the other bow, the median first turn is 143° (p90 173°), against 61° in iteration 2. The owner checks this in the viewer (US1). A cap on turn size would be a further change to the problem. -- **Longer windows.** Redraws for second turns that do not fit remove short windows: p10 window - 16.5 min, against 12.7. The share reaching zig + 600 s rises from 61% to 66%. +- **Longer windows.** Redraws for second turns that do not fit remove short windows (see "Reading + the gate under three legs"). - **An easier problem.** Every estimator gains, so a G2 pass is a pass on the three-leg problem, and is reported as such. From 04c18447540641019a384eebff90e06df2011909 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 09:24:58 +0000 Subject: [PATCH 13/21] G0 round 2 at 5989b3e (checks after panel round 1); tma-g2 writes paired.json - experiments/20261017-g0-i3-r2: machine pass, kinematic and second-turn checks included, zig bands to 180 deg - tma-g2 --compare-models writes paired.json: per model, carried / other phase median-error ratio with its paired bootstrap interval on the same sigma = 1 deg rows (range and course at zig + 300 s, speed at zig + 600 s), the input to spec 007 FR-006's validation rule - G0 guard test follows the round-2 G0 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- experiments/20261017-g0-i3-r2/manifest.json | 163 ++++++ experiments/20261017-g0-i3-r2/metrics.csv | 31 ++ experiments/20261017-g0-i3-r2/metrics.json | 542 ++++++++++++++++++++ experiments/20261017-g0-i3-r2/seeds.txt | 2 + experiments/20261017-g0-i3-r2/verdict.json | 32 ++ packages/eval/src/bootstrap.ts | 17 +- packages/models/src/g2.ts | 72 +++ packages/models/test/g0-guard.test.ts | 7 +- packages/models/test/g2.test.ts | 10 + 9 files changed, 869 insertions(+), 7 deletions(-) create mode 100644 experiments/20261017-g0-i3-r2/manifest.json create mode 100644 experiments/20261017-g0-i3-r2/metrics.csv create mode 100644 experiments/20261017-g0-i3-r2/metrics.json create mode 100644 experiments/20261017-g0-i3-r2/seeds.txt create mode 100644 experiments/20261017-g0-i3-r2/verdict.json diff --git a/experiments/20261017-g0-i3-r2/manifest.json b/experiments/20261017-g0-i3-r2/manifest.json new file mode 100644 index 00000000..cb44140d --- /dev/null +++ b/experiments/20261017-g0-i3-r2/manifest.json @@ -0,0 +1,163 @@ +{ + "command_lines": [ + "tma-generate --seed 42 --count 100000 --noise-free --ranges sha256:bdf46080f66c24c867245c482121a2f1b82d3f13e59ef8c0cabccde08d1172be --heldout-ranges sha256:7d9e444c99d8ac6f759d0226c1724a7ea19cb34237465730f4d1aed51de1aca1", + "tma-g0 --data data/g0-i3 --experiment experiments/20261017-g0-i3-r2" + ], + "created": "2026-10-17T09:20:07.240Z", + "datasets": [ + { + "digest": "f76097e8d6d3ee83aad97ef77722c62298fc33312f595ae5a39d9e1da107c6d0", + "header": { + "created_by": "tma-generate --seed 42 --count 100000 --noise-free --ranges sha256:bdf46080f66c24c867245c482121a2f1b82d3f13e59ef8c0cabccde08d1172be --heldout-ranges sha256:7d9e444c99d8ac6f759d0226c1724a7ea19cb34237465730f4d1aed51de1aca1", + "dataset_seed": "42", + "generator_git_commit": "90f4873b54cce6af72efadec5f1ed4f976c0008c", + "generator_git_dirty": false, + "generator_version": "0.3.0", + "index_end": 80000, + "index_start": 0, + "parameter_ranges": { + "dropout_fraction": { + "max": 0, + "min": 0 + }, + "dt_s": 10, + "los_offset_rad": { + "max": 1.5707963267948966, + "min": 0.5235987755982988 + }, + "min_range_m": 100, + "noise_sigma_rad_values": [ + 0 + ], + "ownship_speed_mps": { + "max": 7.716666666666667, + "min": 2.5722222222222224 + }, + "ownship_turn_rate_rad_s": { + "max": 0.05235987755982989, + "min": 0.008726646259971648 + }, + "range_distribution": "log_uniform", + "target_range0_m": { + "max": 20000, + "min": 2000 + }, + "target_speed_mps": { + "max": 10.28888888888889, + "min": 1.5433333333333334 + }, + "window_s": { + "max": 2400, + "min": 600 + }, + "zig2_delay_s": { + "max": 300, + "min": 180 + }, + "zig_angle_abs_rad": { + "max": 1.5707963267948966, + "min": 0.5235987755982988 + }, + "zig_fraction": { + "max": 0.6, + "min": 0.4 + }, + "zig_rule": "across_los" + }, + "record": "header", + "redraw_count": 17247, + "scenario_count": 80000, + "schema_version": "0.9.0", + "split": "train" + }, + "path": "data/g0-i3/train.f32pack" + }, + { + "digest": "ea38d17219a941d86c95f4323ca669d9f8f264dcc807d597c2229a5ac51c6564", + "header": { + "created_by": "tma-generate --seed 42 --count 100000 --noise-free --ranges sha256:bdf46080f66c24c867245c482121a2f1b82d3f13e59ef8c0cabccde08d1172be --heldout-ranges sha256:7d9e444c99d8ac6f759d0226c1724a7ea19cb34237465730f4d1aed51de1aca1", + "dataset_seed": "42", + "generator_git_commit": "90f4873b54cce6af72efadec5f1ed4f976c0008c", + "generator_git_dirty": false, + "generator_version": "0.3.0", + "index_end": 100000, + "index_start": 90000, + "parameter_ranges": { + "dropout_fraction": { + "max": 0, + "min": 0 + }, + "dt_s": 10, + "los_offset_rad": { + "max": 1.5707963267948966, + "min": 0.5235987755982988 + }, + "min_range_m": 100, + "noise_sigma_rad_values": [ + 0 + ], + "ownship_speed_mps": { + "max": 7.716666666666667, + "min": 2.5722222222222224 + }, + "ownship_turn_rate_rad_s": { + "max": 0.05235987755982989, + "min": 0.008726646259971648 + }, + "range_distribution": "log_uniform", + "target_range0_m": { + "max": 20000, + "min": 2000 + }, + "target_speed_mps": { + "max": 10.28888888888889, + "min": 1.5433333333333334 + }, + "window_s": { + "max": 2400, + "min": 600 + }, + "zig2_delay_s": { + "max": 300, + "min": 180 + }, + "zig_angle_abs_rad": { + "max": 1.5707963267948966, + "min": 0.5235987755982988 + }, + "zig_fraction": { + "max": 0.6, + "min": 0.4 + }, + "zig_rule": "across_los" + }, + "record": "header", + "redraw_count": 2052, + "scenario_count": 10000, + "schema_version": "0.9.0", + "split": "test" + }, + "path": "data/g0-i3/test.f32pack" + } + ], + "estimators": [ + { + "name": "naive", + "settings_json": "{\"fitted_on\":\"train\",\"means\":{\"course_cos\":-0.0010852328029513894,\"course_sin\":-0.0022651612364012388,\"log_range_km\":2.27659913273481,\"n\":80000,\"speed_mps\":5.9123806832298635}}" + }, + { + "name": "classical", + "settings_json": "{\"max_iterations\":50,\"method\":\"levenberg-marquardt\",\"mode\":\"final\"}" + }, + { + "name": "classical:per_timestep", + "settings_json": "{\"grid_every\":6,\"max_iterations\":50,\"method\":\"levenberg-marquardt\",\"mode\":\"per_timestep\",\"note\":\"first scenarios of the test split, for check classical-per-timestep-recovers\",\"scenarios\":1000}" + } + ], + "generator_version": "0.3.0", + "git_commit": "5989b3edfdf080e8867e5b6987570597837c2acd", + "git_dirty": false, + "id": "20261017-g0-i3-r2", + "node_version": "v22.22.0", + "schema_version": "0.9.0" +} diff --git a/experiments/20261017-g0-i3-r2/metrics.csv b/experiments/20261017-g0-i3-r2/metrics.csv new file mode 100644 index 00000000..01546b9f --- /dev/null +++ b/experiments/20261017-g0-i3-r2/metrics.csv @@ -0,0 +1,31 @@ +estimator,slice,point,n,excluded,range_rel_median,range_rel_p90,course_deg_median,course_deg_p90,speed_kt_median,speed_kt_p90,gross_error_rate,non_converged_rate,position_m_mean,position_window_m_median,position_window_m_p90 +naive,overall,final,10000,0,0.41296159627504475,1.5327997889522416,89.45474167284688,162.58337096507148,4.243544316961235,7.647355148653232,0.3878,0,5452.883076396702,4401.405811239404,11507.173492769414 +naive,dropout_band=0-5pct,final,10000,0,0.41296159627504475,1.5327997889522416,89.45474167284688,162.58337096507148,4.243544316961235,7.647355148653232,0.3878,0,5452.883076396702,4401.405811239404,11507.173492769414 +naive,range_band=0-2km,final,133,0,3.554155199769074,10.189099437703875,90.76955308807433,167.12415266965337,4.888747783767997,7.68978966492556,1,0,7541.811836895824,7604.0155201139005,8872.685837083234 +naive,range_band=10-20km,final,3634,0,0.4051582349171333,0.5659043446436742,89.08235975929779,161.3485137442682,4.333764790973704,7.650062383894786,0.25949367088607594,0,6745.622183900448,6441.378841890848,12363.699782051417 +naive,range_band=2-5km,final,1863,0,1.090394363945709,3.7852475362152567,91.6602780577357,163.413635327571,4.118415500896281,7.688682949568745,0.775630703166935,0,4947.390624738301,5082.411505237025,7707.336892376554 +naive,range_band=20-30km,final,515,0,0.634768967095896,0.7013337367422446,87.71670441588876,163.07976179319132,4.6361965443094135,7.529825268244897,0.9825242718446602,0,17343.277986636796,16934.112145062863,22879.839699095115 +naive,range_band=30+km,final,9,0,0.7485700238919062,0.7642858167300461,106.55888773201448,157.09385230648385,3.79518092837241,8.285669219674096,1,0,28713.815931989186,29008.802337317957,31592.53709787277 +naive,range_band=5-10km,final,3846,0,0.24299138820601726,1.0084669091137104,89.37135872345937,162.82039583211494,4.159294451802132,7.6459358371874675,0.218928757150286,0,2757.4044547509648,2380.8681528827433,5669.739080810058 +naive,sigma=0deg,final,10000,0,0.41296159627504475,1.5327997889522416,89.45474167284688,162.58337096507148,4.243544316961235,7.647355148653232,0.3878,0,5452.883076396702,4401.405811239404,11507.173492769414 +naive,zig_band=120-150deg,final,3350,0,0.41814549189676226,1.4205594803025299,91.70037126816366,162.25843837568647,4.284804222650713,7.6459358371874675,0.38985074626865673,0,5616.710800217959,4563.653024001928,12066.507695840213 +naive,zig_band=150-180deg,final,4019,0,0.4175174029873284,1.5933215071523037,89.34677004244732,163.1154153806588,4.2366588604887925,7.675463077059059,0.39462552873849216,0,5637.500113282189,4494.309434812314,11926.065519513926 +naive,zig_band=30-50deg,final,84,0,0.4614850895772321,1.6337174892005477,82.8358375746193,162.83246477637834,3.8864580652883705,7.198060923467186,0.4642857142857143,0,4648.201741165161,4436.822024701133,8943.548089216078 +naive,zig_band=50-70deg,final,319,0,0.39053573219917426,1.700969404255579,93.63977616223211,164.3109514297814,4.118990177212214,7.61714084617005,0.3667711598746082,0,4489.542019678022,3594.839080360501,8942.730785294072 +naive,zig_band=70-90deg,final,501,0,0.37249923148578495,1.6897176776549168,88.19526845027487,165.7484057217456,4.185012607286095,7.589079494352731,0.3333333333333333,0,4447.1342740142945,3821.0031499270726,8588.865422100125 +naive,zig_band=90-120deg,final,1727,0,0.4050221004673685,1.5631300243029322,86.56928727683997,160.2920542343965,4.236357155422927,7.6209944216273,0.38390272148233934,0,5214.307762674768,4283.707052404278,10806.868545155921 +classical,overall,final,10000,0,9.625246868512277e-8,4.812320236817345e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0027786471935062848,0.0009841623227903502,0.006905339660024878 +classical,dropout_band=0-5pct,final,10000,0,9.625246868512277e-8,4.812320236817345e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0027786471935062848,0.0009841623227903502,0.006905339660024878 +classical,range_band=0-2km,final,133,0,4.794212208460275e-8,1.6569781425015622e-7,0,0,0,9.268972837641738e-7,0,0,0.00013530523967983556,0.00006103515625,0.00048828125 +classical,range_band=10-20km,final,3634,0,1.3944097671445805e-7,6.497986438982158e-7,0,0.00005464151336010202,0.0000018537945675283476,0.000013903459256462607,0,0,0.004558875236756464,0.0021836601342771385,0.010708843847393217 +classical,range_band=2-5km,final,1863,0,6.045721709550033e-8,2.276486079566025e-7,0,0.000005122641877509563,2.3172432094104345e-7,0.0000018537945675283476,0,0,0.00043650168202587875,0.000244140625,0.0010429692071920813 +classical,range_band=20-30km,final,515,0,1.990455683591539e-7,8.591447716087028e-7,0.0000034150945850063763,0.00005464151336010202,0.0000055613837025850425,0.000027806918512925215,0,0,0.009508762893649318,0.005859375,0.02344258571124571 +classical,range_band=30+km,final,9,0,6.851410989829762e-8,0.0000011607263146006765,0.0000034150945850063763,0.000040981135020076506,0.0000037075891350566953,0.00005376004245832208,0,0,0.00874825760899502,0.0021836601342771385,0.0453970285123807 +classical,range_band=5-10km,final,3846,0,8.70445563787829e-8,3.6632019879375324e-7,0,0.000025613209387547814,9.268972837641738e-7,0.0000055613837025850425,0,0,0.0014073255032468878,0.0009765625,0.0034375312110177547 +classical,sigma=0deg,final,10000,0,9.625246868512277e-8,4.812320236817345e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0027786471935062848,0.0009841623227903502,0.006905339660024878 +classical,zig_band=120-150deg,final,3350,0,9.259584615706119e-8,4.7333682342916056e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0027263890771233945,0.0009841623227903502,0.006842746068748117 +classical,zig_band=150-180deg,final,4019,0,9.512052121246996e-8,4.70733436301668e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0028004878106140706,0.0009795381985903341,0.007109482313750507 +classical,zig_band=30-50deg,final,84,0,1.1411674223616281e-7,5.385067567064481e-7,0,0.000040981135020076506,0.0000018537945675283476,0.000011122767405170085,0,0,0.005687213290986321,0.0009765625,0.006176323555016366 +classical,zig_band=50-70deg,final,319,0,1.1695404891416201e-7,5.556545196015425e-7,0,0.00002732075668005101,0.0000018537945675283476,0.000009268972837641738,0,0,0.0025582287232674573,0.001078527651798846,0.007814645472616538 +classical,zig_band=70-90deg,final,501,0,1.0593828399940142e-7,5.632217038077209e-7,0,0.000040981135020076506,0.0000013903459256462606,0.000011122767405170085,0,0,0.0029160767709236514,0.0010066175843793117,0.007042092334890605 +classical,zig_band=90-120deg,final,1727,0,9.884345867216302e-8,4.662596728109262e-7,0,0.00002732075668005101,0.0000013903459256462606,0.000008342075553877564,0,0,0.0026885655323933434,0.0010066175843793117,0.006253051013118016 diff --git a/experiments/20261017-g0-i3-r2/metrics.json b/experiments/20261017-g0-i3-r2/metrics.json new file mode 100644 index 00000000..99cd210b --- /dev/null +++ b/experiments/20261017-g0-i3-r2/metrics.json @@ -0,0 +1,542 @@ +[ + { + "course_deg_median": 89.45474167284688, + "course_deg_p90": 162.58337096507148, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3878, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5452.883076396702, + "position_window_m_median": 4401.405811239404, + "position_window_m_p90": 11507.173492769414, + "range_rel_median": 0.41296159627504475, + "range_rel_p90": 1.5327997889522416, + "slice": "overall", + "speed_kt_median": 4.243544316961235, + "speed_kt_p90": 7.647355148653232 + }, + { + "course_deg_median": 89.45474167284688, + "course_deg_p90": 162.58337096507148, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3878, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5452.883076396702, + "position_window_m_median": 4401.405811239404, + "position_window_m_p90": 11507.173492769414, + "range_rel_median": 0.41296159627504475, + "range_rel_p90": 1.5327997889522416, + "slice": "dropout_band=0-5pct", + "speed_kt_median": 4.243544316961235, + "speed_kt_p90": 7.647355148653232 + }, + { + "course_deg_median": 90.76955308807433, + "course_deg_p90": 167.12415266965337, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 1, + "n": 133, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 7541.811836895824, + "position_window_m_median": 7604.0155201139005, + "position_window_m_p90": 8872.685837083234, + "range_rel_median": 3.554155199769074, + "range_rel_p90": 10.189099437703875, + "slice": "range_band=0-2km", + "speed_kt_median": 4.888747783767997, + "speed_kt_p90": 7.68978966492556 + }, + { + "course_deg_median": 89.08235975929779, + "course_deg_p90": 161.3485137442682, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.25949367088607594, + "n": 3634, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 6745.622183900448, + "position_window_m_median": 6441.378841890848, + "position_window_m_p90": 12363.699782051417, + "range_rel_median": 0.4051582349171333, + "range_rel_p90": 0.5659043446436742, + "slice": "range_band=10-20km", + "speed_kt_median": 4.333764790973704, + "speed_kt_p90": 7.650062383894786 + }, + { + "course_deg_median": 91.6602780577357, + "course_deg_p90": 163.413635327571, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.775630703166935, + "n": 1863, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 4947.390624738301, + "position_window_m_median": 5082.411505237025, + "position_window_m_p90": 7707.336892376554, + "range_rel_median": 1.090394363945709, + "range_rel_p90": 3.7852475362152567, + "slice": "range_band=2-5km", + "speed_kt_median": 4.118415500896281, + "speed_kt_p90": 7.688682949568745 + }, + { + "course_deg_median": 87.71670441588876, + "course_deg_p90": 163.07976179319132, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.9825242718446602, + "n": 515, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 17343.277986636796, + "position_window_m_median": 16934.112145062863, + "position_window_m_p90": 22879.839699095115, + "range_rel_median": 0.634768967095896, + "range_rel_p90": 0.7013337367422446, + "slice": "range_band=20-30km", + "speed_kt_median": 4.6361965443094135, + "speed_kt_p90": 7.529825268244897 + }, + { + "course_deg_median": 106.55888773201448, + "course_deg_p90": 157.09385230648385, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 1, + "n": 9, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 28713.815931989186, + "position_window_m_median": 29008.802337317957, + "position_window_m_p90": 31592.53709787277, + "range_rel_median": 0.7485700238919062, + "range_rel_p90": 0.7642858167300461, + "slice": "range_band=30+km", + "speed_kt_median": 3.79518092837241, + "speed_kt_p90": 8.285669219674096 + }, + { + "course_deg_median": 89.37135872345937, + "course_deg_p90": 162.82039583211494, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.218928757150286, + "n": 3846, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 2757.4044547509648, + "position_window_m_median": 2380.8681528827433, + "position_window_m_p90": 5669.739080810058, + "range_rel_median": 0.24299138820601726, + "range_rel_p90": 1.0084669091137104, + "slice": "range_band=5-10km", + "speed_kt_median": 4.159294451802132, + "speed_kt_p90": 7.6459358371874675 + }, + { + "course_deg_median": 89.45474167284688, + "course_deg_p90": 162.58337096507148, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3878, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5452.883076396702, + "position_window_m_median": 4401.405811239404, + "position_window_m_p90": 11507.173492769414, + "range_rel_median": 0.41296159627504475, + "range_rel_p90": 1.5327997889522416, + "slice": "sigma=0deg", + "speed_kt_median": 4.243544316961235, + "speed_kt_p90": 7.647355148653232 + }, + { + "course_deg_median": 91.70037126816366, + "course_deg_p90": 162.25843837568647, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.38985074626865673, + "n": 3350, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5616.710800217959, + "position_window_m_median": 4563.653024001928, + "position_window_m_p90": 12066.507695840213, + "range_rel_median": 0.41814549189676226, + "range_rel_p90": 1.4205594803025299, + "slice": "zig_band=120-150deg", + "speed_kt_median": 4.284804222650713, + "speed_kt_p90": 7.6459358371874675 + }, + { + "course_deg_median": 89.34677004244732, + "course_deg_p90": 163.1154153806588, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.39462552873849216, + "n": 4019, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5637.500113282189, + "position_window_m_median": 4494.309434812314, + "position_window_m_p90": 11926.065519513926, + "range_rel_median": 0.4175174029873284, + "range_rel_p90": 1.5933215071523037, + "slice": "zig_band=150-180deg", + "speed_kt_median": 4.2366588604887925, + "speed_kt_p90": 7.675463077059059 + }, + { + "course_deg_median": 82.8358375746193, + "course_deg_p90": 162.83246477637834, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.4642857142857143, + "n": 84, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 4648.201741165161, + "position_window_m_median": 4436.822024701133, + "position_window_m_p90": 8943.548089216078, + "range_rel_median": 0.4614850895772321, + "range_rel_p90": 1.6337174892005477, + "slice": "zig_band=30-50deg", + "speed_kt_median": 3.8864580652883705, + "speed_kt_p90": 7.198060923467186 + }, + { + "course_deg_median": 93.63977616223211, + "course_deg_p90": 164.3109514297814, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3667711598746082, + "n": 319, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 4489.542019678022, + "position_window_m_median": 3594.839080360501, + "position_window_m_p90": 8942.730785294072, + "range_rel_median": 0.39053573219917426, + "range_rel_p90": 1.700969404255579, + "slice": "zig_band=50-70deg", + "speed_kt_median": 4.118990177212214, + "speed_kt_p90": 7.61714084617005 + }, + { + "course_deg_median": 88.19526845027487, + "course_deg_p90": 165.7484057217456, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3333333333333333, + "n": 501, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 4447.1342740142945, + "position_window_m_median": 3821.0031499270726, + "position_window_m_p90": 8588.865422100125, + "range_rel_median": 0.37249923148578495, + "range_rel_p90": 1.6897176776549168, + "slice": "zig_band=70-90deg", + "speed_kt_median": 4.185012607286095, + "speed_kt_p90": 7.589079494352731 + }, + { + "course_deg_median": 86.56928727683997, + "course_deg_p90": 160.2920542343965, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.38390272148233934, + "n": 1727, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5214.307762674768, + "position_window_m_median": 4283.707052404278, + "position_window_m_p90": 10806.868545155921, + "range_rel_median": 0.4050221004673685, + "range_rel_p90": 1.5631300243029322, + "slice": "zig_band=90-120deg", + "speed_kt_median": 4.236357155422927, + "speed_kt_p90": 7.6209944216273 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0027786471935062848, + "position_window_m_median": 0.0009841623227903502, + "position_window_m_p90": 0.006905339660024878, + "range_rel_median": 9.625246868512277e-8, + "range_rel_p90": 4.812320236817345e-7, + "slice": "overall", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0027786471935062848, + "position_window_m_median": 0.0009841623227903502, + "position_window_m_p90": 0.006905339660024878, + "range_rel_median": 9.625246868512277e-8, + "range_rel_p90": 4.812320236817345e-7, + "slice": "dropout_band=0-5pct", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 133, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.00013530523967983556, + "position_window_m_median": 0.00006103515625, + "position_window_m_p90": 0.00048828125, + "range_rel_median": 4.794212208460275e-8, + "range_rel_p90": 1.6569781425015622e-7, + "slice": "range_band=0-2km", + "speed_kt_median": 0, + "speed_kt_p90": 9.268972837641738e-7 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00005464151336010202, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 3634, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.004558875236756464, + "position_window_m_median": 0.0021836601342771385, + "position_window_m_p90": 0.010708843847393217, + "range_rel_median": 1.3944097671445805e-7, + "range_rel_p90": 6.497986438982158e-7, + "slice": "range_band=10-20km", + "speed_kt_median": 0.0000018537945675283476, + "speed_kt_p90": 0.000013903459256462607 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.000005122641877509563, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 1863, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.00043650168202587875, + "position_window_m_median": 0.000244140625, + "position_window_m_p90": 0.0010429692071920813, + "range_rel_median": 6.045721709550033e-8, + "range_rel_p90": 2.276486079566025e-7, + "slice": "range_band=2-5km", + "speed_kt_median": 2.3172432094104345e-7, + "speed_kt_p90": 0.0000018537945675283476 + }, + { + "course_deg_median": 0.0000034150945850063763, + "course_deg_p90": 0.00005464151336010202, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 515, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.009508762893649318, + "position_window_m_median": 0.005859375, + "position_window_m_p90": 0.02344258571124571, + "range_rel_median": 1.990455683591539e-7, + "range_rel_p90": 8.591447716087028e-7, + "slice": "range_band=20-30km", + "speed_kt_median": 0.0000055613837025850425, + "speed_kt_p90": 0.000027806918512925215 + }, + { + "course_deg_median": 0.0000034150945850063763, + "course_deg_p90": 0.000040981135020076506, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 9, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.00874825760899502, + "position_window_m_median": 0.0021836601342771385, + "position_window_m_p90": 0.0453970285123807, + "range_rel_median": 6.851410989829762e-8, + "range_rel_p90": 0.0000011607263146006765, + "slice": "range_band=30+km", + "speed_kt_median": 0.0000037075891350566953, + "speed_kt_p90": 0.00005376004245832208 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.000025613209387547814, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 3846, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0014073255032468878, + "position_window_m_median": 0.0009765625, + "position_window_m_p90": 0.0034375312110177547, + "range_rel_median": 8.70445563787829e-8, + "range_rel_p90": 3.6632019879375324e-7, + "slice": "range_band=5-10km", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.0000055613837025850425 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0027786471935062848, + "position_window_m_median": 0.0009841623227903502, + "position_window_m_p90": 0.006905339660024878, + "range_rel_median": 9.625246868512277e-8, + "range_rel_p90": 4.812320236817345e-7, + "slice": "sigma=0deg", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 3350, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0027263890771233945, + "position_window_m_median": 0.0009841623227903502, + "position_window_m_p90": 0.006842746068748117, + "range_rel_median": 9.259584615706119e-8, + "range_rel_p90": 4.7333682342916056e-7, + "slice": "zig_band=120-150deg", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 4019, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0028004878106140706, + "position_window_m_median": 0.0009795381985903341, + "position_window_m_p90": 0.007109482313750507, + "range_rel_median": 9.512052121246996e-8, + "range_rel_p90": 4.70733436301668e-7, + "slice": "zig_band=150-180deg", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.000040981135020076506, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 84, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.005687213290986321, + "position_window_m_median": 0.0009765625, + "position_window_m_p90": 0.006176323555016366, + "range_rel_median": 1.1411674223616281e-7, + "range_rel_p90": 5.385067567064481e-7, + "slice": "zig_band=30-50deg", + "speed_kt_median": 0.0000018537945675283476, + "speed_kt_p90": 0.000011122767405170085 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 319, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0025582287232674573, + "position_window_m_median": 0.001078527651798846, + "position_window_m_p90": 0.007814645472616538, + "range_rel_median": 1.1695404891416201e-7, + "range_rel_p90": 5.556545196015425e-7, + "slice": "zig_band=50-70deg", + "speed_kt_median": 0.0000018537945675283476, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.000040981135020076506, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 501, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0029160767709236514, + "position_window_m_median": 0.0010066175843793117, + "position_window_m_p90": 0.007042092334890605, + "range_rel_median": 1.0593828399940142e-7, + "range_rel_p90": 5.632217038077209e-7, + "slice": "zig_band=70-90deg", + "speed_kt_median": 0.0000013903459256462606, + "speed_kt_p90": 0.000011122767405170085 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 1727, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0026885655323933434, + "position_window_m_median": 0.0010066175843793117, + "position_window_m_p90": 0.006253051013118016, + "range_rel_median": 9.884345867216302e-8, + "range_rel_p90": 4.662596728109262e-7, + "slice": "zig_band=90-120deg", + "speed_kt_median": 0.0000013903459256462606, + "speed_kt_p90": 0.000008342075553877564 + } +] diff --git a/experiments/20261017-g0-i3-r2/seeds.txt b/experiments/20261017-g0-i3-r2/seeds.txt new file mode 100644 index 00000000..feb4bb1f --- /dev/null +++ b/experiments/20261017-g0-i3-r2/seeds.txt @@ -0,0 +1,2 @@ +train seed=42 index=[0, 80000) data/g0-i3/train.f32pack +test seed=42 index=[90000, 100000) data/g0-i3/test.f32pack diff --git a/experiments/20261017-g0-i3-r2/verdict.json b/experiments/20261017-g0-i3-r2/verdict.json new file mode 100644 index 00000000..a24cff0c --- /dev/null +++ b/experiments/20261017-g0-i3-r2/verdict.json @@ -0,0 +1,32 @@ +{ + "checks": [ + { + "detail": "10000/10000 scenarios within tolerance (range 0.001, course 0.1°, speed 0.01 kt); worst range 8.70e-6, course 2.54e-3°, speed 3.43e-4 kt", + "name": "classical-recovers-noise-free-truth", + "result": "pass" + }, + { + "detail": "1000 scenarios, 72208 rows: 0 non-converged; 67208/67208 rows ≥ 60 s after the zig within tolerance (worst range 9.78e-6); worst range in the first 60 s 1.92e-5 (reported, not gated)", + "name": "classical-per-timestep-recovers", + "result": "pass" + }, + { + "detail": "10000/10000 final solutions with RMS bearing residual ≤ 0.000001 rad; worst 2.77e-7 rad", + "name": "bearing-residuals-at-float32-precision", + "result": "pass" + }, + { + "detail": "hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2), leg-3 and second-turn (both directions, mid-arc and leg 3) cases match; t = 0 bearing and range match the drawn parameters, and ownship kinematics (step ≤ v·dt, chord heading between the stored courses, turn ≤ ω·dt per step, |turn| ≤ 180°) hold, on 10000 scenarios; 20000 turns put the target on the other bow inside the drawn offset range", + "name": "independent-geometry", + "result": "pass" + }, + { + "detail": "two in-process naive fits gave identical metrics (determinism only; cross-process and cross-commit reproduction is shown by re-running G0 from the manifest)", + "name": "naive-baseline-reproduces", + "result": "pass" + } + ], + "decided_by": "harness (pending agent panel review)", + "gate": "G0", + "result": "pass" +} diff --git a/packages/eval/src/bootstrap.ts b/packages/eval/src/bootstrap.ts index 6fdd1369..700fb719 100644 --- a/packages/eval/src/bootstrap.ts +++ b/packages/eval/src/bootstrap.ts @@ -26,14 +26,15 @@ export const BOOTSTRAP_RESAMPLES = 2000; export const BOOTSTRAP_SEED = 20261011n; /** - * Range errors of two estimators on the scenarios where both have a row `seconds` after the zig - * (non-converged rows count as +Infinity, as everywhere else). + * One error metric of two estimators on the scenarios where both have a row `seconds` after the + * zig (non-converged rows count as +Infinity, as everywhere else). */ -export function pairedRangeErrors( +export function pairedErrors( truth: Iterable, a: PredictionSet, b: PredictionSet, seconds: number, + metric: "range_rel" | "course_deg" | "speed_kt", ): { a: Float64Array; b: Float64Array } { const ea: number[] = []; const eb: number[] = []; @@ -43,7 +44,7 @@ export function pairedRangeErrors( throw new Error(`${set.label}: no prediction for ${String(s.metadata.index)}`); for (const row of p.rows) { const e = rowError(s, row); - if (Math.abs(e.t_since_zig_s - seconds) < 1e-6) return e.range_rel; + if (Math.abs(e.t_since_zig_s - seconds) < 1e-6) return e.converged ? e[metric] : Infinity; } return undefined; }; @@ -57,6 +58,14 @@ export function pairedRangeErrors( return { a: Float64Array.from(ea), b: Float64Array.from(eb) }; } +/** Range errors of two estimators (the G1 ratio's inputs). */ +export const pairedRangeErrors = ( + truth: Iterable, + a: PredictionSet, + b: PredictionSet, + seconds: number, +): { a: Float64Array; b: Float64Array } => pairedErrors(truth, a, b, seconds, "range_rel"); + const sortedMedian = (xs: Float64Array): number => median(ascending(Array.from(xs))); /** NaN (and a missing value) ranks as +Infinity, as in `ascending`. */ diff --git a/packages/models/src/g2.ts b/packages/models/src/g2.ts index 656ccd83..a391df95 100644 --- a/packages/models/src/g2.ts +++ b/packages/models/src/g2.ts @@ -23,6 +23,7 @@ import { BOOTSTRAP_RESAMPLES, BOOTSTRAP_SEED, bootstrapEvaluation, + bootstrapMedianRatio, compareOnCommonRows, evaluate, type G1Ids, @@ -30,9 +31,11 @@ import { g2Verdict, loadPredictionSet, makeManifest, + pairedErrors, type PredictionSet, perTimestep, prettyJson, + sliceKeys, writeBootstrap, writeComparison, writeEvaluation, @@ -128,6 +131,71 @@ export function gitDecisionPrecedes(path: string, commit: string): boolean { } } +/** Paired comparison of the carried set with the other phase, on the same rows (spec 007 FR-006). */ +export const PAIRED_FILE = "paired.json"; + +export interface PairedRecord { + variant: "instantaneous" | "time_late"; + model: string; + other: string; + slice: string; + metric: "range_rel" | "course_deg" | "speed_kt"; + point_s: number; + /** median(model) / median(other) with its paired 95% bootstrap interval. */ + ratio: number; + lo: number; + hi: number; + n: number; + resamples: number; + seed: string; +} + +/** + * For each sequence model, the paired bootstrap of median error, carried / other phase, at the G2 + * points on σ = 1°: range and course at 300 s, speed at 600 s. Spec 007 FR-006's validation rule + * reads the instantaneous speed record; the rest are reported. + */ +export function pairedComparison( + carried: Scored, + carriedIds: G1Ids, + other: Scored, + otherIds: G1Ids, + resamples: number, +): PairedRecord[] { + const slice = "sigma=1deg"; + const truth = carried.scenarios.filter((s) => sliceKeys(s.metadata).includes(slice)); + const find = (sc: Scored, id: string): PredictionSet => { + const set = sc.sets.find((x) => x.estimator === id); + if (set === undefined) throw new Error(`paired comparison: no predictions for ${id}`); + return set; + }; + const out: PairedRecord[] = []; + for (const variant of ["instantaneous", "time_late"] as const) { + const key = variant === "instantaneous" ? "instantaneous" : "timeLate"; + const a = find(carried, carriedIds[key]); + const b = find(other, otherIds[key]); + for (const [metric, point] of [ + ["range_rel", 300], + ["course_deg", 300], + ["speed_kt", 600], + ] as const) { + const e = pairedErrors(truth, a, b, point, metric); + if (e.a.length === 0) continue; + const r = bootstrapMedianRatio(e.a, e.b, resamples); + out.push({ + variant, + model: carriedIds[key], + other: otherIds[key], + slice, + metric, + point_s: point, + ...r, + }); + } + } + return out; +} + /** * Every estimator must predict the same rows (scenario and timestamp) as naive, so the gate's * same-sample check compares the same rows, not only the same counts (G2 panel R19). @@ -431,6 +499,10 @@ export async function runG2(opts: G2RunOptions): Promise { copyModels(dir, other); requireSameRows(o.sets, `${opts.split} (other phase)`); for (const n of o.notes) notes.add(n); + writeFileSync( + join(opts.experiment, PAIRED_FILE), + prettyJson(pairedComparison(scored, carried.ids, o, other.ids, resamples)), + ); } // FR-019: the held-out geometry split is scored with the gate run only (never for validation). if (opts.split === "test") { diff --git a/packages/models/test/g0-guard.test.ts b/packages/models/test/g0-guard.test.ts index 96154396..9459f009 100644 --- a/packages/models/test/g0-guard.test.ts +++ b/packages/models/test/g0-guard.test.ts @@ -137,10 +137,11 @@ describe("G0 guard (constitution V)", () => { }); it("the committed iteration-3 G0 (generator 0.3.0) is the latest, and needs its panel review", () => { - // Generator 0.3.0 (spec 007) no longer accepts the iteration-2 G0 (20261013-g0-i2-r2, 0.2.0); - // G0 was re-run on 0.3.0 at c804757, whose generator source HEAD still matches. + // Generator 0.3.0 (spec 007) no longer accepts the iteration-2 G0 (20261013-g0-i2-r2, 0.2.0). + // G0 ran on 0.3.0 at c804757 and, after the round-1 panel fixes to its checks, again at + // 5989b3e (round 2); the generator source HEAD still matches. const dir = new URL("../../../experiments", import.meta.url).pathname; - const want = { id: "20261017-g0-i3", commit: "c804757853ce2a82e6aeef0e226a499726049f48" }; + const want = { id: "20261017-g0-i3-r2", commit: "5989b3edfdf080e8867e5b6987570597837c2acd" }; expect(g0Accepted(dir, { requireReview: false })).toEqual(want); const verdict = JSON.parse(readFileSync(join(dir, want.id, "verdict.json"), "utf8")) as { decided_by: string; diff --git a/packages/models/test/g2.test.ts b/packages/models/test/g2.test.ts index 0cf624e4..43210abf 100644 --- a/packages/models/test/g2.test.ts +++ b/packages/models/test/g2.test.ts @@ -18,6 +18,8 @@ import { DIAGNOSTIC_FILE, featuresPath, type G2RunOptions, + PAIRED_FILE, + type PairedRecord, requireDecision, requireSameRows, runG2, @@ -223,6 +225,14 @@ describe("tma-g2 end to end (tiny)", () => { expect(diag.decided_by).toMatch(/not gated/); expect(readFileSync(join(other, "metrics.csv"), "utf8")).toContain("lstm:per_timestep"); expect(readFileSync(join(exp, "manifest.json"), "utf8")).toContain("feature_set: derived"); + // Spec 007 FR-006: carried / other phase, paired on the same rows, per model and metric. + const paired = JSON.parse(readFileSync(join(exp, PAIRED_FILE), "utf8")) as PairedRecord[]; + expect(paired.length).toBeGreaterThan(0); + for (const r of paired) { + expect(r.slice).toBe("sigma=1deg"); + expect(r.lo).toBeLessThanOrEqual(r.hi); + expect(["instantaneous", "time_late"]).toContain(r.variant); + } // The other phase's predictions are tagged, so they never overwrite the carried set's // (phases A and B share model ids; G2 panel round 1). const preds = readdirSync(join(data, "predictions")).filter((f) => f.startsWith("test.")); From c648455294646347f58af0731ae0fe14f1c57239 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 09:25:40 +0000 Subject: [PATCH 14/21] G0 panel round 1 record and responses (review.md) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- experiments/20261017-g0-i3-r2/review.md | 66 +++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 experiments/20261017-g0-i3-r2/review.md diff --git a/experiments/20261017-g0-i3-r2/review.md b/experiments/20261017-g0-i3-r2/review.md new file mode 100644 index 00000000..c776c211 --- /dev/null +++ b/experiments/20261017-g0-i3-r2/review.md @@ -0,0 +1,66 @@ +# G0 agent panel review: iteration 3 (generator 0.3.0) + +Constitution 1.2.0 (Governance): five independent specialist reviewers, read-only, iterated until +none rejects and no blocker or major finding is open. + +**Why G0 was re-run.** Iteration 3 (spec 007) changes the generator to 0.3.0. It adds the +across-the-line-of-sight rule (`zig_rule: across_los`) and an optional second zig +(`zig2_delay_s`). The G0 guard therefore refuses the 0.2.0 G0. + +**Data.** Seed 42, 100,000 noise-free scenarios from `configs/iter3-ranges.json`, generated at +90f4873. Every scenario has two rate-limited turns, both across the line of sight. + +**Evidence.** + +- Round 1: `../20261017-g0-i3/` at c804757. +- Round 2: this folder, at 5989b3e. It carries the round-1 fixes to the G0 checks and the zig + bands up to 180°. The generator source (`packages/sim/src`, `packages/data/src`) is unchanged + from 90f4873, so the same data stand. + +| Check | Round 1 (c804757) | Round 2 (5989b3e) | +| --- | --- | --- | +| classical-recovers-noise-free-truth | pass; worst range 8.70e-6, course 2.54e-3°, speed 3.43e-4 kt | same | +| classical-per-timestep-recovers | pass; 67,208/67,208 rows ≥ 60 s after the zig | same | +| bearing-residuals-at-float32-precision | pass; worst 2.77e-7 rad | same | +| independent-geometry | pass; hand cases incl. leg 3; 20,000 turns across the line of sight | pass; adds the second turn in both directions (mid-arc and leg 3), ownship kinematics on every scenario, \|turn\| ≤ 180°, unchecked turns counted | +| naive-baseline-reproduces | pass | pass | + +## Round 1 (2026-10-17) + +| Reviewer | Verdict | +| --- | --- | +| TMA geometry (independent RK4 integration of all 10,000 test and held-out tracks: worst 6.6e-4 m, 1.4e-5°; 0 index mismatches in 50,000; every one of 20,000 turns across the line of sight, inside 30–90°, the shorter way) | approve-with-findings | +| Estimation and numerics (verdict figures reproduced; 716,632 per-timestep rows over the whole split, rows in and after the second turn ≤ 1.24e-5 range; headroom 3–115× better than iteration 2; `simulateTruth` from stored float32 parameters reproduces every stored value bit for bit on all four splits) | approve-with-findings | +| Evaluation statistics (re-ran G0 at eb19125; redraw, window, coverage and band statistics; regenerated data identical) | approve-with-findings | +| Reproducibility (re-ran G0 from the manifest at c804757: data, metrics, seeds and verdict byte-identical; 0.3.0 reproduces every pinned iteration-2 digest, seeds 201 and 203; seed-204 digests reproduce; guard behaviour exercised case by case) | approve-with-findings | +| Red team (ten mutations of the second turn and the rule; decision-record bypass; leakage) | approve-with-findings | + +### Major findings and responses + +| # | Finding (reviewer) | Response | Commit | +| --- | --- | --- | --- | +| M1 | Two second-turn mutations pass G0: the second arc flown the wrong way (`omega` sign) and leg 3 left on the leg-2 track while its course is written. Both corrupt the model's course input. G0's checks use positions only, and the across-LOS check reads only the post-turn course (red team) | `independent-geometry` now checks ownship kinematics on every scenario: each step ≤ v·dt, the chord's heading between the stored courses at its ends, and the course change ≤ ω·dt per step. It also adds hand-computed second-turn cases in both directions, mid-arc and on leg 3, and \|turn\| ≤ 180°. Both mutations fail it (unit tests in `packages/eval/test/g0-checks.test.ts`). The real data pass: 0 of 10,000 on the G0 test and held-out splits, seed-204 validation and iteration-2 test. The reviewer's stricter mean-course chord test fails at every step where a turn ends mid-step, so the bracketing form is used | 5989b3e | +| M2 | The seed-204 test split could be scored now with the 2c decision record: `ChangeDecision` names no plan, and a decision is required only because `configs/iter2/b` exists (red team) | `ChangeDecision` gains `plan` and `test_seed` (schema 0.9.0). From iteration 3, `tma-g2 --split test` refuses without a committed decision that names the plan being scored, and checks its test seed against the plan. The 2c decision is refused for `configs/iter3/a` (tests in `packages/models/test/g2.test.ts`) | 5989b3e | +| M3 | FR-006's validation rule compared with 2c's iteration-2 validation figures. The easier problem alone clears 1.36 kt (R2 forecasts 1.1–1.3 kt), and the "or" gives two chances (statistics) | FR-006 rewritten. Both sets are scored on the same seed-204 validation rows: the 2c models as the other phase, which `tma-g2 --compare-models` now writes paired into `paired.json`. The carried pair goes to test only if two conditions hold. First, the iteration-3 LSTM / 2c LSTM median speed ratio at zig + 600 s, σ = 1°, has a paired 95% interval wholly below 1. Second, the LSTM's G2 speed interval has a lower bound under 1 kt. The time-late model is reported, not a condition. The model / classical ratio is reported beside, against 2c's 2.37 | 5989b3e, 04c1844 | +| M4 | "zig + 300 s" changes meaning. On the test split 59% of scenarios are mid-way through the second turn there and 37% on leg 3; at zig + 600 s, 99.9% are on leg 3, with a median of 280 s of it (statistics; geometry as a minor) | Spec 007 "Reading the gate under three legs" and the decision record state it. The G2 report will say that range and course are judged mostly during the second manoeuvre and speed after 4–5 min of leg 3, with re-baselined classical beside every figure. The SRD §3 row says so | 5989b3e | +| M5 | `pnpm test` failed at eb19125: the G0 guard test still expected no accepted G0 once the 0.3.0 G0 was committed (reproducibility) | Fixed in 7d860de. The test asserts that the committed 0.3.0 G0 is the latest and is accepted only with `decided_by` from a panel round and `review.md` beside it; it now names round 2. The full suite passes (320 tests) | 7d860de, 04c1844 | + +### Minor findings and responses + +| Finding (reviewers) | Response | +| --- | --- | +| The committed metrics predate the zig-band change; a re-run at HEAD splits "90+deg" into three bands (statistics, red team, reproducibility, numerics) | Round 2 re-ran G0 at 5989b3e: this folder has the 90–120°, 120–150° and 150–180° bands | +| G0's across-LOS check would not catch a turn the long way round; unchecked turns were skipped silently (geometry, red team M2b) | \|zig\|, \|zig2\| ≤ 180° is now checked, and skipped turns fail the check (5989b3e) | +| Planning a turn from the observed bearing cannot be seen on noise-free G0 data (red team M4) | The generated-scenario unit test (`packages/sim/test/unit/second-zig.test.ts`) now runs at σ = 2°, so a rule planned from the observed bearing breaks its 30–90° offset bounds | +| Research R4 said targets on the quarter are crossed by turning away; 3,398 of 9,191 turn towards, through the bow line of sight (geometry) | R4 and the generator note corrected: the turn is towards when \|ρ\| + φ < 180°, otherwise away | +| The rule holds at the turn's start; on the new leg 5.9% of turns are outside 30–90° and 26 of 20,000 have the target back on the original bow, all under about 3 km (geometry) | R4, the generator note and the glossary say "judged from the bearing at the start of the turn" and give the figures | +| Turns near 180° are near-ambiguous in direction: 207 of 20,000 are over 179° (geometry) | Documented in R4 and the generator note as a property of the rule; a cap would change the problem (spec 007 Risks) | +| Redraws select longer windows and faster turns: window p10 16.7 min, 10–15 min windows 5.6% against 16.7% if uniform (statistics) | Spec 007 gives the realised distribution and coverage at both gate points; SRD §3 row updated | +| Some first-turn bands are tiny (30–50°: 84 test scenarios); bands are not comparable across iterations (statistics) | FR-008: every slice is reported with its n, under 100 rows is indicative, and bands are not compared across iterations | +| The second turn is not a slice key (statistics) | Not added; the second-turn size is in the metadata for a diagnostic if needed | +| Delay quantisation: the end values 180 s and 300 s carry half weight (statistics) | Documented in spec 007 | +| Per-timestep speed headroom is thin on the full split: leg-2 rows reach 7.09e-3 kt, and one ungated early row 1.36e-2 kt; the check reports worst range only (numerics) | Two-leg conditioning, as in iteration 2, not a 0.3.0 effect. The numerics reviewer's full-split figures are recorded here; extending the check's detail is left for the next G0 change, as it would change no verdict | +| The drawn offsets are not stored, so G0 checks the offset range to 1e-4 rather than the value to 1e-7 (numerics) | Left as is. The realised turns are stored, so the tracks reproduce exactly; recording the offsets would need another schema change | +| The manifest's `tma-generate` line has no `--out`, and the data and G0 commits differ (reproducibility) | The data are identified by their sha256 in the manifest, and `generator_git_commit` (90f4873) is in the dataset header. The lineage is: data at 90f4873, G0 round 1 at c804757, round 2 at 5989b3e, with no generator change between them | +| G0 does not score the held-out split (red team) | As in iterations 1–2. The geometry reviewer integrated the held-out split independently: worst 6.7e-4 m, 0 index mismatches | +| Leakage: the post-turn course bounds the true bearing to a 60° band. It cuts the RMS bearing error at the turn sample by 1.5% (1 bearing, σ = 1°) to 0.5% (10 bearings); turn timing and redraws reveal no range, course or speed (red team) | Negligible; recorded | From 3df0750ee3a3870569444c319b4e52e2b76c06e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 09:33:15 +0000 Subject: [PATCH 15/21] G0 panel round 2 fixes: step lower bound, turn sums, decision binding by content - kinematicFailure: each step at least the full-step arc chord (a track slower than the stored speed fails); each turn's summed course change equals its stored angle (a turn the other way round fails); tests - tma-g2: a decision binds to a plan by its path or its content (test seed 204 or later), compared by resolved real path; a bound decision must name its test seed; tests Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- packages/eval/src/g0-checks.ts | 34 +++++++++++++++-- packages/eval/test/g0-checks.test.ts | 22 +++++++++++ packages/models/src/g2.ts | 56 ++++++++++++++++++++++++---- packages/models/test/g2.test.ts | 26 +++++++++++++ 4 files changed, 126 insertions(+), 12 deletions(-) diff --git a/packages/eval/src/g0-checks.ts b/packages/eval/src/g0-checks.ts index ee747b0f..d803c408 100644 --- a/packages/eval/src/g0-checks.ts +++ b/packages/eval/src/g0-checks.ts @@ -219,9 +219,10 @@ export function secondTurnBearings(): { name: string; got: number; want: number const wrapPi = (a: number): number => Math.atan2(Math.sin(a), Math.cos(a)); /** - * Kinematic consistency of one scenario's ownship track (G0 red team round 1): each step is at - * most v·dt; the chord's heading lies between the stored courses at the step's ends (equal to - * them on a leg); and the course changes by at most ω·dt per step (rate-limited turns). + * Kinematic consistency of one scenario's ownship track (G0 panel rounds 1–2): each step lies + * between the full-step arc chord and v·dt; the chord's heading lies between the stored courses at + * the step's ends (equal to them on a leg); the course changes by at most ω·dt per step; and each + * rate-limited turn's summed course change equals its stored angle. * The stored course is a model input, so a course that disagrees with the positions fails here * even when the solver, which uses positions only, is unaffected. Returns the first failure. */ @@ -231,6 +232,8 @@ export function kinematicFailure(s: Scenario): string | null { const dt = p.dt_s; const rate = p.ownship_turn_rate_rad_s ?? 0; const own = s.ownship.states; + // The shortest a step at speed v can be: the chord of a full-step arc at ω (v·dt on a leg). + const minStep = rate > 0 ? 2 * (v / rate) * Math.sin((rate * dt) / 2) : v * dt; for (let i = 1; i < own.length; i++) { const a = own[i - 1]; const b = own[i]; @@ -239,6 +242,8 @@ export function kinematicFailure(s: Scenario): string | null { const dy = b.y_m - a.y_m; const step = Math.hypot(dx, dy); if (step > v * dt + 0.05) return `sample ${String(i)}: step ${step.toFixed(3)} m > v·dt`; + if (step < minStep - 0.05) + return `sample ${String(i)}: step ${step.toFixed(3)} m < ${minStep.toFixed(3)} m (slower than the stored speed)`; const dc = wrapPi(b.course_rad - a.course_rad); if (rate > 0 && Math.abs(dc) > rate * dt + 1e-5) return `sample ${String(i)}: course changed ${dc.toFixed(5)} rad > ω·dt`; @@ -252,6 +257,27 @@ export function kinematicFailure(s: Scenario): string | null { return `sample ${String(i)}: chord heading ${off.toExponential(2)} rad off the course, outside [0, ${dc.toExponential(2)}]`; } } + // Each rate-limited turn's summed course change equals its stored signed angle, so a track that + // turns 2π − |zig| the other way to the same course fails (G0 panel round 2, geometry). + const m = s.metadata; + const turns: [number | null | undefined, number | null | undefined, number | null | undefined][] = + [ + [m.turn_start_index, m.zig_index, p.zig_angle_rad], + [m.turn2_start_index, m.zig2_index, p.zig2_angle_rad], + ]; + for (const [start, end, angle] of turns) { + if (start == null || end == null || angle == null) continue; + let sum = 0; + for (let i = start; i <= end; i++) { + const a = own[i - 1]; + const b = own[i]; + if (a === undefined || b === undefined) + return `turn at sample ${String(start)}: samples missing`; + sum += wrapPi(b.course_rad - a.course_rad); + } + if (Math.abs(sum - angle) > 1e-4) + return `turn at sample ${String(start)}: course changed ${sum.toFixed(5)} rad, stored ${angle.toFixed(5)}`; + } return null; } @@ -390,7 +416,7 @@ export function checkIndependentGeometry( "independent-geometry", fails.length === 0 && skipped === 0, fails.length === 0 && skipped === 0 - ? `hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2), leg-3 and second-turn (both directions, mid-arc and leg 3) cases match; t = 0 bearing and range match the drawn parameters, and ownship kinematics (step ≤ v·dt, chord heading between the stored courses, turn ≤ ω·dt per step, |turn| ≤ 180°) hold, on ${String(n)} scenarios` + + ? `hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2), leg-3 and second-turn (both directions, mid-arc and leg 3) cases match; t = 0 bearing and range match the drawn parameters, and ownship kinematics (arc chord ≤ step ≤ v·dt, chord heading between the stored courses, turn ≤ ω·dt per step, summed turn = stored angle, |turn| ≤ 180°) hold, on ${String(n)} scenarios` + (los != null ? `; ${String(turns)} turns put the target on the other bow inside the drawn offset range` : "") diff --git a/packages/eval/test/g0-checks.test.ts b/packages/eval/test/g0-checks.test.ts index 8d961f6a..928e0f5b 100644 --- a/packages/eval/test/g0-checks.test.ts +++ b/packages/eval/test/g0-checks.test.ts @@ -115,4 +115,26 @@ describe("G0 kinematic consistency (red team round 1 mutations)", () => { } expect(kinematicFailure(s)).not.toBeNull(); }); + + it("catches leg 3 flown slower than the stored speed (red team round 2, M8)", () => { + const s = structuredClone(base); + const own = s.ownship.states; + const start = own[z2]; + if (start === undefined) throw new Error("index"); + for (let i = z2 + 1; i < own.length; i++) { + const o = own[i]; + if (o === undefined) continue; + o.x_m = Math.fround(start.x_m + 0.98 * (o.x_m - start.x_m)); + o.y_m = Math.fround(start.y_m + 0.98 * (o.y_m - start.y_m)); + } + expect(kinematicFailure(s)).toMatch(/slower than the stored speed/); + }); + + it("catches a turn whose stored angle disagrees with the turn flown (geometry round 2)", () => { + const s = structuredClone(base); + const zig = s.metadata.parameters.zig_angle_rad; + // The same final course reached the other way round: stored angle replaced by 2π − |zig|. + s.metadata.parameters.zig_angle_rad = zig - Math.sign(zig) * 2 * Math.PI; + expect(kinematicFailure(s)).toMatch(/course changed .* stored/); + }); }); diff --git a/packages/models/src/g2.ts b/packages/models/src/g2.ts index a391df95..e2eb5e86 100644 --- a/packages/models/src/g2.ts +++ b/packages/models/src/g2.ts @@ -8,7 +8,14 @@ // a committed decision record (SC-005), and it also scores the held-out split (FR-019). // Lives here rather than in @tma/eval so tfjs-node stays out of the evaluation package. import { execFileSync } from "node:child_process"; -import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + realpathSync, + writeFileSync, +} from "node:fs"; import { join } from "node:path"; import { CLASSICAL, @@ -114,6 +121,31 @@ export function checkPlanDigests(plan: string, data: string): string[] { /** A repository-relative path in one spelling: forward slashes, no leading "./". */ const normPath = (p: string): string => p.replace(/\\/g, "/").replace(/^(\.\/)+/, ""); +/** Test seeds from this one on are bound to their decision by content (spec 007: seed 204). */ +export const FIRST_BOUND_SEED = 204; + +/** Two spellings of one file: the same resolved real path when both exist, else the same text. */ +function samePath(a: string, b: string): boolean { + try { + return realpathSync(a) === realpathSync(b); + } catch { + return normPath(a) === normPath(b); + } +} + +/** The plan's test seed (its fresh test split's, else its dataset's); undefined if unreadable. */ +function testSeedOf(plan: string | undefined): string | undefined { + if (plan === undefined || !existsSync(plan)) return undefined; + try { + const p = JSON.parse(readFileSync(plan, "utf8")) as { + dataset?: { seed?: string; fresh_test?: { seed?: string } }; + }; + return p.dataset?.fresh_test?.seed ?? p.dataset?.seed; + } catch { + return undefined; + } +} + /** True when the commit that first added `path` is an ancestor of (or equal to) `commit`. */ export function gitDecisionPrecedes(path: string, commit: string): boolean { try { @@ -242,8 +274,12 @@ export function requireDecision(opts: G2RunOptions): ChangeDecision | undefined const phaseB = opts.compareModels !== undefined || existsSync(opts.phaseBDir ?? PHASE_B_CONFIGS); // From iteration 3 (spec 007, G0 red team): a test split is scored only with the decision that // names its plan, so an earlier iteration's decision cannot unlock a later iteration's test. - const iteration = /(?:^|\/)configs\/iter(\d+)\//.exec(normPath(opts.plan ?? "")); - const bound = iteration !== null && Number(iteration[1]) >= 3; + // Bound by the plan's path or by its content (a test seed of 204 or later), however spelled. + const planSeed = testSeedOf(opts.plan); + const iteration = /(?:^|\/)configs\/+(?:\.\/)*iter(\d+)\//.exec(normPath(opts.plan ?? "")); + const bound = + (iteration !== null && Number(iteration[1]) >= 3) || + (planSeed !== undefined && /^\d+$/.test(planSeed) && Number(planSeed) >= FIRST_BOUND_SEED); if (opts.decision === undefined) { if (bound) { throw new RecordError( @@ -267,18 +303,22 @@ export function requireDecision(opts: G2RunOptions): ChangeDecision | undefined opts.decision, "decision", ); - if (bound && (d.plan == null || normPath(d.plan) !== normPath(opts.plan ?? ""))) { + if (bound && (d.plan == null || !samePath(d.plan, opts.plan ?? ""))) { throw new RecordError( opts.decision, "plan", `the decision pre-registers ${d.plan ?? "no plan"}, not ${String(opts.plan)}`, ); } + if (bound && d.test_seed == null) { + throw new RecordError( + opts.decision, + "test_seed", + "a decision bound to a plan must name its test seed", + ); + } if (d.test_seed != null && opts.plan !== undefined) { - const p = JSON.parse(readFileSync(opts.plan, "utf8")) as { - dataset?: { seed?: string; fresh_test?: { seed?: string } }; - }; - const seed = p.dataset?.fresh_test?.seed ?? p.dataset?.seed; + const seed = planSeed; if (seed !== d.test_seed) { throw new RecordError( opts.decision, diff --git a/packages/models/test/g2.test.ts b/packages/models/test/g2.test.ts index 43210abf..fe4ccbac 100644 --- a/packages/models/test/g2.test.ts +++ b/packages/models/test/g2.test.ts @@ -354,4 +354,30 @@ describe("decisions bind to their plan from iteration 3 (spec 007, G0 red team)" // Validation needs nothing. expect(requireDecision(opts({ plan: p, split: "validation" }))).toBeUndefined(); }); + + it("binds by content and resolved path, however the plan is spelled (red team round 2)", () => { + const dir = tempDir(); + const p = plan(dir, "204"); + const old = join(dir, "old.json"); + writeFileSync(old, JSON.stringify(DECISION)); + // A copy of the plan outside configs/iter3 is still bound: its test seed is 204. + mkdirSync(join(dir, "elsewhere")); + const copy = join(dir, "elsewhere", "plan.json"); + writeFileSync(copy, JSON.stringify({ dataset: { seed: "204" } })); + expect(() => requireDecision(opts({ plan: copy, decision: old }))).toThrow( + /pre-registers no plan/, + ); + expect(() => requireDecision(opts({ plan: copy }))).toThrow(/needs --decision/); + // A bound decision must name its test seed. + const d = join(dir, "decision.json"); + writeFileSync(d, JSON.stringify({ ...DECISION, plan: p })); + expect(() => requireDecision(opts({ plan: p, decision: d }))).toThrow( + /must name its test seed/, + ); + // Another spelling of the same file is the same plan. + writeFileSync(d, JSON.stringify({ ...DECISION, plan: p, test_seed: "204" })); + const respelled = `${dir}/configs/./iter3/a/../a/plan.json`; // not normalised (join would) + expect(respelled).not.toBe(p); + expect(requireDecision(opts({ plan: respelled, decision: d }))?.plan).toBe(p); + }); }); From 928995a3d2e2fc8000b97ba732b2672527eb233f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 09:51:02 +0000 Subject: [PATCH 16/21] G0 panel round 2 responses: classical reference in paired.json, FR-006 relabelled, schema 0.9.1 - paired.json adds each model / classical on the same rows (the reported measure of what the models gained) - Spec 007 FR-006: a change to the problem has no like-for-like counterfactual; the operative condition is a possible G2 speed pass; beating 2c on the new problem is a sanity check (2c degrades there: 1.93 kt against 1.49 kt on scratch seed 9001), never lever evidence - Schema 0.9.1 (ChangeDecision plan and test_seed); goldens change only in schema_version - Round-1 G0 folder marked superseded Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- experiments/20261017-g0-i3/verdict.json | 2 +- packages/data/test/golden/digests.json | 16 +++--- .../test/golden/seed1-count10-test.ndjson | 2 +- packages/eval/test/golden/verdict.json | 2 +- .../features/test/golden/seed1-2.features.bin | Bin 34020 -> 34020 bytes .../test/golden/seed1-4.derived.features.bin | Bin 78296 -> 78296 bytes packages/models/src/g2.ts | 51 ++++++++++-------- packages/models/test/g2.test.ts | 1 + packages/types/src/generated/tma.schema.json | 2 +- packages/types/src/generated/version.ts | 2 +- schema/tma.yaml | 2 +- specs/007-iteration-3-second-zig/spec.md | 40 ++++++++------ 12 files changed, 68 insertions(+), 52 deletions(-) diff --git a/experiments/20261017-g0-i3/verdict.json b/experiments/20261017-g0-i3/verdict.json index 3564fff1..a490f1d0 100644 --- a/experiments/20261017-g0-i3/verdict.json +++ b/experiments/20261017-g0-i3/verdict.json @@ -26,7 +26,7 @@ "result": "pass" } ], - "decided_by": "harness (pending agent panel review)", + "decided_by": "superseded: round-1 evidence of the iteration-3 G0 panel; the round-1 fixes changed the G0 checks (packages/eval), so G0 was re-run and the verdict of record is ../20261017-g0-i3-r3 (see ../20261017-g0-i3-r2/review.md)", "gate": "G0", "result": "pass" } diff --git a/packages/data/test/golden/digests.json b/packages/data/test/golden/digests.json index 6d764f0c..6b6ba26a 100644 --- a/packages/data/test/golden/digests.json +++ b/packages/data/test/golden/digests.json @@ -1,10 +1,10 @@ { - "train.ndjson": "9e1ed453f0fb5511979e04b2fdfdfa6d3d5e18756092645233d4f133eaffb6e3", - "train.f32pack": "2d5919fc4a619cc50e5d324e0ffbc9805ed3bb98ae565f31c90bcc40420a3a30", - "validation.ndjson": "fbad0af145c206ad1127cd1266d586b48aaaba4b9002a2d6ad8aa1413fdfcd59", - "validation.f32pack": "006d3e71f7b3b16ba96630bbad261e9fefae005be0f3e3784a194a064d15a5f8", - "test.ndjson": "c746ace467b192676c5f8667ae541415ff8b3a3e80034e03bd7f771098a6bccb", - "test.f32pack": "e53663577ea827c98cd273e3a86b770f68ea49158dfb34886bfde970ab4b47ea", - "heldout.ndjson": "514403783aaf1d79d9018658992d9201e3130e665b3909fddcbb5481a62813d3", - "heldout.f32pack": "6cae688cada64acd7de81086612622444b480866b9df9709e0c9686cdfe35aae" + "train.ndjson": "2905ab8c808bebdf11a772c445289822ccdad73930a27b6ac29b78e404b3944c", + "train.f32pack": "cdc09137d1a0b0058e42d8cd7c1e09f70b9e8f4879b951e21d15854c5011a796", + "validation.ndjson": "baff74821a16c805e349c1b31cb40bc4d39230ee52d925096d57602520d404f8", + "validation.f32pack": "a573fbc82ca9b2b0b99e4f6dd113a9eb61ec150e4745fbfe41741e8d2238c441", + "test.ndjson": "e5ca87085d51faf0943abdaba32b8e06494b2552201ee806d3b391c359021e35", + "test.f32pack": "b2aee5953e287618086dc462562f58c1e717ce2a0b9ba42db5f3a6cada041558", + "heldout.ndjson": "ca2ada40a85db67538fcd0109879e72309c7a9b6a574ec5f9aa4ee2b88aadac4", + "heldout.f32pack": "4d2f8b5330054f64ff3ed0667753e69305703ea32cc1403a5eef264732f01764" } diff --git a/packages/data/test/golden/seed1-count10-test.ndjson b/packages/data/test/golden/seed1-count10-test.ndjson index 3c11e6b3..184e1b66 100644 --- a/packages/data/test/golden/seed1-count10-test.ndjson +++ b/packages/data/test/golden/seed1-count10-test.ndjson @@ -1,2 +1,2 @@ -{"created_by":"tma-generate --seed 1 --count 10","dataset_seed":"1","generator_git_commit":"0123456789abcdef0123456789abcdef01234567","generator_git_dirty":false,"generator_version":"0.3.0","index_end":10,"index_start":9,"parameter_ranges":{"dropout_fraction":{"max":0.1,"min":0},"dt_s":10,"min_range_m":100,"noise_sigma_rad_values":[0.008726646259971648,0.017453292519943295,0.03490658503988659],"ownship_speed_mps":{"max":7.716666666666667,"min":2.5722222222222224},"range_distribution":"log_uniform","target_range0_m":{"max":20000,"min":2000},"target_speed_mps":{"max":10.28888888888889,"min":1.5433333333333334},"window_s":{"max":2400,"min":600},"zig_angle_abs_rad":{"max":1.5707963267948966,"min":0.5235987755982988},"zig_fraction":{"max":0.6,"min":0.4}},"record":"header","redraw_count":0,"scenario_count":1,"schema_version":"0.9.0","split":"test"} +{"created_by":"tma-generate --seed 1 --count 10","dataset_seed":"1","generator_git_commit":"0123456789abcdef0123456789abcdef01234567","generator_git_dirty":false,"generator_version":"0.3.0","index_end":10,"index_start":9,"parameter_ranges":{"dropout_fraction":{"max":0.1,"min":0},"dt_s":10,"min_range_m":100,"noise_sigma_rad_values":[0.008726646259971648,0.017453292519943295,0.03490658503988659],"ownship_speed_mps":{"max":7.716666666666667,"min":2.5722222222222224},"range_distribution":"log_uniform","target_range0_m":{"max":20000,"min":2000},"target_speed_mps":{"max":10.28888888888889,"min":1.5433333333333334},"window_s":{"max":2400,"min":600},"zig_angle_abs_rad":{"max":1.5707963267948966,"min":0.5235987755982988},"zig_fraction":{"max":0.6,"min":0.4}},"record":"header","redraw_count":0,"scenario_count":1,"schema_version":"0.9.1","split":"test"} {"metadata":{"index":9,"mean_range_m":5958.755859375,"min_range_m":991.6001586914062,"parameters":{"dropout_fraction":0.03438697010278702,"dt_s":10,"noise_sigma_rad":0.03490658476948738,"ownship_course0_rad":2.5504064559936523,"ownship_speed_mps":5.665868282318115,"target_bearing0_rad":2.595902442932129,"target_course_rad":6.078586101531982,"target_range0_m":5175.45263671875,"target_speed_mps":9.046577453613281,"window_s":1390,"zig_angle_rad":1.5534991025924683,"zig_fraction":0.4844420850276947},"samples":139,"seed":"14646652180046636950","zig_index":67,"zig_time_s":670},"observations":[{"bearing_rad":2.6220357418060303,"timestamp_s":0,"valid":true},{"bearing_rad":0,"timestamp_s":10,"valid":false},{"bearing_rad":2.615083694458008,"timestamp_s":20,"valid":true},{"bearing_rad":2.58514404296875,"timestamp_s":30,"valid":true},{"bearing_rad":2.574988842010498,"timestamp_s":40,"valid":true},{"bearing_rad":2.527792453765869,"timestamp_s":50,"valid":true},{"bearing_rad":2.5440268516540527,"timestamp_s":60,"valid":true},{"bearing_rad":2.596759557723999,"timestamp_s":70,"valid":true},{"bearing_rad":2.575688600540161,"timestamp_s":80,"valid":true},{"bearing_rad":2.616668224334717,"timestamp_s":90,"valid":true},{"bearing_rad":2.5857462882995605,"timestamp_s":100,"valid":true},{"bearing_rad":2.500842332839966,"timestamp_s":110,"valid":true},{"bearing_rad":2.5180552005767822,"timestamp_s":120,"valid":true},{"bearing_rad":2.519590377807617,"timestamp_s":130,"valid":true},{"bearing_rad":2.478708505630493,"timestamp_s":140,"valid":true},{"bearing_rad":2.4860692024230957,"timestamp_s":150,"valid":true},{"bearing_rad":2.455496072769165,"timestamp_s":160,"valid":true},{"bearing_rad":2.447126865386963,"timestamp_s":170,"valid":true},{"bearing_rad":2.408547878265381,"timestamp_s":180,"valid":true},{"bearing_rad":2.3457624912261963,"timestamp_s":190,"valid":true},{"bearing_rad":2.3567445278167725,"timestamp_s":200,"valid":true},{"bearing_rad":2.330347776412964,"timestamp_s":210,"valid":true},{"bearing_rad":2.3350954055786133,"timestamp_s":220,"valid":true},{"bearing_rad":2.2078351974487305,"timestamp_s":230,"valid":true},{"bearing_rad":2.1612424850463867,"timestamp_s":240,"valid":true},{"bearing_rad":2.199679374694824,"timestamp_s":250,"valid":true},{"bearing_rad":2.1880311965942383,"timestamp_s":260,"valid":true},{"bearing_rad":2.0965542793273926,"timestamp_s":270,"valid":true},{"bearing_rad":2.030519723892212,"timestamp_s":280,"valid":true},{"bearing_rad":1.982517957687378,"timestamp_s":290,"valid":true},{"bearing_rad":1.8200387954711914,"timestamp_s":300,"valid":true},{"bearing_rad":1.7571829557418823,"timestamp_s":310,"valid":true},{"bearing_rad":0,"timestamp_s":320,"valid":false},{"bearing_rad":1.5004644393920898,"timestamp_s":330,"valid":true},{"bearing_rad":1.3480144739151,"timestamp_s":340,"valid":true},{"bearing_rad":1.2327461242675781,"timestamp_s":350,"valid":true},{"bearing_rad":1.1466983556747437,"timestamp_s":360,"valid":true},{"bearing_rad":1.007601261138916,"timestamp_s":370,"valid":true},{"bearing_rad":0.8127108812332153,"timestamp_s":380,"valid":true},{"bearing_rad":0.7107616066932678,"timestamp_s":390,"valid":true},{"bearing_rad":0.628229022026062,"timestamp_s":400,"valid":true},{"bearing_rad":0.5733001828193665,"timestamp_s":410,"valid":true},{"bearing_rad":0.47830432653427124,"timestamp_s":420,"valid":true},{"bearing_rad":0.34476739168167114,"timestamp_s":430,"valid":true},{"bearing_rad":0.3072022497653961,"timestamp_s":440,"valid":true},{"bearing_rad":0.21448193490505219,"timestamp_s":450,"valid":true},{"bearing_rad":0.2762027382850647,"timestamp_s":460,"valid":true},{"bearing_rad":0.1993727833032608,"timestamp_s":470,"valid":true},{"bearing_rad":0.12604738771915436,"timestamp_s":480,"valid":true},{"bearing_rad":0.1762598752975464,"timestamp_s":490,"valid":true},{"bearing_rad":0.11149193346500397,"timestamp_s":500,"valid":true},{"bearing_rad":0.035279180854558945,"timestamp_s":510,"valid":true},{"bearing_rad":6.274420738220215,"timestamp_s":520,"valid":true},{"bearing_rad":0.01893194206058979,"timestamp_s":530,"valid":true},{"bearing_rad":6.234099388122559,"timestamp_s":540,"valid":true},{"bearing_rad":6.275217533111572,"timestamp_s":550,"valid":true},{"bearing_rad":6.24389123916626,"timestamp_s":560,"valid":true},{"bearing_rad":6.210912704467773,"timestamp_s":570,"valid":true},{"bearing_rad":6.221532821655273,"timestamp_s":580,"valid":true},{"bearing_rad":6.1376118659973145,"timestamp_s":590,"valid":true},{"bearing_rad":6.246764659881592,"timestamp_s":600,"valid":true},{"bearing_rad":6.193379878997803,"timestamp_s":610,"valid":true},{"bearing_rad":6.177923202514648,"timestamp_s":620,"valid":true},{"bearing_rad":6.193901062011719,"timestamp_s":630,"valid":true},{"bearing_rad":6.178808689117432,"timestamp_s":640,"valid":true},{"bearing_rad":6.147686004638672,"timestamp_s":650,"valid":true},{"bearing_rad":6.212806224822998,"timestamp_s":660,"valid":true},{"bearing_rad":6.125346660614014,"timestamp_s":670,"valid":true},{"bearing_rad":6.2379584312438965,"timestamp_s":680,"valid":true},{"bearing_rad":6.065344333648682,"timestamp_s":690,"valid":true},{"bearing_rad":6.137896537780762,"timestamp_s":700,"valid":true},{"bearing_rad":6.209718704223633,"timestamp_s":710,"valid":true},{"bearing_rad":6.189711570739746,"timestamp_s":720,"valid":true},{"bearing_rad":6.182713508605957,"timestamp_s":730,"valid":true},{"bearing_rad":6.21376371383667,"timestamp_s":740,"valid":true},{"bearing_rad":6.191138744354248,"timestamp_s":750,"valid":true},{"bearing_rad":6.180636882781982,"timestamp_s":760,"valid":true},{"bearing_rad":6.251753330230713,"timestamp_s":770,"valid":true},{"bearing_rad":6.239569187164307,"timestamp_s":780,"valid":true},{"bearing_rad":0,"timestamp_s":790,"valid":false},{"bearing_rad":6.198854446411133,"timestamp_s":800,"valid":true},{"bearing_rad":0.002407611347734928,"timestamp_s":810,"valid":true},{"bearing_rad":6.25775146484375,"timestamp_s":820,"valid":true},{"bearing_rad":6.27123498916626,"timestamp_s":830,"valid":true},{"bearing_rad":0.03430521488189697,"timestamp_s":840,"valid":true},{"bearing_rad":6.239800453186035,"timestamp_s":850,"valid":true},{"bearing_rad":6.269564151763916,"timestamp_s":860,"valid":true},{"bearing_rad":0.043648820370435715,"timestamp_s":870,"valid":true},{"bearing_rad":6.223535060882568,"timestamp_s":880,"valid":true},{"bearing_rad":0.005696240346878767,"timestamp_s":890,"valid":true},{"bearing_rad":0.020358096808195114,"timestamp_s":900,"valid":true},{"bearing_rad":0.012834550812840462,"timestamp_s":910,"valid":true},{"bearing_rad":0.041951797902584076,"timestamp_s":920,"valid":true},{"bearing_rad":0.00724956626072526,"timestamp_s":930,"valid":true},{"bearing_rad":6.24912691116333,"timestamp_s":940,"valid":true},{"bearing_rad":0.036373239010572433,"timestamp_s":950,"valid":true},{"bearing_rad":0.02393398806452751,"timestamp_s":960,"valid":true},{"bearing_rad":0.04807974770665169,"timestamp_s":970,"valid":true},{"bearing_rad":0.05394063889980316,"timestamp_s":980,"valid":true},{"bearing_rad":0.00478453328832984,"timestamp_s":990,"valid":true},{"bearing_rad":0.01687886379659176,"timestamp_s":1000,"valid":true},{"bearing_rad":0.05434570461511612,"timestamp_s":1010,"valid":true},{"bearing_rad":0.02123403735458851,"timestamp_s":1020,"valid":true},{"bearing_rad":0,"timestamp_s":1030,"valid":false},{"bearing_rad":0.02340744249522686,"timestamp_s":1040,"valid":true},{"bearing_rad":0.06245235726237297,"timestamp_s":1050,"valid":true},{"bearing_rad":0.09659411758184433,"timestamp_s":1060,"valid":true},{"bearing_rad":0.05827644094824791,"timestamp_s":1070,"valid":true},{"bearing_rad":0.05699284374713898,"timestamp_s":1080,"valid":true},{"bearing_rad":0.079716257750988,"timestamp_s":1090,"valid":true},{"bearing_rad":6.27664041519165,"timestamp_s":1100,"valid":true},{"bearing_rad":0.08928501605987549,"timestamp_s":1110,"valid":true},{"bearing_rad":0.05191955342888832,"timestamp_s":1120,"valid":true},{"bearing_rad":0.040906764566898346,"timestamp_s":1130,"valid":true},{"bearing_rad":0.07411102205514908,"timestamp_s":1140,"valid":true},{"bearing_rad":0.07693947851657867,"timestamp_s":1150,"valid":true},{"bearing_rad":0.07902813702821732,"timestamp_s":1160,"valid":true},{"bearing_rad":0.06636165827512741,"timestamp_s":1170,"valid":true},{"bearing_rad":0.0027758220676332712,"timestamp_s":1180,"valid":true},{"bearing_rad":0.12958376109600067,"timestamp_s":1190,"valid":true},{"bearing_rad":0.1077728122472763,"timestamp_s":1200,"valid":true},{"bearing_rad":0.046574659645557404,"timestamp_s":1210,"valid":true},{"bearing_rad":0.06341735273599625,"timestamp_s":1220,"valid":true},{"bearing_rad":0.0646064430475235,"timestamp_s":1230,"valid":true},{"bearing_rad":0.07916583865880966,"timestamp_s":1240,"valid":true},{"bearing_rad":0.07559680193662643,"timestamp_s":1250,"valid":true},{"bearing_rad":0.08714760839939117,"timestamp_s":1260,"valid":true},{"bearing_rad":0.030894936993718147,"timestamp_s":1270,"valid":true},{"bearing_rad":0.06951107084751129,"timestamp_s":1280,"valid":true},{"bearing_rad":0.048993512988090515,"timestamp_s":1290,"valid":true},{"bearing_rad":0.0934167131781578,"timestamp_s":1300,"valid":true},{"bearing_rad":0.08700275421142578,"timestamp_s":1310,"valid":true},{"bearing_rad":0.11747071892023087,"timestamp_s":1320,"valid":true},{"bearing_rad":0.04383845254778862,"timestamp_s":1330,"valid":true},{"bearing_rad":0.1259775459766388,"timestamp_s":1340,"valid":true},{"bearing_rad":0.10197585821151733,"timestamp_s":1350,"valid":true},{"bearing_rad":0.04319304972887039,"timestamp_s":1360,"valid":true},{"bearing_rad":0.1367194503545761,"timestamp_s":1370,"valid":true},{"bearing_rad":0,"timestamp_s":1380,"valid":false}],"ownship":{"id":"ownship","states":[{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":0,"x_m":0,"y_m":0},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":10,"x_m":31.578506469726562,"y_m":-47.042579650878906},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":20,"x_m":63.157012939453125,"y_m":-94.08515930175781},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":30,"x_m":94.73551940917969,"y_m":-141.1277313232422},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":40,"x_m":126.31402587890625,"y_m":-188.17031860351562},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":50,"x_m":157.8925323486328,"y_m":-235.212890625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":60,"x_m":189.47103881835938,"y_m":-282.2554626464844},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":70,"x_m":221.04954528808594,"y_m":-329.2980651855469},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":80,"x_m":252.6280517578125,"y_m":-376.34063720703125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":90,"x_m":284.2065734863281,"y_m":-423.3832092285156},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":100,"x_m":315.7850646972656,"y_m":-470.42578125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":110,"x_m":347.36358642578125,"y_m":-517.4683837890625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":120,"x_m":378.94207763671875,"y_m":-564.5109252929688},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":130,"x_m":410.5205993652344,"y_m":-611.5535278320312},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":140,"x_m":442.0990905761719,"y_m":-658.5961303710938},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":150,"x_m":473.6776123046875,"y_m":-705.638671875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":160,"x_m":505.256103515625,"y_m":-752.6812744140625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":170,"x_m":536.8345947265625,"y_m":-799.7238159179688},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":180,"x_m":568.4131469726562,"y_m":-846.7664184570312},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":190,"x_m":599.9916381835938,"y_m":-893.8090209960938},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":200,"x_m":631.5701293945312,"y_m":-940.8515625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":210,"x_m":663.1486206054688,"y_m":-987.8941650390625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":220,"x_m":694.7271728515625,"y_m":-1034.936767578125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":230,"x_m":726.3056640625,"y_m":-1081.9793701171875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":240,"x_m":757.8841552734375,"y_m":-1129.0218505859375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":250,"x_m":789.462646484375,"y_m":-1176.064453125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":260,"x_m":821.0411987304688,"y_m":-1223.1070556640625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":270,"x_m":852.6196899414062,"y_m":-1270.149658203125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":280,"x_m":884.1981811523438,"y_m":-1317.1922607421875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":290,"x_m":915.7766723632812,"y_m":-1364.2347412109375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":300,"x_m":947.355224609375,"y_m":-1411.27734375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":310,"x_m":978.9337158203125,"y_m":-1458.3199462890625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":320,"x_m":1010.51220703125,"y_m":-1505.362548828125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":330,"x_m":1042.0906982421875,"y_m":-1552.4051513671875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":340,"x_m":1073.669189453125,"y_m":-1599.4476318359375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":350,"x_m":1105.2476806640625,"y_m":-1646.490234375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":360,"x_m":1136.8262939453125,"y_m":-1693.5328369140625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":370,"x_m":1168.40478515625,"y_m":-1740.575439453125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":380,"x_m":1199.9832763671875,"y_m":-1787.6180419921875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":390,"x_m":1231.561767578125,"y_m":-1834.66064453125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":400,"x_m":1263.1402587890625,"y_m":-1881.703125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":410,"x_m":1294.71875,"y_m":-1928.7457275390625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":420,"x_m":1326.2972412109375,"y_m":-1975.788330078125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":430,"x_m":1357.875732421875,"y_m":-2022.8309326171875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":440,"x_m":1389.454345703125,"y_m":-2069.87353515625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":450,"x_m":1421.0328369140625,"y_m":-2116.916015625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":460,"x_m":1452.611328125,"y_m":-2163.958740234375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":470,"x_m":1484.1898193359375,"y_m":-2211.001220703125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":480,"x_m":1515.768310546875,"y_m":-2258.043701171875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":490,"x_m":1547.3468017578125,"y_m":-2305.08642578125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":500,"x_m":1578.92529296875,"y_m":-2352.12890625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":510,"x_m":1610.5037841796875,"y_m":-2399.171630859375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":520,"x_m":1642.0823974609375,"y_m":-2446.214111328125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":530,"x_m":1673.660888671875,"y_m":-2493.256591796875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":540,"x_m":1705.2393798828125,"y_m":-2540.29931640625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":550,"x_m":1736.81787109375,"y_m":-2587.341796875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":560,"x_m":1768.3963623046875,"y_m":-2634.384521484375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":570,"x_m":1799.974853515625,"y_m":-2681.427001953125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":580,"x_m":1831.5533447265625,"y_m":-2728.469482421875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":590,"x_m":1863.1319580078125,"y_m":-2775.51220703125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":600,"x_m":1894.71044921875,"y_m":-2822.5546875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":610,"x_m":1926.2889404296875,"y_m":-2869.597412109375},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":620,"x_m":1957.867431640625,"y_m":-2916.639892578125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":630,"x_m":1989.4459228515625,"y_m":-2963.682373046875},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":640,"x_m":2021.0244140625,"y_m":-3010.72509765625},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":650,"x_m":2052.60302734375,"y_m":-3057.767578125},{"course_rad":2.5504064559936523,"speed_mps":5.665868282318115,"timestamp_s":660,"x_m":2084.181396484375,"y_m":-3104.810302734375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":670,"x_m":2115.760009765625,"y_m":-3151.852783203125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":680,"x_m":2069.2705078125,"y_m":-3184.240234375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":690,"x_m":2022.78125,"y_m":-3216.627685546875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":700,"x_m":1976.2918701171875,"y_m":-3249.01513671875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":710,"x_m":1929.8026123046875,"y_m":-3281.402587890625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":720,"x_m":1883.313232421875,"y_m":-3313.7900390625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":730,"x_m":1836.8238525390625,"y_m":-3346.177490234375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":740,"x_m":1790.33447265625,"y_m":-3378.56494140625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":750,"x_m":1743.84521484375,"y_m":-3410.952392578125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":760,"x_m":1697.3558349609375,"y_m":-3443.33984375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":770,"x_m":1650.866455078125,"y_m":-3475.727294921875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":780,"x_m":1604.3770751953125,"y_m":-3508.11474609375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":790,"x_m":1557.8878173828125,"y_m":-3540.502197265625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":800,"x_m":1511.3984375,"y_m":-3572.8896484375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":810,"x_m":1464.9090576171875,"y_m":-3605.277099609375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":820,"x_m":1418.419677734375,"y_m":-3637.66455078125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":830,"x_m":1371.930419921875,"y_m":-3670.052001953125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":840,"x_m":1325.4410400390625,"y_m":-3702.439453125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":850,"x_m":1278.95166015625,"y_m":-3734.826904296875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":860,"x_m":1232.4622802734375,"y_m":-3767.21435546875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":870,"x_m":1185.9730224609375,"y_m":-3799.601806640625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":880,"x_m":1139.483642578125,"y_m":-3831.9892578125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":890,"x_m":1092.9942626953125,"y_m":-3864.376708984375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":900,"x_m":1046.5048828125,"y_m":-3896.76416015625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":910,"x_m":1000.0155639648438,"y_m":-3929.151611328125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":920,"x_m":953.5262451171875,"y_m":-3961.5390625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":930,"x_m":907.036865234375,"y_m":-3993.926513671875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":940,"x_m":860.5475463867188,"y_m":-4026.31396484375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":950,"x_m":814.0581665039062,"y_m":-4058.701416015625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":960,"x_m":767.56884765625,"y_m":-4091.0888671875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":970,"x_m":721.0794677734375,"y_m":-4123.47607421875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":980,"x_m":674.5901489257812,"y_m":-4155.86376953125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":990,"x_m":628.1007690429688,"y_m":-4188.2509765625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1000,"x_m":581.6114501953125,"y_m":-4220.638671875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1010,"x_m":535.1220703125,"y_m":-4253.02587890625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1020,"x_m":488.63275146484375,"y_m":-4285.41357421875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1030,"x_m":442.1434020996094,"y_m":-4317.80078125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1040,"x_m":395.654052734375,"y_m":-4350.1884765625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1050,"x_m":349.1647033691406,"y_m":-4382.57568359375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1060,"x_m":302.67535400390625,"y_m":-4414.96337890625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1070,"x_m":256.1860046386719,"y_m":-4447.3505859375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1080,"x_m":209.6966552734375,"y_m":-4479.73828125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1090,"x_m":163.20730590820312,"y_m":-4512.12548828125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1100,"x_m":116.71796417236328,"y_m":-4544.51318359375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1110,"x_m":70.2286148071289,"y_m":-4576.900390625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1120,"x_m":23.7392635345459,"y_m":-4609.2880859375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1130,"x_m":-22.750085830688477,"y_m":-4641.67529296875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1140,"x_m":-69.23943328857422,"y_m":-4674.06298828125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1150,"x_m":-115.7287826538086,"y_m":-4706.4501953125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1160,"x_m":-162.21812438964844,"y_m":-4738.837890625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1170,"x_m":-208.7074737548828,"y_m":-4771.22509765625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1180,"x_m":-255.1968231201172,"y_m":-4803.61279296875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1190,"x_m":-301.6861877441406,"y_m":-4836},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1200,"x_m":-348.175537109375,"y_m":-4868.3876953125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1210,"x_m":-394.6648864746094,"y_m":-4900.77490234375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1220,"x_m":-441.15423583984375,"y_m":-4933.16259765625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1230,"x_m":-487.6435852050781,"y_m":-4965.5498046875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1240,"x_m":-534.1329345703125,"y_m":-4997.9375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1250,"x_m":-580.6222534179688,"y_m":-5030.32470703125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1260,"x_m":-627.1116333007812,"y_m":-5062.71240234375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1270,"x_m":-673.6009521484375,"y_m":-5095.099609375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1280,"x_m":-720.09033203125,"y_m":-5127.4873046875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1290,"x_m":-766.5796508789062,"y_m":-5159.87451171875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1300,"x_m":-813.0690307617188,"y_m":-5192.26220703125},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1310,"x_m":-859.558349609375,"y_m":-5224.6494140625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1320,"x_m":-906.0477294921875,"y_m":-5257.037109375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1330,"x_m":-952.5370483398438,"y_m":-5289.42431640625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1340,"x_m":-999.0264282226562,"y_m":-5321.81201171875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1350,"x_m":-1045.5157470703125,"y_m":-5354.19921875},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1360,"x_m":-1092.005126953125,"y_m":-5386.5869140625},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1370,"x_m":-1138.4945068359375,"y_m":-5418.97412109375},{"course_rad":4.10390567779541,"speed_mps":5.665868282318115,"timestamp_s":1380,"x_m":-1184.9837646484375,"y_m":-5451.36181640625}]},"record":"scenario","target":{"id":"target","states":[{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":0,"x_m":2686.102294921875,"y_m":-4423.81787109375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":10,"x_m":2667.721923828125,"y_m":-4335.23876953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":20,"x_m":2649.341552734375,"y_m":-4246.66015625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":30,"x_m":2630.961181640625,"y_m":-4158.0810546875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":40,"x_m":2612.580810546875,"y_m":-4069.50244140625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":50,"x_m":2594.200439453125,"y_m":-3980.92333984375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":60,"x_m":2575.820068359375,"y_m":-3892.344482421875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":70,"x_m":2557.439697265625,"y_m":-3803.765625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":80,"x_m":2539.059326171875,"y_m":-3715.186767578125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":90,"x_m":2520.678955078125,"y_m":-3626.60791015625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":100,"x_m":2502.298583984375,"y_m":-3538.029052734375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":110,"x_m":2483.918212890625,"y_m":-3449.4501953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":120,"x_m":2465.537841796875,"y_m":-3360.871337890625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":130,"x_m":2447.15771484375,"y_m":-3272.29248046875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":140,"x_m":2428.77734375,"y_m":-3183.71337890625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":150,"x_m":2410.39697265625,"y_m":-3095.134521484375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":160,"x_m":2392.0166015625,"y_m":-3006.5556640625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":170,"x_m":2373.63623046875,"y_m":-2917.976806640625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":180,"x_m":2355.255859375,"y_m":-2829.39794921875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":190,"x_m":2336.87548828125,"y_m":-2740.819091796875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":200,"x_m":2318.4951171875,"y_m":-2652.240234375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":210,"x_m":2300.11474609375,"y_m":-2563.661376953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":220,"x_m":2281.734375,"y_m":-2475.08251953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":230,"x_m":2263.35400390625,"y_m":-2386.50341796875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":240,"x_m":2244.9736328125,"y_m":-2297.924560546875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":250,"x_m":2226.59326171875,"y_m":-2209.345703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":260,"x_m":2208.212890625,"y_m":-2120.766845703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":270,"x_m":2189.83251953125,"y_m":-2032.18798828125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":280,"x_m":2171.4521484375,"y_m":-1943.609130859375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":290,"x_m":2153.07177734375,"y_m":-1855.0302734375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":300,"x_m":2134.69140625,"y_m":-1766.4512939453125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":310,"x_m":2116.31103515625,"y_m":-1677.8724365234375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":320,"x_m":2097.9306640625,"y_m":-1589.2935791015625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":330,"x_m":2079.55029296875,"y_m":-1500.7147216796875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":340,"x_m":2061.169921875,"y_m":-1412.1358642578125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":350,"x_m":2042.7896728515625,"y_m":-1323.556884765625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":360,"x_m":2024.4093017578125,"y_m":-1234.97802734375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":370,"x_m":2006.0289306640625,"y_m":-1146.399169921875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":380,"x_m":1987.6485595703125,"y_m":-1057.8203125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":390,"x_m":1969.2681884765625,"y_m":-969.2413940429688},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":400,"x_m":1950.8878173828125,"y_m":-880.6624755859375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":410,"x_m":1932.5074462890625,"y_m":-792.0836181640625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":420,"x_m":1914.127197265625,"y_m":-703.5047607421875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":430,"x_m":1895.746826171875,"y_m":-614.9258422851562},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":440,"x_m":1877.366455078125,"y_m":-526.3469848632812},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":450,"x_m":1858.986083984375,"y_m":-437.7680969238281},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":460,"x_m":1840.605712890625,"y_m":-349.189208984375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":470,"x_m":1822.225341796875,"y_m":-260.6103210449219},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":480,"x_m":1803.844970703125,"y_m":-172.03143310546875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":490,"x_m":1785.464599609375,"y_m":-83.45254516601562},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":500,"x_m":1767.084228515625,"y_m":5.126338005065918},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":510,"x_m":1748.703857421875,"y_m":93.7052230834961},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":520,"x_m":1730.323486328125,"y_m":182.2841033935547},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":530,"x_m":1711.9432373046875,"y_m":270.86297607421875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":540,"x_m":1693.5628662109375,"y_m":359.4418640136719},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":550,"x_m":1675.1824951171875,"y_m":448.020751953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":560,"x_m":1656.8021240234375,"y_m":536.5996704101562},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":570,"x_m":1638.4217529296875,"y_m":625.1785278320312},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":580,"x_m":1620.0413818359375,"y_m":713.7573852539062},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":590,"x_m":1601.6610107421875,"y_m":802.3363037109375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":600,"x_m":1583.2806396484375,"y_m":890.9151611328125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":610,"x_m":1564.9002685546875,"y_m":979.4940795898438},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":620,"x_m":1546.5198974609375,"y_m":1068.072998046875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":630,"x_m":1528.1395263671875,"y_m":1156.65185546875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":640,"x_m":1509.75927734375,"y_m":1245.230712890625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":650,"x_m":1491.37890625,"y_m":1333.8095703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":660,"x_m":1472.99853515625,"y_m":1422.388427734375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":670,"x_m":1454.6181640625,"y_m":1510.9674072265625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":680,"x_m":1436.23779296875,"y_m":1599.5462646484375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":690,"x_m":1417.857421875,"y_m":1688.1251220703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":700,"x_m":1399.47705078125,"y_m":1776.7039794921875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":710,"x_m":1381.0966796875,"y_m":1865.282958984375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":720,"x_m":1362.71630859375,"y_m":1953.86181640625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":730,"x_m":1344.3359375,"y_m":2042.440673828125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":740,"x_m":1325.95556640625,"y_m":2131.01953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":750,"x_m":1307.5751953125,"y_m":2219.598388671875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":760,"x_m":1289.1949462890625,"y_m":2308.17724609375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":770,"x_m":1270.8145751953125,"y_m":2396.756103515625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":780,"x_m":1252.4342041015625,"y_m":2485.335205078125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":790,"x_m":1234.0538330078125,"y_m":2573.9140625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":800,"x_m":1215.6734619140625,"y_m":2662.492919921875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":810,"x_m":1197.2930908203125,"y_m":2751.07177734375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":820,"x_m":1178.9127197265625,"y_m":2839.650634765625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":830,"x_m":1160.5323486328125,"y_m":2928.2294921875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":840,"x_m":1142.1519775390625,"y_m":3016.808349609375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":850,"x_m":1123.7716064453125,"y_m":3105.38720703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":860,"x_m":1105.3912353515625,"y_m":3193.966064453125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":870,"x_m":1087.010986328125,"y_m":3282.545166015625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":880,"x_m":1068.630615234375,"y_m":3371.1240234375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":890,"x_m":1050.250244140625,"y_m":3459.702880859375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":900,"x_m":1031.869873046875,"y_m":3548.28173828125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":910,"x_m":1013.489501953125,"y_m":3636.860595703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":920,"x_m":995.109130859375,"y_m":3725.439453125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":930,"x_m":976.728759765625,"y_m":3814.018310546875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":940,"x_m":958.348388671875,"y_m":3902.59716796875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":950,"x_m":939.968017578125,"y_m":3991.176025390625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":960,"x_m":921.5877075195312,"y_m":4079.755126953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":970,"x_m":903.2073364257812,"y_m":4168.333984375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":980,"x_m":884.8269653320312,"y_m":4256.91259765625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":990,"x_m":866.4465942382812,"y_m":4345.49169921875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1000,"x_m":848.0662231445312,"y_m":4434.0703125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1010,"x_m":829.6859130859375,"y_m":4522.6494140625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1020,"x_m":811.3055419921875,"y_m":4611.228515625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1030,"x_m":792.9251708984375,"y_m":4699.80712890625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1040,"x_m":774.5447998046875,"y_m":4788.38623046875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1050,"x_m":756.1644287109375,"y_m":4876.96484375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1060,"x_m":737.7840576171875,"y_m":4965.5439453125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1070,"x_m":719.4037475585938,"y_m":5054.12255859375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1080,"x_m":701.0233764648438,"y_m":5142.70166015625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1090,"x_m":682.6430053710938,"y_m":5231.2802734375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1100,"x_m":664.2626342773438,"y_m":5319.859375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1110,"x_m":645.8822631835938,"y_m":5408.4384765625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1120,"x_m":627.5018920898438,"y_m":5497.01708984375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1130,"x_m":609.12158203125,"y_m":5585.59619140625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1140,"x_m":590.7412109375,"y_m":5674.1748046875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1150,"x_m":572.36083984375,"y_m":5762.75390625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1160,"x_m":553.98046875,"y_m":5851.33251953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1170,"x_m":535.60009765625,"y_m":5939.91162109375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1180,"x_m":517.2197875976562,"y_m":6028.490234375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1190,"x_m":498.8393859863281,"y_m":6117.0693359375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1200,"x_m":480.45904541015625,"y_m":6205.6484375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1210,"x_m":462.07867431640625,"y_m":6294.22705078125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1220,"x_m":443.69830322265625,"y_m":6382.80615234375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1230,"x_m":425.3179626464844,"y_m":6471.384765625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1240,"x_m":406.9375915527344,"y_m":6559.9638671875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1250,"x_m":388.5572204589844,"y_m":6648.54248046875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1260,"x_m":370.1768798828125,"y_m":6737.12158203125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1270,"x_m":351.7965087890625,"y_m":6825.7001953125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1280,"x_m":333.4161682128906,"y_m":6914.279296875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1290,"x_m":315.0357971191406,"y_m":7002.8583984375},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1300,"x_m":296.6554260253906,"y_m":7091.43701171875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1310,"x_m":278.27508544921875,"y_m":7180.01611328125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1320,"x_m":259.89471435546875,"y_m":7268.5947265625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1330,"x_m":241.5143585205078,"y_m":7357.173828125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1340,"x_m":223.1339874267578,"y_m":7445.75244140625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1350,"x_m":204.75363159179688,"y_m":7534.33154296875},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1360,"x_m":186.37327575683594,"y_m":7622.91015625},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1370,"x_m":167.99290466308594,"y_m":7711.4892578125},{"course_rad":6.078586101531982,"speed_mps":9.046577453613281,"timestamp_s":1380,"x_m":149.612548828125,"y_m":7800.068359375}]}} diff --git a/packages/eval/test/golden/verdict.json b/packages/eval/test/golden/verdict.json index 2be8715b..65687cb6 100644 --- a/packages/eval/test/golden/verdict.json +++ b/packages/eval/test/golden/verdict.json @@ -16,7 +16,7 @@ "result": "pass" }, { - "detail": "hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2), leg-3 and second-turn (both directions, mid-arc and leg 3) cases match; t = 0 bearing and range match the drawn parameters, and ownship kinematics (step ≤ v·dt, chord heading between the stored courses, turn ≤ ω·dt per step, |turn| ≤ 180°) hold, on 4 scenarios", + "detail": "hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2), leg-3 and second-turn (both directions, mid-arc and leg 3) cases match; t = 0 bearing and range match the drawn parameters, and ownship kinematics (arc chord ≤ step ≤ v·dt, chord heading between the stored courses, turn ≤ ω·dt per step, summed turn = stored angle, |turn| ≤ 180°) hold, on 4 scenarios", "name": "independent-geometry", "result": "pass" }, diff --git a/packages/features/test/golden/seed1-2.features.bin b/packages/features/test/golden/seed1-2.features.bin index ed0917db221bc7be4aa02f080b5eebe94b40962a..d8434ac3e33c8a1dac21f333414134de2e94228a 100644 GIT binary patch delta 15 WcmaFT$@HX?X~TaOM#D|4PwD|W2L|2% delta 15 WcmaFT$@HX?X~TaOMuSbPPwD|W0tVdx diff --git a/packages/features/test/golden/seed1-4.derived.features.bin b/packages/features/test/golden/seed1-4.derived.features.bin index 958d147b186a6f402d2681bf9f354e02244978b6..1fa74378a03cbce5d0312e88fe816b91d01c9508 100644 GIT binary patch delta 20 bcmccdnB~S}mJQjgjE2oQtlM)~8FO?1U$_T` delta 20 bcmccdnB~S}mJQjgj0Vj)tlM)~8FO?1U# { // Spec 007 FR-006: carried / other phase, paired on the same rows, per model and metric. const paired = JSON.parse(readFileSync(join(exp, PAIRED_FILE), "utf8")) as PairedRecord[]; expect(paired.length).toBeGreaterThan(0); + expect(new Set(paired.map((r) => r.reference))).toEqual(new Set(["other-phase", "classical"])); for (const r of paired) { expect(r.slice).toBe("sigma=1deg"); expect(r.lo).toBeLessThanOrEqual(r.hi); diff --git a/packages/types/src/generated/tma.schema.json b/packages/types/src/generated/tma.schema.json index 78eaf80b..e615dc33 100644 --- a/packages/types/src/generated/tma.schema.json +++ b/packages/types/src/generated/tma.schema.json @@ -1705,5 +1705,5 @@ ], "title": "tma", "type": "object", - "version": "0.9.0" + "version": "0.9.1" } diff --git a/packages/types/src/generated/version.ts b/packages/types/src/generated/version.ts index 9d332f87..629ada9a 100644 --- a/packages/types/src/generated/version.ts +++ b/packages/types/src/generated/version.ts @@ -1,2 +1,2 @@ // GENERATED from schema/tma.yaml by packages/types/scripts/generate.ts. Do not edit. -export const SCHEMA_VERSION = "0.9.0"; +export const SCHEMA_VERSION = "0.9.1"; diff --git a/schema/tma.yaml b/schema/tma.yaml index f1f5a185..1cf2427e 100644 --- a/schema/tma.yaml +++ b/schema/tma.yaml @@ -6,7 +6,7 @@ description: >- principle I). Frame: local flat-earth Cartesian, x east, y north, metres, origin at ownship's window-start position. Bearings and courses are true, clockwise from north, radians in [0, 2π). Speeds in m/s. Times in seconds since window start. All stored floats are float32-rounded. -version: 0.9.0 +version: 0.9.1 license: Apache-2.0 prefixes: linkml: https://w3id.org/linkml/ diff --git a/specs/007-iteration-3-second-zig/spec.md b/specs/007-iteration-3-second-zig/spec.md index 617d8576..0961c900 100644 --- a/specs/007-iteration-3-second-zig/spec.md +++ b/specs/007-iteration-3-second-zig/spec.md @@ -89,22 +89,27 @@ seed-204 test split, with the 2c models beside it as the other phase. Then the a - **FR-004**: Generator 0.3.0 reads 0.1.0 and 0.2.0 datasets. `tma-train` requires a G0 on 0.3.0. - **FR-005**: The decision record MUST be committed before any iteration-3 weights are trained. `tma-g2` checks ancestry. -- **FR-006 (validation rule, B.3.3; amended after the G0 panel's statistics finding).** The rule - compares models **on the same rows of the new problem**. Comparing against 2c's iteration-2 - validation figures would credit the easier problem to the lever. The steps: - 1. Score both sets of models on seed 204's validation split, σ = 1°, zig + 600 s, rows common to - both: the iteration-3 models, and the 2c models, whose other-phase predictions - `tma-g2 --compare-models` already writes. - 2. **Primary condition (instantaneous model).** Take the paired bootstrap (eval's - `bootstrapMedianRatio`: 2,000 resamples, seed 20261011) of median speed error, iteration-3 - LSTM / 2c LSTM. Its 95% interval must lie wholly below 1. - 3. **Second condition.** The iteration-3 LSTM's G2 speed check on validation must not be a - fail: its interval's lower bound is under 1 kt. - - The time-late model is reported the same way but is not a condition. The carried pair goes to - the test split only if both conditions hold. Otherwise the iteration stops at validation and is - written up. The model / classical speed ratio on the same rows is reported beside, against 2c's - 2.37 on its own validation. +- **FR-006 (validation rule, B.3.3; amended after G0 panel rounds 1–2, statistics).** A lever + that changes the problem has no like-for-like counterfactual for B.3.3's "moved speed". 2c's + figures are on another problem. 2c scored on the new problem degrades out of distribution: on + scratch seed 9001 its LSTM reaches 1.93 kt here, against 1.49 kt on the iteration-2 ranges. The + rule is therefore: + 1. Score both sets on seed 204's validation split, σ = 1°, zig + 600 s, on rows common to both: + the iteration-3 models, and the 2c models as the other phase (`tma-g2 --compare-models`). It + writes `paired.json`, with paired bootstrap intervals from `bootstrapMedianRatio`, 2,000 + resamples, seed 20261011. + 2. **Operative condition.** The iteration-3 LSTM's G2 speed interval has a lower bound under + 1 kt, so a G2 pass is possible. + 3. **Sanity condition (weak reference).** The paired median speed ratio, iteration-3 LSTM over 2c + LSTM, has a 95% interval wholly below 1. 2c degrades on the new problem, so this shows only + that training on it helped, never that the lever worked. + 4. **Reported measure of what the models gained.** The paired median speed ratio, iteration-3 + LSTM over classical on the same rows, with its interval. It is set beside 2c's 2.37 (1.42 / + 0.60 kt) on its own validation. + + The time-late model is reported the same way, not as a condition. The carried pair goes to the + test split only if conditions 2 and 3 both hold. Otherwise the iteration stops at validation and + is written up. The write-up does not cite condition 3 as evidence that the lever worked. - **FR-007**: The carried set is the iteration-3 LSTM and BiLSTM, unconditionally. The 2c models are scored beside them on the same split, reported and never gated. - **FR-008**: Thresholds, slices, bootstrap and the gate rule are spec 003's as amended by the G2 @@ -114,7 +119,8 @@ seed-204 test split, with the 2c models beside it as the other phase. Then the a across-LOS turn is not the same population as iteration 2's. - **FR-009 (decision binding; G0 red team).** From iteration 3, `tma-g2 --split test` requires a committed decision whose `plan` names the plan being scored and whose `test_seed` matches it. An - earlier iteration's decision cannot unlock seed 204's test split. + earlier iteration's decision cannot unlock seed 204's test split. A decision binds by the plan's + path or by its content (a test seed of 204 or later), and must name its `test_seed`. ## Reading the gate under three legs (G0 panel, statistics and geometry) From b1e36fceaa24c0c9b358ced7aa04f8002f105a16 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 09:55:15 +0000 Subject: [PATCH 17/21] G0 round 3 at 928995a (checks after panel round 2); review.md moves to the folder of record Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- experiments/20261017-g0-i3-r2/verdict.json | 2 +- experiments/20261017-g0-i3-r3/manifest.json | 163 ++++++ experiments/20261017-g0-i3-r3/metrics.csv | 31 + experiments/20261017-g0-i3-r3/metrics.json | 542 ++++++++++++++++++ .../review.md | 50 +- experiments/20261017-g0-i3-r3/seeds.txt | 2 + experiments/20261017-g0-i3-r3/verdict.json | 32 ++ packages/models/test/g0-guard.test.ts | 4 +- 8 files changed, 818 insertions(+), 8 deletions(-) create mode 100644 experiments/20261017-g0-i3-r3/manifest.json create mode 100644 experiments/20261017-g0-i3-r3/metrics.csv create mode 100644 experiments/20261017-g0-i3-r3/metrics.json rename experiments/{20261017-g0-i3-r2 => 20261017-g0-i3-r3}/review.md (65%) create mode 100644 experiments/20261017-g0-i3-r3/seeds.txt create mode 100644 experiments/20261017-g0-i3-r3/verdict.json diff --git a/experiments/20261017-g0-i3-r2/verdict.json b/experiments/20261017-g0-i3-r2/verdict.json index a24cff0c..f61209d6 100644 --- a/experiments/20261017-g0-i3-r2/verdict.json +++ b/experiments/20261017-g0-i3-r2/verdict.json @@ -26,7 +26,7 @@ "result": "pass" } ], - "decided_by": "harness (pending agent panel review)", + "decided_by": "superseded: round-2 evidence of the iteration-3 G0 panel; the round-2 fixes changed the G0 checks (packages/eval), so G0 was re-run and the verdict of record is ../20261017-g0-i3-r3 (see its review.md)", "gate": "G0", "result": "pass" } diff --git a/experiments/20261017-g0-i3-r3/manifest.json b/experiments/20261017-g0-i3-r3/manifest.json new file mode 100644 index 00000000..96af08ac --- /dev/null +++ b/experiments/20261017-g0-i3-r3/manifest.json @@ -0,0 +1,163 @@ +{ + "command_lines": [ + "tma-generate --seed 42 --count 100000 --noise-free --ranges sha256:bdf46080f66c24c867245c482121a2f1b82d3f13e59ef8c0cabccde08d1172be --heldout-ranges sha256:7d9e444c99d8ac6f759d0226c1724a7ea19cb34237465730f4d1aed51de1aca1", + "tma-g0 --data data/g0-i3 --experiment experiments/20261017-g0-i3-r3" + ], + "created": "2026-10-17T09:54:35.957Z", + "datasets": [ + { + "digest": "f76097e8d6d3ee83aad97ef77722c62298fc33312f595ae5a39d9e1da107c6d0", + "header": { + "created_by": "tma-generate --seed 42 --count 100000 --noise-free --ranges sha256:bdf46080f66c24c867245c482121a2f1b82d3f13e59ef8c0cabccde08d1172be --heldout-ranges sha256:7d9e444c99d8ac6f759d0226c1724a7ea19cb34237465730f4d1aed51de1aca1", + "dataset_seed": "42", + "generator_git_commit": "90f4873b54cce6af72efadec5f1ed4f976c0008c", + "generator_git_dirty": false, + "generator_version": "0.3.0", + "index_end": 80000, + "index_start": 0, + "parameter_ranges": { + "dropout_fraction": { + "max": 0, + "min": 0 + }, + "dt_s": 10, + "los_offset_rad": { + "max": 1.5707963267948966, + "min": 0.5235987755982988 + }, + "min_range_m": 100, + "noise_sigma_rad_values": [ + 0 + ], + "ownship_speed_mps": { + "max": 7.716666666666667, + "min": 2.5722222222222224 + }, + "ownship_turn_rate_rad_s": { + "max": 0.05235987755982989, + "min": 0.008726646259971648 + }, + "range_distribution": "log_uniform", + "target_range0_m": { + "max": 20000, + "min": 2000 + }, + "target_speed_mps": { + "max": 10.28888888888889, + "min": 1.5433333333333334 + }, + "window_s": { + "max": 2400, + "min": 600 + }, + "zig2_delay_s": { + "max": 300, + "min": 180 + }, + "zig_angle_abs_rad": { + "max": 1.5707963267948966, + "min": 0.5235987755982988 + }, + "zig_fraction": { + "max": 0.6, + "min": 0.4 + }, + "zig_rule": "across_los" + }, + "record": "header", + "redraw_count": 17247, + "scenario_count": 80000, + "schema_version": "0.9.0", + "split": "train" + }, + "path": "data/g0-i3/train.f32pack" + }, + { + "digest": "ea38d17219a941d86c95f4323ca669d9f8f264dcc807d597c2229a5ac51c6564", + "header": { + "created_by": "tma-generate --seed 42 --count 100000 --noise-free --ranges sha256:bdf46080f66c24c867245c482121a2f1b82d3f13e59ef8c0cabccde08d1172be --heldout-ranges sha256:7d9e444c99d8ac6f759d0226c1724a7ea19cb34237465730f4d1aed51de1aca1", + "dataset_seed": "42", + "generator_git_commit": "90f4873b54cce6af72efadec5f1ed4f976c0008c", + "generator_git_dirty": false, + "generator_version": "0.3.0", + "index_end": 100000, + "index_start": 90000, + "parameter_ranges": { + "dropout_fraction": { + "max": 0, + "min": 0 + }, + "dt_s": 10, + "los_offset_rad": { + "max": 1.5707963267948966, + "min": 0.5235987755982988 + }, + "min_range_m": 100, + "noise_sigma_rad_values": [ + 0 + ], + "ownship_speed_mps": { + "max": 7.716666666666667, + "min": 2.5722222222222224 + }, + "ownship_turn_rate_rad_s": { + "max": 0.05235987755982989, + "min": 0.008726646259971648 + }, + "range_distribution": "log_uniform", + "target_range0_m": { + "max": 20000, + "min": 2000 + }, + "target_speed_mps": { + "max": 10.28888888888889, + "min": 1.5433333333333334 + }, + "window_s": { + "max": 2400, + "min": 600 + }, + "zig2_delay_s": { + "max": 300, + "min": 180 + }, + "zig_angle_abs_rad": { + "max": 1.5707963267948966, + "min": 0.5235987755982988 + }, + "zig_fraction": { + "max": 0.6, + "min": 0.4 + }, + "zig_rule": "across_los" + }, + "record": "header", + "redraw_count": 2052, + "scenario_count": 10000, + "schema_version": "0.9.0", + "split": "test" + }, + "path": "data/g0-i3/test.f32pack" + } + ], + "estimators": [ + { + "name": "naive", + "settings_json": "{\"fitted_on\":\"train\",\"means\":{\"course_cos\":-0.0010852328029513894,\"course_sin\":-0.0022651612364012388,\"log_range_km\":2.27659913273481,\"n\":80000,\"speed_mps\":5.9123806832298635}}" + }, + { + "name": "classical", + "settings_json": "{\"max_iterations\":50,\"method\":\"levenberg-marquardt\",\"mode\":\"final\"}" + }, + { + "name": "classical:per_timestep", + "settings_json": "{\"grid_every\":6,\"max_iterations\":50,\"method\":\"levenberg-marquardt\",\"mode\":\"per_timestep\",\"note\":\"first scenarios of the test split, for check classical-per-timestep-recovers\",\"scenarios\":1000}" + } + ], + "generator_version": "0.3.0", + "git_commit": "928995a3d2e2fc8000b97ba732b2672527eb233f", + "git_dirty": false, + "id": "20261017-g0-i3-r3", + "node_version": "v22.22.0", + "schema_version": "0.9.1" +} diff --git a/experiments/20261017-g0-i3-r3/metrics.csv b/experiments/20261017-g0-i3-r3/metrics.csv new file mode 100644 index 00000000..01546b9f --- /dev/null +++ b/experiments/20261017-g0-i3-r3/metrics.csv @@ -0,0 +1,31 @@ +estimator,slice,point,n,excluded,range_rel_median,range_rel_p90,course_deg_median,course_deg_p90,speed_kt_median,speed_kt_p90,gross_error_rate,non_converged_rate,position_m_mean,position_window_m_median,position_window_m_p90 +naive,overall,final,10000,0,0.41296159627504475,1.5327997889522416,89.45474167284688,162.58337096507148,4.243544316961235,7.647355148653232,0.3878,0,5452.883076396702,4401.405811239404,11507.173492769414 +naive,dropout_band=0-5pct,final,10000,0,0.41296159627504475,1.5327997889522416,89.45474167284688,162.58337096507148,4.243544316961235,7.647355148653232,0.3878,0,5452.883076396702,4401.405811239404,11507.173492769414 +naive,range_band=0-2km,final,133,0,3.554155199769074,10.189099437703875,90.76955308807433,167.12415266965337,4.888747783767997,7.68978966492556,1,0,7541.811836895824,7604.0155201139005,8872.685837083234 +naive,range_band=10-20km,final,3634,0,0.4051582349171333,0.5659043446436742,89.08235975929779,161.3485137442682,4.333764790973704,7.650062383894786,0.25949367088607594,0,6745.622183900448,6441.378841890848,12363.699782051417 +naive,range_band=2-5km,final,1863,0,1.090394363945709,3.7852475362152567,91.6602780577357,163.413635327571,4.118415500896281,7.688682949568745,0.775630703166935,0,4947.390624738301,5082.411505237025,7707.336892376554 +naive,range_band=20-30km,final,515,0,0.634768967095896,0.7013337367422446,87.71670441588876,163.07976179319132,4.6361965443094135,7.529825268244897,0.9825242718446602,0,17343.277986636796,16934.112145062863,22879.839699095115 +naive,range_band=30+km,final,9,0,0.7485700238919062,0.7642858167300461,106.55888773201448,157.09385230648385,3.79518092837241,8.285669219674096,1,0,28713.815931989186,29008.802337317957,31592.53709787277 +naive,range_band=5-10km,final,3846,0,0.24299138820601726,1.0084669091137104,89.37135872345937,162.82039583211494,4.159294451802132,7.6459358371874675,0.218928757150286,0,2757.4044547509648,2380.8681528827433,5669.739080810058 +naive,sigma=0deg,final,10000,0,0.41296159627504475,1.5327997889522416,89.45474167284688,162.58337096507148,4.243544316961235,7.647355148653232,0.3878,0,5452.883076396702,4401.405811239404,11507.173492769414 +naive,zig_band=120-150deg,final,3350,0,0.41814549189676226,1.4205594803025299,91.70037126816366,162.25843837568647,4.284804222650713,7.6459358371874675,0.38985074626865673,0,5616.710800217959,4563.653024001928,12066.507695840213 +naive,zig_band=150-180deg,final,4019,0,0.4175174029873284,1.5933215071523037,89.34677004244732,163.1154153806588,4.2366588604887925,7.675463077059059,0.39462552873849216,0,5637.500113282189,4494.309434812314,11926.065519513926 +naive,zig_band=30-50deg,final,84,0,0.4614850895772321,1.6337174892005477,82.8358375746193,162.83246477637834,3.8864580652883705,7.198060923467186,0.4642857142857143,0,4648.201741165161,4436.822024701133,8943.548089216078 +naive,zig_band=50-70deg,final,319,0,0.39053573219917426,1.700969404255579,93.63977616223211,164.3109514297814,4.118990177212214,7.61714084617005,0.3667711598746082,0,4489.542019678022,3594.839080360501,8942.730785294072 +naive,zig_band=70-90deg,final,501,0,0.37249923148578495,1.6897176776549168,88.19526845027487,165.7484057217456,4.185012607286095,7.589079494352731,0.3333333333333333,0,4447.1342740142945,3821.0031499270726,8588.865422100125 +naive,zig_band=90-120deg,final,1727,0,0.4050221004673685,1.5631300243029322,86.56928727683997,160.2920542343965,4.236357155422927,7.6209944216273,0.38390272148233934,0,5214.307762674768,4283.707052404278,10806.868545155921 +classical,overall,final,10000,0,9.625246868512277e-8,4.812320236817345e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0027786471935062848,0.0009841623227903502,0.006905339660024878 +classical,dropout_band=0-5pct,final,10000,0,9.625246868512277e-8,4.812320236817345e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0027786471935062848,0.0009841623227903502,0.006905339660024878 +classical,range_band=0-2km,final,133,0,4.794212208460275e-8,1.6569781425015622e-7,0,0,0,9.268972837641738e-7,0,0,0.00013530523967983556,0.00006103515625,0.00048828125 +classical,range_band=10-20km,final,3634,0,1.3944097671445805e-7,6.497986438982158e-7,0,0.00005464151336010202,0.0000018537945675283476,0.000013903459256462607,0,0,0.004558875236756464,0.0021836601342771385,0.010708843847393217 +classical,range_band=2-5km,final,1863,0,6.045721709550033e-8,2.276486079566025e-7,0,0.000005122641877509563,2.3172432094104345e-7,0.0000018537945675283476,0,0,0.00043650168202587875,0.000244140625,0.0010429692071920813 +classical,range_band=20-30km,final,515,0,1.990455683591539e-7,8.591447716087028e-7,0.0000034150945850063763,0.00005464151336010202,0.0000055613837025850425,0.000027806918512925215,0,0,0.009508762893649318,0.005859375,0.02344258571124571 +classical,range_band=30+km,final,9,0,6.851410989829762e-8,0.0000011607263146006765,0.0000034150945850063763,0.000040981135020076506,0.0000037075891350566953,0.00005376004245832208,0,0,0.00874825760899502,0.0021836601342771385,0.0453970285123807 +classical,range_band=5-10km,final,3846,0,8.70445563787829e-8,3.6632019879375324e-7,0,0.000025613209387547814,9.268972837641738e-7,0.0000055613837025850425,0,0,0.0014073255032468878,0.0009765625,0.0034375312110177547 +classical,sigma=0deg,final,10000,0,9.625246868512277e-8,4.812320236817345e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0027786471935062848,0.0009841623227903502,0.006905339660024878 +classical,zig_band=120-150deg,final,3350,0,9.259584615706119e-8,4.7333682342916056e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0027263890771233945,0.0009841623227903502,0.006842746068748117 +classical,zig_band=150-180deg,final,4019,0,9.512052121246996e-8,4.70733436301668e-7,0,0.00002732075668005101,9.268972837641738e-7,0.000009268972837641738,0,0,0.0028004878106140706,0.0009795381985903341,0.007109482313750507 +classical,zig_band=30-50deg,final,84,0,1.1411674223616281e-7,5.385067567064481e-7,0,0.000040981135020076506,0.0000018537945675283476,0.000011122767405170085,0,0,0.005687213290986321,0.0009765625,0.006176323555016366 +classical,zig_band=50-70deg,final,319,0,1.1695404891416201e-7,5.556545196015425e-7,0,0.00002732075668005101,0.0000018537945675283476,0.000009268972837641738,0,0,0.0025582287232674573,0.001078527651798846,0.007814645472616538 +classical,zig_band=70-90deg,final,501,0,1.0593828399940142e-7,5.632217038077209e-7,0,0.000040981135020076506,0.0000013903459256462606,0.000011122767405170085,0,0,0.0029160767709236514,0.0010066175843793117,0.007042092334890605 +classical,zig_band=90-120deg,final,1727,0,9.884345867216302e-8,4.662596728109262e-7,0,0.00002732075668005101,0.0000013903459256462606,0.000008342075553877564,0,0,0.0026885655323933434,0.0010066175843793117,0.006253051013118016 diff --git a/experiments/20261017-g0-i3-r3/metrics.json b/experiments/20261017-g0-i3-r3/metrics.json new file mode 100644 index 00000000..99cd210b --- /dev/null +++ b/experiments/20261017-g0-i3-r3/metrics.json @@ -0,0 +1,542 @@ +[ + { + "course_deg_median": 89.45474167284688, + "course_deg_p90": 162.58337096507148, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3878, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5452.883076396702, + "position_window_m_median": 4401.405811239404, + "position_window_m_p90": 11507.173492769414, + "range_rel_median": 0.41296159627504475, + "range_rel_p90": 1.5327997889522416, + "slice": "overall", + "speed_kt_median": 4.243544316961235, + "speed_kt_p90": 7.647355148653232 + }, + { + "course_deg_median": 89.45474167284688, + "course_deg_p90": 162.58337096507148, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3878, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5452.883076396702, + "position_window_m_median": 4401.405811239404, + "position_window_m_p90": 11507.173492769414, + "range_rel_median": 0.41296159627504475, + "range_rel_p90": 1.5327997889522416, + "slice": "dropout_band=0-5pct", + "speed_kt_median": 4.243544316961235, + "speed_kt_p90": 7.647355148653232 + }, + { + "course_deg_median": 90.76955308807433, + "course_deg_p90": 167.12415266965337, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 1, + "n": 133, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 7541.811836895824, + "position_window_m_median": 7604.0155201139005, + "position_window_m_p90": 8872.685837083234, + "range_rel_median": 3.554155199769074, + "range_rel_p90": 10.189099437703875, + "slice": "range_band=0-2km", + "speed_kt_median": 4.888747783767997, + "speed_kt_p90": 7.68978966492556 + }, + { + "course_deg_median": 89.08235975929779, + "course_deg_p90": 161.3485137442682, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.25949367088607594, + "n": 3634, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 6745.622183900448, + "position_window_m_median": 6441.378841890848, + "position_window_m_p90": 12363.699782051417, + "range_rel_median": 0.4051582349171333, + "range_rel_p90": 0.5659043446436742, + "slice": "range_band=10-20km", + "speed_kt_median": 4.333764790973704, + "speed_kt_p90": 7.650062383894786 + }, + { + "course_deg_median": 91.6602780577357, + "course_deg_p90": 163.413635327571, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.775630703166935, + "n": 1863, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 4947.390624738301, + "position_window_m_median": 5082.411505237025, + "position_window_m_p90": 7707.336892376554, + "range_rel_median": 1.090394363945709, + "range_rel_p90": 3.7852475362152567, + "slice": "range_band=2-5km", + "speed_kt_median": 4.118415500896281, + "speed_kt_p90": 7.688682949568745 + }, + { + "course_deg_median": 87.71670441588876, + "course_deg_p90": 163.07976179319132, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.9825242718446602, + "n": 515, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 17343.277986636796, + "position_window_m_median": 16934.112145062863, + "position_window_m_p90": 22879.839699095115, + "range_rel_median": 0.634768967095896, + "range_rel_p90": 0.7013337367422446, + "slice": "range_band=20-30km", + "speed_kt_median": 4.6361965443094135, + "speed_kt_p90": 7.529825268244897 + }, + { + "course_deg_median": 106.55888773201448, + "course_deg_p90": 157.09385230648385, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 1, + "n": 9, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 28713.815931989186, + "position_window_m_median": 29008.802337317957, + "position_window_m_p90": 31592.53709787277, + "range_rel_median": 0.7485700238919062, + "range_rel_p90": 0.7642858167300461, + "slice": "range_band=30+km", + "speed_kt_median": 3.79518092837241, + "speed_kt_p90": 8.285669219674096 + }, + { + "course_deg_median": 89.37135872345937, + "course_deg_p90": 162.82039583211494, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.218928757150286, + "n": 3846, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 2757.4044547509648, + "position_window_m_median": 2380.8681528827433, + "position_window_m_p90": 5669.739080810058, + "range_rel_median": 0.24299138820601726, + "range_rel_p90": 1.0084669091137104, + "slice": "range_band=5-10km", + "speed_kt_median": 4.159294451802132, + "speed_kt_p90": 7.6459358371874675 + }, + { + "course_deg_median": 89.45474167284688, + "course_deg_p90": 162.58337096507148, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3878, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5452.883076396702, + "position_window_m_median": 4401.405811239404, + "position_window_m_p90": 11507.173492769414, + "range_rel_median": 0.41296159627504475, + "range_rel_p90": 1.5327997889522416, + "slice": "sigma=0deg", + "speed_kt_median": 4.243544316961235, + "speed_kt_p90": 7.647355148653232 + }, + { + "course_deg_median": 91.70037126816366, + "course_deg_p90": 162.25843837568647, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.38985074626865673, + "n": 3350, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5616.710800217959, + "position_window_m_median": 4563.653024001928, + "position_window_m_p90": 12066.507695840213, + "range_rel_median": 0.41814549189676226, + "range_rel_p90": 1.4205594803025299, + "slice": "zig_band=120-150deg", + "speed_kt_median": 4.284804222650713, + "speed_kt_p90": 7.6459358371874675 + }, + { + "course_deg_median": 89.34677004244732, + "course_deg_p90": 163.1154153806588, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.39462552873849216, + "n": 4019, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5637.500113282189, + "position_window_m_median": 4494.309434812314, + "position_window_m_p90": 11926.065519513926, + "range_rel_median": 0.4175174029873284, + "range_rel_p90": 1.5933215071523037, + "slice": "zig_band=150-180deg", + "speed_kt_median": 4.2366588604887925, + "speed_kt_p90": 7.675463077059059 + }, + { + "course_deg_median": 82.8358375746193, + "course_deg_p90": 162.83246477637834, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.4642857142857143, + "n": 84, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 4648.201741165161, + "position_window_m_median": 4436.822024701133, + "position_window_m_p90": 8943.548089216078, + "range_rel_median": 0.4614850895772321, + "range_rel_p90": 1.6337174892005477, + "slice": "zig_band=30-50deg", + "speed_kt_median": 3.8864580652883705, + "speed_kt_p90": 7.198060923467186 + }, + { + "course_deg_median": 93.63977616223211, + "course_deg_p90": 164.3109514297814, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3667711598746082, + "n": 319, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 4489.542019678022, + "position_window_m_median": 3594.839080360501, + "position_window_m_p90": 8942.730785294072, + "range_rel_median": 0.39053573219917426, + "range_rel_p90": 1.700969404255579, + "slice": "zig_band=50-70deg", + "speed_kt_median": 4.118990177212214, + "speed_kt_p90": 7.61714084617005 + }, + { + "course_deg_median": 88.19526845027487, + "course_deg_p90": 165.7484057217456, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.3333333333333333, + "n": 501, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 4447.1342740142945, + "position_window_m_median": 3821.0031499270726, + "position_window_m_p90": 8588.865422100125, + "range_rel_median": 0.37249923148578495, + "range_rel_p90": 1.6897176776549168, + "slice": "zig_band=70-90deg", + "speed_kt_median": 4.185012607286095, + "speed_kt_p90": 7.589079494352731 + }, + { + "course_deg_median": 86.56928727683997, + "course_deg_p90": 160.2920542343965, + "estimator": "naive", + "excluded": 0, + "gross_error_rate": 0.38390272148233934, + "n": 1727, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 5214.307762674768, + "position_window_m_median": 4283.707052404278, + "position_window_m_p90": 10806.868545155921, + "range_rel_median": 0.4050221004673685, + "range_rel_p90": 1.5631300243029322, + "slice": "zig_band=90-120deg", + "speed_kt_median": 4.236357155422927, + "speed_kt_p90": 7.6209944216273 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0027786471935062848, + "position_window_m_median": 0.0009841623227903502, + "position_window_m_p90": 0.006905339660024878, + "range_rel_median": 9.625246868512277e-8, + "range_rel_p90": 4.812320236817345e-7, + "slice": "overall", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0027786471935062848, + "position_window_m_median": 0.0009841623227903502, + "position_window_m_p90": 0.006905339660024878, + "range_rel_median": 9.625246868512277e-8, + "range_rel_p90": 4.812320236817345e-7, + "slice": "dropout_band=0-5pct", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 133, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.00013530523967983556, + "position_window_m_median": 0.00006103515625, + "position_window_m_p90": 0.00048828125, + "range_rel_median": 4.794212208460275e-8, + "range_rel_p90": 1.6569781425015622e-7, + "slice": "range_band=0-2km", + "speed_kt_median": 0, + "speed_kt_p90": 9.268972837641738e-7 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00005464151336010202, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 3634, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.004558875236756464, + "position_window_m_median": 0.0021836601342771385, + "position_window_m_p90": 0.010708843847393217, + "range_rel_median": 1.3944097671445805e-7, + "range_rel_p90": 6.497986438982158e-7, + "slice": "range_band=10-20km", + "speed_kt_median": 0.0000018537945675283476, + "speed_kt_p90": 0.000013903459256462607 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.000005122641877509563, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 1863, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.00043650168202587875, + "position_window_m_median": 0.000244140625, + "position_window_m_p90": 0.0010429692071920813, + "range_rel_median": 6.045721709550033e-8, + "range_rel_p90": 2.276486079566025e-7, + "slice": "range_band=2-5km", + "speed_kt_median": 2.3172432094104345e-7, + "speed_kt_p90": 0.0000018537945675283476 + }, + { + "course_deg_median": 0.0000034150945850063763, + "course_deg_p90": 0.00005464151336010202, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 515, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.009508762893649318, + "position_window_m_median": 0.005859375, + "position_window_m_p90": 0.02344258571124571, + "range_rel_median": 1.990455683591539e-7, + "range_rel_p90": 8.591447716087028e-7, + "slice": "range_band=20-30km", + "speed_kt_median": 0.0000055613837025850425, + "speed_kt_p90": 0.000027806918512925215 + }, + { + "course_deg_median": 0.0000034150945850063763, + "course_deg_p90": 0.000040981135020076506, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 9, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.00874825760899502, + "position_window_m_median": 0.0021836601342771385, + "position_window_m_p90": 0.0453970285123807, + "range_rel_median": 6.851410989829762e-8, + "range_rel_p90": 0.0000011607263146006765, + "slice": "range_band=30+km", + "speed_kt_median": 0.0000037075891350566953, + "speed_kt_p90": 0.00005376004245832208 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.000025613209387547814, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 3846, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0014073255032468878, + "position_window_m_median": 0.0009765625, + "position_window_m_p90": 0.0034375312110177547, + "range_rel_median": 8.70445563787829e-8, + "range_rel_p90": 3.6632019879375324e-7, + "slice": "range_band=5-10km", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.0000055613837025850425 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 10000, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0027786471935062848, + "position_window_m_median": 0.0009841623227903502, + "position_window_m_p90": 0.006905339660024878, + "range_rel_median": 9.625246868512277e-8, + "range_rel_p90": 4.812320236817345e-7, + "slice": "sigma=0deg", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 3350, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0027263890771233945, + "position_window_m_median": 0.0009841623227903502, + "position_window_m_p90": 0.006842746068748117, + "range_rel_median": 9.259584615706119e-8, + "range_rel_p90": 4.7333682342916056e-7, + "slice": "zig_band=120-150deg", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 4019, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0028004878106140706, + "position_window_m_median": 0.0009795381985903341, + "position_window_m_p90": 0.007109482313750507, + "range_rel_median": 9.512052121246996e-8, + "range_rel_p90": 4.70733436301668e-7, + "slice": "zig_band=150-180deg", + "speed_kt_median": 9.268972837641738e-7, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.000040981135020076506, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 84, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.005687213290986321, + "position_window_m_median": 0.0009765625, + "position_window_m_p90": 0.006176323555016366, + "range_rel_median": 1.1411674223616281e-7, + "range_rel_p90": 5.385067567064481e-7, + "slice": "zig_band=30-50deg", + "speed_kt_median": 0.0000018537945675283476, + "speed_kt_p90": 0.000011122767405170085 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 319, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0025582287232674573, + "position_window_m_median": 0.001078527651798846, + "position_window_m_p90": 0.007814645472616538, + "range_rel_median": 1.1695404891416201e-7, + "range_rel_p90": 5.556545196015425e-7, + "slice": "zig_band=50-70deg", + "speed_kt_median": 0.0000018537945675283476, + "speed_kt_p90": 0.000009268972837641738 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.000040981135020076506, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 501, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0029160767709236514, + "position_window_m_median": 0.0010066175843793117, + "position_window_m_p90": 0.007042092334890605, + "range_rel_median": 1.0593828399940142e-7, + "range_rel_p90": 5.632217038077209e-7, + "slice": "zig_band=70-90deg", + "speed_kt_median": 0.0000013903459256462606, + "speed_kt_p90": 0.000011122767405170085 + }, + { + "course_deg_median": 0, + "course_deg_p90": 0.00002732075668005101, + "estimator": "classical", + "excluded": 0, + "gross_error_rate": 0, + "n": 1727, + "non_converged_rate": 0, + "point": "final", + "position_m_mean": 0.0026885655323933434, + "position_window_m_median": 0.0010066175843793117, + "position_window_m_p90": 0.006253051013118016, + "range_rel_median": 9.884345867216302e-8, + "range_rel_p90": 4.662596728109262e-7, + "slice": "zig_band=90-120deg", + "speed_kt_median": 0.0000013903459256462606, + "speed_kt_p90": 0.000008342075553877564 + } +] diff --git a/experiments/20261017-g0-i3-r2/review.md b/experiments/20261017-g0-i3-r3/review.md similarity index 65% rename from experiments/20261017-g0-i3-r2/review.md rename to experiments/20261017-g0-i3-r3/review.md index c776c211..03215968 100644 --- a/experiments/20261017-g0-i3-r2/review.md +++ b/experiments/20261017-g0-i3-r3/review.md @@ -13,16 +13,20 @@ across-the-line-of-sight rule (`zig_rule: across_los`) and an optional second zi **Evidence.** - Round 1: `../20261017-g0-i3/` at c804757. -- Round 2: this folder, at 5989b3e. It carries the round-1 fixes to the G0 checks and the zig - bands up to 180°. The generator source (`packages/sim/src`, `packages/data/src`) is unchanged - from 90f4873, so the same data stand. +- Round 2: `../20261017-g0-i3-r2/` at 5989b3e. It carries the round-1 fixes to the G0 checks and + the zig bands up to 180°. +- Round 3: this folder, at 928995a. It carries the round-2 fixes: the step lower bound and the + turn sums. Metrics and seeds are byte-identical to round 2. -| Check | Round 1 (c804757) | Round 2 (5989b3e) | +The generator source (`packages/sim/src`, `packages/data/src`) is unchanged from 90f4873, so the +same data stand throughout. + +| Check | Round 1 (c804757) | Rounds 2–3 (5989b3e, 928995a) | | --- | --- | --- | | classical-recovers-noise-free-truth | pass; worst range 8.70e-6, course 2.54e-3°, speed 3.43e-4 kt | same | | classical-per-timestep-recovers | pass; 67,208/67,208 rows ≥ 60 s after the zig | same | | bearing-residuals-at-float32-precision | pass; worst 2.77e-7 rad | same | -| independent-geometry | pass; hand cases incl. leg 3; 20,000 turns across the line of sight | pass; adds the second turn in both directions (mid-arc and leg 3), ownship kinematics on every scenario, \|turn\| ≤ 180°, unchecked turns counted | +| independent-geometry | pass; hand cases incl. leg 3; 20,000 turns across the line of sight | pass; adds the second turn in both directions (mid-arc and leg 3), ownship kinematics on every scenario (round 3: also arc chord ≤ step and summed turn = stored angle), \|turn\| ≤ 180°, unchecked turns counted | | naive-baseline-reproduces | pass | pass | ## Round 1 (2026-10-17) @@ -64,3 +68,39 @@ across-the-line-of-sight rule (`zig_rule: across_los`) and an optional second zi | The manifest's `tma-generate` line has no `--out`, and the data and G0 commits differ (reproducibility) | The data are identified by their sha256 in the manifest, and `generator_git_commit` (90f4873) is in the dataset header. The lineage is: data at 90f4873, G0 round 1 at c804757, round 2 at 5989b3e, with no generator change between them | | G0 does not score the held-out split (red team) | As in iterations 1–2. The geometry reviewer integrated the held-out split independently: worst 6.7e-4 m, 0 index mismatches | | Leakage: the post-turn course bounds the true bearing to a 60° band. It cuts the RMS bearing error at the turn sample by 1.5% (1 bearing, σ = 1°) to 0.5% (10 bearings); turn timing and redraws reveal no range, course or speed (red team) | Negligible; recorded | + +## Round 2 (2026-10-17) + +The reviewers re-checked their round-1 findings at c648455 against the round-2 evidence (G0 at +5989b3e). + +| Reviewer | Verdict | +| --- | --- | +| TMA geometry: own RK4 tracks pass the new checks; mutations: long-way turn with matching \|zig\| and leg-3 course +5° fail, long-way turn with the short angle stored and leg 3 at 0.98 v pass | approve-with-findings | +| Estimation and numerics: new checks' margins on all four splits (110,000 scenarios): step 50×, course rate 21×, chord bracket 11× | approve | +| Evaluation statistics: FR-006 condition against 2c is a weak reference, since 2c degrades on the new problem | approve-with-findings | +| Reproducibility: round 2 re-run from its manifest byte-identical; full suite 320/320; guard accepts round 2 only once panel-decided | approve-with-findings | +| Red team: round-1 survivors M1b, M5b and M2b now fail G0, and M4 fails the σ = 2° unit test; new M7 (second turn at half rate) fails; new M8 (leg 3 at 0.9 v) is caught only by the hand case; decision-binding bypass attempts stopped | approve-with-findings | + +### Major finding and response + +| # | Finding (reviewer) | Response | Commit | +| --- | --- | --- | --- | +| M6 | FR-006 condition 1 (iteration-3 LSTM beats 2c on the same rows) compares with a reference that degrades on the new problem. On scratch seed 9001, 2c's LSTM is at 1.93 kt here against 1.49 kt on the iteration-2 ranges, so a model worse than 2c on its own problem would pass (statistics) | Spec 007 FR-006 and the decision record now say that a change to the problem has no like-for-like counterfactual. The **operative** condition is a possible G2 speed pass: the iteration-3 LSTM's validation interval has a lower bound under 1 kt. Beating 2c on the same rows stays as a **sanity** condition, labelled a weak reference and never cited as evidence that the lever worked. The measure of what the models gained is the paired ratio of model to classical speed error on the same rows, with its interval, against 2c's 2.37. `paired.json` now carries it | 928995a | + +### Minor findings and responses + +| Finding (reviewers) | Response | +| --- | --- | +| No lower bound on step length: a slower ownship (0.98 v, 0.9 v, frozen) passes the per-scenario check (geometry, red team, numerics) | Each step must be at least the full-step arc chord, 2(v/ω) sin(ω dt/2), which is v·dt on a leg (3df0750). No false failures on the four splits; tests added | +| A turn flown the other way round to the same course, with the short angle stored, passes (geometry) | Each rate-limited turn's summed course change must equal its stored signed angle, to within 1e-4 rad (3df0750); test added | +| Decision binding keyed on the plan's path: a copy elsewhere, or another spelling, escaped it; a bound decision without `test_seed` passed (red team, reproducibility, statistics) | A decision binds by the plan's path or its content (a test seed of 204 or later). Paths are compared resolved, and a bound decision must name its `test_seed` (3df0750); tests added. The train-plan seed ownership was already a second layer | +| The planned gate command needs `stalled: true` because `--compare-models` implies a phase B (red team) | The decision record sets `stalled: true` and says why: the previous configuration (2c) failed G2 | +| `tma-model-predict` plus `tma-eval` can score seed 204's test split outside the gate (red team) | Known limit since iteration 2: only the gate CLIs check the decision. The gate of record is `tma-g2` with the committed decision; any other scoring is not a gate result | +| The schema changed under the same version (reproducibility) | Bumped to 0.9.1; the goldens change only in `schema_version` (928995a) | +| The round-1 folder's verdict still read "pending" (reproducibility) | Marked superseded (928995a); round 2 likewise, once round 3 ran | +| `pnpm test` timed out once under load (train.test.ts, time-late resume) (reproducibility) | Load-dependent; it passes alone and on a re-run (320/320, then 323/323 at 928995a) | + +**G0 round 3.** G0 was re-run at 928995a (this folder) so the verdict's checks are the code of +record. The kinematic and turn-sum checks pass on all 10,000 test scenarios; the data and every +other figure are unchanged. diff --git a/experiments/20261017-g0-i3-r3/seeds.txt b/experiments/20261017-g0-i3-r3/seeds.txt new file mode 100644 index 00000000..feb4bb1f --- /dev/null +++ b/experiments/20261017-g0-i3-r3/seeds.txt @@ -0,0 +1,2 @@ +train seed=42 index=[0, 80000) data/g0-i3/train.f32pack +test seed=42 index=[90000, 100000) data/g0-i3/test.f32pack diff --git a/experiments/20261017-g0-i3-r3/verdict.json b/experiments/20261017-g0-i3-r3/verdict.json new file mode 100644 index 00000000..aa18d088 --- /dev/null +++ b/experiments/20261017-g0-i3-r3/verdict.json @@ -0,0 +1,32 @@ +{ + "checks": [ + { + "detail": "10000/10000 scenarios within tolerance (range 0.001, course 0.1°, speed 0.01 kt); worst range 8.70e-6, course 2.54e-3°, speed 3.43e-4 kt", + "name": "classical-recovers-noise-free-truth", + "result": "pass" + }, + { + "detail": "1000 scenarios, 72208 rows: 0 non-converged; 67208/67208 rows ≥ 60 s after the zig within tolerance (worst range 9.78e-6); worst range in the first 60 s 1.92e-5 (reported, not gated)", + "name": "classical-per-timestep-recovers", + "result": "pass" + }, + { + "detail": "10000/10000 final solutions with RMS bearing residual ≤ 0.000001 rad; worst 2.77e-7 rad", + "name": "bearing-residuals-at-float32-precision", + "result": "pass" + }, + { + "detail": "hand-computed 315°, abeam, leg-2, turn (starboard and port, mid-turn and leg 2), leg-3 and second-turn (both directions, mid-arc and leg 3) cases match; t = 0 bearing and range match the drawn parameters, and ownship kinematics (arc chord ≤ step ≤ v·dt, chord heading between the stored courses, turn ≤ ω·dt per step, summed turn = stored angle, |turn| ≤ 180°) hold, on 10000 scenarios; 20000 turns put the target on the other bow inside the drawn offset range", + "name": "independent-geometry", + "result": "pass" + }, + { + "detail": "two in-process naive fits gave identical metrics (determinism only; cross-process and cross-commit reproduction is shown by re-running G0 from the manifest)", + "name": "naive-baseline-reproduces", + "result": "pass" + } + ], + "decided_by": "harness (pending agent panel review)", + "gate": "G0", + "result": "pass" +} diff --git a/packages/models/test/g0-guard.test.ts b/packages/models/test/g0-guard.test.ts index 9459f009..1541c2f6 100644 --- a/packages/models/test/g0-guard.test.ts +++ b/packages/models/test/g0-guard.test.ts @@ -139,9 +139,9 @@ describe("G0 guard (constitution V)", () => { it("the committed iteration-3 G0 (generator 0.3.0) is the latest, and needs its panel review", () => { // Generator 0.3.0 (spec 007) no longer accepts the iteration-2 G0 (20261013-g0-i2-r2, 0.2.0). // G0 ran on 0.3.0 at c804757 and, after the round-1 panel fixes to its checks, again at - // 5989b3e (round 2); the generator source HEAD still matches. + // 5989b3e (round 2) and 928995a (round 3); the generator source HEAD still matches. const dir = new URL("../../../experiments", import.meta.url).pathname; - const want = { id: "20261017-g0-i3-r2", commit: "5989b3edfdf080e8867e5b6987570597837c2acd" }; + const want = { id: "20261017-g0-i3-r3", commit: "928995a3d2e2fc8000b97ba732b2672527eb233f" }; expect(g0Accepted(dir, { requireReview: false })).toEqual(want); const verdict = JSON.parse(readFileSync(join(dir, want.id, "verdict.json"), "utf8")) as { decided_by: string; From 0df0df221b145cb51bef09db036439d64924ac5b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 10:00:26 +0000 Subject: [PATCH 18/21] tma-g2: a decision binds by the test data's own seed too (G0 red team round 3) A forged plan copy with another seed can no longer unbind the seed-204 test split; a bound decision's test_seed must match the data's header. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- packages/models/src/g2.ts | 26 +++++++++++++++++++++- packages/models/test/g2.test.ts | 28 ++++++++++++++++++++++++ specs/007-iteration-3-second-zig/spec.md | 4 ++-- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/models/src/g2.ts b/packages/models/src/g2.ts index 28816c8a..ecf6b6c1 100644 --- a/packages/models/src/g2.ts +++ b/packages/models/src/g2.ts @@ -133,6 +133,17 @@ function samePath(a: string, b: string): boolean { } } +/** `dataset_seed` from the header of `/test.f32pack`; undefined when absent or unreadable. */ +function dataTestSeed(data: string): string | undefined { + const path = join(data, "test.f32pack"); + if (!existsSync(path)) return undefined; + try { + return readDataset(path).header.dataset_seed; + } catch { + return undefined; + } +} + /** The plan's test seed (its fresh test split's, else its dataset's); undefined if unreadable. */ function testSeedOf(plan: string | undefined): string | undefined { if (plan === undefined || !existsSync(plan)) return undefined; @@ -285,10 +296,16 @@ export function requireDecision(opts: G2RunOptions): ChangeDecision | undefined // names its plan, so an earlier iteration's decision cannot unlock a later iteration's test. // Bound by the plan's path or by its content (a test seed of 204 or later), however spelled. const planSeed = testSeedOf(opts.plan); + // The test data's own header names its seed, so a forged plan copy cannot unbind it (G0 red + // team round 3). + const dataSeed = dataTestSeed(opts.data); + const isBoundSeed = (seed: string | undefined): boolean => + seed !== undefined && /^\d+$/.test(seed) && Number(seed) >= FIRST_BOUND_SEED; const iteration = /(?:^|\/)configs\/+(?:\.\/)*iter(\d+)\//.exec(normPath(opts.plan ?? "")); const bound = (iteration !== null && Number(iteration[1]) >= 3) || - (planSeed !== undefined && /^\d+$/.test(planSeed) && Number(planSeed) >= FIRST_BOUND_SEED); + isBoundSeed(planSeed) || + isBoundSeed(dataSeed); if (opts.decision === undefined) { if (bound) { throw new RecordError( @@ -326,6 +343,13 @@ export function requireDecision(opts: G2RunOptions): ChangeDecision | undefined "a decision bound to a plan must name its test seed", ); } + if (d.test_seed != null && dataSeed !== undefined && dataSeed !== d.test_seed) { + throw new RecordError( + opts.decision, + "test_seed", + `the decision commits to test seed ${d.test_seed}, but the test data are seed ${dataSeed}`, + ); + } if (d.test_seed != null && opts.plan !== undefined) { const seed = planSeed; if (seed !== d.test_seed) { diff --git a/packages/models/test/g2.test.ts b/packages/models/test/g2.test.ts index e9006bd1..6b413160 100644 --- a/packages/models/test/g2.test.ts +++ b/packages/models/test/g2.test.ts @@ -381,4 +381,32 @@ describe("decisions bind to their plan from iteration 3 (spec 007, G0 red team)" expect(respelled).not.toBe(p); expect(requireDecision(opts({ plan: respelled, decision: d }))?.plan).toBe(p); }); + + it("binds by the test data's own seed, so a forged plan copy cannot unbind it (red team round 3)", () => { + const dir = tempDir(); + const data = join(dir, "data"); + generateDataset({ + seed: 204n, + count: 20, + outDir: data, + ranges: DEFAULT_RANGES, + heldoutRanges: DEFAULT_RANGES, + format: "f32pack", + createdBy: "test", + provenance: { generator_git_commit: "0".repeat(40), generator_git_dirty: false }, + }); + mkdirSync(join(dir, "elsewhere")); + const forged = join(dir, "elsewhere", "plan.json"); + writeFileSync(forged, JSON.stringify({ dataset: { seed: "100" } })); + const old = join(dir, "old.json"); + writeFileSync(old, JSON.stringify(DECISION)); + expect(() => requireDecision(opts({ data, plan: forged, decision: old }))).toThrow( + /pre-registers no plan/, + ); + const d = join(dir, "decision.json"); + writeFileSync(d, JSON.stringify({ ...DECISION, plan: forged, test_seed: "205" })); + expect(() => requireDecision(opts({ data, plan: forged, decision: d }))).toThrow( + /test data are seed 204/, + ); + }); }); diff --git a/specs/007-iteration-3-second-zig/spec.md b/specs/007-iteration-3-second-zig/spec.md index 0961c900..bcdff73b 100644 --- a/specs/007-iteration-3-second-zig/spec.md +++ b/specs/007-iteration-3-second-zig/spec.md @@ -104,8 +104,8 @@ seed-204 test split, with the 2c models beside it as the other phase. Then the a LSTM, has a 95% interval wholly below 1. 2c degrades on the new problem, so this shows only that training on it helped, never that the lever worked. 4. **Reported measure of what the models gained.** The paired median speed ratio, iteration-3 - LSTM over classical on the same rows, with its interval. It is set beside 2c's 2.37 (1.42 / - 0.60 kt) on its own validation. + LSTM over classical on the same rows, with its interval. 2c's 2.37 (1.42 / 0.60 kt) on its + own validation is context only: unpaired, and on another problem. The time-late model is reported the same way, not as a condition. The carried pair goes to the test split only if conditions 2 and 3 both hold. Otherwise the iteration stops at validation and From 491ee1b4a8c1a39202779ac4a897c87fa1387ff6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 10:06:49 +0000 Subject: [PATCH 19/21] G0 on generator 0.3.0 passes: agent review panel round 3 (no rejections, no open blocker or major) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- AGENTS.md | 5 +++-- experiments/20261017-g0-i3-r3/review.md | 25 ++++++++++++++++++++++ experiments/20261017-g0-i3-r3/verdict.json | 2 +- experiments/20261017-g0-i3/verdict.json | 2 +- specs/007-iteration-3-second-zig/tasks.md | 6 +++--- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8b21e1ab..4d811ac1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -200,8 +200,9 @@ site page `/viewer`) is built, and the owner's realism check is recorded (Annex levers are pre-registered one at a time, each with a decision record committed before training and a fresh test seed (204 onward). **Iteration 3** (spec 007, decision 22) is lever B.2.1: generator 0.3.0 adds a second zig 3–5 min into leg 2, and both turns cross the line of sight -(`zig_rule: across_los`). Its dataset is seed 204, and G0 must be re-run on 0.3.0 before -training. The classical forecast is 0.36 kt at zig + 600 s, and the model forecast is 1.1–1.3 kt +(`zig_rule: across_los`). Its dataset is seed 204. G0 passed on 0.3.0 (panel round 3, +`experiments/20261017-g0-i3-r3/`). From iteration 3, `tma-g2 --split test` needs the committed +decision that names its plan and test seed (schema 0.9.1 `ChangeDecision.plan`, `test_seed`). The classical forecast is 0.36 kt at zig + 600 s, and the model forecast is 1.1–1.3 kt (spec 007 research R2). Gate decisions come from an **agent review panel**, never from waiting on humans (constitution diff --git a/experiments/20261017-g0-i3-r3/review.md b/experiments/20261017-g0-i3-r3/review.md index 03215968..142b47d2 100644 --- a/experiments/20261017-g0-i3-r3/review.md +++ b/experiments/20261017-g0-i3-r3/review.md @@ -104,3 +104,28 @@ The reviewers re-checked their round-1 findings at c648455 against the round-2 e **G0 round 3.** G0 was re-run at 928995a (this folder) so the verdict's checks are the code of record. The kinematic and turn-sum checks pass on all 10,000 test scenarios; the data and every other figure are unchanged. + +## Round 3 (2026-10-17): final confirmation + +The reviewers checked their round-2 findings at b1e36fc against this folder (G0 at 928995a). + +| Reviewer | Verdict | +| --- | --- | +| TMA geometry: both long-way mutations (short angle stored) now fail on the turn sum; leg 3 at 0.98 v fails on the step floor; own RK4 tracks and the real data pass | approve | +| Estimation and numerics: no false failures on 110,000 scenarios; leg-3 mutations at 0.8 v, frozen, and +0.01° course now caught 193/200, 193/200 and 200/200, where the 7 misses have a one-sample leg 3 that the mutation does not change | approve | +| Evaluation statistics: relabelled FR-006 answers M6; `pairedComparison` emits both references on the same rows | approve | +| Reproducibility: round 3 re-run from its manifest byte-identical; full suite 323/323 at b1e36fc; binding and guard cases re-exercised | approve-with-findings | +| Red team: M8 now fails G0; every path spelling and plan copy refused with the 2c decision; the correct decision accepted for every spelling and a symlink | approve-with-findings | + +### Minor findings and responses + +| Finding (reviewers) | Response | +| --- | --- | +| A forged plan copy, with its dataset seed edited and the digests kept, still bound nothing, so the 2c decision unlocked seed 204's test data (red team) | A decision now also binds by the `dataset_seed` in the test data's own header, and a bound decision's `test_seed` must match it (0df0df2); test added. It is outside G0's scope, so no re-run was needed | +| 2c's 2.37 is an unpaired ratio from another problem (statistics) | Spec 007 and the decision record call it context only, not a threshold the paired interval is tested against | +| The round-1 verdict linked to `../20261017-g0-i3-r2/review.md`, which moved (reproducibility) | The link now points to this folder | + +**Decision.** No reviewer rejects, and no blocker or major finding is open. G0 passes on generator +0.3.0, and `decided_by` names panel round 3. Training on the iteration-3 data may start once the +iteration-3 decision record is committed (spec 007 FR-005). + diff --git a/experiments/20261017-g0-i3-r3/verdict.json b/experiments/20261017-g0-i3-r3/verdict.json index aa18d088..6b0afb93 100644 --- a/experiments/20261017-g0-i3-r3/verdict.json +++ b/experiments/20261017-g0-i3-r3/verdict.json @@ -26,7 +26,7 @@ "result": "pass" } ], - "decided_by": "harness (pending agent panel review)", + "decided_by": "agent review panel, round 3 (no rejections; no open blocker or major findings): G0 PASS on generator 0.3.0 (second zig, across-LOS turns; spec 007); see review.md", "gate": "G0", "result": "pass" } diff --git a/experiments/20261017-g0-i3/verdict.json b/experiments/20261017-g0-i3/verdict.json index a490f1d0..47830380 100644 --- a/experiments/20261017-g0-i3/verdict.json +++ b/experiments/20261017-g0-i3/verdict.json @@ -26,7 +26,7 @@ "result": "pass" } ], - "decided_by": "superseded: round-1 evidence of the iteration-3 G0 panel; the round-1 fixes changed the G0 checks (packages/eval), so G0 was re-run and the verdict of record is ../20261017-g0-i3-r3 (see ../20261017-g0-i3-r2/review.md)", + "decided_by": "superseded: round-1 evidence of the iteration-3 G0 panel; the round-1 fixes changed the G0 checks (packages/eval), so G0 was re-run and the verdict of record is ../20261017-g0-i3-r3 (see its review.md)", "gate": "G0", "result": "pass" } diff --git a/specs/007-iteration-3-second-zig/tasks.md b/specs/007-iteration-3-second-zig/tasks.md index 09c36caa..97024ae1 100644 --- a/specs/007-iteration-3-second-zig/tasks.md +++ b/specs/007-iteration-3-second-zig/tasks.md @@ -11,10 +11,10 @@ golden tests; 0.2.0 goldens unchanged; 0.1.0 and 0.2.0 data readable - [x] T004 Viewer: second turn, leg 3, "zig 2" marker and readout - [x] T005 Ranges `configs/iter3-ranges.json`, `configs/iter3-heldout-ranges.json` -- [ ] T006 SRD: Annex A §A.7 record, §3 rows, §8 decision 22; AGENTS.md -- [ ] T007 G0 on generator 0.3.0 (seed 42, noise-free, iteration-3 ranges) and its agent panel; +- [x] T006 SRD: Annex A §A.7 record, §3 rows, §8 decision 22; AGENTS.md +- [x] T007 G0 on generator 0.3.0 (seed 42, noise-free, iteration-3 ranges) and its agent panel; the G0 guard test moves to it -- [ ] T008 Dataset seed 204 generated at a clean commit; plan `configs/iter3/a/plan.json` with +- [x] T008 Dataset seed 204 generated at a clean commit; plan `configs/iter3/a/plan.json` with digests; seed 204 reserved in `scripts/train-plan.mjs`; viewer catalogue shows the validation split - [ ] T009 Decision record `experiments/-i3-decision/` (change, forecast, validation rule, From 3128a3a71f2799afd8a8c713ad504bb0aa8f1427 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 10:07:07 +0000 Subject: [PATCH 20/21] Iteration 3 decision record (pre-registered before training): plan configs/iter3/a, test seed 204 Change, forecast (1.1-1.3 kt), validation rule (FR-006: operative possible G2 speed pass; sanity paired ratio to 2c), carried set, stalled true. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- .../20261017-i3-decision/decision.json | 11 +++ experiments/20261017-i3-decision/decision.md | 91 +++++++++++++++++++ specs/007-iteration-3-second-zig/tasks.md | 2 +- 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 experiments/20261017-i3-decision/decision.json create mode 100644 experiments/20261017-i3-decision/decision.md diff --git a/experiments/20261017-i3-decision/decision.json b/experiments/20261017-i3-decision/decision.json new file mode 100644 index 00000000..49298244 --- /dev/null +++ b/experiments/20261017-i3-decision/decision.json @@ -0,0 +1,11 @@ +{ + "carried_set_rule": "The iteration-3 LSTM and BiLSTM (configs/iter3/a, from scratch on seed 204) are carried unconditionally once the validation rule passes; the 2c models (experiments/20261016-i2d-g2/models) are scored beside them on the same seed-204 rows as the other phase, reported and never gated.", + "change": "the problem: generator 0.3.0 with configs/iter3-ranges.json, a second zig 180-300 s after leg 2 starts and both turns across the line of sight (target 30-90 deg on the other bow, shorter turn); first-zig timing, ranges, noise and dropouts as iteration 2; models the 2c configuration from scratch (configs/iter3/a); dataset seed 204, whose own test split decides G2 (spec 007, decision 22)", + "decided_at": "2026-10-17T10:07:00Z", + "git_commit": "491ee1b4a8c1a39202779ac4a897c87fa1387ff6", + "phase_a_experiment": "20261016-i2d-g2", + "plan": "configs/iter3/a/plan.json", + "rationale": "2c failed G2 on speed (1.47 / 1.53 kt against 1 kt; classical 0.61 kt; experiments/20261016-i2d-g2). Owner's realism check (SRD Annex A A.7): on steady legs there is too little information; operators zig again and turn across towards the contact. Spec 007 R1 (seed-203 slices): shorter first legs are worse, a towards turn helps only when it crosses the line of sight, larger turns help every estimator. R2 (classical forecast): speed at zig + 600 s 0.67 -> 0.36 kt. Forecast recorded before training: models 1.1-1.3 kt, probably still above 1 kt. Validation rule (FR-006): carry to test only if the iteration-3 LSTM's G2 speed interval has a lower bound under 1 kt (operative) and its paired speed ratio to the 2c LSTM on the same rows is wholly below 1 (sanity, weak reference). stalled is true because the previous configuration (2c) failed G2.", + "stalled": true, + "test_seed": "204" +} diff --git a/experiments/20261017-i3-decision/decision.md b/experiments/20261017-i3-decision/decision.md new file mode 100644 index 00000000..990844f4 --- /dev/null +++ b/experiments/20261017-i3-decision/decision.md @@ -0,0 +1,91 @@ +# Iteration 3 decision (spec 007) + +Committed before any iteration-3 weights are trained. `tma-g2` checks that the commit adding +`decision.json` is an ancestor of every carried model's training commit. It uses no seed-204 test +result; seed 204's test split is scored once, at the gate. + +## Evidence + +- **The 2c gate** (`../20261016-i2d-g2/`): G2 failed on speed, 1.47 kt and 1.53 kt against 1 kt. + Classical reached 0.61 kt. +- **The owner's realism check** (SRD Annex A §A.7, 2026-10-17): with both platforms on steady legs + there is too little information. An operator zigs again, and turns across towards the contact. +- **Spec 007 research R1** (seed-203 gate predictions, observational slices): + - Shorter first legs are worse. + - A turn towards the target helps only when it crosses the line of sight. + - Larger turns help every estimator. + - No slice brings the models within 2× of classical. +- **Spec 007 research R2** (classical forecast, scratch seed 9001): on the iteration-3 ranges, + classical speed at zig + 600 s is 0.36 kt, against 0.67 kt on the iteration-2 ranges. At + zig + 300 s it is 0.56 kt, against 1.13 kt. + +## The single change (decision 22, Annex A D4, 2026-10-17) + +- **The problem.** + - Generator 0.3.0 with `configs/iter3-ranges.json`: a second zig 180–300 s after leg 2 starts. + - Both turns cross the line of sight, putting the target 30–90° on the other bow, by the + shorter turn. + - Everything else is as iteration 2: first zig at 40–60% of the window, turn rate 0.5–3°/s, + σ 1–2°, dropouts ≤ 10%, 2–20 km, held-out 20–30 km. +- **Models.** The 2c configuration from scratch (`configs/iter3/a`, identical to + `configs/iter2/d`): 2 × 128 stacked LSTM / BiLSTM / shuffled control, CNN and MLP controls, + `loss_weight_speed` 4, `max_epochs` 150, patience 6, raw features, seeds 1001–1005. +- **Data.** Seed 204, count 100,000, content digests pinned in `configs/iter3/a/plan.json`. Its own + test split decides G2. Seeds 201–203 are not used. + +## Forecast, recorded before the run + +- **Speed at zig + 600 s, σ = 1°.** The instantaneous model should reach about 1.1–1.3 kt, + probably still above 1 kt. That is from R1's cross-slice relation (model ≈ 1.0 + 0.75 × + classical) with classical at 0.36 kt. Time-late should be similar. +- **Range and course.** Both should pass with larger margins than 2c's 13% and 7°. +- **A pass** needs the models, trained on an all-favourable distribution, to use the extra + information better than 2c did in iteration 2's best slices (1.12–1.32 kt). + +## Validation rule (spec 007 FR-006, Annex A B.3.3) + +After training, `tma-g2 --split validation --compare-models experiments/20261016-i2d-g2/models` runs +on seed 204's validation split. It scores the iteration-3 models, the 2c models and classical on +the same rows, and writes `paired.json`. Everything below is at σ = 1°, zig + 600 s. + +A change to the problem has no like-for-like counterfactual for "moved speed". 2c's own figures +are on another problem, and 2c degrades on this one: on scratch seed 9001 its LSTM reaches +1.93 kt here, against 1.49 kt on the iteration-2 ranges (G0 panel round 2, statistics). The +carried pair goes to the test split only if both of these hold: + +1. **A G2 pass is possible (operative).** The iteration-3 LSTM's G2 speed interval on validation + has a lower bound under 1 kt. +2. **Training on the new problem helped (sanity, weak reference).** The paired median speed ratio, + iteration-3 LSTM over 2c LSTM (`bootstrapMedianRatio`, 2,000 resamples, seed 20261011), has a + 95% interval wholly below 1. This is not cited as evidence that the lever worked. + +**Reported, as the measure of what the models gained.** The paired median speed ratio, +iteration-3 LSTM over classical on the same rows, with its interval. 2c's 2.37 on its own +validation is context only: an unpaired ratio of medians on another problem, not a threshold the +interval is tested against. The time-late model is reported the same way, not as a condition. If either +condition fails, iteration 3 stops at validation and is written up without test scoring. + +## How the gate reads under three legs + +Gate times still count from the end of the first turn (spec 007, "Reading the gate under three +legs"). On the G0 data, at zig + 300 s, 59% of scenarios are mid-way through the second turn and +37% are on leg 3. Range and course are therefore judged mostly during the second manoeuvre. At +zig + 600 s, 99.9% are on leg 3, with a median of 280 s of it. Coverage: 98.9% of scenarios reach +zig + 300 s and 65.4% reach zig + 600 s. Windows skew long (p10 16.7 min). Slices carry their n. + +## Record fields + +`decision.json` names the plan (`configs/iter3/a/plan.json`) and its test seed (204). `tma-g2` +scores seed 204's test split only with this record (spec 007 FR-009). + +`stalled` is true: the previous configuration failed G2 (2c, `../20261016-i2d-g2/`), which is what +the field records. The gate command scores the 2c models beside the carried set +(`--compare-models`). That makes `tma-g2` treat the run as having a phase B, and it requires a +stalled decision. + +## Carried set (pre-declared) + +The iteration-3 LSTM and BiLSTM are carried unconditionally. The 2c models are scored beside them +on the same seed-204 split as the other phase: reported, never gated. The split is scored once: + +`tma-g2 --split test --plan configs/iter3/a/plan.json --decision experiments/20261017-i3-decision/decision.json --compare-models experiments/20261016-i2d-g2/models` diff --git a/specs/007-iteration-3-second-zig/tasks.md b/specs/007-iteration-3-second-zig/tasks.md index 97024ae1..c2a43490 100644 --- a/specs/007-iteration-3-second-zig/tasks.md +++ b/specs/007-iteration-3-second-zig/tasks.md @@ -17,7 +17,7 @@ - [x] T008 Dataset seed 204 generated at a clean commit; plan `configs/iter3/a/plan.json` with digests; seed 204 reserved in `scripts/train-plan.mjs`; viewer catalogue shows the validation split -- [ ] T009 Decision record `experiments/-i3-decision/` (change, forecast, validation rule, +- [x] T009 Decision record `experiments/-i3-decision/` (change, forecast, validation rule, carried set, seed 204), committed before training ## Run From e3a8522fbb876d7ad4556bc76c117cd19ac1ad47 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 10:14:33 +0000 Subject: [PATCH 21/21] Validation compares with 2c on the same rows: compare_models on validation; other phase on its own normalisation - train-plan: a validation evaluation may name compare_models (must exist); a decision stays test-only - configs/iter3/a: evaluate scores the 2c models beside (spec 007 FR-006) - tma-g2: an other phase trained on other data is scored on inputs built with its own (inputs-only) normalisation, with a note in the manifest; the carried set still refuses a mismatch; test added Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv4979qzsScV5ohT5RNUoG --- configs/iter3/a/plan.json | 3 ++- packages/models/src/g2.ts | 36 +++++++++++++++++++------ packages/models/test/g2.test.ts | 36 ++++++++++++++++++++++++- packages/models/test/train-plan.test.ts | 13 +++++++++ scripts/train-plan.mjs | 8 ++++-- 5 files changed, 84 insertions(+), 12 deletions(-) diff --git a/configs/iter3/a/plan.json b/configs/iter3/a/plan.json index 688fdd29..8395ba20 100644 --- a/configs/iter3/a/plan.json +++ b/configs/iter3/a/plan.json @@ -18,6 +18,7 @@ "models": ["instantaneous", "time-late", "causal-cnn", "mlp", "instantaneous-shuffled"], "evaluate": { "command": "tma-g2", - "split": "validation" + "split": "validation", + "compare_models": "experiments/20261016-i2d-g2/models" } } diff --git a/packages/models/src/g2.ts b/packages/models/src/g2.ts index ecf6b6c1..3ff80689 100644 --- a/packages/models/src/g2.ts +++ b/packages/models/src/g2.ts @@ -25,7 +25,7 @@ import { NAIVE, naivePredict, } from "@tma/classical"; -import { readDataset } from "@tma/data"; +import { fileSha256, readDataset } from "@tma/data"; import { BOOTSTRAP_RESAMPLES, BOOTSTRAP_SEED, @@ -49,7 +49,7 @@ import { writeManifest, writeVerdict, } from "@tma/eval"; -import { openFeatures } from "@tma/features"; +import { openFeatures, writeFeatures } from "@tma/features"; import { ChangeDecision, type FeatureSet, @@ -433,15 +433,35 @@ export async function scoreSplit( for (const m of g.models) { const fs = m.manifest.config.feature_set ?? "raw"; const path = featuresPath(opts.data, split, fs); - const features = openFeatures(path); + let features = openFeatures(path); try { log(`${split}: predicting ${m.manifest.id} (${fs}, ${path})`); requireSameFeatureSet(features, m.manifest.config.feature_set, m.manifest.id); - const note = requireSameNormalisation(features, m.manifest.normalisation, m.manifest.id, { - header: trainHeader, - modelSchemaVersion: m.manifest.schema_version, - modelSourceHeader: m.manifest.source_dataset_header, - }); + let note: string | undefined; + try { + note = requireSameNormalisation(features, m.manifest.normalisation, m.manifest.id, { + header: trainHeader, + modelSchemaVersion: m.manifest.schema_version, + modelSourceHeader: m.manifest.source_dataset_header, + }); + } catch (e) { + // The other phase may come from other training data (spec 007: the 2c models beside + // iteration 3). Its inputs are then built with its own normalisation, which holds + // inputs-only constants from its training data; the carried set never takes this path. + if (tag === undefined) throw e; + features.close(); + const own = join(predDir, `${split}.${tag}.${m.manifest.id}.features.bin`); + const truth = readDataset(truthPath); + writeFeatures( + own, + () => readDataset(truthPath).scenarios(), + m.manifest.normalisation, + { header: truth.header, digest: fileSha256(truthPath) }, + fs, + ); + features = openFeatures(own); + note = `${m.manifest.id} (${tag}): trained on other data, so scored on inputs built with its own normalisation (${own})`; + } if (note !== undefined) notes.push(note); const model = await loadModel(m.dir); const ps = predictScenarios( diff --git a/packages/models/test/g2.test.ts b/packages/models/test/g2.test.ts index 6b413160..df4c4cf6 100644 --- a/packages/models/test/g2.test.ts +++ b/packages/models/test/g2.test.ts @@ -1,6 +1,14 @@ // T038: tma-g2 one-command run, tiny. Validation reads only train + validation and writes a // diagnostic (not a verdict); test needs a committed decision once a phase B exists (SC-005). -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + cpSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; import { join } from "node:path"; import { fileSha256, generateDataset, readDataset } from "@tma/data"; import { computeNormalisation, writeFeatures } from "@tma/features"; @@ -271,6 +279,32 @@ describe("tma-g2 end to end (tiny)", () => { ).rejects.toThrow(/not committed before .* was trained/); }); + it("validation with an other phase trained on other data: its inputs use its own normalisation (spec 007)", async () => { + // A copy of the raw models whose manifests carry another normalisation, as models trained on + // another dataset would (the 2c models beside iteration 3). + const foreign = tempDir(); + cpSync(modelsA, foreign, { recursive: true }); + for (const id of readdirSync(foreign)) { + const mp = join(foreign, id, "manifest.json"); + const m = JSON.parse(readFileSync(mp, "utf8")) as { normalisation: Record }; + m.normalisation = { ...m.normalisation, train_header_digest: "f".repeat(64) }; + writeFileSync(mp, JSON.stringify(m)); + } + const exp = join(tempDir(), "val-foreign"); + await runG2(base({ split: "validation", compareModels: foreign, experiment: exp })); + expect(readFileSync(join(exp, "manifest.json"), "utf8")).toMatch( + /scored on inputs built with its own normalisation/, + ); + expect(readdirSync(join(data, "predictions")).some((f) => f.endsWith(".features.bin"))).toBe( + true, + ); + expect(existsSync(join(exp, PAIRED_FILE))).toBe(true); + // The carried set never takes this path: foreign models as the carried set are refused. + await expect( + runG2(base({ split: "validation", models: foreign, experiment: join(tempDir(), "x") })), + ).rejects.toThrow(); + }); + it("requireSameRows refuses estimators that predict different rows", () => { const pred = (idx: number, ts: number[]): Prediction => ({ record: "prediction", diff --git a/packages/models/test/train-plan.test.ts b/packages/models/test/train-plan.test.ts index 9f6875c5..b6f9b28a 100644 --- a/packages/models/test/train-plan.test.ts +++ b/packages/models/test/train-plan.test.ts @@ -173,6 +173,19 @@ describe("plan validation", () => { ), ).err, ).toMatch(/validation evaluation takes no decision/); + // A reference set may be scored beside a validation run (spec 007 FR-006), if it exists. + expect( + validate( + workspace( + (p) => + (p.evaluate = { + command: "tma-g2", + split: "validation", + compare_models: "no/such/models", + }), + ), + ).err, + ).toMatch(/does not exist/); expect(validate(workspace((p) => (p.models = ["no-such-model"]))).err).toMatch(/no config/); // A scan never touches the test split. expect( diff --git a/scripts/train-plan.mjs b/scripts/train-plan.mjs index 268a6f00..9379c764 100644 --- a/scripts/train-plan.mjs +++ b/scripts/train-plan.mjs @@ -196,8 +196,12 @@ export function loadPlan(path) { for (const k of ["decision", "compare_models"]) { if (e[k] !== undefined && typeof e[k] !== "string") fail(`evaluate.${k} must be a path`); } - if (e.split === "validation" && (e.decision !== undefined || e.compare_models !== undefined)) - fail("a validation evaluation takes no decision or compare_models"); + // A validation run may score a reference set beside it on the same rows (spec 007 FR-006: + // paired.json); a decision is only for test. + if (e.split === "validation" && e.decision !== undefined) + fail("a validation evaluation takes no decision"); + if (e.compare_models !== undefined && !existsSync(e.compare_models)) + fail(`evaluate.compare_models ${e.compare_models} does not exist`); // A scan (side experiment, spec 003 R20) reports every model on validation; it never gates. if (e.command === "tma-scan" && e.split !== "validation") fail("a tma-scan evaluation is validation only");