From f2aa2e8a7006986830c8ef4af7154718b0367c10 Mon Sep 17 00:00:00 2001 From: btol Date: Wed, 25 Feb 2026 15:53:12 +0100 Subject: [PATCH 1/7] Make all wrapped tools optional dependencies Move pywake, floris, foxes, wayve, and code_saturne from hard dependencies to optional extras in pyproject.toml. Users can now install only the tools they need: `pip install wifa[pywake]`, `pip install wifa[foxes]`, etc., or all at once with `wifa[all]`. - Add `wifa/_optional.py` with `require()` helper that gives clear install instructions when an optional tool is missing - Add `require()` checks at the top of each `run_*` function - Add `pytest.importorskip()` to test modules so tests skip cleanly when a tool is not installed - Move matplotlib and mpmath to lazy imports in wayve_api.py - Move FoxesWakeModel import into the foxes branch of wake_model_setup() - Add scipy and pyyaml as core dependencies (used at module level) - Add missing run_floris export in __init__.py Closes #50 Co-Authored-By: Claude Opus 4.6 --- pyproject.toml | 21 ++++++++++++++++----- tests/test_floris.py | 5 ++++- tests/test_foxes.py | 4 ++++ tests/test_pywake.py | 3 +++ tests/test_wayve.py | 4 ++++ wifa/__init__.py | 1 + wifa/_optional.py | 10 ++++++++++ wifa/floris_api.py | 3 +++ wifa/foxes_api.py | 3 +++ wifa/pywake_api.py | 4 ++++ wifa/wayve_api.py | 14 ++++++++++---- 11 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 wifa/_optional.py diff --git a/pyproject.toml b/pyproject.toml index 5d64209..728106e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,16 +38,27 @@ classifiers = [ ] requires-python = ">=3.9,<3.12" dependencies = [ - "py_wake>=2.6.5", - "foxes>=1.6.2", "windIO @ git+https://github.com/EUFlow/windIO.git", - "wayve @ git+https://gitlab.kuleuven.be/TFSO-software/wayve@dev_foxes", - "floris @ git+https://github.com/lejeunemax/floris.git@windIO", "xarray>=2022.0.0,<2025", - "mpmath", + "scipy", + "pyyaml", ] [project.optional-dependencies] +pywake = ["py_wake>=2.6.5"] +foxes = ["foxes>=1.6.2"] +floris = ["floris @ git+https://github.com/lejeunemax/floris.git@windIO"] +wayve = [ + "wayve @ git+https://gitlab.kuleuven.be/TFSO-software/wayve@dev_foxes", + "mpmath", +] +cs = [] # code_saturne is a subprocess, no pip dependency +all = [ + "wifa[pywake]", + "wifa[foxes]", + "wifa[floris]", + "wifa[wayve]", +] test = [ "pytest", "pytest-cov", diff --git a/tests/test_floris.py b/tests/test_floris.py index 2f8cd8e..c4fb51b 100644 --- a/tests/test_floris.py +++ b/tests/test_floris.py @@ -11,10 +11,13 @@ import shutil from pathlib import Path -import floris import numpy as np import pytest import xarray as xr + +pytest.importorskip("floris", reason="floris not installed, install with: pip install wifa[floris]") + +import floris from floris.turbine_library import build_cosine_loss_turbine_dict from windIO import __path__ as wiop from windIO import load_yaml diff --git a/tests/test_foxes.py b/tests/test_foxes.py index deb17de..2c3e8b7 100644 --- a/tests/test_foxes.py +++ b/tests/test_foxes.py @@ -3,6 +3,10 @@ from shutil import rmtree import numpy as np +import pytest + +pytest.importorskip("foxes", reason="foxes not installed, install with: pip install wifa[foxes]") + from windIO import __path__ as wiop from windIO import validate as validate_yaml diff --git a/tests/test_pywake.py b/tests/test_pywake.py index 3c01eb2..a0414b0 100644 --- a/tests/test_pywake.py +++ b/tests/test_pywake.py @@ -4,6 +4,9 @@ import numpy as np import pytest import xarray as xr + +pytest.importorskip("py_wake", reason="py_wake not installed, install with: pip install wifa[pywake]") + from py_wake.deficit_models.gaussian import BastankhahGaussian from py_wake.examples.data.dtu10mw._dtu10mw import DTU10MW from py_wake.examples.data.hornsrev1 import Hornsrev1Site diff --git a/tests/test_wayve.py b/tests/test_wayve.py index 902edf0..07024d3 100644 --- a/tests/test_wayve.py +++ b/tests/test_wayve.py @@ -1,6 +1,10 @@ import os from pathlib import Path +import pytest + +pytest.importorskip("wayve", reason="wayve not installed, install with: pip install wifa[wayve]") + from windIO import __path__ as wiop from windIO import validate as validate_yaml diff --git a/wifa/__init__.py b/wifa/__init__.py index 88be468..005a376 100644 --- a/wifa/__init__.py +++ b/wifa/__init__.py @@ -1,6 +1,7 @@ # __init__.py from .cs_api.cs_modules.csLaunch.cs_run_function import run_code_saturne +from .floris_api import run_floris from .foxes_api import run_foxes from .main_api import run_api from .pywake_api import run_pywake diff --git a/wifa/_optional.py b/wifa/_optional.py new file mode 100644 index 0000000..0870f42 --- /dev/null +++ b/wifa/_optional.py @@ -0,0 +1,10 @@ +import importlib.util + + +def require(package_name: str, extra_name: str) -> None: + """Raise a clear ImportError if an optional dependency is missing.""" + if importlib.util.find_spec(package_name) is None: + raise ImportError( + f"'{package_name}' is required for this functionality but is not installed. " + f"Install it with: pip install wifa[{extra_name}]" + ) diff --git a/wifa/floris_api.py b/wifa/floris_api.py index b5d5812..7a23c94 100644 --- a/wifa/floris_api.py +++ b/wifa/floris_api.py @@ -5,6 +5,8 @@ import windIO from windIO import load_yaml +from wifa._optional import require + if TYPE_CHECKING: from floris import FlorisModel @@ -30,6 +32,7 @@ def run_floris(yaml_input): - Some advanced windIO properties may not be fully supported. These include: blockage_model, rotor_averaging, and axial_induction_model. """ + require("floris", "floris") from floris import FlorisModel from floris.read_windio import TrackedDict diff --git a/wifa/foxes_api.py b/wifa/foxes_api.py index 8c97417..09f240a 100644 --- a/wifa/foxes_api.py +++ b/wifa/foxes_api.py @@ -3,6 +3,8 @@ from windIO import load_yaml +from wifa._optional import require + def run_foxes( input_yaml, @@ -54,6 +56,7 @@ def run_foxes( foxes output class """ + require("foxes", "foxes") from foxes.input.yaml import run_dict from foxes.input.yaml.windio import read_windio_dict diff --git a/wifa/pywake_api.py b/wifa/pywake_api.py index 703f5dd..2f3c02f 100644 --- a/wifa/pywake_api.py +++ b/wifa/pywake_api.py @@ -11,6 +11,8 @@ from windIO import dict_to_netcdf, load_yaml from windIO import validate as validate_yaml +from wifa._optional import require + # Define default values for wind_deficit_model parameters DEFAULTS = { "wind_deficit_model": { @@ -1052,6 +1054,8 @@ def run_pywake(yaml_input, output_dir="output"): float: Total AEP in GWh """ # Step 1: Load and validate configuration + require("py_wake", "pywake") + system_dat, output_dir = load_and_validate_config(yaml_input, output_dir) # Step 2: Create turbine objects diff --git a/wifa/wayve_api.py b/wifa/wayve_api.py index 768df92..79601b1 100644 --- a/wifa/wayve_api.py +++ b/wifa/wayve_api.py @@ -3,16 +3,18 @@ import warnings from pathlib import Path -import matplotlib.pyplot as plt -import mpmath import numpy as np import xarray as xr from scipy.interpolate import interp1d from scipy.special import gamma as scipy_gamma from windIO import load_yaml +from wifa._optional import require + def run_wayve(yamlFile, output_dir="output", debug_mode=False): + require("wayve", "wayve") + # General APM setup from wayve.apm import APM from wayve.grid.grid import Stat2Dgrid @@ -271,6 +273,8 @@ def run_wayve(yamlFile, output_dir="output", debug_mode=False): def nieuwstadt83_profiles(zh, v, wd, z0=1.0e-1, h=1.5e3, fc=1.0e-4, ust=0.666): """Set up the cubic analytical profile from Nieuwstadt (1983), based on hub height velocity information""" + import mpmath + # Atmospheric state setup from wayve.abl.abl_tools import Cg_cubic, alpha_cubic @@ -393,6 +397,8 @@ def rotate_xy_arrays(xs, ys, angle): def ci_fitting( zs, ths, l_mo=5.0e3, blh=1.0e3, dh_max=300.0, serz=True, plot_fits=False ): + import matplotlib.pyplot as plt + # Atmospheric state setup from wayve.abl import ci_methods @@ -638,8 +644,6 @@ def wm_coupling_setup(analysis_dat, wake_model): def wake_model_setup(analysis_dat, debug_mode=False): - # WAYVE imports - from wayve.couplings.foxes_coupling import FoxesWakeModel from wayve.forcing.wind_farms.wake_model_coupling.wake_models.lanzilao_merging import ( Lanzilao, ) @@ -666,6 +670,8 @@ def wake_model_setup(analysis_dat, debug_mode=False): # Use wake merging method of Lanzilao and Meyers (2021) wake_model = Lanzilao(ka=k_a, kb=k_b, eps_beta=ceps) elif wake_tool == "foxes": + require("foxes", "foxes") + from wayve.couplings.foxes_coupling import FoxesWakeModel from foxes import ModelBook from foxes.input.yaml.windio.read_attributes import _read_analysis from foxes.utils import Dict From 4dd492c1493612ae62430717358203316373e646 Mon Sep 17 00:00:00 2001 From: btol Date: Wed, 25 Feb 2026 15:56:52 +0100 Subject: [PATCH 2/7] Fix pre-commit: black line length and isort import order Co-Authored-By: Claude Opus 4.6 --- tests/test_floris.py | 4 +++- tests/test_foxes.py | 4 +++- tests/test_pywake.py | 4 +++- tests/test_wayve.py | 4 +++- wifa/wayve_api.py | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/test_floris.py b/tests/test_floris.py index c4fb51b..653deed 100644 --- a/tests/test_floris.py +++ b/tests/test_floris.py @@ -15,7 +15,9 @@ import pytest import xarray as xr -pytest.importorskip("floris", reason="floris not installed, install with: pip install wifa[floris]") +pytest.importorskip( + "floris", reason="floris not installed, install with: pip install wifa[floris]" +) import floris from floris.turbine_library import build_cosine_loss_turbine_dict diff --git a/tests/test_foxes.py b/tests/test_foxes.py index 2c3e8b7..54791a3 100644 --- a/tests/test_foxes.py +++ b/tests/test_foxes.py @@ -5,7 +5,9 @@ import numpy as np import pytest -pytest.importorskip("foxes", reason="foxes not installed, install with: pip install wifa[foxes]") +pytest.importorskip( + "foxes", reason="foxes not installed, install with: pip install wifa[foxes]" +) from windIO import __path__ as wiop from windIO import validate as validate_yaml diff --git a/tests/test_pywake.py b/tests/test_pywake.py index a0414b0..5bd4003 100644 --- a/tests/test_pywake.py +++ b/tests/test_pywake.py @@ -5,7 +5,9 @@ import pytest import xarray as xr -pytest.importorskip("py_wake", reason="py_wake not installed, install with: pip install wifa[pywake]") +pytest.importorskip( + "py_wake", reason="py_wake not installed, install with: pip install wifa[pywake]" +) from py_wake.deficit_models.gaussian import BastankhahGaussian from py_wake.examples.data.dtu10mw._dtu10mw import DTU10MW diff --git a/tests/test_wayve.py b/tests/test_wayve.py index 07024d3..4957f04 100644 --- a/tests/test_wayve.py +++ b/tests/test_wayve.py @@ -3,7 +3,9 @@ import pytest -pytest.importorskip("wayve", reason="wayve not installed, install with: pip install wifa[wayve]") +pytest.importorskip( + "wayve", reason="wayve not installed, install with: pip install wifa[wayve]" +) from windIO import __path__ as wiop from windIO import validate as validate_yaml diff --git a/wifa/wayve_api.py b/wifa/wayve_api.py index 79601b1..d0ae9d3 100644 --- a/wifa/wayve_api.py +++ b/wifa/wayve_api.py @@ -671,10 +671,10 @@ def wake_model_setup(analysis_dat, debug_mode=False): wake_model = Lanzilao(ka=k_a, kb=k_b, eps_beta=ceps) elif wake_tool == "foxes": require("foxes", "foxes") - from wayve.couplings.foxes_coupling import FoxesWakeModel from foxes import ModelBook from foxes.input.yaml.windio.read_attributes import _read_analysis from foxes.utils import Dict + from wayve.couplings.foxes_coupling import FoxesWakeModel verbosity = 1 if debug_mode else 0 From fc824f3424b4e04765a08101284dc69281a0e621 Mon Sep 17 00:00:00 2001 From: btol Date: Wed, 25 Feb 2026 16:01:06 +0100 Subject: [PATCH 3/7] Install all tool extras in CI so all tests run Co-Authored-By: Claude Opus 4.6 --- .github/workflows/run_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 330cdeb..1fee02d 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -44,7 +44,7 @@ jobs: apt-get update && apt-get install -y git-lfs git lfs install pip install --upgrade pip - pip install -e .[test] + pip install -e ".[all,test]" - name: Test with pytest run: | py.test --durations=0 tests/ From 3b75ad62883dfab319a988fddf897afd54a5c613 Mon Sep 17 00:00:00 2001 From: btol Date: Wed, 25 Feb 2026 16:04:41 +0100 Subject: [PATCH 4/7] Fix numpy 2.x compatibility: replace np.trapz with np.trapezoid np.trapz was removed in numpy 2.0. Use np.trapezoid with fallback for older numpy versions. Co-Authored-By: Claude Opus 4.6 --- wifa/wayve_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/wifa/wayve_api.py b/wifa/wayve_api.py index d0ae9d3..b933d59 100644 --- a/wifa/wayve_api.py +++ b/wifa/wayve_api.py @@ -830,8 +830,9 @@ def flow_io_abl(wind_resource_dat, time_index, zh, h1, dh_max=None, serz=True): ) # Geostrophic wind speed z = np.linspace(h, 15.0e3, 1000) - U3 = np.trapz(np.interp(z, zs, us), z) / (15.0e3 - h) - V3 = np.trapz(np.interp(z, zs, vs), z) / (15.0e3 - h) + _trapz = getattr(np, "trapezoid", np.trapz) + U3 = _trapz(np.interp(z, zs, us), z) / (15.0e3 - h) + V3 = _trapz(np.interp(z, zs, vs), z) / (15.0e3 - h) # Upper layer thickness h2 = h - h1 if ( From 928c81a18ac7a58df4252f7c3822705ca62427ce Mon Sep 17 00:00:00 2001 From: btol Date: Wed, 25 Feb 2026 16:07:28 +0100 Subject: [PATCH 5/7] Skip floris tests on Python < 3.10 floris uses PEP 604 union types (str | Path) which require Python 3.10+. Co-Authored-By: Claude Opus 4.6 --- tests/test_floris.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_floris.py b/tests/test_floris.py index 653deed..1495535 100644 --- a/tests/test_floris.py +++ b/tests/test_floris.py @@ -9,12 +9,16 @@ import os import shutil +import sys from pathlib import Path import numpy as np import pytest import xarray as xr +if sys.version_info < (3, 10): + pytest.skip("floris requires Python >= 3.10", allow_module_level=True) + pytest.importorskip( "floris", reason="floris not installed, install with: pip install wifa[floris]" ) From 7e174e91f5c701c12cb96a5b358f8ba94fb51645 Mon Sep 17 00:00:00 2001 From: btol Date: Wed, 25 Feb 2026 16:12:33 +0100 Subject: [PATCH 6/7] Pin numpy<2 for wayve extra (wayve uses removed np.trapz) Co-Authored-By: Claude Opus 4.6 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 728106e..51481ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ floris = ["floris @ git+https://github.com/lejeunemax/floris.git@windIO"] wayve = [ "wayve @ git+https://gitlab.kuleuven.be/TFSO-software/wayve@dev_foxes", "mpmath", + "numpy<2", # wayve uses np.trapz, removed in numpy 2.0 ] cs = [] # code_saturne is a subprocess, no pip dependency all = [ From c7d34c6a8722c593c277917b82927e63eee19182 Mon Sep 17 00:00:00 2001 From: btol Date: Wed, 25 Feb 2026 16:17:56 +0100 Subject: [PATCH 7/7] Remove numpy<2 pin: wayve main branch has np.trapz fix Also update wayve URL to main branch. Co-Authored-By: Claude Opus 4.6 --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 51481ba..56c5d0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,9 +49,8 @@ pywake = ["py_wake>=2.6.5"] foxes = ["foxes>=1.6.2"] floris = ["floris @ git+https://github.com/lejeunemax/floris.git@windIO"] wayve = [ - "wayve @ git+https://gitlab.kuleuven.be/TFSO-software/wayve@dev_foxes", + "wayve @ git+https://gitlab.kuleuven.be/TFSO-software/wayve", "mpmath", - "numpy<2", # wayve uses np.trapz, removed in numpy 2.0 ] cs = [] # code_saturne is a subprocess, no pip dependency all = [