diff --git a/PARITY.md b/PARITY.md index 83d374b0..4e5e9aeb 100644 --- a/PARITY.md +++ b/PARITY.md @@ -93,11 +93,11 @@ saves its MATLAB outputs as an artifact. | Sphere declutching controller | Pinned MATLAB Applications `Controls/Declutching` and MATLAB-generated Sphere HDF5 | A configurable body-local PTO endpoint uses the published 232,020 N s/m engaged damping and switches off for 0.8 s after velocity reversal. In 40,001 paired samples over 400 s, heave differs by at most 1.99 mm and velocity by 4.89 mm/s. Both runs have 83 disengagement events, each 0.8 s long; event times differ by at most one 0.01 s sample, affecting four force samples. Where both runs are engaged, force differs by at most 1.14 kN and power by 1.06 kW; both report zero force while disengaged. Absorbed energy differs by 10.5 kJ over a 22.4 MJ MATLAB total. A full-sample force maximum is 106 kN because one-step mode differences occur at two transitions, so force parity is gated by event timing and within-mode values. The [portable source trace](tests/fixtures/matlab/README.md) now enforces these same full-duration gates in ordinary Python CI. | | Sphere latching controller | Pinned MATLAB Applications `Controls/Latching` and MATLAB-generated Sphere HDF5 | The public `LatchingControl` applies the published 49,181 N s/m normal damping and 37,308,296 N s/m latch damping for 2.4 s after velocity reversal. Both force laws dissipate energy. In 40,001 paired samples over 400 s, MATLAB and Python each have 83 latch starts; start times differ by at most 0.05 s, and each completed latch lasts 2.41 s in the sampled source implementation. Maximum heave and velocity differences are 0.379 m and 0.573 m/s; the heave extrema differ by at most 0.033 m. Absorbed energy differs by 2.43 MJ against MATLAB's 299.98 MJ (0.81%). This switched high-gain case has 553 samples in different modes, so pointwise controller force is not a useful single parity metric; the paired gate checks the exact force and power laws, event timing, motion, envelope, and integrated energy. The published linear model does not establish a feasible physical stroke or rigid latch. | | Configured Sphere spring, damper, and PTO attachment | Derived from the pinned Sphere passive case with 50,000 N/m PTO stiffness, 100,000 N s/m native PTO damping, and the PTO moved to x = 1 m | A fresh MATLAB run and the Python `WEC` builder agree within 0.051 mm position and PTO stroke, 0.054 mm/s velocity and PTO speed, 52 N combined force, and 27 W combined power. The x offset has no effect on heave-only motion, so this validates the force settings but not attachment-location dynamics. | -| Configured RM3 spring and PTO pretension | Pinned core RM3 regular-wave input copied with only 50,000 N/m PTO stiffness and 5,000 N pretension added; [focused R2025b source run](https://github.com/cmudrc/wec-sim-python/actions/runs/38196940943) | Source initialization sets equilibrium to −0.1 m and applies −5,000 N at zero stroke. The public `WEC.floating_joint(..., stiffness=50000, pretension=5000)` computes that equilibrium and advances the same 400 s sea independently. Across all 4,001 samples, maximum float/spar surge differences are 23.7/36.7 mm, heave differences 3.99/1.34 mm, pitch difference `0.000667` rad, PTO stroke/speed differences 4.86 mm/3.83 mm/s, force difference 4.58 kN, and mechanical-power difference 5.47 kW; full-duration per-channel gates pass. The source PTO power equals its total spring-and-damper force times speed within `1.2e-9` W. Python's `-force * velocity` matches source-signed total mechanical power, while its separately documented `absorbed_power` reports damper dissipation. This is a derived PTO setting, not another published input or a general attachment-geometry gate. | +| Configured RM3 spring and PTO pretension | Pinned core RM3 regular-wave input copied with only 50,000 N/m PTO stiffness and 5,000 N pretension added; [focused R2025b source run](https://github.com/cmudrc/wec-sim-python/actions/runs/38196940943) | Source initialization sets equilibrium to −0.1 m and applies −5,000 N at zero stroke. The public `WEC.floating_joint(..., stiffness=50000, pretension=5000)` computes that equilibrium and advances the same 400 s sea independently. Across all 4,001 samples, maximum float/spar surge differences are 23.7/36.7 mm, heave differences 3.99/1.34 mm, pitch difference `0.000667` rad, PTO stroke/speed differences 4.86 mm/3.83 mm/s, force difference 4.58 kN, and mechanical-power difference 5.47 kW; full-duration per-channel gates pass. The source PTO power equals its total spring-and-damper force times speed within `1.2e-9` W. Python's `-force * velocity` matches source-signed total mechanical power, while its separately documented `absorbed_power` reports damper dissipation. Damper work derived from source PTO speed now passes a 1% gate in ramp, two steady, and full 400 s windows; the largest observed gap is 0.7934%. This is a derived PTO setting, not another published input or a general attachment-geometry gate. | | Configured Sphere PTO pretension | The same derived 400 s passive case, with MATLAB's 5,000 N pretension and −0.1 m spring equilibrium; Python reverses the PTO-axis signs | A fresh pinned MATLAB run and independent Python `WEC.pto(..., pretension=-5000)` motion differ by at most 0.051 mm heave and stroke, 0.123 mm/s speed, 12.5 N native force, 106 N controller force, 119 N combined force, and 23 W controller power across 20,001 samples. The speed and controller-force maxima occur at 0.04 s under the nonzero initial load; after 0.1 s they fall below 0.054 mm/s and 47 N. The source pretension and equilibrium are exported and checked separately. No dynamics coefficient was changed to fit the run. This is a derived heave-only configuration, not a new published case or an attachment-moment gate. | | OSWEC PM equal-energy waves, directional excitation, pitch, and PTO | Current MATLAB OSWEC example and current OSWEC HDF5 | Python recreates all 500 PM equal-energy bins from the HDF5 range, then synthesizes wave elevation and six-component excitation using the saved MATLAB phase matrix. Local component differences are below `6e-15`, elevation below `2e-13` m, and excitation below `1e-7` N. The public two-body `WEC.fixed_hinge` call passed a [fresh pinned R2025b paired run](https://github.com/cmudrc/wec-sim-python/actions/runs/37795546236) with source phases: maximum flap differences over 4,001 samples were 10.0 mm surge, 1.66 mm heave, 0.00201 rad pitch, 0.00297 rad/s pitch speed, and 35.6 N m PTO torque; the fixed hydrodynamic base agreed exactly. A [portable pinned fixture](tests/fixtures/matlab/README.md) now gates the published sea, six-component flap excitation, both bodies, and hinge PTO at all samples in ordinary Python CI. A Python seed produces a separate reproducible realization. This model covers pitch about a fixed hinge, not a general six-DOF device. | | OSWEC PTO-Sim hydraulic adjustable rod and fixed crank | Pinned MATLAB Applications `PTO-Sim/OSWEC/OSWEC_Hydraulic_PTO` and `OSWEC_Hydraulic_Crank_PTO`, BEMIO OSWEC HDF5, and [fresh 400 s R2025b source run](https://github.com/cmudrc/wec-sim-python/actions/runs/37831809681) | Python couples the published hinged flap and 30 s radiation IRF to either rotary-to-linear linkage, compressible cylinder, rectifying valve, gas accumulators, motor, generator, and PI load. The pinned crank laws map angle to stroke and cylinder force to torque with opposite signs; source-driven cylinder pressure steps, valve flows, accumulator pressures, motor and generator steps agree to numerical precision. Across both independently integrated 40,001-sample trajectories, maximum pitch/speed errors are 0.00136 rad and 0.000722 rad/s, flap-center surge/heave errors 6.55/2.75 mm, crank torque 57.9 kN m against a 2.10 MN m source peak, cylinder force 20.0 kN, chamber pressure 0.390 MPa, accumulator pressure 5.24 kPa, shaft speed 0.0595 rpm, current 0.167 A, and voltage 0.175 V. Valve-port flow differs by up to 0.0540 m³/s at reversals; accumulated port-volume error stays below 0.00129 m³. Explicit paired gates cover body motion, torque, hydraulic states, valve flow, and electrical outputs for each layout. A [portable pinned fixture](tests/fixtures/matlab/oswec_hydraulic/README.md) now checks both independently integrated 400 s layouts every 50 ms in ordinary Python CI, plus signed crank work integrated over all 40,001 Python and source steps. A fresh R2025b source run and the portable gate pass on Linux with the exact source HDF5 and unchanged force limits. With direct scalar division for the one-coordinate mass equation, the independent 400 s fixed-crank run also passes locally on ARM64 macOS: peak torque and cylinder-force gaps are 43.62 kN m and 14.27 kN, below the unchanged 75 kN m and 25 kN gates. The platform-specific expected failure is removed. The adjustable-rod source commands negative load resistance after 46 s for 77.0% of its nonzero-current samples; this is a source-case limitation. General hydraulic networks and arbitrary attachment geometry remain unpaired. | -| OSWEC `Multiple_Wave_Spectra` with two independent PM seas | Pinned MATLAB Applications `Multiple_Wave_Spectra` input and BEMIO-generated OSWEC HDF5 | The public `run_case` interface and `WEC.run(MultiPMWave(...))` configuration sum separate 2 m, 0° and 1 m, 90° seas with saved independent phase realizations over 100 s. The Python builder reproduces the paired case's body positions, velocities, wave elevation, and PTO torque within numerical precision. Combined elevation agrees with MATLAB within `1e-11` m and both bodies' six excitation-force components within `1e-6` N or N m. The fixed hydrodynamic base remains stationary. With the ordinary implicit added-mass default, maximum flap differences are 106 mm surge, 10.8 mm heave, and 0.0213 rad pitch; PTO torque differs by at most 7.25 kN m. The explicit source-delay comparison reduces these to 10.3 mm, 1.20 mm, 0.00207 rad, and 818 N m. Both dynamics paths have separate paired gates. The source enables passive-yaw preprocessing, but its hinge has zero yaw motion; the source's spline frequency interpolation is selected explicitly for this case. Moving-yaw dynamics and fixed-base reactions remain unsupported. | +| OSWEC `Multiple_Wave_Spectra` with two independent PM seas | Pinned MATLAB Applications `Multiple_Wave_Spectra` input and BEMIO-generated OSWEC HDF5 | The public `run_case` interface and `WEC.run(MultiPMWave(...))` configuration sum separate 2 m, 0° and 1 m, 90° seas with saved independent phase realizations over 100 s. Combined elevation agrees with MATLAB within `1e-11` m and both bodies' six excitation-force components within `1e-6` N or N m. The fixed hydrodynamic base remains stationary. With the ordinary implicit added-mass default, maximum flap differences are 106 mm surge, 10.8 mm heave, and 0.0213 rad pitch; PTO torque differs by at most 7.25 kN m. The explicit source-delay comparison reduces these to 10.3 mm, 1.20 mm, 0.00207 rad, and 818 N m. Both dynamics paths have separate paired gates. The public source-delay path now pairs PTO absorbed work in three disjoint intervals and over the full run under preset 5% limits; the largest measured gap is 3.913%. The source enables passive-yaw preprocessing, but its hinge has zero yaw motion; the source's spline frequency interpolation is selected explicitly for this case. Moving-yaw dynamics and fixed-base reactions remain unsupported. | | OSWEC `Full_Directional_Waves` imported spectrum | Pinned MATLAB Applications `Full_Directional_Waves` input and BEMIO-generated OSWEC HDF5 | The imported 47-frequency, 180-heading spectrum now generates all 8,460 source phases from `seed=1, phase_generator="matlab"` within `1e-13` rad and reproduces all 8,001 wave-elevation samples within `9e-14` m. The source force block omits heading-bin width although its wave-elevation block includes it: logged excitation is about `1/sqrt(2° in radians) = 5.352` times the physically integrated force. Python keeps heading integration as its default. Explicit `matlab_omitted` quadrature plus `spline_frequency` interpolation matches all six logged excitation components for both bodies within `4e-7` N or N m. With that source forcing and ordinary implicit added mass, the 400 s flap differs by at most 12.8 mm surge, 3.52 mm heave, 0.00258 rad pitch, and 56.6 N m PTO torque. The separate opt-in Simulink-delay comparison through public `WEC.run(FullDirectionalSpectrumWave(...))` reduces these to 1.53 mm, 0.293 mm, 0.000306 rad, and 5.54 N m; the paired gate checks the full flap, base, and PTO trajectories. A [portable pinned fixture](tests/fixtures/matlab/oswec_full_directional/README.md) now gates the full 400 s wave, both bodies' six excitation channels, public hinge motion, PTO torque, and signed work in ordinary Python CI. Source-compatible agreement does not establish that the omitted-width force is physically correct. The fixed base remains stationary; its ground reactions are not computed. | | OSWEC passive yaw off and on | Pinned MATLAB Applications `PassiveYawOFF` and `PassiveYawON` inputs, BEMIO-generated OSWEC HDF5, and fresh R2025b trajectories | The public `WEC` interface defines a yawing flap, fixed hydrodynamic base, and 120,000 N m s/rad rotational PTO. Both published 600 s cases use 2.5 m, 8 s waves at 10°. With passive yaw off, Python and MATLAB flap yaw agree within `4e-14` rad. With passive yaw on, excitation follows the body-relative wave heading and turns the flap toward the 10° incident heading; yaw and angular-speed differences stay below 0.00149 rad and 0.000193 rad/s. PTO torque and absorbed power differ by at most 23.2 N m and 0.128 W, accounting for MATLAB's negative absorbed-power sign. On the saved MATLAB yaw trajectory, all six reconstructed excitation components differ by at most 100 N or N m except yaw moment, which differs by 467 N m. The same full-duration active-motion, wave, PTO, and six-component ON source-force gates now run in ordinary Python CI from [compact source fixtures](tests/fixtures/matlab/oswec_passive_yaw/README.md); the fresh MATLAB path remains separate. The source holds heading coefficients until yaw changes by 0.01°, while Python interpolates continuously. The fixed base is stationary; full three-dimensional joint mechanics remain unsupported. | | OSWEC variable-hydrodynamics passive yaw, derived 2° bank | Pinned MATLAB Applications `Variable_Hydro/Passive_Yaw` regular-wave, 600 s, 0.01 s case with its direction-bank generator resampled from the published 0.05° grid to `-40:2:40`; [fresh passing R2025b and Python run](https://github.com/cmudrc/wec-sim-python/actions/runs/38026261576) | `WEC.body(..., passive_yaw=True, yaw_heading_bank=range(-40, 41, 2))` follows the source's nearest-heading selection, single-direction `interp1(...,'spline')` frequency coefficients, and unrotated variable-hydro force convention. On all 60,001 saved MATLAB states, heading selection is identical and all six excitation components differ by at most `3.66e-7` N or N m, below the `1e-6` gate. An independent Python run from the same wave and HDF5 agrees over the full 600 s within `4.9e-15` rad flap yaw, `4.3e-16` rad/s yaw speed, and `1.9e-10` N m PTO torque on local ARM64; PTO work differs by `7.2e-14` relative. Paired gates are `1e-6` rad yaw, `1e-6` rad/s speed, `0.01` N m torque, and `1e-6` relative PTO work. The original 0.25° selected bank is paired separately below; other variable-hydrodynamics layouts remain unpaired. | @@ -116,7 +116,7 @@ saves its MATLAB outputs as an artifact. | Ellipsoid `ode45` MaxStep-only isolation | Pinned 150 s `Regular` and `RegularCIC` source inputs, a local R2025b run of each at the published 0.05 s settings and again with only Simulink `MaxStep=0.005`, and the [archived jointly refined 0.005 s source](https://github.com/cmudrc/wec-sim-python/actions/runs/38110743157) | Each pair has identical wave histories and retains `dtOut=nonlinearDt=cicDt=0.05` s. MaxStep-only changes published heave by 6.19 mm (Regular) and 5.09 mm (RegularCIC), but its gaps to the jointly refined source are 21.24 and 21.39 mm, versus 17.09 and 17.92 mm for the published source. An independent public Python run at 0.05 s is only 1.85 and 2.02 mm from the jointly refined source, but 18.94 and 19.93 mm from published MATLAB and 23.09 and 23.40 mm from MaxStep-only MATLAB. Both source restoring histories still reconstruct from the preceding 0.05 s output state within `1e-6` N. [Fresh paired gates](tests/test_ellipsoid_ode45_maxstep_only.py) verify source settings, wave identity, restoring timing, and independent Python motion. Reducing solver MaxStep alone does not remove the published trajectory gap; this experiment does not separately isolate output, nonlinear-force, or CIC sample timing and does not promote either published case. | | Ellipsoid `ode45` nonlinear-force and CIC clock isolation | Pinned 150 s `Regular` and `RegularCIC` inputs, the archived published and MaxStep-only source traces above, and new local R2025b arms with `dt=MaxStep=0.005` and `dtOut=0.05` s | With wave and solver unchanged, changing only `nonlinearDt` from 0.005 to the published 0.05 s moves source heave by 21.24 mm (Regular) and 21.41 mm (RegularCIC), heave speed by 26.72 and 28.96 mm/s, and restoring force by 43.81 and 46.11 kN. Regular's coarse-force arm exactly reproduces the earlier MaxStep-only heave, speed, restoring, excitation, and PTO histories at all 3,001 saved samples. For RegularCIC, independently restoring `cicDt` from 0.005 to 0.05 s shifts heave a further 0.112 mm and then reproduces MaxStep-only heave and speed within `1.1e-14` and `1.1e-15` in their units; force differences are below `1.1e-9` N. Independent public Python remains 1.85 and 2.02 mm from the fine-clock source, versus 23.09 and 23.42 mm from its coarse-force arm. [Paired clock gates](tests/test_ellipsoid_ode45_nonlinear_clock.py) check clock settings, wave identity, source reconstruction, and independent motion. These controlled derived arms attribute the MaxStep-only-to-refined gap to source sample clocks for the pinned inputs; neither published 0.05 s trajectory is promoted. | | Ellipsoid `ode45/Regular` added-mass feedback | The pinned published input, an independent local R2025b 2 s log of the active `10e-8` s Transport Delay, and the [full 150 s published force output](https://github.com/cmudrc/wec-sim-python/actions/runs/39054809363) | The logged delay input equals source heave acceleration at all 41 prefix samples; its output follows two-sample extrapolation to `t−1e-7` s within `1.2e-17` m/s². Across all 3,001 published samples, the logged added-mass force follows that same delay law within `2.1e-8` N. Independently rebuilding the complete right-hand side from Python's bundled mesh and BEM coefficients, source motion, and logged added-mass force reproduces source acceleration within `5e-13` m/s². The [portable gate](tests/test_ellipsoid_ode45_delay_prefix.py) and [replay script](tests/matlab/ellipsoid_ode45_delay_prefix.m) pin this force law. Holding mesh forces on Python's 0.05 s output clock alone reduced the published heave gap from 18.94 to 15.29 mm in a local prototype, so that option was discarded. This source-state force gate is not an independent Python trajectory gate; the published ode45 case remains qualified. | -| Sphere variable draft and mass in heave | Pinned MATLAB Applications `Variable_Hydro/Variable_Mass`, nine BEMIO-generated draft HDF5 files, and its full 900 s ode4 run | The Python `WEC.variable_body` selects the published state every 100 s and changes equilibrium draft, rigid mass, added mass, radiation damping, restoring stiffness, and excitation together. On MATLAB's 9,001 saved states, state-selected excitation differs by at most `3e-8` N, radiation damping by `3e-10` N, restoring plus weight by `8e-9` N, and assembled hydrodynamic force by `9e-9` N. The independent Python trajectory differs by at most 2.23 mm heave, 4.14 mm/s velocity, and 827 N PTO force over all nine states. The source reaches −6.63 m by 900 s; agreement with its linear coefficients does not establish physical accuracy at that excursion. Other variable-hydrodynamic motions remain unsupported. | +| Sphere variable draft and mass in heave | Pinned MATLAB Applications `Variable_Hydro/Variable_Mass`, nine BEMIO-generated draft HDF5 files, and its full 900 s ode4 run | The Python `WEC.variable_body` selects the published state every 100 s and changes equilibrium draft, rigid mass, added mass, radiation damping, restoring stiffness, and excitation together. On MATLAB's 9,001 saved states, state-selected excitation differs by at most `3e-8` N, radiation damping by `3e-10` N, restoring plus weight by `8e-9` N, and assembled hydrodynamic force by `9e-9` N. The independent Python trajectory differs by at most 2.23 mm heave, 4.14 mm/s velocity, and 827 N PTO force over all nine states. Its newly paired PTO speed and absorbed power differ by at most 4.135 mm/s and 1.052 kW; full-run damper work differs by 0.00376%, below the fixed 1% gate. The source reaches −6.63 m by 900 s; agreement with its linear coefficients does not establish physical accuracy at that excursion. Other variable-hydrodynamic motions remain unsupported. | | Barge generalized body modes | Pinned MATLAB Applications `Generalized_Body_Modes`, its BEMIO-generated ten-DOF barge HDF5, and a fresh 400 s R2025b run including the model's separate `Flex_out` signal | `BodyClass` pairs flexible effective mass, stiffness, damping, hydrostatic, added-mass, radiation-damping, and excitation matrices; the largest absolute matrix difference is `1.91e-6` in applied added mass. The source flexible acceleration equation and six signed force components reconstruct from those matrices, with restoring and radiation differences below `1e-6` N, and sampled added-mass feedback below `0.05` N. The public `WEC.floating_gbm` interface runs a coupled three-rigid/four-flexible-coordinate implicit-mass model in the published 2 m, 8 s regular wave. Over all 8,001 samples, maximum rigid position and velocity differences are 1.06 mm and 0.838 mm/s; flexible displacement, velocity, and acceleration differ by at most 0.043 mm, 0.034 mm/s, and `1.26e-4` m/s². This validates one single-body floating 3-DOF joint with four modes, zero-heading regular waves, and no PTO or mooring; other GBM layouts and wave/radiation options remain unsupported. | | OWC `OrificeModel` wave, force, and early coupled motion | Pinned MATLAB Applications `OWC/OrificeModel`, BEMIO-cleaned seven-DOF HDF5, and a fresh 130 s R2025b `ode23t` [source run](https://github.com/cmudrc/wec-sim-python/actions/runs/37799665808) | `wecsim.OrificePTO` reproduces the embedded Simulink orifice block on its 418,828 logged internal solver evaluations. Maximum force, pressure, and source-power differences are `4.8e-10` N, `2.9e-12` kPa, and `4.2e-13` kW; the Mach flag is identical. Interpolating source flow to the independent 26,001-sample flexible-mode output gives a piston-speed difference below `3.6e-13` m/s. The 500 PM bins agree within `5.2e-15`, wave elevation within `2.8e-14` m, and all six rigid plus one flexible excitation channels within `1.5e-10` N. Flexible mass/stiffness/damping, added mass, and hydrostatic matrices agree within `2.3e-13` in their units. With the [rigid-force diagnostic run](https://github.com/cmudrc/wec-sim-python/actions/runs/37804483371), radiation reconstructed from all seven MATLAB velocities differs by at most `0.0074` N in the flexible channel and `0.22` N across rigid channels (peak rigid magnitude `1,182` N). The rigid restoring and linear-damping forces reproduce the source logs within `8.6e-11` N and `1.1e-12` N, and the rigid hydrodynamic force sum closes within `1.7e-10` N. MATLAB's flexible hydrodynamic force sum closes within `1e-12` N; its effective-mass acceleration including the orifice force closes within `3.5e-8` N. The source rigid surge, heave, and pitch balances close within `1.2e-10` in their force units when using its adjusted translational mass and logged piston reaction. `WEC.floating_gbm(..., orifice=...)` now integrates the seven-DOF PM case with memory radiation, quadratic drag, and the coupled piston force. Against the saved source phases, maximum surge, heave, unwrapped-pitch, and flexible-position differences through 6 s are `0.0113` m, `0.0244` m, `0.0098` rad, and `0.0070` m. At 130 s the physical coupled path has heave and flexible-displacement differences of `1.94` m and `0.280` m. The separate opt-in `orifice_force_path="published_owc"` follows the pinned Simulink routing, which leaves the piston out of the flexible state-space input but retains it in reported acceleration and rigid added-mass feedback. Over all 26,001 samples, its maximum surge, heave, unwrapped-pitch, and flexible-position differences are `0.174` m, `0.0367` m, `0.0108` rad, and `0.0054` m; PTO reaction and absorbed-power maxima differ by `1.98` kN and `1.75` kW, and integrated absorbed work by `4.76%`. These are explicit 130 s source-convention gates, **not physical coupled-path parity**. Python reports SI watts while the source's numeric power is kW. The source first exceeds its Mach threshold at 10.13 s; it is above 0.3 for 61.4% of simulated time and peaks at Mach 1.11. Matching its incompressible law after that point does not establish physical accuracy. | | Case-driven dynamics runner | Current MATLAB RM3 and OSWEC examples plus Sphere and RM3 Applications cases | One generalized-coordinate engine assembles rigid inertia, hydrodynamic added mass and radiation, hydrostatic restoring, excitation, and linear PTO forces. The heave, fixed-hinge, and floating-joint layouts run the paired cases above, including imported elevation, a joint surge spring for RM3 MooringMatrix, and native MoorDyn coupling for the published RM3 mooring. A fourth `linear_subspace` layout maps independent coordinates into arbitrary bodies; its RM3 two-body heave, Sphere free decay, configured Sphere PTO, and two instantaneous nonlinear-hydro cases have paired MATLAB checks. Arbitrary Simscape layouts, general moorings, and other nonlinear-hydro/application cases remain unsupported. | diff --git a/tests/fixtures/matlab/oswec_multi_wave/README.md b/tests/fixtures/matlab/oswec_multi_wave/README.md index b7f7ecb9..8cb79e10 100644 --- a/tests/fixtures/matlab/oswec_multi_wave/README.md +++ b/tests/fixtures/matlab/oswec_multi_wave/README.md @@ -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. diff --git a/tests/fixtures/matlab/rm3_pto_pretension/README.md b/tests/fixtures/matlab/rm3_pto_pretension/README.md index 5e6d4415..f72aa77d 100644 --- a/tests/fixtures/matlab/rm3_pto_pretension/README.md +++ b/tests/fixtures/matlab/rm3_pto_pretension/README.md @@ -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%. diff --git a/tests/fixtures/matlab/sphere_variable_mass/README.md b/tests/fixtures/matlab/sphere_variable_mass/README.md index 5e168084..93b7d1a6 100644 --- a/tests/fixtures/matlab/sphere_variable_mass/README.md +++ b/tests/fixtures/matlab/sphere_variable_mass/README.md @@ -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. diff --git a/tests/reference_case_status.csv b/tests/reference_case_status.csv index 47bc8659..5891facc 100644 --- a/tests/reference_case_status.csv +++ b/tests/reference_case_status.csv @@ -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, @@ -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. diff --git a/tests/test_oswec_multi_wave_portable.py b/tests/test_oswec_multi_wave_portable.py index b81581b4..90cb4279 100644 --- a/tests/test_oswec_multi_wave_portable.py +++ b/tests/test_oswec_multi_wave_portable.py @@ -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%" + ) diff --git a/tests/test_rm3_pto_pretension_parity.py b/tests/test_rm3_pto_pretension_parity.py index 24e75d80..6a5cdd3d 100644 --- a/tests/test_rm3_pto_pretension_parity.py +++ b/tests/test_rm3_pto_pretension_parity.py @@ -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%" + ) diff --git a/tests/test_sphere_variable_mass_parity.py b/tests/test_sphere_variable_mass_parity.py index f4dad04b..e6adc32c 100644 --- a/tests/test_sphere_variable_mass_parity.py +++ b/tests/test_sphere_variable_mass_parity.py @@ -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): @@ -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],