Skip to content

Commit 10fed13

Browse files
committed
Pair pressure-integration mean drift with MATLAB
1 parent af2507f commit 10fed13

11 files changed

Lines changed: 134 additions & 11 deletions

File tree

‎PARITY.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ saves its MATLAB outputs as an artifact.
6262
| Sphere `Mean_Drift` regularCIC application | Pinned MATLAB Applications `Mean_Drift` input and its BEMIO-generated Sphere HDF5 | The public Python `WEC` builder selects the control-surface mean-drift coefficient and runs the published 100 s surge, heave, and pitch case with 10 s radiation memory. The source excitation equals the first-order regular force plus amplitude-squared mean drift, each with one ramp factor; the surge drift component is 12.247 N after ramp. Paired maximum position differences are 0.043 mm surge, 0.551 mm heave, and `3.2e-7` rad pitch; velocity differences are below 0.081 mm/s surge, 1.738 mm/s heave, and `1.7e-7` rad/s pitch. The wave elevation matches within `3e-15` m, and reconstructed radiation forces differ by at most 0.24 N surge, 2.95 N heave, and `0.0002` N m pitch. The MATLAB linear model moves 16.41 m in surge by 100 s, so agreement is numerical parity, not evidence that the small-motion hydrodynamics remain physically accurate over that travel. |
6363
| Derived Sphere momentum-conservation mean drift | The pinned `Mean_Drift` input with only `body(1).meanDrift` changed from 1 to 2; local R2025b Update 7 and pinned BEMIO | A [portable full-duration public `WEC.run` gate](tests/test_sphere_momentum_drift_parity.py) independently advances the 100 s surge/heave/pitch motion using the existing Sphere HDF5 and [fresh MATLAB trace](tests/fixtures/matlab/sphere_momentum_drift/README.md). It retains the published control-surface case's numerical limits; maximum position differences are 0.0421 mm surge, 0.551 mm heave, and `4.62e-8` rad pitch, and speed differences are 0.0805 mm/s, 1.738 mm/s, and `1.32e-7` rad/s. Wave and excitation differences stay below `2.85e-15` m and `9.30e-12` N or N m. The active HDF5 momentum-drift coefficients match the fresh BEMIO file exactly. This is a derived option check and does not change the Sphere badge. |
6464
| Sphere spectral mean drift in PM waves | A [local R2025b derived 30 s input](tests/fixtures/matlab/sphere_irregular_mean_drift/README.md) from the pinned `Mean_Drift` application, retaining its control-surface coefficients and three-coordinate joint | The public `WEC.run` now permits mean drift in irregular PM, JONSWAP, and imported-spectrum mapped-coordinate cases and weights each HDF5 coefficient by spectral energy as pinned `irregExcF` does. On the 0.5 m, 8 s PM realization, independent Python matches all 3,001 MATLAB samples within `5.1e-15` m wave elevation, `1.4e-10` N or N m six-component excitation, 0.043/0.134 mm surge/heave, and `5.2e-8` rad pitch. The largest active speed error is 0.425 mm/s. Omitting drift changes final Python surge by 0.081 m and pitch by 0.0252 rad. This closes a configuration and force-path gap, but the derived variant is not another published input; the 63-case badge is unchanged. |
65+
| Sphere pressure-integration mean drift in PM waves | The [same derived 30 s Sphere PM input](tests/fixtures/matlab/sphere_pressure_drift/README.md), changing only `body(1).meanDrift` from 1 to 3; local R2025b run of the pinned application | Public `WEC.body(mean_drift="pressure_integration")` now selects the HDF5 pressure-integration coefficients, which MATLAB `readBEMIOH5` also supports. The independent 3,001-sample Python run matches wave elevation within `5.1e-15` m, all six excitation channels within `1.3e-10` N or N m, surge/heave within 0.0425/0.134 mm, and pitch within `5.2e-8` rad; maximum active speed error is 0.425 mm/s. A separate HDF5 spectral-energy calculation gates the reported drift force. With the same sea and model, MATLAB's pressure-integration option moves final surge by 18.95 mm and pitch by 0.00329 rad relative to control surface. This is a derived configuration gate and leaves the published-case count unchanged. |
6566
| RM3 regular-wave heave, excitation, and PTO | Current MATLAB RM3 example and current RM3 HDF5 | A focused two-body linear heave solver with relative-motion PTO damping agrees locally over 400 s within 5.9 mm position and 6.1 mm/s velocity for both bodies. Heave excitation matches to less than `1e-5` N; PTO internal force differs by at most 8.2 kN over a 1.63 MN range. The pinned full-duration source channels are retained in a [portable fixture](tests/fixtures/matlab/README.md), so the reduced force, motion, and PTO comparison now runs in ordinary Python CI. This reduced model does not cover surge, pitch, or full Simscape joint mechanics. |
6667
| RM3 coupled surge, heave, pitch, and PTO | Current MATLAB RM3 example and current RM3 HDF5 | A four-coordinate two-body model uses the published pitched-slider geometry, body inertias, regular-wave forcing, and nonlinear rotation kinematics. Over 400 s, the two body surge positions differ by at most 36.8 mm, heaves by 4.18 mm, shared pitch by 0.000672 rad, and PTO force by 4.74 kN. It covers the canonical active DOFs but not general Simscape joint mechanics or other RM3 cases. |
6768
| RM3 body-to-body Cases 1 and 2 | Pinned MATLAB Applications inputs and RM3 HDF5 generated with current BEMIO | Both published regular-wave cases run for 400 s with coupling off and on. Across the two, body positions differ by at most 36.8 mm surge, 4.57 mm heave, and 0.000948 rad pitch; PTO force differs by at most 5.38 kN over a 1.65 MN range. The public `WEC.floating_joint` path now has [ordinary-CI gates](tests/test_rm3_b2b_parity.py) against the archived source for both complete trajectories, including PTO stroke, speed, force, instantaneous power, and integrated work; its largest observed work difference is 0.82%. A separate short regular-wave check verifies that cross-body radiation damping gives the same motion under two coordinate bases. Turning on cross-body coupling substantially reduces the body-1 heave error against Case 2. These are reduced-model comparisons, not full Simscape mechanics. |

‎tests/fixtures/matlab/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ The published 100 s Sphere mean-drift motion and force gate is documented in
3030
[sphere_mean_drift/README.md](sphere_mean_drift/README.md).
3131
The derived irregular-wave drift path is documented in
3232
[sphere_irregular_mean_drift/README.md](sphere_irregular_mean_drift/README.md).
33+
The pressure-integration coefficient variant is documented in
34+
[sphere_pressure_drift/README.md](sphere_pressure_drift/README.md).
3335
The four published 500 s RM3 radiation-option motion and PTO gates are
3436
documented in [rm3_radiation_options/README.md](rm3_radiation_options/README.md).
3537
The published 400 s RM3 imported-elevation MooringMatrix gate is documented in
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Sphere pressure-integration mean drift
2+
3+
This is the 30 s PM-wave Sphere variant in
4+
[sphere_irregular_mean_drift](../sphere_irregular_mean_drift/README.md), with
5+
only `body(1).meanDrift` changed from control surface (`1`) to pressure
6+
integration (`3`). The pinned HDF5 contains both coefficient families. A local
7+
R2025b run used core `0753b2e47f2457c078751dcfe5d251d1767b80ab` and
8+
Applications `d53d4d4c9eda2581f04204f5d394a6ef84bb099e`. The shared
9+
[MATLAB generator](../../../matlab/sphere_irregular_mean_drift_reference.m)
10+
reproduced the independently prepared source run exactly across all body,
11+
wave, and 500 component samples.
12+
13+
`source.npz` stores all 3,001 body and wave samples, sea components, and body
14+
mass properties. Its SHA-256 is
15+
`ac602a48bd02e017b2fdc4501ae7f8911d7f8afd970c64e1b3460f7f547fd608`.
16+
The independent public Python `WEC.run` has maximum MATLAB differences of
17+
`5.1e-15` m wave elevation, `1.3e-10` N or N m excitation, 0.0425/0.134 mm
18+
surge/heave, `5.2e-8` rad pitch, and 0.425 mm/s speed. Against the same-sea
19+
control-surface MATLAB run, pressure integration shifts final surge by
20+
18.95 mm and pitch by 0.00329 rad. This gates a distinct selectable coefficient
21+
path; it is a derived input and does not add a published-case badge.
291 KB
Binary file not shown.

‎tests/matlab/sphere_irregular_mean_drift_reference.m‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
function sphere_irregular_mean_drift_reference(caseSource, coreSource, hydroFile, outDir)
1+
function sphere_irregular_mean_drift_reference(caseSource, coreSource, hydroFile, outDir, driftOption)
22
% Derive a short PM-wave input from the pinned Sphere Mean_Drift application.
3+
if nargin < 5
4+
driftOption = 1;
5+
end
6+
assert(ismember(driftOption, [1, 3]), ...
7+
'Expected control-surface or pressure-integration mean drift');
38
addpath(genpath(coreSource));
49
if ~isfolder(outDir)
510
mkdir(outDir);
@@ -36,6 +41,11 @@ function sphere_irregular_mean_drift_reference(caseSource, coreSource, hydroFile
3641
'Pinned Sphere Mean_Drift input changed');
3742
input = strrep(input, edits{index, 1}, edits{index, 2});
3843
end
44+
if driftOption == 3
45+
oldDrift = 'body(1).meanDrift = 1;';
46+
assert(contains(input, oldDrift), 'Pinned Sphere drift option changed');
47+
input = strrep(input, oldDrift, 'body(1).meanDrift = 3;');
48+
end
3949
geometry = fullfile(fileparts(caseSource), '_Common_Input_Files', ...
4050
'Sphere', 'geometry', 'sphere.stl');
4151
oldGeometry = '''../_Common_Input_Files/Sphere/geometry/sphere.stl''';
@@ -54,7 +64,7 @@ function sphere_irregular_mean_drift_reference(caseSource, coreSource, hydroFile
5464
strcmp(waves.type, 'irregular') && ...
5565
strcmp(waves.spectrumType, 'PM') && waves.phaseSeed == 1 && ...
5666
waves.height == 0.5 && waves.period == 8 && ...
57-
body(1).meanDrift == 1, 'Derived Sphere settings changed');
67+
body(1).meanDrift == driftOption, 'Derived Sphere settings changed');
5868
record = output.bodies(1);
5969
assert(numel(record.time) == 3001, 'Incomplete Sphere source run');
6070
assert(all(isfinite(record.forceExcitation), 'all'), ...

‎tests/test_case_dynamics.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,21 @@ def test_mapped_pto_equilibrium_changes_generalized_force():
145145
rtol=0, atol=1e-12)
146146

147147

148-
def test_mean_drift_requires_the_selected_h5_coefficient():
148+
@pytest.mark.parametrize("option", [
149+
"control_surface", "momentum_conservation", "pressure_integration",
150+
])
151+
def test_mean_drift_requires_the_selected_h5_coefficient(option):
149152
hydro = ROOT / "examples/data/rm3.h5"
150153
case = {
151154
"simulation": {"dt": 0.1, "end_time": 0,
152155
"radiation_memory": 0.1},
153156
"wave": {"type": "regularCIC", "height": 0.1, "period": 8},
154157
"bodies": [{"hydro_file": str(hydro),
155-
"mean_drift": "control_surface",
158+
"mean_drift": option,
156159
"coordinate_map": [[0], [0], [1], [0], [0], [0]]}],
157160
"constraint": {"kind": "linear_subspace"},
158161
}
159-
with pytest.raises(ValueError, match="lacks finite control_surface"):
162+
with pytest.raises(ValueError, match=f"lacks finite {option}"):
160163
run_case(case)
161164

162165

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Pair WEC-Sim's pressure-integration spectral mean-drift option."""
2+
3+
from pathlib import Path
4+
5+
import h5py
6+
import numpy as np
7+
from scipy.interpolate import CubicSpline
8+
9+
from wecsim import PMWave, WEC
10+
from wecsim.environment.irregularWave import pm_equal_energy_components
11+
12+
13+
FIXTURE = Path(__file__).parent / "fixtures/matlab/sphere_pressure_drift"
14+
HYDRO = Path(__file__).parent / "fixtures/matlab/sphere_mean_drift/sphere.h5"
15+
16+
17+
def test_pressure_integration_drift_against_pinned_matlab():
18+
with np.load(FIXTURE / "source.npz") as source:
19+
body, wave, recorded, mass = (source[key] for key in
20+
("body", "wave", "components", "mass"))
21+
assert (body.shape, wave.shape, recorded.shape, mass.shape) == (
22+
(3001, 19), (3001, 2), (500, 4), (4,),
23+
)
24+
assert all(np.isfinite(values).all() for values in
25+
(body, wave, recorded, mass))
26+
27+
components = pm_equal_energy_components(
28+
HYDRO, significant_height=.5, peak_period=8,
29+
directions=[0], spreading=[1], seed=1, phase_generator="matlab",
30+
)
31+
for actual, expected in (
32+
(components.omega, recorded[:, 0]),
33+
(components.spectral_amplitude, recorded[:, 1]),
34+
(components.d_omega, recorded[:, 2]),
35+
(components.phase[:, 0], recorded[:, 3]),
36+
):
37+
np.testing.assert_allclose(actual, expected, rtol=0, atol=1e-12)
38+
39+
# Compute the selected mean-drift term directly from the HDF5 rather
40+
# than asking the production irregular-wave builder for its result.
41+
with h5py.File(HYDRO) as h5:
42+
frequency = np.asarray(h5["simulation_parameters/w"]).ravel()
43+
coefficient = np.asarray(h5[
44+
"body1/hydro_coeffs/mean_drift/pressure_integration/val"
45+
])[:, 0, :]
46+
expected_drift = (
47+
CubicSpline(frequency, coefficient, axis=1)(components.omega)
48+
@ (components.spectral_amplitude * components.d_omega)
49+
) * 1000 * 9.81
50+
assert np.max(abs(expected_drift)) > 1
51+
52+
wec = WEC("Sphere pressure-integration mean drift")
53+
sphere = wec.body("sphere", HYDRO, inertia=mass[1:],
54+
mean_drift="pressure_integration")
55+
for axis in ("surge", "heave", "pitch"):
56+
wec.coordinate(axis, sphere.move(axis))
57+
result = wec.run(
58+
PMWave(.5, 8, seed=1, phase_generator="matlab"),
59+
dt=.01, end_time=30, ramp_time=10, radiation_memory=10,
60+
)
61+
output = dict(result.raw.extra_outputs)
62+
ramp = np.where(result.time < 10,
63+
(1 - np.cos(np.pi * result.time / 10)) / 2, 1)
64+
np.testing.assert_allclose(result.time, body[:, 0], rtol=0, atol=1e-9)
65+
np.testing.assert_allclose(result.wave_elevation, wave[:, 1],
66+
rtol=0, atol=1e-10)
67+
np.testing.assert_allclose(output["body1_mean_drift_force"],
68+
ramp[:, None] * expected_drift,
69+
rtol=0, atol=1e-6)
70+
assert np.max(abs(output["body1_excitation_force"]
71+
- body[:, 13:19])) < 1e-5
72+
for axis, position_limit, speed_limit in (
73+
(0, .003, .004),
74+
(2, .003, .004),
75+
(4, 1e-4, 5e-5),
76+
):
77+
position = result.bodies["sphere"].position[:, axis]
78+
speed = result.bodies["sphere"].velocity[:, axis]
79+
assert np.max(abs(position - body[:, 1 + axis])) < position_limit
80+
assert np.max(abs(speed - body[:, 7 + axis])) < speed_limit

‎wecsim/dynamics/subspace_case.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@ def _run_linear_subspace(case, sim, wave, constraint, bodies, hydro,
128128
body.bodyTotal = len(bodies)
129129
drift_option = bodies[index - 1].get("mean_drift", "none")
130130
drift_flags = {"none": 0, "control_surface": 1,
131-
"momentum_conservation": 2}
131+
"momentum_conservation": 2, "pressure_integration": 3}
132132
if not isinstance(drift_option, str) or drift_option not in drift_flags:
133133
raise ValueError(
134-
"body.mean_drift must be none, control_surface, or momentum_conservation"
134+
"body.mean_drift must be none, control_surface, momentum_conservation, or pressure_integration"
135135
)
136136
body.meanDriftForce = drift_flags[drift_option]
137137
body.readH5file()

‎wecsim/environment/irregularWave.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,9 +395,9 @@ def synthesize_irregular_response(
395395
if excitation_interpolation not in ("linear", "spline"):
396396
raise ValueError("excitation_interpolation must be linear or spline")
397397
drift_flags = {"none": 0, "control_surface": 1,
398-
"momentum_conservation": 2}
398+
"momentum_conservation": 2, "pressure_integration": 3}
399399
if not isinstance(mean_drift, str) or mean_drift not in drift_flags:
400-
raise ValueError("mean_drift must be none, control_surface, or momentum_conservation")
400+
raise ValueError("mean_drift must be none, control_surface, momentum_conservation, or pressure_integration")
401401
steps = round(end_time / dt)
402402
if not np.isclose(steps * dt, end_time, rtol=0, atol=1e-10):
403403
raise ValueError("end_time must be an integer multiple of dt")

‎wecsim/hydrodynamics/bodyClass.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def inputFileProperties(self):#input file properties
126126
'rgME': [0,0,0]} # Vector from center of gravity to point of application for Morrison Element (format [X, Y, Z], default = [0 0 0]).
127127
self.nhBody = 0 # Flag for non-hydro body.
128128
self.flexHydroBody = 0 # Flag for flexible body.
129-
self.meanDriftForce = 0 # Flag for mean drift force. 0: No 1: from control surface 2: from momentum conservation.
129+
self.meanDriftForce = 0 # 0: none, 1: control surface, 2: momentum, 3: pressure integration.
130130

131131
def bodyGeometryFileProperties(self): # body geometry stl file properties
132132
self.bodyGeometry = { # Structure defining body's mesh
@@ -277,6 +277,8 @@ def readH5file(self):
277277
self.hydroData['hydro_coeffs']['mean_drift'] = np.array(f.get(name + '/hydro_coeffs/mean_drift/control_surface/val'))
278278
elif self.meanDriftForce == 2:
279279
self.hydroData['hydro_coeffs']['mean_drift'] = np.array(f.get(name + '/hydro_coeffs/mean_drift/momentum_conservation/val'))
280+
elif self.meanDriftForce == 3:
281+
self.hydroData['hydro_coeffs']['mean_drift'] = np.array(f.get(name + '/hydro_coeffs/mean_drift/pressure_integration/val'))
280282
else:
281283
warnings.warn("Wrong flag for mean drift force.",DeprecationWarning)
282284
f.close()

0 commit comments

Comments
 (0)