Skip to content

Commit

Permalink
feat: add timeline plot for wind-up input data
Browse files Browse the repository at this point in the history
Plot includes key milestones, analysis and exclusion periods. This
provides a useful overview of the configuration used for an analysis.
  • Loading branch information
samuelwnaylor committed Feb 19, 2025
1 parent c16c93a commit 2302d07
Show file tree
Hide file tree
Showing 8 changed files with 450 additions and 39 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,5 @@ output/
/tests/**/*.log
/tests/test_data/wedowind/Pitch Angle/
/tests/test_data/wedowind/Vortex Generator/
# matplotlib testing
**/result_images/
48 changes: 34 additions & 14 deletions examples/smarteole_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,18 +296,15 @@ def _download_data_from_zenodo(analysis_timebase_s: int, cache_dir: Path, zip_fi
return SmarteoleData(scada_df=scada_df, metadata_df=metadata_df, toggle_df=toggle_df)


def main_smarteole_analysis(
def _construct_assessment_inputs(
*,
smarteole_data: SmarteoleData,
logger: logging.Logger,
analysis_timebase_s: int = ANALYSIS_TIMEBASE_S,
check_results: bool = CHECK_RESULTS,
analysis_output_dir: Path = ANALYSIS_OUTPUT_DIR,
cache_sub_dir: Path = CACHE_SUBDIR,
reanalysis_file_path: Path | str = REANALYSIS_DATA_FILE_PATH,
) -> None:
setup_logger(ANALYSIS_OUTPUT_DIR / "analysis.log")
logger = logging.getLogger(__name__)

cache_sub_dir: Path = CACHE_SUBDIR,
) -> AssessmentInputs:
logger.info("Merging SMV6 yaw offset command signal into SCADA data")
toggle_df_no_tz = smarteole_data.toggle_df.copy()
toggle_df_no_tz.index = toggle_df_no_tz.index.tz_localize(None)
Expand All @@ -317,19 +314,19 @@ def main_smarteole_analysis(
scada_df["yaw_offset_command"] = scada_df["yaw_offset_command"].where(scada_df["TurbineName"] == "SMV6", 0)
del toggle_df_no_tz

logger.info("Loading reference reanalysis data")
reanalysis_dataset = ReanalysisDataset(
id="ERA5T_50.00N_2.75E_100m_1hr",
data=pd.read_parquet(reanalysis_file_path),
)

logger.info("Defining Assessment Configuration")
cfg = define_smarteole_example_config(
analysis_timebase_s=analysis_timebase_s, analysis_output_dir=analysis_output_dir
)
plot_cfg = PlotConfig(show_plots=False, save_plots=True, plots_dir=cfg.out_dir / "plots")

assessment_inputs = AssessmentInputs.from_cfg(
logger.info("Loading reference reanalysis data")
reanalysis_dataset = ReanalysisDataset(
id="ERA5T_50.00N_2.75E_100m_1hr",
data=pd.read_parquet(reanalysis_file_path),
)

return AssessmentInputs.from_cfg(
cfg=cfg,
plot_cfg=plot_cfg,
toggle_df=smarteole_data.toggle_df,
Expand All @@ -338,6 +335,29 @@ def main_smarteole_analysis(
reanalysis_datasets=[reanalysis_dataset],
cache_dir=cache_sub_dir,
)


def main_smarteole_analysis(
*,
smarteole_data: SmarteoleData,
analysis_timebase_s: int = ANALYSIS_TIMEBASE_S,
check_results: bool = CHECK_RESULTS,
analysis_output_dir: Path = ANALYSIS_OUTPUT_DIR,
cache_sub_dir: Path = CACHE_SUBDIR,
reanalysis_file_path: Path | str = REANALYSIS_DATA_FILE_PATH,
) -> None:
setup_logger(analysis_output_dir / "analysis.log")
logger = logging.getLogger(__name__)

assessment_inputs = _construct_assessment_inputs(
smarteole_data=smarteole_data,
logger=logger,
analysis_timebase_s=analysis_timebase_s,
analysis_output_dir=analysis_output_dir,
reanalysis_file_path=reanalysis_file_path,
cache_sub_dir=cache_sub_dir,
)

results_per_test_ref_df = run_wind_up_analysis(assessment_inputs)

net_p50, net_p95, net_p5 = calc_net_uplift(results_per_test_ref_df, confidence=0.9)
Expand Down
58 changes: 58 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
from __future__ import annotations

import logging
import shutil
from pathlib import Path
from typing import TYPE_CHECKING

import pytest

from examples.smarteole_example import (
SmarteoleData,
_construct_assessment_inputs,
unpack_smarteole_metadata,
unpack_smarteole_scada,
unpack_smarteole_toggle_data,
)
from wind_up.models import WindUpConfig

if TYPE_CHECKING:
from wind_up.interface import AssessmentInputs

TEST_DATA_FLD = Path(__file__).parent / "test_data"
TEST_CONFIG_DIR = TEST_DATA_FLD / "config"

SMARTEOLE_DATA_DIR = TEST_DATA_FLD / "smarteole"
SMARTEOLE_CACHE_DIR = Path(__file__).parent / "timebase_600"
SMARTEOLE_OUTPUT_DIR = Path(__file__).parent / "output"

logger = logging.getLogger(__name__)


@pytest.fixture
def test_lsa_t13_config() -> WindUpConfig:
Expand Down Expand Up @@ -55,3 +74,42 @@ def test_homer_with_t00_config() -> WindUpConfig:
cfg.asset.wtgs[-1].latitude = -58.601587635380
cfg.asset.wtgs[-1].longitude = 103.692588907983
return cfg


@pytest.fixture(scope="session")
def smarteole_assessment_inputs() -> tuple[AssessmentInputs, SmarteoleData]:
timebase_s = 600
output_dir = SMARTEOLE_OUTPUT_DIR
output_dir.mkdir(parents=True, exist_ok=True)
cache_subdir = SMARTEOLE_CACHE_DIR
cache_subdir.mkdir(parents=True, exist_ok=True)

scada_df = unpack_smarteole_scada(
timebase_s=timebase_s, scada_data_file=SMARTEOLE_DATA_DIR / "SMARTEOLE_WakeSteering_SCADA_1minData.csv"
)
metadata_df = unpack_smarteole_metadata(
timebase_s=timebase_s, metadata_file=SMARTEOLE_DATA_DIR / "SMARTEOLE_WakeSteering_Coordinates_staticData.csv"
)
toggle_df = unpack_smarteole_toggle_data(
timebase_s=timebase_s, toggle_file=SMARTEOLE_DATA_DIR / "SMARTEOLE_WakeSteering_ControlLog_1minData.csv"
)
smarteole_data = SmarteoleData(scada_df=scada_df, metadata_df=metadata_df, toggle_df=toggle_df)

return _construct_assessment_inputs(
smarteole_data=smarteole_data,
logger=logger,
reanalysis_file_path=SMARTEOLE_DATA_DIR / "ERA5T_50.00N_2.75E_100m_1hr_20200201_20200531.parquet",
analysis_timebase_s=timebase_s,
analysis_output_dir=output_dir,
cache_sub_dir=cache_subdir,
), smarteole_data


def pytest_sessionfinish(session, exitstatus) -> None: # noqa: ARG001,ANN001
# remove directories created during tests
plot_testing = [
Path(__file__).parents[1] / "result_images", # if pytest run from root dir
]
for d in [SMARTEOLE_OUTPUT_DIR, SMARTEOLE_CACHE_DIR, *plot_testing]:
if d.is_dir():
shutil.rmtree(d)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 changes: 101 additions & 0 deletions tests/plots/test_input_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import copy
import logging
import re

import numpy as np
import pandas as pd
import pytest
from matplotlib.testing.decorators import image_comparison

from examples.smarteole_example import SmarteoleData
from wind_up.interface import AssessmentInputs
from wind_up.plots.input_data import plot_input_data_timeline

logger = logging.getLogger(__name__)


class TestInputDataTimeline:
@pytest.mark.slow
@pytest.mark.filterwarnings("ignore")
@pytest.mark.parametrize("exclusion_period_attribute_name", ["yaw_data_exclusions_utc", "exclusion_periods_utc"])
def test_data_is_present_within_an_exclusion_period(
self, exclusion_period_attribute_name: str, smarteole_assessment_inputs: tuple[AssessmentInputs, SmarteoleData]
) -> None:
"""Test that a ValueError is raised if any non-NaN data is present within any exclusion period."""

assessment_inputs = copy.deepcopy(smarteole_assessment_inputs[0])

setattr(
assessment_inputs.cfg,
exclusion_period_attribute_name,
[
("SMV1", pd.Timestamp("2020-03-01T00:00:00+0000"), pd.Timestamp("2020-03-03T00:00:00+0000")),
],
)

with pytest.raises(
ValueError,
match=re.escape(
"Data is not all NaN within exclusion period "
f"{getattr(assessment_inputs.cfg, exclusion_period_attribute_name)[0]}"
),
):
plot_input_data_timeline(assessment_inputs)

@pytest.mark.slow
@pytest.mark.filterwarnings("ignore")
@image_comparison(
baseline_images=["input_data_timeline_fig_toggle"], remove_text=False, extensions=["png"], style="mpl20"
)
def test_toggle(self, smarteole_assessment_inputs: tuple[AssessmentInputs, SmarteoleData]) -> None:
"""Test plotting timeline of input data on the Smarteole wind farm."""

assessment_inputs = copy.deepcopy(smarteole_assessment_inputs[0])

assessment_inputs.cfg.yaw_data_exclusions_utc = [
("SMV1", pd.Timestamp("2020-03-01T00:00:00+0000"), pd.Timestamp("2020-03-03T00:00:00+0000")),
("SMV4", pd.Timestamp("2020-04-02T00:00:00+0000"), pd.Timestamp("2020-05-20T00:00:00+0000")),
]

assessment_inputs.cfg.exclusion_periods_utc = [
("SMV3", pd.Timestamp("2020-04-01T00:00:00+0000"), pd.Timestamp("2020-04-10T00:00:00+0000")),
("SMV6", pd.Timestamp("2020-03-10T00:00:00+0000"), pd.Timestamp("2020-03-12T00:00:00+0000")),
]

for exclusion in [*assessment_inputs.cfg.yaw_data_exclusions_utc, *assessment_inputs.cfg.exclusion_periods_utc]:
turbine_name = exclusion[0]
start, end = exclusion[1], exclusion[2]
assessment_inputs.wf_df.loc[pd.IndexSlice[turbine_name, start:end], "ActivePowerMean"] = np.nan

plot_input_data_timeline(assessment_inputs)

@pytest.mark.slow
@pytest.mark.filterwarnings("ignore")
@image_comparison(
baseline_images=["input_data_timeline_fig_prepost"], remove_text=False, extensions=["png"], style="mpl20"
)
def test_prepost(self, smarteole_assessment_inputs: tuple[AssessmentInputs, SmarteoleData]) -> None:
"""Test plotting timeline of input data on the Smarteole wind farm."""

assessment_inputs = copy.deepcopy(smarteole_assessment_inputs[0])

# manual adjustments to the configuration for the test
assessment_inputs.cfg.toggle = None
assessment_inputs.cfg.upgrade_first_dt_utc_start = pd.Timestamp("2020-03-01T00:00:00+0000")

assessment_inputs.cfg.yaw_data_exclusions_utc = [
("SMV1", pd.Timestamp("2020-03-01T00:00:00+0000"), pd.Timestamp("2020-03-03T00:00:00+0000")),
("SMV4", pd.Timestamp("2020-04-02T00:00:00+0000"), pd.Timestamp("2020-05-20T00:00:00+0000")),
]

assessment_inputs.cfg.exclusion_periods_utc = [
("SMV3", pd.Timestamp("2020-04-01T00:00:00+0000"), pd.Timestamp("2020-04-10T00:00:00+0000")),
("SMV6", pd.Timestamp("2020-03-10T00:00:00+0000"), pd.Timestamp("2020-03-12T00:00:00+0000")),
]

for exclusion in [*assessment_inputs.cfg.yaw_data_exclusions_utc, *assessment_inputs.cfg.exclusion_periods_utc]:
turbine_name = exclusion[0]
start, end = exclusion[1], exclusion[2]
assessment_inputs.wf_df.loc[pd.IndexSlice[turbine_name, start:end], "ActivePowerMean"] = np.nan

plot_input_data_timeline(assessment_inputs)
35 changes: 10 additions & 25 deletions tests/test_smarteole.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,30 @@
"""Tests running the Smarteole dataset through wind-up analysis."""

import copy
from pathlib import Path

import pytest

from examples.smarteole_example import (
SmarteoleData,
main_smarteole_analysis,
unpack_smarteole_metadata,
unpack_smarteole_scada,
unpack_smarteole_toggle_data,
)
from examples.smarteole_example import SmarteoleData, main_smarteole_analysis
from tests.conftest import SMARTEOLE_CACHE_DIR
from wind_up.interface import AssessmentInputs

SMARTEOLE_DATA_DIR = Path(__file__).parents[1] / "tests/test_data/smarteole"


@pytest.mark.slow
@pytest.mark.filterwarnings("ignore")
def test_smarteole_analysis(tmp_path: Path) -> None:
def test_smarteole_analysis(
smarteole_assessment_inputs: tuple[AssessmentInputs, SmarteoleData], tmp_path: Path
) -> None:
"""Test running the Smarteole analysis."""

timebase_s = 600
cache_subdir = tmp_path / f"timebase_{timebase_s}"
cache_subdir.mkdir(parents=True, exist_ok=True)

scada_df = unpack_smarteole_scada(
timebase_s=timebase_s, scada_data_file=SMARTEOLE_DATA_DIR / "SMARTEOLE_WakeSteering_SCADA_1minData.csv"
)
metadata_df = unpack_smarteole_metadata(
timebase_s=timebase_s, metadata_file=SMARTEOLE_DATA_DIR / "SMARTEOLE_WakeSteering_Coordinates_staticData.csv"
)
toggle_df = unpack_smarteole_toggle_data(
timebase_s=timebase_s, toggle_file=SMARTEOLE_DATA_DIR / "SMARTEOLE_WakeSteering_ControlLog_1minData.csv"
)
smarteole_data = SmarteoleData(scada_df=scada_df, metadata_df=metadata_df, toggle_df=toggle_df)
smarteole_data = copy.deepcopy(smarteole_assessment_inputs[1])

main_smarteole_analysis(
smarteole_data=smarteole_data,
reanalysis_file_path=SMARTEOLE_DATA_DIR / "ERA5T_50.00N_2.75E_100m_1hr_20200201_20200531.parquet",
analysis_timebase_s=timebase_s,
analysis_timebase_s=600,
check_results=True, # asserts expected results
analysis_output_dir=tmp_path,
cache_sub_dir=cache_subdir,
cache_sub_dir=SMARTEOLE_CACHE_DIR,
)
Loading

0 comments on commit 2302d07

Please sign in to comment.