First of all, great library!
I think I have found a bug.
Summary
TurbinePointCloud states return permuted/duplicated ambient data across turbines as soon as the wind farm contains turbines with different hub heights. With uniform hub heights the results are correct. The corruption is silent — no warning or error — and affects all downstream results (ambient rotor variables, wakes, power, AEP).
Environment
- foxes 1.8.3 (reproduced on current
main, commit d2449ce)
- numpy 2.x, xarray 2024.07, single-chunk engine (also reproduces with default engine)
Minimal reproducer
import numpy as np, pandas as pd, xarray as xr
import foxes, foxes.variables as FV, foxes.constants as FC
from foxes.core import Engine
ws = np.array([[10.0, 10.5, 9.5]] * 2) # distinct value per turbine
sdata = xr.Dataset(
coords={FC.STATE: pd.date_range("2000-01-01", periods=2, freq="1h"),
FC.TURBINE: np.arange(3)},
data_vars={"ws": ((FC.STATE, FC.TURBINE), ws),
"wd": ((FC.STATE, FC.TURBINE), np.full((2, 3), 270.0))},
)
for hubs in ([100.0, 100.0, 100.0], [100.0, 100.0, 90.0]):
states = foxes.input.states.TurbinePointCloud(
data_source=sdata,
output_vars=[FV.WS, FV.WD, FV.TI, FV.RHO],
var2ncvar={FV.WS: "ws", FV.WD: "wd"},
fixed_vars={FV.TI: 0.06, FV.RHO: 1.225},
)
farm = foxes.WindFarm()
for i, h in enumerate(hubs):
farm.add_turbine(
foxes.Turbine(xy=np.array([i * 500.0, 0.0]), H=h,
turbine_models=["null_type"]),
verbosity=0,
)
algo = foxes.algorithms.Downwind(
farm, states, wake_models=[], rotor_model="centre",
mbook=foxes.models.ModelBook(), verbosity=0,
)
with Engine.new("single", verbosity=0):
got = algo.calc_farm()[FV.AMB_REWS].to_numpy()[0]
print(f"H={hubs} -> AMB_REWS={got}, expected {ws[0]}")
Output:
H=[100.0, 100.0, 100.0] -> AMB_REWS=[10. 10.5 9.5], expected [10. 10.5 9.5] # OK
H=[100.0, 100.0, 90.0] -> AMB_REWS=[10.5 10.5 10. ], expected [10. 10.5 9.5] # scrambled
With mixed hub heights the per-turbine data comes back as [T1, T1, T0] instead of [T0, T1, T2].
Root cause
In DatasetStates.calculate → _analyze_points (foxes/input/states/dataset_states.py), the turbine-point-cloud branch (3D FC.TURBINE hcoords) sets
_points_data["up"] = points # L1257
_points_data["points_vary"] = False # L1258
but leaves heights_vary unset. The subsequent height analysis then runs: with per-turbine differing z, pmax[2] - pmin[2] > 1e-4 holds, so heights_vary=True and uh, uh2h = np.unique(points[:, :, 2], return_inverse=True) are computed (L1271–1274).
Later, the height reconstruction intended for gridded (x, y, h) states executes (L1382):
d = d[:, points_data["uh2h"], :].reshape(shp)
For a turbine point cloud, axis 1 of d is the turbine axis (the data was already returned per turbine, verbatim, by TurbinePointCloud.interpolate_data's exact-match branch). Re-indexing it with uh2h (inverse indices into the unique-heights array) permutes/duplicates turbines by their hub-height rank: H=[100, 100, 90] → unique [90, 100] → uh2h=[1, 1, 0] → data served as [T1, T1, T0].
Suggested fix
The turbine-cloud branch carries per-turbine coordinates verbatim and needs no height reconstruction — mark heights as non-varying there:
):
_points_data["up"] = points
_points_data["points_vary"] = False
+ # turbine point clouds carry per-turbine coordinates verbatim;
+ # the uh2h height reconstruction must not re-index the turbine axis
+ _points_data["heights_vary"] = False
+ _points_data["uh"] = None
+ _points_data["uh2h"] = None
elif np.max(pmax - pmin) > 1e-4:
With this patch the reproducer returns correct data for uniform and mixed hub heights (tested [100,100,100], [100,100,90], [120,100,90]); the existing uniform-height behavior is unchanged. Happy to open a PR.
First of all, great library!
I think I have found a bug.
Summary
TurbinePointCloudstates return permuted/duplicated ambient data across turbines as soon as the wind farm contains turbines with different hub heights. With uniform hub heights the results are correct. The corruption is silent — no warning or error — and affects all downstream results (ambient rotor variables, wakes, power, AEP).Environment
main, commitd2449ce)Minimal reproducer
Output:
With mixed hub heights the per-turbine data comes back as
[T1, T1, T0]instead of[T0, T1, T2].Root cause
In
DatasetStates.calculate→_analyze_points(foxes/input/states/dataset_states.py), the turbine-point-cloud branch (3DFC.TURBINEhcoords) setsbut leaves
heights_varyunset. The subsequent height analysis then runs: with per-turbine differing z,pmax[2] - pmin[2] > 1e-4holds, soheights_vary=Trueanduh, uh2h = np.unique(points[:, :, 2], return_inverse=True)are computed (L1271–1274).Later, the height reconstruction intended for gridded
(x, y, h)states executes (L1382):For a turbine point cloud, axis 1 of
dis the turbine axis (the data was already returned per turbine, verbatim, byTurbinePointCloud.interpolate_data's exact-match branch). Re-indexing it withuh2h(inverse indices into the unique-heights array) permutes/duplicates turbines by their hub-height rank:H=[100, 100, 90]→ unique[90, 100]→uh2h=[1, 1, 0]→ data served as[T1, T1, T0].Suggested fix
The turbine-cloud branch carries per-turbine coordinates verbatim and needs no height reconstruction — mark heights as non-varying there:
): _points_data["up"] = points _points_data["points_vary"] = False + # turbine point clouds carry per-turbine coordinates verbatim; + # the uh2h height reconstruction must not re-index the turbine axis + _points_data["heights_vary"] = False + _points_data["uh"] = None + _points_data["uh2h"] = None elif np.max(pmax - pmin) > 1e-4:With this patch the reproducer returns correct data for uniform and mixed hub heights (tested
[100,100,100],[100,100,90],[120,100,90]); the existing uniform-height behavior is unchanged. Happy to open a PR.