# This class defines key analytical routines for estimating wake losses for an operating
# wind plant using SCADA data. At a high level, for each SCADA time step, freestream wind
# turbines are identified using the turbine coordinates and a reference wind direction
# signal. The mean power production for all turbines in the wind plant is summed over all
# time steps and compared to the mean power of the freestream turbines summed over all time
# steps to estimate wake losses during the period of record. Methods for calclating the
# long-term wake losses using reanalaysis data and quantifying uncertainty are provided as well.
# The general approach for estimating wake losses and quantifying uncertainty using bootstrapping
# is based in part on the following publications:
# 1. Barthelmie, R. J. and Jensen, L. E. Evaluation of wind farm efficiency and wind turbine wakes
# at the Nysted offshore wind farm, *Wind Energy* 13(6):573–586 (2010).
# https://doi.org/10.1002/we.408.
# 2. Nygaard, N. G. Systematic quantification of wake model uncertainty. Proc. EWEA Offshore,
# Copenhagen, Denmark, March 10-12 (2015).
# 3. Walker, K., Adams, N., Gribben, B., Gellatly, B., Nygaard, N. G., Henderson, A., Marchante
# Jimémez, M., Schmidt, S. R., Rodriguez Ruiz, J., Paredes, D., Harrington, G., Connell, N.,
# Peronne, O., Cordoba, M., Housley, P., Cussons, R., Håkansson, M., Knauer, A., and Maguire,
# E.: An evaluation of the predictive accuracy of wake effects models for offshore wind farms.
# *Wind Energy* 19(5):979–996 (2016). https://doi.org/10.1002/we.1871.
from __future__ import annotations
import random
from copy import deepcopy
import attrs
import numpy as np
import pandas as pd
import numpy.typing as npt
from tqdm import tqdm
from attrs import field, define
from sklearn.linear_model import LinearRegression
from openoa.plant import PlantData, convert_to_list
from openoa.utils import plot, filters
from openoa.utils import met_data_processing as met
from openoa.schema import FromDictMixin, ResetValuesMixin
from openoa.logging import logging, logged_method_call
from openoa.analysis._analysis_validators import (
validate_UQ_input,
validate_half_closed_0_1_right,
validate_reanalysis_selections,
)
logger = logging.getLogger(__name__)
NDArrayFloat = npt.NDArray[np.float64]
plot.set_styling()
[docs]
@define(auto_attribs=True)
class WakeLosses(FromDictMixin, ResetValuesMixin):
"""
A serial implementation of a method for estimating wake losses from SCADA data. Wake losses are
estimated for the entire wind plant as well as for each individual turbine for a) the period of
record for which data are available, and b) the estimated long-term wind conditions the wind
plant will experience based on historical reanalysis wind resource data.
The method is comprised of the following core steps:
1. Calculate a representative wind plant-level wind direction at each time step using the
mean wind direction of the specified wind turbines or meteorological (met) towers.
Note that time steps for which any necessary plant-level or turbine-level data are
missing are discarded.
a. If :py:attr:`UQ` is selected, wake losses are calculated multiple times using a Monte
Carlo approach with randomly chosen analysis parameters and randomly sampled, with
replacement, time steps for each iteration. The remaining steps described below are
performed for each Monte Carlo iteration. If UQ is not used, wake losses are calculated
once using the specified analysis parameters for the full set of available time steps.
2. Identify the set of derated, curtailed, or unavailable turbines (i.e., turbines whose power
production is limited not by wake losses but by operating mode) for each time step using a
power curve outlier detection method.
3. Calculate the average wind speed and power production for the set of normally operating
(i.e., not derated) freestream turbines for each time step.
a. Freestream turbines are those without any upstream turbines located within a
user-specified sector of wind directions centered on the representative plant-level wind
direction.
4. Calculate the POR wake losses for the wind plant by comparing the potential energy production
(sum of the mean freestream power production at each time step multiplied by the number of
turbines in the wind power plant) to the actual energy production (sum of the actual wind
plant power production at each time step). This procedure is then used to estimate the wake
losses for each individual wind turbine.
a. If :py:attr:`correct_for_derating` is True, then the potential power production of the
wind plant is assumed to be the actual power produced by the derated turbines plus the
mean power production of the freestream turbines for all other turbines in the wind
plant. Again, a similar procedure is used to estimate individual turbine wake losses.
5. Finally, estimate the long-term corrected wake losses using the long-term historical
reanalysis data. Note that the long-term correction is determined for each reanalysis
product specified by the user. If UQ is used, a random reanalysis product is selected
each iteration. If UQ is not selected, the long-term corrected wake losses are calculated
as the average wake losses determined for all reanalysis products.
a. Calculate the long-term occurence frequencies for a set of wind direction and wind
speed bins based on the hourly reanalysis data (typically, 10-20 years).
b. Next, using a linear regression, compare the mean freestream wind speeds calculated
from the SCADA data to the wind speeds from the reanalysis data and correct to remove
biases.
c. Compute the average potential and actual wind power plant production using the
representative wind plant wind directions from the SCADA or met tower data in
conjunction with the corrected freestream wind speeds for each wind direction and wind
speed bin.
d. Estimate the long-term corrected wake losses by comparing the long-term
corrected potential and actual energy production. These are computed by weighting
the average potential and actual power production for each wind condition bin
with the long-term frequencies.
e. Repeat to estimate the long-term corrected wake losses for each individual turbine.
Args:
plant (:obj:`PlantData`): A :py:attr:`openoa.plant.PlantData` object that has been validated
with at least :py:attr:`openoa.plant.PlantData.analysis_type` = "WakeLosses".
wind_direction_col (:obj:`string`, optional): Column name to use for wind direction.
Defaults to "WMET_HorWdDir"
wind_direction_data_type (:obj:`string`, optional): Data type to use for wind directions
("scada" for turbine measurements or "tower" for meteorological tower measurements).
Defaults to "scada".
wind_direction_asset_ids (:obj:`list`, optional): List of asset IDs (turbines or met towers)
used to calculate the average wind direction at each time step. If None, all assets of
the corresponding data type will be used. Defaults to None.
UQ (:obj:`bool`, optional): Dertermines whether to perform uncertainty quantification using
Monte Carlo simulation (True) or provide a single wake loss estimate (False). Defaults
to True.
start_date (:obj:`pandas.Timestamp` or :obj:`string`, optional): Start datetime for wake
loss analysis. If None, the earliest SCADA datetime will be used. Default is None.
end_date (:obj:`pandas.Timestamp` or :obj:`string`, optional): End datetime for wake loss
analysis. If None, the latest SCADA datetime will be used. Default is None.
reanalysis_products (:obj:`list`, optional): List of reanalysis products to use for long-term
correction. If UQ = True, a single product will be selected form this list each Monte
Carlo iteration. Defaults to ["merra2", "era5"].
end_date_lt (:obj:`string` or :obj:`pandas.Timestamp`): The last date to use for the
long-term correction. If None, the most recent date common to all reanalysis products
will be used.
wd_bin_width (float, optional): Wind direction bin size when identifying freestream wind
turbines (degrees). Defaults to 5 degrees.
freestream_sector_width (tuple | float, optional): Wind direction sector size to use when
identifying freestream wind turbines (degrees). If no turbines are located upstream
of a particular turbine within the sector, the turbine will be classified as a
freestream turbine. When :py:attr:`UQ` = True, then this should be a tuple of the
lower and upper bounds for the Monte Carlo sampling, and when :py:attr:`UQ` = False
this should be a single value. If None, then a default value of 90 degrees will be
used if :py:attr:`UQ` = False and a default value of (50, 110) will be used if
:py:attr:`UQ` = True. Defaults to None.
freestream_power_method (str, optional): Method used to determine the representative power
prouction of the freestream turbines ("mean", "median", "max"). Defaults to "mean".
freestream_wind_speed_method (str, optional): Method used to determine the representative
wind speed of the freestream turbines ("mean", "median"). Defaults to "mean".
correct_for_derating (bool, optional): Indicates whether derated, curtailed, or otherwise
unavailable turbines should be flagged and excluded from the calculation of ideal
freestream wind plant power production for a given time stamp. If True, ideal
freestream power production will be calculated as the sum of the derated turbine
powers added to the mean power of the freestream turbines in normal operation
multiplied by the number of turbines operating normally in the wind plant. Defaults
to True.
derating_filter_wind_speed_start (tuple | float, optional): The wind speed above which
turbines will be flagged as derated/curtailed/shutdown if power is less than 1% of
rated power (m/s). Only used when :py:attr:`correct_for_derating` is True. This should
be a tuple when :py:attr:`UQ` = True (values are Monte-Carlo sampled within the
specified range) or a single value when :py:attr:`UQ` = False. If undefined (None), a
value of 4.5 m/s will be used if :py:attr:`UQ` = False and values of (4.0, 5.0) will
be used if :py:attr:`UQ` = True. Defaults to None.
max_power_filter (tuple | float, optional): Maximum power threshold, defined as a fraction
of rated power, to which the power curve bin filter should be applied. Only used when
:py:attr:`correct_for_derating` = True. This should be a tuple when :py:attr:`UQ` =
True (values are Monte-Carlo sampled within the specified range) or a single value
when :py:attr:`UQ` = False. If undefined (None), a value of 0.95 will be used if
:py:attr:`UQ` = False and values of (0.92, 0.98) will be used if :py:attr:`UQ` =
True. Defaults to None.
wind_bin_mad_thresh (tuple | float, optional): The filter threshold for each power bin
used to identify derated/curtailed/shutdown turbines, expressed as the number of
median absolute deviations above the median wind speed. Only used when
:py:attr:`correct_for_derating` is True. This should be a tuple when
:py:attr:`UQ` = True (values are Monte-Carlo sampled within the specified range) or
a single value when :py:attr:`UQ` = False. If undefined (None), a value of 7.0 will
be used if :py:attr:`UQ` = False and values of (4.0, 13.0) will be used if
:py:attr:`UQ` = True. Defaults to None.
wd_bin_width_LT_corr (float, optional): Size of wind direction bins used to calculate
long-term frequencies from historical reanalysis data and correct wake losses during
the period of record (degrees). Defaults to 5 degrees.
ws_bin_width_LT_corr (float, optional): Size of wind speed bins used to calculate
long-term frequencies from historical reanalysis data and correct wake losses during
the period of record (m/s). Defaults to 1 m/s.
num_years_LT (tuple | int, optional): Number of years of historical reanalysis data to
use for long-term correction. This should be a tuple when :py:attr:`UQ` = True
(values are Monte-Carlo sampled within the specified range) or a single value when
:py:attr:`UQ` = False. If undefined (None), a value of 20 will be used if
:py:attr:`UQ` = False and values of (10, 20) will be used if :py:attr:`UQ` = True.
Defaults to None.
assume_no_wakes_high_ws_LT_corr (bool, optional): If True, wind direction and wind speed
bins for which operational data are missing above a certain wind speed threshold are
corrected by assigning the wind turbines' rated power to both the actual and
potential power production variables during the long term-correction process. This
assumes there are no wake losses above the wind speed threshold. Defaults to True.
no_wakes_ws_thresh_LT_corr (float, optional): The wind speed threshold (inclusive) above
which rated power is assigned to both the actual and potential power production
variables if operational data are missing for any wind direction and wind speed bin
during the long term-correction process. This wind speed corresponds to the wind
speed measured at freestream wind turbines. Only used if
:py:attr:`assume_no_wakes_high_ws_LT_corr` = True. Defaults to 13 m/s.
min_ws_bin_lin_reg (float, optional): The minimum wind speed bin to consider when finding
linear regression from SCADA freestream wind speeds to reanalysis wind speeds. Defaults
to 3.0
bin_count_thresh_lin_reg (int, optional): The minimum number of samples required in a wind
speed bin to include when finding linear regression from SCADA freestream wind speeds to
reanalysis wind speeds. Defaults to 50.
"""
plant: PlantData = field(converter=deepcopy, validator=attrs.validators.instance_of(PlantData))
wind_direction_col: str = field(default="WMET_HorWdDir", converter=str)
wind_direction_data_type: str = field(
default="scada", validator=attrs.validators.in_(("scada", "tower"))
)
wind_direction_asset_ids: list[str] = field(default=None)
UQ: bool = field(default=True, converter=bool)
num_sim: int = field(default=100, converter=int)
start_date: str | pd.Timestamp = field(default=None)
end_date: str | pd.Timestamp = field(default=None)
reanalysis_products: list[str] = field(
default=None,
converter=convert_to_list,
validator=(
attrs.validators.deep_iterable(
iterable_validator=attrs.validators.instance_of(list),
member_validator=attrs.validators.instance_of((str, type(None))),
),
validate_reanalysis_selections,
),
)
end_date_lt: str | pd.Timestamp = field(default=None)
wd_bin_width: float = field(default=5.0)
freestream_sector_width: float | tuple[float, float] = field(
default=(50.0, 110.0), validator=validate_UQ_input
)
freestream_power_method: str = field(default="mean")
freestream_wind_speed_method: str = field(default="mean")
correct_for_derating: bool = field(default=True)
derating_filter_wind_speed_start: float | tuple[float, float] = field(
default=(4.0, 5.0), validator=validate_UQ_input
)
max_power_filter: float | tuple[float, float] = field(
default=(0.92, 0.98), validator=validate_UQ_input
)
wind_bin_mad_thresh: float | tuple[float, float] = field(
default=(4.0, 13.0), validator=validate_UQ_input
)
wd_bin_width_LT_corr: float = field(default=5.0)
ws_bin_width_LT_corr: float = field(default=1.0)
num_years_LT: int | tuple[int, int] = field(default=(10, 20), validator=validate_UQ_input)
assume_no_wakes_high_ws_LT_corr: bool = field(default=True)
no_wakes_ws_thresh_LT_corr: float = field(default=13.0)
min_ws_bin_lin_reg: float = field(default=3.0)
bin_count_thresh_lin_reg: int = field(default=50, validator=attrs.validators.instance_of(int))
# Internally created attributes need to be given a type before usage
turbine_ids: list[str] = field(init=False)
aggregate_df: pd.DataFrame = field(init=False)
inputs: pd.DataFrame = field(init=False)
aggregate_df_sample: pd.DataFrame = field(init=False)
wake_losses_por: NDArrayFloat = field(init=False)
turbine_wake_losses_por: NDArrayFloat = field(init=False)
wake_losses_lt: NDArrayFloat = field(init=False)
turbine_wake_losses_lt: NDArrayFloat = field(init=False)
wake_losses_por_wd: NDArrayFloat = field(init=False)
turbine_wake_losses_por_wd: NDArrayFloat = field(init=False)
wake_losses_lt_wd: NDArrayFloat = field(init=False)
turbine_wake_losses_lt_wd: NDArrayFloat = field(init=False)
energy_por_wd: NDArrayFloat = field(init=False)
energy_lt_wd: NDArrayFloat = field(init=False)
wake_losses_por_ws: NDArrayFloat = field(init=False)
turbine_wake_losses_por_ws: NDArrayFloat = field(init=False)
wake_losses_lt_ws: NDArrayFloat = field(init=False)
turbine_wake_losses_lt_ws: NDArrayFloat = field(init=False)
energy_por_ws: NDArrayFloat = field(init=False)
energy_lt_ws: NDArrayFloat = field(init=False)
wake_losses_lt_mean: float = field(init=False)
turbine_wake_losses_lt_mean: float = field(init=False)
wake_losses_por_mean: float = field(init=False)
turbine_wake_losses_por_mean: float = field(init=False)
wake_losses_lt_std: float = field(init=False)
turbine_wake_losses_lt_std: float = field(init=False)
wake_losses_por_std: float = field(init=False)
turbine_wake_losses_por_std: float = field(init=False)
_run: pd.DataFrame = field(init=False)
run_parameters: list[str] = field(
init=False,
default=[
"num_sim",
"reanalysis_products",
"wd_bin_width",
"freestream_sector_width",
"freestream_power_method",
"freestream_wind_speed_method",
"correct_for_derating",
"derating_filter_wind_speed_start",
"max_power_filter",
"wind_bin_mad_thresh",
"wd_bin_width_LT_corr",
"ws_bin_width_LT_corr",
"num_years_LT",
"assume_no_wakes_high_ws_LT_corr",
"no_wakes_ws_thresh_LT_corr",
"min_ws_bin_lin_reg",
"bin_count_thresh_lin_reg",
],
)
[docs]
@reanalysis_products.validator
def check_reanalysis_products(self, attribute: attrs.Attribute, value: list[str]) -> None:
"""Checks that the provided reanalysis products actually exist in the reanalysis data."""
if value == [None]:
return
valid = [*self.plant.reanalysis]
invalid = list(set(value).difference(valid))
if invalid:
raise ValueError(
f"The following input to `reanalysis_products`: {invalid} are not contained in `plant.reanalysis`: {valid}"
)
@logged_method_call
def __attrs_post_init__(self):
"""
Initialize logging and post-initialization setup steps.
"""
logger.info("Initializing WakeLosses analysis object")
if self.wind_direction_data_type == "scada":
if {"WakeLosses-scada", "all"}.intersection(self.plant.analysis_type) == set():
self.plant.analysis_type.append("WakeLosses-scada")
if self.wind_direction_data_type == "tower":
if {"WakeLosses-tower", "all"}.intersection(self.plant.analysis_type) == set():
self.plant.analysis_type.append("WakeLosses-tower")
# Ensure the data are up to spec before continuing with initialization
self.plant.validate()
# Check that selected UQ is allowed and reset num_sim if no UQ
if self.UQ:
logger.info("Note: uncertainty quantification will be performed in the calculation")
else:
logger.info("Note: uncertainty quantification will NOT be performed in the calculation")
# set default start and end dates if undefined
if self.start_date is None:
self.start_date = self.plant.scada.index.get_level_values("time").min()
if self.end_date is None:
self.end_date = self.plant.scada.index.get_level_values("time").max()
self.turbine_ids = list(self.plant.turbine_ids)
if (self.wind_direction_asset_ids is None) & (self.wind_direction_data_type == "scada"):
self.wind_direction_asset_ids = self.turbine_ids
elif (self.wind_direction_asset_ids is None) & (self.wind_direction_data_type == "tower"):
self.wind_direction_asset_ids = list(self.plant.tower_ids)
if self.end_date_lt is not None:
# Set minutes to 30 to handle time indices on the hour and on the half hour
self.end_date_lt = pd.to_datetime(self.end_date_lt).replace(minute=30)
else:
# Find most recent time common to all reanalysis products
self.end_date_lt = min(
[self.plant.reanalysis[product].index.max() for product in self.reanalysis_products]
).replace(minute=30)
# Run preprocessing steps
self._calculate_aggregate_dataframe()
[docs]
@logged_method_call
def run(
self,
num_sim: int | None = None,
reanalysis_products: list[str] | None = None,
wd_bin_width: float | None = None,
freestream_sector_width: float | None = None,
freestream_power_method: str | None = None,
freestream_wind_speed_method: str | None = None,
correct_for_derating: bool | None = None,
derating_filter_wind_speed_start: float | None = None,
max_power_filter: float | None = None,
wind_bin_mad_thresh: float | None = None,
wd_bin_width_LT_corr: float | None = None,
ws_bin_width_LT_corr: float | None = None,
num_years_LT: int | None = None,
assume_no_wakes_high_ws_LT_corr: bool | None = None,
no_wakes_ws_thresh_LT_corr: float | None = None,
min_ws_bin_lin_reg: float | None = None,
bin_count_thresh_lin_reg: int | None = None,
):
"""
Estimates wake losses by comparing wind plant energy production to energy production of the
turbines identified as operating in freestream conditions. Wake losses are expressed as a
fractional loss (e.g., 0.05 indicates a wake loss values of 5%).
.. note:: If None is provided to any of the inputs, then the last used input value will be
used for the analysis, and if no prior values were set, then this is the model's defaults.
Args:
num_sim (int, optional): Number of Monte Carlo iterations to perform. Only used if
:py:attr:`UQ` = True. Defaults to 100.
wd_bin_width (float, optional): Wind direction bin size when identifying freestream wind
turbines (degrees). Defaults to 5 degrees.
freestream_sector_width (tuple | float, optional): Wind direction sector size to use when
identifying freestream wind turbines (degrees). If no turbines are located upstream
of a particular turbine within the sector, the turbine will be classified as a
freestream turbine. When :py:attr:`UQ` = True, then this should be a tuple of the
lower and upper bounds for the Monte Carlo sampling, and when :py:attr:`UQ` = False
this should be a single value. If None, then a default value of 90 degrees will be
used if :py:attr:`UQ` = False and a default value of (50, 110) will be used if
:py:attr:`UQ` = True. Defaults to None.
freestream_power_method (str, optional): Method used to determine the representative power
prouction of the freestream turbines ("mean", "median", "max"). Defaults to "mean".
freestream_wind_speed_method (str, optional): Method used to determine the representative
wind speed of the freestream turbines ("mean", "median"). Defaults to "mean".
correct_for_derating (bool, optional): Indicates whether derated, curtailed, or otherwise
unavailable turbines should be flagged and excluded from the calculation of ideal
freestream wind plant power production for a given time stamp. If True, ideal
freestream power production will be calculated as the sum of the derated turbine
powers added to the mean power of the freestream turbines in normal operation
multiplied by the number of turbines operating normally in the wind plant. Defaults
to True.
derating_filter_wind_speed_start (tuple | float, optional): The wind speed above which
turbines will be flagged as derated/curtailed/shutdown if power is less than 1% of
rated power (m/s). Only used when :py:attr:`correct_for_derating` is True. This should
be a tuple when :py:attr:`UQ` = True (values are Monte-Carlo sampled within the
specified range) or a single value when :py:attr:`UQ` = False. If undefined (None), a
value of 4.5 m/s will be used if :py:attr:`UQ` = False and values of (4.0, 5.0) will
be used if :py:attr:`UQ` = True. Defaults to None.
max_power_filter (tuple | float, optional): Maximum power threshold, defined as a fraction
of rated power, to which the power curve bin filter should be applied. Only used when
:py:attr:`correct_for_derating` = True. This should be a tuple when :py:attr:`UQ` =
True (values are Monte-Carlo sampled within the specified range) or a single value
when :py:attr:`UQ` = False. If undefined (None), a value of 0.95 will be used if
:py:attr:`UQ` = False and values of (0.92, 0.98) will be used if :py:attr:`UQ` =
True. Defaults to None.
wind_bin_mad_thresh (tuple | float, optional): The filter threshold for each power bin
used to identify derated/curtailed/shutdown turbines, expressed as the number of
median absolute deviations above the median wind speed. Only used when
:py:attr:`correct_for_derating` is True. This should be a tuple when
:py:attr:`UQ` = True (values are Monte-Carlo sampled within the specified range) or
a single value when :py:attr:`UQ` = False. If undefined (None), a value of 7.0 will
be used if :py:attr:`UQ` = False and values of (4.0, 13.0) will be used if
:py:attr:`UQ` = True. Defaults to None.
wd_bin_width_LT_corr (float, optional): Size of wind direction bins used to calculate
long-term frequencies from historical reanalysis data and correct wake losses during
the period of record (degrees). Defaults to 5 degrees.
ws_bin_width_LT_corr (float, optional): Size of wind speed bins used to calculate
long-term frequencies from historical reanalysis data and correct wake losses during
the period of record (m/s). Defaults to 1 m/s.
num_years_LT (tuple | int, optional): Number of years of historical reanalysis data to
use for long-term correction. This should be a tuple when :py:attr:`UQ` = True
(values are Monte-Carlo sampled within the specified range) or a single value when
:py:attr:`UQ` = False. If undefined (None), a value of 20 will be used if
:py:attr:`UQ` = False and values of (10, 20) will be used if :py:attr:`UQ` = True.
Defaults to None.
assume_no_wakes_high_ws_LT_corr (bool, optional): If True, wind direction and wind speed
bins for which operational data are missing above a certain wind speed threshold are
corrected by assigning the wind turbines' rated power to both the actual and
potential power production variables during the long term-correction process. This
assumes there are no wake losses above the wind speed threshold. Defaults to True.
no_wakes_ws_thresh_LT_corr (float, optional): The wind speed threshold (inclusive) above
which rated power is assigned to both the actual and potential power production
variables if operational data are missing for any wind direction and wind speed bin
during the long term-correction process. This wind speed corresponds to the wind
speed measured at freestream wind turbines. Only used if
:py:attr:`assume_no_wakes_high_ws_LT_corr` = True. Defaults to 13 m/s.
min_ws_bin_lin_reg (float, optional): The minimum wind speed bin to consider when
finding linear regression from SCADA freestream wind speeds to reanalysis wind
speeds. Defaults to 3.0
bin_count_thresh_lin_reg (int, optional): The minimum number of samples required in a
wind speed bin to include when finding linear regression from SCADA freestream wind
speeds to reanalysis wind speeds. Defaults to 50.
"""
initial_parameters = {}
# Assign default parameter values depending on whether UQ is performed
if num_sim is not None:
initial_parameters["num_sim"] = num_sim
self.num_sim = num_sim
if reanalysis_products is not None:
initial_parameters["reanalysis_products"] = reanalysis_products
self.reanalysis_products = reanalysis_products
logger.warning(
f"`reanalysis_products` has been changed, be sure the `end_date_lt`"
f"({self.end_date_lt}) is contained in the updated reanalyis products subset."
)
if wd_bin_width is not None:
initial_parameters["wd_bin_width"] = self.wd_bin_width
self.wd_bin_width = wd_bin_width
if freestream_sector_width is not None:
initial_parameters["freestream_sector_width"] = self.freestream_sector_width
self.freestream_sector_width = freestream_sector_width
if freestream_power_method is not None:
initial_parameters["freestream_power_method"] = self.freestream_power_method
self.freestream_power_method = freestream_power_method
if freestream_wind_speed_method is not None:
initial_parameters["freestream_wind_speed_method"] = self.freestream_wind_speed_method
self.freestream_wind_speed_method = freestream_wind_speed_method
if correct_for_derating is not None:
initial_parameters["correct_for_derating"] = correct_for_derating
self.correct_for_derating = correct_for_derating
if derating_filter_wind_speed_start is not None:
initial_parameters[
"derating_filter_wind_speed_start"
] = self.derating_filter_wind_speed_start
self.derating_filter_wind_speed_start = derating_filter_wind_speed_start
if max_power_filter is not None:
initial_parameters["max_power_filter"] = self.max_power_filter
self.max_power_filter = max_power_filter
if wind_bin_mad_thresh is not None:
initial_parameters["wind_bin_mad_thresh"] = self.wind_bin_mad_thresh
self.wind_bin_mad_thresh = wind_bin_mad_thresh
if wd_bin_width_LT_corr is not None:
initial_parameters["wd_bin_width_LT_corr"] = self.wd_bin_width_LT_corr
self.wd_bin_width_LT_corr = wd_bin_width_LT_corr
if ws_bin_width_LT_corr is not None:
initial_parameters["ws_bin_width_LT_corr"] = self.ws_bin_width_LT_corr
self.ws_bin_width_LT_corr = ws_bin_width_LT_corr
if num_years_LT is not None:
initial_parameters["num_years_LT"] = self.num_years_LT
self.num_years_LT = num_years_LT
if assume_no_wakes_high_ws_LT_corr is not None:
initial_parameters[
"assume_no_wakes_high_ws_LT_corr"
] = self.assume_no_wakes_high_ws_LT_corr
self.assume_no_wakes_high_ws_LT_corr = assume_no_wakes_high_ws_LT_corr
if no_wakes_ws_thresh_LT_corr is not None:
initial_parameters["no_wakes_ws_thresh_LT_corr"] = self.no_wakes_ws_thresh_LT_corr
self.no_wakes_ws_thresh_LT_corr = no_wakes_ws_thresh_LT_corr
if min_ws_bin_lin_reg is not None:
initial_parameters["min_ws_bin_lin_reg"] = self.min_ws_bin_lin_reg
self.min_ws_bin_lin_reg = min_ws_bin_lin_reg
if bin_count_thresh_lin_reg is not None:
initial_parameters["bin_count_thresh_lin_reg"] = self.bin_count_thresh_lin_reg
self.bin_count_thresh_lin_reg = bin_count_thresh_lin_reg
# Set up Monte Carlo simulation inputs if UQ = True or single simulation inputs if UQ = False.
self._setup_monte_carlo_inputs()
for n in tqdm(range(self.num_sim)):
self._run = self.inputs.loc[n].copy()
# Estimate periods when each turbine is unavailable, derated, or curtailed, based on power curve filtering
for t in self.turbine_ids:
self.aggregate_df[("derate_flag", t)] = False
if self.correct_for_derating:
self._identify_derating()
# Randomly resample 10-minute periods for bootstrapping
if self.UQ:
self.aggregate_df_sample = self.aggregate_df.sample(frac=1.0, replace=True)
else:
self.aggregate_df_sample = self.aggregate_df.copy()
# For a set of wind direction bins, identify freestream turbines and calculate mean energy production and
# wind speed
self.aggregate_df_sample["power_mean_freestream"] = np.nan
self.aggregate_df_sample["windspeed_mean_freestream"] = np.nan
wd_bins = np.arange(0.0, 360.0, self.wd_bin_width)
# Create columns for turbine power and wind speed during normal operation (NaN otherwise)
for t in self.turbine_ids:
valid_inds = ~self.aggregate_df_sample[("derate_flag", t)]
self.aggregate_df_sample.loc[
valid_inds, ("power_normal", t)
] = self.aggregate_df_sample.loc[valid_inds, ("WTUR_W", t)]
for t in self.turbine_ids:
valid_inds = ~self.aggregate_df_sample[("derate_flag", t)]
self.aggregate_df_sample.loc[
valid_inds, ("windspeed_normal", t)
] = self.aggregate_df_sample.loc[valid_inds, ("WMET_HorWdSpd", t)]
# Find freestream turbines for each wind direction. Update the dictionary only when the set of turbines
# differs from the previous wind direction bin.
freestream_turbine_dict = {}
freestream_turbine_ids_prev = []
for wd in wd_bins:
# identify freestream turbines
freestream_turbine_ids = self.plant.get_freestream_turbines(
wd, sector_width=self._run.freestream_sector_width
)
if freestream_turbine_ids != freestream_turbine_ids_prev:
freestream_turbine_dict[wd] = freestream_turbine_ids
freestream_turbine_ids_prev = freestream_turbine_ids
if freestream_turbine_dict[0.0] == list(freestream_turbine_dict.values())[-1]:
freestream_turbine_dict.pop(0.0)
# Find freestream energy production for each wind direction sector containing the same freestream turbines
freestream_sector_wds = list(freestream_turbine_dict.keys())
for i_wd, wd in enumerate(freestream_sector_wds):
freestream_turbine_ids = freestream_turbine_dict[wd]
# if UQ is enabled, randomly resample set of freestream turbines
if self.UQ:
freestream_turbine_ids = random.choices(
freestream_turbine_ids, k=len(freestream_turbine_ids)
)
# Check whether last wind direction in dictionary and handle wind direction wrapping
# between 0 and 360 degrees
_agg_wd = self.aggregate_df_sample["wind_direction_ref"]
if wd == 0.0:
wd_bin_flag = _agg_wd >= 360.0 - 0.5 * self.wd_bin_width
wd_bin_flag |= _agg_wd < (
freestream_sector_wds[i_wd + 1] - 0.5 * self.wd_bin_width
)
elif i_wd < len(freestream_sector_wds) - 1:
wd_bin_flag = _agg_wd >= (wd - 0.5 * self.wd_bin_width)
wd_bin_flag &= _agg_wd < (
freestream_sector_wds[i_wd + 1] - 0.5 * self.wd_bin_width
)
elif (i_wd == len(freestream_sector_wds) - 1) & (freestream_sector_wds[0] == 0.0):
wd_bin_flag = _agg_wd >= (wd - 0.5 * self.wd_bin_width)
wd_bin_flag &= _agg_wd < (360.0 - 0.5 * self.wd_bin_width)
else: # last wind direction in dictionary and first wind direction is not zero:
wd_bin_flag = _agg_wd >= (wd - 0.5 * self.wd_bin_width)
wd_bin_flag |= _agg_wd < (freestream_sector_wds[0] - 0.5 * self.wd_bin_width)
# Assign representative energy and wind speed of freestream turbines. If correct_for_derating
# is True, only freestream turbines operating normally will be considered.
_power = self.aggregate_df_sample.loc[wd_bin_flag, "power_normal"]
if self.freestream_power_method == "mean":
_power = _power[freestream_turbine_ids].mean(axis=1)
elif self.freestream_power_method == "median":
_power = _power[freestream_turbine_ids].median(axis=1)
elif self.freestream_power_method == "max":
_power = _power[freestream_turbine_ids].max(axis=1)
self.aggregate_df_sample.loc[wd_bin_flag, "power_mean_freestream"] = _power
_ws = self.aggregate_df_sample.loc[wd_bin_flag, "windspeed_normal"]
if self.freestream_wind_speed_method == "mean":
_ws = _ws[freestream_turbine_ids].mean(axis=1)
elif self.freestream_wind_speed_method == "median":
_ws = _ws[freestream_turbine_ids].median(axis=1)
self.aggregate_df_sample.loc[wd_bin_flag, "windspeed_mean_freestream"] = _ws
# Remove rows where no freestream turbines in normal operation were identified
self.aggregate_df_sample = self.aggregate_df_sample.dropna(
subset=[("power_mean_freestream", ""), ("windspeed_mean_freestream", "")]
)
# calculate total plant-level wake losses during period of record
# Determine ideal wind plant energy, correcting for derated turbines if correct_for_derating is True. If
# correct_for_derating is True, ideal energy is calculated as the sum of the power produced by derated
# turbines and the mean power produced by freestream turbines operating normally multiplied by the total
# number of turbines operating normally
total_derated_turbine_power = (
self.aggregate_df_sample["WTUR_W"] * self.aggregate_df_sample["derate_flag"]
).sum(axis=1)
total_potential_freestream_power = self.aggregate_df_sample["power_mean_freestream"] * (
~self.aggregate_df_sample["derate_flag"]
).sum(axis=1)
# Assign total potential power
self.aggregate_df_sample["potential_plant_power"] = (
total_potential_freestream_power + total_derated_turbine_power
)
# Assign actual total power produced by wind plant
self.aggregate_df_sample["actual_plant_power"] = self.aggregate_df_sample["WTUR_W"].sum(
axis=1
)
wake_losses_por = (
1
- self.aggregate_df_sample["actual_plant_power"].sum()
/ self.aggregate_df_sample["potential_plant_power"].sum()
)
# bin wake losses by wind direction
# group wind farm efficiency by wind direction bin
self.aggregate_df_sample["wind_direction_bin"] = (
self.wd_bin_width_LT_corr
* (
self.aggregate_df_sample["wind_direction_ref"] / self.wd_bin_width_LT_corr
).round()
)
self.aggregate_df_sample.loc[
self.aggregate_df_sample["wind_direction_bin"] == 360.0, "wind_direction_bin"
] = 0.0
# Calculate turbine-level wake losses during period of record
turbine_wake_losses_por = len(self.turbine_ids) * [0.0]
for i, t in enumerate(self.turbine_ids):
# Determine ideal turbine energy as sum of the power produced by the turbine when it
# is derated and the mean power produced by all freestream turbines when the turbine
# is operating normally
self.aggregate_df_sample.loc[
~self.aggregate_df_sample[("derate_flag", t)], ("potential_turbine_power", t)
] = self.aggregate_df_sample.loc[
~self.aggregate_df_sample[("derate_flag", t)], "power_mean_freestream"
]
self.aggregate_df_sample.loc[
self.aggregate_df_sample[("derate_flag", t)], ("potential_turbine_power", t)
] = self.aggregate_df_sample.loc[
self.aggregate_df_sample[("derate_flag", t)], ("WTUR_W", t)
]
turbine_wake_losses_por[i] = (
1
- self.aggregate_df_sample[("WTUR_W", t)].sum()
/ self.aggregate_df_sample[("potential_turbine_power", t)].sum()
)
df_wd_bin = self.aggregate_df_sample.groupby("wind_direction_bin").sum()
# Save plant and turbine-level wake losses binned by wind direction
wake_losses_por_wd = (
df_wd_bin["actual_plant_power"] / df_wd_bin["potential_plant_power"]
).values
turbine_wake_losses_por_wd = np.empty(
[len(self.turbine_ids), int(360.0 / self.wd_bin_width_LT_corr)]
)
for i, t in enumerate(self.turbine_ids):
turbine_wake_losses_por_wd[i, :] = (
df_wd_bin[("WTUR_W", t)] / df_wd_bin[("potential_turbine_power", t)]
).values
if self.UQ:
self.wake_losses_por[n] = wake_losses_por
self.turbine_wake_losses_por[n, :] = turbine_wake_losses_por
self.wake_losses_por_wd[n, :] = wake_losses_por_wd
self.turbine_wake_losses_por_wd[n, :, :] = turbine_wake_losses_por_wd
self.energy_por_wd[n, :] = (
df_wd_bin["actual_plant_power"].values / df_wd_bin["actual_plant_power"].sum()
)
# apply long-term correction to wake losses
(
wake_losses_lt,
turbine_wake_losses_lt,
wake_losses_lt_wd,
turbine_wake_losses_lt_wd,
energy_lt_wd,
wake_losses_por_ws,
turbine_wake_losses_por_ws,
energy_por_ws,
wake_losses_lt_ws,
turbine_wake_losses_lt_ws,
energy_lt_ws,
) = self._apply_LT_correction()
self.wake_losses_lt[n] = wake_losses_lt
self.turbine_wake_losses_lt[n, :] = turbine_wake_losses_lt
self.wake_losses_lt_wd[n, :] = wake_losses_lt_wd
self.turbine_wake_losses_lt_wd[n, :, :] = turbine_wake_losses_lt_wd
self.energy_lt_wd[n, :] = energy_lt_wd
self.wake_losses_por_ws[n, :] = wake_losses_por_ws
self.turbine_wake_losses_por_ws[n, :, :] = turbine_wake_losses_por_ws
self.energy_por_ws[n, :] = energy_por_ws
self.wake_losses_lt_ws[n, :] = wake_losses_lt_ws
self.turbine_wake_losses_lt_ws[n, :, :] = turbine_wake_losses_lt_ws
self.energy_lt_ws[n, :] = energy_lt_ws
if not self.UQ:
# apply long-term correction to wake losses and average results over all reanalysis products
self.wake_losses_por = wake_losses_por
self.turbine_wake_losses_por = turbine_wake_losses_por
self.wake_losses_por_wd = wake_losses_por_wd
self.turbine_wake_losses_por_wd = turbine_wake_losses_por_wd
self.energy_por_wd = (
df_wd_bin["actual_plant_power"].values / df_wd_bin["actual_plant_power"].sum()
)
wake_losses_lt_all_products = np.empty([len(self.reanalysis_products), 1])
turbine_wake_losses_lt_all_products = np.empty(
[len(self.reanalysis_products), len(self.turbine_ids)]
)
wake_losses_lt_wd_all_products = np.empty(
[len(self.reanalysis_products), int(360.0 / self.wd_bin_width_LT_corr)]
)
turbine_wake_losses_lt_wd_all_products = np.empty(
[
len(self.reanalysis_products),
len(self.turbine_ids),
int(360.0 / self.wd_bin_width_LT_corr),
]
)
energy_lt_wd_all_products = np.empty(
[len(self.reanalysis_products), int(360.0 / self.wd_bin_width_LT_corr)]
)
wake_losses_por_ws_all_products = np.empty(
[len(self.reanalysis_products), int(30.0 / self.ws_bin_width_LT_corr) + 1]
)
turbine_wake_losses_por_ws_all_products = np.empty(
[
len(self.reanalysis_products),
len(self.turbine_ids),
int(30.0 / self.ws_bin_width_LT_corr) + 1,
]
)
energy_por_ws_all_products = np.empty(
[len(self.reanalysis_products), int(30.0 / self.ws_bin_width_LT_corr) + 1]
)
wake_losses_lt_ws_all_products = np.empty(
[len(self.reanalysis_products), int(30.0 / self.ws_bin_width_LT_corr) + 1]
)
turbine_wake_losses_lt_ws_all_products = np.empty(
[
len(self.reanalysis_products),
len(self.turbine_ids),
int(30.0 / self.ws_bin_width_LT_corr) + 1,
]
)
energy_lt_ws_all_products = np.empty(
[len(self.reanalysis_products), int(30.0 / self.ws_bin_width_LT_corr) + 1]
)
for i_rean, product in enumerate(self.reanalysis_products):
self._run.reanalysis_product = product
(
wake_losses_lt,
turbine_wake_losses_lt,
wake_losses_lt_wd,
turbine_wake_losses_lt_wd,
energy_lt_wd,
wake_losses_por_ws,
turbine_wake_losses_por_ws,
energy_por_ws,
wake_losses_lt_ws,
turbine_wake_losses_lt_ws,
energy_lt_ws,
) = self._apply_LT_correction()
wake_losses_lt_all_products[i_rean] = wake_losses_lt
turbine_wake_losses_lt_all_products[i_rean] = turbine_wake_losses_lt
wake_losses_lt_wd_all_products[i_rean] = wake_losses_lt_wd
turbine_wake_losses_lt_wd_all_products[i_rean] = turbine_wake_losses_lt_wd
energy_lt_wd_all_products[i_rean] = energy_lt_wd
wake_losses_por_ws_all_products[i_rean] = wake_losses_por_ws
turbine_wake_losses_por_ws_all_products[i_rean] = turbine_wake_losses_por_ws
energy_por_ws_all_products[i_rean] = energy_por_ws
wake_losses_lt_ws_all_products[i_rean] = wake_losses_lt_ws
turbine_wake_losses_lt_ws_all_products[i_rean] = turbine_wake_losses_lt_ws
energy_lt_ws_all_products[i_rean] = energy_lt_ws
self.wake_losses_lt = np.mean(wake_losses_lt_all_products)
self.turbine_wake_losses_lt = np.mean(turbine_wake_losses_lt_all_products, axis=0)
self.wake_losses_lt_wd = np.mean(wake_losses_lt_wd_all_products, axis=0)
self.turbine_wake_losses_lt_wd = np.mean(turbine_wake_losses_lt_wd_all_products, axis=0)
self.energy_lt_wd = np.mean(energy_lt_wd_all_products, axis=0)
self.wake_losses_por_ws = np.mean(wake_losses_por_ws_all_products, axis=0)
self.turbine_wake_losses_por_ws = np.mean(
turbine_wake_losses_por_ws_all_products, axis=0
)
self.energy_por_ws = np.mean(energy_por_ws_all_products, axis=0)
self.wake_losses_lt_ws = np.mean(wake_losses_lt_ws_all_products, axis=0)
self.turbine_wake_losses_lt_ws = np.mean(turbine_wake_losses_lt_ws_all_products, axis=0)
self.energy_lt_ws = np.mean(energy_lt_ws_all_products, axis=0)
else:
# Calculate mean and standard deviation of wake losses from Monte Carlo simulations
self.wake_losses_lt_mean = np.mean(self.wake_losses_lt)
self.turbine_wake_losses_lt_mean = np.mean(self.turbine_wake_losses_lt, axis=0)
self.wake_losses_por_mean = np.mean(self.wake_losses_por)
self.turbine_wake_losses_por_mean = np.mean(self.turbine_wake_losses_por, axis=0)
self.wake_losses_lt_std = np.std(self.wake_losses_lt)
self.turbine_wake_losses_lt_std = np.std(self.turbine_wake_losses_lt, axis=0)
self.wake_losses_por_std = np.std(self.wake_losses_por)
self.turbine_wake_losses_por_std = np.std(self.turbine_wake_losses_por, axis=0)
self.set_values(initial_parameters)
@logged_method_call
def _setup_monte_carlo_inputs(self):
"""
Create and populate the data frame defining the Monte Carlo simulation parameters. This
data frame is stored as ``self.inputs``.
"""
if self.UQ:
inputs = {
"reanalysis_product": random.choices(self.reanalysis_products, k=self.num_sim),
"freestream_sector_width": np.random.randint(
self.freestream_sector_width[0],
self.freestream_sector_width[1] + 1,
self.num_sim,
),
"wind_bin_mad_thresh": np.random.randint(
self.wind_bin_mad_thresh[0], self.wind_bin_mad_thresh[1] + 1, self.num_sim
),
"derating_filter_wind_speed_start": np.random.randint(
self.derating_filter_wind_speed_start[0] * 10,
self.derating_filter_wind_speed_start[1] * 10 + 1,
self.num_sim,
)
/ 10.0,
"max_power_filter": np.random.randint(
self.max_power_filter[0] * 100,
self.max_power_filter[1] * 100 + 1,
self.num_sim,
)
/ 100.0,
"num_years_LT": np.random.randint(
self.num_years_LT[0], self.num_years_LT[1] + 1, self.num_sim
),
}
self.inputs = pd.DataFrame(inputs)
self.wake_losses_por = np.empty([self.num_sim, 1])
self.turbine_wake_losses_por = np.empty([self.num_sim, len(self.turbine_ids)])
self.wake_losses_lt = np.empty([self.num_sim, 1])
self.turbine_wake_losses_lt = np.empty([self.num_sim, len(self.turbine_ids)])
# For saving wake losses and energy production binned by wind direction
self.wake_losses_por_wd = np.empty(
[self.num_sim, int(360.0 / self.wd_bin_width_LT_corr)]
)
self.turbine_wake_losses_por_wd = np.empty(
[self.num_sim, len(self.turbine_ids), int(360.0 / self.wd_bin_width_LT_corr)]
)
self.wake_losses_lt_wd = np.empty(
[self.num_sim, int(360.0 / self.wd_bin_width_LT_corr)]
)
self.turbine_wake_losses_lt_wd = np.empty(
[self.num_sim, len(self.turbine_ids), int(360.0 / self.wd_bin_width_LT_corr)]
)
self.energy_por_wd = np.empty([self.num_sim, int(360.0 / self.wd_bin_width_LT_corr)])
self.energy_lt_wd = np.empty([self.num_sim, int(360.0 / self.wd_bin_width_LT_corr)])
# For saving wake losses and energy production binned by wind speed
self.wake_losses_por_ws = np.empty(
[self.num_sim, int(30.0 / self.ws_bin_width_LT_corr) + 1]
)
self.turbine_wake_losses_por_ws = np.empty(
[self.num_sim, len(self.turbine_ids), int(30.0 / self.ws_bin_width_LT_corr) + 1]
)
self.wake_losses_lt_ws = np.empty(
[self.num_sim, int(30.0 / self.ws_bin_width_LT_corr) + 1]
)
self.turbine_wake_losses_lt_ws = np.empty(
[self.num_sim, len(self.turbine_ids), int(30.0 / self.ws_bin_width_LT_corr) + 1]
)
self.energy_por_ws = np.empty([self.num_sim, int(30.0 / self.ws_bin_width_LT_corr) + 1])
self.energy_lt_ws = np.empty([self.num_sim, int(30.0 / self.ws_bin_width_LT_corr) + 1])
elif not self.UQ:
inputs = {
"reanalysis_product": self.reanalysis_products,
"freestream_sector_width": len(self.reanalysis_products)
* [self.freestream_sector_width],
"wind_bin_mad_thresh": len(self.reanalysis_products) * [self.wind_bin_mad_thresh],
"derating_filter_wind_speed_start": len(self.reanalysis_products)
* [self.derating_filter_wind_speed_start],
"max_power_filter": len(self.reanalysis_products) * [self.max_power_filter],
"num_years_LT": len(self.reanalysis_products) * [self.num_years_LT],
}
self.inputs = pd.DataFrame(inputs)
self.num_sim = 1
@logged_method_call
def _calculate_aggregate_dataframe(self):
"""
Creates a data frame with relevant scada columns, plant-level columns, and reanalysis
variables to be used for the wake loss analysis. The reference mean wind direction is then
added to the data frame.
"""
# keep relevant SCADA columns, create a unique time index and two-level turbine variable columns
# (variable name and turbine asset_id)
# include scada wind direction column only if using scada to determine mean wind direction for wind plant
scada_cols = ["WMET_HorWdSpd", "WTUR_W"]
if self.wind_direction_data_type == "scada":
scada_cols.insert(1, self.wind_direction_col)
self.aggregate_df = self.plant.scada.loc[
self.start_date : self.end_date, scada_cols
].unstack()
# Calculate reference mean wind direction
self._calculate_mean_wind_direction()
# Add reanalysis data to aggregate data frame
self._include_reanal_data()
# remove times with any missing data
# TODO: revisit because this may remove too many samples
self.aggregate_df = self.aggregate_df.dropna(how="any")
# Drop turbine-level wind direction column
if self.wind_direction_data_type == "scada":
self.aggregate_df = self.aggregate_df.drop(columns=[self.wind_direction_col])
@logged_method_call
def _calculate_mean_wind_direction(self):
"""
Calculates the mean wind direction at each time step using the specified wind direction column for the
specified subset of turbines or met towers. This reference mean wind direction is added to the plant-level data
frame.
"""
if self.wind_direction_data_type == "scada":
self.aggregate_df["wind_direction_ref"] = met.circular_mean(
self.aggregate_df[self.wind_direction_col][self.wind_direction_asset_ids], axis=1
)
elif self.wind_direction_data_type == "tower":
df_tower = self.plant.tower[[self.wind_direction_col]].unstack()
self.aggregate_df["wind_direction_ref"] = met.circular_mean(
df_tower[self.wind_direction_col][self.wind_direction_asset_ids], axis=1
)
@logged_method_call
def _include_reanal_data(self):
"""
Combines reanalysis data columns with the aggregate data frame for use in long-term correction.
"""
# combine all wind speed and wind direction reanalysis variables into aggregate data frame
for product in self.reanalysis_products:
df_rean = self.plant.reanalysis[product][["WMETR_HorWdSpd", "WMETR_HorWdDir"]].copy()
# Drop minute field
df_rean.index = df_rean.index.floor("h")
# Upsample to match SCADA data frequency
df_rean = df_rean.resample(self.plant.metadata.scada.frequency).ffill()
df_rean = df_rean.add_suffix(f"_{product}")
df_rean = df_rean[df_rean.index.isin(self.aggregate_df.index)]
self.aggregate_df[[col for col in df_rean.columns]] = df_rean
@logged_method_call
def _identify_derating(self):
"""
Estimates whether each turbine is derated, curtailed, or otherwise not operating for each time stamp based on
power curve filtering. A derated flag is then added to the aggregate data frame for each turbine.
"""
for t in self.turbine_ids:
# Apply window range filter to flag samples for which wind speed is greater than a threshold and power is
# below 1% of rated power
turb_capac = self.plant.asset.loc[t, "rated_power"]
flag_window = filters.window_range_flag(
window_col=self.aggregate_df[("WMET_HorWdSpd", t)],
window_start=self._run.derating_filter_wind_speed_start,
window_end=40,
value_col=self.aggregate_df[("WTUR_W", t)],
value_min=0.01 * turb_capac,
value_max=1.2 * turb_capac,
)
# Apply bin-based filter to flag samples for which wind speed is greater than a threshold from the median
# wind speed in each power bin
bin_width_frac = 0.04 * (
self._run.max_power_filter - 0.01
) # split into 25 bins TODO: make this an optional argument?
flag_bin = filters.bin_filter(
bin_col=self.aggregate_df[("WTUR_W", t)],
value_col=self.aggregate_df[("WMET_HorWdSpd", t)],
bin_width=bin_width_frac * turb_capac,
threshold=self._run.wind_bin_mad_thresh, # wind bin thresh
center_type="median",
bin_min=0.01 * turb_capac,
bin_max=self._run.max_power_filter * turb_capac,
threshold_type="mad",
direction="above",
)
self.aggregate_df[("derate_flag", t)] = flag_window | flag_bin
@logged_method_call
def _apply_LT_correction(self):
"""
Estimates long term-corrected wake losses by binning wake losses by wind direction and wind
speed and weighting by bin frequencies from long-term historical reanalysis data.
Returns:
tuple[float, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: The estimated long
term-corrected wake losses, an array containing the estimated turbine-level long
term-corrected wake losses, and arrays containing the long-term corrected plant and
turbine-level wake losses as well as the normalized wind plant energy production
binned by wind direction
"""
# First, create hourly data frame for LT correction to match resolution of reanalysis data
df_1hr = self.aggregate_df_sample[
[
("wind_direction_ref", ""),
("windspeed_mean_freestream", ""),
("actual_plant_power", ""),
("potential_plant_power", ""),
]
+ [("WTUR_W", t) for t in self.turbine_ids]
+ [("potential_turbine_power", t) for t in self.turbine_ids]
+ [(f"WMETR_HorWdSpd_{self._run.reanalysis_product}", "")]
].copy()
df_1hr = df_1hr.resample("h").mean().dropna(how="any")
df_1hr["windspeed_mean_freestream_bin"] = df_1hr["windspeed_mean_freestream"].round()
# Bin by integer wind speeds
df_ws_bin = df_1hr.groupby(("windspeed_mean_freestream_bin", "")).mean()
df_ws_bin_count = df_1hr.groupby(("windspeed_mean_freestream_bin", "")).count()
valid_ws_bins = (df_ws_bin.index >= self.min_ws_bin_lin_reg) & (
df_ws_bin_count["windspeed_mean_freestream"] >= self.bin_count_thresh_lin_reg
)
# Find linear regression mapping from SCADA freestream wind speed to reanalysis wind speeds
# and use to correct SCADA freestream wind speeds
reg = LinearRegression().fit(
df_ws_bin.loc[valid_ws_bins].index.values.reshape(-1, 1),
df_ws_bin.loc[valid_ws_bins, f"WMETR_HorWdSpd_{self._run.reanalysis_product}"].values,
)
df_1hr[f"windspeed_mean_freestream_corr_{self._run.reanalysis_product}"] = reg.predict(
df_1hr["windspeed_mean_freestream"].values.reshape(-1, 1)
)
# adjust the no_wakes_ws_thresh_LT_corr parameter to relect the SCADA wind speed correction as well
no_wakes_ws_corr_thresh_LT_corr = np.round(
reg.predict(np.array(self.no_wakes_ws_thresh_LT_corr).reshape(1, -1))[0]
)
# get reanalysis data and limit date range
df_reanal = self.plant.reanalysis[self._run.reanalysis_product].copy()
df_reanal = df_reanal.loc[
(df_reanal.index <= self.end_date_lt)
& (
df_reanal.index
> self.end_date_lt - pd.offsets.DateOffset(years=self._run.num_years_LT)
)
]
df_reanal["windspeed_bin"] = (
self.ws_bin_width_LT_corr
* (df_reanal["WMETR_HorWdSpd"] / self.ws_bin_width_LT_corr).round()
)
df_reanal["wind_direction_bin"] = (
self.wd_bin_width_LT_corr
* (df_reanal["WMETR_HorWdDir"] / self.wd_bin_width_LT_corr).round()
)
df_reanal.loc[df_reanal["wind_direction_bin"] == 360.0, "wind_direction_bin"] = 0.0
df_reanal["freq"] = 1.0
df_reanal = df_reanal.groupby(["wind_direction_bin", "windspeed_bin"]).count()["freq"]
# Create data frame with long-term frequencies of wind direction and wind speed bins from reanalysis data
df_reanal_freqs = pd.DataFrame(df_reanal / df_reanal.sum())
# Weight wake losses in each wind direction and wind speed bin by long-term frequencies to
# estimate long-term wake losses
df_1hr["windspeed_bin"] = (
self.ws_bin_width_LT_corr
* (
df_1hr[f"windspeed_mean_freestream_corr_{self._run.reanalysis_product}"]
/ self.ws_bin_width_LT_corr
).round()
)
df_1hr["wind_direction_bin"] = (
self.wd_bin_width_LT_corr
* (df_1hr["wind_direction_ref"] / self.wd_bin_width_LT_corr).round()
)
df_1hr.loc[df_1hr["wind_direction_bin"] == 360.0, "wind_direction_bin"] = 0.0
# First, compute POR wake losses as a function of wind speed
df_1hr_ws_por_bin = df_1hr.groupby(("windspeed_bin", "")).sum()
# reindex to fill in missing wind speed bins
index = np.arange(0.0, 31.0, self.ws_bin_width_LT_corr).tolist()
df_1hr_ws_por_bin = df_1hr_ws_por_bin.reindex(index)
wake_losses_por_ws = (
df_1hr_ws_por_bin[("actual_plant_power", "")]
/ df_1hr_ws_por_bin[("potential_plant_power", "")]
).values
energy_por_ws = (
df_1hr_ws_por_bin[("actual_plant_power", "")].values
/ df_1hr_ws_por_bin[("actual_plant_power", "")].sum()
)
turbine_wake_losses_por_ws = np.empty(
[len(self.turbine_ids), int(30.0 / self.ws_bin_width_LT_corr) + 1]
)
for i, t in enumerate(self.turbine_ids):
turbine_wake_losses_por_ws[i, :] = (
df_1hr_ws_por_bin[("WTUR_W", t)] / df_1hr_ws_por_bin[("potential_turbine_power", t)]
).values
# Bin variables by wind direction and wind speed
df_1hr_bin = df_1hr.groupby([("wind_direction_bin", ""), ("windspeed_bin", "")]).mean()
df_1hr_bin = pd.concat([df_reanal_freqs, df_1hr_bin], axis=1)
# If specified, assume no wake losses at wind speeds above a given threshold for bins where
# data are missing by assigning rated power to the actual and potential power production
if self.assume_no_wakes_high_ws_LT_corr:
fill_inds = (df_1hr_bin[("actual_plant_power", "")].isna()) & (
df_1hr_bin.index.get_level_values(1) >= no_wakes_ws_corr_thresh_LT_corr
)
df_1hr_bin.loc[
fill_inds, [("actual_plant_power", ""), ("potential_plant_power", "")]
] = (self.plant.metadata.capacity * 1e3)
df_1hr_bin.loc[
fill_inds,
[("WTUR_W", t) for t in self.turbine_ids]
+ [("potential_turbine_power", t) for t in self.turbine_ids],
] = 2 * [self.plant.asset.loc[t, "rated_power"] for t in self.turbine_ids]
df_1hr_bin["actual_plant_energy"] = (
df_1hr_bin["freq"] * df_1hr_bin[("actual_plant_power", "")]
)
df_1hr_bin["potential_plant_energy"] = (
df_1hr_bin["freq"] * df_1hr_bin[("potential_plant_power", "")]
)
wake_losses_lt = 1 - (
df_1hr_bin["actual_plant_energy"].sum() / df_1hr_bin["potential_plant_energy"].sum()
)
# Calculate long-term corrected turbine-level wake losses
turbine_wake_losses_lt = len(self.turbine_ids) * [0.0]
for i, t in enumerate(self.turbine_ids):
# determine ideal turbine energy as sum of the power produced by the turbine when it is derated and the
# mean power produced by all freestream turbines when the turbine is operating normally
df_1hr_bin[("energy_avg", t)] = df_1hr_bin["freq"] * df_1hr_bin[("WTUR_W", t)]
df_1hr_bin[("potential_turbine_energy", t)] = (
df_1hr_bin["freq"] * df_1hr_bin[("potential_turbine_power", t)]
)
turbine_wake_losses_lt[i] = 1 - (
df_1hr_bin[("energy_avg", t)].sum()
/ df_1hr_bin[("potential_turbine_energy", t)].sum()
)
# Save long-term corrected plant and turbine-level wake losses binned by wind direction
df_1hr_wd_bin = df_1hr_bin.groupby(level=[0]).sum()
wake_losses_lt_wd = (
df_1hr_wd_bin["actual_plant_energy"] / df_1hr_wd_bin["potential_plant_energy"]
).values
energy_lt_wd = (
df_1hr_wd_bin["actual_plant_energy"].values / df_1hr_wd_bin["actual_plant_energy"].sum()
)
turbine_wake_losses_lt_wd = np.empty(
[len(self.turbine_ids), int(360.0 / self.wd_bin_width_LT_corr)]
)
for i, t in enumerate(self.turbine_ids):
turbine_wake_losses_lt_wd[i, :] = (
df_1hr_wd_bin[("energy_avg", t)] / df_1hr_wd_bin[("potential_turbine_energy", t)]
).values
# Save long-term corrected plant and turbine-level wake losses binned by wind speed
df_1hr_ws_bin = df_1hr_bin.groupby(level=[1]).sum()
# reindex to fill in missing wind speed bins
index = np.arange(0.0, 31.0, self.ws_bin_width_LT_corr).tolist()
df_1hr_ws_bin = df_1hr_ws_bin.reindex(index)
wake_losses_lt_ws = (
df_1hr_ws_bin["actual_plant_energy"] / df_1hr_ws_bin["potential_plant_energy"]
).values
energy_lt_ws = (
df_1hr_ws_bin["actual_plant_energy"].values / df_1hr_ws_bin["actual_plant_energy"].sum()
)
turbine_wake_losses_lt_ws = np.empty(
[len(self.turbine_ids), int(30.0 / self.ws_bin_width_LT_corr) + 1]
)
for i, t in enumerate(self.turbine_ids):
turbine_wake_losses_lt_ws[i, :] = (
df_1hr_ws_bin[("energy_avg", t)] / df_1hr_ws_bin[("potential_turbine_energy", t)]
).values
return (
wake_losses_lt,
turbine_wake_losses_lt,
wake_losses_lt_wd,
turbine_wake_losses_lt_wd,
energy_lt_wd,
wake_losses_por_ws,
turbine_wake_losses_por_ws,
energy_por_ws,
wake_losses_lt_ws,
turbine_wake_losses_lt_ws,
energy_lt_ws,
)
[docs]
def plot_wake_losses_by_wind_direction(
self,
plot_norm_energy: bool = True,
turbine_id: str = None,
xlim: tuple[float, float] = (None, None),
ylim_efficiency: tuple[float, float] = (None, None),
ylim_energy: tuple[float, float] = (None, None),
return_fig: bool = False,
figure_kwargs: dict = None,
plot_kwargs_line: dict = {},
plot_kwargs_fill: dict = {},
legend_kwargs: dict = {},
):
"""
Plots wake losses in the form of wind farm efficiency as well as normalized wind plant energy
production for both the period of record and with the long-term correction as a function of
wind direction.
Args:
plot_norm_energy (bool, optional): If True, include a plot of normalized wind plant energy
production as a function of wind direction in addition to the wind farm efficiency plot.
Defaults to True.
turbine_id (str, optional): Turbine asset_id to plot wake losses for. If None, wake losses for the
entire wind plant will be plotted. Defaults to None.
xlim (:obj:`tuple[float, float]`, optional): A tuple of floats representing the x-axis
wind direction plotting display limits (degrees). Defaults to (None, None).
ylim_efficiency (:obj:`tuple[float, float]`, optional): A tuple of the y-axis plotting display
limits for the wind farm efficiency plot (top plot). Defaults to (None, None).
ylim_energy (:obj:`tuple[float, float]`, optional): If `plot_norm_energy` is True, a tuple
of the y-axis plotting display limits for the wind farm energy distribution plot (bottom
plot). Defaults to (None, None).
return_fig (:obj:`bool`, optional): Flag to return the figure and axes objects. Defaults to False.
figure_kwargs (:obj:`dict`, optional): Additional figure instantiation keyword arguments
that are passed to `plt.figure()`. Defaults to None.
plot_kwargs_line (:obj:`dict`, optional): Additional plotting keyword arguments that are passed to
``ax.plot()`` for plotting lines for the wind farm efficiency and, if `plot_norm_energy` is True,
energy distributions subplots. Defaults to {}.
plot_kwargs_fill (:obj:`dict`, optional): If `UQ` is True, additional plotting keyword arguments
that are passed to ``ax.fill_between()`` for plotting shading regions for 95% confidence
intervals for the wind farm efficiency and, if `plot_norm_energy` is True, energy
distributions subplots. Defaults to {}.
legend_kwargs (:obj:`dict`, optional): Additional legend keyword arguments that are passed to
``ax.legend()`` for the wind farm efficiency and, if `plot_norm_energy` is True, energy
distributions subplots. Defaults to {}.
Returns:
None | tuple[matplotlib.pyplot.Figure, matplotlib.pyplot.Axes] | tuple[matplotlib.pyplot.Figure, tuple [matplotlib.pyplot.Axes, matplotlib.pyplot.Axes]]:
If :py:attr:`return_fig` is True, then the figure and axes object(s), corresponding to the wake
loss plot or, if :py:attr:`plot_norm_energy` is True, wake loss and normalized energy plots, are
returned for further tinkering/saving.
"""
wd_bins = np.arange(0.0, 360.0, self.wd_bin_width_LT_corr)
if turbine_id is None:
efficiency_data_por = self.wake_losses_por_wd
efficiency_data_lt = self.wake_losses_lt_wd
else:
turbine_index = self.turbine_ids.index(turbine_id)
if self.UQ:
efficiency_data_por = self.turbine_wake_losses_por_wd[:, turbine_index, :]
efficiency_data_lt = self.turbine_wake_losses_lt_wd[:, turbine_index, :]
else:
efficiency_data_por = self.turbine_wake_losses_por_wd[turbine_index, :]
efficiency_data_lt = self.turbine_wake_losses_lt_wd[turbine_index, :]
if plot_norm_energy:
energy_data_por = self.energy_por_wd
energy_data_lt = self.energy_lt_wd
else:
energy_data_por = None
energy_data_lt = None
return plot.plot_wake_losses(
bins=wd_bins,
efficiency_data_por=efficiency_data_por,
efficiency_data_lt=efficiency_data_lt,
energy_data_por=energy_data_por,
energy_data_lt=energy_data_lt,
bin_axis_label=r"Wind Direction ($^\circ$)",
turbine_id=turbine_id,
xlim=xlim,
ylim_efficiency=ylim_efficiency,
ylim_energy=ylim_energy,
return_fig=return_fig,
figure_kwargs=figure_kwargs,
plot_kwargs_line=plot_kwargs_line,
plot_kwargs_fill=plot_kwargs_fill,
legend_kwargs=legend_kwargs,
)
[docs]
def plot_wake_losses_by_wind_speed(
self,
plot_norm_energy: bool = True,
turbine_id: str = None,
xlim: tuple[float, float] = (None, None),
ylim_efficiency: tuple[float, float] = (None, None),
ylim_energy: tuple[float, float] = (None, None),
return_fig: bool = False,
figure_kwargs: dict = None,
plot_kwargs_line: dict = {},
plot_kwargs_fill: dict = {},
legend_kwargs: dict = {},
):
"""
Plots wake losses in the form of wind farm efficiency as well as normalized wind plant energy
production for both the period of record and with the long-term correction as a function of
wind speed.
Args:
plot_norm_energy (bool, optional): If True, include a plot of normalized wind plant energy
production as a function of wind speed in addition to the wind farm efficiency plot. Defaults to
True.
turbine_id (str, optional): Turbine asset_id to plot wake losses for. If None, wake losses for the
entire wind plant will be plotted. Defaults to None.
xlim (:obj:`tuple[float, float]`, optional): A tuple of floats representing the x-axis
wind speed plotting display limits (degrees). Defaults to (None, None).
ylim_efficiency (:obj:`tuple[float, float]`, optional): A tuple of the y-axis plotting display
limits for the wind farm efficiency plot (top plot). Defaults to (None, None).
ylim_energy (:obj:`tuple[float, float]`, optional): If `plot_norm_energy` is True, a tuple
of the y-axis plotting display limits for the wind farm energy distribution plot (bottom
plot). Defaults to (None, None).
return_fig (:obj:`bool`, optional): Flag to return the figure and axes objects. Defaults to False.
figure_kwargs (:obj:`dict`, optional): Additional figure instantiation keyword arguments
that are passed to ``plt.figure()``. Defaults to None.
plot_kwargs_line (:obj:`dict`, optional): Additional plotting keyword arguments that are passed to
``ax.plot()`` for plotting lines for the wind farm efficiency and, if :py:attr:`plot_norm_energy` is True,
energy distributions subplots. Defaults to {}.
plot_kwargs_fill (:obj:`dict`, optional): If `UQ` is True, additional plotting keyword arguments
that are passed to ``ax.fill_between()`` for plotting shading regions for 95% confidence
intervals for the wind farm efficiency and, if :py:attr:`plot_norm_energy` is True, energy
distributions subplots. Defaults to {}.
legend_kwargs (:obj:`dict`, optional): Additional legend keyword arguments that are passed to
``ax.legend()`` for the wind farm efficiency and, if :py:attr:`plot_norm_energy` is True, energy
distributions subplots. Defaults to {}.
Returns:
None | tuple[matplotlib.pyplot.Figure, matplotlib.pyplot.Axes] | tuple[matplotlib.pyplot.Figure, tuple [matplotlib.pyplot.Axes, matplotlib.pyplot.Axes]]:
If :py:attr:`return_fig` is True, then the figure and axes object(s), corresponding to the wake
loss plot or, if :py:attr:`plot_norm_energy` is True, wake loss and normalized energy plots, are
returned for further tinkering/saving.
"""
ws_bins_orig = np.arange(0.0, 31.0, self.ws_bin_width_LT_corr)
if xlim == (None, None):
# Default to the range 4 - 20 m/s
ws_min = 4.0
ws_max = 20.0
else:
ws_min = np.max([0.0, np.floor(xlim[0])])
ws_max = np.min([ws_bins_orig[-1], np.ceil(xlim[1])])
ws_bins = np.arange(ws_min, ws_max + 1, self.ws_bin_width_LT_corr)
mask = (ws_bins_orig >= ws_min) & (ws_bins_orig <= ws_max)
if turbine_id is None:
if self.UQ:
efficiency_data_por = self.wake_losses_por_ws[:, mask]
efficiency_data_lt = self.wake_losses_lt_ws[:, mask]
else:
efficiency_data_por = self.wake_losses_por_ws[mask]
efficiency_data_lt = self.wake_losses_lt_ws[mask]
else:
turbine_index = self.turbine_ids.index(turbine_id)
if self.UQ:
efficiency_data_por = self.turbine_wake_losses_por_ws[:, turbine_index, mask]
efficiency_data_lt = self.turbine_wake_losses_lt_ws[:, turbine_index, mask]
else:
efficiency_data_por = self.turbine_wake_losses_por_ws[turbine_index, mask]
efficiency_data_lt = self.turbine_wake_losses_lt_ws[turbine_index, mask]
if plot_norm_energy:
if self.UQ:
energy_data_por = self.energy_por_ws[:, mask]
energy_data_lt = self.energy_lt_ws[:, mask]
else:
energy_data_por = self.energy_por_ws[mask]
energy_data_lt = self.energy_lt_ws[mask]
else:
energy_data_por = None
energy_data_lt = None
return plot.plot_wake_losses(
bins=ws_bins,
efficiency_data_por=efficiency_data_por,
efficiency_data_lt=efficiency_data_lt,
energy_data_por=energy_data_por,
energy_data_lt=energy_data_lt,
bin_axis_label=r"Freestream Wind Speed (m/s)",
turbine_id=turbine_id,
xlim=xlim,
ylim_efficiency=ylim_efficiency,
ylim_energy=ylim_energy,
return_fig=return_fig,
figure_kwargs=figure_kwargs,
plot_kwargs_line=plot_kwargs_line,
plot_kwargs_fill=plot_kwargs_fill,
legend_kwargs=legend_kwargs,
)
__defaults_wind_direction_col = WakeLosses.__attrs_attrs__.wind_direction_col.default
__defaults_wind_direction_data_type = WakeLosses.__attrs_attrs__.wind_direction_data_type.default
__defaults_wind_direction_asset_ids = WakeLosses.__attrs_attrs__.wind_direction_asset_ids.default
__defaults_UQ = WakeLosses.__attrs_attrs__.UQ.default
__defaults_num_sim = WakeLosses.__attrs_attrs__.num_sim.default
__defaults_start_date = WakeLosses.__attrs_attrs__.start_date.default
__defaults_end_date = WakeLosses.__attrs_attrs__.end_date.default
__defaults_reanalysis_products = WakeLosses.__attrs_attrs__.reanalysis_products.default
__defaults_end_date_lt = WakeLosses.__attrs_attrs__.end_date_lt.default
__defaults_wd_bin_width = WakeLosses.__attrs_attrs__.wd_bin_width.default
__defaults_freestream_sector_width = WakeLosses.__attrs_attrs__.freestream_sector_width.default
__defaults_freestream_power_method = WakeLosses.__attrs_attrs__.freestream_power_method.default
__defaults_freestream_wind_speed_method = (
WakeLosses.__attrs_attrs__.freestream_wind_speed_method.default
)
__defaults_correct_for_derating = WakeLosses.__attrs_attrs__.correct_for_derating.default
__defaults_derating_filter_wind_speed_start = (
WakeLosses.__attrs_attrs__.derating_filter_wind_speed_start.default
)
__defaults_max_power_filter = WakeLosses.__attrs_attrs__.max_power_filter.default
__defaults_wind_bin_mad_thresh = WakeLosses.__attrs_attrs__.wind_bin_mad_thresh.default
__defaults_wd_bin_width_LT_corr = WakeLosses.__attrs_attrs__.wd_bin_width_LT_corr.default
__defaults_ws_bin_width_LT_corr = WakeLosses.__attrs_attrs__.ws_bin_width_LT_corr.default
__defaults_num_years_LT = WakeLosses.__attrs_attrs__.num_years_LT.default
__defaults_assume_no_wakes_high_ws_LT_corr = (
WakeLosses.__attrs_attrs__.assume_no_wakes_high_ws_LT_corr.default
)
__defaults_no_wakes_ws_thresh_LT_corr = (
WakeLosses.__attrs_attrs__.no_wakes_ws_thresh_LT_corr.default
)
def create_WakeLosses(
project: PlantData,
wind_direction_col: str = __defaults_wind_direction_col,
wind_direction_data_type: str = __defaults_wind_direction_data_type,
wind_direction_asset_ids: list[str] = __defaults_wind_direction_asset_ids,
UQ: bool = __defaults_UQ,
num_sim: int = __defaults_num_sim,
start_date: str | pd.Timestamp = __defaults_start_date,
end_date: str | pd.Timestamp = __defaults_end_date,
reanalysis_products: list[str] = __defaults_reanalysis_products,
end_date_lt: str | pd.Timestamp = __defaults_end_date_lt,
wd_bin_width: float = __defaults_wd_bin_width,
freestream_sector_width: float = __defaults_freestream_sector_width,
freestream_power_method: str = __defaults_freestream_power_method,
freestream_wind_speed_method: str = __defaults_freestream_wind_speed_method,
correct_for_derating: bool = __defaults_correct_for_derating,
derating_filter_wind_speed_start: float = __defaults_derating_filter_wind_speed_start,
max_power_filter: float = __defaults_max_power_filter,
wind_bin_mad_thresh: float = __defaults_wind_bin_mad_thresh,
wd_bin_width_LT_corr: float = __defaults_wd_bin_width_LT_corr,
ws_bin_width_LT_corr: float = __defaults_ws_bin_width_LT_corr,
num_years_LT: int = __defaults_num_years_LT,
assume_no_wakes_high_ws_LT_corr: bool = __defaults_assume_no_wakes_high_ws_LT_corr,
no_wakes_ws_thresh_LT_corr: float = __defaults_no_wakes_ws_thresh_LT_corr,
) -> WakeLosses:
return WakeLosses(
plant=project,
wind_direction_col=wind_direction_col,
wind_direction_data_type=wind_direction_data_type,
wind_direction_asset_ids=wind_direction_asset_ids,
UQ=UQ,
num_sim=num_sim,
start_date=start_date,
end_date=end_date,
reanalysis_products=reanalysis_products,
end_date_lt=end_date_lt,
wd_bin_width=wd_bin_width,
freestream_sector_width=freestream_sector_width,
freestream_power_method=freestream_power_method,
freestream_wind_speed_method=freestream_wind_speed_method,
correct_for_derating=correct_for_derating,
derating_filter_wind_speed_start=derating_filter_wind_speed_start,
max_power_filter=max_power_filter,
wind_bin_mad_thresh=wind_bin_mad_thresh,
wd_bin_width_LT_corr=wd_bin_width_LT_corr,
ws_bin_width_LT_corr=ws_bin_width_LT_corr,
num_years_LT=num_years_LT,
assume_no_wakes_high_ws_LT_corr=assume_no_wakes_high_ws_LT_corr,
no_wakes_ws_thresh_LT_corr=no_wakes_ws_thresh_LT_corr,
)
create_WakeLosses.__doc__ = WakeLosses.__doc__