From c7ac71bd67da03773c47db5ecf481afea60c399f Mon Sep 17 00:00:00 2001 From: Julian Quick Date: Wed, 1 Jul 2026 22:37:52 -0700 Subject: [PATCH] fix(pywake): support circle-based site boundaries construct_site assumed boundaries.polygons and raised KeyError for the windIO circle boundary type. Flow-field bounds now come from a _site_bounds helper that handles both types; polygon bounds also span all polygons instead of only the first. Closes #26 --- tests/test_pywake.py | 34 ++++++++++++++++++++++++++++++++++ wifa/pywake_api.py | 31 ++++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/tests/test_pywake.py b/tests/test_pywake.py index 2605cf5..ef3e2ee 100644 --- a/tests/test_pywake.py +++ b/tests/test_pywake.py @@ -466,6 +466,40 @@ def test_pywake_dict_timeseries_per_turbine_with_density(tmp_path): assert aep_with != aep_without +def test_site_bounds_circle_and_polygons(): + from wifa.pywake_api import _site_bounds + + # Circle: bounding box is center +/- radius + assert _site_bounds( + {"circle": {"center": {"x": 100.0, "y": -50.0}, "radius": 500.0}} + ) == (-400.0, 600.0, -550.0, 450.0) + + # Polygons: bounding box spans all polygons, not just the first + assert _site_bounds( + { + "polygons": [ + {"x": [0, 10], "y": [0, 10]}, + {"x": [-5, 3], "y": [2, 20]}, + ] + } + ) == (-5, 10, 0, 20) + + with pytest.raises(ValueError, match="polygons.*or.*circle"): + _site_bounds({}) + + +def test_pywake_circle_boundaries(tmp_path): + """Circle-based site boundaries run through pywake (GH issue #26).""" + from conftest import make_timeseries_per_turbine_system_dict + + system_dict = make_timeseries_per_turbine_system_dict("pywake") + system_dict["site"]["boundaries"] = { + "circle": {"center": {"x": 1250.0, "y": 0.0}, "radius": 1500.0} + } + aep = run_pywake(system_dict, output_dir=str(tmp_path)) + assert np.isfinite(aep) and aep > 0 + + # if __name__ == "__main__": # test_heterogeneous_wind_rose() # simple_yaml_to_pywake('../examples/cases/windio_4turbines_multipleTurbines/plant_energy_turbine/IEA_10MW_turbine.yaml') diff --git a/wifa/pywake_api.py b/wifa/pywake_api.py index 91cf99a..bfe019e 100644 --- a/wifa/pywake_api.py +++ b/wifa/pywake_api.py @@ -254,6 +254,31 @@ def get_flow_field_param(system_dat, param_name, default=None): return default +def _site_bounds(boundaries): + """Bounding box (xlb, xub, ylb, yub) of the windIO site boundaries. + + Supports both boundary types of the windIO site schema: a group of + polygons, or a circle with center and radius. + """ + if "polygons" in boundaries: + xs = np.concatenate([np.atleast_1d(p["x"]) for p in boundaries["polygons"]]) + ys = np.concatenate([np.atleast_1d(p["y"]) for p in boundaries["polygons"]]) + return np.min(xs), np.max(xs), np.min(ys), np.max(ys) + if "circle" in boundaries: + center = boundaries["circle"]["center"] + radius = boundaries["circle"]["radius"] + return ( + center["x"] - radius, + center["x"] + radius, + center["y"] - radius, + center["y"] + radius, + ) + raise ValueError( + "Site boundaries must define either 'polygons' or 'circle', " + f"got: {list(boundaries)}" + ) + + def construct_site(system_dat, resource_dat, hub_heights, x_positions): """Construct site object and wind conditions for simulation. @@ -272,11 +297,7 @@ def construct_site(system_dat, resource_dat, hub_heights, x_positions): from windIO import dict_to_netcdf # Get flow field bounds from config or site boundaries - boundaries = system_dat["site"]["boundaries"]["polygons"][0] - WFXLB = np.min(boundaries["x"]) - WFXUB = np.max(boundaries["x"]) - WFYLB = np.min(boundaries["y"]) - WFYUB = np.max(boundaries["y"]) + WFXLB, WFXUB, WFYLB, WFYUB = _site_bounds(system_dat["site"]["boundaries"]) # Override with explicit flow field bounds if specified WFXLB = get_flow_field_param(system_dat, "xlb", WFXLB)