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
6 changes: 3 additions & 3 deletions PARITY.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions tests/fixtures/matlab/oswec_multi_wave/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ the saved phases, advances the public hinge over 100 s, and checks wave,
both-body excitation, motion, PTO torque, and mechanical power against the
existing published-case limits. This makes an already counted OSWEC case
durable in ordinary Python CI; it does not add a case badge.

The test also gates absorbed PTO work in 0–20 s, 20–60 s, 60–100 s, and full
100 s windows at 5% relative error. The largest observed window gap is
3.913%; source work is computed from its logged mechanical-power history.
6 changes: 6 additions & 0 deletions tests/fixtures/matlab/rm3_pto_pretension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ The bundled `examples/data/rm3.h5` SHA-256 is
The test still accepts fresh MATLAB CSVs when they are supplied by the
reference-model workflow. This configured PTO is an RM3 example variant,
not an additional published input in the 63-case inventory.

The paired test also computes irreversible damper work as
`1_200_000 * pto_speed**2` from MATLAB's saved PTO speed. It gates separate
0–100 s, 100–250 s, 250–400 s, and full 400 s windows against the public
Python absorbed-power history at 1% relative error. The largest observed
window gap is 0.7934%.
8 changes: 5 additions & 3 deletions tests/fixtures/matlab/sphere_variable_mass/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ e20b75b6d1da9437ec37ba3d1aecc8bf043893709ccd099014b9e141d3c8d73c
The ordinary Python test extracts these files into a temporary directory,
checks source state selection and force assembly at all 9,001 source output
times, then independently advances the public `WEC.variable_body` for the full
900 s. It uses the same numerical limits as the fresh MATLAB workflow. This
adds a durable regression for an already-gated case; it does not add a new
badge case.
900 s. It uses the same motion and force limits as the fresh MATLAB workflow.
The additional PTO speed, pointwise absorbed-power, and integrated-work gates
are 0.005 m/s, 2.5 kW, and 1%, respectively; the measured maxima are
0.004135 m/s, 1.052 kW, and 0.00376% work. This tightens an already-gated
case; it does not add a new badge case.
4 changes: 2 additions & 2 deletions tests/reference_case_status.csv
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Multiple_Condition_Runs/RM3_MCROPT1,independent_motion,
Multiple_Condition_Runs/RM3_MCROPT2,independent_motion,
Multiple_Condition_Runs/RM3_MCROPT3,independent_motion,
Multiple_Condition_Runs/RM3_MCROPT3_SeaState,independent_motion,
Multiple_Wave_Spectra,independent_motion,
Multiple_Wave_Spectra,independent_motion,Full 100 s two-sea motion PTO power and absorbed work independently pair across three disjoint intervals and the complete run.
Nonhydro_Body,independent_motion,
Nonlinear_Hydro/ode4/Regular,independent_motion,
Nonlinear_Hydro/ode4/RegularCIC,independent_motion,
Expand All @@ -56,7 +56,7 @@ RM3_PTO_Extension/float,qualified_motion,Published-step opt-in source ode4 pairs
RM3_PTO_Extension/spar,qualified_motion,Published-step opt-in source ode4 pairs; physical Python pairs a derived full-duration FixedStep-only source and exposes published ode4 step sensitivity.
Radiation_Force_Options,independent_motion,"All four radiation modes have motion gates; the fitted state-space option is nonpassive."
Variable_Hydro/Passive_Yaw,mixed_modes,Published regular-wave motion and opt-in source ode4 numerical irregular motion are gated; the physical default differs and the MATLAB source is step-sensitive.
Variable_Hydro/Variable_Mass,independent_motion,
Variable_Hydro/Variable_Mass,independent_motion,Full 900 s body motion and PTO stroke force speed absorbed power and integrated work independently pair across all nine draft states.
WECCCOMP/WECCCOMP,independent_motion,
WECCCOMP/WECCCOMP_Fault_Implementation,independent_motion,
WECCCOMP/WECCCOMP_Nonlinear_Model_Predictive,qualified_motion,Independent full 225 s opt-in source-ode8 plant and NMPC pass unchanged pitch/speed/PTO-force/work gates for Apple silicon Python against the Mali R2025b source. Against the archived Linux R2025b source one Linux x86 runner passes the same gates at 0.007327 rad pitch and 2.980 N force while another fails at 0.013109 rad and 6.034 N with the same phases and package versions; no durable Linux x86 pointwise gate is established. Local ARM and Rosetta x86 Python plants given identical MATLAB torque requests differ by only 2.36e-16 rad pitch over 225 s while their autonomous controller runs differ by 0.011797 rad; holding source stroke and prior request reduces active controller output difference to 8.98e-7 N m. Mali and archived Linux MATLAB use identical HDF5 and waves but differ by up to 0.007543 rad pitch under feedback. With published MATLAB torque requests prescribed the refined physical Python plant still misses pitch by 0.03652 rad and PTO work by 6.84%. The physical default misses its aggregate split-step pitch gate. Portable physical parity remains open and the strict badge count is unchanged.
Expand Down
13 changes: 13 additions & 0 deletions tests/test_oswec_multi_wave_portable.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,16 @@ def test_published_two_sea_oswec_motion_and_pto(tmp_path):
"PTO torque (N m)")
_max_error(-result.ptos["hinge"].absorbed_power, source_pto[:, 23], 350,
"PTO mechanical power (W)")
source_power = -source_pto[:, 23]
python_power = result.ptos["hinge"].absorbed_power
# The pointwise 350 W gate alone allows a biased work integral. Bound
# three disjoint intervals as well as the complete published run.
for start, stop in ((0, 20), (20, 60), (60, 100), (0, 100)):
window = (time >= start) & (time <= stop)
source_work = np.trapezoid(source_power[window], time[window])
python_work = np.trapezoid(python_power[window], time[window])
assert source_work > 10_000
relative_gap = abs(python_work - source_work) / source_work
assert relative_gap < .05, (
f"PTO work {start}–{stop} s: {relative_gap:.4%} exceeds 5%"
)
16 changes: 16 additions & 0 deletions tests/test_rm3_pto_pretension_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,19 @@ def test_rm3_configured_spring_and_pretension_against_matlab():
_max_error(-history.force * history.velocity,
-source["pto_mechanical_power"],
8_000, "PTO mechanical power")

# The source logs spring exchange and damping together as mechanical
# power. Compare irreversible damper work from its independently saved
# PTO speed, including separate ramp and steady windows.
source_damper_power = 1_200_000 * source["pto_velocity"]**2
for start, stop in ((0, 100), (100, 250), (250, 400), (0, 400)):
window = (time >= start) & (time <= stop)
source_work = np.trapezoid(source_damper_power[window], time[window])
python_work = np.trapezoid(
history.absorbed_power[window], result.time[window],
)
assert source_work > 1e6
relative_gap = abs(python_work - source_work) / source_work
assert relative_gap < .01, (
f"PTO damper work {start}–{stop} s: {relative_gap:.4%} exceeds 1%"
)
14 changes: 14 additions & 0 deletions tests/test_sphere_variable_mass_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ def test_source_uses_all_nine_draft_states_and_matches_bem_forces(paired_paths):
"source hydrodynamic force assembly")
_max_error(-200_000 * body[:, 9], pto[:, 15], 1e-6,
"source PTO damping")
_max_error(pto[:, 9], body[:, 9], 1e-10,
"source PTO speed")
_max_error(pto[:, 21], pto[:, 15] * pto[:, 9], 1e-8,
"source PTO power")


def test_public_python_variable_body_tracks_published_trajectory(paired_paths):
Expand All @@ -143,6 +147,16 @@ def test_public_python_variable_body_tracks_published_trajectory(paired_paths):
.003, "PTO stroke")
_max_error(result.ptos["PTO1"].force[sample], pto[:, 15],
1_000, "PTO force")
_max_error(result.ptos["PTO1"].velocity[sample], pto[:, 9],
.005, "PTO speed")
# The 2.5 kW power limit follows the existing .005 m/s speed limit,
# 200 kN s/m damper, and the source's approximately 1.17 m/s peak speed.
python_power = result.ptos["PTO1"].absorbed_power[sample]
source_power = -pto[:, 21]
_max_error(python_power, source_power, 2_500, "PTO absorbed power")
python_work = np.trapezoid(python_power, result.time[sample])
source_work = np.trapezoid(source_power, pto[:, 0])
assert abs(python_work - source_work) / source_work < .01
_max_error(result.wave_elevation, wave[:, 1], 1e-12,
"wave elevation")
np.testing.assert_array_equal(extras["body1_hydro_state"][sample],
Expand Down
Loading