diff --git a/test/plant/test_mixed_types.py b/test/plant/test_mixed_types.py new file mode 100644 index 00000000..dc15dcf6 --- /dev/null +++ b/test/plant/test_mixed_types.py @@ -0,0 +1,71 @@ +"""Functional coverage for mixed turbine types and per-turbine wind resources. + +The windIO standard supports assigning different turbine types (and therefore +different hub heights) to different positions in a wind farm, and supports +wind-resource data dimensioned per turbine. These tests assert that those +structures parse and round-trip through windIO's public API, rather than only +being validated implicitly via the example-validation sweep. +""" + +from pathlib import Path + +import windIO + + +def _plant_examples_dir(): + return Path(windIO.plant_ex.__file__).parent + + +def test_mixed_turbine_types_wind_farm(): + """A wind farm assigns >1 turbine type by per-position index, and the + referenced types have distinct hub heights (mixed hub heights).""" + farm_yaml = _plant_examples_dir() / "plant_wind_farm" / "multiple_types.yaml" + + # Mixed types are a first-class windIO feature -> must validate. + windIO.validate(input=farm_yaml, schema_type="plant/wind_farm") + + farm = windIO.load_yaml(farm_yaml) + layout = farm["layouts"][0] + type_idx = layout["turbine_types"] + n_positions = len(layout["coordinates"]["x"]) + + # A per-position type index referencing a multi-entry turbine_types map. + assert len(farm["turbine_types"]) >= 2 + assert len(type_idx) == n_positions + assert set(type_idx) == set(farm["turbine_types"].keys()) + # The example genuinely uses both types. + assert len(set(type_idx)) >= 2 + + # The assigned types have distinct hub heights -> mixed hub heights. + hub_heights = {k: t["hub_height"] for k, t in farm["turbine_types"].items()} + assert len(set(hub_heights.values())) >= 2 + + +def test_per_turbine_wind_resource_roundtrip(): + """A wind resource dimensioned per turbine (one hub height each, no shared + vertical profile) round-trips through dict_to_netcdf with the + ``wind_turbine`` dimension and per-turbine height preserved.""" + res_yaml = _plant_examples_dir() / "plant_energy_resource" / "WTResource.yaml" + + windIO.validate(input=res_yaml, schema_type="plant/energy_resource") + + resource = windIO.load_yaml(res_yaml) + ds = windIO.dict_to_netcdf(resource["wind_resource"]) + + # Per-turbine resource: wind_turbine is a real dimension. + assert "wind_turbine" in ds.dims + assert ds.sizes["wind_turbine"] >= 2 + + # Height is given per turbine (not a shared scalar / vertical profile). + assert "height" in ds.variables + assert ds["height"].dims == ("wind_turbine",) + assert ds["height"].sizes["wind_turbine"] == ds.sizes["wind_turbine"] + + # The resource's data variables carry the per-turbine dimension (here + # wind_speed / wind_direction are binned coordinate axes, so check the + # actual per-turbine fields). + per_turbine_vars = [ + v for v in ds.data_vars if "wind_turbine" in ds[v].dims + ] + assert per_turbine_vars, "no data variable carries the wind_turbine dimension" + assert "sector_probability" in per_turbine_vars diff --git a/windIO/examples/plant/wind_energy_system/flow_example_timeseries.yaml b/windIO/examples/plant/wind_energy_system/flow_example_timeseries.yaml index 781e5432..46fa2baf 100644 --- a/windIO/examples/plant/wind_energy_system/flow_example_timeseries.yaml +++ b/windIO/examples/plant/wind_energy_system/flow_example_timeseries.yaml @@ -18,7 +18,7 @@ attributes: ws_superposition: Linear ti_superposition: Linear rotor_averaging: - name: GQGrid + name: gq_grid n_x_grid_points: 5 n_y_grid_points: 5 background_averaging: center diff --git a/windIO/schemas/plant/wind_energy_system.yaml b/windIO/schemas/plant/wind_energy_system.yaml index 29910eef..a34d9709 100644 --- a/windIO/schemas/plant/wind_energy_system.yaml +++ b/windIO/schemas/plant/wind_energy_system.yaml @@ -66,15 +66,13 @@ properties: type: number # (default 0) free_stream_ti: title: Flag deciding to use freestream or waked TI + description: TI feeding the wake-expansion coefficient (k = k_a*TI + k_b) and TI-dependent deficits. False (default) = waked/effective TI; True = freestream TI. type: boolean # (default to False) ceps: title: Bastankhah c_epsilon factor type: number use_effective_ws: - title: flag to use freestream wind speed for deficit computation - type: boolean - use_effective_ti: - title: flag to use effective turbulence intensity + title: flag to use local (effective) wind speed for deficit computation (True=waked, False=freestream) type: boolean A: title: TurboNOJ wake expansion parameter @@ -113,6 +111,9 @@ properties: coefficients: title: coefficients type: array + c0: + title: STF/IEC model coefficient 0 + type: number c1: title: STF model coefficient 1 type: number @@ -128,7 +129,7 @@ properties: ws_superposition: title: Speed superposition model name type: string - enum: ["Linear", "Squared", "Max", "Product", "Weighted", "Cumulative"] + enum: ["Linear", "Squared", "Max", "Product", "Weighted", "Cumulative", "Vector"] ti_superposition: title: TI superposition model name type: string @@ -141,8 +142,16 @@ properties: properties: name: title: Rotor averaging model name + description: >- + Engine-neutral: center, grid (regular rotor grid / GridRotorAvg), + eq_grid, gq_grid, polar_grid, cgi, gaussian_overlap, area_overlap. + gaussian_overlap/area_overlap are non-node overlap models — NOT compatible + with 'Weighted' superposition, which requires a node model (use grid). + Capitalized names are deprecated aliases of the lowercase forms. type: string - enum: ["Center", "Avg_Deficit", "EqGrid", "GQGrid", "PolarGrid", "CGI"] + enum: ["center", "grid", "eq_grid", "gq_grid", "polar_grid", "cgi", + "gaussian_overlap", "area_overlap", + "Center", "Avg_Deficit", "EqGrid", "GQGrid", "PolarGrid", "CGI"] n: title: Number of grid or integration points type: integer