Skip to content

Commit 2900ff1

Browse files
committed
fix: disambiguate WindIO cone / uptilt rad vs deg by magnitude
The WindIO v2 turbine schema annotates hub.cone_angle and drivetrain uptilt as degrees, but the IEA reference ontologies store radians (IEA-22 cone_angle 0.0698 = 4 deg, uptilt 0.1047 = 6 deg, matched to <0.5 % on CM against the ElastoDyn deck). A pure-radians reader rejects schema- conforming degree files; a pure-degrees reader mis-reads the reference files as ~0 precone. Physical precone and shaft tilt are small (well under ~20 deg), so the two encodings never overlap: interpret a stored magnitude at or below 0.5 as radians and a larger one as degrees, and reject a value non-physical under either (> 90). Both a 4 and a 0.0698 cone now assemble to the same 4 deg RNA.
1 parent 6690b5d commit 2900ff1

2 files changed

Lines changed: 50 additions & 32 deletions

File tree

src/pybmodes/io/windio.py

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

734734

735-
def _finite_angle(value: Any, what: str, max_abs: float = float(np.pi / 4)) -> float:
736-
"""Coerce ``value`` to a finite angle in radians within a physical bound.
737-
738-
WindIO stores angles in radians (SI). A rotor precone or shaft tilt beyond
739-
~45 deg is non-physical for a horizontal-axis turbine and almost always
740-
means degrees were supplied by mistake (e.g. ``cone_angle: 4`` read as
741-
4 rad = 229 deg instead of 4 deg), which would silently corrupt the coned
742-
lever and axial offset. Reject it with a clear message rather than
743-
evaluate the trig at the wrong angle.
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.
738+
739+
The WindIO v2 turbine schema annotates ``hub.cone_angle`` and drivetrain
740+
``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).
744749
"""
745750
ang = _finite_float(value, what)
746-
if abs(ang) > max_abs:
747-
raise ValueError(
748-
f"{what} = {ang:.4f} rad ({np.degrees(ang):.1f} deg) is outside the "
749-
f"physical range +/-{np.degrees(max_abs):.0f} deg. WindIO angles are "
750-
f"in radians; a value near a small integer usually means degrees "
751-
f"were supplied."
752-
)
753-
return ang
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+
)
754761

755762

756763
def _positive_mass(value: Any, what: str) -> float:
@@ -1076,7 +1083,7 @@ def _nac_geom(key: str) -> float:
10761083
return _finite_float(val, f"nacelle.drivetrain.{key}")
10771084

10781085
overhang = _nac_geom("overhang")
1079-
uptilt = _finite_angle(_nac_geom("uptilt"), "nacelle.drivetrain.uptilt")
1086+
uptilt = _windio_angle(_nac_geom("uptilt"), "nacelle.drivetrain.uptilt")
10801087
dist_tt_hub = _nac_geom("distance_tt_hub")
10811088

10821089
# --- Hub: components.<hub>.elastic_properties_mb ---
@@ -1103,7 +1110,7 @@ def _nac_geom(key: str) -> float:
11031110
)
11041111
if hub_r < 0.0:
11051112
raise ValueError(f"hub diameter must be non-negative; got {hub_diam!r}.")
1106-
cone_ang = _finite_angle(hub.get("cone_angle", 0.0), "hub cone_angle")
1113+
cone_ang = _windio_angle(hub.get("cone_angle", 0.0), "hub cone_angle")
11071114
cone_cos = float(np.cos(cone_ang))
11081115
cone_sin = float(np.sin(cone_ang))
11091116

tests/test_windio_rna.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -200,28 +200,39 @@ def _mk(cone: float):
200200
assert coned.mass == pytest.approx(flat.mass)
201201

202202

203-
def test_rna_rejects_degree_valued_cone_angle(tmp_path: pathlib.Path) -> None:
204-
"""WindIO angles are radians; a degrees-valued cone_angle (e.g. 4 read as
205-
4 rad = 229 deg) is non-physical and rejected rather than silently
206-
evaluated at the wrong angle (Codex review on #130)."""
203+
def test_rna_cone_and_uptilt_degrees_match_radians(tmp_path: pathlib.Path) -> None:
204+
"""WindIO's rad/deg ambiguity: the v2 schema annotates cone_angle / uptilt
205+
as degrees while the IEA reference files store radians. The reader
206+
disambiguates by magnitude, so a degrees encoding (4, 6) and the
207+
equivalent radians encoding (0.0698, 0.1047) assemble to the same RNA
208+
(Codex review on #130)."""
207209
pytest.importorskip("yaml")
210+
import numpy as np
211+
208212
from pybmodes.io.windio import read_windio_rna
209213

210-
o = _base_ontology()
211-
o["components"]["hub"]["cone_angle"] = 4.0 # 4 deg mistakenly left un-converted
212-
with pytest.raises(ValueError, match="radians"):
213-
read_windio_rna(_write(o, tmp_path, "cone_deg.yaml"))
214+
def _mk(cone: float, uptilt: float):
215+
o = _base_ontology()
216+
o["components"]["hub"]["cone_angle"] = cone
217+
o["components"]["nacelle"]["drivetrain"]["uptilt"] = uptilt
218+
return read_windio_rna(_write(o, tmp_path, f"a_{cone}_{uptilt}.yaml"))
219+
220+
rad = _mk(float(np.radians(4.0)), float(np.radians(6.0))) # 0.0698, 0.1047
221+
deg = _mk(4.0, 6.0) # degrees encoding of the same angles
222+
for attr in ("mass", "cm_axial", "ixx", "iyy", "izz", "izx"):
223+
assert getattr(deg, attr) == pytest.approx(getattr(rad, attr))
214224

215225

216-
def test_rna_rejects_degree_valued_uptilt(tmp_path: pathlib.Path) -> None:
217-
"""A degrees-valued uptilt is likewise rejected as out of physical range."""
226+
def test_rna_rejects_nonphysical_angle(tmp_path: pathlib.Path) -> None:
227+
"""A value non-physical as both radians and degrees (e.g. 200) is rejected
228+
rather than silently evaluated at a meaningless angle."""
218229
pytest.importorskip("yaml")
219230
from pybmodes.io.windio import read_windio_rna
220231

221232
o = _base_ontology()
222-
o["components"]["nacelle"]["drivetrain"]["uptilt"] = 6.0 # 6 deg un-converted
223-
with pytest.raises(ValueError, match="radians"):
224-
read_windio_rna(_write(o, tmp_path, "uptilt_deg.yaml"))
233+
o["components"]["hub"]["cone_angle"] = 200.0
234+
with pytest.raises(ValueError, match="non-physical"):
235+
read_windio_rna(_write(o, tmp_path, "cone_bad.yaml"))
225236

226237

227238
@pytest.mark.parametrize("n_bl", [1, 2])

0 commit comments

Comments
 (0)