Skip to content

Commit bb3ddad

Browse files
committed
fix: rotate the hub tensor by the correct WindIO hub-frame handedness
The WindIO hub-aligned frame is the tower-top frame tilted about the lateral y-axis (x along the shaft, z up including tilt), so the shaft-tilt rotation carries sign_x on the sin (off-diagonal) terms, not the cos. The previous matrix put sign_x on the cos terms, which for an upwind rotor flipped the signs of a hub tensor Ixy / Iyz. A diagonal hub (every current reference, incl. IEA-22) and the symmetric rotor tensor are invariant to the choice, so results are unchanged there, but a full off-diagonal hub inertia is now oriented correctly. The outboard shaft axis for the coned-CM offset is built explicitly, since it is the shaft line (opposite sense to the hub-frame x basis for an upwind rotor). Also assert the >= 3-blade precondition at the axisymmetric split, which the blade-count guard upstream already enforces.
1 parent e0b1fce commit bb3ddad

2 files changed

Lines changed: 44 additions & 17 deletions

File tree

src/pybmodes/io/windio.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,10 @@ def _nac_geom(key: str) -> float:
12181218
# A flat rotor (``sin(cone) = 0``) leaves the offset and the axial term
12191219
# zero, recovering the planar ``I_polar/2`` split about the apex. Lumping
12201220
# the blades as a bare point mass at the apex would drop the whole tensor.
1221+
# n_blades is 0 or >= 3 here: 1 and 2 are rejected above, since the
1222+
# I_polar/2 transverse split holds only for an azimuthally symmetric rotor
1223+
# (>= 3 evenly spaced blades). n_blades == 0 gives a zero rotor tensor.
1224+
assert n_blades == 0 or n_blades >= 3
12211225
i_polar_rotor = n_blades * i_polar_each
12221226
cm_axial_offset = (m_axial1_each / m_blade_each) if m_blade_each > 0.0 else 0.0
12231227
i_axial_cm = max(
@@ -1235,25 +1239,29 @@ def _nac_geom(key: str) -> float:
12351239
dtype=float,
12361240
)
12371241

1238-
# The hub and rotor inertia tensors are in the shaft frame; rotate them
1239-
# into the tower-top frame by the shaft tilt before they are summed as
1240-
# tower-top tensors (a tilted rotor otherwise loses the tensor izx
1241-
# product and misallocates the large polar term between ixx and izz).
1242-
# The shaft unit axis is ``[sign_x*cos(uptilt), 0, sin(uptilt)]``; R maps
1243-
# the shaft x-axis onto it and fixes the lateral y-axis. The nacelle
1244-
# tensor is already in the tower frame and is left alone.
1242+
# The hub and rotor inertia tensors are in the WindIO hub-aligned frame
1243+
# (x along the tilted shaft, y lateral, z up including tilt); rotate them
1244+
# into the tower-top frame before they are summed as tower-top tensors (a
1245+
# tilted rotor otherwise loses the tensor izx product and misallocates the
1246+
# large polar term between ixx and izz). The hub frame is the tower frame
1247+
# tilted about the lateral y-axis by the shaft tilt, so ``sign_x`` rides
1248+
# the sin (off-diagonal) terms, not the cos: for an upwind rotor the
1249+
# downwind-positive shaft tilts the opposite way. A diagonal hub or the
1250+
# symmetric rotor tensor is invariant to that choice, but a hub with
1251+
# off-diagonal Ixy / Iyz would be mis-signed with sign_x on the cos terms.
1252+
# The nacelle tensor is already in the tower frame and is left alone.
12451253
c_t, s_t = float(np.cos(uptilt)), float(np.sin(uptilt))
1246-
r_tilt = np.array(
1247-
[[sign_x * c_t, 0.0, -s_t], [0.0, 1.0, 0.0], [s_t, 0.0, sign_x * c_t]]
1254+
r_hub = np.array(
1255+
[[c_t, 0.0, -sign_x * s_t], [0.0, 1.0, 0.0], [sign_x * s_t, 0.0, c_t]]
12481256
)
1249-
i_hub = r_tilt @ i_hub @ r_tilt.T
1250-
i_blades = r_tilt @ i_blades @ r_tilt.T
1251-
1252-
# The coned rotor CM sits ``cm_axial_offset`` beyond the apex along the
1253-
# shaft axis (r_tilt's first column, the outboard unit vector the overhang
1254-
# already runs along), so a precone displaces the blade mass further from
1255-
# the tower in the same sense for both rotor orientations.
1256-
shaft_axis = r_tilt[:, 0]
1257+
i_hub = r_hub @ i_hub @ r_hub.T
1258+
i_blades = r_hub @ i_blades @ r_hub.T
1259+
1260+
# Outboard shaft direction (tower top toward the apex and beyond), along
1261+
# which the overhang runs and the coned blade CM is displaced. This is the
1262+
# shaft *line*, not the hub-frame x basis rotated above (they point in
1263+
# opposite senses for an upwind rotor), so build it explicitly.
1264+
shaft_axis = np.array([sign_x * c_t, 0.0, s_t])
12571265
rotor_cm = apex + cm_axial_offset * shaft_axis
12581266

12591267
bodies = [

tests/test_windio_rna.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,25 @@ def _mk(y_off: float):
347347
assert swept.izz > straight.izz
348348

349349

350+
def test_rna_upwind_hub_offdiagonal_inertia_sign(tmp_path: pathlib.Path) -> None:
351+
"""The WindIO hub frame has z up, so at zero tilt the hub tensor rotates by
352+
the identity and its off-diagonal Iyz passes through unchanged, even for an
353+
upwind rotor. A sign_x on the cos terms would flip it (Codex review on
354+
#130). The hub sits on the tower axis (y = 0), so no other body adds iyz."""
355+
pytest.importorskip("yaml")
356+
from pybmodes.io.windio import read_windio_rna
357+
358+
o = _base_ontology()
359+
assert o["assembly"]["rotor_orientation"] == "Upwind"
360+
o["components"]["nacelle"]["drivetrain"]["uptilt"] = 0.0
361+
# hub 6-vector [Ixx, Iyy, Izz, Ixy, Ixz, Iyz] with a nonzero Iyz
362+
o["components"]["hub"]["elastic_properties_mb"]["system_inertia"] = [
363+
2.0e6, 1.0e6, 1.0e6, 0.0, 0.0, 5.0e5,
364+
]
365+
rna = read_windio_rna(_write(o, tmp_path, "hub_offdiag.yaml"))
366+
assert rna.iyz == pytest.approx(5.0e5)
367+
368+
350369
def test_rna_hub_radius_projected_through_cone(tmp_path: pathlib.Path) -> None:
351370
"""The hub radius is coned like the span (WISDEM Rtip*cos(precone)), so a
352371
coned rotor with a hub radius carries an extra Rhub*sin(cone) axial offset

0 commit comments

Comments
 (0)