Validation matrix
VALIDATION.md at the repository root is the single
structured source of truth for cross-checked numerics. Every
validated case carries:
the reference being matched (citable publication, BModes Fortran solver output, closed-form analytical formula);
the quantity being compared (mode frequency, mode-shape MAC, polynomial coefficient, derived quantity);
the tolerance (relative or absolute, with units);
the worst-observed margin at the time the matrix was last regenerated;
the test file that enforces the tolerance in CI;
the external-data flag — whether the test runs on a fresh clone (self-contained) or only with upstream data staged under
external/.
Mechanical audit
scripts/audit_validation_claims.py parses every test-file
link in VALIDATION.md, asserts the path exists, and
asserts the file (or directory glob) contains at least one
def test_… method. Runs as a required CI step alongside
ruff and mypy, plus step 4.5 of Release checklist.
Claims cannot drift ahead of tests. If you remove a test, you have to remove (or replace) its row in the matrix in the same PR; if you add a row with no test, the audit blocks the PR.
Tracks
The matrix splits validation work into three tracks:
Track A — frequency accuracy
Direct mode-frequency comparisons against a reference. The golden cases:
NREL 5MW on OC3 Monopile vs BModes JJ (CS_Monopile.bmi) — ≤ 0.005 % across 10 modes.
NREL 5MW on OC3 Hywind floating spar vs BModes JJ (OC3Hywind.bmi) — ≤ 0.0003 % across 9 modes.
BModes CertTest Test03 + Test04 (82.4 m tower with top mass + tension-wire support) — < 0.005 %.
Rotating uniform blade vs Wright 1982 / Bir 2009 Table 3a closed form — synthetic, self-contained.
Rotating blade with tip mass vs Bir 2010 Table 5 — same.
Rotating pinned-free cable vs Bir 2009 Eq. 8 (analytical Legendre polynomial) — same.
Track B — coefficient consistency
Whether the polynomial blocks shipped in industry
ElastoDyn .dat files are reproducible from the
structural-property blocks in the same files.
They are not — see cases/ECOSYSTEM_FINDING.md. The
worst observation: TwSSM2Sh on NREL 5MW land deck,
file-RMS 5.90 vs pyBmodes-RMS 0.0023 (ratio 2,529×).
pybmodes validate surfaces this as a per-block PASS /
WARN / FAIL verdict; pybmodes patch rewrites the blocks
from the structural inputs.
Six patched reference decks ship in the wheel under
src/pybmodes/_examples/reference_decks/ — every coefficient
block reaches PASS or WARN; no FAIL after patching. The
before_patch.txt snapshots preserve the original drift for
reference.
Track C — supporting pipeline
Component-level regressions for everything that supports the two main tracks:
BMI / ElastoDyn / SubDyn / WAMIT / MoorDyn parser round-trips
Mooring catenary closed forms (Jonkman 2007 Appendix B)
Hydrostatic restoring vs cylinder closed form
Mode-shape classifier (FA / SS / twist labelling)
Degenerate-pair resolver (symmetric-tower 2-fold eigenspaces)
Pre-solve sanity checks
Serialisation round-trips
CLI smoke tests (every subcommand, every flag)
What “self-contained” means
The default pytest run is self-contained: every test
in it runs from numbers either constructed inline in the test
or validated against published closed-form formulas. No
third-party reference data is bundled in the repo for this
default run.
Any commit that re-introduces a .bmi / .dat / .out
file under tests/data/ for a default-run test should be
questioned. See Data sources for how external data is
staged under external/ and gated behind the
integration pytest marker.
The full matrix
pyBmodes validation matrix
This document is the single structured source of truth for what
pyBmodes is validated against, at what tolerance, with what worst
observed error, and which test file enforces it. Prose-heavy reports
elsewhere in the repo (cases/ECOSYSTEM_FINDING.md,
src/pybmodes/_examples/reference_decks/VALIDATION_SUMMARY.md, the README’s Validation
section) refer back to this matrix.
Enforcement. The
Validation workflow
(weekly cron + workflow_dispatch) clones the upstream OpenFAST /
IEA-Task-37 repositories on the fly and runs pytest -m integration
hard-fail — no exit-5 tolerance, unlike the per-PR ci.yml. The
verifier report uploaded by every run is the machine-checkable
record that the workflow’s coverage holds at the tagged commit.
The workflow validates the cases in the Needs external data
column whose upstream is clonable from a public GitHub repository
at a manifest-pinned SHA (every clone in
external/MANIFEST.toml got a real commit SHA in 1.8.1).
Cloning is manifest-driven — verify_external_data.py --clone
fetches every entry not marked optional = true, so the workflow
and the manifest can’t drift. The required upstreams actively
cloned and checked by validation.yml are:
OpenFAST/r-test— the NREL 5MW land + monopile + OC3 Hywind regression decks.IEAWindTask37/IEA-3.4-130-RWT— IEA-3.4 reference turbine.IEAWindTask37/IEA-10.0-198-RWT— IEA-10 reference turbine.IEAWindTask37/IEA-15-240-RWT— IEA-15 (v1.1 tower; Allen 2020 free-decay reference).IEAWindTask37/IEA-22-280-RWT— IEA-22 reference turbine.WISDEM/WISDEM— the WindIO ontology examples used by the geometry-corpus rows.
verify_external_data.py --strict then hard-fails if any of these
required clones is missing or off its pin, so the verifier report
can’t look green while a pinned, non-optional entry went unchecked.
The only entries left out of CI are the ones marked
optional = true — MoorPy and RAFT (cross-comparison references)
and the BModes archive (not a public clone) — which stay
maintainer-local until a corresponding test lands. Adding a turbine
to, or removing one from, the required set is a deliberate
maintainer action (flip optional) documented under Changed in
the CHANGELOG.
The two BModes CertTest cases (Test03, Test04) depend on
external/BModes, the BModes v3.00 distribution
— Fortran source + CertTest decks, also on GitHub at
old-NWTC/BModes, but
government-funded reference data pyBmodes does not bundle or
auto-fetch in CI. Those tests
skip cleanly when the data is absent (module-level
pytestmark = pytest.mark.integration plus per-file
.is_file() guards) and stay a maintainer-local enforcement
until the BModes archive is mirrored somewhere CI can clone. The
matrix row’s worst-observed columns reflect the local run; the
public CI workflow does not currently re-verify them.
The validation work is split into two tracks with different metrics:
Track A — frequency accuracy. Eigenvalue agreement against an external reference (closed-form formula, published table, or the BModes Fortran solver). Tolerance is a relative frequency error.
Track B — coefficient consistency. The mode-shape polynomial blocks shipped in OpenFAST ElastoDyn
.datfiles. Tolerance is the RMS residual of the polynomial against the FEM mode shape.
Track A — frequency-accuracy cases
Case |
Source / reference |
Quantity |
Tolerance |
Worst observed |
Test file |
Needs external data |
|---|---|---|---|---|---|---|
Uniform Euler-Bernoulli cantilever, modes 1-3 |
\(\beta_n L = \{1.875,\,4.694,\,7.855,\ldots\}\) (textbook) |
flap frequency |
< 0.5 % |
< 0.005 % |
no |
|
Same, mode 4 |
textbook |
flap frequency |
< 1 % |
(within tol) |
no |
|
Same, mode 5 |
textbook |
flap frequency |
< 2 % |
(within tol) |
no |
|
Hermite cubic mesh-convergence |
analytical \(h^4\) rate |
error ratio at h × 2 |
> 5 (i.e. ≥ 5×) |
confirmed |
no |
|
Cantilever + tip mass, \(\mu \in [0, 5]\) |
Blevins (1979) / Karnovsky & Lebed (2001) frequency equation |
1st bending frequency |
< 0.5 % |
(within tol) |
no |
|
Rotating uniform blade, flap modes 1-3, Ω ∈ [0, 12] rad/s |
Wright et al. (1982) — Vibration Modes of Centrifugally Stiffened Beams, J. Appl. Mech. (transcribed from Bir 2009 Table 3a) |
flap frequency |
< 0.5 % |
(within tol) |
no |
|
Rotating uniform blade + tip mass, flap modes 1-2, Ω ∈ [0, 12] rad/s |
Wright et al. (1982) (Bir 2010 Table 5) |
flap frequency |
< 0.1 % |
(within tol) |
no |
|
Inextensible spinning cable, flap modes 1-3, Ω ∈ [2, 30] rad/s |
Bir 2009 §III.B / Eq. 8: \(\omega_k = \Omega\sqrt{k(2k-1)}\) |
flap frequency |
< 0.5 % |
(within tol) |
no |
|
Uniform steel tube cantilever via |
Euler-Bernoulli closed form, \(\beta_1 L = 1.875104\) |
1st bending frequency |
< 0.1 % |
machine-exact (forward-derived from geometry) |
no |
|
|
uniform tube: identical to native grid + Euler-Bernoulli; taper: self-convergence \(n{=}200\) vs \(400\) |
1st & 4th bending frequency; mode-shape sample count |
uniform = EB < 0.1 %; taper 1st < 0.2 %, 4th < 2 % (\(200\) vs \(400\)) |
(within tol) |
no |
|
BModes v3.00 CertTest Test01 — non-uniform rotating blade, 60 rpm |
BModes Fortran solver |
per-mode frequency, modes 1-6 |
< 1 % |
< 0.005 % across 20 modes |
yes |
|
BModes v3.00 CertTest Test01, modes 7+ |
BModes Fortran solver |
per-mode frequency |
< 3 % |
(within tol) |
yes |
|
BModes v3.00 CertTest Test02 — rotating blade + tip mass + offsets |
BModes Fortran solver |
per-mode frequency, modes 1-6 / 7+ |
< 1 % / < 3 % |
< 0.005 % across 20 modes |
yes |
|
BModes v3.00 CertTest Test03 — 82.4 m tower with top mass + c.m. offsets |
BModes Fortran solver |
per-mode frequency, modes 1-6 / 7+ |
< 1 % / < 3 % |
< 0.005 % across 20 modes |
yes |
|
BModes v3.00 CertTest Test04 — Test03 + tension-wire support |
BModes Fortran solver |
per-mode frequency, modes 1-6 / 7+ |
< 1 % / < 3 % |
< 0.005 % across 20 modes |
yes |
|
CS_Monopile — NREL 5MW Reference Turbine on the OC3 Monopile ( |
BModes JJ (v1.03.01) |
per-mode frequency, first 10 modes |
< 0.01 % |
< 0.005 % |
yes |
|
OC3Hywind — NREL 5MW on the OC3 Hywind floating spar ( |
BModes JJ (v1.03.01) |
per-mode frequency, first 9 modes |
< 0.01 % |
≤ 0.0003 % across surge / sway / yaw / roll / pitch / heave + 1st-2nd tower bending |
yes |
|
Degenerate-pair resolver, symmetric tower |
construction ( |
post-rotation FA / SS purity |
\(p_{\text{FA}}, p_{\text{SS}} > 0.99\) |
(within tol) |
no |
|
IEA-3.4 modes 1-2 — degenerate-pair resolver fires no warning |
IEA Wind Task 37 RWT deck |
classifier verdict |
no |
clean |
yes |
Citations (full author / year forms used in the table above).
NREL reports link to the canonical docs.nrel.gov PDF; journal /
conference papers link by DOI; textbooks have no DOI and are cited
by title + publisher:
Blevins (1979). Formulas for Natural Frequency and Mode Shape. Krieger Publishing. (Textbook; no DOI.)
Karnovsky & Lebed (2001). Formulas for Structural Dynamics. McGraw-Hill. (Textbook; no DOI.)
Wright, Smith, Thresher & Wang (1982). Vibration Modes of Centrifugally Stiffened Beams. Journal of Applied Mechanics 49(1), 197–202. DOI 10.1115/1.3161966.
Bir (2005). User’s Guide to BModes (Software for Computing Rotating Beam-Coupled Modes). NREL/TP-500-39133. PDF.
Bir (2009). Blades and Towers Modal Analysis Code (BModes): Verification of Blade Modal Analysis Capability. AIAA 2009-1035. DOI 10.2514/6.2009-1035.
Bir (2010). Verification of BModes: Rotary Beam and Tower Modal Analysis Code. NREL/CP-500-47953. PDF.
Jonkman (2007). Dynamics Modeling and Loads Analysis of an Offshore Floating Wind Turbine (Appendix B, catenary mooring). NREL/TP-500-41958. PDF.
Jonkman, Butterfield, Musial & Scott (2009). Definition of a 5-MW Reference Wind Turbine for Offshore System Development. NREL/TP-500-38060. PDF.
Jonkman & Musial (2010). Offshore Code Comparison Collaboration (OC3) for IEA Wind Task 23. NREL/TP-5000-48191. PDF.
Jonkman (2010). Definition of the Floating System for Phase IV of OC3. NREL/TP-500-47535. PDF.
Bortolotti, Tarrés, Dykes, Merz, Sethuraman, Verelst & Zahle (2019). IEA Wind TCP Task 37: Systems Engineering in Wind Energy — WP2.1 Reference Wind Turbines. NREL/TP-5000-73492. PDF.
Gaertner et al. (2020). IEA Wind TCP Task 37: Definition of the IEA 15-Megawatt Offshore Reference Wind Turbine. NREL/TP-5000-75698. PDF.
Allen et al. (2020). Definition of the UMaine VolturnUS-S Reference Platform Developed for the IEA Wind 15-Megawatt Offshore Reference Wind Turbine. NREL/TP-5000-76773. PDF.
Track B — coefficient-consistency cases
The metric here is the RMS residual of an ElastoDyn polynomial block
evaluated against the FEM-computed mode shape produced by the deck’s
own structural inputs. See pybmodes.elastodyn.validate for the
implementation. Per-block verdicts: PASS < 0.01, WARN < 0.10, FAIL ≥ 0.10.
Case |
Source / reference |
Quantity |
Tolerance |
Worst observed (file_rms) |
Test file |
Needs external data |
|---|---|---|---|---|---|---|
Validator on stock NREL 5MW r-test deck — TwFAM2Sh / TwSSM2Sh |
OpenFAST r-test (commit |
file polynomial RMS vs pyBmodes mode shape (detection target) |
verdict = FAIL |
5.08 (TwFAM2Sh) / 5.90 (TwSSM2Sh) |
yes |
|
Validator on same deck — 1st tower modes + blade modes |
OpenFAST r-test |
file polynomial RMS |
verdict = PASS |
0.0081 / 0.0075 / 0.0020-0.0090 |
yes |
|
Patch round-trip on staged NREL 5MW copy |
self (post- |
file polynomial RMS after patching |
verdict = PASS, ratio ≈ 1.0 |
ratio drift < 1 % (text-precision artefact) |
yes |
|
|
committed deliverable |
per-block verdict |
PASS or WARN, no FAIL |
all PASS |
no (artefact tracked) |
|
|
committed deliverable |
per-block verdict |
PASS or WARN, no FAIL |
all PASS |
no |
|
|
committed deliverable |
per-block verdict |
PASS or WARN, no FAIL |
all PASS |
no |
|
|
committed deliverable |
per-block verdict |
PASS or WARN, no FAIL |
all PASS |
no |
|
|
committed deliverable |
per-block verdict |
PASS or WARN, no FAIL |
all PASS |
no |
|
|
committed deliverable |
per-block verdict |
PASS or WARN, no FAIL |
WARN on TwSSM2Sh (1.6 % RMS — ElastoDyn-basis representation limit; see footer in the deck’s |
no |
|
Pre-patch sanity — at least one |
committed before-patch reports |
per-deck overall verdict |
≥ 1 FAIL/WARN |
6/6 FAIL |
no |
Track C — supporting-pipeline behavioural cases
These tests gate the workflow layers that sit between the FEM core and the user — pre-solve sanity checks, mode-by-mode comparison, result serialisation, bundled report generation, batch directory processing, sparse-solver dispatch, Campbell-diagram orchestration, the ElastoDyn-compatibility blade adapter, polynomial- fit conditioning, and parser / writer round-trips. They don’t have a separate external-reference frequency to compare against; the gate is behavioural / contract-style.
Case |
Source / reference |
Quantity |
Tolerance |
Worst observed |
Test file |
Needs external data |
|---|---|---|---|---|---|---|
|
construction |
|
always |
(within tol) |
no |
|
|
construction |
|
always |
(within tol) |
no |
|
|
construction |
|
always |
(within tol) |
no |
|
|
construction |
|
always |
(within tol) |
no |
|
|
construction |
|
always |
(within tol) |
no |
|
|
construction |
|
always |
(within tol) |
no |
|
|
construction |
|
always |
(within tol) |
no |
|
|
construction |
|
always |
(within tol) |
no |
|
|
construction |
|
always |
(within tol) |
no |
|
|
construction |
diagonal entries equal 1, off-diag < 1 for distinct shapes |
exact / |
(within tol) |
no |
|
|
construction |
every entry = 0 |
exact / |
(within tol) |
no |
|
|
construction |
sign of |
exact / |
(within tol) |
no |
|
|
construction |
output pair = identity when shapes match |
exact |
(within tol) |
no |
|
|
construction |
Figure has ≥ 1 Axes; title carries labels |
structural |
(within tol) |
no |
|
|
self |
per-field |
|
(within tol) |
no |
|
|
self |
per-field equality vs original |
exact / |
(within tol) |
no |
|
|
self |
metadata dict populated with non-empty pybmodes_version |
always |
(within tol) |
no |
|
|
self |
per-field |
|
(within tol) |
no |
|
|
self |
header = |
exact match |
(within tol) |
no |
|
NPZ load is pickle-free on modern archives; legacy |
construction (forged legacy archive) |
modern load emits no warning; default legacy load raises |
exact / structural |
(within tol) |
no |
|
|
construction (bad participation / label shapes) |
|
always raises |
(within tol) |
no |
|
|
construction (nselt=10 ⇒ n_free_dof=90) |
n_modes=80 clean; n_modes=200 ERROR |
always |
(within tol) |
no |
|
|
construction |
exit 2 + clear message only when paths differ |
exact match |
(within tol) |
no |
|
|
textbook: |
low-freq plateau + monotonicity |
exact / |
(within tol) |
no |
|
|
textbook JONSWAP; the Hs identity is exact by construction |
|
peak |
(within tol) |
no |
|
|
construction |
axvspans / vlines / legend entries; |
structural / always raises |
(within tol) |
no |
|
|
construction (malformed |
|
exact match / always raises |
(within tol) |
no |
|
|
construction |
every 4-dp frequency appears in body |
exact match |
(within tol) |
no |
|
|
construction |
DOCTYPE + balanced |
structural |
(within tol) |
no |
|
|
construction |
second header row has |
exact column match |
(within tol) |
no |
|
|
self (committed |
6 main decks found; 0 aux files |
exact |
(within tol) |
no |
|
|
construction |
header = |
exact column match |
(within tol) |
no |
|
|
OpenFAST r-test 5MW deck |
per-deck |
no FAIL |
(within tol) |
yes |
|
|
self (committed bundle) |
four analytical-reference subdirs + |
exact match |
(within tol) |
no |
|
|
self (committed bundle) |
|
exact match |
(within tol) |
no |
|
|
self (committed bundle) |
both |
exact match |
(within tol) |
no |
|
|
construction (pre-existing target subdir) |
exit code 2 + preexisting file preserved |
exact match |
(within tol) |
no |
|
|
construction (pre-existing target subdir + stale file) |
stale file removed; real bundle present |
exact match |
(within tol) |
no |
|
|
IEA-15-240-RWT-UMaineSemi WAMIT |
|
|
(within tol) |
yes |
|
|
IEA-15-240-RWT-UMaineSemi WAMIT |
|
|
(within tol) |
yes |
|
|
IEA-15-240-RWT-UMaineSemi WAMIT |
|
|
(within tol) |
yes |
|
|
IEA-15-240-RWT-UMaineSemi WAMIT |
|
|
(within tol) |
yes |
|
|
IEA-15-240-RWT-UMaineSemi WAMIT |
|
|
(within tol) |
yes |
|
|
IEA-15-240-RWT-UMaineSemi WAMIT pair |
|
always |
(within tol) |
yes |
|
|
construction (4 spellings of one path) |
all forms resolve to the same Path |
exact match |
(within tol) |
yes |
|
|
construction (non-existent root) |
|
exact match |
(within tol) |
yes |
|
|
IEA-15-240-RWT-UMaineSemi |
6 scalar fields |
exact / default |
(within tol) |
yes |
|
|
synthetic 2-entry input |
|
exact |
(within tol) |
no |
|
|
synthetic |
dimensional values match the C-form equivalents |
|
(within tol) |
no |
|
|
synthetic deck with |
|
exact match |
(within tol) |
no |
|
|
construction (EA → ∞) |
residuals on Irvine eq. (2.27)–(2.29) |
|
(within tol) |
no |
|
|
construction (ΔZ = 0) |
|
|
(within tol) |
no |
|
|
construction (typical slack-line geometry) |
both catenary residuals after solve |
|
(within tol) |
no |
|
|
construction (synthetic 3-line layout) |
F_x, F_y, M_x, M_y, M_z all |
|
(within tol) |
no |
|
|
construction (synthetic 3-line) |
every diag entry > 0 |
always |
(within tol) |
no |
|
|
construction (synthetic 3-line) |
|
always |
(within tol) |
no |
|
|
OpenFAST r-test 5MW_OC3Spar MoorDyn |
3 lines / 6 points / fairlead radius 5.2 / anchor radius 853.87 / L 902.2 |
|
(within tol) |
yes |
|
|
OC3 r-test MoorDyn |
|
|
within 0.01 % |
yes |
|
|
OC3 r-test MoorDyn |
|
|
(within tol) |
yes |
|
|
OC3 r-test MoorDyn |
K[0,0] = K[1,1], K[3,3] = K[4,4], K[0,4]·K[1,3] < 0 |
|
(within tol) |
yes |
|
|
OC3 ElastoDyn + r-test MoorDyn |
hub_conn=2, PlatformSupport populated, 1st tower-bending in 0.4–0.6 Hz |
|
within 1.2 % of Jonkman 2010’s 0.482 Hz |
yes |
|
|
Yu and Amdahl (2023) Marine Structures 92, 103482, Table 9 row 2 |
K_hh, K_hr, K_rr via Shadlou and Bhattacharya (2016) formulas |
|
within 3.0 % on all three springs |
no |
|
|
Yu and Amdahl (2023) Table 9 row 1 |
K_hh, K_hr, K_rr via Shadlou and Bhattacharya (2016) rigid-pile formulas |
|
within 2.9 % on all three springs |
no |
|
|
construction |
K[0,4] < 0, K[1,3] > 0, K[0,4]·K[1,3] < 0 |
always |
(within tol) |
no |
|
Randolph (1981) pile classifier — auto dispatch to flexible / rigid / intermediate |
construction (synthetic geometries) |
high L/D → flexible; low L/D → rigid; intermediate emits |
always |
(within tol) |
no |
|
Sparse |
construction (SPD problem at threshold + 100 DOFs) |
lowest-k eigenvalues |
|
(within tol) |
no |
|
Sparse path triggered above threshold |
construction |
log message announces “sparse shift-invert” |
always |
(within tol) |
no |
|
Sparse path skipped below threshold |
construction |
log message does not mention “sparse” |
always |
(within tol) |
no |
|
Asymmetric problems fall back to dense general |
construction (asymmetric K) |
log message announces “dense general”; sparse not invoked |
always |
(within tol) |
no |
|
|
construction |
mtime of every staged file unchanged |
exact match |
(within tol) |
yes |
|
|
construction |
stdout contains |
exact match |
(within tol) |
yes |
|
|
construction |
source-file sha256 unchanged |
exact match |
(within tol) |
yes |
|
Default in-place |
construction |
stdout contains |
exact match |
(within tol) |
yes |
|
Tower torsion-contamination filter — rejects T_tor ≥ 10 % |
construction (synthetic torsion-contaminated mode) |
|
always |
(within tol) |
no |
|
Tower torsion-contamination filter — accepts pure bending |
construction (synthetic clean modes) |
|
always |
(within tol) |
no |
|
IEA-3.4 deck — torsion participations populated, summing to 1 |
OpenFAST IEA-3.4-130-RWT deck |
per-mode |
|
(within tol) |
yes |
|
Campbell sweep — Hungarian MAC tracking on bundled NREL 5MW reference deck |
committed |
|
always |
(within tol) |
no |
|
Campbell sweep — input validation (NaN / inf / negative / unsorted RPM) |
construction |
|
always raises |
(within tol) |
no |
|
Campbell sweep — restores |
construction |
post-sweep / post-exception BMI state |
unchanged |
(within tol) |
no |
|
Campbell sweep — tower modes constant across all rotor speeds |
construction |
tower frequency vs rotor speed |
exactly constant (no |
(within tol) |
no |
|
|
construction (synthetic CampbellResult + platform_modes) |
horizontal refs + right-margin labels + |
structural / exact |
(within tol) |
no |
|
ElastoDyn-compat blade adapter — strips |
Jonkman 2015 NREL forum guidance |
resulting BMI fields |
zeroed for compat-on, preserved for compat-off |
(within tol) |
no |
|
ElastoDyn-compat — frequency drift on flap modes is small |
construction |
rel freq diff vs compat-off |
≤ ~ few % on flap modes |
(within tol) |
no |
|
ElastoDyn |
self |
per-field |
|
(within tol) |
yes |
|
Polynomial-fit design-matrix cond-number reporting |
construction |
|
WARN > 1e4, FAIL > 1e6 |
(within tol) |
no |
|
BMI / section-properties parser primitives |
construction (synthetic fixtures) |
round-trip equality |
exact / |
(within tol) |
no |
|
FEM building blocks — boundary conditions, eigensolver, normalisation |
construction |
per-DOF / per-mode invariants |
exact / |
(within tol) |
no |
|
|
\(A=\pi(R_o^2-R_i^2)\), \(I=\tfrac{\pi}{4}(R_o^4-R_i^4)\), \(J=2I\), \(G=E/2(1+\nu)\) (textbook) |
mass_den / flp_stff / tor_stff / axial_stff / flp_iner |
exact |
|
no |
|
|
construction (factor 1.07) |
mass × 1.07; EI / GJ / EA / structural ρI unchanged |
exact |
|
no |
|
Geometry guard — rejects D ≤ 0 / t ≤ 0 / 2t ≥ D |
construction (degenerate sections) |
|
always raises |
(within tol) |
no |
|
WindIO dialect equivalence — modern |
construction (numerically identical fixtures) |
same |
exact / |
(within tol) |
no |
|
WindIO duplicate-anchor tolerance — IEA-10’s redefined-anchor habit |
construction ( |
parses (last-wins) where strict PyYAML fails |
exact |
(within tol) |
no |
|
WindIO orthotropic wall material — clear out-of-scope error |
construction (list-valued composite E) |
|
exact match |
(within tol) |
no |
|
WindIO thickness interp — linear vs piecewise-constant differs on a taper |
construction (smoothly tapered wall) |
mid-station wall thickness diverges by > 0.01 m |
exact |
0.035 vs 0.05 m |
no |
|
WindIO PyYAML-absent guard — names the |
construction (monkeypatched import) |
|
exact match |
(within tol) |
no |
|
IEA-15 base WindIO yaml → derived mass / EI vs IEA-15 Monopile ElastoDyn tower table (RNA-independent like-for-like; that deck was generated from this geometry) |
IEA Wind Task 37 IEA-15-240-RWT ontology + OpenFAST Monopile deck |
distributed mass_den & FA EI, station-by-station |
< 0.5 % |
7.5 × 10⁻¹² (machine-exact) |
yes |
|
Upstream WindIO corpus — IEA-3.4 / 10 / 15 / 22 + WISDEM examples (both dialects + IEA-10 dup-anchor) parse to a physically sane tube |
IEA Wind Task 37 RWT ontologies + WISDEM example yamls |
grid ∈ [0,1] monotone, D / t > 0, 2t < D, steel E / ρ / ν range, span > 0 |
structural |
12 yamls clean |
yes |
|
|
IEA Wind Task 37 RWT ontologies |
full FEM solve → bare-member spectrum |
finite, positive, ascending |
(within tol) |
yes |
|
Older-dialect yaml-derived mass / EI vs same-turbine ElastoDyn tower table (ballpark — those decks were not 1:1 geometry round-trips, unlike IEA-15) |
IEA-3.4 / 10 / 22 ontology + own OpenFAST tower deck |
distributed mass_den & FA EI envelope |
mass < 25 %, EI < 30 % |
(within tol) |
yes |
|
CLT laminate primitives — reduced stiffness / ply transform / ABD / membrane condensation |
Jones, Mechanics of Composite Materials (2nd ed.) §2.5–4.3 closed forms |
\(Q\), \(\bar Q(\theta)\), \(A/B/D\), \(\tilde A\) |
exact |
|
no |
|
Airfoil |
construction (analytic circle & ellipse) |
|
exact (≤ 2e-3) |
(within tol) |
no |
|
WindIO web/layer |
construction (numerically identical fixtures) |
identical resolved bands from both dialects |
exact / |
(within tol) |
no |
|
Thin-wall reduction — isotropic tube & box vs exact closed form |
textbook thin-ring / thin-wall-box EA / EI / GJ / mass |
section properties |
exact |
< 2 % (discretisation) |
no |
|
Multi-cell Bredt–Batho — symmetric interior web carries zero shear flow |
analytic (a symmetric diametral web ⇒ GJ unchanged vs webless) |
|
exact |
< 2 % |
no |
|
|
construction (tube blade, both WindIO dialects) |
identical |
exact / |
(within tol) |
no |
|
6×6 cross-sectional decoupling (issue #50) — rigid-offset congruence → tension centre → principal-axis eigen-decomposition → shear centre / |
construction (build a 6×6 from known decoupled props at a known offset / rotation) |
recovered |
exact ( |
(within tol) |
no |
|
WindIO published distributed elastic properties → decoupled beam (issue #48/#50) — |
construction (tube blade with embedded published properties, both dialects) |
|
exact / |
(within tol) |
no |
|
WindIO published elastic properties vs the turbine’s own BeamDyn 6×6, IEA-15, span 0.15–0.90 (issue #48/#50) — decoupled-vs-decoupled (the BeamDyn 6×6 is also referenced at the blade axis, so the apples-to-apples oracle is the decoupled BeamDyn, not its raw diagonal); also pins that raw≠decoupled so the case genuinely exercises #50, and that the published path stays far tighter than PreComp |
IEA Wind Task 37 IEA-15 ontology + companion |
decoupled |
mass & EA med < 3 %; EI med < 5 %; GJ med < 8 %; raw-vs-decoupled EI > 5 %; strictly tighter than |
(within tol) |
yes |
|
WindIO composite blade → distributed beam props vs the turbine’s own BeamDyn 6×6 (WISDEM-PreComp-generated; |
IEA Wind Task 37 RWT ontology + companion |
|
mass med < 6 % / max < 15 %; EA med < 10 % / max < 18 %; GJ med < 22 % / max < 47 %; EI med < 35 % / max < 55 % |
mass ≈ 1.5–4 % med ; EA ≈ 1–8 % med ; GJ ≈ 3–18 % med (IEA-10 worst, composite multi-cell torsion) ; EI ≈ 2–27 % med (weak axis worst — omits spar-cap-offset / bend-twist coupling) |
yes |
|
WindIO |
IEA-10 ontology |
documented divergence (not a parser bug — same pattern as |
n/a (documented, not gated) |
≈ 50 % (intrinsic to upstream data) |
yes |
|
|
IEA Wind Task 37 RWT ontologies |
full FEM → parked-blade spectrum |
finite, positive, ascending |
(within tol) |
yes |
|
WindIO floating reader — joints (cartesian + cylindrical |
construction |
parsed |
exact / |
(within tol) |
no |
|
Hydrostatic restoring — single surface-piercing cylinder & off-axis column vs closed form |
textbook waterplane + buoyancy (WAMIT/ |
|
exact |
< 1 % (discretisation) |
no |
|
Morison added mass + RAFT end-cap & rigid-body inertia vs closed form |
thin-ring transverse + RAFT |
|
exact |
|
no |
|
|
construction |
|
exact / symmetry |
(within tol) |
no |
|
|
construction |
full coupled FEM spectrum |
finite, ascending, soft RB ≪ tower |
(within tol) |
no |
|
WindIO floating C_hst vs the turbine’s own potential-flow WAMIT |
IEA-15 UMaine VolturnUS-S ontology + companion HydroDyn/WAMIT |
heave / roll / pitch restoring |
< 3 % / < 4 % |
heave 0.8 %, roll/pitch 1.6 % |
yes |
|
WindIO screening-preview Morison |
IEA-15 UMaine VolturnUS-S + companion HydroDyn / ElastoDyn |
added-mass diagonal / |
surge/sway/yaw < 45 %; heave/roll/pitch factor ~2; mass a lower bound |
surge 22 %, heave 53 % (Morison ≠ BEM, as RAFT/WISDEM also find — supply a HydroDyn deck for industry grade); struct+fixed mass ≈ 0.36·PtfmMass (trim ballast excluded by design) |
yes |
|
|
IEA-15 UMaine VolturnUS-S ontology + companion MoorDyn |
deck-fallback line props; 6×6 |
props exact (rel<1e-9); roll/pitch<15 %, heave<15 %, yaw<20 %, surge/sway<40 % |
props exact; roll/pitch ~3 %, heave ~9 %, surge/sway ~32 % (WindIO centreline vs MoorDyn surface-attachment radius — the screening path; the coupled industry path below uses the full MoorDyn system) |
yes |
|
|
IEA Wind Task 37 ontology + companion HydroDyn/MoorDyn/ElastoDyn |
coupled rigid-body + tower-bending frequencies |
every platform RB mode + 1st tower bending < 1 %; 2nd+ tower harmonics < 8 % |
all 6 platform rigid-body modes + 1st tower bending 0.0–0.3 % (reference grade); 2nd+ tower harmonics ≤ 6 % (the Phase-1 WindIO-vs-ElastoDyn tower-discretisation residual, orthogonal to the platform) |
yes |
|
|
construction (synthetic FOWT yaml + stable diagonal PlatformSupport) |
byte-equivalence to the manual WindIO-tower-BMI + attach-PlatformSupport recipe; no screening warning; decks mutually-exclusive |
|
(within tol) — adds no new numerics; the free-free PlatformSupport FEM is the OC3-Hywind-certified one ( |
no |
|
|
construction |
discovery dict + report file + exit code + SCREENING warning |
exact / structural |
(within tol) |
no |
|
|
IEA Wind Task 37 IEA-15 RWT tree (ontology + companion HydroDyn/MoorDyn/ElastoDyn) |
discovered config + exit code + report + no screening warning |
exact / structural |
(within tol) |
yes |
What “needs external data” means — and how integration coverage is gated
yes rows skip in the default pytest run (they carry the
integration marker). Run them locally with pytest -m integration
once you have the upstream sources:
external/OpenFAST_files/r-test/— clone of the OpenFAST regression-test corpus (any recent commit; pyBmodes was last validated againstdd5feaaa).external/OpenFAST_files/IEA-3.4-130-RWT/,IEA-10.0-198-RWT/,IEA-15-240-RWT/,IEA-22-280-RWT/— clones of the IEA Wind Task 37 reference-turbine repositories (each ships a WindIO ontology.yamlplus matching OpenFAST/ElastoDyn decks; the WindIO validation rows consume both).external/OpenFAST_files/WISDEM/examples/— clone of the WISDEM examples tree (modern-dialect WindIO ontology yamls for the corpus-parse coverage).external/BModes/CertTest/— BModes v3.00 CertTest reference outputs.external/BModes/docs/examples/— the bundledCS_Monopile.bmiandOC3Hywind.bmiexample decks plus their BModes JJ.outfiles.
These directories are gitignored under the Independence stance. The data is not bundled in the repo because the licence terms of the upstream NREL / IEA Wind Task 37 packages include attribution / indemnification obligations that pyBmodes can’t inherit by republication. The contributor clones them locally.
Integration-track coverage is split by whether the upstream data is publicly clonable:
Public required set — CI-gated. The Validation workflow clones the required public upstreams (OpenFAST r-test + IEA-3.4 / 10 / 15 / 22 + WISDEM) at their manifest-pinned commit SHAs and runs
pytest -m integrationhard-fail — no exit-5 tolerance — on everyworkflow_dispatchand the weekly cron. It first runsverify_external_data.py --strict(pinned commit + line-ending- normalized content hashes; a missing required clone is a hard FAIL) and uploads the verifier report. Publishing to PyPI is gated on a green run of this workflow for the tagged commit. So the public integration cases carry re-runnable CI evidence, not just a maintainer claim.BModes CertTest + optional cross-comparisons — maintainer-local. The BModes Fortran solver and its CertTest decks are on GitHub (old-NWTC/BModes), but they are government-funded reference data pyBmodes does not bundle or auto-fetch in CI; the CS_Monopile / OC3Hywind rows additionally compare against
.outfiles from the patched BModes_JJ binary (distributed only via the author’s personal drive, not an official release). MoorPy / RAFT areoptional = truecross-comparison clones; those rows stay maintainer-local until/unless mirrored somewhere CI can fetch. Their tests skip cleanly when the data is absent.Per-PR
ci.ymlruns the self-contained suite and tolerates exit code 5 (“no tests collected”) on its integration step, because the default PR runner has no upstream decks — the hard-fail enforcement lives invalidation.ymlabove, not in the per-PR job.scripts/audit_validation_claims.py(run in CI and the release checklist) scans this matrix and asserts that every test-file path named in a row exists and contains at least one collected test method — the gate that catches “claim ahead of test” drift.
For users: the public integration cases (the NREL 5MW family and the IEA Wind Task 37 turbines) carry re-runnable CI evidence via the Validation workflow, pinned to immutable upstream commits and hashed for content. Only the BModes CertTest guarantee remains “the maintainer ran these locally before tagging,” because that reference data isn’t publicly clonable.
Reproducing every row
Track A (no external data):
pytest tests/fem/test_cantilever.py
pytest tests/fem/test_uniform_tower_analytical.py
pytest tests/fem/test_rotating_uniform_blade.py
pytest tests/fem/test_rotating_blade_with_tip_mass.py
pytest tests/fem/test_rotating_cable.py
pytest tests/test_classifier.py # 3 of 4 pass without external data
pytest tests/test_geometry_windio.py # closed-form tube + dialect/anchor robustness
pytest tests/test_windio_blade.py # CLT + thin-wall + multi-cell closed forms
pytest tests/test_windio_floating.py # hydrostatic / Morison / mooring closed forms
Track A (external data needed):
pytest tests/test_certtest.py -m integration
pytest tests/test_geometry_windio.py -m integration # WindIO corpus + IEA-15 anchor
pytest tests/test_windio_blade.py -m integration # blade vs BeamDyn 6×6 (4 RWTs)
pytest tests/test_windio_floating.py -m integration # IEA-15 VolturnUS-S vs WAMIT/MoorDyn/ED
Track B:
pytest tests/test_validate.py -m integration # validator round-trip
pytest tests/test_reference_decks.py # always self-contained
Or run the full matrix at once:
pytest -m "" # default + integration combined