* 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>
11 KiB
Design — Alinhamento dos Modelos de Força ao Golden
Spec: spec.md · Status: Draft Pesquisa: fórmulas do engine de referência extraídas de
dynamics/nashville/src/*Forces/(fastTime C++ que gerou o golden), cruzadas com o Ymir emcore/libs/physics/src/forces/.
Como abordar (HOW)
O alinhamento é numérico, não estrutural. Para cada modelo:
- Comparar a fórmula do Ymir com a do engine de referência (feito — abaixo).
- Corrigir a divergência mínima (constante, sinal, área, unidade) — sem reescrever arquitetura, sem portar código 1:1.
- Verificar contra o golden via
ymir_golden_tests(método state-in→force-out).
Cada correção cita a linha da referência no commit/ADR.
Achado que corrige a spec ⚠️
A hipótese da spec (FMA-04) de que restauração divergia por falta de
acoplamento hidrostático off-diagonal está errada: o engine de referência
também é diagonal (RestoringForces.cpp:35-40, off-diagonal comentado). A
fórmula do Ymir é idêntica. Logo o resíduo medido (6,5% Fr_z / 13,5% Mr_y)
não é de modelo — é artefato de cruzamento por zero (a força decai a ~0 no
assentamento, inflando o erro relativo). FMA-04 reclassificado: não há
mudança de código; ajustar piso absoluto do teste e documentar. Ver componente
Restauração abaixo.
Architecture Overview
Nenhuma mudança arquitetural. Alterações confinadas a:
core/libs/physics/src/forces/*.cpp(fórmulas)core/libs/common/include/ymir/common/PhysicalConstants.h(constantes)core/libs/simulation/src/{NavalDomain,NavalSimulation}.cpp(vento aparente)core/tests/golden/(novos testes + tolerâncias)
graph LR
G[Golden state.csv] --> F[Force model Ymir]
C[vessel1.json params ×1000] --> F
E[Env + yaw → GoldenFrame] --> F
F --> A{≤5%?}
L[Golden loads.csv] --> A
Code Reuse Analysis
| Componente | Local | Uso |
|---|---|---|
| Harness golden | core/tests/golden/GoldenCsv.h, Vessel1Params.h, GoldenFrame.h |
Estender p/ cenários 01,03,05,06,07 |
nautToBodyFrame |
NavalDomain.cpp:15 / GoldenFrame.h |
Reconstruir fluxo no corpo (já existe) |
| Interpolação Cd | ymir::math::linear |
Reusar nas tabelas de coeficiente |
| Constantes | PhysicalConstants.h (rho_water, g, rho_air) |
Ajustar rho_air |
Componentes (fórmula ref → mudança no Ymir)
FMA-01 · Corrente (OBOKATA) — 2 bugs
- Local:
core/libs/physics/src/forces/CurrentForces.cpp:31-101 - Ref:
dynamics/nashville/src/currentForces/CurrentForces.cpp:142-195
Bug A — sinal (inversão de 180°)
- Ref:
angle = atan2(vc_y, vc_x)comvc = speedToWater(sem negar). - Ymir (
:57-58):water_x = -vc_x; water_y = -vc_y - r·xi→ gira 180° →Fc_xcom sinal trocado. - Fix: usar
vc_x,vc_ydiretos (remover a negação); manter o termo de yaw-r·xiemvc_y(a ref usavc_sec[1] = speedToWater[1] - r·sectionPos[i]).
Bug B — área seccional errada (~2× baixa)
- Ref:
hydro_scale = 0.5·rho_water·dL·sub_depth, comsub_depth = wavesOriginPosition[2] - q[2](calado submerso ≈ 23 m) edL = length_BP / n_sections. Mesma área p/ Fx e Fy. - Ymir (
:71-72):ds_x = frontalHeight·dx(11,5 m),ds_y = lateralHeight·dx;dx = length_BP/(n_sections-1). - Fix: área seccional =
sub_depth·dL(nãofrontalHeight/lateralHeight);dL = length_BP/n_sections.sub_depthvem do estado (orig[2] - z).
Mz (menor): ref soma termo cd_z + correção de midship
(CurrentForces.cpp:190-192); Ymir só faz Mz -= Fy·xo. Ajustar se Mc_z não
fechar ≤5% após A+B.
Roll/pitch arms: idênticos — sem mudança.
Config novo em Vessel1Params.h: n_sections, sectionLocalPositions
(ou default −L/2..L/2), midshipDistance, wavesOriginPosition (z p/ sub_depth).
Corrente lê current.coefficients (col0 ângulo, col1 cdx, col2 cdy).
Efeito esperado: Fc_x −83 kN → ~+166 kN (golden +172 kN).
FMA-02 · Squat — divisão dupla por (ρ·g)
- Local:
core/libs/physics/src/forces/SquatForces.cpp:13,49 - Ref:
dynamics/nashville/src/squatForces/SquatForces.cpp:48-49
Fórmula é idêntica exceto o numerador do sinkage:
- Ref:
s = -(Cs+Cf)·(volumetricWeight / (ρ·g·L²))·fn²/√(1-fn²)(volumetricWeight= peso em N,×1000na carga —Vessel.cpp:112). - Ymir:
nabla_ = volumetricWeight/(ρ·g)(:13), depoiss = ...(nabla_/(ρ·g·L²))...(:49) → divide por(ρ·g)duas vezes → fator1/(ρ·g)=1/10055a menos ≈ ~4 ordens de grandeza. - Fix: numerador =
volumetricWeight/(ρ·g·L²)(divisão única). Equivale a trocar o denominador do termosde(ρ·g·L²)paraL²(poisnabla_já contém1/(ρ·g)), ou usarcfg.volumetricWeightdireto. - Profundidade/clamp:
depth = max(|waterDepth+tide|, |z|)e o clamps = max(-depth-0.1-z, s)já batem com a ref — validar na implementação (o número absoluto do golden depende da convenção deq[2]).
Efeito esperado: Fsq_z −2,1e4 → ordem de −1e8..−7e8 (validar contra golden).
FMA-03 · Vento + constantes globais
-
Local:
PhysicalConstants.h:8; wind formula já correta (WindForces.cpp). -
Ref:
PhysicalProps.cpp:6-8. -
Fix 1:
rho_air1,225 → 1,275 (casa o resíduo de 4,08%).rho_water(1025) eg(9,81) já iguais. -
Fix 2 (correção, simulação): vento aparente. Ref subtrai velocidade do vessel (
speedToWind = wind - vessel_velno corpo); Ymir usa vento puro (NavalDomain.cpp:194-196,speedToWind[0]=wu). AdicionarspeedToWind[i] -= bs.u()/v(). Impacto pequeno em baixa velocidade, mas necessário p/ correção geral. Revalidar todos os testes golden após mudarrho_air(constante compartilhada).
Efeito esperado: Fwd_x de ~4% → ≤2%.
FMA-04 · Restauração — SEM mudança de código (reclassificado)
- Local:
RestoringForces.cpp(Ymir) — já correto. - Ref:
RestoringForces.cpp:18-56— diagonal, fórmula idêntica.
Não adicionar off-diagonal (a ref não usa). O resíduo é cruzamento por zero no
assentamento. Ação: no TestGolden08Restoring.cpp, manter tolerância
relativa mas com piso absoluto adequado; documentar que ≤5% vale fora dos
cruzamentos. Remedir Fr_z/Mr_y com piso p/ confirmar.
FMA-05 · Amortecimento — sinal e norma
- Local:
DampingForces.cpp:20-45 - Ref:
DampingForces.cpp:32-50
Duas divergências pequenas:
- Sinal surge/sway: ref
aux_velocity[0] = -speedToWater[0]; Ymiraux[0] = +ctx.speedToWater[0]. Alinhar sinal. vNorm2: ref soma os 6 componentes²; Ymir só surge²+sway². Alinhar (usar os 6) — afeta odecayFactor.
Heave (Fd_z) já bate (0,05–0,6% com movimento). Após 1+2, apertar piso p/ ≤5%
em baixa velocidade.
FMA-06 · Leme
- Local:
RudderForces.cpp(Ymir) — comparar (ainda não lido em detalhe). - Ref:
RudderForces.cpp:34-69.
Fórmula ref: fd=0.5·ρ·A·v²·cd, fl=...·cl; α=atan2(v_rel,u_rel);
β=wrap360(-(δ-α)·180/π); cl/cd interp em β (cd negado); decomposição
xr=fd·cos α + fl·cos(α-π/2), yr=fd·sin α + fl·sin(α-π/2); momentos sobre
localPosition - wavesOrigin. Inflow sem thruster:
u_rel = -(currentLocalVel[0]-localVel[0]).
- Ação: medir baseline (FMA-11), então alinhar convenção de
β, sinal de cd, e decomposição. Config:controlSurfaces(área, tabela cl/cd, posição, ângulo).
FMA-07 · Onda (regular + JONSWAP) — o mais difícil
-
Local:
core/libs/world/src/wave/*(Ymir). -
Ref:
WaveComponent.cpp:308-401,SurfaceWaves.cpp:91-187. -
Excitação:
F = Σ a(ω)·|H(ω)|·cos(ζ - φ_H),ζ = -ω·t + phase + k·x,|H|/φ_Hda RAO de força (wvForcesAmplitude/Phase). -
Bloqueio de reprodutibilidade: fase inicial usa
rand()semsrand()(SurfaceWaves.cpp:115) → não reproduzível instante-a-instante fora do binário original. (Opção comentadaphase={0,0}p/ validação determinística.) -
Ação:
03_wave_regular: validarFwv_md(mean drift) instantaneamente (independe de fase); validar excitação por estatística (RMS/média) OU setar fase=0 em ambos e comparar (se o golden puder ser regenerado com fase=0 — requer rerun do engine de ref).04_wave_jonswap: só estatística (média/RMS), documentado.
-
Requer mapear RAO/QTF do
wavesblock dovessel1.jsonp/ oWaveRaoDatado Ymir. Maior esforço; manter P2 e possivelmente fatiar.
Tech Decisions
| Decisão | Escolha | Racional |
|---|---|---|
| Off-diagonal restauração | Não implementar | Ref é diagonal; resíduo é zero-crossing |
rho_air |
1,275 (valor da ref) | Casa o desvio de 4% do vento |
| Vento aparente | Subtrair vel. do vessel | Correção física + alinha à ref |
| Squat | Corrigir divisão dupla | Bug numérico claro (~4 ordens) |
| Corrente sinal+área | Usar atan2(vc) sem negar + área sub_depth·dL |
Reproduz sinal e magnitude do golden |
| Onda instantânea | Validar mean-drift + estatística | Fase rand() não reproduzível |
| Ordem de execução | FMA-11 (baseline) → 02,03(rho),01 → 05 → 06 → 07 | Constante antes de apertar tolerâncias |
Error Handling / Tolerância
| Cenário | Tratamento |
|---|---|
| Coluna golden cruza zero | Piso absoluto no Approx().margin(); rel só onde ` |
Mudança de rho_air quebra outro teste |
Revalidar suíte golden inteira |
| Golden não reproduzível (onda irregular) | Validar estatística; documentar |
Convenção de q[2]/profundidade no squat |
Verificar contra rerun/valor de ref na implementação |
Impacto em testes / traceability atualizada
| ID | Mudança | Arquivo | Verifica |
|---|---|---|---|
| FMA-01 | sinal + área seccional | CurrentForces.cpp |
TestGolden01Current.cpp (novo) |
| FMA-02 | divisão dupla | SquatForces.cpp |
TestGolden07Squat.cpp (novo) |
| FMA-03 | rho_air=1,275 + vento aparente |
PhysicalConstants.h, NavalDomain.cpp |
TestGolden02Wind.cpp (aperta) |
| FMA-04 | só tolerância/piso | TestGolden08Restoring.cpp |
idem |
| FMA-05 | sinal + vNorm2 |
DampingForces.cpp |
TestGolden08Restoring.cpp |
| FMA-06 | convenção β/cd/decomposição | RudderForces.cpp |
TestGolden06Rudder.cpp (novo) |
| FMA-07 | mapear RAO/QTF; mean-drift + estatística | world/wave/* |
TestGolden03/04* (novo) |
| FMA-10 | fixtures + testes p/ 01,03,05,06,07 | core/tests/golden/, fixtures/ |
— |
| FMA-11 | medir baseline leme/onda | probes | — |
Próximo (Tasks)
Quebrar por modelo com dependências: FMA-11 (baseline) e FMA-10 (harness) antes dos testes; FMA-03 (rho_air) antes de apertar tolerâncias que usam densidade. Um modelo por task, cada uma com "Done when: TestGolden ≤5% verde".