Skip to content

Commit 2b9c9cf

Browse files
bjarketolclaude
andcommitted
Fix tests, refactor pywake_api, and clean up test infrastructure
- Add test cleanup fixtures and output_dir management (conftest.py) - Simplify foxes engine handling and fix test compatibility - Split run_pywake into focused helper functions - Add foxes turbine-based data test with density support - Fix wayve test performance (510s to 6s) - Pin numpy<2.0 for wayve compatibility (np.trapz removal) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d595e6d commit 2b9c9cf

9 files changed

Lines changed: 177 additions & 102 deletions

File tree

examples/cases/KUL_LES/wind_energy_system/analysis_US.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ HPC_config:
6464
mesh_node_number: 2
6565
mesh_ntasks_per_node: 48
6666
mesh_wall_time_hours: 1
67-
run_partition: ""
67+
mesh_partition: ""
6868
#
6969
wckey: ""
7070

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies = [
4343
"windIO @ git+https://github.com/EUFlow/windIO.git",
4444
"wayve @ git+https://gitlab.kuleuven.be/TFSO-software/wayve@dev_foxes",
4545
"floris @ git+https://github.com/lejeunemax/floris.git@windIO",
46+
"numpy<2.0",
4647
"xarray>=2022.0.0,<2025",
4748
"mpmath",
4849
]

tests/conftest.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,110 @@
1+
"""
2+
Pytest configuration and fixtures for WIFA tests.
3+
4+
Provides:
5+
- Pre-test cleanup of leftover output directories
6+
- Output directory fixtures with conditional cleanup (preserved on failure)
7+
"""
8+
9+
import shutil
10+
from pathlib import Path
11+
112
import numpy as np
13+
import pytest
14+
15+
# Store test outcomes for conditional cleanup
16+
_test_outcomes = {}
17+
18+
19+
@pytest.hookimpl(hookwrapper=True)
20+
def pytest_runtest_makereport(item, call):
21+
"""Track test outcomes to conditionally preserve output on failure."""
22+
outcome = yield
23+
report = outcome.get_result()
24+
if report.failed:
25+
_test_outcomes[item.nodeid] = True
26+
27+
28+
def pytest_configure(config):
29+
"""Register custom markers."""
30+
config.addinivalue_line(
31+
"markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')"
32+
)
33+
34+
35+
@pytest.fixture(scope="session", autouse=True)
36+
def cleanup_old_outputs():
37+
"""Remove test output directories from previous test runs at session start."""
38+
patterns = [
39+
"output_pywake_*",
40+
"output_test_*",
41+
]
42+
for pattern in patterns:
43+
for path in Path(".").glob(pattern):
44+
if path.is_dir():
45+
shutil.rmtree(path)
46+
elif path.is_file():
47+
path.unlink()
48+
yield
49+
50+
51+
@pytest.fixture
52+
def output_dir(request, tmp_path):
53+
"""
54+
Provide a unique temporary output directory for tests.
55+
56+
Cleans up automatically on test success, preserves on failure for debugging.
57+
"""
58+
yield tmp_path
59+
60+
# Only clean up if test passed
61+
test_failed = _test_outcomes.get(request.node.nodeid, False)
62+
if not test_failed:
63+
if tmp_path.exists():
64+
shutil.rmtree(tmp_path)
65+
66+
67+
@pytest.fixture
68+
def named_output_dir(request):
69+
"""
70+
Provide a named output directory based on test name.
71+
72+
Use this when tests need output in the current working directory
73+
(e.g., for Code Saturne integration).
74+
75+
Cleans up automatically on test success, preserves on failure.
76+
"""
77+
test_name = request.node.name.replace("[", "_").replace("]", "_").rstrip("_")
78+
output_path = Path(f"output_test_{test_name}")
79+
output_path.mkdir(parents=True, exist_ok=True)
80+
81+
yield output_path
82+
83+
# Only clean up if test passed
84+
test_failed = _test_outcomes.get(request.node.nodeid, False)
85+
if not test_failed:
86+
if output_path.exists():
87+
shutil.rmtree(output_path)
88+
89+
90+
@pytest.fixture
91+
def cleanup_output_dir(request):
92+
"""
93+
Cleanup fixture for tests that write to the default 'output/' directory.
94+
95+
Use this when tests can't control their output location (e.g., when YAML
96+
specifies output_folder). Only cleans up directories created during the test.
97+
"""
98+
output_path = Path("output")
99+
existed_before = output_path.exists()
100+
101+
yield output_path
102+
103+
# Only clean up if test passed and the directory was created by the test
104+
test_failed = _test_outcomes.get(request.node.nodeid, False)
105+
if not test_failed and not existed_before:
106+
if output_path.exists():
107+
shutil.rmtree(output_path)
2108

3109
# DTU 10MW turbine data
4110
# (from examples/cases/windio_4turbines/plant_energy_turbine/DTU_10MW_turbine.yaml)

tests/test_cs.py

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,51 +11,46 @@
1111

1212

1313
def _run_cs(wes_dir, output_dir):
14+
"""Run Code Saturne on all system* files in the given directory."""
1415
i = 1
1516
for yaml_input in wes_dir.glob("system*"):
1617
print("\nRUNNING CODE_SATURNE ON", yaml_input, "\n")
1718
validate_yaml(yaml_input, Path("plant/wind_energy_system"))
19+
# Pass subdirectory path - run_code_saturne will create it
20+
sub_output_dir = output_dir / f"run_{i}"
1821
run_code_saturne(
1922
yaml_input,
2023
test_mode=False,
21-
output_dir="output_test_" + output_dir + "_" + str(i),
24+
output_dir=str(sub_output_dir),
2225
)
2326
i += 1
2427

2528

26-
def test_cs_KUL():
29+
def test_cs_KUL(output_dir):
2730
wes_dir = test_path / "../examples/cases/KUL_LES/wind_energy_system/"
28-
_run_cs(wes_dir, "KUL")
31+
_run_cs(wes_dir, output_dir)
2932

3033

31-
def test_cs_4wts():
34+
def test_cs_4wts(output_dir):
3235
wes_dir = test_path / "../examples/cases/windio_4turbines/wind_energy_system/"
33-
_run_cs(wes_dir, "4wts")
36+
_run_cs(wes_dir, output_dir)
3437

3538

36-
def test_cs_abl():
39+
def test_cs_abl(output_dir):
3740
wes_dir = test_path / "../examples/cases/windio_4turbines_ABL/wind_energy_system/"
38-
_run_cs(wes_dir, "abl")
41+
_run_cs(wes_dir, output_dir)
3942

4043

41-
def test_cs_abl_stable():
44+
def test_cs_abl_stable(output_dir):
4245
wes_dir = (
4346
test_path / "../examples/cases/windio_4turbines_ABL_stable/wind_energy_system/"
4447
)
45-
_run_cs(wes_dir, "abl_stable")
48+
_run_cs(wes_dir, output_dir)
4649

4750

48-
def test_cs_profiles():
51+
def test_cs_profiles(output_dir):
4952
wes_dir = (
5053
test_path
5154
/ "../examples/cases/windio_4turbines_profiles_stable/wind_energy_system/"
5255
)
53-
_run_cs(wes_dir, "profiles")
54-
55-
56-
if __name__ == "__main__":
57-
test_cs_KUL()
58-
test_cs_4wts()
59-
test_cs_abl()
60-
test_cs_abl_stable()
61-
test_cs_profiles()
56+
_run_cs(wes_dir, output_dir)

tests/test_foxes.py

Lines changed: 26 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,112 +12,93 @@
1212
windIO_path = Path(wiop[0])
1313

1414

15-
def _run_foxes(wes_dir):
15+
def _run_foxes(wes_dir, output_dir):
16+
"""Run FOXES on all system.yaml files in the given directory."""
1617
assert wes_dir.is_dir(), f"{wes_dir} is not a directory"
1718

1819
for yaml_input in wes_dir.glob("system.yaml"):
1920
print("\nRUNNING FOXES ON", yaml_input, "\n")
2021
validate_yaml(yaml_input, Path("plant/wind_energy_system"))
21-
output_dir_name = Path("output_test_foxes")
22-
output_dir_name.mkdir(parents=True, exist_ok=True)
23-
run_foxes(yaml_input, output_dir=output_dir_name)
24-
rmtree(output_dir_name)
22+
run_foxes(yaml_input, output_dir=output_dir)
2523

2624

27-
def test_foxes_KUL():
25+
def test_foxes_KUL(output_dir):
2826
wes_dir = test_path / "../examples/cases/KUL_LES/wind_energy_system/"
29-
_run_foxes(wes_dir)
27+
_run_foxes(wes_dir, output_dir)
3028

3129

32-
def test_foxes_4wts():
30+
def test_foxes_4wts(output_dir):
3331
wes_dir = test_path / "../examples/cases/windio_4turbines/wind_energy_system/"
34-
_run_foxes(wes_dir)
32+
_run_foxes(wes_dir, output_dir)
3533

3634

37-
def test_foxes_abl():
35+
def test_foxes_abl(output_dir):
3836
wes_dir = test_path / "../examples/cases/windio_4turbines_ABL/wind_energy_system/"
39-
_run_foxes(wes_dir)
37+
_run_foxes(wes_dir, output_dir)
4038

4139

42-
def test_foxes_abl_stable():
40+
def test_foxes_abl_stable(output_dir):
4341
wes_dir = (
4442
test_path / "../examples/cases/windio_4turbines_ABL_stable/wind_energy_system/"
4543
)
46-
_run_foxes(wes_dir)
44+
_run_foxes(wes_dir, output_dir)
4745

4846

49-
def test_foxes_profiles():
47+
def test_foxes_profiles(output_dir):
5048
wes_dir = (
5149
test_path
5250
/ "../examples/cases/windio_4turbines_profiles_stable/wind_energy_system/"
5351
)
54-
_run_foxes(wes_dir)
52+
_run_foxes(wes_dir, output_dir)
5553

5654

57-
def test_foxes_heterogeneous_wind_rose_at_turbines():
55+
def test_foxes_heterogeneous_wind_rose_at_turbines(output_dir):
5856
wes_dir = (
5957
test_path
6058
/ "../examples/cases/heterogeneous_wind_rose_at_turbines/wind_energy_system/"
6159
)
62-
_run_foxes(wes_dir)
60+
_run_foxes(wes_dir, output_dir)
6361

6462

65-
def test_foxes_heterogeneous_wind_rose_map():
63+
def test_foxes_heterogeneous_wind_rose_map(output_dir):
6664
wes_dir = (
6765
test_path / "../examples/cases/heterogeneous_wind_rose_map/wind_energy_system/"
6866
)
69-
_run_foxes(wes_dir)
67+
_run_foxes(wes_dir, output_dir)
7068

7169

72-
def test_foxes_simple_wind_rose():
70+
def test_foxes_simple_wind_rose(output_dir):
7371
wes_dir = test_path / "../examples/cases/simple_wind_rose/wind_energy_system/"
74-
_run_foxes(wes_dir)
72+
_run_foxes(wes_dir, output_dir)
7573

7674

77-
def test_foxes_timeseries_with_operating_flag():
75+
def test_foxes_timeseries_with_operating_flag(output_dir):
7876
wes_dir = (
7977
test_path
8078
/ "../examples/cases/timeseries_with_operating_flag/wind_energy_system/"
8179
)
82-
_run_foxes(wes_dir)
80+
_run_foxes(wes_dir, output_dir)
8381

8482

85-
def test_timeseries_per_turbine_with_density(tmp_path=Path(".")):
83+
def test_timeseries_per_turbine_with_density(output_dir):
8684
import foxes.variables as FV
8785
from conftest import make_timeseries_per_turbine_system_dict
8886

8987
# Run with density
9088
system_dict = make_timeseries_per_turbine_system_dict("foxes")
91-
output_dir = tmp_path / "output_foxes_ts"
92-
farm_results = run_foxes(system_dict, verbosity=0, output_dir=str(output_dir))[0]
89+
out_with = output_dir / "output_foxes_ts"
90+
farm_results = run_foxes(system_dict, verbosity=0, output_dir=str(out_with))[0]
9391
farmP_with = farm_results[FV.P].sum()
94-
# print("Farm power with density:", farmP_with)
9592
assert np.isfinite(farmP_with) and farmP_with > 0
9693

9794
# Run without density — same config but density removed
9895
system_dict_no = make_timeseries_per_turbine_system_dict("foxes")
9996
del system_dict_no["site"]["energy_resource"]["wind_resource"]["density"]
100-
output_dir_no = tmp_path / "output_foxes_ts_no_density"
97+
out_without = output_dir / "output_foxes_ts_no_density"
10198
farm_results_no = run_foxes(
102-
system_dict_no, verbosity=0, output_dir=str(output_dir_no)
99+
system_dict_no, verbosity=0, output_dir=str(out_without)
103100
)[0]
104101
farmP_without = farm_results_no[FV.P].sum()
105-
# print("Farm power without density:", farmP_without)
106-
107-
rmtree(output_dir)
108-
rmtree(output_dir_no)
109102

110103
# Density correction should change AEP (test data varies around 1.225)
111104
assert farmP_with != farmP_without
112-
113-
114-
if __name__ == "__main__":
115-
test_foxes_KUL()
116-
test_foxes_4wts()
117-
test_foxes_abl()
118-
test_foxes_abl_stable()
119-
test_foxes_profiles()
120-
test_foxes_heterogeneous_wind_rose_at_turbines()
121-
test_foxes_heterogeneous_wind_rose_map()
122-
test_foxes_simple_wind_rose()
123-
test_timeseries_per_turbine_with_density()

0 commit comments

Comments
 (0)