Skip to content

TurbinePointCloud silently scrambles per-turbine data when turbines have different hub heights #63

Description

@ews-ffarella

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions