from pathlib import Path
import numpy as np
import openmdao.api as om
import floris
[docs]
def create_windresource_from_windIO(
windIOdict: dict,
resource_type: str = None, # ["probability", "timeseries", "weibull_sector"]
):
"""
takes a windIO plant specification and creates an appropriate wind resource
Parameters
----------
windIOdict : dict
a full, presumed validated, windIO plant specification file
resource_type : str, optional
one of "probability", "timeseries", "weibull_sector" indicating, either
a "probability"-based representation by a FLORIS WindRose object, a
"timeseries" representation using a FLORIS TimeSeries object, or a
"weibull_sector" representation which has not yet been implemented
Returns
-------
floris.WindDataBase
a FLORIS wind resource object that encodes the windIO wind resource
Raises
------
KeyError
if the input file is not read correctly
ValueError
if values found in the input file have an issue
NotImplementedError
if an unimplemented case is found
"""
if not "site" in windIOdict: # make sure the site is specified
raise KeyError("No site specified in windIO plant dictionary.")
if "energy_resource" not in windIOdict["site"]:
raise KeyError("Missing 'energy_resource' in windIOdict['site'].")
if "wind_resource" not in windIOdict["site"]["energy_resource"]:
raise KeyError(
"Missing 'wind_resource' in windIOdict['site']['energy_resource']."
)
# get the wind resource specification out of the dictionary
wind_resource = windIOdict["site"]["energy_resource"]["wind_resource"]
# figure out the case in play
fields_wind_resource = wind_resource.keys()
case_probability_based = all(
val in fields_wind_resource
for val in ["probability", "wind_direction", "wind_speed"]
)
case_weibull_based = all(
val in fields_wind_resource
for val in ["weibull_a", "weibull_k", "weibull_probability"]
)
case_timeseries_based = all(
val in fields_wind_resource for val in ["time", "wind_direction", "wind_speed"]
)
if case_weibull_based and not (case_probability_based or case_timeseries_based):
if resource_type is not None and resource_type != "weibull_sector":
raise ValueError(
f"Attempted to load {resource_type}-type wind resource and "
"only weibull_sector was found."
)
raise NotImplementedError(
"Sector Weibull wind resource has not yet been implemented for FLORIS."
)
wind_resource_representation = None
if case_probability_based and case_timeseries_based:
raise ValueError(
"Both probability-based and time-series-based wind resource "
"specifications have been found; this is ambiguous."
)
elif case_probability_based:
if resource_type is not None and resource_type != "probability":
raise ValueError(
f"Attempted to load {resource_type}-type wind resource and "
"only probability was found."
)
# extract key variables
wind_directions = np.array(
wind_resource["wind_direction"]["data"]
if "data" in wind_resource["wind_direction"]
else wind_resource["wind_direction"]
)
wind_speeds = np.array(
wind_resource["wind_speed"]["data"]
if "data" in wind_resource["wind_speed"]
else wind_resource["wind_speed"]
)
probabilities = np.array(wind_resource["probability"]["data"])
if "turbulence_intensity" not in wind_resource:
raise KeyError(
"windIO does not require turbulence intensities to be set, but "
"FLORIS requires turbulence intensities; please set the "
"turbulence intensities in the windIO file."
)
turbulence_intensities = np.array(
wind_resource["turbulence_intensity"]["data"]
if "data" in wind_resource["turbulence_intensity"]
else wind_resource["turbulence_intensity"]
)
# create FLORIS representation
wind_resource_representation = floris.WindRose(
wind_directions=wind_directions,
wind_speeds=wind_speeds,
freq_table=probabilities,
ti_table=turbulence_intensities,
)
# stash some metadata for the wind resource
wind_resource_representation.reference_height = (
wind_resource["reference_height"]
if "reference_height" in wind_resource
else None
)
return wind_resource_representation
elif case_timeseries_based:
if resource_type is not None and resource_type != "timeseries":
raise ValueError(
f"Attempted to load {resource_type}-type wind resource and "
"only time-series was found."
)
wind_directions = np.array(
wind_resource["wind_direction"].get("data", wind_resource["wind_direction"])
if type(wind_resource["wind_direction"]) is dict
else wind_resource["wind_direction"]
)
wind_speeds = np.array(
wind_resource["wind_speed"].get("data", wind_resource["wind_speed"])
if type(wind_resource["wind_speed"]) is dict
else wind_resource["wind_speed"]
)
if "turbulence_intensity" in wind_resource:
if "data" in wind_resource["turbulence_intensity"]:
turbulence_intensities = np.array(
wind_resource["turbulence_intensity"]["data"]
)
else:
turbulence_intensities = np.array(wind_resource["turbulence_intensity"])
else:
raise KeyError(
"Missing 'turbulence_intensity' in time-series wind resource."
)
wind_resource_representation = floris.TimeSeries(
wind_directions=wind_directions,
wind_speeds=wind_speeds,
turbulence_intensities=turbulence_intensities,
)
# stash some metadata for the wind resource
wind_resource_representation.reference_height = (
wind_resource["reference_height"]
if "reference_height" in wind_resource
else None
)
wind_resource_representation.time = (
wind_resource["time"] if "time" in wind_resource else None
)
return wind_resource_representation
else:
raise ValueError(
"You shouldn't have ended here, try validating the windIO yaml file."
)
[docs]
class FarmAeroTemplate(om.ExplicitComponent):
"""
Template component for using a farm aerodynamics model.
A farm aerodynamics component, based on this template, will compute the
aerodynamics for a farm with some layout and yaw configuration.
Options
-------
modeling_options : dict
a modeling options dictionary
Inputs
------
x_turbines : np.ndarray
a 1D numpy array indicating the x-dimension locations of the turbines,
with length `N_turbines`
y_turbines : np.ndarray
a 1D numpy array indicating the y-dimension locations of the turbines,
with length `N_turbines`
yaw_turbines : np.ndarray
a numpy array indicating the yaw angle to drive each turbine to with
respect to the ambient wind direction, with length `N_turbines`
Outputs
-------
None
"""
[docs]
def initialize(self):
"""Initialization of OM component."""
self.options.declare("modeling_options")
self.options.declare("data_path")
[docs]
def setup(self):
"""Setup of OM component."""
# load modeling options
self.modeling_options = self.options["modeling_options"]
self.windIO = self.modeling_options["windIO_plant"]
self.N_turbines = self.modeling_options["layout"]["N_turbines"]
# set up inputs and outputs for farm layout
self.add_input("x_turbines", np.zeros((self.N_turbines,)), units="m")
self.add_input("y_turbines", np.zeros((self.N_turbines,)), units="m")
self.add_input(
"yaw_turbines",
np.zeros((self.N_turbines,)),
units="deg",
)
[docs]
def compute(self, inputs, outputs):
"""
Computation for the OM component.
For a template class this is not implemented and raises an error!
"""
#############################################
# #
# IMPLEMENT THE AERODYNAMICS COMPONENT HERE #
# #
#############################################
raise NotImplementedError(
"This is an abstract class for a derived class to implement!"
)
[docs]
class BatchFarmPowerTemplate(FarmAeroTemplate):
"""
Template component for computing power using a farm aerodynamics model.
A farm power component, based on this template, will compute the power and
thrust for a farm composed of a given rotor type.
Options
-------
modeling_options : dict
a modeling options dictionary (inherited from `FarmAeroTemplate`)
Inputs
------
x_turbines : np.ndarray
a 1D numpy array indicating the x-dimension locations of the turbines,
with length `N_turbines` (inherited from `FarmAeroTemplate`)
y_turbines : np.ndarray
a 1D numpy array indicating the y-dimension locations of the turbines,
with length `N_turbines` (inherited from `FarmAeroTemplate`)
yaw_turbines : np.ndarray
a numpy array indicating the yaw angle to drive each turbine to with
respect to the ambient wind direction, with length `N_turbines`
(inherited from `FarmAeroTemplate`)
Outputs
-------
power_farm : np.ndarray
an array of the farm power for each of the wind conditions that have
been queried
power_turbines : np.ndarray
an array of the farm power for each of the turbines in the farm across
all of the conditions that have been queried on the wind rose
(`N_turbines`, `N_wind_conditions`)
thrust_turbines : np.ndarray
an array of the wind turbine thrust for each of the turbines in the farm
across all of the conditions that have been queried on the wind rose
(`N_turbines`, `N_wind_conditions`)
"""
[docs]
def initialize(self):
"""Initialization of OM component."""
super().initialize()
[docs]
def setup(self):
"""Setup of OM component."""
super().setup()
# unpack wind query object
self.wind_query = create_windresource_from_windIO(
self.windIO,
"timeseries",
)
self.directions_wind = self.wind_query.wind_directions.tolist()
self.speeds_wind = self.wind_query.wind_speeds.tolist()
self.TIs_wind = self.wind_query.turbulence_intensities.tolist()
self.N_wind_conditions = len(self.directions_wind)
# add the outputs we want for a batched power analysis:
# - farm and turbine powers
# - turbine thrusts
# self.add_output(
# "AEP_farm",
# np.array([0.0]),
# units="W*h",
# )
self.add_output(
"AEP_farm",
0.0,
units="W*h",
)
self.add_output(
"power_farm",
np.zeros((self.N_wind_conditions,)),
units="W",
)
if self.options["modeling_options"]["aero"]["return_turbine_output"]:
self.add_output(
"power_turbines",
np.zeros((self.N_turbines, self.N_wind_conditions)),
units="W",
)
self.add_output(
"thrust_turbines",
np.zeros((self.N_turbines, self.N_wind_conditions)),
units="N",
)
[docs]
def setup_partials(self):
"""Derivative setup for OM component."""
# the default (but not preferred!) derivatives are FDM
self.declare_partials("*", "*", method="fd")
[docs]
def compute(self, inputs, outputs):
"""
Computation for the OM component.
For a template class this is not implemented and raises an error!
"""
#############################################
# #
# IMPLEMENT THE AERODYNAMICS COMPONENT HERE #
# #
#############################################
raise NotImplementedError(
"This is an abstract class for a derived class to implement!"
)
# the following should be set
outputs["power_farm"] = np.zeros((self.N_wind_conditions,))
if self.options["modeling_options"]["aero"]["return_turbine_output"]:
outputs["power_turbines"] = np.zeros(
(self.N_turbines, self.N_wind_conditions)
)
outputs["thrust_turbines"] = np.zeros(
(self.N_turbines, self.N_wind_conditions)
)
[docs]
class FarmAEPTemplate(FarmAeroTemplate):
"""
A template component for computing power using a farm aerodynamics model.
A farm power component, based on this template, will compute the power and
thrust for a farm composed of a given rotor type.
Options
-------
modeling_options : dict
a modeling options dictionary (inherited from FarmAeroTemplate)
data_path: str
absolute path to data directory
Inputs
------
x_turbines : np.ndarray
a 1D numpy array indicating the x-dimension locations of the turbines,
with length `N_turbines` (inherited from `FarmAeroTemplate`)
y_turbines : np.ndarray
a 1D numpy array indicating the y-dimension locations of the turbines,
with length `N_turbines` (inherited from `FarmAeroTemplate`)
yaw_turbines : np.ndarray
a numpy array indicating the yaw angle to drive each turbine to with
respect to the ambient wind direction, with length `N_turbines`
(inherited from `FarmAeroTemplate`)
Outputs
-------
AEP_farm : float
the AEP of the farm given by the analysis
power_farm : np.ndarray
an array of the farm power for each of the wind conditions that have
been queried
power_turbines : np.ndarray
an array of the farm power for each of the turbines in the farm across
all of the conditions that have been queried on the wind rose
(`N_turbines`, `N_wind_conditions`)
thrust_turbines : np.ndarray
an array of the wind turbine thrust for each of the turbines in the farm
across all of the conditions that have been queried on the wind rose
(`N_turbines`, `N_wind_conditions`)
"""
[docs]
def initialize(self):
"""Initialization of OM component."""
super().initialize()
self.options.declare("data_path", default=None)
[docs]
def setup(self):
"""Setup of OM component."""
super().setup()
data_path = str(self.options["data_path"])
self.wind_query = create_windresource_from_windIO(
self.windIO,
"probability",
)
if data_path is None:
data_path = ""
self.directions_wind = self.wind_query.wind_directions
self.speeds_wind = self.wind_query.wind_speeds
self.TIs_wind = self.wind_query.ti_table_flat
self.pmf_wind = self.wind_query.freq_table_flat
self.N_wind_conditions = len(self.pmf_wind)
# add the outputs we want for an AEP analysis:
# - AEP estimate
# - farm and turbine powers
# - turbine thrusts
self.add_output(
"AEP_farm",
0.0,
units="W*h",
)
self.add_output(
"power_farm",
np.zeros((self.N_wind_conditions,)),
units="W",
)
if self.options["modeling_options"]["aero"]["return_turbine_output"]:
self.add_output(
"power_turbines",
np.zeros((self.N_turbines, self.N_wind_conditions)),
units="W",
)
self.add_output(
"thrust_turbines",
np.zeros((self.N_turbines, self.N_wind_conditions)),
units="N",
)
# ... more outputs can be added here
[docs]
def setup_partials(self):
"""Derivative setup for OM component."""
# the default (but not preferred!) derivatives are FDM
self.declare_partials("*", "*", method="fd")
[docs]
def compute(self, inputs, outputs):
"""
Computation for the OM component.
For a template class this is not implemented and raises an error!
"""
#############################################
# #
# IMPLEMENT THE AERODYNAMICS COMPONENT HERE #
# #
#############################################
raise NotImplementedError(
"This is an abstract class for a derived class to implement!"
)
# the following should be set
outputs["AEP_farm"] = 0.0
outputs["power_farm"] = np.zeros((self.N_wind_conditions,))
outputs["power_turbines"] = np.zeros((self.N_turbines, self.N_wind_conditions))
outputs["thrust_turbines"] = np.zeros((self.N_turbines, self.N_wind_conditions))