API reference
The full public API of pybmodes. The semver-stable surface is
enumerated in API contract.
High-level models
RotatingBlade: high-level model for a rotating wind-turbine blade.
- class RotatingBlade(bmi_path)[source]
Bases:
objectCompute natural frequencies and mode shapes for a rotating blade.
- Parameters:
bmi_path (path to the .bmi input file (beam_type must be 1).)
- coeff_validation: ValidationResult | None = None
- classmethod from_elastodyn(main_dat_path, *, elastodyn_compatible=True, validate_coeffs=False)[source]
Build a blade model from an OpenFAST ElastoDyn main
.dat.The blade .dat is resolved relative to the main file via
BldFile(1)(orBldFile1in the IEA-RWT convention). Centrifugal stiffening usesRotSpeedfrom the main file.- Parameters:
main_dat_path (str | Path) – Path to the ElastoDyn main
.datfile.elastodyn_compatible (bool) – When
True(default) the blade section properties are overridden in place to match ElastoDyn’s simplified structural model — pure flap/edge bending, no axial / torsional DOFs, no offsets — per Jonkman’s March 2015 NREL forum guidance for generating ElastoDyn polynomial inputs. See_apply_elastodyn_compatibility()for the exact settings. Set toFalseto keep the structural-property blocks exactly as parsed; aUserWarningis emitted in that case because the resulting mode shapes will not match ElastoDyn’s expectations and any polynomial fit generated from them is unsafe to feed back into ElastoDyn.validate_coeffs (bool) – If
True, runpybmodes.elastodyn.validate_dat_coefficients()after building the model and attach the result asself.coeff_validation. Emits aUserWarningif any block fails or warns. DefaultFalse.
- Return type:
- classmethod from_windio(yaml_path, *, component='blade', n_span=30, rot_rpm=0.0, n_perim=300, elastic='auto')[source]
Build a blade model from a WindIO ontology
.yaml(issue #35).elastic(issue #48) selects the beam-property source:"auto"(default) uses the WindIO published distributed properties (elastic_properties/elastic_properties_mb) when present so the model matches the reference exactly, and reduces the composite layup via the PreComp-class thin-wall cross-section reduction (pybmodes.io.windio_blade) only when they are absent;"precomp"always reduces the layup (the pre-1.5 behaviour);"file"requires the published properties. The blade is clamped at the root (hub_conn = 1);rot_rpmsets the centrifugal stiffening (default 0 = parked). Needs the optional[windio]extra (PyYAML).Unlike
from_elastodyn(), this keeps the physical section properties (twist, offsets, torsion / axial) — the WindIO path’s purpose is structural fidelity, not regenerating ElastoDyn polynomial coefficients.
- run(n_modes=20, *, check_model=True, on_error='raise')[source]
Solve the eigenvalue problem and return frequencies + mode shapes.
- Parameters:
n_modes (number of modes to extract (must be >= 1; default 20).)
check_model (run
pybmodes.checks.check_model()before the) – solve (defaultTrue). INFO findings are silent (callpybmodes.checks.check_model(model)explicitly to see those). Passcheck_model=Falseto skip the pre-solve checks for scripted callers that have already validated their inputs.on_error (how ERROR-severity findings are handled when) –
check_modelruns (default"raise", 1.14.0). ERROR findings flag non-physical input, so the solve fails closed by raisingpybmodes.checks.ModelValidationErrorrather than feeding the eigensolver garbage. Passon_error="warn"to downgrade ERROR findings toUserWarningand continue, the pre-1.14.0 behaviour. WARN findings always emit asUserWarningregardless.
- Return type:
Tower: high-level model for a wind-turbine tower.
Phase 3 PR C3 of the v1.x architecture refactor pulled three module-
private helpers out of this file into siblings under
pybmodes.models:
pybmodes.models._shared._run_validation_and_warn()— cross-model (also used byRotatingBlade.from_elastodyn).pybmodes.models._platform._scan_platform_fields(),pybmodes.models._platform._platform_inertia_matrix()— tower- side platform-scalar parsers + inertia assembler.
All three are re-exported below so callers / tests that still import
via from pybmodes.models.tower import _scan_platform_fields (etc.)
keep working unchanged.
- class Tower(bmi_path)[source]
Bases:
objectCompute natural frequencies and mode shapes for a tower.
- Parameters:
bmi_path (path to the .bmi input file (beam_type must be 2).)
- attach_mudline_foundation(foundation)[source]
Attach a mudline coupled-spring soil foundation to a clamped monopile model and switch the boundary condition to
hub_conn = 3(soft monopile, axial + torsion clamped, lateral + rocking free).Wires the foundation’s 6 x 6
mooring_Kblock into a freshPlatformSupportcarrying zero hydro and zero platform inertia, setstow_support = 1(inline platform block) and flipshub_connto3. The tower’s section properties and tip mass are preserved. Returnsselffor chaining.Use this to convert a rigid-clamped monopile model built via
from_windio_with_monopile(),from_elastodyn_with_subdyn(), or any otherhub_conn = 1constructor into a soft monopile with the soil-pile interaction computed frompybmodes.MudlineFoundation. The mudline stiffness affects the coupled-system frequency only; ElastoDyn polynomial coefficient generation continues to use the cantilever path regardless of soil flexibility, for the same architectural reasonsrc/pybmodes/_examples/reference_decks/FLOATING_CASES.mdrecords for floating platforms.Raises
ValueErrorif the tower already carries a free-base floating model (hub_conn = 2) or a pinned-free cable BC (hub_conn = 4). Replaces any existingsupporton the BMI; use a freshTower.from_*build if you need to preserve a pre-existing support block.- Parameters:
foundation (MudlineFoundation)
- Return type:
- coeff_validation: ValidationResult | None = None
- classmethod from_bmi(bmi_path)[source]
Build a tower model from a BModes-format
.bmideck.Equivalent to
Tower(bmi_path)— exposed as an explicit classmethod so callers can pick the constructor by source format symmetrically withfrom_elastodyn()andfrom_elastodyn_with_subdyn().The BMI parser already covers all four certtest configurations (cantilever blade, blade + tip mass, cantilever tower, tension- wire-supported tower) plus the offshore platform-support paths (
hub_conn∈ {1, 2, 3},tow_support∈ {0, 1, 2}, withPlatformSupportcarrying hydro / mooring / platform-inertia 6×6 matrices). All of those flow through the standard FEM pipeline; this constructor is a thin handle.
- classmethod from_elastodyn(main_dat_path, *, validate_coeffs=False)[source]
Build a tower model from an OpenFAST ElastoDyn main
.dat.The main file is parsed plus the tower file referenced via
TwrFileand (when the path is resolvable) the first blade file referenced viaBldFile(1)— the latter is read only to compute the rotor-mass contribution to the lumped tower-top assembly.- Parameters:
main_dat_path (str | Path) – Path to the ElastoDyn main
.datfile.validate_coeffs (bool) – If
True, runpybmodes.elastodyn.validate_dat_coefficients()after building the model and attach the result asself.coeff_validation. Emits aUserWarningif any block fails or warns. DefaultFalseso the standard constructor stays cheap.
- Return type:
- classmethod from_elastodyn_with_mooring(main_dat_path, moordyn_dat_path, hydrodyn_dat_path=None)[source]
Build a free-free floating tower model with a populated
PlatformSupportblock.Assembles the platform-support 6 × 6 matrices from three OpenFAST decks:
Mooring stiffness
K_moorfrom a MoorDyn.dat(parsed viapybmodes.mooring.MooringSystem.from_moordynand linearised at zero offset).Hydrodynamic added mass
A_infand hydrostatic restoringC_hstfrom a HydroDyn.dat(parsed viapybmodes.io.HydroDynReader, which followsPotFileto the WAMIT.1and.hstfiles). Optional — ifhydrodyn_dat_pathis omitted, both default to zero, so the resulting model couples only mooring + platform inertia.Platform inertia from the
PtfmMass/PtfmRIner/PtfmPIner/PtfmYIner/PtfmCM*/PtfmRefztscalars in the ElastoDyn main file. The 6 × 6i_matrixis stored AT THE CM (no parallel-axis transfer); the downstreampybmodes.fem.nondim.nondim_platformapplies the rigid-arm transform from CM to tower base usingcm_pform - draft.cm_pformanddraftare written in BModes file convention (positive distance below MSL; signed draft with negative = base above MSL).
Sets
hub_conn = 2(free-free floating base) andtow_support = 1(inline platform-support block).Notes
For ElastoDyn polynomial-coefficient generation use the standard cantilever
Tower.from_elastodyn()instead. The polynomial ansatz lives in a clamped-base frame regardless of platform configuration, and the audit trail (OpenFAST source-code line citations) is recorded insrc/pybmodes/_examples/reference_decks/FLOATING_CASES.mdandcases/ECOSYSTEM_FINDING.md. This method is for coupled-frequency prediction only.To reconcile pyBmodes-generated polynomial coefficients against an OpenFAST linearisation frequency on the same deck (the polynomial encodes the cantilever 1st FA, OpenFAST linearisation reports the coupled 1st FA, and they can differ by 20-30 percent on floating platforms), call
pybmodes.elastodyn.report_floating_frequency_gap().
- classmethod from_elastodyn_with_subdyn(main_dat_path, subdyn_dat_path)[source]
Build a combined pile + tower cantilever from an ElastoDyn deck plus a SubDyn substructure file.
The pile geometry comes from the SubDyn file (joints + members + circular cross-section properties); the tower above the transition piece comes from the ElastoDyn main + tower files. The two are spliced into a single cantilever with a clamped base at the SubDyn reaction joint (no soil flexibility).
Designed for OC3-style fixed-base monopiles. Does not handle soil springs, hydrodynamic added mass, or non-circular substructure members. See
pybmodes.io.subdyn_reader.to_pybmodes_pile_tower()for the assembly details.
- classmethod from_geometry(station_grid, outer_diameter, wall_thickness, *, flexible_length, E=200000000000.0, rho=7850.0, nu=0.3, outfitting_factor=1.0, hub_conn=1, tip_mass=None, n_nodes=None)[source]
Build a tower model from tubular geometry instead of pre-computed structural properties (issue #35).
The user supplies only what they actually know — the circular tube’s outer diameter and wall thickness per station, plus the material — and pyBmodes derives mass / EI / GJ / EA from the exact closed-form tube relations (
pybmodes.io.geometry.tubular_section_props()), eliminating the hand-computed-properties error class.- Parameters:
station_grid ((n,) normalised station locations
[0, 1]) – from tower base (0) to top (1). WindIO grids are already in this form. Duplicate-pair stations encoding a property step are handled exactly as the ElastoDyn path does.outer_diameter ((n,) metres, per station.)
wall_thickness ((n,) metres, per station.)
flexible_length (physical flexible tower length (m), i.e.) –
TowerHt - TowerBsHt. Sets the FEM beam length.E (isotropic material (default ASTM-A572 steel:) – 200 GPa, 7850 kg/m^3, 0.3).
rho (isotropic material (default ASTM-A572 steel:) – 200 GPa, 7850 kg/m^3, 0.3).
nu (isotropic material (default ASTM-A572 steel:) – 200 GPa, 7850 kg/m^3, 0.3).
outfitting_factor (non-structural mass multiplier (internals) – / flanges / paint / bolts). Scales the distributed mass density only — not rotary inertia (a structural section property) and never stiffness. This is the WindIO-native way to “account for internals/flanges”; for a single discrete tower-top mass pass
tip_mass.hub_conn (root BC — 1 cantilever (default; the basis) – ElastoDyn polynomial coefficients require), 3 soft monopile, etc.
tip_mass (optional RNA / tower-top lump — a) –
pybmodes.io.bmi.TipMassProps, or a bare float (the RNA mass in kg; the inertia / offset terms default to zero — the common case, issue #35).None-> zero tip mass.n_nodes (optional FE-mesh refinement (issue #35). When given,) – the geometry is re-gridded onto ``n_nodes`` evenly- spaced stations over the normalised span and the outer diameter / wall thickness are linearly interpolated onto it before the closed-form tube reduction (so each refined station still gets exact tube properties, not interpolated ones). A finer mesh resolves the higher tower-bending mode shapes; frequencies are unbiased (convergent) — see
tests/test_geometry_windio.py.Nonekeeps the supplied grid verbatim. Note: a uniform resample linearly smooths a deliberately stepped geometry (e.g. a wall-thickness jump); omitn_nodesto preserve such steps exactly.
- Return type:
Notes
Arbitrary discrete mid-span point masses are not yet modelled (a separate FEM-assembly extension with its own validation track — see issue #35);
outfitting_factorcovers the dominant distributed non-structural mass andtip_massthe tower-top lump.
- classmethod from_windio(yaml_path, *, component='tower', thickness_interp='linear', hub_conn=1, tip_mass=None, n_nodes=None)[source]
Build a tower (or monopile) model directly from a WindIO ontology
.yaml(issue #35).Parses the structural subset —
components.<component>’souter_shape.outer_diameter,structure.layerswall thickness,structure.outfitting_factor,reference_axis— plus the referenced entry in the top-levelmaterialslist, and feeds it tofrom_geometry().- Parameters:
yaml_path (path to a WindIO ontology file.)
component (
"tower"(default) or"monopile".)thickness_interp (how a layer thickness grid maps onto the) – FEM stations —
"linear"(WindIO-native piecewise- linear, default) or"piecewise_constant"(WISDEM-style constant-per-segment). The choice measurably moves the 2nd tower-bending polynomial coefficients; seetests/test_windio.py.hub_conn (root BC (default 1 cantilever; use 3 for a) – soil-flexible monopile).
tip_mass (optional tower-top RNA lump (issue #35) — a) –
pybmodes.io.bmi.TipMassPropsor a bare float (RNA mass in kg). Replaces thetower._bmi.tip_mass = …workaround and mirrorsfrom_windio_floating()’srna_tip.None-> zero tip mass.n_nodes (optional FE-mesh refinement (issue #35) — re-grid) – the tower onto
n_nodesevenly-spaced stations (geometry linearly interpolated, properties recomputed exactly), to resolve higher tower-bending mode shapes.Nonekeeps the WindIO grid. The WindIO blade path has the analogousn_span.
- Return type:
Notes
Requires the optional
[windio]extra (PyYAML). This is the tubular tower / monopile path; for a WindIO blade composite layup usepybmodes.models.RotatingBlade.from_windio()(PreComp-class thin-wall cross-section reduction).
- classmethod from_windio_floating(yaml_path, *, component_tower='tower', water_depth=None, hydrodyn_dat=None, moordyn_dat=None, elastodyn_dat=None, platform_support=None, rna_tip=None, n_nodes=None, rho=1025.0, g=9.80665)[source]
Coupled floating tower+platform model from a WindIO
.yaml(issue #35).The WindIO-native analogue of
from_elastodyn_with_mooring(). The tower beam always comes fromcomponents.tower(the validated Phase-1 tubular path — machine-exact vs the ElastoDyn tower). The platform has two fidelity tiers:Industry-grade (companion decks present). When a
hydrodyn_dat/moordyn_dat/elastodyn_datis supplied, that leg uses the complete deck model — WAMITA_inf+C_hst, the full MoorDyn system (its own anchor/fairlead geometry and line properties), and the ElastoDynPtfmMass/RIner(incl. trim ballast) + lumped RNA + draft convention. With all three present this is byte-identical to the BModes-JJ-validatedfrom_elastodyn_with_mooring()except the tower is the WindIO one (≈ 0.0003 % reference grade).Screening preview (yaml-only legs). Any leg without a deck falls to the WindIO model — member-waterplane
C_hst(geometry-exact, ≈ 1.6 %), Morison + end-capA_inf(heave is screening-only — Morison ≠ potential flow, as RAFT/WISDEM also find), WindIO catenary mooring geometry, and structural + fixed-ballast inertia (no trim ballast). AUserWarningnames every screening leg; it is not industry-grade for the platform and is for fast pre-deck previewing only.Injected platform (``platform_support=``). When a
pybmodes.io.bmi.PlatformSupportis passed, the floater is taken verbatim from it — its ownA_inf/C_hst/mooring_K/ 6×6 inertia /draft/ref_msl. The tower geometry still comes from the WindIO.yaml; nothing about the floating substructure is read from the yaml or any deck. This is the “floater designed separately” workflow — a frequency-domain tool / WAMIT export / published 6×6 set feeding the same BModes-JJ-validated free-free FEM that reproduces OC3 Hywind to ≈ 0.0003 %. The tower beam length stays the WindIOflexible_lengthindependent of the supplieddraft(theradius + draftcancellation the deck path also relies on — seemake_params). No screening warning (the caller owns the platform fidelity). Mutually exclusive with the companion decks; optionally passrna_tipfor the tower-top RNA lump (default: bare tower top, no RNA). The frequencies inherit the validated PlatformSupport assembly; this path adds no new numerics.
n_nodesre-grids the tower beam onto that many evenly-spaced stations (geometry linearly interpolated, closed-form tube properties recomputed exactly), mirroringfrom_windio()/from_geometry()(issue #58 — uniform mesh-refinement kwarg across the WindIO/geometry constructors).Nonekeeps the WindIO grid. It refines only the tower discretisation; the platform assembly is unaffected.Sets
hub_conn = 2/tow_support = 1and reuses the existing BModes-JJ-validated free-freePlatformSupportFEM unchanged. Needs the optional[windio]extra. For ElastoDyn polynomial generation use the cantileverfrom_windio()regardless of platform (seecases/ECOSYSTEM_FINDING.md).- Parameters:
yaml_path (str | pathlib.Path)
component_tower (str)
water_depth (float | None)
hydrodyn_dat (str | pathlib.Path | None)
moordyn_dat (str | pathlib.Path | None)
elastodyn_dat (str | pathlib.Path | None)
platform_support (PlatformSupport | None)
rna_tip (TipMassProps | None)
n_nodes (int | None)
rho (float)
g (float)
- Return type:
- classmethod from_windio_with_monopile(yaml_path, *, component_tower='tower', component_monopile='monopile', thickness_interp='linear', tip_mass=None, n_nodes=None)[source]
Build a combined monopile + tower fixed-bottom cantilever from a WindIO ontology
.yaml(issue #92).from_windio()reduces a single tube; this constructor reduces themonopileandtowercomponents separately (each keeps its own wall schedule and steel grade) and splices them bottom-to-top at the transition piece — the elevation where the monopile top meets the tower base — into one cantilever clamped at the mudline (hub_conn = 1), with the RNA lumped at the tower top viatip_mass. It is the WindIO analog offrom_elastodyn_with_subdyn()(the ElastoDyn + SubDyn splice).- Parameters:
yaml_path (path to a WindIO ontology file carrying both a) –
monopileand atowercomponent.component_tower (component names to splice) – (defaults
"tower"/"monopile").component_monopile (component names to splice) – (defaults
"tower"/"monopile").thickness_interp (
"linear"(default) or) –"piecewise_constant"— seefrom_windio().tip_mass (optional tower-top RNA lump — a) –
pybmodes.io.bmi.TipMassPropsor a bare float (RNA mass in kg).None-> zero tip mass.n_nodes (optional FE-mesh refinement applied per segment) – (each of the monopile and tower is re-gridded onto
n_nodesevenly-spaced stations), mirroringfrom_windio()’sn_nodes.Nonekeeps each component’s native WindIO grid.
- Return type:
Notes
Requires the optional
[windio]extra (PyYAML). This is the rigid fixed-base monopile path: the pile is clamped at the mudline with no soil flexibility, matchingfrom_elastodyn_with_subdyn()and the bundled monopile samples. Distributed soil springs (a Winklerdistr_k/hub_conn = 3foundation) and Morison hydrodynamics are out of scope here and tracked separately. RaisesValueErrorif the monopile top and tower base do not meet at a common transition-piece elevation.
- run(n_modes=20, *, check_model=True, on_error='raise')[source]
Solve the eigenvalue problem and return frequencies + mode shapes.
- Parameters:
n_modes (number of modes to extract (must be >= 1; default 20).)
check_model (run
pybmodes.checks.check_model()before the) – solve (defaultTrue). INFO findings are silent (callpybmodes.checks.check_model(model)explicitly to see those). Passcheck_model=Falseto skip the pre-solve checks for scripted callers that have already validated their inputs.on_error (how ERROR-severity findings are handled when) –
check_modelruns (default"raise", 1.14.0). ERROR findings flag non-physical input (NaN section properties, non-positive mass, a malformed support matrix), so the solve fails closed by raisingpybmodes.checks.ModelValidationErrorrather than feeding the eigensolver garbage. Passon_error="warn"to downgrade ERROR findings toUserWarningand continue, the pre-1.14.0 behaviour. WARN findings always emit asUserWarningregardless.
- Return type:
Warning
n_modesaffects the LAPACK solver path. For symmetric or nearly-symmetric towers (EI_FA ≈ EI_SSand small RNA c.m. offset), usen_modes >= 6. Withn_modes <= 4,scipy.linalg.eighinvokes a subset eigenvalue routine that can artificially lift the degeneracy of the 1st FA / SS bending pair — the modes come back at slightly different frequencies and pre-separated, which prevents the degenerate-pair resolver inpybmodes.elastodyn.paramsfrom triggering. The polynomial fits still succeed, but the FA / SS classification may flip relative to a full solve, and downstreamcompute_tower_params_reportmay select different modes forTwFAM1Sh/TwSSM1Shbetween runs at differentn_modes.Minimum recommended:
n_modes >= 6for reliable FA / SS classification on symmetric structures. The default of 20 is safely above this threshold.
ModalResult dataclass returned by RotatingBlade and Tower.
- class ModalResult(frequencies, shapes, participation=None, fit_residuals=None, metadata=None, mode_labels=None, ignored_physics=(), diagnostics=None)[source]
Bases:
objectFrequencies and mode shapes from a single FEM solve.
- Variables:
frequencies ((n_modes,) array of natural frequencies in Hz.)
shapes (list of
NodeModeShape, one per mode, ordered root) – to tip.participation ((n_modes, 3) array of energy fractions in the) – per-mode (flap or FA, edge or SS, torsion) axes — populated by downstream classification code such as
pybmodes.campbell._participation(). Each row sums to 1.Nonewhen not yet computed; included in the saved archive only when set.fit_residuals (optional
{block_name: rms_value}dict of) – polynomial-fit RMS residuals — populated bypybmodes.elastodyn.compute_tower_params()/compute_blade_paramscallers that want to embed the fit quality in the serialised result.Nonewhen not set.metadata (optional metadata dict (pyBmodes version, source file,) – save timestamp, git hash) attached automatically by
save()/to_json()if not already populated.mode_labels (optional per-mode classification labels, one entry) – per mode (parallel to
shapes/frequencies). For a floating model (hub_conn = 2with aPlatformSupport) the platform rigid-body modes are named"surge"/"sway"/"heave"/"roll"/"pitch"/"yaw"; a mode the classifier can’t confidently attribute to a single platform DOF (a flexible tower mode, or a strongly coupled / rotated pair) is left asNone. The whole list isNonefor a non-floating model (cantilever / monopile have no rigid-body modes to name). Added 1.3.0 — the last dataclass field, so the pre-1.3.0 positional constructor ABI is preserved (see the field-order note below); included in the saved archive only when set, likeparticipation/fit_residuals.ignored_physics (tuple of short labels naming any parsed-but-not-) – modelled physics the solve silently dropped, e.g.
"distributed added mass (distr_m)". Empty when the model was solved at full fidelity. Lets a downstream consumer of a saved result see that it is an approximation rather than guess. Added 1.14.0; persisted when non-empty.diagnostics (optional
SolverDiagnostics) – for the solve that produced this result (path taken, sparse-to- dense fallback, mode-count guarantee, per-mode residuals, mass-matrix conditioning). Attached byrun_fem(); not serialised (it describes the live solve run, not the persistent result data) and excluded from equality. Added 1.14.0.
- Parameters:
- frequencies: np.ndarray
- classmethod from_json(path)[source]
Read a result back from a JSON file saved by
to_json().- Parameters:
- Return type:
- classmethod load(path, *, allow_legacy_pickle=False)[source]
Read a result back from a
.npzarchive saved bysave(). The reconstructed instance is value-equal to the original modulo numpy dtype promotion.Archives written by current pyBmodes are pickle-free and load under
allow_pickle=False. A legacy pre-1.0 archive whose__meta__is a pickled object array is refused by default (object-array unpickling can execute arbitrary code); passallow_legacy_pickle=Trueto opt in for a file you trust.- Parameters:
- Return type:
Input / output
Parse errors (common across every pybmodes.io.* reader)
Unified parse-error base class for every pybmodes.io.* reader.
Every input-format parser pybmodes ships (BModes .bmi /
section-properties .dat / OpenFAST ElastoDyn / SubDyn / HydroDyn /
MoorDyn .dat / WAMIT .1 .hst / BModes .out reference
output / WISDEM / WindIO ontology .yaml) previously raised either a
bare ValueError with a format-specific message or, in the
case of pybmodes.io.out_parser, the legacy
BModeOutParseError. That made downstream
try / except ValueError callers correct but indiscriminate, and
the per-error file / line context was unstructured prose.
ParseError is the unified base. It inherits ValueError
so existing except ValueError callers continue to catch every
parse error untouched — the inheritance addition is non-breaking.
Each parser will (incrementally) start raising ParseError
subclasses with structured file / line / column /
context fields. Callers that want file / line context can switch
to except ParseError and read the typed fields; callers that
only need “the file is broken” stay on except ValueError.
Examples
Catch a parse error and pull out the file / line / column context:
from pybmodes.io.errors import ParseError
from pybmodes.io.bmi import read_bmi
try:
bmi = read_bmi("malformed.bmi")
except ParseError as err:
print(f"parse failed in {err.file}:{err.line}: {err}")
print(err.format_diagnostic()) # one-line diagnostic string
Keep an older except ValueError pattern working — ParseError
is a subclass so the catch still triggers:
try:
bmi = read_bmi("malformed.bmi")
except ValueError as err:
print(f"oops: {err}") # also catches every ParseError
- exception BMIParseError(message, file=None, line=None, column=None, context=None)[source]
Bases:
ParseErrorRaised by
pybmodes.io.bmi.read_bmi()and the companion section-properties parser when the input deck is malformed.The parser is line-oriented;
lineis the 1-based row in the source file.contextis the offending line (truncated).
- exception ElastoDynParseError(message, file=None, line=None, column=None, context=None)[source]
Bases:
ParseErrorRaised by the ElastoDyn deck reader (
pybmodes.io.elastodyn_reader/ the privatepybmodes.io._elastodynsub-package) on malformed input.
- exception MoorDynParseError(message, file=None, line=None, column=None, context=None)[source]
Bases:
ParseErrorRaised by
pybmodes.mooring.MooringSystem.from_moordyn()when a MoorDyn.datcarries an unrecognised layout or a point-ID column ordering the parser can’t auto-detect.
- exception ParseError(message, file=None, line=None, column=None, context=None)[source]
Bases:
ValueErrorBase class for every
pybmodes.io.*parser exception.Inherits
ValueErrorsoexcept ValueErrorcatches it unchanged. Inheriting parsers add structured context fields below.- Variables:
message (str) – The human-readable description of the failure. Same as
str(err).file (pathlib.Path-compatible str | None) – The source file the parser was reading from, when known.
Nonefor in-memory parses (e.g. yaml from a string).line (int | None) – 1-based line number where the error was detected, when known. The parser is encouraged to populate this even for token-level errors — fall back to the start of the containing block.
column (int | None) – 1-based column number where the error was detected, when known. Optional; many parsers don’t track column.
context (str | None) – A short snippet — typically the offending line or token — that gives the reader visual confirmation of what the parser tripped on. Up to one or two lines; the parser should truncate large blobs.
- Parameters:
- Return type:
None
- exception SubDynParseError(message, file=None, line=None, column=None, context=None)[source]
Bases:
ParseErrorRaised by
pybmodes.io.subdyn_readerwhen the SubDyn joints / members / reaction-joint block can’t be parsed.
- exception WAMITParseError(message, file=None, line=None, column=None, context=None)[source]
Bases:
ParseErrorRaised by
pybmodes.io.wamit_reader.HydroDynReaderand the underlying.1/.hstreaders on malformed WAMIT output (bad re-dimensionalisation, missing files behindPotFile, asymmetric matrices that can’t be mirrored).
- exception WindIOParseError(message, file=None, line=None, column=None, context=None)[source]
Bases:
ParseErrorRaised by the WindIO ontology readers (
pybmodes.io.windio/pybmodes.io.windio_blade/pybmodes.io.windio_floating) on schema-drift or malformed published blocks.Includes both
components.<component>lookup failures and the “elastic_properties block is present but unparseable” path thatelastic="auto"/"file"distinguish.
BModes .bmi
Parser for .bmi main input files.
The .bmi format is line-ordered: values precede their labels. The reader follows the format conventions:
ReadCom -> consume one line verbatim (section headers / blank lines)
ReadStr -> consume one line, return it as a string
ReadVar -> skip blanks, return first whitespace token of next non-blank line
ReadAry -> skip blanks, return first N tokens of next non-blank line
- class BMIFile(title, echo, beam_type, rot_rpm, rpm_mult, radius, hub_rad, precone, bl_thp, hub_conn, n_modes_print, tab_delim, mid_node_tw, tip_mass, id_mat, sec_props_file, scaling, n_elements, el_loc, tow_support=0, support=None, source_file=None)[source]
Bases:
objectAll parameters parsed from a .bmi main input file.
See Coordinate systems, origins & datums for the coordinate system, origin (tower base) and boundary-condition definitions.
Key fields
- beam_typeint
1= blade (rotating),2= tower.- radiusfloat
Span length of the finite-element beam, metres — the blade
radiusor the flexible tower length (TowerHt - TowerBsHt). The beam runs from base (z = 0) to this length; the absolute base elevation is conveyed separately (via thePlatformSupport.draftfor a floater).- hub_radfloat
Hub radius (blade root radial offset from the rotation axis), m.
- hub_connint
Base boundary condition:
1cantilever (clamped),2free-free floating (reactions fromsupport),3soft monopile (lateral + rocking free),4pinned-free (bending slopes free). See thehub_conntable in Coordinate systems, origins & datums.- rot_rpm, preconefloat
Rotor speed (rpm) and coning angle (deg) for a rotating blade.
- tip_massTipMassProps
Tower-top RNA / blade-tip lumped mass.
- tow_supportint
0none / wires-only,1inline platform block, normalised to aPlatformSupportby the parser.- supportTensionWireSupport | PlatformSupport | None
The offshore platform or guy-wire support, when present.
- scaling: ScalingFactors
- support: TensionWireSupport | PlatformSupport | None = None
- tip_mass: TipMassProps
- Parameters:
title (str)
echo (bool)
beam_type (int)
rot_rpm (float)
rpm_mult (float)
radius (float)
hub_rad (float)
precone (float)
bl_thp (float)
hub_conn (int)
n_modes_print (int)
tab_delim (bool)
mid_node_tw (bool)
tip_mass (TipMassProps)
id_mat (int)
sec_props_file (str)
scaling (ScalingFactors)
n_elements (int)
el_loc (ndarray)
tow_support (int)
support (TensionWireSupport | PlatformSupport | None)
source_file (Path | None)
- class PlatformSupport(draft, cm_pform, mass_pform, i_matrix, ref_msl, hydro_M, hydro_K, mooring_K, distr_m_z, distr_m, distr_k_z, distr_k, wires=None, cm_pform_x=0.0, cm_pform_y=0.0, ref_x=0.0, ref_y=0.0)[source]
Bases:
objectOffshore floating-platform or monopile support.
All quantities follow the conventions in Coordinate systems, origins & datums — read that page if anything below is unclear. The single origin is the tower base; the vertical datum is mean sea level (MSL, z = 0); the 6×6 matrices use OpenFAST DOF order (surge, sway, heave, roll, pitch, yaw — see
pybmodes.coords).- Variables:
draft (float) –
Signed elevation of the tower base relative to MSL, in metres, negative = above MSL (a tower base 15 m above the waterline is
draft = -15).Note
Despite the name, this is not the naval-architecture draft (the keel depth below the waterline). The field name is inherited verbatim from the BModes
.bmiplatform block; here it means “tower-base z relative to MSL, negative up”. pyBmodes forms the CM→tower-base vertical lever internally ascm_pform - draft, so the base elevation enters the maths exactly once — do not also add it tocm_pform/ref_msl.cm_pform (float) – Platform centre-of-mass depth below MSL, metres, positive downward. The vertical reference of
i_matrix.mass_pform (float) – Platform (substructure) mass, kg.
i_matrix (np.ndarray) – Platform 6×6 rigid-body inertia (kg, kg·m²) about the platform CM, in OpenFAST DOF order. Transferred to the tower base by the rigid-arm transform using
cm_pform(vertical) andcm_pform_x/cm_pform_y(horizontal).ref_msl (float) – Depth below MSL of the reference point for the hydro and mooring matrices, metres, positive downward (the WAMIT/HydroDyn
PtfmRefzt; usually 0, i.e. at the waterline). This reference is assumed to lie on the tower axis (PtfmRefxt = PtfmRefyt = 0);hydro_*/mooring_Kreceive no horizontal arm (seecm_pform_x).hydro_M (np.ndarray) – Infinite-frequency hydrodynamic added mass
A_inf, 6×6, about theref_mslreference, OpenFAST DOF order.hydro_K (np.ndarray) – Hydrostatic restoring
C_hst, 6×6, about theref_mslreference. May be (legitimately) negative-definite in roll/pitch for an unstable spar stabilised by mooring.mooring_K (np.ndarray) – Linearised mooring stiffness, 6×6, about the
ref_mslreference, OpenFAST DOF order.distr_m (distr_m_z,) – Optional distributed added mass vs depth (Morison), if used.
distr_k (distr_k_z,) – Optional distributed Winkler soil/foundation stiffness vs depth (the
hub_conn = 3soft-monopile path).wires (TensionWireSupport | None) – Optional guy-wire support.
cm_pform_y (cm_pform_x,) – Horizontal offset of the platform CM from the tower axis (applied to the inertia transform; see the inline note below).
ref_y (ref_x,) – Horizontal position of the hydro/mooring reference point (
PtfmRefxt/PtfmRefyt) from the tower axis, metres. Default 0.0 (reference on the tower axis — every standard deck). Set non-zero for an off-axis floater sohydro_*/mooring_Kare carried horizontally to the tower base too (issue #100, 1.13.0).tower_base_z (float (property)) – Intuitive positive-up alias for
draft(tower_base_z == -draft).
- Parameters:
draft (float)
cm_pform (float)
mass_pform (float)
i_matrix (ndarray)
ref_msl (float)
hydro_M (ndarray)
hydro_K (ndarray)
mooring_K (ndarray)
distr_m_z (ndarray)
distr_m (ndarray)
distr_k_z (ndarray)
distr_k (ndarray)
wires (TensionWireSupport | None)
cm_pform_x (float)
cm_pform_y (float)
ref_x (float)
ref_y (float)
- property tower_base_z: float
Elevation of the tower base above MSL, metres, positive up.
Intuitive alias for
draft, which is the BModes-inherited signed, negative-up spelling:tower_base_z == -draft. A tower base 15 m above the waterline istower_base_z = 15(equivalentlydraft = -15). Reading or assigning one keeps the other in sync. Added 1.13.0 (issue #100).
- wires: TensionWireSupport | None = None
- class ScalingFactors(sec_mass=1.0, flp_iner=1.0, lag_iner=1.0, flp_stff=1.0, edge_stff=1.0, tor_stff=1.0, axial_stff=1.0, cg_offst=1.0, sc_offst=1.0, tc_offst=1.0)[source]
Bases:
objectMultiplicative scaling factors applied to all section properties.
- Parameters:
- class TensionWireSupport(n_attachments, n_wires, node_attach, wire_stiffness, th_wire)[source]
Bases:
objectTension wire (guy wire) support for land-based towers.
- Parameters:
- class TipMassProps(mass, cm_offset, cm_axial, ixx, iyy, izz, ixy, izx, iyz)[source]
Bases:
objectMass and inertia of the blade tip or tower-top concentrated mass.
Lumps the rotor-nacelle assembly (RNA) at the tower top, or a tip mass at the blade tip. Offsets are in the span-end section frame:
zalong the span (positive toward / beyond the top), the transverse axis aligned with the lateral/sway direction. See Coordinate systems, origins & datums.- Variables:
mass (float) – Concentrated mass, kg. Omitting the RNA (mass = 0) on a tower makes the 1st fore-aft frequency ~10–30 % too high.
cm_offset (float) – CM offset transverse to the span (along the tip-section axis aligned with lateral/sway), metres.
cm_axial (float) – CM offset along the span (z), metres — how far the lumped CM sits beyond the span end (e.g. the RNA CM above the tower top).
iyz (ixx, iyy, izz, ixy, izx,) – Mass moments / products of inertia about the CM, kg·m².
- Parameters:
ElastoDyn deck reader
Parser, writer, and pyBmodes adapter for OpenFAST ElastoDyn .dat
files.
ElastoDyn .dat files come in three flavours, all of which share a
common line-ordered convention: each significant line carries a value
(or short list) followed by a label keyword and an optional -
comment tail. The parsers below are label-based rather than
position-based, which keeps them robust across the FAST v8 → OpenFAST
v3+ format drift (renamed/added/removed scalars between versions;
BldFile(1) vs BldFile1).
Three entry points cover the three file flavours:
read_elastodyn_main()— the top-level ElastoDyn input file.read_elastodyn_tower()— the tower-properties file referenced viaTwrFile.read_elastodyn_blade()— the blade-properties file referenced viaBldFile(1..3).
Each returns a dataclass holding the parsed values plus enough raw-
line metadata to re-emit a semantically identical file via the
matching write_* function.
Two adapter helpers turn a parsed ElastoDyn bundle into pyBmodes
BMIFile + SectionProperties records ready for the FEM core:
to_pybmodes_tower(main, tower, blade=None)()to_pybmodes_blade(main, blade)()
The implementation lives in the private sub-package
pybmodes.io._elastodyn:
types— dataclasseslex— line/token scanning helpersparser— line-driven flavour parserswriter— canonical re-emittersadapter— pyBmodes BMI/SectionProperties synthesisers
This module re-exports the public names from those sub-modules so
existing from pybmodes.io.elastodyn_reader import … imports keep
working unchanged. Field-set discrepancies vs. the user-specified
spec are documented under each dataclass.
Round-trip contract
write_* functions emit a canonically formatted file that parses
back to an equal dataclass but is not byte-identical to the
original. Whitespace, label column position, and comment text are
normalised. The test suite compares the parse-emit-reparse fixed
point (see tests/test_elastodyn_reader.py).
- class ElastoDynBlade(header, title, source_file=None, n_bl_inp_st=0, bld_fl_dmp=<factory>, bld_ed_dmp=<factory>, fl_st_tunr=<factory>, adj_bl_ms=1.0, adj_fl_st=1.0, adj_ed_st=1.0, bl_fract=<factory>, pitch_axis=None, strc_twst=<factory>, b_mass_den=<factory>, flp_stff=<factory>, edg_stff=<factory>, rotary_inertia_available=False, bld_fl1_sh=<factory>, bld_fl2_sh=<factory>, bld_edg_sh=<factory>, distr_header_lines=<factory>, section_dividers=<factory>)[source]
Bases:
objectParsed ElastoDyn blade input file.
ElastoDyn ships only translational mass density (
BMassDen) and bending stiffnesses (FlpStff,EdgStff) per spanwise station; it has no per-section rotary mass moments of inertia. Those live in BeamDyn or come from a cross-section pre-processor (VABS, PreComp).The
rotary_inertia_availableflag is therefore alwaysFalseafter parsing an ElastoDyn blade file. Downstream code (to_pybmodes_blade) treats the rotary inertia contributions as zero, which is the correct Euler-Bernoulli limit for slender blades and is sufficient for the bending modes (1–4 flap/edge) pyBmodes targets. A tiny regularisation floor is added in the section-property builder to keep the global mass matrix positive-definite without fabricating physically meaningful rotary terms.- Parameters:
header (str)
title (str)
source_file (Path | None)
n_bl_inp_st (int)
adj_bl_ms (float)
adj_fl_st (float)
adj_ed_st (float)
bl_fract (ndarray)
pitch_axis (ndarray | None)
strc_twst (ndarray)
b_mass_den (ndarray)
flp_stff (ndarray)
edg_stff (ndarray)
rotary_inertia_available (bool)
bld_fl1_sh (ndarray)
bld_fl2_sh (ndarray)
bld_edg_sh (ndarray)
- class ElastoDynMain(header, title, source_file=None, num_bl=3, tip_rad=0.0, hub_rad=0.0, pre_cone=<factory>, hub_cm=0.0, overhang=0.0, shft_tilt=0.0, twr2shft=0.0, tower_ht=0.0, tower_bs_ht=0.0, nac_cm_xn=0.0, nac_cm_yn=0.0, nac_cm_zn=0.0, rot_speed_rpm=0.0, tip_mass=<factory>, hub_mass=0.0, hub_iner=0.0, gen_iner=0.0, nac_mass=0.0, nac_y_iner=0.0, yaw_br_mass=0.0, bld_file=<factory>, twr_file='', scalars=<factory>, out_list=<factory>, nodal_out_list=<factory>, section_dividers=<factory>)[source]
Bases:
objectParsed top-level ElastoDyn input file.
- Parameters:
header (str)
title (str)
source_file (Path | None)
num_bl (int)
tip_rad (float)
hub_rad (float)
hub_cm (float)
overhang (float)
shft_tilt (float)
twr2shft (float)
tower_ht (float)
tower_bs_ht (float)
nac_cm_xn (float)
nac_cm_yn (float)
nac_cm_zn (float)
rot_speed_rpm (float)
hub_mass (float)
hub_iner (float)
gen_iner (float)
nac_mass (float)
nac_y_iner (float)
yaw_br_mass (float)
twr_file (str)
- compute_rot_mass(blade)[source]
Total rotor mass = hub + N · AdjBlMs · ∫ BMassDen ds along the blade.
Requires the blade file to integrate the distributed mass density. The
AdjBlMsscalar from the blade file is an ElastoDyn-side multiplier on the entire mass distribution (the “blade-mass adjustment factor”); ignoring it under- / over-reports rotor mass on any deck where it deviates from 1.0. The adapter atto_pybmodes_blade()already applies it; this method previously did not.- Parameters:
blade (ElastoDynBlade)
- Return type:
- class ElastoDynTower(header, title, source_file=None, n_tw_inp_st=0, twr_fa_dmp=<factory>, twr_ss_dmp=<factory>, fa_st_tunr=<factory>, ss_st_tunr=<factory>, adj_tw_ma=1.0, adj_fa_st=1.0, adj_ss_st=1.0, ht_fract=<factory>, t_mass_den=<factory>, tw_fa_stif=<factory>, tw_ss_stif=<factory>, tw_fa_iner=<factory>, tw_ss_iner=<factory>, tw_fa_cg_of=<factory>, tw_ss_cg_of=<factory>, tw_fa_m1_sh=<factory>, tw_fa_m2_sh=<factory>, tw_ss_m1_sh=<factory>, tw_ss_m2_sh=<factory>, distr_header_lines=<factory>, section_dividers=<factory>)[source]
Bases:
objectParsed ElastoDyn tower input file.
- Parameters:
header (str)
title (str)
source_file (Path | None)
n_tw_inp_st (int)
adj_tw_ma (float)
adj_fa_st (float)
adj_ss_st (float)
ht_fract (ndarray)
t_mass_den (ndarray)
tw_fa_stif (ndarray)
tw_ss_stif (ndarray)
tw_fa_iner (ndarray)
tw_ss_iner (ndarray)
tw_fa_cg_of (ndarray)
tw_ss_cg_of (ndarray)
tw_fa_m1_sh (ndarray)
tw_fa_m2_sh (ndarray)
tw_ss_m1_sh (ndarray)
tw_ss_m2_sh (ndarray)
- to_pybmodes_blade(main, blade)[source]
Build pyBmodes
BMIFileandSectionPropertiesfor blade modal analysis at the operatingRotSpeedfrom the main file.- Parameters:
main (ElastoDynMain)
blade (ElastoDynBlade)
- Return type:
- to_pybmodes_tower(main, tower, blade=None, *, physical_sec_props=False)[source]
Build pyBmodes
BMIFileandSectionPropertiesfor tower modal analysis from a parsed ElastoDyn bundle.bladeis optional; when omitted, the rotor mass is approximated asHubMassonly.physical_sec_propsselects the section-property synthesis (see_stack_tower_section_props()):False(default) for the clamped-base cantilever / monopile path;Truefor the free-base floating path, where the cantilever proxies wreck conditioning.- Parameters:
main (ElastoDynMain)
tower (ElastoDynTower)
blade (ElastoDynBlade | None)
physical_sec_props (bool)
- Return type:
- write_elastodyn_blade(obj, path=None)[source]
- Parameters:
obj (ElastoDynBlade)
- Return type:
- write_elastodyn_main(obj, path=None)[source]
Re-emit a main ElastoDyn file. Returns the text and optionally writes it to
path.- Parameters:
obj (ElastoDynMain)
- Return type:
SubDyn reader
Minimal parser and adapter for OpenFAST SubDyn .dat substructure files.
Scope is intentionally narrow — only what’s needed to drive a clamped-base monopile model from the existing reference decks shipped with the OpenFAST regression-test corpus. Specifically:
JOINTS — XYZ positions and joint type.
BASE REACTION JOINTS — read the single clamped-base joint id and its DOF flags; SSI files are not handled (the 5MW OC3 deck has none).
INTERFACE JOINTS — read the transition-piece joint id.
MEMBERS — beam connectivity (joint pairs and property-set ids).
CIRCULAR BEAM CROSS-SECTION PROPERTIES —
E,G,MatDens, outer diameter, wall thickness.
Sections we don’t parse: rectangular / arbitrary cross-sections, cables,
rigid links, spring elements, cosine matrices, concentrated masses, output
settings. These are zero in the bundled OC3 monopile and SubDyn raises an
explicit error if asked to read tables we don’t know about, so silently
skipping them is fine; we just consume their rows until the next section
divider. Extending the parser to cover one of those sections is a localised
change — drop another _consume_table call into _parse() and add a
typed list field on SubDynFile.
The companion to_pybmodes_pile_tower() adapter takes the parsed
SubDyn together with the ElastoDyn main + tower files and synthesises a
single combined-cantilever BMIFile + SectionProperties for
pyBmodes’ tower modal solver — a “rigid base + flexible pile + flexible
tower” model with no soil flexibility (matching the OC3 reference design,
which clamps the pile rigidly at the seabed).
- class SubDynCircProp(prop_set_id, E, G, rho, D, t)[source]
Bases:
objectCircular beam cross-section property set.
- class SubDynFile(header='', title='', source_file=None, joints=<factory>, members=<factory>, circ_props=<factory>, reaction_joint_id=0, interface_joint_id=0)[source]
Bases:
objectAll parameters parsed from a SubDyn
.datfile.- Parameters:
header (str)
title (str)
source_file (Path | None)
joints (list[SubDynJoint])
members (list[SubDynMember])
circ_props (list[SubDynCircProp])
reaction_joint_id (int)
interface_joint_id (int)
- circ_props: list[SubDynCircProp]
- joints: list[SubDynJoint]
- members: list[SubDynMember]
- class SubDynJoint(joint_id, x, y, z, joint_type=1)[source]
Bases:
objectOne node in the substructure graph (SS-coordinate system, metres).
- class SubDynMember(member_id, joint_a, joint_b, prop_set_a, prop_set_b, member_type='1c')[source]
Bases:
objectOne beam member connecting two joints.
- Parameters:
- read_subdyn(path)[source]
Parse a SubDyn
.datfile. See module docstring for the supported subset of sections.- Parameters:
- Return type:
- to_pybmodes_pile_tower(main, tower, subdyn, blade=None)[source]
Build a combined pile + tower BMI + SectionProperties for OC3-style rigid-base monopiles.
Layout (axial coordinate
zincreases upward):z = z_seabed --- rigid clamped base (SubDyn reaction joint) ... pile section, properties from SubDyn members z = z_TP --- transition piece (SubDyn interface joint / ElastoDyn ``TowerBsHt``) ... tower section, properties from ElastoDyn tower z = z_top --- tower top, lumped RNA tip massThe returned
BMIFiledescribes the full beam fromz_seabedtoz_topas a single cantilever; theSectionPropertiesarray splices the pile and tower property tables at the transition piece with two stations very close together so the FE interpolant captures the cross-section discontinuity correctly.No soil flexibility, no hydrodynamic added mass — the OC3 design fixes the pile rigidly at the seabed and the user-selected scope here excludes hydro coupling. See the case-study script in
cases/nrel5mw_monopile/run.pyfor context.- Parameters:
subdyn (SubDynFile)
WAMIT / HydroDyn reader
WAMIT v7 output file reader for OpenFAST / HydroDyn floating-platform decks.
Parses .1 (added-mass + radiation-damping) and .hst (hydrostatic
restoring) files written by WAMIT, redimensionalises them per the WAMIT v7
convention (ρ·L^k and ρ·g·L^k factors), and returns the 6 × 6 SI
matrices a pyBmodes PlatformSupport block consumes.
The reader is read-only and pure-Python; pairs with a thin
HydroDynReader that picks WAMITULEN and the PotFile root
from an OpenFAST HydroDyn .dat so callers can chain
HydroDynReader(...).read_platform_matrices() in a single step.
WAMIT file formats
.1 (added mass / radiation damping) — each line carries:
period i j A(i,j) [B(i,j)]
period = -1.0rows are the infinite-frequency added massA_inf(only theAcolumn is present; no damping at infinite frequency).period = 0.0rows are the zero-frequency added massA_0(alsoAonly).period > 0rows are frequency-dependentA(ω) + B(ω); this reader currently extracts onlyA_infandA_0.
Indices i, j are 1-indexed over the six rigid-body DOFs
{1: surge, 2: sway, 3: heave, 4: roll, 5: pitch, 6: yaw}. The file is
sparse — only non-trivial entries are listed; missing entries are zero.
.hst (hydrostatic restoring) — each line carries:
i j C(i,j)
Same 1-indexed convention. Some WAMIT runs write only the upper triangle; others write the full 6 × 6 including explicit zeros.
WAMIT v7 nondimensionalisation
All WAMIT outputs are dimensionless. Redimensionalisation factors depend on the DOF-pair type:
Added mass (
.1):trans-trans → ρ·L³,trans-rot → ρ·L⁴,rot-rot → ρ·L⁵.Hydrostatic stiffness (
.hst):trans-trans → ρ·g·L²,trans-rot → ρ·g·L³,rot-rot → ρ·g·L⁴.
Here L = WAMITULEN (declared in the HydroDyn .dat), ρ is the
sea-water density, and g is gravitational acceleration. The exponent
table is captured in WamitReader._dim_factor() as
base + n_rot_dofs where base ∈ {3, 2} and n_rot_dofs ∈ {0, 1, 2}.
- class HydroDynReader(dat_path)[source]
Bases:
objectMinimal reader for the floating-platform section of a HydroDyn
.dat.Surfaces
WAMITULEN,PotMod,PotFile, andPtfmRefzt— the four values needed to driveWamitReader.WtrDensandGravityare NOT in the modern HydroDyn.dat(HydroDyn ≥ v2.03 delegates those to the paired SeaState input file), so the corresponding properties fall back to standard sea-water defaults.The parser is deliberately loose: it scans each non-blank line for the pattern
<value> <label> ...and stores the first occurrence of each label. Matrix continuation rows (e.g. the five extra rows ofAddCLin) and wrapped comment text get parsed too but produce harmless_values[<numeric_string>] = ...entries that no caller asks for.- Parameters:
dat_path (str | pathlib.Path)
- property gravity: float
Gravitational acceleration (
m/s²).OpenFAST stores
Gravityat the top-level.fstfile, not in HydroDyn. We fall back to ISO standard gravity (9.80665) so the reader is self-contained.
- read_platform_matrices()[source]
Resolve
PotFilealongside this deck and read its WAMIT outputs.Raises
ValueErrorifPotMod == 0(no WAMIT data attached).- Return type:
- class WamitData(A_inf, A_0, C_hst, rho, g, ulen, pot_file_root)[source]
Bases:
objectDimensionalised WAMIT output for a single floating body.
All matrices are in SI units. Added-mass entries are
kgfor trans-trans,kg·mfor trans-rot, andkg·m²for rot-rot. Hydrostatic-stiffness entries areN/mfor trans-trans,N(orN·m/m, same thing) for trans-rot, andN·m/radfor rot-rot.- Variables:
A_inf (ndarray, shape (6, 6)) – Infinite-frequency added mass (
period = -1rows of.1).A_0 (ndarray, shape (6, 6)) – Zero-frequency added mass (
period = 0rows of.1).C_hst (ndarray, shape (6, 6)) – Hydrostatic restoring matrix (all rows of
.hst).ulen (rho, g,) – Re-dimensionalisation constants applied; stored so downstream code can audit the SI conversion that was used.
pot_file_root (pathlib.Path) – Absolute path of the resolved
PotFile(no extension).
- Parameters:
- class WamitReader(pot_file_root, dat_dir, rho=1025.0, g=9.80665, ulen=1.0)[source]
Bases:
objectRead a WAMIT v7
.1/.hstpair and return SI 6 × 6 matrices.The constructor only normalises the inputs; on-disk reads happen inside
read()so callers can catchFileNotFoundErrorat a single well-defined point.- Parameters:
pot_file_root (str or Path) –
PotFilevalue taken verbatim from the HydroDyn.dat. May carry surrounding double or single quotes, may use backslashes on Windows, and may be relative (resolved againstdat_dir) or absolute.dat_dir (pathlib.Path) – Directory of the HydroDyn
.datwhosePotFileis being followed. Used to resolve relativePotFilevalues.rho (float) – Sea-water density (
kg/m³), gravitational acceleration (m/s²), and WAMIT reference length (WAMITULEN, m). Default to standard sea-water values so simple smoke tests don’t need an explicitHydroDynReader.g (float) – Sea-water density (
kg/m³), gravitational acceleration (m/s²), and WAMIT reference length (WAMITULEN, m). Default to standard sea-water values so simple smoke tests don’t need an explicitHydroDynReader.ulen (float) – Sea-water density (
kg/m³), gravitational acceleration (m/s²), and WAMIT reference length (WAMITULEN, m). Default to standard sea-water values so simple smoke tests don’t need an explicitHydroDynReader.
- read()[source]
Parse the
.1and.hstfiles alongside the resolved root.- Raises:
FileNotFoundError – If either
<root>.1or<root>.hstis missing; the message names the expected absolute path and the verbatimPotFilevalue that produced it.- Return type:
BModes .out output
Parser for modal analysis output files (.out).
- Output format:
Header / title
“rotating blade frequencies & mode shapes” OR “tower frequencies & mode shapes”
- For each mode:
——– Mode No. N (freq = X.XXXE+XX Hz) <column-header line> <blank line> <data rows: span_loc + 5 columns> <blank line>
- Column order:
Blade : span_loc flap_disp flap_slope lag_disp lag_slope twist Tower : span_loc ss_disp ss_slope fa_disp fa_slope twist
- exception BModeOutParseError(message, file=None, line=None, column=None, context=None)[source]
Bases:
ParseErrorRaised by
read_out()understrict=Truewhen a.outfile is not cleanly parseable.The default (
strict=False) parser is intentionally tolerant — it skips malformed rows so a partially-corrupt file still yields whatever modes it can. Validation / cross-solver-comparison workflows that must trust every value should passstrict=True; the message then carries the source file, the 1-based line number, and the mode context so the offending location is unambiguous.Inherits
pybmodes.io.errors.ParseError(which in turn inheritsValueError), so existingexcept ValueErrorcallers still catch this exception unchanged — the inheritance addition is backward-compatible. New callers canexcept ParseErrorto get a typed handler that works across everypybmodes.io.*parser.
- class BModeOutput(title, beam_type, modes, source_file=None)[source]
Bases:
objectAll mode shapes parsed from a .out file.
- class ModeShape(mode_number, frequency, span_loc, col1, col2, col3, col4, twist, col_names=<factory>)[source]
Bases:
objectMode shape data for a single mode.
- Parameters:
- read_out(path, *, strict=False)[source]
Parse a .out file and return a
BModeOutput.With
strict=False(default, unchanged behaviour) malformed data rows are silently skipped so a partially-corrupt file still yields whatever modes it can. Withstrict=Truethe parser raisesBModeOutParseError— with the source file, the 1-based line number, and the mode context — on a short data row, an unparseable number, a non-finite (NaN/inf) value, a duplicate mode number, or a file that yields no modes at all. Usestrict=Truefor validation / cross-solver comparison.- Parameters:
- Return type:
WindIO ontology — tubular tower / monopile
Read the structural subset of a WindIO ontology .yaml for a
tubular tower or monopile (issue #35).
WindIO describes a tower / monopile as a circular tube via:
components.<component>.outer_shape.outer_diameter.{grid, values}components.<component>.structure.layers[]— each{material, thickness.{grid, values}}(summed for the wall)components.<component>.structure.outfitting_factor— the non-structural mass multiplier (internals / flanges / paint)components.<component>.reference_axis.z.{grid, values}— physical station heights (m); the span =|z[-1] - z[0]|top-level
materials[]— the layer’s material{E, rho, nu}
That is exactly what pybmodes.io.geometry.tubular_section_props()
needs, so pybmodes.models.Tower.from_windio() is a thin wrapper.
This module is the tubular (tower / monopile) reader only. A WindIO
blade is a composite layup whose beam properties need a PreComp-class
thin-wall cross-section reduction — that lives in
pybmodes.io.windio_blade (
read_windio_blade() / pybmodes.models.RotatingBlade.from_windio()),
not here.
Requires the optional [windio] extra (PyYAML); the runtime core
stays numpy + scipy only, mirroring the [plots] /
[notebook] extras.
- class WindIOMonopileTower(section_props, combined_length, el_loc, transition_frac, z_base, z_transition, z_top)[source]
Bases:
objectA monopile + tower spliced into a single fixed-bottom cantilever.
The combined cantilever runs from the monopile base (mudline,
z_base) up through the transition piece (z_transition, where the monopile meets the tower) to the tower top (z_top). The section-property table carries both segments’ own wall schedules and materials, joined at the transition with a near-coincident station pair so the FE interpolant captures the cross-section step.- Parameters:
- section_props: SectionProperties
- class WindIOTubular(station_grid, outer_diameter, wall_thickness, flexible_length, E, rho, nu, outfitting_factor, z_base=0.0, z_top=0.0)[source]
Bases:
objectGeometry + material extracted from a WindIO tower / monopile.
- Parameters:
- read_windio_monopile_tower(yaml_path, *, component_tower='tower', component_monopile='monopile', thickness_interp='linear', n_nodes=None)[source]
Reduce the
monopileandtowercomponents and splice them into one fixed-bottom cantilever (issue #92).Each component is reduced independently through the closed-form circular-tube relations (so they keep their own wall schedule and steel grade), then concatenated bottom-to-top at the transition piece — the elevation where the monopile top meets the tower base. The result is the WindIO analog of
pybmodes.io.subdyn_reader.to_pybmodes_pile_tower()(the ElastoDyn + SubDyn splice).- Parameters:
yaml_path (path to a WindIO ontology file carrying both a) –
monopileand atowercomponent.component_tower (component names to splice) – (defaults
"tower"/"monopile").component_monopile (component names to splice) – (defaults
"tower"/"monopile").thickness_interp (
"linear"or"piecewise_constant"— passed) – through to each component’s reduction (seeread_windio_tubular()).n_nodes (optional FE-mesh refinement, applied per segment: each) – of the monopile and tower is re-gridded onto
n_nodesevenly- spaced stations (geometry interpolated, tube properties recomputed exactly), mirroringTower.from_windio()’sn_nodes.Nonekeeps each component’s native WindIO grid.
- Return type:
:raises ValueError : when the monopile top and tower base do not meet at a: common transition-piece elevation (a gap or overlap of more than 1 mm), since a non-contiguous pair cannot be spliced into one beam.
- read_windio_tubular(yaml_path, *, component='tower', thickness_interp='linear')[source]
Parse the structural subset of
componentfrom a WindIO file.Handles both WindIO key dialects (modern
outer_shape/structureand olderouter_shape_bem/internal_structure_2d_fem); see_shape_and_structure().- Parameters:
- Return type:
WindIO ontology — composite blade
Read a WindIO ontology .yaml blade and reduce it to the FEM
section-property table (issue #35).
This is the public glue that ties Phase-2 together, mirroring
pybmodes.io.windio (tower) / pybmodes.io.geometry.
tubular_section_props():
read_windio_blade()— parse the blade component (dialect-robust, reusing the duplicate-anchor-tolerant loader frompybmodes.io.windio): span axis, chord, twist, reference-axis chordwise location, the spanwise airfoil set, the resolved web / layernd_arcbands (pybmodes.io._precomp.arc_resolver), and the material table.windio_blade_section_props()— walk the span, blend the airfoil, build each station’s shell-layer / web stacks, run the thin-wall reduction (pybmodes.io._precomp.reduction), and assemble apybmodes.io.sec_props.SectionPropertiesready forpybmodes.models.RotatingBlade.
Both WindIO key dialects are handled — modern outer_shape /
structure (IEA-15 WT_Ontology, every WISDEM example incl. the
floating ones) and older outer_shape_bem /
internal_structure_2d_fem (IEA-3.4 / 10 / 22). Needs the optional
[windio] extra (PyYAML); the runtime core stays numpy+scipy.
- class WindIOBlade(span_grid, flexible_length, chord, twist_deg, ref_axis_xc, profiles, resolved, materials, elastic=None, elastic_parse_error=None)[source]
Bases:
objectGeometry + layup of a WindIO blade, resolved onto a span grid.
- Parameters:
- elastic: dict | None = None
Pre-computed distributed beam properties parsed straight from the WindIO
elastic_properties/elastic_properties_mbblock (the published reference), interpolated ontospan_grid;Nonewhen the file carries only the layup. Keys:mass_den/flp_iner/edge_iner/flp_stff/edge_stff/tor_stff/axial_stff/cg_offst.
- elastic_parse_error: str | None = None
Non-
Nonewhen a published block was present but could not be parsed (schema drift / malformed).elasticis thenNonetoo, but this distinguishes “absent” (silent PreComp fallback is correct) from “present-but-broken” ("auto"warns;"file"raises) — so a typo can’t hide behind a plausible lower-fidelity result.
- resolved: ResolvedBladeStructure
- read_windio_blade(yaml_path, *, component='blade', n_span=30)[source]
Parse the structural subset of a WindIO blade component.
- Parameters:
- Return type:
- windio_blade_section_props(blade, *, n_perim=300, title='WindIO composite-blade section properties', elastic='auto')[source]
Reduce every span station to the FEM section-property table.
elasticselects the property source (issue #48 — keep deltas to the reference model small):"auto"(default) — use the WindIO published distributed beam properties (elastic_properties/elastic_properties_mb) when the file carries them, so pyBmodes matches the reference model’s stiffness/inertia exactly; fall back to the PreComp thin-wall reduction of the layup only when they are absent."precomp"— always run the PreComp reduction (the pre-1.5 behaviour), even when published properties exist."file"— require the published properties; raiseValueErrorif the file has only the layup or carries a published block that could not be parsed.
If a published block is present but unparseable (schema drift / malformed),
"auto"does not silently fall back to the lower-fidelity PreComp result — it emits aUserWarningnaming the parse problem before reducing the layup, and"file"raises (issue #47 follow-up — static review).- Parameters:
blade (WindIOBlade)
n_perim (int)
title (str)
elastic (str)
- Return type:
WindIO ontology — floating substructure
Read the WindIO components.floating_platform + mooring
blocks (issue #35).
A WindIO floating substructure is a set of named joints (3-D
points, MSL datum, z up) connected by slender circular
members (each a wall layup + optional bulkhead + ballast, plus
Morison Ca/Cd). The downstream physics — hydrostatic
restoring, Morison added mass + buoyancy + rigid-body
inertia, and the catenary mooring stiffness
(pybmodes.mooring) — all build on the geometry parsed here.
This module is only the parser + geometry primitives. Conventions:
cylindrical: truejoints givelocation = [r, theta_deg, z](WISDEM windIO convention) →x = r cos θ,y = r sin θ.axial_joints(named fractions along a member, e.g. a fairlead) are resolved into the joint table somooringcan reference them.the joint flagged
transition: trueis where the tower foots.
Needs the optional [windio] extra (PyYAML); runtime core stays
numpy+scipy. Dialect-robust via the duplicate-anchor-tolerant loader
shared with pybmodes.io.windio.
- class FloatingMember(name, end1, end2, od_grid, od_values, wall_material, wall_t_grid, wall_t_values, ca=1.0, bulkhead_material=None, bulkhead_t=0.0, ballast=<factory>)[source]
Bases:
objectOne circular member: end points (m, MSL datum), the spanwise outer-diameter curve, the wall + bulkhead layup, ballast, and the Morison coefficients.
- Parameters:
- class WindIOFloating(members, joints, transition_joint, transition_piece_mass, mooring, materials)[source]
Bases:
objectParsed floating substructure + raw mooring block.
- Parameters:
- members: list[FloatingMember]
- added_mass(floating, ref_point=None, *, rho=1025.0, z_msl=0.0, n_seg=200, ca_end=0.6)[source]
6×6 infinite-frequency added mass (Morison + member-end caps).
Following RAFT (
raft_member): each submerged member element contributes a transverse added massρ·Ca·(πD²/4)⟂ to its axis (M3 = a'(I − n nᵀ)), and each submerged member end contributes an axial end-cap added massρ·Ca_End·(2/3)πr³along the axis (n nᵀ) — the heave-plate / end effect that closes most of the strip-only heave gap (Ca_Enddefault 0.6, RAFT’s default). Both are kinematically transformed to the platform reference. Still a Morison proxy (no radiation diffraction / member interaction), so a documented approximation to a potential-flowA_inf; the WAMIT deck-fallback supplies the exact matrix when present.
- hydrostatic_restoring(floating, *, rho=1025.0, g=9.80665, z_msl=0.0, n_seg=200)[source]
6×6 hydrostatic restoring (DOF order surge, sway, heave, roll, pitch, yaw) from member geometry — the WAMIT/.hst buoyancy + waterplane convention (no gravitational term; that enters via the body mass elsewhere). For a freely-floating semi the heave / roll / pitch entries are geometry-exact, so this matches a potential-flow .hst closely (integration anchor).
- read_windio_floating(yaml_path, *, component='floating_platform')[source]
Parse the floating substructure + mooring from a WindIO file.
- Parameters:
- Return type:
- rigid_body_inertia(floating, ref_point=None, *, n_seg=200)[source]
Structural rigid-body mass of the substructure about
ref_point: returns(mass, M6x6, cg).Counts the member steel wall (thin-wall
ρ·πD·tper length), end bulkheads, fixed ballast (explicitvolume× material density) and the transition-piece mass. Variable (trim) ballast is intentionally excluded — it is an equilibrium quantity of the fully-assembled turbine, not derivable from the floating component alone (the validated total is taken from the companion ElastoDynPtfmMasswhen available).
Geometry & section properties
Build pyBmodes section properties from tubular geometry instead of pre-computed structural properties.
Wind-turbine towers and monopiles are circular tubes: given the outer diameter, wall thickness, and material, every structural property the FEM needs is an exact closed-form expression — so the user supplies only what they actually know (geometry) and pyBmodes derives mass / EI / GJ / EA, eliminating the hand-computation error class (issue #35).
For a circular tube of outer radius Ro and inner Ri made of an
isotropic material (E, rho, nu):
A = pi (Ro^2 - Ri^2) cross-section area
I = (pi/4) (Ro^4 - Ri^4) area 2nd moment (FA == SS)
J = 2 I polar 2nd moment (tube)
G = E / (2 (1 + nu)) shear modulus
mass_den = rho * A * outfitting_factor kg/m (outfitting lumps
internals / flanges /
paint into the mass)
flp_stff = edge_stff = E * I N*m^2
tor_stff = G * J = E * I / (1 + nu) N*m^2
axial_stff = E * A N
flp_iner = edge_iner = rho * I kg*m (rotary inertia)
These are the same homogeneous-material identities the floating
section-property path uses (axial = mass.E/rho,
tor = EI/(1+nu), rho.I = EI.rho/E) — here derived forward from
geometry rather than back-solved from stiffness. outfitting_factor
multiplies only the mass terms (it is non-structural mass), never
the stiffness — the same separation the AdjTwMa fix established.
- tubular_section_props(span_loc, outer_diameter, wall_thickness, *, E, rho, nu=0.3, outfitting_factor=1.0, title='geometry-derived tubular section properties')[source]
Exact circular-tube section properties for a steel/iso tower.
- Parameters:
span_loc ((n,) normalised station locations in
[0, 1](root) – -> tip), strictly the same convention the solver expects.outer_diameter ((n,) metres, per station.)
wall_thickness ((n,) metres, per station.)
E (isotropic material — Young's modulus (Pa), density) – (kg/m^3), Poisson’s ratio.
rho (isotropic material — Young's modulus (Pa), density) – (kg/m^3), Poisson’s ratio.
nu (isotropic material — Young's modulus (Pa), density) – (kg/m^3), Poisson’s ratio.
outfitting_factor (non-structural mass multiplier (internals,) – flanges, paint, bolts). Multiplies the distributed mass density ONLY. Rotary inertia is treated as a structural section property and is not scaled (it stays
rho * i_area); stiffness is never scaled.title (str)
- Returns:
SectionProperties (FA == SS, no twist / offsets — an axisymmetric
tube has none), ready for the FEM pipeline.
- Return type:
Parser for beam section-properties files (.dat).
File structure (all units SI):
Line 1 : title string
Line 2 : n_secs label description
Line 3 : blank
Line 4 : column header
Line 5 : column units
Lines 6+: one row per spanwise station (13 space-separated values)
trailing notes / blank lines after the data are ignored
Column order:
span_loc str_tw tw_iner mass_den flp_iner edge_iner
flp_stff edge_stff tor_stff axial_stff cg_offst sc_offst tc_offst
- class SectionProperties(title, n_secs, span_loc, str_tw, tw_iner, mass_den, flp_iner, edge_iner, flp_stff, edge_stff, tor_stff, axial_stff, cg_offst, sc_offst, tc_offst, source_file=None)[source]
Bases:
objectSpanwise section property table.
- Parameters:
Polynomial fitting + ElastoDyn helpers
Constrained 6th-order polynomial fit for ElastoDyn mode shapes.
ElastoDyn requires mode shapes of the form
phi(x) = C2*x^2 + C3*x^3 + C4*x^4 + C5*x^5 + C6*x^6 with
constraint phi(1) = 1, i.e. C2+C3+C4+C5+C6 = 1.
The constraint is enforced analytically by substituting
C6 = 1 - C2 - C3 - C4 - C5 and solving the reduced
least-squares system.
- class PolyFitResult(c2, c3, c4, c5, c6, rms_residual, tip_slope, cond_number)[source]
Bases:
objectPolynomial fit coefficients and quality metrics for one mode component.
- Parameters:
- fit_mode_shape(span_loc, displacement)[source]
Fit a constrained 6th-order polynomial to a normalised mode shape.
- Parameters:
span_loc (1-D array of normalised span positions, x in [0, 1].)
displacement (1-D array of mode-shape displacements at each station.)
- Raises:
ValueError – If the inputs are not 1-D, lengths differ, the array is too short to fit five free coefficients, any element is non-finite,
span_locis not strictly increasing, or the tip displacement is effectively zero.- Return type:
Map ModalResult + poly fit to named ElastoDyn input parameters.
- class BladeElastoDynParams(BldFl1Sh, BldFl2Sh, BldEdgSh)[source]
Bases:
objectPolynomial fits for the three blade mode shapes required by ElastoDyn.
- Parameters:
BldFl1Sh (PolyFitResult)
BldFl2Sh (PolyFitResult)
BldEdgSh (PolyFitResult)
- BldEdgSh: PolyFitResult
- BldFl1Sh: PolyFitResult
- BldFl2Sh: PolyFitResult
- class TowerElastoDynParams(TwFAM1Sh, TwFAM2Sh, TwSSM1Sh, TwSSM2Sh)[source]
Bases:
objectPolynomial fits for the four tower mode shapes required by ElastoDyn.
Field names match the OpenFAST tower sub-file
<turbine>_ElastoDyn_tower.dat:TwFAM1Sh/TwFAM2Sh— fore-aft modes 1 and 2TwSSM1Sh/TwSSM2Sh— side-side modes 1 and 2
- Parameters:
TwFAM1Sh (PolyFitResult)
TwFAM2Sh (PolyFitResult)
TwSSM1Sh (PolyFitResult)
TwSSM2Sh (PolyFitResult)
- TwFAM1Sh: PolyFitResult
- TwFAM2Sh: PolyFitResult
- TwSSM1Sh: PolyFitResult
- TwSSM2Sh: PolyFitResult
- class TowerFamilyMemberReport(mode_number, frequency_hz, family_rank, is_fa, fa_rms, ss_rms, direction_ratio, fit_rms, fit_is_good, selected, fa_participation=0.0, ss_participation=0.0, torsion_participation=0.0, torsion_rejected=False)[source]
Bases:
objectDiagnostic view of one scored FA/SS tower family candidate.
fa_participation/ss_participation/torsion_participationare normalised modal kinetic-energy fractions (unit-mass approximation:Σ φ_axis² / Σ φ_total²over all FEM nodes). Sum to 1 for every mode.torsion_rejectedisTruewhen the torsion fraction crosses_TORSION_REJECT_THRESHOLD(10 %); such modes are dropped from the family selection because they are no longer pure bending modes ElastoDyn’s polynomial ansatz can faithfully represent.- Parameters:
- class TowerSelectionReport(fa_family, ss_family, selected_fa_modes, selected_ss_modes, rejected_fa_modes=(), rejected_ss_modes=())[source]
Bases:
objectStructured report of tower-mode family scoring and final selection.
rejected_fa_modes/rejected_ss_modescarry the mode numbers of family candidates that were dropped because their torsion-participation crossed_TORSION_REJECT_THRESHOLD. They’re empty when every candidate passed the gate.- Parameters:
- fa_family: tuple[TowerFamilyMemberReport, ...]
- ss_family: tuple[TowerFamilyMemberReport, ...]
- compute_blade_params(modal)[source]
Fit polynomials to the 1st/2nd flap and 1st edge modes.
- Parameters:
modal (ModalResult)
- Return type:
- compute_tower_params(modal)[source]
Fit polynomials to the 1st/2nd FA and 1st/2nd SS tower modes.
- Parameters:
modal (ModalResult)
- Return type:
- compute_tower_params_report(modal)[source]
Compute tower ElastoDyn parameters and return a selection report.
Pre-rotates any degenerate FA/SS eigenpair (consecutive modes whose relative frequency gap is below
_DEGENERATE_FREQ_RTOL) into clean direction-aligned shapes before classification. The originalmodal.shapesis not mutated; the rotation only feeds the candidate builder used here. See_rotate_degenerate_pairs().Emits a
RuntimeWarningif the polynomial-fit design-matrix condition number exceeds_FIT_COND_WARN(1e4) — the fit may be unreliable because the reduced Vandermonde basis is poorly conditioned at the mesh stations supplied. Emits a stronger warning above_FIT_COND_FAIL(1e6); the reconstructed shape may still be accurate but individual C2..C6 coefficients are likely to swing wildly under small perturbations of the input data.- Parameters:
modal (ModalResult)
- Return type:
Coefficient-consistency validation for OpenFAST ElastoDyn decks.
The polynomial coefficient blocks shipped in industry ElastoDyn .dat
files (NREL 5MW Reference Turbine, IEA-3.4-130-RWT, and others) are
demonstrably inconsistent with the structural-property blocks in the
same files — see cases/ECOSYSTEM_FINDING.md for the longer write-up.
This module exposes a programmatic way to surface that inconsistency:
validate_dat_coefficients()parses the deck, runs pyBmodes on the same structural inputs, fits its own polynomials, and computes per-block RMS residuals for both the file’s polynomial and pyBmodes’ polynomial against the FEM-computed mode shape.ValidationResultandCoeffBlockResultcarry the results in a form the CLI (pybmodes validate) and theTower.from_elastodyn(validate_coeffs=True)path both consume.
- class CoeffBlockResult(name, file_rms, pybmodes_rms, ratio, verdict, file_coeffs, pybmodes_coeffs, fa_participation=nan, ss_participation=nan, torsion_participation=nan, rejected_modes=<factory>)[source]
Bases:
objectValidation result for a single ElastoDyn coefficient block.
Both
file_rmsandpybmodes_rmsare RMS residuals of the respective polynomial evaluated at the FEM stations against the same tip-normalised mode-shape data. They are directly comparable.ratio = file_rms / pybmodes_rmsquantifies how much worse the file polynomial fits the structural model than pyBmodes’ fit does; a ratio above ~50× indicates the file polynomial was generated against a different structural model than the one in the deck.Tower blocks (
TwFAM1Sh/TwFAM2Sh/TwSSM1Sh/TwSSM2Sh) carry four extra fields populated from the family- selection report:fa_participation/ss_participation/torsion_participation— modal kinetic-energy fractions (unit-mass approximation) of the selected mode for this block. Sum to 1.rejected_modes— mode numbers that were dropped from this block’s family during selection because their torsion fraction exceeded 10 %. Empty when every candidate passed the gate.
Blade blocks leave all four fields at their NaN / empty defaults because the blade adapter doesn’t run a torsion-contamination filter (blade torsion is part of the structural model and not handled the same way as tower torsion).
- Parameters:
- class ValidationResult(dat_path, tower_results=<factory>, blade_results=<factory>, overall='PASS', summary='')[source]
Bases:
objectAggregated coefficient-validation result for an ElastoDyn deck.
- Parameters:
dat_path (Path)
tower_results (dict[str, CoeffBlockResult])
blade_results (dict[str, CoeffBlockResult])
overall (Literal['PASS', 'WARN', 'FAIL'])
summary (str)
- blade_results: dict[str, CoeffBlockResult]
- tower_results: dict[str, CoeffBlockResult]
- validate_dat_coefficients(dat_path, *, verbose=False, n_modes=10)[source]
Validate the polynomial coefficient blocks in an ElastoDyn deck.
Parses the main
.datfile plus the tower and blade files it references, builds pyBmodes Tower and RotatingBlade models from the structural-property blocks (NOT the polynomial blocks), runs the eigensolver, fits 6th-order polynomials, and compares those fits against the polynomial coefficients embedded in the deck.- Parameters:
dat_path (Path | str) – Path to the ElastoDyn main
.datfile.verbose (bool) – Reserved for future diagnostic output. The function currently emits no prints regardless of this flag — the CLI front-end is responsible for stdout formatting.
n_modes (int) – Number of FEM modes to extract per model (default 10; must be large enough to cover the four tower bending modes plus the three blade bending modes — pyBmodes’ family selectors warn if any required mode falls outside the requested range).
- Returns:
Carries seven
CoeffBlockResultentries (4 tower + 3 blade) and an overall PASS/WARN/FAIL verdict (worst across all blocks).- Return type:
Patch ElastoDyn .dat files with computed polynomial coefficients.
Each coefficient occupies a separate line of the form
<value> <ParamName(k)> - comment text. The writer finds each
line by matching the parameter name token, then replaces the leading
value in-place, preserving indentation and all trailing text.
- patch_dat(path, params)[source]
Patch named mode-shape coefficient lines in an ElastoDyn .dat file.
Each parameter in params is located by searching for its exact name as a whitespace-delimited token. The value on the same line (the token before the name) is replaced with the computed coefficient. All other content (indentation, comment text) is left unchanged.
- Parameters:
path (path to the ElastoDyn .dat file (modified in place).)
params (BladeElastoDynParams or TowerElastoDynParams.)
- Raises:
KeyError if a required parameter name is not found in the file. –
- Return type:
None
Floating-tower frequency-gap diagnostic.
A floating ElastoDyn deck has two natural tower bending frequencies that
differ by design. The polynomial coefficient blocks in the deck encode
the cantilever tower mode shape, which is the only basis ElastoDyn’s
SHP = sum_i c_i * (h/H)^(i+1) ansatz can represent (the source-code
citations live in
src/pybmodes/_examples/reference_decks/FLOATING_CASES.md). The
coupled-system frequency that OpenFAST linearisation reports includes
platform 6-DOF participation, mooring restoring, and hydrostatic
restoring. The two can differ by 20-30 percent on floating platforms,
and the gap is expected rather than a bug.
report_floating_frequency_gap runs both pyBmodes solves on the same
deck and reports the gap as a short text block, so users reconciling
pyBmodes-generated polynomials against OpenFAST linearisation output do
not have to re-derive the cantilever-vs-coupled architecture from
scratch.
- class FloatingFrequencyGap(cantilever_fa_1, cantilever_ss_1, coupled_fa_1, coupled_ss_1)[source]
Bases:
objectCantilever vs coupled tower bending frequencies for one deck.
Cantilever frequencies come from a clamped-base
from_elastodyn()solve. This is the modal basis ElastoDyn integrates intoMTFA/KTFAat runtime and the basis the shipped polynomial coefficients describe.Coupled frequencies come from a free-base
from_elastodyn_with_mooring()solve with mooring stiffness, hydrostatic restoring, and platform inertia engaged. These are the numbers an OpenFAST linearisation reports.- Parameters:
- report_floating_frequency_gap(main_dat_path, moordyn_dat_path, hydrodyn_dat_path=None, *, n_modes=10)[source]
Run cantilever and coupled solves on the same floating deck.
The cantilever solve is the modal basis ElastoDyn consumes for its runtime tower-bending DOFs. The coupled solve is what OpenFAST linearisation produces when platform 6-DOF, mooring, and hydrostatic restoring are all engaged. The returned
FloatingFrequencyGaplets a user reconcile pyBmodes-generated polynomial coefficients against OpenFAST linearisation output without re-deriving the architectural reason they differ.On the coupled solve,
n_modes + 6modes are requested so that after the six platform rigid-body modes (surge / sway / heave / roll / pitch / yaw) are filtered out, the tower-bending family selector still seesn_modescandidates.
Campbell
Campbell-diagram support: rotor-speed sweep with MAC-tracked blade modes and constant-frequency tower modes overlaid on the same plot.
A Campbell diagram plots a turbine’s natural frequencies against rotor speed and overlays the per-revolution excitation lines (1P, 2P, 3P, …); crossings between excitation lines and structural-mode lines flag resonance risks. For a wind-turbine blade the centrifugal-stiffening contribution to the FEM stiffness matrix raises flap-dominated frequencies markedly with rotor speed while edgewise (lag-dominated) modes barely move. The tower lives in an Earth-fixed frame, so its fore-aft / side-to-side bending frequencies don’t depend on rotor speed at all and show up as horizontal lines on the diagram. The NREL 5MW turbine’s canonical resonance call-out — 3P crossing the 1st tower fore-aft mode near ~6.4 rpm — sits right where the cut-in operating envelope begins, which is exactly the kind of constraint this diagram is designed to surface.
Module layout
Phase 3 PR C1 of the v1.x architecture refactor split this from a single 1301-line module into a sub-package. The public API is unchanged; internal helpers live in private sub-modules so each file covers one concern and stays under a few hundred lines:
pybmodes.campbell.result—CampbellResultdataclass plus its NPZ / CSV round-trip._models— input dispatcher: path-vs-loaded model,.datvs..bmi, optionaltower_inputkeyword._classify— mode-naming heuristics (1st flap/1st tower FA/ platform DOFs)._mac— MAC matrix helpers + Hungarian assignment used by the blade-sweep tracker._sweep— rotor-speed sweep drivers and the publiccampbell_sweep()entry point._plot—plot_campbell().
Public API
campbell_sweep()— given an OpenFAST ElastoDyn main.dat, loads the blade and tower from the same deck, sweeps the blade acrossomega_rpm(with MAC-based mode tracking), solves the tower once, and packs both into a singleCampbellResult..bmiinputs are also accepted and route to blade-only or tower-only sweeps based onbeam_type; an explicittower_input=...keyword adds a tower file alongside a blade.bmi.plot_campbell()— renders the result with blade modes as solid coloured lines, tower modes as horizontal dashed dark-grey lines, and the per-rev excitation family as light grey rays from the origin. Optional vertical marker at the rated rotor speed.
Defaults are deliberately spare (n_blade_modes=4, n_tower_modes=4)
so the diagram shows the modes that actually drive resonance design —
1st/2nd flap, 1st/2nd edge, 1st/2nd tower FA, 1st/2nd tower SS —
without crowding the plot with high-order modes that the per-rev
family doesn’t reach inside any realistic operating envelope.
- class CampbellResult(omega_rpm, frequencies, labels, participation, n_blade_modes, n_tower_modes, mac_to_previous=<factory>)[source]
Bases:
objectFrequencies and labels from a Campbell sweep — blade + tower combined.
- Variables:
omega_rpm ((N,) array of rotor speeds in rpm.)
frequencies ((N, n_total_modes) array of natural frequencies in Hz.) – Columns are ordered blade modes first, then tower modes. With MAC tracking enabled, blade columns hold the same physical mode across all rotor speeds. Tower columns are constant across rows (tower frequencies don’t depend on rotor speed).
labels (list of length
n_total_modeswith human-readable mode) – names — blade modes look like"1st flap"/"2nd edge", tower modes are prefixed with"tower"(e.g."1st tower FA","1st tower SS") so callers can split the two by string match if needed.participation ((N, n_total_modes, 3) array of energy fractions in) – the FEM’s per-mode (flap or FA, edge or SS, torsion) axes. Each row sums to 1. Note the axis interpretation is beam-type-specific: blade columns use flap/edge/torsion, tower columns use FA/SS/torsion.
mac_to_previous ((N, n_total_modes) array of per-step MAC values) – between each output slot’s mode shape at step
kand the same slot at stepk - 1(i.e. the tracking confidence). Row 0 is filled with NaN (no previous step). Tower columns are also NaN (tower modes don’t change with rotor speed, so a MAC confidence is not meaningful for them).n_blade_modes (how many of the leading columns are blade modes.)
n_tower_modes (how many of the trailing columns are tower modes.)
- Parameters:
- classmethod load(path, *, allow_legacy_pickle=False)[source]
Read a sweep result back from a
.npzarchive saved bysave().A legacy pre-1.0 archive whose
__meta__is a pickled object array is refused by default (object-array unpickling can execute arbitrary code); passallow_legacy_pickle=Trueto opt in for a file you trust.- Parameters:
- Return type:
- save(path, *, source_file=None)[source]
Write the sweep result to a
.npzarchive.Arrays go in as named keys; labels and the two integer scalars ride in via the embedded JSON
__meta__blob alongside the standard pyBmodes-version / timestamp / source-file / git-hash metadata captured bypybmodes.io._serialize._capture_metadata().
- to_csv(path)[source]
Write a spreadsheet-friendly CSV with one row per rotor-speed step.
Columns:
rpm, then one frequency column per mode (named by the mode’s label), then one MAC-confidence column per mode suffixed with_mac. Tower-mode MAC columns are NaN throughout because tower modes don’t change with rotor speed — kept as columns for shape-stability across blade-only / tower- only / mixed sweeps.
- campbell_sweep(input_path, omega_rpm, n_blade_modes=4, n_tower_modes=4, *, tower_input=None, track_by_mac=True)[source]
Build a Campbell-diagram dataset for the given turbine.
- Parameters:
input_path (str | pathlib.Path | RotatingBlade | Tower) –
Either a path or an already-loaded model (issue #51):
an OpenFAST ElastoDyn main
.datfile — the function loads the blade and the tower from the deck and runs both;a blade
.bmi(beam_type = 1) — blade-only sweep unlesstower_inputis also supplied;a tower
.bmi(beam_type = 2) — tower-only result (frequencies are constant acrossomega_rpm; the result is mostly useful for overlay against the per-rev family);an already-constructed
RotatingBladeorTower(from any constructor —__init__,from_elastodyn,from_windio,from_windio_floating, …). The model is used verbatim, with no disk re-read, so a single load point feeds both.run()and the sweep, and afrom_windio/from_elastodynmodel (whose section properties no path can re-read) can finally be swept. Routed to blade/tower by itsbeam_typeso either may be passed here.
omega_rpm (np.ndarray) – 1-D array of rotor speeds in rpm.
Ω = 0is fine and produces the parked-rotor frequencies.n_blade_modes (int) – Number of blade modes to extract per speed and report in
frequencies[:, :n_blade_modes]. Default 4 covers 1st/2nd flap and 1st/2nd edge — the modes that actually drive resonance design. Pushing this much higher just adds high-order flap modes that no realistic per-rev family crosses inside the operating envelope; raise it deliberately when you need them.n_tower_modes (int) – Number of tower modes (default 4 — 1st/2nd FA + 1st/2nd SS). Drop to 2 to overlay only the 1st FA + 1st SS pair, or push higher for offshore decks where 3rd-mode crossings matter. Ignored when no tower model is available.
tower_input (str | pathlib.Path | Tower | None) – Optional explicit tower — a tower
.bmipath or a loaded :class:`~pybmodes.models.Tower` (keyword-only). Useful wheninput_pathis a blade-only deck or a loadedRotatingBlade. Overrides the deck-supplied tower ifinput_pathwas an ElastoDyn.dat. So the single-load-point form iscampbell_sweep(blade, omega, tower_input=tower).track_by_mac (bool) – Whether to use MAC across consecutive rotor speeds to keep each blade output column corresponding to the same physical mode.
Falsereturns the eigensolver’s native order (useful for debugging mode re-ordering issues). Tower modes don’t change with rotor speed and are unaffected by this flag.
- Return type:
- plot_campbell(result, excitation_orders=None, rated_rpm=None, ax=None, platform_modes=None, log_freq=False, *, operating_rpm=None, freq_max=None)[source]
Render a Campbell diagram from a
CampbellResult.Engineering-report style (issue #54): structural modes are coloured by family, the legend carries only those four family keys, mode names are written inline next to their lines, and the per-rev family is the only thing the legend enumerates as a group:
Blades — green; per-blade curves, name written at the line.
Tower — black; horizontal lines, name at the line.
Platform — red; floating-platform rigid-body modes (surge / sway / heave / roll / pitch / yaw), near-degenerate symmetric pairs merged (
surge/sway) to keep the figure clean.Blade Passing — blue; the per-rev rays (default 1P / 3P / 6P / 9P), each tagged
↑ nPinline (no legend clutter).
operating_rpm=(lo, hi)shades the operating rotor-speed window grey (outside it stays white) and draws a↔ Operating Speed Rangemarker.- Parameters:
result (CampbellResult) – Output of
campbell_sweep().excitation_orders (list[int] | None) – Per-rev orders. Default
[1, 3, 6, 9].rated_rpm (float | None) – If given, a thin reference line at the operating rotor speed.
ax (matplotlib.axes.Axes | None) – Existing Axes to draw into; a fresh figure if
None.platform_modes (list[tuple[str, float]] | None) – Optional
[(dof, freq_hz), ...]floating rigid-body modes for the screening path (when the result has no platform columns). Drawn in the red Platform family. The coupled-tower path classifies these natively — seecampbell_sweep().log_freq (bool) – Log-scaled frequency axis (the per-rev rays are densely sampled so they render correctly). Default
False.operating_rpm (tuple[float, float] | None) –
(lo, hi)rotor-speed operating window (rpm) — shaded grey with anOperating Speed Rangemarker.None(default) draws no band — backward compatible.freq_max (float | None) – Upper frequency-axis limit (Hz).
None(default) auto-caps the axis just above the highest structural mode so the modes of interest fill the figure (the steep per-rev rays run off the top, as in a standard Campbell report) instead of the axis stretching to the highest ray. Ignored whenlog_freq=True.jitter (Note on blade-line)
-------------------------
shows (For ElastoDyn-derived blade FEMs the 1st-flap line typically)
The ()
axial (BMI adapter floors rotary inertia and forces near-rigid)
1e6) (behaviour (EA / EI ≈)
matrices (leaving the dense FEM)
1e11) (ill-conditioned (κ(M) ≈)
subset (which makes LAPACK's)
the (eigenvalue routines wobble on the lowest mode even when)
tracker (underlying eigenvector is identical step to step. The MAC)
flap-dominant (catches this — the participation array stays > 98 %)
correct (in the 1st-flap slot — so the mode identity is)
only
is (the eigenvalue precision suffers. Centrifugal stiffening)
endpoint-to-endpoint (monotonic in physics (Wright 1982);)
reliable (comparisons (parked vs rated) are)
individual-step
not. (monotonicity is)
- Return type:
matplotlib.figure.Figurefor the rendered axes.
Modal Assurance Criterion
Modal Assurance Criterion (MAC) utilities for comparing mode shapes.
Two public entry points:
mac_matrix()— pairwise MAC between two lists of mode shapes.compare_modes()— full mode-by-mode comparison between twoModalResultrecords: MAC matrix, Hungarian-optimal mode pairing, per-pair frequency shift in percent, and the source labels for display.
The MAC formula:
MAC_ij = |φ_i · φ_j|² / ((φ_i · φ_i) (φ_j · φ_j))
where each φ is the concatenated FEM displacement vector across
flap / lag / twist axes (3 × n_nodes entries). The metric is
sign- and amplitude-invariant; the only quantity that survives is
the directional alignment of the two shapes.
Typical use cases:
compare_modes(baseline_result, patched_result)— confirm the polynomial-coefficient patch didn’t actually change the underlying mode shapes (MAC diagonal stays at ~ 1.0; frequency shifts are the only visible delta).compare_modes(land_result, monopile_result)— quantify the boundary-condition effect on a tower’s mode shapes; the MAC diagonal drops as the lower modes pick up rigid-body contributions from the soft monopile base.
plot_mac() is a lightweight matplotlib helper that renders the
MAC matrix as a heatmap with the Hungarian-paired cells highlighted.
matplotlib is imported lazily so the rest of this module works
without the [plots] extra installed.
- class ModeComparison(mac, frequency_shift, paired_modes, label_A='A', label_B='B', freqs_A=<factory>, freqs_B=<factory>)[source]
Bases:
objectResult of a
compare_modes()call.- Variables:
mac ((n, m) MAC matrix between
result_A.shapesand) –result_B.shapes.frequency_shift ((n_pairs,) percent change in frequency from) –
result_Atoresult_Bfor each Hungarian-paired mode. Positive values meanresult_Bis higher in frequency.NaNfor pairs where either side’s frequency is non-positive (e.g. rigid-body modes).paired_modes (list of
(i, j)tuples mapping) –result_A.shapes[i]→result_B.shapes[j]under the Hungarian-optimal MAC assignment. Length ismin(n, m).label_B (label_A /) – by plot_mac as axis titles.
freqs_B (freqs_A /) – the comparison object is self-contained for downstream reporting.
- Parameters:
- compare_modes(result_A, result_B, *, label_A='baseline', label_B='modified')[source]
Compare two
ModalResultrecords mode-by-mode.Builds the full MAC matrix, finds the Hungarian-optimal pairing (each
result_Amode mapped to oneresult_Bmode), and computes the per-pair frequency shift(f_B - f_A) / f_Ain percent.Pairs are returned in
result_Amode-index order (i.e.paired_modes[0]is(0, j₀)for whateverj₀matched the firstresult_Amode).- Parameters:
result_A (ModalResult)
result_B (ModalResult)
label_A (str)
label_B (str)
- Return type:
- mac_matrix(shapes_A, shapes_B)[source]
Compute the pairwise MAC matrix between two mode-shape lists.
Returns an
(n, m)ndarray whereout[i, j]is the MAC value betweenshapes_A[i]andshapes_B[j]. Values are in[0, 1]; 0 means perfectly orthogonal, 1 means perfectly aligned (or anti-aligned — MAC squares the inner product).Empty inputs are accepted and return a correctly-shaped zero- dimensional ndarray. Zero-norm shapes (i.e. all-zero displacements) get MAC = 0 against every counterpart, since the denominator is undefined and the closest reasonable answer is “no correlation”.
- plot_mac(comparison, ax=None, *, annotate=True, cmap='viridis')[source]
Render the MAC matrix as an annotated heatmap.
Hungarian-paired cells get a red outline; other cells are plain. Cell colour ramps from 0 (dark) to 1 (light) via the supplied
cmap. Setannotate=Falseto drop the numerical overlay (useful for large matrices where labels collide).Returns the parent
Figureso the caller can save / show. RaisesImportErrorif matplotlib isn’t installed (the optional[plots]extra is required).- Parameters:
comparison (ModeComparison)
ax (Axes | None)
annotate (bool)
cmap (str)
- Return type:
Figure
- shape_to_vector(shape)[source]
Flatten a
NodeModeShapeinto one (3·n_nodes,) vector.The concatenation order is
[flap_disp, lag_disp, twist]— matches the convention used internally by the Campbell tracker. Slope arrays are not included; the MAC contract operates on displacement directions only.- Parameters:
shape (NodeModeShape)
- Return type:
Pre-solve sanity checks
Pre-solve sanity checks for Tower and RotatingBlade models.
The check_model() entry point runs a small suite of cheap,
deterministic checks on a parsed model and returns a list of
ModelWarning records describing any anomalies it found. The
list is empty when the model is clean.
The checks run automatically inside Tower.run() and
RotatingBlade.run() with check_model=True (the default).
WARN- and ERROR-severity findings are routed through Python’s
warnings module so they surface at the call site without changing
the function’s return type. INFO-severity findings are surfaced only
when check_model() is called directly — they’re useful context
but not actionable noise on every solve.
Suppress the auto-run with Tower(...).run(n_modes, check_model=False)
(symmetric for RotatingBlade.run). Suppression is meant for
scripted callers that have already validated their input and want
the solver path to stay quiet.
Checks performed (see check_model() for the details):
Every section-property field is finite (no NaN, no ±Inf). Runs first so the per-field checks below don’t have to be NaN-aware (NaN silently passes every
<=/>/ ratio comparison).Span stations are strictly increasing.
Mass density is strictly positive at every station.
Bending stiffness (FA + SS) does not jump by more than 5× between adjacent stations.
EI_FA / EI_SS ratio stays within
[0.1, 10]at every station.Tower-top RNA mass is not larger than the integrated tower mass.
PlatformSupport 6×6 inertia / hydro / mooring matrices are well- formed: shape (6, 6), all entries finite, and symmetric (within
1e-6 · max|A|). Rank deficiency is not flagged — surge / sway / yaw hydrostatic restoring is legitimately zero on most floaters and mooring layouts can be low-rank by design.The horizontal platform CM offset (
cm_pform_x/cm_pform_y) does not exceed the platform’s yaw radius of gyration√(I_yaw / m)— a larger value is almost always a coordinate- origin offset leaking into a field that means “CM offset from the tower axis”, which mislabels the rigid-body modes (issue #95).Floating platform inertia is physical: positive
mass_pformand strictly-positivei_matrixdiagonal (ERROR otherwise).Floating model carries hydrodynamic added mass (
hydro_Mnot all zero) — omitting it biases every rigid-body frequency high (WARN).Floating model has some restoring (
hydro_Kormooring_Knon-zero); with neither, the rigid-body modes collapse to ~0 Hz (WARN).The requested
n_modesdoes not exceed the model’s DOF count.The polynomial-fit design matrix on the mesh stations is not ill-conditioned (cond > 1e4 ⇒ WARN, > 1e6 ⇒ ERROR).
Checks 7–10 are the “floating-model readiness” gates (hub_conn = 2
only): they catch the seakeeping omissions a non-specialist makes when
a WindIO .yaml (geometry + material only) is treated as sufficient
for a floating system — it is not, the way it is for a land tower
(issue #95).
- exception ModelValidationError(findings)[source]
Bases:
ValueErrorRaised by
.run()when the pre-solve checks find ERROR-severity, non-physical input andon_error="raise"(the default).Inherits
ValueErrorso existingexcept ValueErrorcallers keep catching it. The offending findings are onfindings.- Variables:
findings (list of
ModelWarning) – The ERROR-severity findings that triggered the fail-closed path. WARN / INFO findings are not collected here (those never raise).- Parameters:
findings (list[ModelWarning])
- class ModelWarning(severity, message, location)[source]
Bases:
objectOne finding from
check_model().- Variables:
severity (
"INFO","WARN", or"ERROR". INFO is) – contextual (e.g. “RNA mass dominates the structure”); WARN indicates the solve will probably complete but with degraded accuracy; ERROR indicates a non-physical input that will produce undefined results.message (human-readable description of the finding.)
location (dotted path to the offending data, e.g.) –
"section_properties.mass_den"or"bmi.support.hydro_K".
- Parameters:
- apply_findings(model, *, n_modes, on_error='raise', stacklevel=2)[source]
Run
check_model()and route the findings for a.run()call.Shared by
Tower.run()andRotatingBlade.run(). INFO findings are dropped (contextual, not actionable on the solve path). WARN findings always go throughUserWarning. ERROR findings are non-physical input, so they fail closed by default (on_error="raise"raisesModelValidationError); passon_error="warn"to downgrade them to warnings and continue, the pre-1.14.0 behaviour.- Parameters:
model (the
Tower/RotatingBladebeing solved.)n_modes (forwarded to
check_model()for the DOF-count gate.)on_error (
"raise"(default, fail closed on ERROR) or"warn".)stacklevel (forwarded to
warnings.warn()so the warning points) – at the user’s.run()call site.
- Return type:
None
- check_model(model, *, n_modes=None)[source]
Run the full pre-solve check suite on
model.- Parameters:
model (a
TowerorRotatingBladeinstance.)n_modes (optional. When supplied, additionally checks that) –
n_modesdoesn’t exceed the model’s DOF count. Skipped whenNone(e.g. when callers want to validate the static model definition before deciding how many modes to request).
- Returns:
Empty when every check passes. The list is ordered roughly by check number (see module docstring), which keeps the output diffable across runs.
- Return type:
list of
ModelWarning
Numerical options
Centralised numerical-options dataclasses. Defaults preserve every
previously-embedded magic number from fem/solver.py,
elastodyn/params.py, and checks.py — importing this module
changes no behaviour; the value is that callers (and reviewers) can
now find every numerical threshold in one place.
Centralised numerical options for pyBmodes.
Three frozen dataclasses gather the magic numbers that used to live as module-level constants scattered across the FEM solver, the polynomial fitter, and the pre-solve sanity checker. Each carries the same default the original constant did, so importing this module changes no behaviour; the value is that callers (and reviewers) can now find every numerical threshold in one place.
The dataclasses are dataclasses.dataclass(frozen=True)() so they
hash, compare structurally, and can’t be mutated after construction —
suitable as defaults on public APIs and as keys in caches.
Examples
Read the default thresholds:
from pybmodes.options import SolverOptions, FitOptions, CheckOptions
SolverOptions().sparse_ndof_threshold # 500
FitOptions().polynomial_rms_threshold # 0.09
CheckOptions().stiffness_jump_factor # 5.0
Override one threshold while keeping the rest at default:
custom = SolverOptions(sparse_ndof_threshold=2000)
The dataclasses are intentionally additive: future fields gain sensible defaults so existing call sites stay working. Removing or renaming a field is a semver-major change (see API contract).
- class CheckOptions(stiffness_jump_factor=5.0, ei_ratio_min=0.1, ei_ratio_max=10.0, support_asymmetry_rtol=1e-06, fit_cond_warn=10000.0, fit_cond_fail=1000000.0, platform_cm_offset_gyradius_factor=1.0)[source]
Bases:
objectPre-solve sanity-check thresholds (consumed by
pybmodes.checks.check_model()).- Variables:
stiffness_jump_factor (float, default 5.0) – Maximum allowed ratio of bending stiffness between adjacent section nodes (forward and backward). A jump above this flags a likely transition-piece discontinuity that the polynomial fit will struggle to represent — typically wants extra mesh refinement around it.
ei_ratio_min (float, default 0.1) – Lower bound on
EI_FA / EI_SSat every section node. Below this is unphysical for a real tower section; the structural inputs have probably swapped FA / SS.ei_ratio_max (float, default 10.0) – Upper bound on
EI_FA / EI_SS. Symmetric toei_ratio_min.support_asymmetry_rtol (float, default 1e-6) – Tolerance (relative to
max|support|) for treating aPlatformSupport6 x 6 matrix as symmetric. Above this, the check warns that the support matrix is asymmetric, which is load-bearing for OC3-Hywind-style cross-coupled platforms but commonly indicates a deck-assembly bug for axisymmetric ones.fit_cond_warn (float, default 1e4) – Polynomial-fit design-matrix condition above which check_model emits a WARN. Mirrors
FitOptions.fit_cond_warnso the check can run independently of the fit module.fit_cond_fail (float, default 1e6) – Polynomial-fit design-matrix condition above which check_model emits a FAIL. Mirrors
FitOptions.fit_cond_fail.platform_cm_offset_gyradius_factor (float, default 1.0) – The horizontal platform CM offset (
cm_pform_x/cm_pform_yon aPlatformSupport) is flagged when its magnitude exceeds this factor times the platform’s yaw radius of gyration√(I_yaw / m).cm_pform_x/cm_pform_yare the CM offset from the tower axis; a value comparable to the platform’s own size is almost always a coordinate-origin error leaking into the field, which injects spurious surge/sway↔yaw coupling and mislabels the rigid-body modes (issue #95).
- Parameters:
- class FitOptions(polynomial_rms_threshold=0.09, torsion_contamination_threshold=0.1, fit_cond_warn=10000.0, fit_cond_fail=1000000.0)[source]
Bases:
objectPolynomial-fit + family-selection thresholds.
- Variables:
polynomial_rms_threshold (float, default 0.09) – Maximum RMS residual a clamped-base polynomial fit is allowed to have for its mode to count as a viable FA / SS family candidate (after the tangent-line root-rigid subtraction). The
_select_tower_familyalgorithm drops candidates above this.torsion_contamination_threshold (float, default 0.10) – Drops candidates whose modal-kinetic-energy torsion fraction
T_tor = sum(phi_tor**2) / sum(phi_total**2)exceeds this. Hybrid bending + twist modes (T_tor in the 1-3 % range are “near-pure-bending”) cannot be expressed by the polynomial ansatz; the filter keeps them out of the FA / SS selection.fit_cond_warn (float, default 1e4) – Condition-number ceiling above which the polynomial-fit design-matrix conditioning emits a
RuntimeWarning. Suggests numerical sensitivity of the polynomial-coefficient solve to perturbations in the input mode shape.fit_cond_fail (float, default 1e6) – Condition-number ceiling above which the polynomial fit is flagged as FAIL by
pybmodes.checks.check_model(). The reconstructed shape may still be a good visual fit, but the coefficient values are not trustworthy beyond a couple of significant figures.
- Parameters:
- class SolverOptions(sparse_ndof_threshold=500, symmetry_rtol=1e-12)[source]
Bases:
objectFEM solver dispatch + matrix-symmetry tolerances.
- Variables:
sparse_ndof_threshold (int, default 500) – Above this many reduced-system DOFs and when a small subset of modes is requested, the solver routes through
scipy.sparse.linalg.eigsh()shift-invert instead of the densescipy.linalg.eigh(). 5-18 x speedup on real-tower meshes; below the threshold the dense path’s LAPACK back-end is faster.symmetry_rtol (float, default 1e-12) – Relative tolerance (multiplied by
max|matrix|) for treating an assembled stiffness or mass matrix as symmetric. Asymmetry beyond this routes the eigenproblem through the general densescipy.linalg.eig()instead of the symmetricscipy.linalg.eigh(). The OC3 Hywind cross-coupledhydro_K + mooring_Kexercises this branch.
- Parameters:
Report generation
Generate a human-readable report on a pyBmodes modal analysis.
The entry point is generate_report(). It builds a structured
representation of the report (sections + tables) once, then renders
that representation in one of three output formats:
Markdown (
format="md") — default. Human-readable plain text with section headers, tables, and inline code blocks. Useful for paste-into-issue or check-in-to-repo workflows.HTML (
format="html") — a self-contained HTML5 document with inline CSS. Same content as the markdown version; emitted directly rather than via an md→html converter so the report module has no runtime dependency on the third-partymarkdownpackage.CSV (
format="csv") — frequencies + polynomial-coefficient rows only, suitable for spreadsheet ingestion. The narrative sections (assumptions, validation verdict, warnings) are dropped because CSV can’t represent them faithfully.
The report’s content is driven by what the caller supplies. The
minimum is a ModalResult; everything else
(model, campbell, validation, check_warnings,
tower_params, blade_params) is optional and unlocks the
corresponding section when supplied.
- generate_report(result, output_path, *, format='md', model=None, campbell=None, validation=None, check_warnings=None, tower_params=None, blade_params=None, elastodyn_compatible=None, source_file=None, status=None)[source]
Render a modal-analysis report to
output_path.source_fileis recorded in the Model summary section and the metadata block. When omitted, the function falls back toresult.metadata["source_file"]and then tomodel._bmi.source_fileif those are populated.statusis an optional completeness stamp shown at the top of the Model summary section ("complete"/"partial"/"screening"/"invalid"). A workflow that may skip part of its output (e.g.pybmodes.workflows.windio.run_windio()) passes it so a reader can tell at a glance whether the report is the full analysis or a reduced one.Noneomits the row.Returns the resolved output path so the caller can chain.
- Parameters:
result (ModalResult)
output_path (str | pathlib.Path)
format (ReportFormat)
model (Tower | RotatingBlade | None)
campbell (CampbellResult | None)
validation (ValidationResult | None)
check_warnings (list[ModelWarning] | None)
tower_params (TowerElastoDynParams | None)
blade_params (BladeElastoDynParams | None)
elastodyn_compatible (bool | None)
source_file (str | pathlib.Path | None)
status (str | None)
- Return type:
Mooring
Quasi-static mooring linearisation for pyBmodes.
Solves the extensible elastic catenary per line, sums fairlead tensions
into a platform 6-DOF restoring force, and returns a finite-difference
6×6 mooring stiffness matrix ready for the PlatformSupport.mooring_K
block.
References
Jonkman, J. M. (2007). Dynamics Modeling and Loads Analysis of an Offshore Floating Wind Turbine, NREL/TP-500-41958. Appendix B, equations B-1 / B-2 are the extensible-catenary boundary-condition equations implemented in
pybmodes.mooring._catenary._catenary_residual(). B-7 / B-8 are the seabed-contact variants (no friction;CB = 0).Irvine, H. M. (1981). Cable Structures, MIT Press, §2.4 the extensible elastic catenary. Equivalent derivation; the EA-correction terms
H · L / EAandL · (V − ½WL) / EAcome from §2.4 eqn (2.49).
Module layout
Phase 3 PR C2 of the v1.x architecture refactor split this from a single 1202-line module into a sub-package. The public API is unchanged; internal helpers live in private sub-modules:
pybmodes.mooring.types—LineType,Point,Linedataclasses.Line.solve_static()is here.pybmodes.mooring.system—MooringSystem: multi-line force assembly, equilibrium Newton, 6×6 stiffness, plus theMooringSystem.from_moordyn()andMooringSystem.from_windio_mooring()classmethod parsers._catenary— extensible-elastic-catenary residual + analytical 2×2 Jacobian (Jonkman 2007 B-1 / B-2 + B-7 / B-8)._rotation— 3-2-1 intrinsic Euler rotation primitive (ElastoDyn attitude convention)._moordyn_parser— MoorDyn.datsection + row tokenisers.
Scope
Implemented:
Extensible elastic catenary per line (Newton on
(H, V); analytical 2 × 2 Jacobian;tol = 1e-6m,MaxIter = 100).Fully-suspended (
V_F ≥ W · L) and anchor-on-seabed (V_F < W · L, zero friction) profiles, branched inside the residual function.Multi-line platform restoring force from a 6-DOF body offset.
Central-difference linearisation around an arbitrary or zero offset producing
K_mooring(6, 6).MoorDyn v1 (
CONNECTION) and v2 (POINT).datparsing.
Known limitations:
Seabed friction (
CB > 0) is parsed fromLineTypebut not consumed by the catenary solver.Sloped seabed, U-shape lines (one line touching the seabed mid- span), and the vertical-line degenerate case (
H → 0) are not handled.Time-domain dynamics, hydrodynamic drag, and added mass on the lines themselves are out of scope — this is a quasi-static linearised model only.
MooringSystem.solve_equilibrium()defaults to the input offset; pure mooring has no z equilibrium without buoyancy / weight, so the Newton iteration is only meaningful for the in-plane DOFs (surge, sway, yaw) of a 3-fold-symmetric layout. Callers wanting platform equilibrium under a full force model should callMooringSystem.restoring_force()and assemble the rest of the forces themselves.
Coordinate / unit conventions
SI throughout: m, N, kg, kg/m, N/m; radians for rotations. Origin at MSL; z positive upward; anchors at negative z (below MSL). Matches OpenFAST / HydroDyn / ElastoDyn without any coordinate transform.
Body rotation uses the 3-2-1 (z-y-x intrinsic) Euler angle convention
R = R_z(yaw) · R_y(pitch) · R_x(roll) — the same convention as
ElastoDyn’s platform 6-DOF state.
- class Line(line_type, point_a, point_b, unstretched_length, seabed_contact=True)[source]
Bases:
objectA single mooring line connecting two
Pointendpoints.The catenary solve is owned by this class; the multi-line force assembly is delegated to
MooringSystem.- Variables:
seabed_contact (bool, default True) – Whether the anchor sits on a seabed. When
True, a solver iterate withV_F < W·Ltriggers the seabed-contact branch of the catenary equations (Jonkman 2007 B-7 / B-8 withCB = 0); the anchor-side portion of the line is treated as resting on the seabed. WhenFalsethe fully-suspended equations (B-1 / B-2) are used unconditionally — appropriate for analytical tests where both endpoints are in free air and the line just sags between them. FOWT use cases default toTrue.- Parameters:
- solve_static(r_a, r_b, tol=1e-06, max_iter=100)[source]
Solve the elastic catenary between
r_a(anchor) andr_b(fairlead).Implements Jonkman 2007 Appendix B equations B-1 and B-2 for the fully-suspended branch; B-7 / B-8 with
CB = 0for the seabed- contact branch. The two unknowns areH(horizontal tension, constant along the line) andV_F(vertical tension at the fairlead, positive when the line pulls the fairlead downward).- Returns:
H (float) – Horizontal tension (N).
V_F (float) – Vertical fairlead tension (N).
f_on_fairlead (ndarray, shape (3,)) – World-frame force the line exerts on
r_b(the fairlead): horizontal component pulls towardr_a, vertical component is-V_F(line pulls fairlead down).
- Parameters:
- Return type:
- class LineType(name, diam, mass_per_length_air, EA, w, CB=0.0)[source]
Bases:
objectMaterial spec for a mooring line.
- Variables:
name (str) – Identifier referenced by
Line.line_typeand by MoorDyn LINES rows.diam (float) – Outer diameter (m).
mass_per_length_air (float) – Mass density in air (kg/m).
EA (float) – Axial stiffness (N). Inextensible limit is
EA → ∞.w (float) – Wet weight per unit length (N/m). For a homogeneous line of diameter
din water of densityρ_w:w = (m - ρ_w · π/4 · d²) · g.CB (float, default 0) – Seabed friction coefficient (sliding friction along the resting segment of a partly-grounded line). Parsed for round-trip identity with MoorDyn
.datinputs; the current catenary solver only handles theCB = 0(frictionless seabed) case.
- Parameters:
- class MooringSystem(depth, rho=1025.0, g=9.80665, line_types=None, points=None, lines=None)[source]
Bases:
objectA collection of catenary lines connecting a platform to anchors.
The system is fully assembled by the constructor or
from_moordyn(). The downstream API is:fairlead_positions()— world-frame fairlead positions at a given body offset.restoring_force()— 6-vector force / moment from all lines on the body, in world frame, about the body origin.solve_equilibrium()— Newton iteration over body DOFs to driverestoring_forceto zero (may not converge for pure mooring without buoyancy — see module docstring).stiffness_matrix()— finite-difference 6 × 6 about a chosen offset (or zero by default; see note in the docstring below).
- Parameters:
- classmethod from_moordyn(dat_path, rho=1025.0, g=9.80665)[source]
Parse a MoorDyn v1 / v2
.datand return a populated system.Sections recognised:
LINE TYPES (or LINE DICTIONARY): rows
Name Diam MassDenInAir EA …. Wet weight is derived asw = (MassDenInAir − ρ · π/4 · d²) · g.POINTS or CONNECTION PROPERTIES: rows
ID Attachment X Y Z ….Attachmentaccepted case-insensitively asFixed,Vessel, orFree.LINES or LINE PROPERTIES: rows
ID LineType AttachA AttachB UnstrLen ….OPTIONS (or SOLVER OPTIONS):
WtrDpth/depthandrhoW/rhoif present override the constructor defaults.
Each section header is detected by
startswith('---')plus a keyword (case-insensitive); the immediately-following 1-2 rows are skipped as column headers / units rows.- Parameters:
- Return type:
- classmethod from_windio_mooring(floating, *, depth, moordyn_fallback=None, rho=1025.0, g=9.80665)[source]
Build a system from a WindIO
mooringblock (issue #35).floatingis a parsedpybmodes.io.windio_floating.WindIOFloating— itsjointstable supplies every anchor / fairlead world position (fairleads are the axial joints resolved during parsing). Line topology (nodes / lines) comes from the WindIO mooring block; line properties (mass/length, EA, wet weight) are resolved in order of preference:explicit WindIO
line_typesfields —mass_density/linear_densityandstiffness/EA/axial_stiffness— when present;a companion MoorDyn deck (
moordyn_fallback): the accurate path, equivalent to how WISDEM/RAFT delegate chain sizing to MoorPyMoorProps(line types matched by name, or the sole entry);a documented studless-chain diameter regression (MoorPy
MoorPropsdefault coefficientsm ≈ 19.9e3·d²,EA ≈ 0.854e11·d²) — a rough last resort that emits aUserWarning; supply a deck or explicit props for quantitative work.
Catenary engine +
stiffness_matrixare unchanged, so the WindIO-topology system and the companion-MoorDyn system are consistent by construction (cross-path consistency anchor).
- restoring_force(body_r6)[source]
6-vector force/moment from all lines on the platform body.
F[:3]= sum of world-frame forces at every Vessel-attached endpoint;F[3:6]= sum of moments (r_endpoint_world − r_body_origin) ×F_endpoint, about the body origin.For each line, the endpoint forces are derived from the catenary solve in this order:
F_on_B(B = the “fairlead” passed asr_btosolve_static) =H · ê_B→A + (-V_F) ẑ.F_on_A=H · ê_A→B + V_A ẑwhereV_A= max(0, V_F − W·L). Fully suspended:V_A = V_F − W·L > 0(line pulls anchor up). Seabed contact (V_F < W·L, CB = 0): the anchor is on the seabed; horizontal tension at the anchor is stillH(no friction decay), andV_A = 0.
Lines with neither endpoint attached to the body contribute nothing. Lines with both endpoints attached to the body contribute both endpoint reactions.
- solve_equilibrium(body_r6_init=None, tol=0.0001, max_iter=50, dx=0.1, dtheta=0.1)[source]
Newton iteration over body 6-DOF to drive
restoring_forceto zero.Warning: pure mooring without buoyancy / weight has no z equilibrium (the lines always pull the platform down). For a 3-fold-symmetric mooring at zero offset the in-plane DOFs (surge, sway, yaw) are already balanced; the heave DOF will not converge. Pass a
body_r6_initclose to your expected operating point and accept the result as a “best effort” local-minimum.
- stiffness_matrix(body_r6=None, dx=0.1, dtheta=0.1)[source]
Linearised 6 × 6 mooring stiffness about
body_r6.Central finite differences with perturbation
dx(m) on translational DOFs anddtheta(rad) on rotational DOFs. The trans-rot off-diagonal blocks are symmetrised after differencing — mooring linearised at static equilibrium is the Hessian of a potential and must therefore be symmetric; finite-difference noise gets averaged out.body_r6 = Noneis treated asnp.zeros(6)(the typical FOWT linearisation point). Pure mooring has no z-direction equilibrium without buoyancy, so a solve-for-equilibrium default would diverge; pass an explicitbody_r6if you want a different linearisation point.
- class Point(id, attachment, r_body)[source]
Bases:
objectEndpoint of a mooring line.
- Variables:
id (int) – MoorDyn point ID (preserved for round-trip identification).
attachment (str) – One of
Fixed/Vessel/Free(case-insensitive on construction; stored title-cased).r_body (ndarray, shape (3,)) – Position in body frame for
Vesselpoints; world frame forFixedandFreepoints.Freepoints are essentiallyFixedplaceholders for this quasi-static solver — they don’t participate in the body-equilibrium DOFs.
- Parameters:
Soil-pile interaction
Mudline coupled-spring foundation for soft monopile soil-pile interaction.
The coupled-spring (CS) model represents the soil-pile reaction at the
mudline as three springs: a lateral spring K_hh, a rotational
spring K_rr, and a cross-coupling K_hr. The model is well
established for monopile-supported offshore wind turbines and is
endorsed by Yu and Amdahl (2023) for first three modes when the
spring stiffnesses are calculated properly.
This module wires closed-form formulas for K_hh, K_hr, K_rr from pile
geometry and soil properties into a 6x6 mooring_K block that drops
straight into pybmodes.io.bmi.PlatformSupport of a
hub_conn = 3 soft-monopile BMI. The dispatch covers two formula
families and three soil profiles, classifying pile behaviour via
Randolph (1981).
Scope. MudlineFoundation produces the linearised mudline stiffness
used for coupled-frequency prediction. For ElastoDyn polynomial
coefficient generation the cantilever path
(pybmodes.models.Tower.from_elastodyn() or
pybmodes.models.Tower.from_geometry()) is still required
regardless of soil flexibility. The mudline stiffness affects the
coupled-system frequency but NOT the polynomial basis. ElastoDyn’s
SHP ansatz requires clamped-base mode shapes
(src/pybmodes/_examples/reference_decks/FLOATING_CASES.md records
the source-code citations and cases/ECOSYSTEM_FINDING.md the
audit trail).
References
Yu, Z. and Amdahl, J. (2023). A Rayleigh-Ritz solution for high order natural frequencies and eigenmodes of monopile supported offshore wind turbines considering tapered towers and soil-pile interactions. Marine Structures 92, 103482. https://doi.org/10.1016/j.marstruc.2023.103482
Shadlou, M. and Bhattacharya, S. (2016). Dynamic stiffness of monopiles supporting offshore wind turbine generators. Soil Dynamics and Earthquake Engineering 88, 15-32.
Psaroudakis, E. G., Mylonakis, G. and Antonopoulos, A. (2021). Analytical formulas for the lateral response of monopiles in homogeneous Winkler-type foundations. (As cited in Yu and Amdahl 2023, Eq 25.)
Randolph, M. F. (1981). The response of flexible piles to lateral loading. Geotechnique 31, 247-259.
- class MudlineFoundation(K_hh, K_hr, K_rr, pile_behaviour, soil_profile, formula)[source]
Bases:
objectThree coupled springs at the mudline for a monopile foundation.
Spring convention matches Eq (3) of Yu and Amdahl (2023): the mudline force-moment vector is
[F, M] = [[K_hh, K_hr], [K_hr, K_rr]] @ [rho, theta]whererhois the mudline lateral displacement andthetathe mudline rotation in the same 2-D plane. By the right-hand convention used in OpenFAST (Jonkman 2010 NREL/TP-500-47535 Table 5-1), the K_hr term is negative for typical sands and clays.The
as_mooring_K()accessor maps the 2-D coupled-spring matrix to the OpenFAST 6-DOF order[surge, sway, heave, roll, pitch, yaw]and is symmetric. Heave and yaw are not modelled by this CS surrogate and stay at zero in the returned matrix. Wire it into the soft monopile viapybmodes.io.bmi.PlatformSupport.mooring_Kof a BMI built forhub_conn = 3.- Parameters:
- as_mooring_K()[source]
Return the 6x6 mudline stiffness in OpenFAST DOF order.
Mapping (Eq 3 of Yu and Amdahl 2023 lifted into 6 DOFs):
K[0, 0] = K[1, 1] = K_hh(lateral on surge and sway, the three-fold axisymmetric assumption that pins both diagonals to the same value).K[3, 3] = K[4, 4] = K_rr(rotational on roll and pitch).K[0, 4] = K[4, 0] = K_hr(surge-pitch coupling).K[1, 3] = K[3, 1] = -K_hr(sway-roll coupling, opposite sign by right-hand rule).K[2, 2] = 0(heave not modelled by the CS surrogate).K[5, 5] = 0(yaw not modelled by the CS surrogate).
The cross-coupling sign convention is pinned by
tests/test_mooring.py::test_oc3hywind_bmi_dof_order_matches_jonkman_2010against Jonkman (2010) OC3 Table 5-1, which reportsK_15 = -2.821e6N for surge-pitch andK_24 = +2.816e6N for sway-roll.- Return type:
- classmethod from_soil_properties(pile_diameter, pile_length_embedded, pile_EI, soil_E, soil_nu=0.3, soil_profile='homogeneous', pile_behaviour='auto', formula='shadlou')[source]
Compute K_hh, K_hr, K_rr from pile geometry and soil properties.
- Parameters:
pile_diameter (float) – Outer diameter of the monopile,
D_Pin m.pile_length_embedded (float) – Embedded pile length,
L_Pin m.pile_EI (float) – Pile bending stiffness
E_P * I_Pin N m^2. For a tubular pile with diameter D and wall thickness t,I_P = pi / 64 * (D^4 - (D - 2 t)^4).soil_E (float) – Soil Young’s modulus
E_SOin Pa. For an inhomogeneous profile this is the reference modulus at the depth used by the chosen formula family.soil_nu (float) – Soil Poisson’s ratio. Default 0.3.
soil_profile (Literal['homogeneous', 'parabolic', 'linear']) – One of
"homogeneous"(constant E_SO with depth),"parabolic"(E_SO proportional to sqrt of depth),"linear"(E_SO proportional to depth).pile_behaviour (str) – One of
"flexible","rigid","auto". When set to"auto", the pile is classified per Randolph (1981). An intermediate L/D ratio falls back to the flexible formulas with aUserWarning.formula (Literal['shadlou', 'psaroudakis']) –
"shadlou"(default) uses Shadlou and Bhattacharya (2016) per Yu Table 1 and covers all three soil profiles."psaroudakis"uses Yu Eq 25 and is restricted to the homogeneous profile.
- Returns:
Coupled-spring stiffness in SI units.
- Return type:
- Raises:
ValueError – On non-positive geometry or soil parameters, an unknown
soil_profileorpile_behaviourtoken, orformula="psaroudakis"paired with a non-homogeneous soil profile.
Plot helpers ([plots] extra)
Professional plotting utilities for pybmodes results.
Requires matplotlib >= 3.7. Install with:
pip install "pybmodes[plots]"
All functions return a matplotlib.figure.Figure object; call
fig.show() or fig.savefig(path) as needed.
- apply_style()[source]
Apply the MATLAB-style plot defaults.
Mutates
matplotlib.rcParamsin place. Safe to call multiple times — the second call simply re-applies the same values. RaisesImportErrorif matplotlib isn’t installed (the optional[plots]extra).- Return type:
None
- bir_mode_shape_plot(result, mode_specs, *, title=None, height_label='Tower section height / H', x_label='Modal displacement', annotations=None, coupling_overlay=None, figsize=(5.5, 6.5), xlim=None, colors=None)[source]
Plot mode shapes in the Bir 2010 figure convention.
- Parameters:
result (ModalResult) –
ModalResultfromTower.run()orRotatingBlade.run().mode_specs (list[ModeSpec]) – List of
(mode_index_1based, component, label)tuples.componentis one of"flap"(fore-aft / F-A),"lag"(side-side / S-S),"twist", or"axial".labelappears in the legend.title (str | None) – Optional figure title.
height_label (str) – Y-axis label. Default matches Bir’s notation; pass
"Span fraction"for blade plots.x_label (str) – X-axis label. Default
"Modal displacement"matches the paper.annotations (dict[str, float] | None) – Optional
{label: y_fraction}dict drawing horizontal markers at the given normalised heights (e.g.{"Mean Sea Level": 0.40, "Mud Line": 0.25}for Bir Fig 8).coupling_overlay (list[ModeSpec] | None) – Optional list of
(mode_index, component, label)plotted as dashed lines on the same axes; used to show e.g. the twist component of an S-S mode (Bir Fig 5b, 6b).figsize (tuple[float, float]) – Matplotlib figure size in inches.
xlim (tuple[float, float] | None) – Optional
(xmin, xmax); auto-fits with a small pad if omitted.colors (list | str | None) – Optional colour control (issue #47): a matplotlib colormap name or an explicit list of colours.
None(default) uses the styled palette, switching to a continuous colormap when there are more curves than distinct palette colours so no two modes share a hue.
- Return type:
- bir_mode_shape_subplot(result, panels, *, suptitle=None, height_label='Tower section height / H', x_label='Modal displacement', annotations=None, figsize=None, colors=None)[source]
Multi-panel Bir-convention plot (matches Bir Fig 8 layout).
- Parameters:
panels (list[tuple[str, list[ModeSpec]]]) – List of
(panel_title, mode_specs)tuples; one subplot per entry.annotations (dict[str, float] | None) – Drawn on every panel (e.g. MSL + Mud Line).
colors (list | str | None) – Optional colour control (issue #47): a matplotlib colormap name or an explicit list of colours, applied per panel.
None(default) uses the styled palette, switching to a continuous colormap when a panel has more curves than distinct palette colours.result (ModalResult)
suptitle (str | None)
height_label (str)
x_label (str)
- Return type:
- blade_fit_pairs(result, params)[source]
Build
(label, span_loc, fem_disp, fit)entries for blade modes.Returns entries for the 1st flap, 2nd flap, and 1st edge modes, matching the order in params.
- Parameters:
result (ModalResult) –
ModalResultfromRotatingBlade.run().params (BladeElastoDynParams) –
BladeElastoDynParamsfromcompute_blade_params(result).
- Return type:
list[FitEntry]
- jonswap_spectrum(f, *, hs, tp, gamma=3.3)[source]
JONSWAP wave elevation spectrum
S(f)(frequency in Hz).Standard Pierson-Moskowitz core times the peak-enhancement factor
gamma. The shape is scaled so the zeroth spectral moment is exactly the significant-wave-height identitym0 = Hs**2 / 16(Hs = 4 sqrt(m0)). The peak sits atf_p = 1/Tp. Returns0forf <= 0.
- kaimal_spectrum(f, *, mean_speed, length_scale, sigma=None, turbulence_intensity=0.14)[source]
One-sided longitudinal Kaimal turbulence spectrum
S_u(f).S_u(f) = 4 sigma_u^2 (L/U) / (1 + 6 f L/U)^(5/3)(IEC 61400-1 form), monotonically decreasing infwith the finite low-frequency plateauS_u(0) = 4 sigma_u^2 L / U. Units arem^2/s(PSD of wind speed) whenmean_speedis inm/sandlength_scaleinm; the plot normalises it, so only the shape matters there.sigma(the longitudinal standard deviation) defaults toturbulence_intensity * mean_speedwhen not given.
- plot_environmental_spectra(*, tower_fa_hz=None, tower_ss_hz=None, rpm_design=None, rpm_constraint=None, harmonics=(1, 3), wind=None, wave=None, freq_max=0.6, n_points=2000, ax=None, title=None)[source]
Draw the environmental-loading frequency-placement diagram.
- Parameters:
tower_fa_hz (float | None) – Tower 1st fore-aft / side-side natural frequencies (Hz), drawn as a solid / dashed black vertical line. Either may be
Noneto omit.tower_ss_hz (float | None) – Tower 1st fore-aft / side-side natural frequencies (Hz), drawn as a solid / dashed black vertical line. Either may be
Noneto omit.rpm_design (tuple[float, float] | None) –
(rpm_min, rpm_max)of the actual rotor operating range — the darker hatched design band for each requested harmonic.rpm_constraint (tuple[float, float] | bool | None) –
(rpm_min, rpm_max)of the allowable placement window — the lighter solid constraint band drawn behind the design band. Defaults to the design range widened by +/-15 % when omitted (None). PassFalseto suppress the constraint band (and its legend entries) entirely and draw only the operating design band — for callers who have a fixed operating range but no separate placement envelope.harmonics (Sequence[int]) – Per-rev orders to draw (default
(1, 3)-> 1P and 3P).wind (dict | None) –
dictofkaimal_spectrum()keyword arguments (mean_speed,length_scale, optionallysigma/turbulence_intensity).Noneskips the wind curve.wave (dict | None) –
dictofjonswap_spectrum()keyword arguments (hs,tp, optionallygamma).Noneskips the wave curve.freq_max (float) – Frequency-axis upper bound (Hz) and sample count.
n_points (int) – Frequency-axis upper bound (Hz) and sample count.
ax (Axes | None) – Existing matplotlib
Axesto draw into; a new figure is created whenNone.title (str | None) – Optional figure title.
- Return type:
- plot_fit_quality(fits, title=None, figsize=None)[source]
Plot polynomial fit vs FEM data with residuals for each mode.
Each subplot shows: * FEM nodal values (circles) * Fitted polynomial (solid line over a fine grid) * Residual band (shaded region between FEM and fit) * RMS residual and tip-slope annotation
- Parameters:
fits (list[FitEntry]) – List of
(label, span_loc, fem_disp, fit)tuples as returned byblade_fit_pairs()ortower_fit_pairs().title (str | None) – Overall figure title.
figsize (tuple[float, float] | None) – Matplotlib figure size. Defaults to
(4.5 * n_cols, 4.0 * n_rows).
- Return type:
- plot_mode_shapes(result, n_modes=6, component='both', title=None, figsize=None, *, normalize='mode', colors=None)[source]
Plot normalised mode shape displacements vs normalised span.
- Parameters:
result (ModalResult) – Output from
RotatingBlade.run()orTower.run().n_modes (int) – Number of modes to overlay (at most
len(result.shapes)).component (str) –
"flap"— fore-aft (w) panel only."lag"— side-side (v) panel only."both"— two side-by-side panels (default).title (str | None) – Overall figure title. Uses a sensible default when None.
figsize (tuple[float, float] | None) – Matplotlib figure size
(width_in, height_in). Defaults to(9, 4)for one panel and(14, 4)for two panels.normalize (str) –
How each mode’s two curves are scaled (issue #47):
"mode"(default) — both panels share one scale per mode, the peak|displacement|across flap and lag. The dominant direction then reaches ±1 and the minor direction stays proportionally small, so a fore-aft mode reads as a full-amplitude curve in the flap panel and a flat near-zero line in the lag panel. This is what makes FA vs SS (and a rigid platform DOF vs a real bending mode) visually distinguishable."component"— the legacy behaviour: each panel is normalised independently to its own peak, so even a 1 % cross-coupling component is blown up to full height and a predominantly-flap mode looks identical in both panels. Kept for reproducing pre-1.5 figures.
colors (list | str | None) – Optional colour control passed to the internal palette picker: a matplotlib colormap name (sampled into
n_modesdistinct colours) or an explicit list of colours.None(default) uses the styledprop_cycle, automatically switching to a continuous colormap when there are more modes than distinct palette colours so no two modes collide on the same hue.
- Return type:
- tower_fit_pairs(result, params)[source]
Build
(label, span_loc, fem_disp, fit)entries for tower modes.Returns entries for FA1, FA2, SS1, SS2, matching the order in params. The rigid-body root component is removed from displacements before fitting (same as
compute_tower_params()).- Parameters:
result (ModalResult) –
ModalResultfromTower.run().params (TowerElastoDynParams) –
TowerElastoDynParamsfromcompute_tower_params(result).
- Return type:
list[FitEntry]
Workflows (library-callable CLI subcommands)
Phase 2 of the v1.x architecture refactor exposes each CLI
subcommand as a typed library function. The CLI in
pybmodes.cli becomes a thin argparse + delegation
layer; notebooks and external scripts can call the same flows
directly without subprocess.
Library-callable workflow functions for every pybmodes CLI subcommand.
Background
Before Phase 2 of the v1.x architecture refactor, each
pybmodes subcommand was implemented inline in a ~60 KB
pybmodes.cli module: argument parsing, business logic, error
formatting, and exit-code mapping were all mixed together. That made
the workflows useful only via subprocess — a notebook or external
script that wanted to run the validate / patch / batch / report /
windio / examples flow had to subprocess.run(["pybmodes", ...])
and parse stdout.
This sub-package separates the layers:
Workflow functions (one per subcommand) live here. Each is a pure-typed library entry point with explicit parameters and a typed result dataclass.
The CLI parser stays in
pybmodes.cli— it parsessys.argv, calls the workflow, formats the result to stdout / stderr, and translates the result into an exit code. Nothing more.
Result-dataclass pattern
Every workflow returns a WorkflowResult subclass carrying:
exit_code: int— what the CLI should return (0 = success, 1 = verdict failure, 2 = usage / IO error)messages: list[str]— info-level output the CLI prints to stdouterrors: list[str]— error-level output the CLI prints to stderr… plus the workflow’s typed payload (validation report, patched-deck path, modal frequencies, etc.)
Exception handling
Workflow functions raise on unrecoverable input errors —
FileNotFoundError when a path doesn’t exist,
pybmodes.io.errors.ParseError (and subclasses) when an
input file is structurally invalid. The CLI catches both and maps
to exit code 2. This keeps the workflow signatures clean for
library callers (a notebook can try / except the typed
exception); the CLI’s translation layer absorbs the messy
argparse.Namespace-to-function-arg shape.
- class BatchResult(exit_code=0, messages=<factory>, errors=<factory>, root=None, out_dir=None, summary_path=None, decks_found=0, decks_failed=0, summary_rows=<factory>)[source]
Bases:
WorkflowResultResult of
run_batch().- Variables:
root (pathlib.Path | None) – Resolved absolute path of the directory walked.
out_dir (pathlib.Path | None) – Resolved absolute path of the output directory (parent of
summary.csv).summary_path (pathlib.Path | None) – Path of the written
summary.csv.decks_found (int) – Number of ElastoDyn main decks discovered under
root.decks_failed (int) – Number of decks that ended at
FAILorERROR(drives the non-zero exit code).summary_rows (list[dict]) – Per-deck summary rows. Each row is the dict written as a CSV line (
filenamerelative toroot,overall_verdict,TwFAM2Sh_ratio,TwSSM2Sh_ratio,n_fail,n_warn).
- Parameters:
- class CampbellWorkflowResult(exit_code=0, messages=<factory>, errors=<factory>, sweep=None, png_path=None, csv_path=None, orders=<factory>)[source]
Bases:
WorkflowResultResult of
run_campbell().- Variables:
sweep (pybmodes.campbell.CampbellResult | None) – The underlying
CampbellResult(frequencies × rpm grid + per-mode MAC tracking).png_path (pathlib.Path | None) – Path of the Campbell-diagram PNG.
Noneonly when figure rendering was skipped (e.g. matplotlib unavailable).csv_path (pathlib.Path | None) – Path of the CSV summary (frequencies + per-step MAC tracking confidence — produced via
CampbellResult.to_csv()).orders (list[int]) – Per-rev excitation orders overlaid on the diagram.
- Parameters:
exit_code (int)
sweep (_CampbellSweepResult | None)
png_path (pathlib.Path | None)
csv_path (pathlib.Path | None)
- csv_path: pathlib.Path | None = None
- png_path: pathlib.Path | None = None
- class ExamplesResult(exit_code=0, messages=<factory>, errors=<factory>, dest=None, copied=<factory>, skipped=<factory>)[source]
Bases:
WorkflowResultResult of
run_examples_copy().- Variables:
dest (pathlib.Path | None) – The destination directory the bundles were copied into.
Noneif the workflow short-circuited before copying anything (e.g. no requested bundles found in the installed package — exit code 2).copied (list[pathlib.Path]) – Absolute paths to each bundle successfully copied (typically
dest / "sample_inputs"and / ordest / "reference_decks").skipped (list[str]) – Bundle names (
"samples"/"decks") that were requested but not found on disk inside the installed package. Each entry produces a warning line inmessages/ aWARN:prefix.
- Parameters:
- class PatchResult(exit_code=0, messages=<factory>, errors=<factory>, main_dat=None, tower_dat=None, blade_dat=None, tower_params=None, blade_params=None, validation=None, tower_patched_text=None, blade_patched_text=None, wrote=<factory>, n_tower_changed=0, n_blade_changed=0)[source]
Bases:
WorkflowResultResult of
run_patch().- Variables:
main_dat (pathlib.Path | None) – Resolved absolute path of the ElastoDyn main
.datfile the workflow operated on.Noneonly when the workflow short- circuited before resolving the main file (currently no such path; reserved).tower_dat (pathlib.Path | None) – Resolved absolute path of the tower side-deck (
TwrFilereferenced from the main).blade_dat (pathlib.Path | None) – Resolved absolute path of the blade-1 side-deck (
BldFile(1)).blade_params (tower_params,) – Fitted polynomial coefficient blocks for the tower and blade sides.
Noneonly if the workflow failed before the fit.validation (pybmodes.elastodyn.validate.ValidationResult | None) – Populated only in
diffmode (the validator is needed for per-block RMS-improvement annotations);Noneotherwise.blade_patched_text (tower_patched_text,) – The full post-patch text of each side-deck, computed without modifying the user’s files. Always populated on success regardless of mode (so callers can compare / diff / persist elsewhere without re-running the workflow).
wrote (list[pathlib.Path]) – Absolute paths of files actually written. Empty in dry-run / diff mode; one entry per side in
output_dirmode; two entries (tower + blade) in in-place mode.n_blade_changed (n_tower_changed,) – Number of changed lines that would (or did) result from the patch, useful for summary print-outs.
- Parameters:
exit_code (int)
main_dat (pathlib.Path | None)
tower_dat (pathlib.Path | None)
blade_dat (pathlib.Path | None)
tower_params (TowerElastoDynParams | None)
blade_params (BladeElastoDynParams | None)
validation (ValidationResult | None)
tower_patched_text (str | None)
blade_patched_text (str | None)
wrote (list[pathlib.Path])
n_tower_changed (int)
n_blade_changed (int)
- blade_dat: pathlib.Path | None = None
- blade_params: BladeElastoDynParams | None = None
- main_dat: pathlib.Path | None = None
- tower_dat: pathlib.Path | None = None
- tower_params: TowerElastoDynParams | None = None
- validation: ValidationResult | None = None
- wrote: list[pathlib.Path]
- class ReportResult(exit_code=0, messages=<factory>, errors=<factory>, out_path=None, tower_modal=None, blade_modal=None, tower_params=None, blade_params=None, validation=None, campbell=None, check_warnings=<factory>)[source]
Bases:
WorkflowResultResult of
run_report().- Variables:
out_path (pathlib.Path | None) – Absolute path of the written report file.
blade_modal (tower_modal,) – Modal-analysis results for the tower and blade sides. The report itself is keyed off
tower_modal(one combined report per deck); both are exposed here so callers can inspect the underlying frequencies / shapes without re-solving.blade_params (tower_params,) – Fitted polynomial coefficient blocks.
validation (ValidationResult | None) – Coefficient validation result;
Nonewhenvalidate=False.campbell (CampbellResult | None) – Campbell sweep result;
Nonewhencampbell=False.check_warnings (list[ModelWarning]) – Pre-solve check findings (tower + blade), captured for the report’s
check_modelsection. Surfaces both sides — the blade-side findings were missing in 0.x and are restored here.
- Parameters:
exit_code (int)
out_path (pathlib.Path | None)
tower_modal (ModalResult | None)
blade_modal (ModalResult | None)
tower_params (TowerElastoDynParams | None)
blade_params (BladeElastoDynParams | None)
validation (ValidationResult | None)
campbell (CampbellResult | None)
check_warnings (list[ModelWarning])
- blade_modal: ModalResult | None = None
- blade_params: BladeElastoDynParams | None = None
- campbell: CampbellResult | None = None
- check_warnings: list[ModelWarning]
- out_path: pathlib.Path | None = None
- tower_modal: ModalResult | None = None
- tower_params: TowerElastoDynParams | None = None
- validation: ValidationResult | None = None
- class ValidateResult(exit_code=0, messages=<factory>, errors=<factory>, validation=None)[source]
Bases:
WorkflowResultResult of
run_validate().- Variables:
validation (pybmodes.elastodyn.validate.ValidationResult | None) – The full validation report — per-block PASS / WARN / FAIL verdicts, file-RMS and pyBmodes-RMS values, and the overall verdict.
Noneonly if the workflow short- circuited before validation could run (currently no such path; reserved for future strict-mode failures).- Parameters:
- validation: ValidationResult | None = None
- class WindioDiscovery(yaml, hydrodyn=None, moordyn=None, elastodyn=None)[source]
Bases:
objectResolved WindIO inputs (ontology + companion decks).
Returned by
discover_windio_inputs().hydrodyn/moordyn/elastodynareNonewhen the companion deck was not auto-discovered under the turbine root — a fully-Nonetriple keeps a floating analysis at “screening preview” rather than industry-grade.
- class WindioResult(exit_code=0, messages=<factory>, errors=<factory>, yaml=None, discovery=None, is_floating=False, model=None, modal=None, blade_params=None, campbell=None, report_path=None, campbell_png_path=None, campbell_csv_path=None, spectra_png_path=None, skipped=<factory>, report_status='complete')[source]
Bases:
WorkflowResultResult of
run_windio().- Variables:
yaml (pathlib.Path | None) – The ontology
.yamlactually loaded.discovery (WindioDiscovery | None) – Resolved companion-deck paths (hydrodyn / moordyn / elastodyn or
Nonefor each leg).is_floating (bool) – Whether the ontology declares a
floating_platformcomponent.model (object | None) – The constructed
Tower(cantilever or coupled-floating).modal (ModalResult | None) – The tower-side modal-solve result.
blade_params (BladeElastoDynParams | None) – Composite-blade fit (
Nonewhen blade extraction was skipped, e.g. ontology has nobladecomponent or the reduction raised).campbell (CampbellResult | None) – Campbell sweep result;
Nonewhencampbell=Falseor the rotor-speed sweep was skipped (no companion ElastoDyn deck).spectra_png_path (report_path, campbell_png_path, campbell_csv_path,) – Resolved paths of every artefact written.
Nonefor plots that were skipped (matplotlib unavailable, CSV-only format, rendering raised).
- Parameters:
exit_code (int)
yaml (pathlib.Path | None)
discovery (WindioDiscovery | None)
is_floating (bool)
model (object | None)
modal (ModalResult | None)
blade_params (BladeElastoDynParams | None)
campbell (CampbellResult | None)
report_path (pathlib.Path | None)
campbell_png_path (pathlib.Path | None)
campbell_csv_path (pathlib.Path | None)
spectra_png_path (pathlib.Path | None)
report_status (str)
- blade_params: BladeElastoDynParams | None = None
- campbell: CampbellResult | None = None
- campbell_csv_path: pathlib.Path | None = None
- campbell_png_path: pathlib.Path | None = None
- discovery: WindioDiscovery | None = None
- modal: ModalResult | None = None
- report_path: pathlib.Path | None = None
- spectra_png_path: pathlib.Path | None = None
- yaml: pathlib.Path | None = None
- class WorkflowResult(exit_code=0, messages=<factory>, errors=<factory>)[source]
Bases:
objectBase class for every
pybmodes.workflows.*return type.- Variables:
exit_code (int) – Process exit code the CLI should return after this workflow runs.
0for success / informational,1for verdict failure,2for usage / IO error (rare on this path — workflows usually raise instead).messages (list[str]) – Info-level lines the CLI prints to stdout. Each entry is a complete line (no trailing newline expected; the CLI adds one). Empty when the workflow has nothing to say.
errors (list[str]) – Error-level lines the CLI prints to stderr. Same line-per- entry convention. Empty when there are no errors.
- Parameters:
- discover_windio_inputs(path)[source]
Resolve a WindIO
.yamlplus any companion OpenFAST decks.pathmay be the ontology.yamlitself or an RWT directory (theIEA-*-RWTlayout). Companion HydroDyn / MoorDyn / ElastoDyn-main decks are auto-discovered so the floating platform uses the industry-grade deck-fallback by default (seepybmodes.models.Tower.from_windio_floating()).Auto-discovery is scoped to a bona-fide turbine root: the directory the user passed, or the nearest ancestor that owns an
OpenFAST/openfasttree, searching up to the enclosing project (.git) boundary so a deeply-nested ontology still resolves its decks without the walk climbing into a broader multi-project workspace. A bare yaml in some scratch directory yields no decks (→ the labelled screening preview). Candidate ontologies are confirmed by a structured YAML parse (_load_windio_doc()), not a substring scan, so a non-WindIO yaml that merely mentionscomponentsis never selected.- Parameters:
- Return type:
- run_batch(root, out_dir, *, kind='elastodyn', validate=False, patch=False, n_modes=10, dry_run=False, backup=True, output_dir=None)[source]
Walk a directory tree of ElastoDyn decks, validate + optionally patch each one, and write a summary CSV.
Library entry point for pybmodes batch.
- Parameters:
root (str or pathlib.Path) – Directory to walk recursively for ElastoDyn main decks.
out_dir (str or pathlib.Path) – Output directory. Created if missing. Receives per-deck validation reports (when
validate=True) and thesummary.csv. Distinct fromoutput_dirbelow —out_dircarries the batch reports,output_dircarries the patched decks.kind ({"elastodyn"}, default "elastodyn") – Which deck flavour to look for. Only ElastoDyn is supported today; passing anything else raises
ValueError.validate (bool, default False) – Write a per-deck
<deckname>_validate.txtcontaining the validation report. The validator itself ALWAYS runs (itsoverall_verdictpopulates the summary CSV); this flag only controls the per-deck text file.patch (bool, default False) – Regenerate the polynomial blocks for each deck and re-validate. Under the default
backup=True(new in 1.8.0 — seerun_patch()), each deck’s tower / blade side-decks are copied to.baksiblings before the in-place rewrite so a botched run is recoverable. Passbackup=Falsefor the legacy “modify in place without safety net” semantics. When combined withvalidate=True, a second per-deck text file named<deckname>_validate_after.txtcaptures the post-patch state.n_modes (int, default 10) – Number of FEM modes to solve when patching.
dry_run (bool, default False) – Patch-mode safety lever — compute the patched coefficients for each deck without writing anything. The summary CSV still carries the BEFORE-patch verdict (source files unchanged); the per-deck messages report what would have changed. Mutually exclusive with
output_dir.backup (bool, default
True) – Patch-mode safety lever — copy each tower / blade side-deck to a.baksibling before overwriting in place. Default changed fromFalsetoTruein 1.8.0:pybmodes batch --patchsweeps a directory tree, so a single bad run can mutate decks the user didn’t realise discovery picked up. Local-only safety artefact;*.bakis gitignored. Ignored indry_runoroutput_dirmode (those write nothing or write elsewhere).output_dir (str, pathlib.Path, or None, default None) – Patch-mode safety lever — write the patched copies of each deck’s tower / blade side-decks into
output_dir / <deck stem>/instead of overwriting the originals. The source tree is untouched. Mutually exclusive withdry_run.
- Returns:
Carries the discovered-deck count, per-deck summary rows, the summary CSV path, and the failed-deck count.
exit_codeis0on all-good and1when any deck failed or errored.- Return type:
- Raises:
ValueError – When
kindis anything other than"elastodyn", or whendry_runis combined withoutput_dir(those modes are mutually exclusive — same convention asrun_patch()).FileNotFoundError – When
rootdoes not exist or is not a directory.
- run_campbell(input_path, *, max_rpm, n_steps=16, orders='1,2,3,6,9', n_blade_modes=4, n_tower_modes=4, tower_input=None, rated_rpm=None, out_path=None)[source]
Run a rotor-speed sweep and write the Campbell diagram + CSV.
Library entry point for pybmodes campbell. Delegates the sweep itself to
pybmodes.campbell.campbell_sweep()and renders the diagram viapybmodes.campbell.plot_campbell().- Parameters:
input_path (str or pathlib.Path) – Source model: a
.bmideck or an ElastoDyn main.dat.max_rpm (float) – Upper end of the rotor-speed sweep, in rpm. Must be > 0.
n_steps (int, default 16) – Number of rotor-speed points in the sweep (including 0 and
max_rpm). Must be >= 2.orders (str or list[int], default
"1,2,3,6,9") – Per-rev excitation orders to overlay. Strings are parsed as a comma-separated list of integers (this matches the CLI--ordersflag); lists are used as-is.n_blade_modes (int, defaults 4, 4) – Modes to track per side across the sweep.
n_tower_modes (int, defaults 4, 4) – Modes to track per side across the sweep.
tower_input (str, pathlib.Path, or None) – Optional tower override. Mirrors the CLI
--towerflag.rated_rpm (float or None) – Operating rotor speed (rpm) drawn as a vertical reference line.
out_path (str, pathlib.Path, or None) – Output PNG path.
None→<input_stem>_campbell.pngalongsideinput_path. The CSV is written next to the PNG with a.csvsuffix.
- Returns:
Carries the resolved PNG and CSV paths, the underlying
CampbellResult, and the parsed excitation orders.exit_codeis0on success.- Return type:
- Raises:
FileNotFoundError – When
input_pathdoes not exist.ValueError – When
orderscannot be parsed,max_rpm <= 0, orn_steps < 2.
- run_examples_copy(dest, *, kind='all', force=False)[source]
Copy bundled example trees out of the installed pyBmodes package.
Library entry point for pybmodes examples --copy DIR.
- Parameters:
dest (str or pathlib.Path) – Destination directory. Created if missing. Existing sub-bundles in this directory are an error unless
force=True.kind ({"all", "samples", "decks"}, default "all") – Which bundle(s) to copy.
"all"copies bothsample_inputs/andreference_decks/;"samples"or"decks"selects only that one.force (bool, default False) – Overwrite existing destination sub-directories rather than erroring. Useful in CI scripts that re-vendor on every run.
- Returns:
Carries the destination, the list of copied bundle paths, and any skipped bundle names. Exit code is
0on success,2if no requested bundles were found in the installed package or if a destination existed withoutforce=True.- Return type:
- run_patch(dat_path, *, n_modes=10, backup=False, output_dir=None, dry_run=False, diff=False)[source]
Regenerate the tower + blade polynomial blocks of an ElastoDyn deck.
Library entry point for pybmodes patch. Builds the cantilever-basis tower model and the rotating-blade model from the deck’s structural inputs, fits 6th-order polynomial mode-shape coefficients, and either writes them back to the deck or returns them for inspection.
- Parameters:
dat_path (str or pathlib.Path) – ElastoDyn main
.datfile. The tower side-deck (TwrFile) and blade-1 side-deck (BldFile(1)) are resolved relative to it.n_modes (int, default 10) – Number of FEM modes to solve before extracting the polynomial blocks. The default matches the CLI default.
backup (bool, default False) – In-place mode only: copy each side-deck to a
.baksibling before overwriting.output_dir (str, pathlib.Path, or None, default None) – Write patched copies into this directory instead of overwriting the originals. Mutually exclusive with
dry_run/diff.dry_run (bool, default False) – Compute the patched text without writing anything. Mutually exclusive with
output_dir.diff (bool, default False) – Compute the patched text without writing anything, AND emit a PR-ready coefficient-only diff into
PatchResult.messageswith per-block RMS-improvement annotations. Implies dry-run.
- Returns:
Carries the resolved side-deck paths, the fitted parameters, the patched text for both sides, and (in
diffmode) theValidationResultused to derive the RMS-improvement ratios.exit_codeis0on success.- Return type:
- Raises:
FileNotFoundError – When
dat_path, the tower side-deck, or the blade side-deck does not exist.ValueError – When
output_diris combined withdry_run/diff(those modes write nothing, so a destination is meaningless).
- run_report(dat_path, out_path, *, n_modes=10, format='md', validate=True, campbell=False, max_rpm=15.0, n_steps=16, n_blade_modes=4, n_tower_modes=4)[source]
Run modal analysis + (optional) validation + (optional) Campbell on one ElastoDyn deck and write a combined report.
Library entry point for pybmodes report.
- Parameters:
dat_path (str or pathlib.Path) – ElastoDyn main
.datfile.out_path (str or pathlib.Path) – Destination report file. Parent directory is created if missing.
n_modes (int, default 10) – Number of FEM modes to solve per side.
format ({"md", "html", "csv"}, default "md") – Report format.
validate (bool, default True) – Run
validate_dat_coefficients()and attach the result to the report’s validation section.campbell (bool, default False) – Run a rotor-speed sweep from 0 to
max_rpminn_stepspoints and attach the result to the report’s Campbell section.max_rpm (float) – Campbell-sweep parameters. Ignored when
campbell=False.n_steps (int) – Campbell-sweep parameters. Ignored when
campbell=False.n_blade_modes (int) – Campbell-sweep parameters. Ignored when
campbell=False.n_tower_modes (int) – Campbell-sweep parameters. Ignored when
campbell=False.
- Returns:
Carries the rendered file path plus every intermediate artefact (modal results, fitted params, validation, Campbell sweep, pre-solve warnings) so callers can introspect without re-running the workflow.
- Return type:
- Raises:
FileNotFoundError – When
dat_pathdoes not exist.
- run_validate(dat_path)[source]
Validate the coefficient blocks in an ElastoDyn
.datdeck.Library entry point for the pybmodes validate subcommand. Parses the deck’s polynomial blocks, re-fits each block from the structural inputs in the same file, scores file-RMS against pyBmodes-RMS, and returns a structured verdict.
- Parameters:
dat_path (str or pathlib.Path) – Path to an ElastoDyn main
.datfile. Resolved to an absolute path before validation.- Returns:
Carries the full
ValidationResultplusexit_codemapping (0for PASS / WARN,1for FAIL) and the per-block printable lines inmessages.- Return type:
- Raises:
FileNotFoundError – When
dat_pathdoes not exist or is not a regular file. Callers (CLI or library) should treat this as a usage / IO error rather than a verdict failure.
Examples
From a notebook:
from pybmodes.workflows import run_validate res = run_validate("NRELOffshrBsline5MW_Onshore_ElastoDyn.dat") if res.validation.overall == "FAIL": print("polynomial blocks are stale — run patch") for line in res.messages: print(line)
- run_windio(input_path, *, out_path=None, format='md', n_modes=12, water_depth=None, campbell=False, max_rpm=12.0, min_rpm=0.0, rated_rpm=None, n_steps=16, n_blade_modes=4, n_tower_modes=4, on_skip='fail-on-data')[source]
One-click WindIO ontology workflow.
Library entry point for pybmodes windio. Resolves the ontology (and any companion OpenFAST decks scoped to the turbine root), solves the blade + tower (or coupled floating tower + platform), optionally runs a Campbell sweep against the discovered ElastoDyn deck, and writes a bundled report.
- Parameters:
input_path (str or pathlib.Path) – WindIO ontology
.yaml, or an RWT directory to discover it in.out_path (str, pathlib.Path, or None) – Destination report file.
None→<yaml-stem>_windio_report.<format>in the current directory.format ({"md", "html", "csv"}, default "md") – Report format.
n_modes (int, default 12) – Number of FEM modes to extract.
water_depth (float or None) – Site water depth (m); only used by the yaml-only floating screening preview when no MoorDyn deck is found.
campbell (bool, default False) – Run a rotor-speed Campbell sweep against the discovered companion ElastoDyn deck. Skipped (with a message) if no ElastoDyn deck was discovered; the
on_skippolicy below controls whether that counts as a failure.max_rpm (float) – Rotor-speed sweep bounds + (optional) rated rpm overlay on the environmental-spectra plot for floating cases.
min_rpm (float) – Rotor-speed sweep bounds + (optional) rated rpm overlay on the environmental-spectra plot for floating cases.
rated_rpm (float | None) – Rotor-speed sweep bounds + (optional) rated rpm overlay on the environmental-spectra plot for floating cases.
n_steps (int) – Campbell-sweep parameters.
n_blade_modes (int) – Campbell-sweep parameters.
n_tower_modes (int) – Campbell-sweep parameters.
on_skip ({“warn”, “fail-on-data”, “fail”}, default
"fail-on-data") –How to handle workflow skips. Three classes of skip exist internally:
data — a computational result is missing (blade composite reduction raised). Under
"fail-on-data"(the new default in 1.8.0) and"fail"these toggleexit_code = 1so library callers / scripted automation notice the missing engineering output instead of silently publishing an incomplete report.presentation — the data was computed but figure rendering failed (Campbell plot, environmental-spectra plot). Under
"warn"and"fail-on-data"these only warn (the CSV / data is still on disk); under"fail"they toggleexit_code = 1.input — an output was requested but its companion input wasn’t discovered (e.g.
campbell=Truewith no ElastoDyn deck under the turbine root). Under"warn"and"fail-on-data"warns; under"fail"fails.
Pass
"warn"to recover the pre-1.8.0 permissive behaviour (every skip just messages, exit_code stays 0).WindioResult.skippedlists every skip regardless of policy.
- Returns:
Carries the loaded yaml path, the auto-discovery result, the solved model + modal result, optional blade fit, optional Campbell sweep, and every written-artefact path.
exit_codeis0on success,1when a skip toggled the failure gate viaon_skip.- Return type:
- Raises:
FileNotFoundError – When
input_pathdoes not resolve to a yaml or a directory containing one.
Shared base dataclass for pybmodes.workflows.* results.
Each workflow function returns a WorkflowResult subclass.
The base carries the three fields the CLI needs to translate a
workflow outcome into terminal output + an exit code; subclasses
add the workflow-specific typed payload.
Exit-code convention (matches the existing CLI semantics so any
downstream caller scripting pybmodes ... keeps working):
0— success. Includes “WARN-verdict but tolerated” cases for validate; warnings are informational, not failures.1— verdict failure (e.g.validatereturned FAIL,patchran but a block ended at FAIL after re-fit).2— usage / IO error (missing input, malformed deck, conflicting flags). Workflows raise rather than return for these; the CLI catches and translates.
- class WorkflowResult(exit_code=0, messages=<factory>, errors=<factory>)[source]
Bases:
objectBase class for every
pybmodes.workflows.*return type.- Variables:
exit_code (int) – Process exit code the CLI should return after this workflow runs.
0for success / informational,1for verdict failure,2for usage / IO error (rare on this path — workflows usually raise instead).messages (list[str]) – Info-level lines the CLI prints to stdout. Each entry is a complete line (no trailing newline expected; the CLI adds one). Empty when the workflow has nothing to say.
errors (list[str]) – Error-level lines the CLI prints to stderr. Same line-per- entry convention. Empty when there are no errors.
- Parameters:
pybmodes validate workflow as a typed library function.
Wraps pybmodes.elastodyn.validate_dat_coefficients() with a
ValidateResult carrying the validation report plus
CLI-mapping fields (exit code, messages, errors).
- class ValidateResult(exit_code=0, messages=<factory>, errors=<factory>, validation=None)[source]
Bases:
WorkflowResultResult of
run_validate().- Variables:
validation (pybmodes.elastodyn.validate.ValidationResult | None) – The full validation report — per-block PASS / WARN / FAIL verdicts, file-RMS and pyBmodes-RMS values, and the overall verdict.
Noneonly if the workflow short- circuited before validation could run (currently no such path; reserved for future strict-mode failures).- Parameters:
- validation: ValidationResult | None = None
- run_validate(dat_path)[source]
Validate the coefficient blocks in an ElastoDyn
.datdeck.Library entry point for the pybmodes validate subcommand. Parses the deck’s polynomial blocks, re-fits each block from the structural inputs in the same file, scores file-RMS against pyBmodes-RMS, and returns a structured verdict.
- Parameters:
dat_path (str or pathlib.Path) – Path to an ElastoDyn main
.datfile. Resolved to an absolute path before validation.- Returns:
Carries the full
ValidationResultplusexit_codemapping (0for PASS / WARN,1for FAIL) and the per-block printable lines inmessages.- Return type:
- Raises:
FileNotFoundError – When
dat_pathdoes not exist or is not a regular file. Callers (CLI or library) should treat this as a usage / IO error rather than a verdict failure.
Examples
From a notebook:
from pybmodes.workflows import run_validate res = run_validate("NRELOffshrBsline5MW_Onshore_ElastoDyn.dat") if res.validation.overall == "FAIL": print("polynomial blocks are stale — run patch") for line in res.messages: print(line)
pybmodes examples --copy workflow as a typed library function.
Vendors the bundled sample_inputs/ and / or reference_decks/
trees out of the installed pybmodes._examples package into a
user-supplied destination directory. Works for wheel installs (where
the bundles are package-data) and editable source installs (where
they’re a literal src/pybmodes/_examples/ directory) — the same
resolver locates pybmodes.__file__ and reads from the sibling
_examples folder.
- class ExamplesResult(exit_code=0, messages=<factory>, errors=<factory>, dest=None, copied=<factory>, skipped=<factory>)[source]
Bases:
WorkflowResultResult of
run_examples_copy().- Variables:
dest (pathlib.Path | None) – The destination directory the bundles were copied into.
Noneif the workflow short-circuited before copying anything (e.g. no requested bundles found in the installed package — exit code 2).copied (list[pathlib.Path]) – Absolute paths to each bundle successfully copied (typically
dest / "sample_inputs"and / ordest / "reference_decks").skipped (list[str]) – Bundle names (
"samples"/"decks") that were requested but not found on disk inside the installed package. Each entry produces a warning line inmessages/ aWARN:prefix.
- Parameters:
- run_examples_copy(dest, *, kind='all', force=False)[source]
Copy bundled example trees out of the installed pyBmodes package.
Library entry point for pybmodes examples --copy DIR.
- Parameters:
dest (str or pathlib.Path) – Destination directory. Created if missing. Existing sub-bundles in this directory are an error unless
force=True.kind ({"all", "samples", "decks"}, default "all") – Which bundle(s) to copy.
"all"copies bothsample_inputs/andreference_decks/;"samples"or"decks"selects only that one.force (bool, default False) – Overwrite existing destination sub-directories rather than erroring. Useful in CI scripts that re-vendor on every run.
- Returns:
Carries the destination, the list of copied bundle paths, and any skipped bundle names. Exit code is
0on success,2if no requested bundles were found in the installed package or if a destination existed withoutforce=True.- Return type:
pybmodes patch workflow as a typed library function.
Regenerates the tower + blade polynomial coefficient blocks in an
ElastoDyn .dat deck from the structural-property inputs.
Five mutually-supportive output modes, exposed both via the CLI and as
keyword arguments of run_patch():
default in-place — overwrites the user’s
.datfiles.backup=True— same as default plus.bakcopies first.output_dir=DIR— writesDIR/<filename>.datinstead of in-place, leaving the originals untouched.dry_run=True— computes the patched text but writes nothing.diff=True— implies dry-run, additionally produces a PR-ready coefficient-only diff with per-block RMS-improvement ratios.
The compute / write split is deliberate: each side’s patched text is generated into a temporary file regardless of mode, so dry-run and diff share a code path with the real writes and can’t drift.
- class PatchResult(exit_code=0, messages=<factory>, errors=<factory>, main_dat=None, tower_dat=None, blade_dat=None, tower_params=None, blade_params=None, validation=None, tower_patched_text=None, blade_patched_text=None, wrote=<factory>, n_tower_changed=0, n_blade_changed=0)[source]
Bases:
WorkflowResultResult of
run_patch().- Variables:
main_dat (pathlib.Path | None) – Resolved absolute path of the ElastoDyn main
.datfile the workflow operated on.Noneonly when the workflow short- circuited before resolving the main file (currently no such path; reserved).tower_dat (pathlib.Path | None) – Resolved absolute path of the tower side-deck (
TwrFilereferenced from the main).blade_dat (pathlib.Path | None) – Resolved absolute path of the blade-1 side-deck (
BldFile(1)).blade_params (tower_params,) – Fitted polynomial coefficient blocks for the tower and blade sides.
Noneonly if the workflow failed before the fit.validation (pybmodes.elastodyn.validate.ValidationResult | None) – Populated only in
diffmode (the validator is needed for per-block RMS-improvement annotations);Noneotherwise.blade_patched_text (tower_patched_text,) – The full post-patch text of each side-deck, computed without modifying the user’s files. Always populated on success regardless of mode (so callers can compare / diff / persist elsewhere without re-running the workflow).
wrote (list[pathlib.Path]) – Absolute paths of files actually written. Empty in dry-run / diff mode; one entry per side in
output_dirmode; two entries (tower + blade) in in-place mode.n_blade_changed (n_tower_changed,) – Number of changed lines that would (or did) result from the patch, useful for summary print-outs.
- Parameters:
exit_code (int)
main_dat (pathlib.Path | None)
tower_dat (pathlib.Path | None)
blade_dat (pathlib.Path | None)
tower_params (TowerElastoDynParams | None)
blade_params (BladeElastoDynParams | None)
validation (ValidationResult | None)
tower_patched_text (str | None)
blade_patched_text (str | None)
wrote (list[pathlib.Path])
n_tower_changed (int)
n_blade_changed (int)
- blade_dat: pathlib.Path | None = None
- blade_params: BladeElastoDynParams | None = None
- main_dat: pathlib.Path | None = None
- tower_dat: pathlib.Path | None = None
- tower_params: TowerElastoDynParams | None = None
- validation: ValidationResult | None = None
- wrote: list[pathlib.Path]
- run_patch(dat_path, *, n_modes=10, backup=False, output_dir=None, dry_run=False, diff=False)[source]
Regenerate the tower + blade polynomial blocks of an ElastoDyn deck.
Library entry point for pybmodes patch. Builds the cantilever-basis tower model and the rotating-blade model from the deck’s structural inputs, fits 6th-order polynomial mode-shape coefficients, and either writes them back to the deck or returns them for inspection.
- Parameters:
dat_path (str or pathlib.Path) – ElastoDyn main
.datfile. The tower side-deck (TwrFile) and blade-1 side-deck (BldFile(1)) are resolved relative to it.n_modes (int, default 10) – Number of FEM modes to solve before extracting the polynomial blocks. The default matches the CLI default.
backup (bool, default False) – In-place mode only: copy each side-deck to a
.baksibling before overwriting.output_dir (str, pathlib.Path, or None, default None) – Write patched copies into this directory instead of overwriting the originals. Mutually exclusive with
dry_run/diff.dry_run (bool, default False) – Compute the patched text without writing anything. Mutually exclusive with
output_dir.diff (bool, default False) – Compute the patched text without writing anything, AND emit a PR-ready coefficient-only diff into
PatchResult.messageswith per-block RMS-improvement annotations. Implies dry-run.
- Returns:
Carries the resolved side-deck paths, the fitted parameters, the patched text for both sides, and (in
diffmode) theValidationResultused to derive the RMS-improvement ratios.exit_codeis0on success.- Return type:
- Raises:
FileNotFoundError – When
dat_path, the tower side-deck, or the blade side-deck does not exist.ValueError – When
output_diris combined withdry_run/diff(those modes write nothing, so a destination is meaningless).
pybmodes report workflow as a typed library function.
Runs the full modal-analysis pipeline on one ElastoDyn deck — tower +
blade FEM solves, polynomial fits, optional coefficient validation,
optional Campbell sweep — and writes a single combined Markdown / HTML
/ CSV report. The report module itself
(pybmodes.report.generate_report()) writes the file; this workflow
is responsible for orchestrating the inputs.
- class ReportResult(exit_code=0, messages=<factory>, errors=<factory>, out_path=None, tower_modal=None, blade_modal=None, tower_params=None, blade_params=None, validation=None, campbell=None, check_warnings=<factory>)[source]
Bases:
WorkflowResultResult of
run_report().- Variables:
out_path (pathlib.Path | None) – Absolute path of the written report file.
blade_modal (tower_modal,) – Modal-analysis results for the tower and blade sides. The report itself is keyed off
tower_modal(one combined report per deck); both are exposed here so callers can inspect the underlying frequencies / shapes without re-solving.blade_params (tower_params,) – Fitted polynomial coefficient blocks.
validation (ValidationResult | None) – Coefficient validation result;
Nonewhenvalidate=False.campbell (CampbellResult | None) – Campbell sweep result;
Nonewhencampbell=False.check_warnings (list[ModelWarning]) – Pre-solve check findings (tower + blade), captured for the report’s
check_modelsection. Surfaces both sides — the blade-side findings were missing in 0.x and are restored here.
- Parameters:
exit_code (int)
out_path (pathlib.Path | None)
tower_modal (ModalResult | None)
blade_modal (ModalResult | None)
tower_params (TowerElastoDynParams | None)
blade_params (BladeElastoDynParams | None)
validation (ValidationResult | None)
campbell (CampbellResult | None)
check_warnings (list[ModelWarning])
- blade_modal: ModalResult | None = None
- blade_params: BladeElastoDynParams | None = None
- campbell: CampbellResult | None = None
- check_warnings: list[ModelWarning]
- out_path: pathlib.Path | None = None
- tower_modal: ModalResult | None = None
- tower_params: TowerElastoDynParams | None = None
- validation: ValidationResult | None = None
- run_report(dat_path, out_path, *, n_modes=10, format='md', validate=True, campbell=False, max_rpm=15.0, n_steps=16, n_blade_modes=4, n_tower_modes=4)[source]
Run modal analysis + (optional) validation + (optional) Campbell on one ElastoDyn deck and write a combined report.
Library entry point for pybmodes report.
- Parameters:
dat_path (str or pathlib.Path) – ElastoDyn main
.datfile.out_path (str or pathlib.Path) – Destination report file. Parent directory is created if missing.
n_modes (int, default 10) – Number of FEM modes to solve per side.
format ({"md", "html", "csv"}, default "md") – Report format.
validate (bool, default True) – Run
validate_dat_coefficients()and attach the result to the report’s validation section.campbell (bool, default False) – Run a rotor-speed sweep from 0 to
max_rpminn_stepspoints and attach the result to the report’s Campbell section.max_rpm (float) – Campbell-sweep parameters. Ignored when
campbell=False.n_steps (int) – Campbell-sweep parameters. Ignored when
campbell=False.n_blade_modes (int) – Campbell-sweep parameters. Ignored when
campbell=False.n_tower_modes (int) – Campbell-sweep parameters. Ignored when
campbell=False.
- Returns:
Carries the rendered file path plus every intermediate artefact (modal results, fitted params, validation, Campbell sweep, pre-solve warnings) so callers can introspect without re-running the workflow.
- Return type:
- Raises:
FileNotFoundError – When
dat_pathdoes not exist.
pybmodes batch workflow as a typed library function.
Walks a directory tree for ElastoDyn main .dat files, runs
validate + optional patch on each deck, and writes a summary CSV plus
optional per-deck validation reports.
Exit-code policy mirrors the original CLI:
0— every deck reaches a non-FAIL overall verdict (PASS or WARN).1— at least one deck remained at FAIL after patching, or errored during parse / fit. The summary CSV still lists every deck the workflow attempted.
- class BatchResult(exit_code=0, messages=<factory>, errors=<factory>, root=None, out_dir=None, summary_path=None, decks_found=0, decks_failed=0, summary_rows=<factory>)[source]
Bases:
WorkflowResultResult of
run_batch().- Variables:
root (pathlib.Path | None) – Resolved absolute path of the directory walked.
out_dir (pathlib.Path | None) – Resolved absolute path of the output directory (parent of
summary.csv).summary_path (pathlib.Path | None) – Path of the written
summary.csv.decks_found (int) – Number of ElastoDyn main decks discovered under
root.decks_failed (int) – Number of decks that ended at
FAILorERROR(drives the non-zero exit code).summary_rows (list[dict]) – Per-deck summary rows. Each row is the dict written as a CSV line (
filenamerelative toroot,overall_verdict,TwFAM2Sh_ratio,TwSSM2Sh_ratio,n_fail,n_warn).
- Parameters:
- find_elastodyn_main_dats(root)[source]
Walk
rootrecursively and return every file that looks like an ElastoDyn main input.Two-stage filter:
Name heuristic: must contain
ElastoDyn(case-insensitive) and must NOT contain any auxiliary-file token (_Tower,_Blade,_SubDyn, etc.).Parse confirmation: must round-trip through
pybmodes.io.elastodyn_reader.read_elastodyn_main()and carry a non-emptyTwrFilereference. Files that fail to parse are silently skipped — the batch workflow can’t act on them anyway.
- run_batch(root, out_dir, *, kind='elastodyn', validate=False, patch=False, n_modes=10, dry_run=False, backup=True, output_dir=None)[source]
Walk a directory tree of ElastoDyn decks, validate + optionally patch each one, and write a summary CSV.
Library entry point for pybmodes batch.
- Parameters:
root (str or pathlib.Path) – Directory to walk recursively for ElastoDyn main decks.
out_dir (str or pathlib.Path) – Output directory. Created if missing. Receives per-deck validation reports (when
validate=True) and thesummary.csv. Distinct fromoutput_dirbelow —out_dircarries the batch reports,output_dircarries the patched decks.kind ({"elastodyn"}, default "elastodyn") – Which deck flavour to look for. Only ElastoDyn is supported today; passing anything else raises
ValueError.validate (bool, default False) – Write a per-deck
<deckname>_validate.txtcontaining the validation report. The validator itself ALWAYS runs (itsoverall_verdictpopulates the summary CSV); this flag only controls the per-deck text file.patch (bool, default False) – Regenerate the polynomial blocks for each deck and re-validate. Under the default
backup=True(new in 1.8.0 — seerun_patch()), each deck’s tower / blade side-decks are copied to.baksiblings before the in-place rewrite so a botched run is recoverable. Passbackup=Falsefor the legacy “modify in place without safety net” semantics. When combined withvalidate=True, a second per-deck text file named<deckname>_validate_after.txtcaptures the post-patch state.n_modes (int, default 10) – Number of FEM modes to solve when patching.
dry_run (bool, default False) – Patch-mode safety lever — compute the patched coefficients for each deck without writing anything. The summary CSV still carries the BEFORE-patch verdict (source files unchanged); the per-deck messages report what would have changed. Mutually exclusive with
output_dir.backup (bool, default
True) – Patch-mode safety lever — copy each tower / blade side-deck to a.baksibling before overwriting in place. Default changed fromFalsetoTruein 1.8.0:pybmodes batch --patchsweeps a directory tree, so a single bad run can mutate decks the user didn’t realise discovery picked up. Local-only safety artefact;*.bakis gitignored. Ignored indry_runoroutput_dirmode (those write nothing or write elsewhere).output_dir (str, pathlib.Path, or None, default None) – Patch-mode safety lever — write the patched copies of each deck’s tower / blade side-decks into
output_dir / <deck stem>/instead of overwriting the originals. The source tree is untouched. Mutually exclusive withdry_run.
- Returns:
Carries the discovered-deck count, per-deck summary rows, the summary CSV path, and the failed-deck count.
exit_codeis0on all-good and1when any deck failed or errored.- Return type:
- Raises:
ValueError – When
kindis anything other than"elastodyn", or whendry_runis combined withoutput_dir(those modes are mutually exclusive — same convention asrun_patch()).FileNotFoundError – When
rootdoes not exist or is not a directory.
pybmodes campbell workflow as a typed library function.
Sweeps a rotor-speed grid, writes the resulting CampbellResult
to CSV alongside a Campbell-diagram PNG (with per-rev excitation
orders overlaid), and returns a typed result carrying both paths.
- class CampbellWorkflowResult(exit_code=0, messages=<factory>, errors=<factory>, sweep=None, png_path=None, csv_path=None, orders=<factory>)[source]
Bases:
WorkflowResultResult of
run_campbell().- Variables:
sweep (pybmodes.campbell.CampbellResult | None) – The underlying
CampbellResult(frequencies × rpm grid + per-mode MAC tracking).png_path (pathlib.Path | None) – Path of the Campbell-diagram PNG.
Noneonly when figure rendering was skipped (e.g. matplotlib unavailable).csv_path (pathlib.Path | None) – Path of the CSV summary (frequencies + per-step MAC tracking confidence — produced via
CampbellResult.to_csv()).orders (list[int]) – Per-rev excitation orders overlaid on the diagram.
- Parameters:
exit_code (int)
sweep (_CampbellSweepResult | None)
png_path (pathlib.Path | None)
csv_path (pathlib.Path | None)
- csv_path: pathlib.Path | None = None
- png_path: pathlib.Path | None = None
- run_campbell(input_path, *, max_rpm, n_steps=16, orders='1,2,3,6,9', n_blade_modes=4, n_tower_modes=4, tower_input=None, rated_rpm=None, out_path=None)[source]
Run a rotor-speed sweep and write the Campbell diagram + CSV.
Library entry point for pybmodes campbell. Delegates the sweep itself to
pybmodes.campbell.campbell_sweep()and renders the diagram viapybmodes.campbell.plot_campbell().- Parameters:
input_path (str or pathlib.Path) – Source model: a
.bmideck or an ElastoDyn main.dat.max_rpm (float) – Upper end of the rotor-speed sweep, in rpm. Must be > 0.
n_steps (int, default 16) – Number of rotor-speed points in the sweep (including 0 and
max_rpm). Must be >= 2.orders (str or list[int], default
"1,2,3,6,9") – Per-rev excitation orders to overlay. Strings are parsed as a comma-separated list of integers (this matches the CLI--ordersflag); lists are used as-is.n_blade_modes (int, defaults 4, 4) – Modes to track per side across the sweep.
n_tower_modes (int, defaults 4, 4) – Modes to track per side across the sweep.
tower_input (str, pathlib.Path, or None) – Optional tower override. Mirrors the CLI
--towerflag.rated_rpm (float or None) – Operating rotor speed (rpm) drawn as a vertical reference line.
out_path (str, pathlib.Path, or None) – Output PNG path.
None→<input_stem>_campbell.pngalongsideinput_path. The CSV is written next to the PNG with a.csvsuffix.
- Returns:
Carries the resolved PNG and CSV paths, the underlying
CampbellResult, and the parsed excitation orders.exit_codeis0on success.- Return type:
- Raises:
FileNotFoundError – When
input_pathdoes not exist.ValueError – When
orderscannot be parsed,max_rpm <= 0, orn_steps < 2.
pybmodes windio workflow as a typed library function.
One-click WISDEM / WindIO ontology entry point:
Resolve the ontology
.yaml(or a turbine-root directory) and discover companion OpenFAST decks scoped to that root.Solve the composite-layup blade.
Solve the tubular tower (fixed cantilever) or the coupled floating tower + platform (industry-grade when the decks are present, screening preview otherwise).
Optionally run a Campbell sweep against the discovered ElastoDyn deck and overlay the platform rigid-body modes.
Optionally emit an environmental-loading frequency-placement plot (floating cases).
Render a bundled report (MD / HTML / CSV).
- class WindioDiscovery(yaml, hydrodyn=None, moordyn=None, elastodyn=None)[source]
Bases:
objectResolved WindIO inputs (ontology + companion decks).
Returned by
discover_windio_inputs().hydrodyn/moordyn/elastodynareNonewhen the companion deck was not auto-discovered under the turbine root — a fully-Nonetriple keeps a floating analysis at “screening preview” rather than industry-grade.
- class WindioResult(exit_code=0, messages=<factory>, errors=<factory>, yaml=None, discovery=None, is_floating=False, model=None, modal=None, blade_params=None, campbell=None, report_path=None, campbell_png_path=None, campbell_csv_path=None, spectra_png_path=None, skipped=<factory>, report_status='complete')[source]
Bases:
WorkflowResultResult of
run_windio().- Variables:
yaml (pathlib.Path | None) – The ontology
.yamlactually loaded.discovery (WindioDiscovery | None) – Resolved companion-deck paths (hydrodyn / moordyn / elastodyn or
Nonefor each leg).is_floating (bool) – Whether the ontology declares a
floating_platformcomponent.model (object | None) – The constructed
Tower(cantilever or coupled-floating).modal (ModalResult | None) – The tower-side modal-solve result.
blade_params (BladeElastoDynParams | None) – Composite-blade fit (
Nonewhen blade extraction was skipped, e.g. ontology has nobladecomponent or the reduction raised).campbell (CampbellResult | None) – Campbell sweep result;
Nonewhencampbell=Falseor the rotor-speed sweep was skipped (no companion ElastoDyn deck).spectra_png_path (report_path, campbell_png_path, campbell_csv_path,) – Resolved paths of every artefact written.
Nonefor plots that were skipped (matplotlib unavailable, CSV-only format, rendering raised).
- Parameters:
exit_code (int)
yaml (pathlib.Path | None)
discovery (WindioDiscovery | None)
is_floating (bool)
model (object | None)
modal (ModalResult | None)
blade_params (BladeElastoDynParams | None)
campbell (CampbellResult | None)
report_path (pathlib.Path | None)
campbell_png_path (pathlib.Path | None)
campbell_csv_path (pathlib.Path | None)
spectra_png_path (pathlib.Path | None)
report_status (str)
- blade_params: BladeElastoDynParams | None = None
- campbell: CampbellResult | None = None
- campbell_csv_path: pathlib.Path | None = None
- campbell_png_path: pathlib.Path | None = None
- discovery: WindioDiscovery | None = None
- modal: ModalResult | None = None
- report_path: pathlib.Path | None = None
- spectra_png_path: pathlib.Path | None = None
- yaml: pathlib.Path | None = None
- discover_windio_inputs(path)[source]
Resolve a WindIO
.yamlplus any companion OpenFAST decks.pathmay be the ontology.yamlitself or an RWT directory (theIEA-*-RWTlayout). Companion HydroDyn / MoorDyn / ElastoDyn-main decks are auto-discovered so the floating platform uses the industry-grade deck-fallback by default (seepybmodes.models.Tower.from_windio_floating()).Auto-discovery is scoped to a bona-fide turbine root: the directory the user passed, or the nearest ancestor that owns an
OpenFAST/openfasttree, searching up to the enclosing project (.git) boundary so a deeply-nested ontology still resolves its decks without the walk climbing into a broader multi-project workspace. A bare yaml in some scratch directory yields no decks (→ the labelled screening preview). Candidate ontologies are confirmed by a structured YAML parse (_load_windio_doc()), not a substring scan, so a non-WindIO yaml that merely mentionscomponentsis never selected.- Parameters:
- Return type:
- run_windio(input_path, *, out_path=None, format='md', n_modes=12, water_depth=None, campbell=False, max_rpm=12.0, min_rpm=0.0, rated_rpm=None, n_steps=16, n_blade_modes=4, n_tower_modes=4, on_skip='fail-on-data')[source]
One-click WindIO ontology workflow.
Library entry point for pybmodes windio. Resolves the ontology (and any companion OpenFAST decks scoped to the turbine root), solves the blade + tower (or coupled floating tower + platform), optionally runs a Campbell sweep against the discovered ElastoDyn deck, and writes a bundled report.
- Parameters:
input_path (str or pathlib.Path) – WindIO ontology
.yaml, or an RWT directory to discover it in.out_path (str, pathlib.Path, or None) – Destination report file.
None→<yaml-stem>_windio_report.<format>in the current directory.format ({"md", "html", "csv"}, default "md") – Report format.
n_modes (int, default 12) – Number of FEM modes to extract.
water_depth (float or None) – Site water depth (m); only used by the yaml-only floating screening preview when no MoorDyn deck is found.
campbell (bool, default False) – Run a rotor-speed Campbell sweep against the discovered companion ElastoDyn deck. Skipped (with a message) if no ElastoDyn deck was discovered; the
on_skippolicy below controls whether that counts as a failure.max_rpm (float) – Rotor-speed sweep bounds + (optional) rated rpm overlay on the environmental-spectra plot for floating cases.
min_rpm (float) – Rotor-speed sweep bounds + (optional) rated rpm overlay on the environmental-spectra plot for floating cases.
rated_rpm (float | None) – Rotor-speed sweep bounds + (optional) rated rpm overlay on the environmental-spectra plot for floating cases.
n_steps (int) – Campbell-sweep parameters.
n_blade_modes (int) – Campbell-sweep parameters.
n_tower_modes (int) – Campbell-sweep parameters.
on_skip ({“warn”, “fail-on-data”, “fail”}, default
"fail-on-data") –How to handle workflow skips. Three classes of skip exist internally:
data — a computational result is missing (blade composite reduction raised). Under
"fail-on-data"(the new default in 1.8.0) and"fail"these toggleexit_code = 1so library callers / scripted automation notice the missing engineering output instead of silently publishing an incomplete report.presentation — the data was computed but figure rendering failed (Campbell plot, environmental-spectra plot). Under
"warn"and"fail-on-data"these only warn (the CSV / data is still on disk); under"fail"they toggleexit_code = 1.input — an output was requested but its companion input wasn’t discovered (e.g.
campbell=Truewith no ElastoDyn deck under the turbine root). Under"warn"and"fail-on-data"warns; under"fail"fails.
Pass
"warn"to recover the pre-1.8.0 permissive behaviour (every skip just messages, exit_code stays 0).WindioResult.skippedlists every skip regardless of policy.
- Returns:
Carries the loaded yaml path, the auto-discovery result, the solved model + modal result, optional blade fit, optional Campbell sweep, and every written-artefact path.
exit_codeis0on success,1when a skip toggled the failure gate viaon_skip.- Return type:
- Raises:
FileNotFoundError – When
input_pathdoes not resolve to a yaml or a directory containing one.
CLI
Command-line interface for pyBmodes.
Exposes seven subcommands:
pybmodes validate <main.dat>— coefficient-consistency report for an OpenFAST ElastoDyn deck. Compares the polynomial blocks shipped in the deck against pyBmodes’ own fits to the FEM mode shapes produced by the deck’s structural inputs.pybmodes patch <main.dat> [--backup]— regenerate the polynomial blocks in the deck’s tower and blade.datfiles in place from the pyBmodes fits. Optional--backupsaves a.bakcopy of each modified file first.pybmodes campbell <input> --rated-rpm R --max-rpm M [--orders 1,2,3,6,9] [--out PATH]— sweep a blade across rotor speeds 0..max_rpm and emit a Campbell diagram (PNG by default) plus a per-step CSV summary. Accepts either a.bmideck or an ElastoDyn main.dat.pybmodes batch <root> [--validate --patch --out OUT]— walk a directory tree for ElastoDyn main decks, run validate / patch per deck, write a per-deck report and a summary CSV.pybmodes report <main.dat> [--format md|html|csv] [--campbell]— one-shot bundled report covering modal solve, coefficient validation, and an optional Campbell sweep.pybmodes windio <ontology.yaml | RWT-dir> [--format md|html|csv] [--campbell] [--water-depth M]— the one-click WISDEM/WindIO entry point. Reads a WindIO ontology.yaml(or scans an RWT directory for one), auto-discovers any companion HydroDyn/MoorDyn/ElastoDyn decks scoped to that turbine root, and solves the composite-layup blade + tubular tower + (for afloating_platform) the coupled platform rigid-body modes, then emits the bundled report (+ optional Campbell PNG/CSV). With the companion decks present the floating platform is the industry-grade deck-backed coupled model; without them it degrades to aUserWarning-labelled screening preview.pybmodes examples --copy DIR [--kind all|samples|decks]— vendorsample_inputs/and/orreference_decks/from the bundledpybmodes._examplespackage into a user-supplied directory, so wheel-installed users can seed a working tree without keeping the full repo checkout around.
The script entry point is wired up in pyproject.toml as
pybmodes = "pybmodes.cli:main".