Skip to content

Commit fb42ee5

Browse files
committed
fix: resolve WindIO rad/deg once per file from the larger rotor angle
The per-value magnitude rule misread a small schema-conforming degrees angle: a WindIO v2 file with cone_angle 0.25 (deg) fell into the <=0.5 radians branch and became 0.25 rad = 14.3 deg. A file uses one angle convention throughout, so decide it once for the (cone_angle, uptilt) pair from the larger magnitude. A degrees file reliably carries a several-degree shaft tilt whose magnitude dwarfs any radians precone/tilt (which stay under ~0.3 rad), so the larger angle disambiguates reliably even when the cone is a fraction of a degree. Larger magnitude over 0.5 => degrees, else radians; over 90 rejected either way.
1 parent 2900ff1 commit fb42ee5

2 files changed

Lines changed: 48 additions & 24 deletions

File tree

src/pybmodes/io/windio.py

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -732,32 +732,34 @@ def _finite_float(value: Any, what: str) -> float:
732732
return f
733733

734734

735-
def _windio_angle(value: Any, what: str) -> float:
736-
"""Interpret a WindIO precone / shaft-tilt angle as radians, tolerant of
737-
the rad/deg ambiguity across WindIO versions.
735+
def _windio_rotor_angles(cone_value: Any, uptilt_value: Any) -> tuple[float, float]:
736+
"""Convert the WindIO precone and shaft tilt to radians, resolving the
737+
rad/deg ambiguity once for the pair.
738738
739739
The WindIO v2 turbine schema annotates ``hub.cone_angle`` and drivetrain
740740
``uptilt`` as degrees, but the IEA reference ontologies store radians
741-
(IEA-22 ``cone_angle`` 0.0698 = 4 deg, ``uptilt`` 0.1047 = 6 deg).
742-
Physical rotor precone and shaft tilt are small (well under ~20 deg), so
743-
the two encodings never overlap: a stored magnitude at or below 0.5 is
744-
radians (0.5 rad = 29 deg, already larger than any real precone / tilt),
745-
a larger one is degrees. Convert to radians on that basis, and reject a
746-
value that is non-physical under either reading (> 90). This keeps the
747-
validated radians reference files working while also accepting a
748-
schema-conforming degrees file (issue #130 review).
741+
(IEA-22 ``cone_angle`` 0.0698 = 4 deg, ``uptilt`` 0.1047 = 6 deg). A file
742+
uses one convention throughout, so decide it once from the *larger* of the
743+
two magnitudes rather than per value: a degrees file reliably carries a
744+
several-degree shaft tilt (magnitude well above any radians precone /
745+
tilt, which stay under ~0.3 rad), whereas judging each value alone would
746+
misread a small schema-conforming degrees angle such as 0.25 deg as
747+
0.25 rad. If the larger magnitude exceeds 0.5 the pair is degrees;
748+
otherwise radians. Anything above 90 is non-physical either way and is
749+
rejected (issue #130 review).
749750
"""
750-
ang = _finite_float(value, what)
751-
mag = abs(ang)
752-
if mag <= 0.5:
753-
return ang
754-
if mag <= 90.0:
755-
return float(np.radians(ang))
756-
raise ValueError(
757-
f"{what} = {ang!r} is non-physical as either radians "
758-
f"({np.degrees(ang):.0f} deg) or degrees; expected a rotor precone / "
759-
f"shaft tilt well under 90 deg."
760-
)
751+
cone = _finite_float(cone_value, "hub cone_angle")
752+
uptilt = _finite_float(uptilt_value, "nacelle.drivetrain.uptilt")
753+
biggest = max(abs(cone), abs(uptilt))
754+
if biggest > 90.0:
755+
raise ValueError(
756+
f"rotor precone / shaft tilt (cone_angle={cone!r}, uptilt="
757+
f"{uptilt!r}) is non-physical as either radians or degrees; "
758+
f"expected values well under 90 deg."
759+
)
760+
if biggest > 0.5: # degrees: a real shaft tilt dwarfs any radians angle
761+
return float(np.radians(cone)), float(np.radians(uptilt))
762+
return cone, uptilt
761763

762764

763765
def _positive_mass(value: Any, what: str) -> float:
@@ -1083,7 +1085,7 @@ def _nac_geom(key: str) -> float:
10831085
return _finite_float(val, f"nacelle.drivetrain.{key}")
10841086

10851087
overhang = _nac_geom("overhang")
1086-
uptilt = _windio_angle(_nac_geom("uptilt"), "nacelle.drivetrain.uptilt")
1088+
uptilt_raw = _nac_geom("uptilt") # rad/deg resolved with cone_angle below
10871089
dist_tt_hub = _nac_geom("distance_tt_hub")
10881090

10891091
# --- Hub: components.<hub>.elastic_properties_mb ---
@@ -1110,7 +1112,7 @@ def _nac_geom(key: str) -> float:
11101112
)
11111113
if hub_r < 0.0:
11121114
raise ValueError(f"hub diameter must be non-negative; got {hub_diam!r}.")
1113-
cone_ang = _windio_angle(hub.get("cone_angle", 0.0), "hub cone_angle")
1115+
cone_ang, uptilt = _windio_rotor_angles(hub.get("cone_angle", 0.0), uptilt_raw)
11141116
cone_cos = float(np.cos(cone_ang))
11151117
cone_sin = float(np.sin(cone_ang))
11161118

tests/test_windio_rna.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,28 @@ def _mk(cone: float, uptilt: float):
223223
assert getattr(deg, attr) == pytest.approx(getattr(rad, attr))
224224

225225

226+
def test_rna_small_degree_cone_keyed_off_uptilt(tmp_path: pathlib.Path) -> None:
227+
"""A schema-conforming degrees file with a small cone (0.25 deg) and a
228+
normal 6 deg uptilt: the unit decision keys off the larger (uptilt)
229+
magnitude, so the small cone is read as 0.25 deg, not 0.25 rad = 14.3 deg
230+
(Codex review on #130)."""
231+
pytest.importorskip("yaml")
232+
import numpy as np
233+
234+
from pybmodes.io.windio import read_windio_rna
235+
236+
def _mk(cone: float, uptilt: float):
237+
o = _base_ontology()
238+
o["components"]["hub"]["cone_angle"] = cone
239+
o["components"]["nacelle"]["drivetrain"]["uptilt"] = uptilt
240+
return read_windio_rna(_write(o, tmp_path, f"s_{cone}_{uptilt}.yaml"))
241+
242+
deg = _mk(0.25, 6.0) # degrees, small cone
243+
rad = _mk(float(np.radians(0.25)), float(np.radians(6.0))) # radians equivalent
244+
for attr in ("cm_axial", "ixx", "iyy", "izz"):
245+
assert getattr(deg, attr) == pytest.approx(getattr(rad, attr))
246+
247+
226248
def test_rna_rejects_nonphysical_angle(tmp_path: pathlib.Path) -> None:
227249
"""A value non-physical as both radians and degrees (e.g. 200) is rejected
228250
rather than silently evaluated at a meaningless angle."""

0 commit comments

Comments
 (0)