|
| 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