Skip to content

Commit f66ed12

Browse files
committed
Add RM3 mooring verification test (WEC-Sim/MoorDyn)
Cross-code verification of the RM3 two-body point absorber with MoorDyn mooring against WEC-Sim/MoorDyn co-simulation reference data. Compares body heave motions and fairlead tensions after the 40s ramp transient. The test uses the new excitation truncation and cosine ramp features to match WEC-Sim's elevationImport mode (CITime=20s, rampTime=40s). Requires HYDROCHRONO_ENABLE_MOORDYN; skipped otherwise. Includes test infrastructure (tests/verification/CMakeLists.txt) that mirrors the existing regression pattern with CTest fixtures.
1 parent 007bbda commit f66ed12

16 files changed

Lines changed: 480841 additions & 3 deletions

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,7 @@ if(HYDROCHRONO_ENABLE_TESTS)
676676
endif()
677677

678678
add_subdirectory(tests/regression)
679+
add_subdirectory(tests/verification)
679680
add_subdirectory(tests/unit)
680681
endif()
681682

build.ps1

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -446,13 +446,15 @@ Write-Host "========================================`n" -ForegroundColor Green
446446

447447
Write-Host "Output: $binPath" -ForegroundColor Cyan
448448
Write-Host ""
449-
Write-Host "Tests: ctest -C $BuildType -L regression --test-dir build" -ForegroundColor Gray
450-
Write-Host " ctest -C $BuildType -L unit --test-dir build" -ForegroundColor Gray
449+
Write-Host "Tests: ctest -C $BuildType -L regression --test-dir build" -ForegroundColor Gray
450+
Write-Host " ctest -C $BuildType -L unit --test-dir build" -ForegroundColor Gray
451+
Write-Host " ctest -C $BuildType -L verification --test-dir build" -ForegroundColor Gray
452+
Write-Host " Cross-code verification (requires specific features, e.g. -MoorDyn)" -ForegroundColor DarkGray
451453
Write-Host " Add -V for verbose output, --output-on-failure for failures only" -ForegroundColor DarkGray
452454
Write-Host ""
453455
Write-Host "Long: `$env:HYDROCHRONO_LONG_TESTS='1'" -ForegroundColor Gray
454456
Write-Host " ctest -C $BuildType -L regression --test-dir build" -ForegroundColor Gray
455-
Write-Host " Runs with extended simulation durations" -ForegroundColor DarkGray
457+
Write-Host " Runs with extended simulation durations (also applies to verification)" -ForegroundColor DarkGray
456458
Write-Host ""
457459
Write-Host "Report: python tests/regression/utilities/generate_report.py --build-dir build --pdf" -ForegroundColor Gray
458460
Write-Host " Generates regression test report (markdown + PDF) in build/bin/report/" -ForegroundColor DarkGray
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Extract reference data from a WEC-Sim/MoorDyn co-simulation of the RM3 mooring case.
4+
5+
Reference case: WEC-Sim Applications Mooring
6+
https://github.com/WEC-Sim/WEC-Sim_Applications/tree/2d7569271c00e8736c2755192ee45941ffd49403/Mooring
7+
8+
Run that case to obtain the .mat outputs (e.g. etaData.mat and
9+
Mooring/Mooring/MoorDyn/output/RM3MoorDyn_matlabWorkspace.mat). Then run this
10+
script with --wecsim-dir pointing to the MoorDyn case directory (the directory
11+
containing etaData.mat and the output/ subdirectory).
12+
13+
Reads the WEC-Sim MATLAB workspace (.mat) and wave elevation file, and writes
14+
plain-text reference files suitable for HydroChrono verification tests.
15+
16+
Output files (committed to repo):
17+
data/verification/rm3_mooring/reference/wecsim_moordyn_body_motions.txt
18+
data/verification/rm3_mooring/reference/wecsim_moordyn_fairlead_tensions.txt
19+
data/verification/rm3_mooring/reference/wecsim_moordyn_wave_elevation.txt
20+
data/verification/rm3_mooring/reference/README.md
21+
data/verification/rm3_mooring/inputs/eta_rm3_mooring.txt
22+
23+
Usage:
24+
python data/verification/rm3_mooring/extract_wecsim_ref.py --wecsim-dir PATH
25+
26+
--wecsim-dir is required: path to the WEC-Sim MoorDyn case directory.
27+
"""
28+
29+
import argparse
30+
import os
31+
import sys
32+
from pathlib import Path
33+
from datetime import datetime
34+
35+
import numpy as np
36+
37+
try:
38+
import scipy.io as sio
39+
except ImportError:
40+
sys.exit("scipy is required: pip install scipy")
41+
42+
try:
43+
import h5py
44+
except ImportError:
45+
sys.exit("h5py is required: pip install h5py")
46+
47+
48+
def extract(wecsim_dir: Path, repo_root: Path):
49+
eta_mat_path = wecsim_dir / "etaData.mat"
50+
workspace_mat_path = wecsim_dir / "output" / "RM3MoorDyn_matlabWorkspace.mat"
51+
52+
for p in (eta_mat_path, workspace_mat_path):
53+
if not p.exists():
54+
sys.exit(f"Source file not found: {p}")
55+
56+
ref_dir = repo_root / "data" / "verification" / "rm3_mooring" / "reference"
57+
inp_dir = repo_root / "data" / "verification" / "rm3_mooring" / "inputs"
58+
ref_dir.mkdir(parents=True, exist_ok=True)
59+
inp_dir.mkdir(parents=True, exist_ok=True)
60+
61+
# ── 1. Wave elevation (etaData.mat -> HydroChrono eta format) ────────
62+
print(f"Reading {eta_mat_path} ...")
63+
eta_raw = sio.loadmat(str(eta_mat_path))
64+
65+
eta_key = [k for k in eta_raw if not k.startswith("__")]
66+
if len(eta_key) != 1:
67+
sys.exit(f"Expected one variable in etaData.mat, found: {eta_key}")
68+
eta_data = eta_raw[eta_key[0]] # (N, 2): [time, elevation]
69+
print(f" eta shape: {eta_data.shape}, t=[{eta_data[0,0]:.2f}, {eta_data[-1,0]:.2f}]s")
70+
71+
# HydroChrono eta format: "time : elevation" per line
72+
eta_out = inp_dir / "eta_rm3_mooring.txt"
73+
with open(eta_out, "w") as f:
74+
for row in eta_data:
75+
f.write(f"{row[0]:.6f} : {row[1]:.10e}\n")
76+
print(f" -> {eta_out} ({len(eta_data)} points)")
77+
78+
# ── 2. WEC-Sim/MoorDyn workspace (.mat v7.3 via h5py) ───────────────
79+
print(f"Reading {workspace_mat_path} ...")
80+
ws = h5py.File(str(workspace_mat_path), "r")
81+
refs = ws["#refs#"]
82+
83+
time = refs["Fb"]["time"][0, :] # (40001,)
84+
elevation = refs["Fb"]["elevation"][0, :] # (40001,)
85+
n_steps = len(time)
86+
dt = time[1] - time[0]
87+
print(f" time: {n_steps} steps, dt={dt:.4f}s, t=[{time[0]:.2f}, {time[-1]:.2f}]s")
88+
89+
# Body positions: refs/8 = float (body1), refs/9 = plate (body2)
90+
# Shape (6, 40001): rows are [x, y, z, rx, ry, rz]
91+
float_pos = refs["8"][:] # body1 (float)
92+
plate_pos = refs["9"][:] # body2 (plate)
93+
print(f" float Z at t=0: {float_pos[2, 0]:.4f}m (expect -0.72)")
94+
print(f" plate Z at t=0: {plate_pos[2, 0]:.4f}m (expect -21.50)")
95+
96+
# Fairlead tensions
97+
ft4 = refs["Cb"]["Lines"]["FairTen4"][0, :]
98+
ft5 = refs["Cb"]["Lines"]["FairTen5"][0, :]
99+
ft6 = refs["Cb"]["Lines"]["FairTen6"][0, :]
100+
print(f" FairTen4 range: [{ft4.min():.1f}, {ft4.max():.1f}] N")
101+
102+
ws.close()
103+
104+
# ── 3. Write reference body motions ──────────────────────────────────
105+
body_out = ref_dir / "wecsim_moordyn_body_motions.txt"
106+
header = "Time(s) FloatHeaveZ(m) PlateHeaveZ(m)"
107+
body_arr = np.column_stack([time, float_pos[2, :], plate_pos[2, :]])
108+
np.savetxt(str(body_out), body_arr, header=header, fmt="%.6f", comments="")
109+
print(f" -> {body_out} ({n_steps} rows)")
110+
111+
# ── 4. Write reference fairlead tensions ─────────────────────────────
112+
tension_out = ref_dir / "wecsim_moordyn_fairlead_tensions.txt"
113+
header = "Time(s) FairTen4(N) FairTen5(N) FairTen6(N)"
114+
tension_arr = np.column_stack([time, ft4, ft5, ft6])
115+
np.savetxt(str(tension_out), tension_arr, header=header, fmt="%.6f", comments="")
116+
print(f" -> {tension_out} ({n_steps} rows)")
117+
118+
# ── 5. Write reference wave elevation ────────────────────────────────
119+
wave_out = ref_dir / "wecsim_moordyn_wave_elevation.txt"
120+
header = "Time(s) Elevation(m)"
121+
wave_arr = np.column_stack([time, elevation])
122+
np.savetxt(str(wave_out), wave_arr, header=header, fmt="%.6f", comments="")
123+
print(f" -> {wave_out} ({n_steps} rows)")
124+
125+
# ── 6. Write README ──────────────────────────────────────────────────
126+
readme_out = ref_dir / "README.md"
127+
readme_out.write_text(f"""\
128+
# WEC-Sim/MoorDyn RM3 Mooring Reference Data
129+
130+
## Provenance
131+
132+
Extracted on **{datetime.now().strftime('%Y-%m-%d %H:%M')}** by
133+
`data/verification/rm3_mooring/extract_wecsim_ref.py` from a WEC-Sim/MoorDyn
134+
co-simulation of the RM3 device with 6-line catenary mooring.
135+
136+
### Source
137+
138+
Reference data were produced by running the [WEC-Sim Applications Mooring](https://github.com/WEC-Sim/WEC-Sim_Applications/tree/2d7569271c00e8736c2755192ee45941ffd49403/Mooring) case (WEC-Sim/MoorDyn co-simulation of the RM3 device). That run generates MATLAB outputs (e.g. `etaData.mat` and `Mooring/Mooring/MoorDyn/output/RM3MoorDyn_matlabWorkspace.mat`). The plain-text reference files in this directory were then generated using `data/verification/rm3_mooring/extract_wecsim_ref.py` and are used by the HydroChrono verification tests and comparison plots.
139+
140+
### WEC-Sim simulation parameters
141+
142+
| Parameter | Value |
143+
|------------------|-------------------------------------------------|
144+
| dt | 0.01 s |
145+
| End time | 400 s |
146+
| Ramp time | 40 s |
147+
| Wave type | Elevation import (etaData.mat) |
148+
| Water depth | 70 m |
149+
| Float mass | 725 834 kg (equilibrium) |
150+
| Plate mass | 886 691 kg (equilibrium) |
151+
| Float inertia | [20907301, 21306090.66, 37085481.11] kg-m^2 |
152+
| Plate inertia | [94419614.57, 94407091.24, 28542224.82] kg-m^2 |
153+
| Plate init disp | [0, 0, -0.21] m |
154+
| PTO damping | 1 200 000 N-s/m |
155+
| PTO stiffness | 0 |
156+
| MoorDyn | 3 lines, 6 segments (anchor–clump + clump–fairlead each); input: data/yaml/rm3/mooring/lines_rm3.txt |
157+
158+
## Files
159+
160+
| File | Description |
161+
|------|-------------|
162+
| `wecsim_moordyn_body_motions.txt` | Time, float heave (Z), plate heave (Z) from WEC-Sim |
163+
| `wecsim_moordyn_fairlead_tensions.txt` | Time, FairTen4, FairTen5, FairTen6 from MoorDyn (inside WEC-Sim) |
164+
| `wecsim_moordyn_wave_elevation.txt` | Time, wave elevation as used in the co-simulation |
165+
""", encoding="utf-8")
166+
print(f" -> {readme_out}")
167+
168+
print("\nDone.")
169+
170+
171+
def main():
172+
parser = argparse.ArgumentParser(description=__doc__,
173+
formatter_class=argparse.RawDescriptionHelpFormatter)
174+
parser.add_argument("--wecsim-dir", type=str, required=True,
175+
help="Path to WEC-Sim MoorDyn case directory (contains etaData.mat and output/)")
176+
args = parser.parse_args()
177+
178+
wecsim_dir = Path(args.wecsim_dir)
179+
repo_root = Path(__file__).resolve().parent.parent
180+
181+
extract(wecsim_dir, repo_root)
182+
183+
184+
if __name__ == "__main__":
185+
main()

0 commit comments

Comments
 (0)