* test(physics): add TMS Dynamics golden validation suite Cross-engine validation of Ymir force models against the TMS Dynamics 3.0 golden dataset (vessel1 VLCC). Strategy is state-in -> force-out: the golden motion trajectory is fed row by row into the matching Ymir force model and the result is compared against the golden per-force decomposition. This is integrator-independent, so it validates the force formula, not the time step. - GoldenCsv.h: header-only CSV reader (column access by name) - Vessel1Params.h: force configs mapped from vessel1.json, with the tonne->SI x1000 unit conversion documented (verified empirically to ~3e-5 on Fr_z) - TestGolden08Restoring.cpp: restoring (Fr_z, Mr_y) + heave damping (Fd_z) - Downsampled golden fixtures under tests/fixtures/golden/08_restoring/ - ymir_golden_tests target wired into tests/CMakeLists.txt Tolerances are set empirically and the residual causes are documented: the restoring divergence (up to ~6.5% Fr_z / ~13.5% Mr_y as pitch grows) is the off-diagonal hydrostatic coupling Ymir omits by design. Findings on squat (diverges ~4 orders — model gap) and the body-frame-flow forces (need TMS heading-convention mapping) are captured under docs/specs/features/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build(tests): run C++ suite through the turbo monorepo Make core/ a pnpm workspace package so `pnpm test` (turbo run test) runs the C++ ctest suite alongside the JS vitest suites — one command for everything. - Add "core" to pnpm-workspace.yaml so turbo discovers @ymir/core - core/package.json: add a `build` script (cmake configure + compile) - core/turbo.json: override `test` to dependsOn ["build"] instead of the root's build:wasm, so native ctest does not require Emscripten; cache off (native) - scripts/run-all-tests.sh: standalone runner (cpp/js/e2e flags) for running the C++ suite without touching the wasm/emcc toolchain @ymir/core#build -> @ymir/core#test: 270/270 tests pass via turbo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(physics): add wind golden validation + reconstruct flow convention Extends the cross-engine golden suite to wind drag (scenario 02_wind) and adds the earth->body flow conversion needed by flow-dependent forces. - GoldenFrame.h: nautToBodyFrame mirroring NavalDomain — rotates the scenario environment (speed + nautical direction) into the body frame using the golden heading, so wind/current vectors can be reconstructed from controlDict + the golden trajectory - Vessel1Params.h: vessel1Wind() config with the full Cd(angle) table - TestGolden02Wind.cpp: Fwd_x validated to ~4% across the trajectory (residual is the rho_air constant: Ymir 1.225 vs reference ~1.27); Fwd_y/Mwd_z ~0 bow-on - Fixtures for 02_wind Documents the remaining divergences with measured numbers: current OBOKATA disagrees in sign and magnitude (Fc_x -83kN vs golden +172kN — sectional-area approximation + sign convention), squat off by ~4 orders. See docs/specs/features/golden-validation/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(graphify): refresh knowledge graph after golden validation work Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(physics): align rho_air, squat sinkage, and damping norm to reference Cross-engine alignment against the TMS Dynamics 3.0 golden dataset (see docs/planning/force-model-alignment/). Verified against the reference engine sources in dynamics/nashville/src/*Forces/. - rho_air 1.225 -> 1.275 (PhysicalConstants.h): matches PhysicalProps.cpp of the reference and closes the ~4% wind gap. Wind golden test tightened to <3%. - SquatForces: fix double division by (rho*g) — the sinkage numerator is now volumetricWeight/(rho*g*L^2) (single division, matching the reference), not nabla_/(rho*g*L^2). Also use heave relative to equilibrium (z + draft) for the depth/seabed clamp, since the reference q[2] is waterline-referenced. Squat goes from ~4 orders of magnitude off to the correct order (partial: <=5% still blocked by the artificial 07 scenario depth dynamics). - DampingForces: the exponential-decay velocity norm now uses all 6 DOF (matching dampingForces/DampingForces.cpp); the surge/sway sign was already correct given Ymir's speedToWater convention. Restoring needs no change: the reference restoring is also diagonal-only, so the residual is a zero-crossing artifact, not a missing off-diagonal term. Full C++ suite: 271/271 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(physics): add force-model-alignment spec/design/tasks + findings TLC spec-driven planning for aligning all force models to the golden dataset: - spec.md: 11 requirements (FMA-01..11), tolerances, decisions - design.md: reference formula per model cross-checked with Ymir; root causes - tasks.md: atomic tasks + execution status of the first pass Records the key research corrections: restoring reference is diagonal (no off-diagonal), squat is a double-division bug, current alignment conflicts with self-propelled physics (reverted, needs speedToWater-convention resolution). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(graphify): refresh graph after force-model alignment --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
Tasks — Alinhamento dos Modelos de Força ao Golden
Status de execução (leva 1)
| Task | Estado | Nota |
|---|---|---|
T3 rho_air=1,275 |
✅ feito | Wind golden ≤3% (was ~4%) |
| T5 squat divisão dupla + z_rel | 🟡 parcial | 4 ordens → ordem correta (e8); ≤5% pendente (cenário artificial) |
T6 damping vNorm2 6-DOF |
✅ feito | sinal do surge/sway já era correto no Ymir (revertido) |
| T4 corrente sinal+área | ⛔ revertido | casa golden em repouso mas vira anti-arrasto em auto-propulsão (186/187) — precisa resolver convenção de speedToWater |
| T7 restauração | ✅ confirmado | ref é diagonal; sem mudança de código |
| T8 vento aparente | ⬜ pendente | fecharia wind <2% |
| T9 leme · T10 onda | ⬜ pendente | fórmulas mapeadas no design |
| T1/T2/T11 | parcial | fixtures 02/08 no repo; suíte 271 verde |
Suíte C++: 271/271 verde após a leva. Corrente é o próximo item de projeto
(resolver convenção de sinal de speedToWater corrente-vs-amortecimento).
Convenções de teste e gate (não há TESTING.md)
- Tipo de teste:
unit(validação golden state-in→force-out por força). - Gate
quick(durante iteração de uma força):cmake --build core/build --target ymir_golden_tests && ./core/build/tests/ymir_golden_tests - Gate
full(fecho de fase):pnpm turbo run test --filter @ymir/core(esperado: 271+ testes verdes; nenhum apagado silenciosamente). - Critério de alinhamento: erro relativo ≤ 5% (piso absoluto perto de cruzamentos por zero), conforme spec.
- Pós-código:
graphify update .(regra do CLAUDE.md). - Tools por task: MCP nenhum; Skill nenhum (exceto
graphifyno fecho).
Execution Plan
Phase 1: Foundation (Sequential)
T1 → T2
Phase 2: Correções (Parallel após deps)
┌→ T3 [P] ─────────────┐
├→ T4 [P] (dep T1) ─────┤
├→ T5 [P] (dep T1) ─────┤
T1,T2 ──┼→ T6 [P] ──────────────┼──→ T11
├→ T7 [P] ──────────────┤
├→ T8 [P] ──────────────┤
├→ T9 (dep T1,T2) ─────┤
└→ T10 (dep T1,T2) ─────┘
Phase 3: Integração (Sequential)
T3..T10 → T11
Task Breakdown
T1: Estender harness golden (fixtures + configs) [FMA-10]
What: Gerar fixtures subamostrados e configs de força p/ os cenários
01_current, 03_wave_regular, 05_thruster, 06_rudder, 07_squat; declarar
todas as vessel1*() configs e registrar arquivos de teste no CMake — para
eliminar contenção de arquivo compartilhado nas tasks seguintes.
Where: core/tests/fixtures/golden/{01,03,05,06,07}/,
core/tests/golden/Vessel1Params.h (add current/squat/rudder/wave configs),
core/tests/CMakeLists.txt
Depends on: None
Reuses: GoldenCsv.h, GoldenFrame.h, script de downsample do README das fixtures
Tools: MCP NONE · Skill NONE
Done when:
- Fixtures
state.csv+loads.csv(~200 linhas) p/ os 5 cenários vessel1Current(),vessel1Squat(),vessel1Rudder(),vessel1Wave()adicionadas aVessel1Params.h(mapeadas dovessel1.json, ×1000 onde couber)- Arquivos-stub
TestGolden0{1,3,6,7}*.cppregistrados no CMake e compilando - Gate
quickpassa (stubs vazios não quebram build)
Tests: none (infra) · Gate: build
T2: Medir baseline leme (06) e onda (03) [FMA-11]
What: Probe rápido comparando Ymir vs golden p/ Frd (06) e Fwv_md/Fwv_ex
(03), registrando erro atual e a causa provável — insumo p/ T9/T10.
Where: docs/planning/force-model-alignment/design.md (seção "baseline")
Depends on: T1
Reuses: método probe (compilar contra libymir_physics.a)
Tools: MCP NONE · Skill NONE
Done when:
- Erro relativo atual de
Frd(06) registrado com números - Erro/estado de
Fwv_mdeFwv_ex(03) registrado - Causa provável de cada divergência anotada no design
Tests: none (medição) · Gate: none
T3: Constante rho_air 1.225 → 1.275 [FMA-03a] [P]
What: Alinhar densidade do ar ao valor do engine de referência.
Where: core/libs/common/include/ymir/common/PhysicalConstants.h:8
Depends on: None
Reuses: —
Ref: dynamics/nashville/src/basic/PhysicalProps.cpp:7
Tools: MCP NONE · Skill NONE
Done when:
rho_air = 1.275TestGolden02Windapertado p/Fwd_x≤ 2% e verde- Gate
quickpassa; nenhum outro teste golden regride - Test count mantido (271+ no gate full)
Tests: unit · Gate: quick
Commit: fix(common): align rho_air to reference (1.275)
T4: Corrigir corrente OBOKATA — sinal + área seccional [FMA-01] [P]
What: Remover a negação de vc (usar atan2(vc_y, vc_x)) e trocar a área
seccional de frontalHeight/lateralHeight·dx por sub_depth·dL
(sub_depth = orig[2]-z, dL = length_BP/n_sections).
Where: core/libs/physics/src/forces/CurrentForces.cpp:31-101;
core/tests/golden/TestGolden01Current.cpp
Depends on: T1
Reuses: Vessel1Params::vessel1Current(), GoldenFrame
Ref: dynamics/nashville/src/currentForces/CurrentForces.cpp:142-195
Tools: MCP NONE · Skill NONE
Done when:
Fc_xcom sinal correto e ≤ 5% vs golden em toda a trajetória (01)Fc_y,Mc_z≤ 5% (ajustar termocd_z/midship no Mz se necessário)TestCurrentForces(unit existente) continua verde- Gate
quickpassa; Test count sobe (novo teste)
Tests: unit · Gate: quick
Commit: fix(physics): correct current OBOKATA sign and sectional area
T5: Corrigir squat — divisão dupla por (ρ·g) [FMA-02] [P]
What: Numerador do sinkage = volumetricWeight/(ρ·g·L²) (divisão única);
remover o /(ρ·g) extra herdado de nabla_.
Where: core/libs/physics/src/forces/SquatForces.cpp:13,49;
core/tests/golden/TestGolden07Squat.cpp
Depends on: T1
Reuses: Vessel1Params::vessel1Squat()
Ref: dynamics/nashville/src/squatForces/SquatForces.cpp:48-49
Tools: MCP NONE · Skill NONE
Done when:
Fsq_z≤ 5% vs golden (07)- Convenção de
depth/clamp validada contra ref (documentar resíduo se houver) TestSquatForces(unit existente) continua verde- Gate
quickpassa
Tests: unit · Gate: quick
Commit: fix(physics): remove double rho*g division in squat sinkage
T6: Corrigir amortecimento — sinal surge/sway + vNorm2 [FMA-05] [P]
What: aux[0]/[1] = -speedToWater (alinhar sinal) e vNorm2 sobre os 6
componentes (não só surge+sway) no decayFactor.
Where: core/libs/physics/src/forces/DampingForces.cpp:20-45
Depends on: None
Reuses: TestGolden08Restoring.cpp (bloco damping)
Ref: dynamics/nashville/src/dampingForces/DampingForces.cpp:32-50
Tools: MCP NONE · Skill NONE
Done when:
Fd_z≤ 5% inclusive em baixa velocidade (08), piso absoluto p/ zero-crossingTestDampingForces(unit existente) continua verde- Gate
quickpassa
Tests: unit · Gate: quick
Commit: fix(physics): align damping surge/sway sign and velocity norm
T7: Restauração — tolerância/piso (sem mudança de código) [FMA-04] [P]
What: Ajustar piso absoluto em TestGolden08Restoring p/ refletir que o
resíduo é cruzamento por zero (ref é diagonal como o Ymir — não há bug de modelo).
Where: core/tests/golden/TestGolden08Restoring.cpp;
docs/specs/features/golden-validation/README.md (atualizar finding);
docs/planning/force-model-alignment/spec.md (reclassificar FMA-04)
Depends on: None
Reuses: —
Ref: dynamics/nashville/src/restoringForces/RestoringForces.cpp:35-40
Tools: MCP NONE · Skill NONE
Done when:
Fr_z/Mr_y≤ 5% fora de cruzamentos (piso absoluto documentado)- Finding e spec atualizados (FMA-04 = sem off-diagonal)
- Gate
quickpassa
Tests: unit · Gate: quick
Commit: test(physics): document restoring residual as zero-crossing
T8: Vento aparente — subtrair velocidade do vessel [FMA-03b] [P]
What: Em speedToWind, subtrair a velocidade do corpo (bs.u()/v()), como a
referência (speedToWind = wind - vessel_vel).
Where: core/libs/simulation/src/NavalDomain.cpp:194-196,
core/libs/simulation/src/NavalSimulation.cpp:135-137
Depends on: None
Reuses: nautToBodyFrame existente
Ref: dynamics/nashville/src/body/Vessel.cpp (velocity_relative)
Tools: MCP NONE · Skill NONE
Done when:
speedToWindsubtrai vel. do vessel- Testes de simulação existentes continuam verdes
- Gate
fullpassa (mudança na camada de simulação)
Tests: unit · Gate: full
Commit: fix(simulation): subtract vessel velocity from apparent wind
T9: Alinhar leme (rudder) [FMA-06]
What: Alinhar convenção β = wrap360(-(δ-α)), sinal de cd (negado),
decomposição xr/yr e momentos sobre localPosition, conforme baseline (T2).
Where: core/libs/physics/src/forces/RudderForces.cpp;
core/tests/golden/TestGolden06Rudder.cpp
Depends on: T1, T2
Reuses: Vessel1Params::vessel1Rudder(), GoldenFrame
Ref: dynamics/nashville/src/rudderForces/RudderForces.cpp:34-69
Tools: MCP NONE · Skill NONE
Done when:
Frd(x,y,Mz) ≤ 5% vs golden (06), isolando o acoplamento corrente/squatFrd≈0 com ângulo 0 e sem fluxoTestRudderForces(unit existente) continua verde- Gate
quickpassa
Tests: unit · Gate: quick
Commit: fix(physics): align rudder lift/drag convention to reference
T10: Alinhar onda — mean-drift + estatística [FMA-07]
What: Mapear RAO/QTF do waves block do vessel1.json p/ o WaveRaoData;
validar Fwv_md instantaneamente e Fwv_ex por estatística (fase rand() não
é reproduzível).
Where: core/libs/world/src/wave/*;
core/tests/golden/TestGolden03WaveRegular.cpp (+ 04 estatístico)
Depends on: T1, T2
Reuses: core/src/wasm/WaveRaoData.h, wave engine existente
Ref: dynamics/nashville/src/waveForces/WaveComponent.cpp:308-401,
SurfaceWaves.cpp:91-187
Tools: MCP NONE · Skill NONE
Done when:
Fwv_md≤ 5% vs golden (03)Fwv_exvalidado por RMS/média (03);04_jonswapsó estatística, documentado- Testes de onda existentes (
TestWave*) continuam verdes - Gate
fullpassa (camada world)
Tests: unit · Gate: full
Commit: fix(world): align wave mean-drift to reference; excitation by RMS
T11: Verificação final + docs + graphify [FMA-10]
What: Rodar suíte completa, atualizar finding doc (divergências fechadas),
graphify update ..
Where: docs/specs/features/golden-validation/README.md, graphify-out/
Depends on: T3, T4, T5, T6, T7, T8, T9, T10
Reuses: —
Tools: MCP NONE · Skill graphify
Done when:
- Os 8 cenários (01–08) têm teste golden verde ≤5% (04 estatístico)
- Gate
fullpassa:pnpm turbo run test --filter @ymir/core - Finding doc atualizado;
graphify update .rodado - Sinal de corrente e magnitude de squat não são mais findings
Tests: unit · Gate: full
Commit: docs(physics): close golden divergence findings
Validação pré-aprovação
Check 1 — Granularidade
| Task | Escopo | Status |
|---|---|---|
| T1 | fixtures + configs (infra coesa) | ✅ |
| T2 | 2 probes de medição | ✅ |
| T3 | 1 constante | ✅ |
| T4 | 1 arquivo (CurrentForces) | ✅ |
| T5 | 1 arquivo (SquatForces) | ✅ |
| T6 | 1 arquivo (DampingForces) | ✅ |
| T7 | 1 teste + docs | ✅ |
| T8 | vento aparente (2 arquivos, mesma mudança) | ✅ |
| T9 | 1 arquivo (RudderForces) | ✅ |
| T10 | onda (1 subsistema coeso) | ⚠️ maior — fatiar se RAO/QTF crescer |
| T11 | verificação + docs | ✅ |
Check 2 — Diagrama × Definição
| Task | Depends on (corpo) | Diagrama | Status |
|---|---|---|---|
| T1 | None | raiz | ✅ |
| T2 | T1 | T1→T2 | ✅ |
| T3 | None | raiz→T3 | ✅ |
| T4 | T1 | T1→T4 | ✅ |
| T5 | T1 | T1→T5 | ✅ |
| T6 | None | raiz→T6 | ✅ |
| T7 | None | raiz→T7 | ✅ |
| T8 | None | raiz→T8 | ✅ |
| T9 | T1,T2 | T1,T2→T9 | ✅ |
| T10 | T1,T2 | T1,T2→T10 | ✅ |
| T11 | T3..T10 | →T11 | ✅ |
[P]: T3,T4,T5,T6,T7,T8 não dependem entre si (arquivos distintos). T1 cria
todas as configs em Vessel1Params.h e registra os stubs no CMake → sem
contenção de arquivo compartilhado nas tasks [P]. T9/T10 são sequenciais
(dependem de baseline T2).
Check 3 — Co-locação de teste
Não há TESTING.md; matriz implícita: camada de física/força → unit obrigatório,
co-locado no mesmo task (teste golden). Nenhuma task difere tests p/ outra.
| Task | Camada | Requer | Task diz | Status |
|---|---|---|---|---|
| T3 | common (constante) | unit (via wind) | unit | ✅ |
| T4 | physics/current | unit | unit | ✅ |
| T5 | physics/squat | unit | unit | ✅ |
| T6 | physics/damping | unit | unit | ✅ |
| T7 | tests/docs | unit | unit | ✅ |
| T8 | simulation | unit | unit | ✅ |
| T9 | physics/rudder | unit | unit | ✅ |
| T10 | world/wave | unit | unit | ✅ |
| T1,T2,T11 | infra/medição/verif. | none | none | ✅ |
Nenhuma violação. Tests: none só em infra/medição/verificação.
Ordem recomendada de execução
- T1 → T2 (foundation).
- Paralelo: T3, T6, T7, T8 (sem deps) + T4, T5 (após T1).
- T9, T10 (após T2).
- T11 (fecho).
Sugestão: executar cada task de código via sub-agente (contexto enxuto), 1
modelo por vez para os que compartilham Vessel1Params.h/CMake, [P] real
apenas onde arquivos não colidem.