Skip to content

Commit

Permalink
feat: add function to quantify wind speed shift
Browse files Browse the repository at this point in the history
In similar way that power curve, rpm and pitch shifts are already
calculated, this change adds wind speed shift to the ops curve shift
module
  • Loading branch information
samuelwnaylor committed Oct 9, 2024
1 parent 5121f08 commit 847827a
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 29 deletions.
41 changes: 37 additions & 4 deletions tests/test_ops_curve_shift.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
import pytest

from wind_up.ops_curve_shift import (
CURVE_CONSTANTS,
CurveConfig,
CurveShiftInput,
CurveThresholds,
CurveTypes,
calculate_pitch_curve_shift,
calculate_power_curve_shift,
calculate_rpm_curve_shift,
calculate_wind_speed_curve_shift,
check_for_ops_curve_shift,
)

Expand Down Expand Up @@ -115,7 +116,7 @@ def test_calculate_power_curve_shift(
y_col="power",
)

if abs(expected) > CurveThresholds.POWER_CURVE.value:
if abs(expected) > CURVE_CONSTANTS[CurveTypes.POWER_CURVE.value]["warning_threshold"]:
assert "Ops Curve Shift warning" in caplog.text

np.testing.assert_almost_equal(actual=actual, desired=expected)
Expand All @@ -140,7 +141,7 @@ def test_calculate_rpm_curve_shift(
y_col="gen_rpm",
)

if abs(expected) > CurveThresholds.RPM.value:
if abs(expected) > CURVE_CONSTANTS[CurveTypes.RPM.value]["warning_threshold"]:
assert "Ops Curve Shift warning" in caplog.text

np.testing.assert_almost_equal(actual=actual, desired=expected)
Expand All @@ -165,7 +166,32 @@ def test_calculate_pitch_curve_shift(
y_col="pitch",
)

if abs(expected) > CurveThresholds.PITCH.value:
if abs(expected) > CURVE_CONSTANTS[CurveTypes.PITCH.value]["warning_threshold"]:
assert "Ops Curve Shift warning" in caplog.text

np.testing.assert_almost_equal(actual=actual, desired=expected)


@pytest.mark.parametrize(
("shift_amount", "expected"),
[
pytest.param(2.0, 0.21621621621621623, id="shift DOES exceed threshold"),
pytest.param(0.05, -0.04729729729729748, id="shift DOES NOT exceed threshold"),
],
)
def test_calculate_wind_speed_curve_shift(
shift_amount: float, expected: float, fake_power_curve_df: pd.DataFrame, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING):
actual = calculate_wind_speed_curve_shift(
turbine_name="anything",
pre_df=fake_power_curve_df.reset_index(),
post_df=(fake_power_curve_df + shift_amount).reset_index(),
x_col="power",
y_col="wind_speed",
)

if abs(expected) > CURVE_CONSTANTS[CurveTypes.WIND_SPEED.value]["warning_threshold"]:
assert "Ops Curve Shift warning" in caplog.text

np.testing.assert_almost_equal(actual=actual, desired=expected)
Expand Down Expand Up @@ -222,6 +248,7 @@ def test_missing_required_column(
f"{CurveTypes.POWER_CURVE.value}_shift": np.nan,
f"{CurveTypes.RPM.value}_shift": np.nan,
f"{CurveTypes.PITCH.value}_shift": np.nan,
f"{CurveTypes.WIND_SPEED.value}_shift": np.nan,
}

assert actual == expected
Expand All @@ -244,6 +271,7 @@ def test_calls_funcs_as_intended(
patch("wind_up.ops_curve_shift.calculate_power_curve_shift", return_value=np.nan) as mock_power,
patch("wind_up.ops_curve_shift.calculate_rpm_curve_shift", return_value=np.nan) as mock_rpm,
patch("wind_up.ops_curve_shift.calculate_pitch_curve_shift", return_value=np.nan) as mock_pitch,
patch("wind_up.ops_curve_shift.calculate_wind_speed_curve_shift", return_value=np.nan) as mock_ws,
patch("wind_up.ops_curve_shift.compare_ops_curves_pre_post", return_value=None) as mock_plot_func,
):
mock_wind_up_conf = Mock()
Expand Down Expand Up @@ -272,6 +300,10 @@ def test_calls_funcs_as_intended(
turbine_name=wtg_name, pre_df=_df, post_df=_df, x_col="wind_speed", y_col="pitch"
)

mock_ws.assert_called_once_with(
turbine_name=wtg_name, pre_df=_df, post_df=_df, x_col="power", y_col="wind_speed"
)

mock_plot_func.assert_called_once_with(
pre_df=_df,
post_df=_df,
Expand All @@ -289,6 +321,7 @@ def test_calls_funcs_as_intended(
f"{CurveTypes.POWER_CURVE.value}_shift": np.nan,
f"{CurveTypes.RPM.value}_shift": np.nan,
f"{CurveTypes.PITCH.value}_shift": np.nan,
f"{CurveTypes.WIND_SPEED.value}_shift": np.nan,
}

assert actual == expected
64 changes: 39 additions & 25 deletions wind_up/ops_curve_shift.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,35 @@
from wind_up.models import PlotConfig, WindUpConfig


class CurveThresholds(Enum):
POWER_CURVE = 0.01
RPM = 0.005
PITCH = 0.1


class CurveTypes(str, Enum):
POWER_CURVE = "powercurve"
RPM = "rpm"
PITCH = "pitch"
WIND_SPEED = "windspeed"


CURVE_CONSTANTS = {
CurveTypes.POWER_CURVE.value: {"warning_threshold": 0.01, "x_bin_width": 1},
CurveTypes.RPM.value: {"warning_threshold": 0.005, "x_bin_width": 0},
CurveTypes.PITCH.value: {"warning_threshold": 0.1, "x_bin_width": 1},
CurveTypes.WIND_SPEED.value: {"warning_threshold": 0.01, "x_bin_width": 1},
}


class CurveConfig(BaseModel):
name: CurveTypes
x_col: str
y_col: str
x_bin_width: int
warning_threshold: float
x_bin_width: int | float | None = None
warning_threshold: float | None = None

@model_validator(mode="after")
def validate_constants(self) -> CurveConfig:
if self.x_bin_width is None:
self.x_bin_width = CURVE_CONSTANTS[self.name]["x_bin_width"]
if self.warning_threshold is None:
self.warning_threshold = CURVE_CONSTANTS[self.name]["warning_threshold"]
return self


class CurveShiftInput(BaseModel):
Expand Down Expand Up @@ -83,6 +94,7 @@ def check_for_ops_curve_shift(
f"{CurveTypes.POWER_CURVE.value}_shift": np.nan,
f"{CurveTypes.RPM.value}_shift": np.nan,
f"{CurveTypes.PITCH.value}_shift": np.nan,
f"{CurveTypes.WIND_SPEED.value}_shift": np.nan,
}

required_cols = OpsCurveRequiredColumns(wind_speed=scada_ws_col, power=pw_col, pitch=pt_col, rpm=rpm_col)
Expand All @@ -104,6 +116,10 @@ def check_for_ops_curve_shift(
turbine_name=wtg_name, pre_df=pre_df, post_df=post_df, x_col=scada_ws_col, y_col=pt_col
)

results_dict[f"{CurveTypes.WIND_SPEED.value}_shift"] = calculate_wind_speed_curve_shift(
turbine_name=wtg_name, pre_df=pre_df, post_df=post_df, x_col=pw_col, y_col=scada_ws_col
)

if plot:
compare_ops_curves_pre_post(
pre_df=pre_df,
Expand All @@ -124,13 +140,7 @@ def check_for_ops_curve_shift(
def calculate_power_curve_shift(
turbine_name: str, pre_df: pd.DataFrame, post_df: pd.DataFrame, x_col: str, y_col: str
) -> float:
curve_config = CurveConfig(
name=CurveTypes.POWER_CURVE.value,
x_col=x_col,
y_col=y_col,
x_bin_width=1,
warning_threshold=CurveThresholds.POWER_CURVE.value,
)
curve_config = CurveConfig(name=CurveTypes.POWER_CURVE.value, x_col=x_col, y_col=y_col)

curve_shift_input = CurveShiftInput(
turbine_name=turbine_name, pre_df=pre_df, post_df=post_df, curve_config=curve_config
Expand All @@ -142,9 +152,7 @@ def calculate_power_curve_shift(
def calculate_rpm_curve_shift(
turbine_name: str, pre_df: pd.DataFrame, post_df: pd.DataFrame, x_col: str, y_col: str
) -> float:
curve_config = CurveConfig(
name=CurveTypes.RPM.value, x_col=x_col, y_col=y_col, x_bin_width=0, warning_threshold=CurveThresholds.RPM.value
)
curve_config = CurveConfig(name=CurveTypes.RPM.value, x_col=x_col, y_col=y_col)

curve_shift_input = CurveShiftInput(
turbine_name=turbine_name, pre_df=pre_df, post_df=post_df, curve_config=curve_config
Expand All @@ -156,14 +164,20 @@ def calculate_rpm_curve_shift(
def calculate_pitch_curve_shift(
turbine_name: str, pre_df: pd.DataFrame, post_df: pd.DataFrame, x_col: str, y_col: str
) -> float:
curve_config = CurveConfig(
name=CurveTypes.PITCH.value,
x_col=x_col,
y_col=y_col,
x_bin_width=1,
warning_threshold=CurveThresholds.PITCH.value,
curve_config = CurveConfig(name=CurveTypes.PITCH.value, x_col=x_col, y_col=y_col)

curve_shift_input = CurveShiftInput(
turbine_name=turbine_name, pre_df=pre_df, post_df=post_df, curve_config=curve_config
)

return _calculate_curve_shift(curve_shift_input=curve_shift_input)


def calculate_wind_speed_curve_shift(
turbine_name: str, pre_df: pd.DataFrame, post_df: pd.DataFrame, x_col: str, y_col: str
) -> float:
curve_config = CurveConfig(name=CurveTypes.WIND_SPEED.value, x_col=x_col, y_col=y_col)

curve_shift_input = CurveShiftInput(
turbine_name=turbine_name, pre_df=pre_df, post_df=post_df, curve_config=curve_config
)
Expand Down Expand Up @@ -194,7 +208,7 @@ def _calculate_curve_shift(curve_shift_input: CurveShiftInput) -> float:
post_df = curve_shift_input.post_df
wtg_name = curve_shift_input.turbine_name

bins = np.arange(0, pre_df[conf.x_col].max() + conf.x_bin_width, conf.x_bin_width) if conf.x_bin_width > 0 else 10
bins = np.arange(0, pre_df[conf.x_col].max() + conf.x_bin_width, conf.x_bin_width) if conf.x_bin_width > 0 else 10 # type: ignore[operator]

mean_curve = pre_df.groupby(pd.cut(pre_df[conf.x_col], bins=bins, retbins=False), observed=True).agg(
x_mean=pd.NamedAgg(column=conf.x_col, aggfunc="mean"),
Expand Down

0 comments on commit 847827a

Please sign in to comment.