Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,10 @@ stop repeating that comparison. In parallel, raise meaningful combined
Python line-and-branch coverage from the measured 79% toward 90% with behavior
and numerical invariants, including portable full-duration gates for already
paired cases. Do not add tests written only to execute lines.
Both already-gated WaveBot published inputs now have portable full-duration
ordinary-CI comparisons: the 100 s adaptive controller and the 600 s
impedance experiment. These strengthen regression coverage without changing
the WaveBot case count or the physical damping default.

| Other published cases | Gated / inventoried |
| --- | ---: |
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/matlab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

The published 100 s WaveBot adaptive controller input and trajectory are
documented in [wavebot_control/README.md](wavebot_control/README.md).
The published 600 s WaveBot impedance experiment is documented in
[wavebot_impedance/README.md](wavebot_impedance/README.md).

## Fixed hydrodynamic monopile

Expand Down
33 changes: 33 additions & 0 deletions tests/fixtures/matlab/wavebot_impedance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Published WaveBot impedance experiment

The [successful 600 s R2025b source run](https://github.com/cmudrc/wec-sim-python/actions/runs/38465422104)
produced `matlab-WAVEBOT_IMPEDANCE-baseline` at repo commit
`794655800211282d1f955a71f8659e11e500002c`. The workflow pins WEC-Sim
core `0753b2e47f2457c078751dcfe5d251d1767b80ab` and Applications
`d53d4d4c9eda2581f04204f5d394a6ef84bb099e`.

`waveBotBuoy.h5` is the source run's exact generated hydrodynamic file (SHA-256
`6449a44c94925dd2e149f3c0ffbd5f7941677f51989781fe762982b9f237a20a`).
`multisine3DOFA.mat` is copied without modification from the pinned
Applications `Load_Mitigating_Controls/CalcImpedance` input (SHA-256
`dba5dbc8ac72bb20054278c0ea1695c2c03784be9bb2e8af5efc0b26313888a7`).

`published_600s.npz` stores float64 values from all 60,001 published body
samples and the raw actuation time series. No source position, velocity, or
force is fed into the Python integrator. Its SHA-256 is
`df5dd29c28cf0726cfa9e0590820b2cf44a47091e31c56ab94f0229764dd72de`.
The extraction is:

| Source CSV suffix | SHA-256 | Archive data |
| --- | --- | --- |
| `CalcImpedance_body1` | `926f37cadb5e7be38738676fde63bf65a0a7bddbe8e116f9d35b3062ce91d302` | time 0, position 1/3/5, speed 7/9/11 |
| `actuation` | `858eaabffde6b63efd2453dac1d1aec74d73b5662288a2d267bc16f13538f685` | all columns |
| `mooring` | `dc9ca764ecf3fac72b949d9fcf9e9556b1eb2d1ea46d5d42c79cefd925267775` | force 13/15/17 |
| `body1_forces` | `0fd447b08f144930c1a0a8a37176294226ad5cfac9982473e144a5d3f6cc5a47` | negative damping 25/27/29 and drag 19/21/23 |

`test_wavebot_impedance_parity.py` uses this fixture in ordinary Python CI
and the full CSVs during a live MATLAB source workflow. Both paths apply the
same preexisting 600 s motion and force gates. The published input's
linearly indexed damping matrix is selected explicitly; the second test
checks that the dissipative diagonal physical default remains distinct.
This case was already counted in the WaveBot badge before the portable gate.
Binary file not shown.
Binary file not shown.
Binary file not shown.
80 changes: 51 additions & 29 deletions tests/test_wavebot_impedance_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,75 +12,97 @@
REFERENCE = os.environ.get("WEC_SIM_MATLAB_MODEL_OUTPUT_DIR")
HYDRO = os.environ.get("WEC_SIM_WAVEBOT_H5")
MULTISINE = os.environ.get("WEC_SIM_WAVEBOT_MULTISINE")
pytestmark = pytest.mark.skipif(
not (REFERENCE and HYDRO and MULTISINE),
reason="pinned WaveBot output, hydrodynamics, and multisine are absent",
)
PORTABLE = Path(__file__).parent / "fixtures/matlab/wavebot_impedance"


def _source(name):
return np.loadtxt(
values = np.loadtxt(
Path(REFERENCE) / f"WAVEBOT_IMPEDANCE_{name}.csv",
delimiter=",", ndmin=2,
)
assert np.isfinite(values).all(), name
return values


@pytest.fixture(scope="module")
def comparison():
return (
_source("CalcImpedance_body1"),
_source("body1_forces"),
_source("mooring"),
_source("actuation"),
run_wavebot_impedance(HYDRO, MULTISINE,
source_linear_damping=True),
)
if any((REFERENCE, HYDRO, MULTISINE)) and not all((REFERENCE, HYDRO, MULTISINE)):
pytest.fail("live WaveBot comparison needs source output, HDF5, and multisine")
if REFERENCE:
body = _source("CalcImpedance_body1")
force = _source("body1_forces")
mooring = _source("mooring")
actuation = _source("actuation")
assert body.shape == (60_001, 25)
assert force.shape == (60_001, 37)
assert mooring.shape == (60_001, 19)
reference = {
"body_time": body[:, 0],
"body_position": body[:, [1, 3, 5]],
"body_velocity": body[:, [7, 9, 11]],
"actuation": actuation,
"mooring_force": mooring[:, [13, 15, 17]],
"linear_damping_force": -force[:, [25, 27, 29]],
"drag_force": -force[:, [19, 21, 23]],
}
hydro, multisine = HYDRO, MULTISINE
else:
with np.load(PORTABLE / "published_600s.npz") as saved:
reference = {name: saved[name].copy() for name in saved.files}
hydro = PORTABLE / "waveBotBuoy.h5"
multisine = PORTABLE / "multisine3DOFA.mat"
return reference, run_wavebot_impedance(
hydro, multisine, source_linear_damping=True,
), hydro, multisine


def test_published_wavebot_coupled_motion_and_force_channels(comparison):
body, force, mooring, actuation, result = comparison
assert body.shape == (60_001, 25)
np.testing.assert_allclose(result.time, body[:, 0], rtol=0, atol=1e-8)
source, result, _, _ = comparison
assert source["body_time"].shape == (60_001,)
np.testing.assert_allclose(result.time, source["body_time"], rtol=0, atol=1e-8)

active = [0, 2, 4]
position_error = np.max(
np.abs(result.body_position[:, active] - body[:, [1, 3, 5]]), axis=0,
np.abs(result.body_position[:, active] - source["body_position"]), axis=0,
)
velocity_error = np.max(
np.abs(result.body_velocity[:, active] - body[:, [7, 9, 11]]), axis=0,
np.abs(result.body_velocity[:, active] - source["body_velocity"]), axis=0,
)
np.testing.assert_array_less(position_error, [.0002, .0003, .0004])
np.testing.assert_array_less(velocity_error, [.0012, .001, .0018])

sampled_command = np.column_stack([
np.interp(result.time, actuation[:, 0], actuation[:, i])
np.interp(result.time, source["actuation"][:, 0],
source["actuation"][:, i])
for i in (1, 2, 3)
])
sampled_command[:, 2] *= -1
np.testing.assert_allclose(result.applied_force, sampled_command,
rtol=0, atol=1e-8)
force_pairs = (
(result.mooring_force, mooring[:, 13:19], [4, 1.3, .4]),
(result.linear_damping_force, -force[:, 25:31], [1.3, 1.3, .15]),
(result.drag_force, -force[:, 19:25], [.2, .8, .5]),
(result.mooring_force, source["mooring_force"], [4, 1.3, .4]),
(result.linear_damping_force,
source["linear_damping_force"], [1.3, 1.3, .15]),
(result.drag_force, source["drag_force"], [.2, .8, .5]),
)
for python_force, matlab_force, limits in force_pairs:
error = np.max(np.abs(python_force[:, active]
- matlab_force[:, active]), axis=0)
error = np.max(np.abs(python_force[:, active] - matlab_force), axis=0)
np.testing.assert_array_less(error, limits)


def test_ordinary_diagonal_damping_stays_the_default(comparison):
body, _, _, _, source_compatible = comparison
physical = run_wavebot_impedance(HYDRO, MULTISINE)
source, source_compatible, hydro, multisine = comparison
physical = run_wavebot_impedance(hydro, multisine)
# Each positive diagonal coefficient dissipates power independently.
damping_power = np.sum(
physical.linear_damping_force * physical.body_velocity, axis=1,
)
assert np.max(damping_power) < 1e-10
# MATLAB's linearly indexed matrix is different from that physical
# default. Keep the source convention isolated and report both traces.
assert np.max(np.abs(physical.body_position[:, 2] - body[:, 3])) > .01
assert np.max(np.abs(physical.body_position[:, 4] - body[:, 5])) > .005
assert np.max(np.abs(physical.body_position[:, 2]
- source["body_position"][:, 1])) > .01
assert np.max(np.abs(physical.body_position[:, 4]
- source["body_position"][:, 2])) > .005
assert np.max(np.abs(source_compatible.body_position[:, 2]
- body[:, 3])) < .0003
- source["body_position"][:, 1])) < .0003
Loading