From 9d189c592515b38373c620b289cd11d97281b1f5 Mon Sep 17 00:00:00 2001 From: Chris McComb Date: Tue, 20 Oct 2026 00:19:59 -0400 Subject: [PATCH 1/4] Gate published RM3 End Stops full-run damper work --- PARITY.md | 10 +++++-- tests/reference_case_status.csv | 2 +- tests/test_rm3_end_stops_source_timing.py | 36 ++++++++++++++++++++--- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/PARITY.md b/PARITY.md index 4e5e9aeb..6b6407cf 100644 --- a/PARITY.md +++ b/PARITY.md @@ -72,7 +72,7 @@ saves its MATLAB outputs as an artifact. | RM3 body-to-body Cases 3 and 4 | Pinned MATLAB Applications `regularCIC` cases, with coupling off and on | Both the ordinary implicit-mass solver and the opt-in `simulink_delay` comparison are paired over 400 s. The ordinary solver differs by at most 67.2 mm surge, 3.64 mm heave, 0.00162 rad pitch, and 4.78 kN PTO force across both bodies and cases. A [full-duration public `WEC.floating_joint` gate](tests/test_rm3_cic_public_work.py) independently advances both ordinary convolution cases and compares body motion, PTO stroke, speed, force, power, and integrated work against the archived MATLAB trajectories. The source and Python absorbed works are 93.062/93.710 MJ in Case 3 and 95.497/96.178 MJ in Case 4, or 0.696% and 0.713% differences, within the predeclared 1% work gate. The source-numerics setting reduces the motion and force maxima to 28.2 mm surge, 0.253 mm heave, 0.0000325 rad pitch, and 0.292 kN PTO force; PTO stroke, speed, and power then differ by at most 0.235 mm, 0.244 mm/s, and 0.392 kW. Cases 5 and 6 use MATLAB's fitted radiation state-space model and have separate opt-in numerical motion gates below. | | RM3 MooringMatrix with imported elevation | Pinned MATLAB Applications `Mooring/MooringMatrix` input, its 3,600 s `etaData.mat`, and BEMIO-generated RM3 HDF5 | The public `run_case` interface loads the MAT elevation, applies the configured joint surge spring, and runs the full 400 s comparison. The source wave agrees with Python interpolation and ramp to `8e-14` m. Production `BodyClass` uses the source's linear excitation-IRF interpolation and noncausal `same` convolution; an explicit opt-in reproduces the source body block's additional force ramp. All logged excitation-force components agree within `1e-3` N or N m. The logged mooring force equals `−100,000` N/m times its surge displacement within `1e-5` N. On all 40,001 samples, implicit-added-mass dynamics agree within 9.77 mm surge, 2.53 mm heave, 0.000605 rad pitch, 2.55 mm PTO stroke, 0.279 mm/s PTO speed, 334 N PTO force, and 1.17 kN mooring force. General mooring layouts remain unsupported. | | RM3 MoorDyn coupled mooring | Pinned MATLAB Applications `Mooring/MoorDyn`, its 3,600 s elevation input, BEMIO RM3 HDF5, and native MoorDyn v2; [fresh 40,001-sample source baseline](https://github.com/cmudrc/wec-sim-python/actions/runs/37886952244) | The public Python `WEC.floating_joint(..., moordyn=MoorDyn(...), moordyn_point=spar.at(0,0,21.5))` configuration advances native MoorDyn from its own predicted connection motion and feeds the resulting six-component load back to the constrained two-body solver once per 0.01 s step. The 400 s paired run uses the full imported-elevation record and the source body block’s second force ramp; every saved source wave-force component matches to numerical precision. A local ARM64 run against the pinned MATLAB output differs by at most 0.785 mm spar surge, 0.0277 mm spar heave, 0.0147 mrad pitch, 116 N PTO force, 61.2 N surge connection force, 41.4 N heave connection force, 573 N m pitch moment, and 42.7 N fairlead tension. Explicit CI gates compare both bodies’ active positions and speeds, PTO force, six-component connection pose/velocity/force, and all three fairlead tensions over all 40,001 samples. The force is evaluated at a predicted pose once per step because legacy MoorDyn cannot roll back an implicit force trial. This validates the reduced RM3 floating joint and published mooring attachment, not arbitrary mooring layouts or the ParaView output. | -| RM3 End_Stops force law and refined-step trajectory | Pinned MATLAB Applications `End_Stops` input; R2025b runs at published 0.1 s and diagnostic 0.025/0.0125 s steps through the published 400 s duration | Effective stroke bounds are ±0.6 m, each spring is 100 MN/m, stop damping is zero, and the 0.0001 m transition is active. The source's logged stop force matches the Python smooth-stop law within `1e-5` N even at saved samples inside the transition. The 0.025-to-0.0125 s source stroke difference is at most 2.79 mm over 400 s; the published 0.1 s run differs from the refined source by up to 286 mm after contact. The opt-in Python adaptive solver with implicit added mass agrees with the 0.0125 s MATLAB trajectory over 400 s within 0.986 mm PTO stroke, 6.77 mm/s PTO speed, 98.2 kN PTO force, and 0.139% ordinary damper energy. Paired gates cover both active body positions and velocities, PTO motion and force, and integrated energy; a separate source convergence gate checks both refined MATLAB steps. The public `WEC.run(..., solver="wecsim_ode45")` now independently pairs the published 120 s prefix through its configured floating joint; the complete published 400 s case remains qualified. | +| RM3 End_Stops force law and refined-step trajectory | Pinned MATLAB Applications `End_Stops` input; R2025b runs at published 0.1 s and diagnostic 0.025/0.0125 s steps through the published 400 s duration | Effective stroke bounds are ±0.6 m, each spring is 100 MN/m, stop damping is zero, and the 0.0001 m transition is active. The source's logged stop force matches the Python smooth-stop law within `1e-5` N even at saved samples inside the transition. The 0.025-to-0.0125 s source stroke difference is at most 2.79 mm over 400 s; the published 0.1 s run differs from the refined source by up to 286 mm after contact. The opt-in Python adaptive solver with implicit added mass agrees with the 0.0125 s MATLAB trajectory over 400 s within 0.986 mm PTO stroke, 6.77 mm/s PTO speed, 98.2 kN PTO force, and 0.139% ordinary damper energy. Paired gates cover both active body positions and velocities, PTO motion and force, and integrated energy; a separate source convergence gate checks both refined MATLAB steps. The public `WEC.run(..., solver="wecsim_ode45")` independently pairs the published 120 s prefix and gates 400 s total damper work within the existing 0.7% limit; the complete published-motion case remains qualified. | | Public RM3 MCR eight-condition PTO sweep | The pinned 400 s R2025b Option 1 array outputs, [archived without rounding](tests/fixtures/matlab/rm3_mcr_array/README.md) | An [ordinary-CI public `WEC.floating_joint` gate](tests/test_rm3_mcr_public_portable.py) independently advances all eight published height × period × damping settings. With the explicit source-delay mass-feedback option, maximum active body differences are 75.1 mm surge, 0.386 mm heave, and 0.000181 rad pitch; PTO stroke, speed, force, and absorbed power differ by at most 0.412 mm, 0.231 mm/s, 478 N, and 485 W. Integrated PTO work differs by at most 0.0597% and the eight MCR mean powers by at most 155 W. A separate check keeps the ordinary implicit-mass default dissipative across all eight conditions and its PTO work within 0.697% of source, while its larger motion gap is not claimed as source parity. These are stronger output gates for already counted inputs; the badge does not change. | | RM3 radiation force options: constant, convolution, FIR, and state-space | Pinned MATLAB Applications `Radiation_Force_Options`, BEMIO-generated RM3 HDF5, 12 s regular waves | All four published 500 s settings are checked against the Python floating-joint solver. The FIR path uses sampled kernel taps with a held radiation force during each RK4 step. Across all four cases, the largest differences are 30.7 mm surge, 2.11 mm heave, 0.00144 rad pitch, 3.25 mm PTO stroke, 1.55 mm/s PTO speed, 1.86 kN PTO force, and 1.57 kW PTO power. Full-run and three interval PTO-work gates add a 1% bound for each mode; locally the largest interval relative differences are 0.546% constant, 0.027% convolution, 0.012% FIR, and 0.432% state-space. Direct FIR radiation-force differences remain under their paired gates. MATLAB FIR differs from MATLAB convolution by up to 345 mm surge, so the methods are not interchangeable for this case. The opt-in `radiation_method="state_space"` advances its 72 active fitted states with the independent RM3 body motion and passes the same full-duration gates; its force law on prescribed source motion is checked separately below. The fitted model has negative low-frequency damping, so this is numerical source parity, not physical passivity evidence. | | RM3 state-space radiation force on prescribed motion | Pinned MATLAB Applications `Radiation_Force_Options` state-space setting and [focused R2025b gate](https://github.com/cmudrc/wec-sim-python/actions/runs/37935700135) | `wecsim.hydrodynamics.radiation.replay_rm3_fitted_radiation` advances the HDF5 A/B/C channel fits from zero state using the saved six-component velocity histories, scales C by water density, and sets D to zero as the pinned MATLAB block does. Over 5,001 samples on both bodies, every radiation-force channel differs by less than `0.026%` of its source peak; the gate allows `0.03%` with a `0.0009` N floor for nearly stationary channels. This is a prescribed-motion force replay, not an independently coupled trajectory or evidence that the fitted model is passive. The default convolution solver is unchanged. | @@ -3217,7 +3217,13 @@ conditioned on source solver times; it isolates the remaining step-selection gap and does not promote `End_Stops` or the RM3 badge. Without prescribed times, the improved 400 s source-style solver still reaches `0.277` m PTO-stroke error and `0.431%` less damper work than the published MATLAB -trajectory. The published full-duration motion gate remains red. +trajectory. The public `WEC.run` path now keeps that full-duration total-work +comparison under the existing `0.7%` End Stops limit in ordinary CI: +`60.678` MJ Python versus `60.941` MJ published MATLAB (`0.4307%` gap). +This is an aggregate gate only. Its 100–200 s and 200–300 s work windows +differ by `0.7993%` and `0.9021%`, above the same `0.7%` limit; the +published full-duration motion gate remains red. The opt-in source-style +work remains about 10% above the refined physical trajectory's work. In a separate local 90 s published-step source run, MATLAB's Solver Profiler recorded zero zero-crossing events and zero solver exceptions; its only solver diff --git a/tests/reference_case_status.csv b/tests/reference_case_status.csv index 5891facc..939c9a9b 100644 --- a/tests/reference_case_status.csv +++ b/tests/reference_case_status.csv @@ -15,7 +15,7 @@ Controls/Passive (P),independent_motion, Controls/Reactive (PI),independent_motion, Controls/ReactiveWithPTO,independent_motion, Desalination,independent_motion, -End_Stops,qualified_motion,Full-duration refined-step and MaxStep-only derived-source motion independently pair with Python; source-timed delayed-mass replay pairs the published 400 s trajectory but uses MATLAB's accepted solver steps. Independently selected source-state ode45 motion now pairs the published first 120 s through public WEC.run; the unchanged full-duration case remains step-sensitive. +End_Stops,qualified_motion,Full-duration refined-step and MaxStep-only derived-source motion independently pair with Python; source-timed delayed-mass replay pairs the published 400 s trajectory but uses MATLAB's accepted solver steps. Independently selected source-state ode45 motion pairs the published first 120 s through public WEC.run; its independent 400 s total damper work passes the existing 0.7% gate at 0.4307%, but two 100 s windows exceed 0.7% and published full-duration motion remains step-sensitive. Free_Decay/0m,independent_motion, Free_Decay/1m,independent_motion, Free_Decay/1m-ME,independent_motion, diff --git a/tests/test_rm3_end_stops_source_timing.py b/tests/test_rm3_end_stops_source_timing.py index 2d69aa3d..c7b60232 100644 --- a/tests/test_rm3_end_stops_source_timing.py +++ b/tests/test_rm3_end_stops_source_timing.py @@ -96,8 +96,8 @@ def test_independently_selected_source_state_steps_through_120_seconds(): assert abs(python_work - matlab_work) / matlab_work < 1e-5 -def test_public_wec_run_pairs_published_end_stops_through_120_seconds(): - """The configured public WEC uses its own adaptive steps and wave input.""" +def _public_end_stops_wec(): + """Build the published RM3 configuration without source motion inputs.""" wec = WEC("RM3 End Stops") float_body = wec.body("float", HYDRO, inertia=(0, 21_306_090.66, 0)) spar_body = wec.body("spar", HYDRO, inertia=(0, 94_407_091.24, 0)) @@ -106,8 +106,15 @@ def test_public_wec_run_pairs_published_end_stops_through_120_seconds(): hard_stops=LinearHardStops(-.6, .6, 1e8, 1e8), added_mass_scheme="simulink_delay", ) - result = wec.run(RegularWave(2.5, 8), dt=.1, end_time=120, - ramp_time=100, solver="wecsim_ode45") + return wec + + +def test_public_wec_run_pairs_published_end_stops_through_120_seconds(): + """The configured public WEC uses its own adaptive steps and wave input.""" + result = _public_end_stops_wec().run( + RegularWave(2.5, 8), dt=.1, end_time=120, + ramp_time=100, solver="wecsim_ode45", + ) with np.load(FIXTURES / "rm3_end_stops_published_full.npz") as source: length = len(result.time) @@ -140,6 +147,27 @@ def test_public_wec_run_pairs_published_end_stops_through_120_seconds(): assert abs(python_work - matlab_work) / matlab_work < 1e-5 +def test_public_wec_run_published_400s_damper_work(): + """Gate independent full-run PTO work while motion remains qualified.""" + result = _public_end_stops_wec().run( + RegularWave(2.5, 8), dt=.1, end_time=400, + ramp_time=100, solver="wecsim_ode45", + ) + with np.load(FIXTURES / "rm3_end_stops_published_full.npz") as source: + np.testing.assert_allclose(result.time, source["time"], + rtol=0, atol=1e-10) + source_work = np.trapezoid( + 1_200_000 * source["pto"][:, 1]**2, source["time"], + ) + python_work = np.trapezoid( + result.ptos["relative_heave"].absorbed_power, result.time, + ) + assert source_work > 0 + # Reuse the existing 0.7% full-run End Stops damper-work limit. + gap = abs(python_work - source_work) / source_work + assert gap < .007, f"published 400 s damper work gap: {gap:.4%}" + + def test_published_full_duration_with_source_major_step_schedule(): """Gate the force law while explicitly retaining the timing condition.""" with np.load(FIXTURES / "rm3_end_stops_internal_steps.npz") as steps: From 7e2cf6d921800d9ef8d766fc78fffdc9a9adb5ef Mon Sep 17 00:00:00 2001 From: Chris McComb Date: Tue, 20 Oct 2026 00:21:04 -0400 Subject: [PATCH 2/4] Verify published End Stops work is damper dissipation --- tests/test_rm3_end_stops_source_timing.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_rm3_end_stops_source_timing.py b/tests/test_rm3_end_stops_source_timing.py index c7b60232..bc3e8aeb 100644 --- a/tests/test_rm3_end_stops_source_timing.py +++ b/tests/test_rm3_end_stops_source_timing.py @@ -159,9 +159,12 @@ def test_public_wec_run_published_400s_damper_work(): source_work = np.trapezoid( 1_200_000 * source["pto"][:, 1]**2, source["time"], ) - python_work = np.trapezoid( - result.ptos["relative_heave"].absorbed_power, result.time, + pto = result.ptos["relative_heave"] + np.testing.assert_allclose( + pto.absorbed_power, 1_200_000 * pto.velocity**2, + rtol=0, atol=1e-8, ) + python_work = np.trapezoid(pto.absorbed_power, result.time) assert source_work > 0 # Reuse the existing 0.7% full-run End Stops damper-work limit. gap = abs(python_work - source_work) / source_work From 0373f2ab1355f84c0ade39b26f6401a6e851e81e Mon Sep 17 00:00:00 2001 From: Chris McComb Date: Tue, 20 Oct 2026 00:25:32 -0400 Subject: [PATCH 3/4] Quote End Stops status CSV note --- tests/reference_case_status.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/reference_case_status.csv b/tests/reference_case_status.csv index 939c9a9b..2c1e7d70 100644 --- a/tests/reference_case_status.csv +++ b/tests/reference_case_status.csv @@ -15,7 +15,7 @@ Controls/Passive (P),independent_motion, Controls/Reactive (PI),independent_motion, Controls/ReactiveWithPTO,independent_motion, Desalination,independent_motion, -End_Stops,qualified_motion,Full-duration refined-step and MaxStep-only derived-source motion independently pair with Python; source-timed delayed-mass replay pairs the published 400 s trajectory but uses MATLAB's accepted solver steps. Independently selected source-state ode45 motion pairs the published first 120 s through public WEC.run; its independent 400 s total damper work passes the existing 0.7% gate at 0.4307%, but two 100 s windows exceed 0.7% and published full-duration motion remains step-sensitive. +End_Stops,qualified_motion,"Full-duration refined-step and MaxStep-only derived-source motion independently pair with Python; source-timed delayed-mass replay pairs the published 400 s trajectory but uses MATLAB's accepted solver steps. Independently selected source-state ode45 motion pairs the published first 120 s through public WEC.run; its independent 400 s total damper work passes the existing 0.7% gate at 0.4307%, but two 100 s windows exceed 0.7% and published full-duration motion remains step-sensitive." Free_Decay/0m,independent_motion, Free_Decay/1m,independent_motion, Free_Decay/1m-ME,independent_motion, From 78dbda58e2633b06015152edfabe99ab9853a975 Mon Sep 17 00:00:00 2001 From: Chris McComb Date: Tue, 20 Oct 2026 01:09:16 -0400 Subject: [PATCH 4/4] Record nonportable End Stops work audit --- PARITY.md | 17 +++++----- tests/reference_case_status.csv | 2 +- tests/test_rm3_end_stops_source_timing.py | 39 +++-------------------- 3 files changed, 14 insertions(+), 44 deletions(-) diff --git a/PARITY.md b/PARITY.md index 6b6407cf..68d6edad 100644 --- a/PARITY.md +++ b/PARITY.md @@ -72,7 +72,7 @@ saves its MATLAB outputs as an artifact. | RM3 body-to-body Cases 3 and 4 | Pinned MATLAB Applications `regularCIC` cases, with coupling off and on | Both the ordinary implicit-mass solver and the opt-in `simulink_delay` comparison are paired over 400 s. The ordinary solver differs by at most 67.2 mm surge, 3.64 mm heave, 0.00162 rad pitch, and 4.78 kN PTO force across both bodies and cases. A [full-duration public `WEC.floating_joint` gate](tests/test_rm3_cic_public_work.py) independently advances both ordinary convolution cases and compares body motion, PTO stroke, speed, force, power, and integrated work against the archived MATLAB trajectories. The source and Python absorbed works are 93.062/93.710 MJ in Case 3 and 95.497/96.178 MJ in Case 4, or 0.696% and 0.713% differences, within the predeclared 1% work gate. The source-numerics setting reduces the motion and force maxima to 28.2 mm surge, 0.253 mm heave, 0.0000325 rad pitch, and 0.292 kN PTO force; PTO stroke, speed, and power then differ by at most 0.235 mm, 0.244 mm/s, and 0.392 kW. Cases 5 and 6 use MATLAB's fitted radiation state-space model and have separate opt-in numerical motion gates below. | | RM3 MooringMatrix with imported elevation | Pinned MATLAB Applications `Mooring/MooringMatrix` input, its 3,600 s `etaData.mat`, and BEMIO-generated RM3 HDF5 | The public `run_case` interface loads the MAT elevation, applies the configured joint surge spring, and runs the full 400 s comparison. The source wave agrees with Python interpolation and ramp to `8e-14` m. Production `BodyClass` uses the source's linear excitation-IRF interpolation and noncausal `same` convolution; an explicit opt-in reproduces the source body block's additional force ramp. All logged excitation-force components agree within `1e-3` N or N m. The logged mooring force equals `−100,000` N/m times its surge displacement within `1e-5` N. On all 40,001 samples, implicit-added-mass dynamics agree within 9.77 mm surge, 2.53 mm heave, 0.000605 rad pitch, 2.55 mm PTO stroke, 0.279 mm/s PTO speed, 334 N PTO force, and 1.17 kN mooring force. General mooring layouts remain unsupported. | | RM3 MoorDyn coupled mooring | Pinned MATLAB Applications `Mooring/MoorDyn`, its 3,600 s elevation input, BEMIO RM3 HDF5, and native MoorDyn v2; [fresh 40,001-sample source baseline](https://github.com/cmudrc/wec-sim-python/actions/runs/37886952244) | The public Python `WEC.floating_joint(..., moordyn=MoorDyn(...), moordyn_point=spar.at(0,0,21.5))` configuration advances native MoorDyn from its own predicted connection motion and feeds the resulting six-component load back to the constrained two-body solver once per 0.01 s step. The 400 s paired run uses the full imported-elevation record and the source body block’s second force ramp; every saved source wave-force component matches to numerical precision. A local ARM64 run against the pinned MATLAB output differs by at most 0.785 mm spar surge, 0.0277 mm spar heave, 0.0147 mrad pitch, 116 N PTO force, 61.2 N surge connection force, 41.4 N heave connection force, 573 N m pitch moment, and 42.7 N fairlead tension. Explicit CI gates compare both bodies’ active positions and speeds, PTO force, six-component connection pose/velocity/force, and all three fairlead tensions over all 40,001 samples. The force is evaluated at a predicted pose once per step because legacy MoorDyn cannot roll back an implicit force trial. This validates the reduced RM3 floating joint and published mooring attachment, not arbitrary mooring layouts or the ParaView output. | -| RM3 End_Stops force law and refined-step trajectory | Pinned MATLAB Applications `End_Stops` input; R2025b runs at published 0.1 s and diagnostic 0.025/0.0125 s steps through the published 400 s duration | Effective stroke bounds are ±0.6 m, each spring is 100 MN/m, stop damping is zero, and the 0.0001 m transition is active. The source's logged stop force matches the Python smooth-stop law within `1e-5` N even at saved samples inside the transition. The 0.025-to-0.0125 s source stroke difference is at most 2.79 mm over 400 s; the published 0.1 s run differs from the refined source by up to 286 mm after contact. The opt-in Python adaptive solver with implicit added mass agrees with the 0.0125 s MATLAB trajectory over 400 s within 0.986 mm PTO stroke, 6.77 mm/s PTO speed, 98.2 kN PTO force, and 0.139% ordinary damper energy. Paired gates cover both active body positions and velocities, PTO motion and force, and integrated energy; a separate source convergence gate checks both refined MATLAB steps. The public `WEC.run(..., solver="wecsim_ode45")` independently pairs the published 120 s prefix and gates 400 s total damper work within the existing 0.7% limit; the complete published-motion case remains qualified. | +| RM3 End_Stops force law and refined-step trajectory | Pinned MATLAB Applications `End_Stops` input; R2025b runs at published 0.1 s and diagnostic 0.025/0.0125 s steps through the published 400 s duration | Effective stroke bounds are ±0.6 m, each spring is 100 MN/m, stop damping is zero, and the 0.0001 m transition is active. The source's logged stop force matches the Python smooth-stop law within `1e-5` N even at saved samples inside the transition. The 0.025-to-0.0125 s source stroke difference is at most 2.79 mm over 400 s; the published 0.1 s run differs from the refined source by up to 286 mm after contact. The opt-in Python adaptive solver with implicit added mass agrees with the 0.0125 s MATLAB trajectory over 400 s within 0.986 mm PTO stroke, 6.77 mm/s PTO speed, 98.2 kN PTO force, and 0.139% ordinary damper energy. Paired gates cover both active body positions and velocities, PTO motion and force, and integrated energy; a separate source convergence gate checks both refined MATLAB steps. The public `WEC.run(..., solver="wecsim_ode45")` independently pairs the published 120 s prefix. A 400 s total-work check passes locally but fails the same 0.7% limit in Linux CI, so no portable full-run gate is claimed; the complete published-motion case remains qualified. | | Public RM3 MCR eight-condition PTO sweep | The pinned 400 s R2025b Option 1 array outputs, [archived without rounding](tests/fixtures/matlab/rm3_mcr_array/README.md) | An [ordinary-CI public `WEC.floating_joint` gate](tests/test_rm3_mcr_public_portable.py) independently advances all eight published height × period × damping settings. With the explicit source-delay mass-feedback option, maximum active body differences are 75.1 mm surge, 0.386 mm heave, and 0.000181 rad pitch; PTO stroke, speed, force, and absorbed power differ by at most 0.412 mm, 0.231 mm/s, 478 N, and 485 W. Integrated PTO work differs by at most 0.0597% and the eight MCR mean powers by at most 155 W. A separate check keeps the ordinary implicit-mass default dissipative across all eight conditions and its PTO work within 0.697% of source, while its larger motion gap is not claimed as source parity. These are stronger output gates for already counted inputs; the badge does not change. | | RM3 radiation force options: constant, convolution, FIR, and state-space | Pinned MATLAB Applications `Radiation_Force_Options`, BEMIO-generated RM3 HDF5, 12 s regular waves | All four published 500 s settings are checked against the Python floating-joint solver. The FIR path uses sampled kernel taps with a held radiation force during each RK4 step. Across all four cases, the largest differences are 30.7 mm surge, 2.11 mm heave, 0.00144 rad pitch, 3.25 mm PTO stroke, 1.55 mm/s PTO speed, 1.86 kN PTO force, and 1.57 kW PTO power. Full-run and three interval PTO-work gates add a 1% bound for each mode; locally the largest interval relative differences are 0.546% constant, 0.027% convolution, 0.012% FIR, and 0.432% state-space. Direct FIR radiation-force differences remain under their paired gates. MATLAB FIR differs from MATLAB convolution by up to 345 mm surge, so the methods are not interchangeable for this case. The opt-in `radiation_method="state_space"` advances its 72 active fitted states with the independent RM3 body motion and passes the same full-duration gates; its force law on prescribed source motion is checked separately below. The fitted model has negative low-frequency damping, so this is numerical source parity, not physical passivity evidence. | | RM3 state-space radiation force on prescribed motion | Pinned MATLAB Applications `Radiation_Force_Options` state-space setting and [focused R2025b gate](https://github.com/cmudrc/wec-sim-python/actions/runs/37935700135) | `wecsim.hydrodynamics.radiation.replay_rm3_fitted_radiation` advances the HDF5 A/B/C channel fits from zero state using the saved six-component velocity histories, scales C by water density, and sets D to zero as the pinned MATLAB block does. Over 5,001 samples on both bodies, every radiation-force channel differs by less than `0.026%` of its source peak; the gate allows `0.03%` with a `0.0009` N floor for nearly stationary channels. This is a prescribed-motion force replay, not an independently coupled trajectory or evidence that the fitted model is passive. The default convolution solver is unchanged. | @@ -3216,13 +3216,14 @@ active positions and speeds, PTO motion and force, and work. The full gate is conditioned on source solver times; it isolates the remaining step-selection gap and does not promote `End_Stops` or the RM3 badge. Without prescribed times, the improved 400 s source-style solver still reaches `0.277` m -PTO-stroke error and `0.431%` less damper work than the published MATLAB -trajectory. The public `WEC.run` path now keeps that full-duration total-work -comparison under the existing `0.7%` End Stops limit in ordinary CI: -`60.678` MJ Python versus `60.941` MJ published MATLAB (`0.4307%` gap). -This is an aggregate gate only. Its 100–200 s and 200–300 s work windows -differ by `0.7993%` and `0.9021%`, above the same `0.7%` limit; the -published full-duration motion gate remains red. The opt-in source-style +PTO-stroke error on local ARM64. Its public `WEC.run` damper work is `60.678` +MJ versus `60.941` MJ published MATLAB (`0.4307%` gap), below the existing +`0.7%` full-run limit locally. The same independent test failed on Linux x86 +with a `0.8246%` gap in [run 39369510668](https://github.com/cmudrc/wec-sim-python/actions/runs/39369510668), +using the same NumPy 2.5.4 and SciPy 1.18.1 versions. The local 100–200 s +and 200–300 s work windows also differ by `0.7993%` and `0.9021%`, above +the unchanged `0.7%` limit. This is a nonportable aggregate diagnostic, +not a published full-duration work or motion gate. The opt-in source-style work remains about 10% above the refined physical trajectory's work. In a separate local 90 s published-step source run, MATLAB's Solver Profiler diff --git a/tests/reference_case_status.csv b/tests/reference_case_status.csv index 2c1e7d70..53805f84 100644 --- a/tests/reference_case_status.csv +++ b/tests/reference_case_status.csv @@ -15,7 +15,7 @@ Controls/Passive (P),independent_motion, Controls/Reactive (PI),independent_motion, Controls/ReactiveWithPTO,independent_motion, Desalination,independent_motion, -End_Stops,qualified_motion,"Full-duration refined-step and MaxStep-only derived-source motion independently pair with Python; source-timed delayed-mass replay pairs the published 400 s trajectory but uses MATLAB's accepted solver steps. Independently selected source-state ode45 motion pairs the published first 120 s through public WEC.run; its independent 400 s total damper work passes the existing 0.7% gate at 0.4307%, but two 100 s windows exceed 0.7% and published full-duration motion remains step-sensitive." +End_Stops,qualified_motion,"Full-duration refined-step and MaxStep-only derived-source motion independently pair with Python; source-timed delayed-mass replay pairs the published 400 s trajectory but uses MATLAB's accepted solver steps. Independently selected source-state ode45 motion pairs the published first 120 s through public WEC.run. Its 400 s total damper-work gap is 0.4307% locally but 0.8246% in Linux CI against an unchanged 0.7% gate, so no portable full-run work gate is claimed; published full-duration motion remains step-sensitive." Free_Decay/0m,independent_motion, Free_Decay/1m,independent_motion, Free_Decay/1m-ME,independent_motion, diff --git a/tests/test_rm3_end_stops_source_timing.py b/tests/test_rm3_end_stops_source_timing.py index bc3e8aeb..2d69aa3d 100644 --- a/tests/test_rm3_end_stops_source_timing.py +++ b/tests/test_rm3_end_stops_source_timing.py @@ -96,8 +96,8 @@ def test_independently_selected_source_state_steps_through_120_seconds(): assert abs(python_work - matlab_work) / matlab_work < 1e-5 -def _public_end_stops_wec(): - """Build the published RM3 configuration without source motion inputs.""" +def test_public_wec_run_pairs_published_end_stops_through_120_seconds(): + """The configured public WEC uses its own adaptive steps and wave input.""" wec = WEC("RM3 End Stops") float_body = wec.body("float", HYDRO, inertia=(0, 21_306_090.66, 0)) spar_body = wec.body("spar", HYDRO, inertia=(0, 94_407_091.24, 0)) @@ -106,15 +106,8 @@ def _public_end_stops_wec(): hard_stops=LinearHardStops(-.6, .6, 1e8, 1e8), added_mass_scheme="simulink_delay", ) - return wec - - -def test_public_wec_run_pairs_published_end_stops_through_120_seconds(): - """The configured public WEC uses its own adaptive steps and wave input.""" - result = _public_end_stops_wec().run( - RegularWave(2.5, 8), dt=.1, end_time=120, - ramp_time=100, solver="wecsim_ode45", - ) + result = wec.run(RegularWave(2.5, 8), dt=.1, end_time=120, + ramp_time=100, solver="wecsim_ode45") with np.load(FIXTURES / "rm3_end_stops_published_full.npz") as source: length = len(result.time) @@ -147,30 +140,6 @@ def test_public_wec_run_pairs_published_end_stops_through_120_seconds(): assert abs(python_work - matlab_work) / matlab_work < 1e-5 -def test_public_wec_run_published_400s_damper_work(): - """Gate independent full-run PTO work while motion remains qualified.""" - result = _public_end_stops_wec().run( - RegularWave(2.5, 8), dt=.1, end_time=400, - ramp_time=100, solver="wecsim_ode45", - ) - with np.load(FIXTURES / "rm3_end_stops_published_full.npz") as source: - np.testing.assert_allclose(result.time, source["time"], - rtol=0, atol=1e-10) - source_work = np.trapezoid( - 1_200_000 * source["pto"][:, 1]**2, source["time"], - ) - pto = result.ptos["relative_heave"] - np.testing.assert_allclose( - pto.absorbed_power, 1_200_000 * pto.velocity**2, - rtol=0, atol=1e-8, - ) - python_work = np.trapezoid(pto.absorbed_power, result.time) - assert source_work > 0 - # Reuse the existing 0.7% full-run End Stops damper-work limit. - gap = abs(python_work - source_work) / source_work - assert gap < .007, f"published 400 s damper work gap: {gap:.4%}" - - def test_published_full_duration_with_source_major_step_schedule(): """Gate the force law while explicitly retaining the timing condition.""" with np.load(FIXTURES / "rm3_end_stops_internal_steps.npz") as steps: