From ffbc97597c47e636d616ad40c9eac020e01a3f8d Mon Sep 17 00:00:00 2001 From: btol Date: Thu, 4 Jun 2026 14:30:27 +0200 Subject: [PATCH 01/12] Refine analysis schema: engine-neutral rotor averaging, single TI flag Follow-up corrections to the PyWake-submodel schema expansion (6d58bb4), scoped to not break the foxes windIO reader. - rotor_averaging.name: restyle to engine-neutral lowercase names and add the native overlap models gaussian_overlap (Niayifar/TurbOPark) and area_overlap (Jensen/PARK2); keep the old capitalized names as deprecated aliases. Note the gaussian_overlap/area_overlap vs Weighted-superposition incompatibility (Weighted needs a node model, e.g. grid). foxes ignores this field (uses background_averaging/wake_averaging), so this is safe. - TI reference flag: remove the redundant top-level use_effective_ti and standardize on the nested wake_expansion_coefficient.free_stream_ti, which foxes already reads; document its polarity. - use_effective_ws: fix misleading title (True = local/effective, not freestream). - turbulence_model: add c0 (foxes IEC reader reads c0/c1/c2). - ws_superposition: add Vector (foxes capability). - example: flow_example_timeseries rotor_averaging name GQGrid -> gq_grid. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../flow_example_timeseries.yaml | 2 +- windIO/schemas/plant/wind_energy_system.yaml | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) 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 From 3762d423c6c5bb35bcb014f98b1df86e248fbd98 Mon Sep 17 00:00:00 2001 From: btol Date: Mon, 8 Jun 2026 12:07:22 +0200 Subject: [PATCH 02/12] Add tests for mixed turbine types and per-turbine wind resources - Assert multiple_types.yaml yields a multi-entry turbine_types map with a per-position index and distinct hub heights - Assert a wind_turbine-dimensioned resource round-trips through dict_to_netcdf with the per-turbine dimension and height preserved Co-Authored-By: Claude Opus 4.8 --- test/plant/test_mixed_types.py | 71 ++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 test/plant/test_mixed_types.py 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 From 1d61d67fff6751b6a3dc1b9a241f55029c41c0c2 Mon Sep 17 00:00:00 2001 From: btol Date: Mon, 8 Jun 2026 14:24:07 +0200 Subject: [PATCH 03/12] Add opt-in array-backed loading for !include netCDF resources - load_yaml/_get_YAML/_ds2yml gain an nc_data option ("list" default, "array" keeps numpy arrays), avoiding the ~4-28x memory blow-up of turning large included netCDF resources into nested Python lists - make _fmt ndarray-safe (the elementwise "!= {}" filter broke on arrays) - validate() gains array_data=True for structure-only validation: numpy arrays are replaced by [] so jsonschema checks keys/dims without materialising or iterating the bulk data Default behaviour (lists, full validation) is unchanged. Co-Authored-By: Claude Opus 4.8 --- windIO/validator.py | 36 ++++++++++++++++++++++++++++++++---- windIO/yaml.py | 37 ++++++++++++++++++++++++++++--------- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/windIO/validator.py b/windIO/validator.py index eec6b952..c07c9c14 100644 --- a/windIO/validator.py +++ b/windIO/validator.py @@ -6,11 +6,30 @@ import copy import jsonschema import jsonschema.validators +import numpy as np from .yaml import load_yaml from .schemas import schemaPath, schema_validation_error_formatter +def _structure_skeleton(obj): + """Return a copy of ``obj`` with numpy arrays replaced by ``[]``. + + Used for structure-only validation of array-backed (memory-efficient) + inputs: jsonschema requires JSON types (it rejects numpy arrays and would + iterate every element of a large list). Replacing each array with an empty + list keeps the surrounding structure (keys, ``dims``) validatable at O(1) + per variable while skipping element-wise checks of the bulk data. + """ + if isinstance(obj, np.ndarray): + return [] + if isinstance(obj, dict): + return {k: _structure_skeleton(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [_structure_skeleton(v) for v in obj] + return obj + + def retrieve_yaml(uri: str): if not uri.endswith(".yaml"): raise NoSuchResource(ref=uri) @@ -51,7 +70,8 @@ def _enforce_no_additional_properties(schema): def validate( - input: dict | str | Path, schema_type: str, restrictive: bool = True, defaults: bool = False, + input: dict | str | Path, schema_type: str, restrictive: bool = True, + defaults: bool = False, array_data: bool = False, ) -> None: """ Validates a given windIO input based on the selected schema type. @@ -65,8 +85,13 @@ def validate( 'turbine/turbine_schema'. restrictive (bool, optional): If True, the schema will be modified to enforce that no additional properties are allowed. Defaults to True. - defaults (bool, optional): If True, default values specified in the schema will + defaults (bool, optional): If True, default values specified in the schema will be applied to the input data during validation. Defaults to False. + array_data (bool, optional): If True, validate structure only: numpy + arrays (from an array-backed ``!include`` netCDF, or an already + array-backed dict) are replaced by ``[]`` so jsonschema checks keys + and ``dims`` without materialising/iterating the bulk data. Avoids + the dict-of-lists memory blow-up for large resources. Defaults to False. Raises: FileNotFoundError: If the schema file corresponding to the schema type is not found. @@ -84,9 +109,12 @@ def validate( raise FileNotFoundError(f"Schema file {schema_file} not found.") if type(input) is dict: - data = copy.deepcopy(input) + data = _structure_skeleton(input) if array_data else copy.deepcopy(input) elif type(input) in [str, Path, PosixPath, WindowsPath]: - data = load_yaml(input) + if array_data: + data = _structure_skeleton(load_yaml(input, nc_data="array")) + else: + data = load_yaml(input) else: raise TypeError(f"Input type {type(input)} is not supported.") diff --git a/windIO/yaml.py b/windIO/yaml.py index cfc9c60c..ca5245fe 100644 --- a/windIO/yaml.py +++ b/windIO/yaml.py @@ -19,22 +19,35 @@ def _fmt(v: Any) -> dict | list | str | float | int: v (Any): Initially, a dictionary of inputs to format. Then, individual values within the dictionary. """ + if isinstance(v, np.ndarray): + # Keep arrays as-is; the elementwise ``!= {}`` below is unsafe on arrays. + return v if isinstance(v, dict): - return {k: _fmt(v) for k, v in v.items() if _fmt(v) != {}} + out = {} + for k, val in v.items(): + fval = _fmt(val) + if not (isinstance(fval, dict) and len(fval) == 0): + out[k] = fval + return out elif isinstance(v, tuple): return list(v) else: return v -def _ds2yml(ds: xr.Dataset) -> dict: +def _ds2yml(ds: xr.Dataset, data: str = "list") -> dict: """ Converts the input xr.Dataset to a format compatible with yaml.load. Args: ds (xr.Dataset): NetCDF data loaded as a xr.Dataset + data (str): How array data is represented, forwarded to + ``xr.Dataset.to_dict``. ``"list"`` (default) yields nested Python + lists (YAML/JSON friendly). ``"array"`` keeps numpy arrays, avoiding + the ~4-28x memory blow-up of lists for large included netCDF + resources (not YAML-serialisable; use with structure-only validation). """ - d = ds.to_dict() + d = ds.to_dict(data=data) return _fmt( { **{k: v["data"] for k, v in d["coords"].items()}, @@ -49,6 +62,7 @@ def _get_YAML( read_numpy: bool = False, read_include: bool = True, n_list_flow_style: int = 1, + nc_data: str = "list", ) -> YAML: """Get `ruamel.yaml.YAML` instance default setting for windIO @@ -128,11 +142,10 @@ def include(constructor, node): filename = Path(constructor.loader.reader.stream.name).parent / node.value ext = os.path.splitext(filename)[1].lower() if ext in [".yaml", ".yml"]: - return load_yaml( - filename, _get_YAML() - ) # TODO: Make `get_YAML()` dynamic to make it possible to update + # Propagate nc_data so nested includes keep the same array mode. + return load_yaml(filename, _get_YAML(nc_data=nc_data)) elif ext in [".nc"]: - return _ds2yml(xr.open_dataset(filename)) + return _ds2yml(xr.open_dataset(filename), data=nc_data) else: raise ValueError(f"Unsupported file extension: {ext}") @@ -141,7 +154,9 @@ def include(constructor, node): return yaml_obj -def load_yaml(filename: str | Path | os.PathLike, loader=None) -> dict: +def load_yaml( + filename: str | Path | os.PathLike, loader=None, nc_data: str = "list" +) -> dict: """ Opens ``filename`` and loads the content into a dictionary with the ``_get_YAML`` function from ruamel.yaml.YAML. @@ -149,12 +164,16 @@ def load_yaml(filename: str | Path | os.PathLike, loader=None) -> dict: Args: filename (str | Path | os.PathLike): Path or file-handle to the local file to be loaded or string path to the file. loader (ruamel.yaml.YAML, optional): Defaults to SafeLoader. + nc_data (str, optional): How ``!include`` netCDF data is represented; + ``"list"`` (default) for nested Python lists, ``"array"`` to keep + numpy arrays (memory-efficient; requires structure-only validation). + Ignored when an explicit ``loader`` is given. Returns: dict: Dictionary representation of the YAML file given in ``filename``. """ if loader is None: - loader = _get_YAML() + loader = _get_YAML(nc_data=nc_data) if isinstance(filename, str): filename = Path(filename) From f2a6a77334b08c7f458582483152b43a16e3dc79 Mon Sep 17 00:00:00 2001 From: btol Date: Mon, 8 Jun 2026 21:30:38 +0200 Subject: [PATCH 04/12] Add CrespoHernandez c-coefficients and a 'none' rotor-averaging option - turbulence_model.c: optional 4-element calibration array for the CrespoHernandez added-turbulence model, so a paper's coefficients (e.g. Niayifar, Zong) can be expressed in windIO. - rotor_averaging.name: add "none" (no rotor-averaging model). This is the only non-node option PyWake's Weighted superposition accepts, and is what the Zong (2020) literature model uses. Both fields are additive and optional; existing inputs are unaffected. Co-Authored-By: Claude Opus 4.8 --- windIO/schemas/plant/wind_energy_system.yaml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/windIO/schemas/plant/wind_energy_system.yaml b/windIO/schemas/plant/wind_energy_system.yaml index a34d9709..6b6af956 100644 --- a/windIO/schemas/plant/wind_energy_system.yaml +++ b/windIO/schemas/plant/wind_energy_system.yaml @@ -120,6 +120,14 @@ properties: c2: title: STF model coefficient 2 type: number + c: + title: CrespoHernandez coefficients + description: >- + Calibration coefficients [c0, c1, c2, c3] for the CrespoHernandez + added-turbulence model. When given, the model is built with these + coefficients (engine-specific), reproducing a paper's calibration + (e.g. Niayifar/Zong); when omitted the engine default is used. + type: array superposition_model: title: Superposition model @@ -143,13 +151,15 @@ properties: name: title: Rotor averaging model name description: >- - Engine-neutral: center, grid (regular rotor grid / GridRotorAvg), + Engine-neutral: none (no rotor-averaging model — rotor centre, + the only non-node option 'Weighted' superposition accepts), + 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. + with 'Weighted' superposition, which requires a node model (use grid) or + 'none'. Capitalized names are deprecated aliases of the lowercase forms. type: string - enum: ["center", "grid", "eq_grid", "gq_grid", "polar_grid", "cgi", + enum: ["none", "center", "grid", "eq_grid", "gq_grid", "polar_grid", "cgi", "gaussian_overlap", "area_overlap", "Center", "Avg_Deficit", "EqGrid", "GQGrid", "PolarGrid", "CGI"] n: From fee3089a0612d2606a6dab7742ef6d94bca796d3 Mon Sep 17 00:00:00 2001 From: Bjarke Tobias Olsen Date: Tue, 9 Jun 2026 12:42:13 +0200 Subject: [PATCH 05/12] Add FUGA LUT-generation parameters to wind_deficit_model (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FUGA is already in the wind_deficit_model name enum, but its on-the-fly LUT generation (pyfuga, in WIFA) had no schema. Add a `fuga` object documenting the knobs: z0 (scalar or sweep list), zi, zeta0, n_z0, ti_min/ti_max/ti_qlo/ti_qhi, nkz0/nbeta/nx/ny, lut_vars, cache_dir. All optional — omitted fields fall back to WIFA defaults (z0 derived from the site TI, swept across its distribution). Co-authored-by: Claude Opus 4.8 --- windIO/schemas/plant/wind_energy_system.yaml | 61 ++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/windIO/schemas/plant/wind_energy_system.yaml b/windIO/schemas/plant/wind_energy_system.yaml index 6b6af956..70029351 100644 --- a/windIO/schemas/plant/wind_energy_system.yaml +++ b/windIO/schemas/plant/wind_energy_system.yaml @@ -77,6 +77,67 @@ properties: A: title: TurboNOJ wake expansion parameter type: number + fuga: + title: FUGA LUT generation parameters + description: >- + Options for on-the-fly Fuga look-up-table generation (pyfuga). + Fuga has no turbulence-intensity input, so ambient turbulence + enters through the roughness z0 and stability zeta0. When z0 is + not given it is derived from the site TI. By default a sweep of + LUTs across the site TI distribution is generated so the wake + honors per-flow-case TI. All omitted fields fall back to WIFA + defaults. + type: object + properties: + z0: + title: Roughness length(s) (m) + description: Single roughness, or an explicit list of roughnesses for the LUT sweep. Overrides the TI-derived z0. + oneOf: + - type: number + - type: array + items: + type: number + zi: + title: Atmospheric inversion / boundary-layer height (m) + type: number + zeta0: + title: Monin-Obukhov stability parameter z0/L (0 = neutral) + type: number + n_z0: + title: Number of z0 LUTs in the TI sweep (1 = single mean-TI LUT) + type: integer + ti_min: + title: Lower TI clamp for the z0 sweep (keeps z0 physical) + type: number + ti_max: + title: Upper TI clamp for the z0 sweep (keeps z0 physical) + type: number + ti_qlo: + title: Lower TI quantile spanned by the sweep + type: number + ti_qhi: + title: Upper TI quantile spanned by the sweep + type: number + nkz0: + title: pyfuga spectral density (wavenumbers per log decade) + type: integer + nbeta: + title: pyfuga azimuthal resolution + type: integer + nx: + title: LUT streamwise grid points + type: integer + ny: + title: LUT cross-stream grid points + type: integer + lut_vars: + title: LUT field components to compute (e.g. ["UL"]) + type: array + items: + type: string + cache_dir: + title: Directory for generated/cached LUTs + type: string axial_induction_model: title: axial induction model From 62125b690e44be58ab32bdef6f2dd9f7b8d57263 Mon Sep 17 00:00:00 2001 From: Bjarke Tobias Olsen Date: Wed, 8 Jul 2026 11:18:36 +0200 Subject: [PATCH 06/12] Add n_cpu to the FUGA LUT-generation schema Worker-process count forwarded by WIFA to pyfuga; n_cpu=1 avoids the per-worker numba re-JIT cost of pyfuga's spawn pools for small LUTs. Co-Authored-By: Claude Fable 5 --- windIO/schemas/plant/wind_energy_system.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/windIO/schemas/plant/wind_energy_system.yaml b/windIO/schemas/plant/wind_energy_system.yaml index 70029351..f0540946 100644 --- a/windIO/schemas/plant/wind_energy_system.yaml +++ b/windIO/schemas/plant/wind_energy_system.yaml @@ -138,6 +138,9 @@ properties: cache_dir: title: Directory for generated/cached LUTs type: string + n_cpu: + title: Worker processes for LUT generation (1 = in-process) + type: integer axial_induction_model: title: axial induction model From 9b95de3bf4f554f416bb3b9aff3ea8baa4c70f85 Mon Sep 17 00:00:00 2001 From: Bjarke Tobias Olsen Date: Wed, 8 Jul 2026 11:18:36 +0200 Subject: [PATCH 07/12] State the windIO wake-expansion convention k = k_a + k_b*TI The free_stream_ti description said k = k_a*TI + k_b, inverting the roles implied by the k_a/k_b titles and defaults (k_a = constant, k_b = coefficient on TI). Document the correct formula on the wake_expansion_coefficient block itself. Co-Authored-By: Claude Fable 5 --- windIO/schemas/plant/wind_energy_system.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windIO/schemas/plant/wind_energy_system.yaml b/windIO/schemas/plant/wind_energy_system.yaml index f0540946..695fc6fc 100644 --- a/windIO/schemas/plant/wind_energy_system.yaml +++ b/windIO/schemas/plant/wind_energy_system.yaml @@ -56,6 +56,7 @@ properties: "TurboNOJ", "GCL", "FUGA"] wake_expansion_coefficient: title: wake expansion coefficient + description: Wake expansion k = k_a + k_b * TI (k_a = constant expansion, k_b = coefficient on turbulence intensity). type: object properties: k_a: @@ -66,7 +67,7 @@ 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. + description: TI feeding the wake-expansion coefficient (k = k_a + k_b*TI) and TI-dependent deficits. False (default) = waked/effective TI; True = freestream TI. type: boolean # (default to False) ceps: title: Bastankhah c_epsilon factor From 46e71bd01ee9173f8229e74ea18da8a3339586e0 Mon Sep 17 00:00:00 2001 From: btol Date: Tue, 14 Jul 2026 13:50:10 +0200 Subject: [PATCH 08/12] Add ground_mirror flag to the blockage_model schema Optional boolean (default false) that enforces the ground (slip) boundary condition on the blockage deficit via an image rotor (method of images). Blockage models are calibrated without ground effects, unlike wake deficit models, so the flag applies to the blockage model only. Ignored for FUGA, whose look-up tables already include the ground. Co-Authored-By: Claude Fable 5 --- windIO/schemas/plant/wind_energy_system.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/windIO/schemas/plant/wind_energy_system.yaml b/windIO/schemas/plant/wind_energy_system.yaml index 695fc6fc..bc039035 100644 --- a/windIO/schemas/plant/wind_energy_system.yaml +++ b/windIO/schemas/plant/wind_energy_system.yaml @@ -270,6 +270,16 @@ properties: ss_alpha: title: ss_alpha factor for SelfSimilarityDeficit2020 type: number + ground_mirror: + title: Mirror the blockage model in the ground plane + description: Enforce the ground (slip) boundary condition on the + blockage deficit by adding an image rotor below ground + (method of images). Applies to the blockage model only; wake + deficit models are typically calibrated including ground + effects and must not be mirrored. Ignored for FUGA, whose + look-up tables already include the ground. Default false. + type: boolean + default: false HPC_config: title: Information to handle parallel runs on HPC clusters From e0a671d005267a2e1a5dc42f3669e267acc663b3 Mon Sep 17 00:00:00 2001 From: btol Date: Sun, 19 Jul 2026 20:02:43 +0200 Subject: [PATCH 09/12] plant schema: add Eddy Viscosity bundle names EddyViscosity wind_deficit_model, QuartonAndAinslie turbulence_model and simplified_gaussian rotor_averaging (pyWake EV-branch bundle). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017wbMsoVker28aF72Xt5VC5 --- windIO/schemas/plant/wind_energy_system.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/windIO/schemas/plant/wind_energy_system.yaml b/windIO/schemas/plant/wind_energy_system.yaml index bc039035..229635c5 100644 --- a/windIO/schemas/plant/wind_energy_system.yaml +++ b/windIO/schemas/plant/wind_energy_system.yaml @@ -53,7 +53,7 @@ properties: type: string enum: ["Jensen", "Jensen_1983", "Bastankhah2014", "Bastankhah2016", "Niayifar2016", "Zong2020", "Carbajofuertes2018", "SuperGaussian", "SuperGaussian2023", "TurbOPark", - "TurboNOJ", "GCL", "FUGA"] + "TurboNOJ", "GCL", "FUGA", "EddyViscosity"] wake_expansion_coefficient: title: wake expansion coefficient description: Wake expansion k = k_a + k_b * TI (k_a = constant expansion, k_b = coefficient on turbulence intensity). @@ -170,7 +170,7 @@ properties: name: title: Turbulence model name type: string - enum: ["STF2005", "STF2017", "IEC-TI-2019", "CrespoHernandez", "GCL", "None"] + enum: ["STF2005", "STF2017", "IEC-TI-2019", "CrespoHernandez", "GCL", "QuartonAndAinslie", "None"] # Q: IEC versus Franson # note: We assume same shape as wind deficit for now coefficients: @@ -219,13 +219,15 @@ properties: Engine-neutral: none (no rotor-averaging model — rotor centre, the only non-node option 'Weighted' superposition accepts), center, grid (regular rotor grid / GridRotorAvg), - eq_grid, gq_grid, polar_grid, cgi, gaussian_overlap, area_overlap. + eq_grid, gq_grid, polar_grid, cgi, gaussian_overlap, area_overlap, + simplified_gaussian (LUT-based line average, the Eddy Viscosity + bundle's companion). gaussian_overlap/area_overlap are non-node overlap models — NOT compatible with 'Weighted' superposition, which requires a node model (use grid) or 'none'. Capitalized names are deprecated aliases of the lowercase forms. type: string enum: ["none", "center", "grid", "eq_grid", "gq_grid", "polar_grid", "cgi", - "gaussian_overlap", "area_overlap", + "gaussian_overlap", "area_overlap", "simplified_gaussian", "Center", "Avg_Deficit", "EqGrid", "GQGrid", "PolarGrid", "CGI"] n: title: Number of grid or integration points From ed736d96e357889ae54cf0adc7ffce3be3d08032 Mon Sep 17 00:00:00 2001 From: btol Date: Tue, 21 Jul 2026 15:39:52 +0200 Subject: [PATCH 10/12] plant schema: allow analysis.abl_setup (Gmode, dh_max, serz) Consumed by WIFA's wayve path to configure the mesoscale ABL setup from height-resolved wind resources (flow_model_chain#27, WIFA 91cb649). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BJhST9egDsb8Jd4UNyFHww --- windIO/schemas/plant/wind_energy_system.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/windIO/schemas/plant/wind_energy_system.yaml b/windIO/schemas/plant/wind_energy_system.yaml index 229635c5..ac2a0885 100644 --- a/windIO/schemas/plant/wind_energy_system.yaml +++ b/windIO/schemas/plant/wind_energy_system.yaml @@ -391,6 +391,25 @@ properties: title: APM grid filter length type: number # (default 1.e3) + abl_setup: + title: settings for the ABL setup from height-resolved wind resources + description: how the atmospheric perturbation model derives its background ABL from a height-resolved wind resource with mesoscale surface scalars (see wayve's abl_setup.mesoscale_based); ignored by scalar and turbulence-profile inputs + type: object + properties: + Gmode: + title: geostrophic wind mode + description: how the free-atmosphere velocity is derived from the profiles — at the inversion center ("h1"), at the inversion top ("h2"), averaged between the inversion and 5 km ("avg", needs profiles reaching 5 km), or averaged to the tropopause ("trop", needs profiles reaching the stratosphere) + type: string + enum: ["h1", "h2", "avg", "trop"] # (default "avg") + dh_max: + title: maximum inversion-layer depth + description: maximum depth of the inversion layer used in the inversion curve fit + type: number + serz: + title: surface-extended Rampanelli-Zardi + description: whether the surface-extended version of the Rampanelli-Zardi inversion fit is used + type: boolean # (default True) + wm_coupling: title: settings for wake model coupling in high-fidelity atmospheric models type: object From 74416704e25d129a6b26aeb57cc0796b82db3225 Mon Sep 17 00:00:00 2001 From: btol Date: Fri, 24 Jul 2026 22:12:22 +0200 Subject: [PATCH 11/12] plant schema: declare boundary_layer_height, distinguish it from ABL_height A height-resolved (mesoscale/reanalysis) wind resource carries both boundary_layer_height -- the model BLH diagnostic that, with friction_velocity and LMO, drives the surface turbulence closure -- and ABL_height, the fitted capping-inversion centre. WIFA's wayve adapter reads both, but the schema declared only the latter, so boundary_layer_height rode in undeclared. Declare it, and add descriptions to both so the two heights are not confused. Co-Authored-By: Claude Opus 4.8 (1M context) --- windIO/schemas/plant/energy_resource.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/windIO/schemas/plant/energy_resource.yaml b/windIO/schemas/plant/energy_resource.yaml index 2bc2fde4..fbee2b1a 100644 --- a/windIO/schemas/plant/energy_resource.yaml +++ b/windIO/schemas/plant/energy_resource.yaml @@ -122,6 +122,23 @@ properties: $ref: "./common.yaml#/definitions/multi_dimensional_data" ABL_height: title: atmoshpheric boundary layer height + description: >- + Height of the centre of the capping inversion, i.e. the top of + the well-mixed boundary layer. Together with + capping_inversion_thickness it bounds the inversion layer + (bottom = ABL_height - thickness/2, top = ABL_height + + thickness/2). Distinct from boundary_layer_height, which is a + reanalysis surface diagnostic. + $ref: "./common.yaml#/definitions/multi_dimensional_data" + boundary_layer_height: + title: Reanalysis boundary-layer height (m) + description: >- + Boundary-layer height as diagnosed by a mesoscale model or + reanalysis (e.g. ERA5 blh). Used together with + friction_velocity and LMO to build the surface-driven + turbulence closure of a height-resolved wind resource. It is + a model diagnostic, not the fitted inversion height -- see + ABL_height. $ref: "./common.yaml#/definitions/multi_dimensional_data" capping_inversion_thickness: title: Capping inversion thickness in units of meters From c8ba6db8135c421eb33e546d9dba1e1e75e9a6b1 Mon Sep 17 00:00:00 2001 From: btol Date: Tue, 28 Jul 2026 13:05:21 +0200 Subject: [PATCH 12/12] plant schema: document number_of_fa_layers <= 0 wayve reads a non-positive layer count as "use the vertical grid of the wind resource profiles", one gravity-wave sublayer per level, rather than an arbitrary number over the same range. The field already allowed it; say so. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JWxbyEe8CKC9Nj7BXfo63N --- windIO/schemas/plant/wind_energy_system.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/windIO/schemas/plant/wind_energy_system.yaml b/windIO/schemas/plant/wind_energy_system.yaml index ac2a0885..d8dc01b6 100644 --- a/windIO/schemas/plant/wind_energy_system.yaml +++ b/windIO/schemas/plant/wind_energy_system.yaml @@ -339,7 +339,10 @@ properties: title: height of the layer containing the farm type: number number_of_fa_layers: - title: number of layers in the free atmosphere + title: >- + number of layers in the free atmosphere; 1 requests a bulk + (uniform) free atmosphere, and a value <= 0 resolves it on + the vertical grid of the wind resource profiles type: integer APM_additional_terms: