Units & conventions
pybmodes is strict SI everywhere on the API boundary.
No imperial units, no degree-vs-radian convention switches at
runtime, no mode-by-mode unit-system flags. The conventions
below are enforced by the public constructors and asserted in
the validation suite.
Quantities & units
Quantity |
Unit |
Notes |
|---|---|---|
Length, displacement, mode-shape ordinates |
m |
Section nodes, tower height |
Distributed mass density |
kg / m |
Section property |
Lumped mass |
kg |
|
Axial stiffness |
N |
Section property |
Bending stiffness |
N · m² |
Section properties |
Torsional stiffness |
N · m² |
Section property |
Frequency |
Hz |
Ordinary frequency, not angular frequency \(\omega\). To convert: \(f\, [\mathrm{Hz}] = \omega\, [\mathrm{rad/s}] / (2\pi)\). |
Rotor speed |
rpm |
|
Time / period |
s |
Mooring quasi-statics have no time dependence; period quantities appear only in the environmental-loading helpers (Kaimal / JONSWAP spectra). |
Angle, pre-twist |
rad |
The WindIO reader auto-detects degree-convention files (where root twist exceeds the radian ceiling) and converts to radians — see Conventions below. |
Gravity |
m / s² |
Default |
Water density |
kg / m³ |
Default |
Conversion tables for common units
If you receive a deck in non-SI units, convert at the boundary
before constructing a Tower / RotatingBlade. The most
common conversions:
From |
To |
Multiplier |
|---|---|---|
inch |
m |
0.0254 |
ft |
m |
0.3048 |
lb (mass) |
kg |
0.45359237 |
lb / ft (mass / length) |
kg / m |
1.48816 |
psi (stress) |
Pa |
6894.757 |
ksi (stress) |
Pa |
6.894757 × 10⁶ |
lbf (force) |
N |
4.4482216 |
lbf · ft (moment) |
N · m |
1.355818 |
rad/s (angular freq) |
Hz (ordinary freq) |
1 / (2π) ≈ 0.159155 |
rad/s (rotational) |
rpm |
60 / (2π) ≈ 9.549297 |
deg (angle) |
rad |
π / 180 ≈ 0.017453 |
knot (water current) |
m / s |
0.51444 |
bar (pressure) |
Pa |
1.0 × 10⁵ |
Mode-shape normalisation
Mode shapes are mass-normalised:
The sign of each mode shape is canonicalised by the rule “maximum-amplitude DOF is positive”, so two solves of the same problem produce sign-stable shapes that can be MAC-compared without a sign-flip ambiguity.
To convert to tip-unit normalisation (max ordinate = 1) post-solve:
for shape in result.shapes:
peak = max(
abs(shape.flap_disp).max(),
abs(shape.lag_disp).max(),
abs(shape.twist).max(),
)
shape.flap_disp /= peak
shape.lag_disp /= peak
shape.twist /= peak
The ElastoDyn polynomial coefficients are written in
tip-unit nondim (\(\mathrm{SHP}(1) = \sum c_i = 1\));
the polynomial-fit routines apply that normalisation
internally before the constrained least-squares fit, so
compute_blade_params / compute_tower_params operate
correctly on either mass-normalised or tip-unit mode shapes.
DOF order
The canonical 6-DOF order for every platform-related
matrix (mooring_K, hydro_K, i_matrix, MoorDyn
output, WAMIT output) is the OpenFAST convention:
index DOF
----- -----
0 surge (translation along X, forward)
1 sway (translation along Y, sideways)
2 heave (translation along Z, vertical)
3 roll (rotation about X)
4 pitch (rotation about Y)
5 yaw (rotation about Z)
See pybmodes.coords for the documented construction and
tests/test_mooring.py::test_oc3hywind_bmi_dof_order_matches_jonkman_2010
for the regression that pins it against Jonkman 2010 Table 5-1.
A common pitfall: BModes JJ uses
[surge, sway, yaw, roll, pitch, heave] in some legacy
contexts. When comparing pyBmodes output against BModes JJ
column-by-column, re-order via the modal-classifier labels,
not by positional index. See cases/iea15_deep_diagnostic.md
for the worked example.
Polynomial coefficients
ElastoDyn-compatible polynomials are written in the dimensionless span coordinate \(s = h / H\) with the first two coefficients implicitly zero:
BladeElastoDynParams.coefficients() and the corresponding
tower accessor return \((c_2, \ldots, c_6)\) as a length-5
array, with the constraint \(\sum c_i = 1\) (tip-unit at
the tip). The constrained least-squares fit handles the
normalisation; you don’t need to pre-scale the input.
Frame conventions
Tower base frame
z-axis along the span (tower base at \(z = 0\)).
For a fixed-base tower this is also the global frame at the tower base elevation (
TowerBsHt).For a floating tower this is the platform-attached frame — the polynomial basis assumes clamped-base modes referenced to it, and platform 6-DOF motion is added at runtime by ElastoDyn as a separate rigid-body sum. See Theory and Limitations.
Blade root frame
z-axis along the span (root at \(z = 0\)).
x-axis is the chordwise (edgewise) direction of the root section before any pre-twist.
y-axis is the flapwise direction of the root section before any pre-twist.
Pre-twist is applied per section as a rotation about z; the reported mode-shape ordinates are in the section frame (i.e. after the pre-twist rotation).
Section centre-of-mass offsets
cm_loc and cm_axial in the BMI header are the section
centre-of-mass offsets along the local section frame’s axes
(cm_loc along axis-1, cm_axial along axis-3 i.e. the
span).
The BMI’s inertia array carries the section mass moments
of inertia about the centre-of-mass — not the elastic
centre — and the parallel-axis transfer is applied during
nondim by pybmodes.fem.nondim().
Twist auto-detection (WindIO)
The WindIO standard prescribes radians for twist in the
blade outer_shape, but WISDEM-generated files for IEA-15
ship degrees. Applying np.degrees unconditionally turns the
already-degree-valued root twist (~15.6°) into ≈894°.
The reader auto-detects: if any twist value exceeds the
radian ceiling (~2π), it’s assumed degrees and returned as-is;
otherwise it’s assumed radians and converted to degrees by
np.degrees. The detection threshold lives in
pybmodes.io.windio_blade._TWIST_RADIAN_CEILING.
Common pitfalls
Mixing ω and f
ElastoDyn / BModes output frequency in Hz, but a lot of hand-written analytical references give angular frequency :math:`omega = 2pi f` in rad/s — especially closed forms for cable / beam modes. When matching against a paper, check which is reported.
Confusing OpenFAST’s two coordinate systems
OpenFAST uses two related but distinct frames:
t: the tower-base frame, origin at
TowerBsHt.z: the platform-reference frame, origin at
PtfmRefzt.
PtfmCMzt is the platform CM in the t-frame;
PtfmRefzt is the platform reference point in the t-frame
(typically the mean water line for a floater). pyBmodes’ BMI
field ref_msl is PtfmRefzt interpreted that way.
If you transcribe cm_pform = PtfmCMzt into a BMI by hand,
don’t add PtfmRefzt to it — that’s a double-count. See
pybmodes.models.tower._scan_platform_fields() for the
exact mapping pyBmodes uses.
Forgetting the rotor mass on a tower deck
Tower.from_elastodyn reads the BldFile(1) referenced
from the main file only to lump the rotor mass into the
tower-top assembly. If you build a tower by hand without that
lump (or set tip_mass = 0), the 1st tower fore-aft
frequency comes out 10–30 % too high — the rotor adds a
substantial concentrated mass at the top.
For a hand-authored BMI:
# IEA-15-240-RWT rotor + nacelle + hub assembly:
tip_mass_props = TipMassProps(
mass=1017000.0, # kg
cm_loc=0.0, # m
cm_axial=4.0, # m, above TowerBsHt
ixx=..., iyy=..., izz=..., ixy=..., iyz=..., izx=...,
)
Treating the polynomial-fit ratio as an absolute error
pybmodes validate reports a ratio between the file-
shipped polynomial’s RMS residual and pyBmodes’ own fit’s RMS
residual. A ratio of 100 means the file’s polynomial is 100×
worse than pyBmodes’ would be — but in absolute terms the
file’s may still be fine if the underlying mode shape is
nearly linear. The verdict (PASS / WARN / FAIL) is the actual
gate; the ratio is informational.