Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/lint-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.13"]
python-version: ["3.10", "3.13"]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v5
Expand All @@ -41,7 +41,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.13"]
python-version: ["3.10", "3.13"]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v5
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,16 @@ The public examples cover several realistic analysis shapes, including
SMARTEOLE toggle data, Kelmarsh turbine data, and WeDoWind challenge-style
pre/post assessments.

## Methodology

The validation methodology that `wind-up` implements is described in detail in
[**wind-up uplift validation methodology v3**](docs/wind-up%20uplift%20validation%20methodology%20v3.pdf)
in the [`docs`](docs) folder.

## Installation

Install the released package with your Python environment manager of choice.
Python `>=3.9,<4.0` is supported.
Python 3.10 to 3.13 is supported.

Using `uv`:

Expand Down Expand Up @@ -146,6 +152,12 @@ When plot saving is enabled, diagnostic figures are written under
`PlotConfig.plots_dir`; CSV results are written under the configured assessment
output directory.

> [!NOTE]
> On import, `wind-up` selects the non-interactive matplotlib `Agg` backend
> unless the `MPLBACKEND` environment variable is already set. This lets analyses
> run headless (CI, SSH, batch servers) without an X server. Set `MPLBACKEND`
> yourself before importing `wind_up` if you want an interactive backend.

## Analysis features

`wind-up` includes utilities for the parts of an uplift study that usually need
Expand Down Expand Up @@ -210,7 +222,7 @@ uv run poe jupy # start JupyterLab for example exploration
> stop it. Use it when you want an interactive notebook session, not as a
> one-shot verification command.

The GitHub Actions workflow runs linting and tests on Python 3.9 and 3.13.
The GitHub Actions workflow runs linting and tests on Python 3.10 and 3.13.

## Project status

Expand Down
3 changes: 2 additions & 1 deletion examples/kelmarsh_kaggle.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import logging
import re
from pathlib import Path
from typing import Any

import ephem
import matplotlib.pyplot as plt
Expand Down Expand Up @@ -732,7 +733,7 @@ def flatten_and_clean_columns(df: pd.DataFrame) -> pd.DataFrame:
X_train = flatten_and_clean_columns(X_train)
X_test = flatten_and_clean_columns(X_test)

automl_settings = {
automl_settings: dict[str, Any] = {
"time_budget": 3600 * 1, # 12 hours was used for best solution
"ensemble": True,
"task": "regression",
Expand Down
4 changes: 2 additions & 2 deletions examples/smarteole_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import zipfile
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING

import pandas as pd
from scipy.stats import circmean
Expand All @@ -17,7 +17,7 @@
from wind_up.constants import PROJECTROOT_DIR, TIMESTAMP_COL, DataColumns

if TYPE_CHECKING:
FilePathOrBuffer = Union[str, bytearray]
FilePathOrBuffer = str | bytearray

SCADA_FILE_PATH = "SMARTEOLE-WFC-open-dataset/SMARTEOLE_WakeSteering_SCADA_1minData.csv"
METADATA_FILE_PATH = "SMARTEOLE-WFC-open-dataset/SMARTEOLE_WakeSteering_Coordinates_staticData.csv"
Expand Down
20 changes: 13 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
[project]
name = "res-wind-up"
version = "0.4.10"
version = "0.5.0"
authors = [
{ name = "Alex Clerc", email = "alex.clerc@res-group.com" }
]
description = "A tool to assess yield uplift of wind turbines"
readme = "README.md"
requires-python = ">=3.9,<4.0"
requires-python = ">=3.10,<3.14"
license = { file = "LICENSE.txt" }
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -25,7 +24,7 @@ dependencies = [
'eval-type-backport',
'geographiclib',
'matplotlib',
'pandas >= 2.0.0',
'pandas >= 2.0.0,<3.0.0',
'pyarrow',
'pydantic >= 2.0.0',
'python-dotenv',
Expand Down Expand Up @@ -57,7 +56,7 @@ dev = [
'types-tqdm',
'types-requests',
'ruff',
'mypy',
'mypy<1.19',
'requests',
'pytest-env',
]
Expand All @@ -74,10 +73,17 @@ examples = [
where = ["."]
include = ["wind_up*"]

[tool.uv]
# Dev/CI-only constraint (not part of published metadata, so consumers are unaffected).
# matplotlib 3.11 dropped Python 3.10, so an unpinned universal lock resolves matplotlib 3.10
# on the 3.10 job but 3.11 on the 3.13 job; the image-comparison baselines are matplotlib-version
# specific, so keep the test environment on a single matplotlib (3.10.x, which supports 3.10-3.13).
constraint-dependencies = ["matplotlib<3.11"]

[tool.ruff]
line-length = 120
show-fixes = true
target-version = "py39"
target-version = "py310"

[tool.ruff.lint]
select = ["ALL"] # https://beta.ruff.rs/docs/rules/
Expand Down Expand Up @@ -117,7 +123,7 @@ max-args = 17 # try to bring this down to 5

[tool.mypy]
plugins = ["pydantic.mypy"]
python_version = "3.9"
python_version = "3.10"
exclude = "build|tests|venv|.venv|__ignore__"
disallow_untyped_defs = true

Expand Down
4 changes: 3 additions & 1 deletion tests/pp_analysis/test_cook_pp.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ def _make_pp_raw_df(
pw_std_col: [50.0 if p is not None else np.nan for p in pw_means],
ws_std_col: [0.2] * n,
count_col: counts,
pw_sem_col: [50.0 / max(1, c) ** 0.5 if p is not None else np.nan for p, c in zip(pw_means, counts)],
pw_sem_col: [
50.0 / max(1, c) ** 0.5 if p is not None else np.nan for p, c in zip(pw_means, counts, strict=True)
],
ws_sem_col: [0.2 / max(1, c) ** 0.5 for c in counts],
}
)
Expand Down
45 changes: 45 additions & 0 deletions tests/test_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

import os
import subprocess
import sys

_PRINT_BACKEND = "import wind_up, matplotlib; print(matplotlib.get_backend())"
# import pyplot (selecting a non-interactive backend) *before* wind_up, as an interactive
# notebook session would
_PYPLOT_THEN_WIND_UP = (
"import matplotlib; matplotlib.use('pdf'); import matplotlib.pyplot;"
" import wind_up; print(matplotlib.get_backend())"
)


def _backend_after(code: str, *, mplbackend: str | None = None) -> str:
env = {k: v for k, v in os.environ.items() if k != "MPLBACKEND"}
if mplbackend is not None:
env["MPLBACKEND"] = mplbackend
result = subprocess.run( # noqa: S603
[sys.executable, "-c", code],
capture_output=True,
text=True,
env=env,
check=True,
)
return result.stdout.strip().lower()


def test_default_backend_is_agg_when_mplbackend_unset() -> None:
# importing wind_up must pin a non-interactive backend so headless / SSH runs don't
# depend on an X server (see wind_up/__init__.py)
assert _backend_after(_PRINT_BACKEND) == "agg"


def test_explicit_mplbackend_is_respected() -> None:
# an explicit MPLBACKEND must win over the wind_up default; "pdf" is non-interactive
# so it is always importable in CI
assert _backend_after(_PRINT_BACKEND, mplbackend="pdf") == "pdf"


def test_backend_left_alone_when_pyplot_already_imported() -> None:
# if pyplot is already imported (e.g. an interactive notebook), wind_up must not switch
# the user's backend out from under them, even with MPLBACKEND unset
assert _backend_after(_PYPLOT_THEN_WIND_UP) == "pdf"
74 changes: 0 additions & 74 deletions tests/test_backporting.py

This file was deleted.

2 changes: 1 addition & 1 deletion tests/test_rolling_circ_mean.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def test_rolling_circ_mean(*, range_360: bool) -> None:
else (
input_df[col]
.rolling(window=4, min_periods=1, center=True)
.apply(lambda x: (circmean(x, low=-180, high=180, nan_policy="omit")))
.apply(lambda x: circmean(x, low=-180, high=180, nan_policy="omit"))
)
)
assert_series_equal(result, expected)
Expand Down
8 changes: 2 additions & 6 deletions tests/test_rolling_circ_median.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def test_rolling_circ_median_all_nans() -> None:

for col in input_df.columns:
result = rolling_circ_median_approx(input_df[col], window=4, min_periods=3, center=True, range_360=True)
expected = input_df[col].rolling(window=4, min_periods=3, center=True).apply(lambda x: circ_median_exact(x))
expected = input_df[col].rolling(window=4, min_periods=3, center=True).apply(circ_median_exact)
assert_series_equal(result, expected)


Expand Down Expand Up @@ -73,11 +73,7 @@ def new_method() -> float:
return rolling_circ_median_approx(input_df[col], window=window_size, min_periods=min_periods, center=True)

def exact_method() -> float:
return (
input_df[col]
.rolling(window=window_size, min_periods=min_periods, center=True)
.apply(lambda x: circ_median_exact(x))
)
return input_df[col].rolling(window=window_size, min_periods=min_periods, center=True).apply(circ_median_exact)

new_method_results = new_method()
exact_method_results = exact_method()
Expand Down
3 changes: 1 addition & 2 deletions tests/test_smart_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import pytest

from tests.conftest import TEST_DATA_FLD
from wind_up.backporting import strict_zip
from wind_up.constants import TIMESTAMP_COL
from wind_up.smart_data import (
add_smart_lat_long_to_cfg,
Expand Down Expand Up @@ -40,7 +39,7 @@ def test_calc_last_xmin_datetime_in_month() -> None:
dt.datetime(2020, 2, 29, 23, 50),
dt.datetime(2020, 2, 29, 23, 50),
]
for i, e in strict_zip(inputs, expected):
for i, e in zip(inputs, expected, strict=True):
assert calc_last_xmin_datetime_in_month(i, TIMEBASE_PD_TIMEDELTA) == pd.Timestamp(e)


Expand Down
Loading
Loading