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 927b423f..4d811ac1 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 @@ -59,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 \ @@ -166,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). @@ -188,9 +193,17 @@ 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. The engagement and model viewer (spec 006, `packages/demos`, +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. 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 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/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..8395ba20 --- /dev/null +++ b/configs/iter3/a/plan.json @@ -0,0 +1,24 @@ +{ + "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": "f8f0629b7db7333c9f079aefadaf02c254ce0f83402e787cadb429e6cc7b32f8", + "validation.f32pack": "01fe0295953e175309c1fe00a6d8a81dcbeb461f237bb2c0e361ea8d464651d5", + "test.f32pack": "947776b48136e31317b5ebd84be80fc8dd64377aab5740400b6c6ef686d9733f", + "heldout.f32pack": "425f4f0df90edddf9392a92de578ece680a78040ffef5742fbcb3caf65b4ee6b" + } + }, + "feature_set": "raw", + "configs": "configs/iter3/a", + "models": ["instantaneous", "time-late", "causal-cnn", "mlp", "instantaneous-shuffled"], + "evaluate": { + "command": "tma-g2", + "split": "validation", + "compare_models": "experiments/20261016-i2d-g2/models" + } +} 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/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 41aaed78..d4ff9273 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, 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 | @@ -115,7 +116,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 +179,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 +200,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 +209,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 +236,12 @@ 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). +- 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 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 +286,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/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..f61209d6 --- /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": "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-r3/review.md b/experiments/20261017-g0-i3-r3/review.md new file mode 100644 index 00000000..142b47d2 --- /dev/null +++ b/experiments/20261017-g0-i3-r3/review.md @@ -0,0 +1,131 @@ +# 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: `../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. + +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 (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) + +| 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 | + +## 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. + +## 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/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..6b0afb93 --- /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": "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/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..47830380 --- /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": "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/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/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/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/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..6b6ba26a 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": "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 1192e748..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.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.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/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..ce247f5e --- /dev/null +++ b/packages/demos/src/core/series.ts @@ -0,0 +1,102 @@ +// 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, + * 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; + 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. */ +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..9d21c183 --- /dev/null +++ b/packages/demos/src/viewer/EngagementViewer.tsx @@ -0,0 +1,654 @@ +// 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 [exportError, setExportError] = useState(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" }]; + 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"], + [600, "zig+10"], + ] as const) { + if (zigT + m <= windowS) markers.push({ t: zigT + m, label }); + } + } + 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, + 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", + }; + + const scenarioPanel = ( +
+

Scenario

+ +
+ ); + + 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(1)} min ( + {tCur >= zigT ? "zig+" : "zig−"} + {(Math.abs(tCur - zigT) / 60).toFixed(1)}) + +
+ {exportError !== null &&

PNG export failed: {exportError}

} + {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" && scenarioPanel} + {v.mode !== "truth" && ( +
+

At the cursor

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

This scenario

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

Models

+ +
+
+
+
+ ); +} diff --git a/packages/demos/src/viewer/Panels.tsx b/packages/demos/src/viewer/Panels.tsx new file mode 100644 index 00000000..16717c9a --- /dev/null +++ b/packages/demos/src/viewer/Panels.tsx @@ -0,0 +1,301 @@ +// 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.isNaN(v) + ? dash + : v === Infinity + ? "∞" + : 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; + 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 ,{" "} + {legName(s, cursor)}. +

+ {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", + `${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)}%`], + [ + "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)}`, + ], + ...(p.zig2_angle_rad == null || m.zig2_index == null + ? [] + : [ + [ + "zig 2", + `${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], + ]), + [ + "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}'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}

} +
+ ); +} diff --git a/packages/demos/src/viewer/PlanView.tsx b/packages/demos/src/viewer/PlanView.tsx new file mode 100644 index 00000000..d9af5700 --- /dev/null +++ b/packages/demos/src/viewer/PlanView.tsx @@ -0,0 +1,484 @@ +// 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, 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); + }; + 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( + , + ); + } + } + + // 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))); + + 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 && ( + + )} + + + + + {turn2 !== undefined && ( + + )} + {leg3 !== undefined && ( + + )} + {estimates} + {ownStart !== null && ( + + )} + {tgtStart !== null && ( + + )} + {zigMarks.map(([label, q]) => ( + + + + {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")} + + + {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..d96fbc2f --- /dev/null +++ b/packages/demos/src/viewer/TimePlot.tsx @@ -0,0 +1,236 @@ +// 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[]; + /** 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. */ + 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; +} + +/** 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; + 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 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( + (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 ?? []).map(([a, b]) => ( + + ))} + {ticks.map((v) => ( + + + + {fmt(v, tickStep)} + + + ))} + {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..685cd944 --- /dev/null +++ b/packages/demos/src/viewer/style.ts @@ -0,0 +1,42 @@ +// 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-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; } +.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..5a5f3d0b --- /dev/null +++ b/packages/demos/test/core.test.ts @@ -0,0 +1,164 @@ +// @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, ranges } 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, + }); + }); +}); + +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/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/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/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/eval/src/g0-checks.ts b/packages/eval/src/g0-checks.ts index 699cba4f..d803c408 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"; @@ -157,12 +163,133 @@ 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 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. + */ +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; + // 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]; + 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`; + 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`; + 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)}]`; + } + } + // 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; +} + /** * 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 * 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 +320,30 @@ 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)}`); + 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. + 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 +355,52 @@ export function checkIndependentGeometry(truth: Iterable): GateCheck { 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][] = [ + [(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) { + 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. + 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 ( @@ -217,9 +414,16 @@ export function checkIndependentGeometry(truth: Iterable): GateCheck { } return check( "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` - : fails.slice(0, 6).join("; "), + 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 (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` + : "") + : [ + ...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/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/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/g0-checks.test.ts b/packages/eval/test/g0-checks.test.ts index 1d94aebe..928e0f5b 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"; @@ -42,3 +44,97 @@ 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/); + }); +}); + +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(); + }); + + 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/eval/test/golden/verdict.json b/packages/eval/test/golden/verdict.json index 6f3d294d..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 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), 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/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(); 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/features/test/golden/seed1-2.features.bin b/packages/features/test/golden/seed1-2.features.bin index ef3a65e9..d8434ac3 100644 Binary files a/packages/features/test/golden/seed1-2.features.bin and b/packages/features/test/golden/seed1-2.features.bin differ diff --git a/packages/features/test/golden/seed1-4.derived.features.bin b/packages/features/test/golden/seed1-4.derived.features.bin index a04930ff..1fa74378 100644 Binary files a/packages/features/test/golden/seed1-4.derived.features.bin and b/packages/features/test/golden/seed1-4.derived.features.bin differ 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/g2.ts b/packages/models/src/g2.ts index 7e57ad76..3ff80689 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, @@ -18,11 +25,12 @@ import { NAIVE, naivePredict, } from "@tma/classical"; -import { readDataset } from "@tma/data"; +import { fileSha256, readDataset } from "@tma/data"; import { BOOTSTRAP_RESAMPLES, BOOTSTRAP_SEED, bootstrapEvaluation, + bootstrapMedianRatio, compareOnCommonRows, evaluate, type G1Ids, @@ -30,16 +38,18 @@ import { g2Verdict, loadPredictionSet, makeManifest, + pairedErrors, type PredictionSet, perTimestep, prettyJson, + sliceKeys, writeBootstrap, writeComparison, writeEvaluation, writeManifest, writeVerdict, } from "@tma/eval"; -import { openFeatures } from "@tma/features"; +import { openFeatures, writeFeatures } from "@tma/features"; import { ChangeDecision, type FeatureSet, @@ -108,6 +118,45 @@ 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); + } +} + +/** `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; + 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 { @@ -125,6 +174,80 @@ 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; + /** The other phase's model of the same variant, or the classical solver on the same rows. */ + reference: "other-phase" | "classical"; + 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 at the G2 points on σ = 1° (range + * and course at 300 s, speed at 600 s), against the other phase's model of the same variant and + * against the classical solver, on the same rows. Spec 007 FR-006 reads the instantaneous speed + * records; 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 refs = [ + { reference: "other-phase" as const, set: find(other, otherIds[key]), name: otherIds[key] }, + { reference: "classical" as const, set: find(carried, CLASSICAL), name: CLASSICAL }, + ]; + for (const ref of refs) { + for (const [metric, point] of [ + ["range_rel", 300], + ["course_deg", 300], + ["speed_kt", 600], + ] as const) { + const e = pairedErrors(truth, a, ref.set, point, metric); + if (e.a.length === 0) continue; + const r = bootstrapMedianRatio(e.a, e.b, resamples); + out.push({ + variant, + model: carriedIds[key], + reference: ref.reference, + other: ref.name, + 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). @@ -169,7 +292,28 @@ 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. + // 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) || + isBoundSeed(planSeed) || + isBoundSeed(dataSeed); 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 +329,37 @@ export function requireDecision(opts: G2RunOptions): ChangeDecision | undefined opts.decision, "decision", ); + 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 && 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) { + 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, @@ -258,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( @@ -397,6 +592,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/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/packages/models/test/g0-guard.test.ts b/packages/models/test/g0-guard.test.ts index fb5f0a82..1541c2f6 100644 --- a/packages/models/test/g0-guard.test.ts +++ b/packages/models/test/g0-guard.test.ts @@ -136,10 +136,12 @@ describe("G0 guard (constitution V)", () => { ).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("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) and 928995a (round 3); the generator source HEAD still matches. const dir = new URL("../../../experiments", import.meta.url).pathname; - const want = { id: "20261013-g0-i2-r2", commit: "24bbdab5d79f3c872f88ac6494c2d73068016225" }; + 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; diff --git a/packages/models/test/g2.test.ts b/packages/models/test/g2.test.ts index 8e538fcc..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, 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"; @@ -18,6 +26,9 @@ import { DIAGNOSTIC_FILE, featuresPath, type G2RunOptions, + PAIRED_FILE, + type PairedRecord, + requireDecision, requireSameRows, runG2, } from "../src/g2.js"; @@ -222,6 +233,15 @@ 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); + 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); + 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.")); @@ -259,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", @@ -304,3 +350,97 @@ 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(); + }); + + 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); + }); + + 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/packages/models/test/train-plan.test.ts b/packages/models/test/train-plan.test.ts index 25e57e18..b6f9b28a 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", ]) { @@ -172,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( @@ -185,7 +199,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 +208,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 +223,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 +294,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/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..bd9593bd --- /dev/null +++ b/packages/sim/test/unit/second-zig.test.ts @@ -0,0 +1,161 @@ +// 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); + }); +}); + +// 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: [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) }, + 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..e615dc33 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": [ @@ -1025,6 +1039,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 +1087,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 +1356,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 +1370,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 +1446,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 +1660,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 +1705,5 @@ ], "title": "tma", "type": "object", - "version": "0.8.2" + "version": "0.9.1" } diff --git a/packages/types/src/generated/version.ts b/packages/types/src/generated/version.ts index 4b1afeec..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.8.2"; +export const SCHEMA_VERSION = "0.9.1"; diff --git a/packages/types/src/generated/zod.ts b/packages/types/src/generated/zod.ts index a076a5f0..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; @@ -55,10 +59,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 +80,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 +459,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 +481,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/pnpm-lock.yaml b/pnpm-lock.yaml index 67bbd5da..a2c75050 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': @@ -134,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 @@ -2332,6 +2384,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 +10088,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/schema/tma.yaml b/schema/tma.yaml index 29829bb0..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.8.2 +version: 0.9.1 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 } } @@ -690,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/scripts/train-plan.mjs b/scripts/train-plan.mjs index 92d7f60c..9379c764 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 {} @@ -195,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"); 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/generator.md b/site/docs/algorithms/generator.md index 717d04ad..9ff2cbc2 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,40 @@ 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°`. + +- **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. + +### 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/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/docs/glossary.md b/site/docs/glossary.md index c1547252..aa939ba3 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, 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 [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/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..cabfa31c 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,134 @@ 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" }; +/** + * 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) { + 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, + }); + } + 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")); + 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..c04184b7 --- /dev/null +++ b/site/src/data/viewer-bookmarks.json @@ -0,0 +1,39 @@ +[ + { + "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", + "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 00000000..07e702a4 Binary files /dev/null and b/site/static/img/blog/viewer-far.jpg differ 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 00000000..854e66d4 Binary files /dev/null and b/site/static/img/blog/viewer-typical-tl.jpg differ diff --git a/site/static/img/blog/viewer-typical.jpg b/site/static/img/blog/viewer-typical.jpg new file mode 100644 index 00000000..16c50c3d Binary files /dev/null and b/site/static/img/blog/viewer-typical.jpg differ diff --git a/site/static/img/blog/viewer-worst.jpg b/site/static/img/blog/viewer-worst.jpg new file mode 100644 index 00000000..2c7d9d2c Binary files /dev/null and b/site/static/img/blog/viewer-worst.jpg differ 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..d8abdc6b --- /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 + +- [x] T003 Browser-safe subpaths: `@tma/features/browser` (with `decode.ts` moved from models), + `@tma/classical/browser`, `@tma/data/canonical`; goldens unchanged +- [x] T004 `@tma/demos` core: catalogue types, `scenarioAt`, geometry, inference glue, errors, + classical and naive rows, URL state, NDJSON export +- [x] T005 Tests: browser bundle (FR-001), agreement (FR-007), golden, server render +- [x] T006 React viewer: controls, plan view, time plots, panels, three modes, model choice, + overlays, PNG and NDJSON export, URL state +- [x] T007 Site: prebuild catalogue and model copy, `/viewer` page, navbar link, featured bookmarks +- [x] T008 Playwright smoke on the built site; screenshots +- [x] 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) 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..9f60ecfd --- /dev/null +++ b/specs/007-iteration-3-second-zig/research.md @@ -0,0 +1,128 @@ +# 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. +- **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 new file mode 100644 index 00000000..bcdff73b --- /dev/null +++ b/specs/007-iteration-3-second-zig/spec.md @@ -0,0 +1,170 @@ +# 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; 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. 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 + 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 + panels. Classical and naive are re-baselined on the new data, and the improvement is reported + 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. 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) + +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 + +- **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 (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. 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..c2a43490 --- /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` +- [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 +- [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 +- [x] 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 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,