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/ diff --git a/pyproject.toml b/pyproject.toml index 5d64209..56c5d0f 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", + "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..1495535 100644 --- a/tests/test_floris.py +++ b/tests/test_floris.py @@ -9,12 +9,21 @@ import os import shutil +import sys from pathlib import Path -import floris 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]" +) + +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..54791a3 100644 --- a/tests/test_foxes.py +++ b/tests/test_foxes.py @@ -3,6 +3,12 @@ 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..5bd4003 100644 --- a/tests/test_pywake.py +++ b/tests/test_pywake.py @@ -4,6 +4,11 @@ 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..4957f04 100644 --- a/tests/test_wayve.py +++ b/tests/test_wayve.py @@ -1,6 +1,12 @@ 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..b933d59 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,9 +670,11 @@ 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 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 @@ -824,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 (