"""
The ``wind_turbine`` module contains the class WindTurbine that implements
a wind turbine in the windpowerlib and functions needed for the modelling of a
wind turbine.
SPDX-FileCopyrightText: 2019 oemof developer group <contact@oemof.org>
SPDX-License-Identifier: MIT
"""
import pandas as pd
import logging
import warnings
import os
from windpowerlib.tools import WindpowerlibUserWarning
from typing import NamedTuple
[docs]
class WindTurbine(object):
r"""
Defines a standard set of wind turbine attributes.
Parameters
----------
hub_height : float
Hub height of the wind turbine in m.
power_curve : :pandas:`pandas.DataFrame<frame>` or dict (optional)
If provided directly sets the power curve. DataFrame/dictionary must
have 'wind_speed' and 'value' columns/keys with wind speeds in m/s and
the corresponding power curve value in W. If not set the value is
retrieved from 'power_curve.csv' file in `path`. In that case a
`turbine_type` is needed. Default: None.
power_coefficient_curve : :pandas:`pandas.DataFrame<frame>` or dict (optional)
If provided directly sets the power coefficient curve.
DataFrame/dictionary must have 'wind_speed' and 'value' columns/keys
with wind speeds in m/s and the corresponding power coefficient curve
value. If not set the value is retrieved from
'power_coefficient_curve.csv' file in `path`. In that case a
`turbine_type` is needed. Default: None.
turbine_type : str (optional)
Name of the wind turbine type. Must be provided if power (coefficient)
curve, nominal power or rotor diameter is retrieved from self-provided
or oedb turbine library csv files. If turbine_type is None it is not
possible to retrieve turbine data from file.
Use :py:func:`~.get_turbine_types` to see a table of all wind turbines
for which power (coefficient) curve data and other turbine data is
provided in the oedb turbine library.
Default: None.
rotor_diameter : float (optional)
Diameter of the rotor in m. If not set the value is
retrieved from 'turbine_data.csv' file in `path`. In that case a
`turbine_type` is needed.
The rotor diameter only needs to be set if power output
is calculated using the power coefficient curve. Default: None.
nominal_power : float (optional)
The nominal power of the wind turbine in W. If not set the value is
retrieved from 'turbine_data.csv' file in `path`. In that case a
`turbine_type` is needed. Default: None.
path : str (optional)
Directory where the turbine database files are located. The files need
to be named 'power_coefficient_curve.csv', 'power_curve.csv', and
'turbine_data.csv'. By default the oedb turbine library files are used.
Set path to `None` to ignore turbine data from files. Default: 'oedb'.
Attributes
----------
turbine_type : str
Name of the wind turbine.
hub_height : float
Hub height of the wind turbine in m.
rotor_diameter : None or float
Diameter of the rotor in m. Default: None.
power_coefficient_curve : None, pandas.DataFrame or dictionary
Power coefficient curve of the wind turbine. DataFrame/dictionary
containing 'wind_speed' and 'value' columns/keys with wind speeds
in m/s and the corresponding power coefficients. Default: None.
power_curve : None, pandas.DataFrame or dictionary
Power curve of the wind turbine. DataFrame/dictionary containing
'wind_speed' and 'value' columns/keys with wind speeds in m/s and the
corresponding power curve value in W. Default: None.
nominal_power : None or float
The nominal output of the wind turbine in W. Default: None.
Notes
------
Your wind turbine object needs to have a power coefficient or power curve.
By default they are fetched from the oedb turbine library that is provided
along with the windpowerlib. In that case `turbine_type` must be specified.
You can also set the curves directly or provide your own csv files with
power coefficient and power curves. See `example_power_curves.csv',
`example_power_coefficient_curves.csv` and `example_turbine_data.csv`
in example/data for the required format of such csv files.
Examples
--------
>>> import os
>>> from windpowerlib import WindTurbine
>>> enerconE126={
... 'hub_height': 135,
... 'turbine_type': 'E-126/4200'}
>>> e126=WindTurbine(**enerconE126)
>>> print(e126.nominal_power)
4200000.0
>>> # Example with own path
>>> path=os.path.join(os.path.dirname(__file__), '../tests/data')
>>> example_turbine={
... 'hub_height': 100,
... 'rotor_diameter': 70,
... 'turbine_type': 'DUMMY 3',
... 'path' : path}
>>> e_t_1=WindTurbine(**example_turbine)
>>> print(e_t_1.power_curve['value'][7])
18000.0
>>> print(e_t_1.nominal_power)
1500000.0
"""
[docs]
def __init__(
self,
hub_height,
nominal_power=None,
path="oedb",
power_curve=None,
power_coefficient_curve=None,
rotor_diameter=None,
turbine_type=None,
**kwargs,
):
self.hub_height = hub_height
self.turbine_type = turbine_type
self.rotor_diameter = rotor_diameter
self.nominal_power = nominal_power
self.power_curve = power_curve
self.power_coefficient_curve = power_coefficient_curve
if path == "oedb":
path = os.path.join(os.path.dirname(__file__), "oedb")
if turbine_type is not None and path is not None:
if power_curve is None:
try:
fn = os.path.join(path, "power_curves.csv")
self.power_curve = get_turbine_data_from_file(
self.turbine_type, fn
)
except KeyError:
msg = "No power curve found for {0}"
logging.debug(msg.format(self.turbine_type))
if power_coefficient_curve is None:
try:
fn = os.path.join(path, "power_coefficient_curves.csv")
self.power_coefficient_curve = get_turbine_data_from_file(
self.turbine_type, fn
)
except KeyError:
msg = "No power coefficient curve found for {0}"
logging.debug(msg.format(self.turbine_type))
if nominal_power is None or (
rotor_diameter is None
and self.power_coefficient_curve is not None
):
turbine_data = None
try:
fn = os.path.join(path, "turbine_data.csv")
turbine_data = get_turbine_data_from_file(
self.turbine_type, fn
)
except KeyError:
msg = "No turbine data found for {0}"
logging.debug(msg.format(self.turbine_type))
if self.nominal_power is None and turbine_data is not None:
self.nominal_power = float(turbine_data["nominal_power"].iloc[0])
if self.rotor_diameter is None and turbine_data is not None:
self.rotor_diameter = float(turbine_data["rotor_diameter"].iloc[0])
if self.rotor_diameter:
if self.hub_height <= 0.5 * self.rotor_diameter:
msg = "1/2rotor_diameter cannot be greater than hub_height"
raise ValueError(msg)
if self.power_curve is None and self.power_coefficient_curve is None:
msg = (
"The WindTurbine has been initialised without a power curve"
" and without a power coefficient curve.\nYou will not be"
" able to calculate the power output.\n"
" Check if the turbine type {0} is in your database file"
" or if you passed a valid curve."
)
warnings.warn(msg.format(turbine_type), WindpowerlibUserWarning)
else:
# power (coefficient) curve to pd.DataFrame in case of being dict
if isinstance(self.power_curve, dict):
self.power_curve = pd.DataFrame(self.power_curve)
if isinstance(self.power_coefficient_curve, dict):
self.power_coefficient_curve = pd.DataFrame(
self.power_coefficient_curve
)
# sort power (coefficient) curve by wind speed
if isinstance(self.power_curve, pd.DataFrame):
self.power_curve.sort_values(by="wind_speed")
elif self.power_curve is not None:
msg = (
"Type of power curve of {} is {} but should be "
"pd.DataFrame or dict."
)
raise TypeError(
msg.format(self.__repr__(), type(self.power_curve))
)
if isinstance(self.power_coefficient_curve, pd.DataFrame):
self.power_coefficient_curve.sort_values(by="wind_speed")
elif self.power_coefficient_curve is not None:
msg = (
"Type of power coefficient curve of {} is {} but "
"should be pd.DataFrame or dict."
)
raise TypeError(
msg.format(
self.__repr__(), type(self.power_coefficient_curve)
)
)
def __repr__(self):
info = []
if self.nominal_power is not None:
info.append("nominal power={} W".format(self.nominal_power))
if self.hub_height is not None:
info.append("hub height={} m".format(self.hub_height))
if self.rotor_diameter is not None:
info.append("rotor diameter={} m".format(self.rotor_diameter))
if self.power_coefficient_curve is not None:
info.append("power_coefficient_curve={}".format("True"))
else:
info.append("power_coefficient_curve={}".format("False"))
if self.power_curve is not None:
info.append("power_curve={}".format("True"))
else:
info.append("power_curve={}".format("False"))
if self.turbine_type is not None:
turbine_repr = "Wind turbine: {name} {info}".format(
name=self.turbine_type, info=info
)
else:
turbine_repr = "Wind turbine: {info}".format(info=info)
return turbine_repr
[docs]
def to_group(self, number_turbines=None, total_capacity=None):
r"""
Creates a :class:`~windpowerlib.wind_turbine.WindTurbineGroup`, a
NamedTuple data container with the fields 'number_of_turbines' and
'wind_turbine'. If no parameter is passed the number of turbines is
set to one.
It can be used to calculate the number of turbines for a given total
capacity or to create a namedtuple that can be used to define a
:class:`~windpowerlib.wind_farm.WindFarm` object.
Parameters
----------
number_turbines : float
Number of turbines of the defined type. Default: 1
total_capacity : float
Total capacity of the group of wind turbines of the same type.
Returns
-------
:class:`~windpowerlib.wind_turbine.WindTurbineGroup`
A namedtuple with two fields: 'number_of_turbines' and
'wind_turbine'.
Examples
--------
>>> from windpowerlib import WindTurbine
>>> enerconE126={
... 'hub_height': 135,
... 'turbine_type': 'E-126/4200'}
>>> e126=WindTurbine(**enerconE126)
>>> e126.to_group(5).number_of_turbines
5
>>> e126.to_group().number_of_turbines
1
>>> e126.to_group(number_turbines=7).number_of_turbines
7
>>> e126.to_group(total_capacity=12600000).number_of_turbines
3.0
>>> e126.to_group(total_capacity=14700000).number_of_turbines
3.5
>>> e126.to_group(total_capacity=12600000).wind_turbine.nominal_power
4200000.0
>>> type(e126.to_group(5))
<class 'windpowerlib.wind_turbine.WindTurbineGroup'>
>>> e126.to_group(5) # doctest: +NORMALIZE_WHITESPACE
WindTurbineGroup(wind_turbine=Wind turbine: E-126/4200 ['nominal
power=4200000.0 W', 'hub height=135 m', 'rotor diameter=127.0 m',
'power_coefficient_curve=True', 'power_curve=True'],
number_of_turbines=5)
"""
if number_turbines is not None and total_capacity is not None:
raise ValueError(
"The 'number' and the 'total_capacity' parameter "
"are mutually exclusive. Use just one of them."
)
elif total_capacity is not None:
number_turbines = total_capacity / self.nominal_power
elif number_turbines is None:
number_turbines = 1
return WindTurbineGroup(
wind_turbine=self, number_of_turbines=number_turbines
)
# This is working for Python >= 3.5.
# There a cleaner solutions for Python >= 3.6, once the support of 3.5 is
# dropped: https://stackoverflow.com/a/50038614
[docs]
class WindTurbineGroup(
NamedTuple(
"WindTurbineGroup",
[("wind_turbine", WindTurbine), ("number_of_turbines", float)],
)
):
"""
A simple data container to define more than one turbine of the same type.
Use the :func:`~windpowerlib.wind_turbine.WindTurbine.to_group` method to
easily create a WindTurbineGroup from a
:class:`~windpowerlib.wind_turbine.WindTurbine` object.
Parameters
----------
'wind_turbine' : WindTurbine
A WindTurbine object with all necessary attributes.
'number_of_turbines' : float
The number of turbines. The number is not restricted to integer values.
"""
__slots__ = ()
WindTurbineGroup.wind_turbine.__doc__ = (
"A :class:`~windpowerlib.wind_farm.WindTurbine` object."
)
WindTurbineGroup.number_of_turbines.__doc__ = (
"Number of turbines of type WindTurbine"
)
[docs]
def get_turbine_data_from_file(turbine_type, path):
r"""
Fetches turbine data from a csv file.
See `example_power_curves.csv', `example_power_coefficient_curves.csv` and
`example_turbine_data.csv` in example/data for the required format of
a csv file. Make sure to provide wind speeds in m/s and power in W or
convert units after loading the data.
Parameters
----------
turbine_type : str
Specifies the turbine type data is fetched for.
path : str
Specifies the source of the turbine data.
See the example below for how to use the example data.
Returns
-------
:pandas:`pandas.DataFrame<frame>` or float
Power curve or power coefficient curve (pandas.DataFrame) or nominal
power (float) of one wind turbine type. Power (coefficient) curve
DataFrame contains power coefficient curve values (dimensionless) or
power curve values (in dimension given in file) with the corresponding
wind speeds (in dimension given in file).
Examples
--------
>>> from windpowerlib import wind_turbine
>>> import os
>>> my_path = os.path.join(os.path.dirname(__file__), '../tests/data',
... 'power_curves.csv')
>>> d3 = get_turbine_data_from_file('DUMMY 3', my_path)
>>> print(d3['value'][7])
18000.0
>>> print(d3['value'].max())
1500000.0
"""
try:
df = pd.read_csv(path, index_col=0)
except FileNotFoundError:
raise FileNotFoundError("The file '{}' was not found.".format(path))
wpp_df = df[df.index == turbine_type].copy()
# if turbine not in data file
if wpp_df.shape[0] == 0:
msg = "Wind converter type {0} not provided. Possible types: {1}"
raise KeyError(msg.format(turbine_type, list(df.index)))
# if turbine in data file
# get nominal power or power (coefficient) curve
if "turbine_data" in path:
return wpp_df
else:
wpp_df.dropna(axis=1, inplace=True)
wpp_df = wpp_df.transpose().reset_index()
wpp_df.columns = ["wind_speed", "value"]
# transform wind speeds to floats
wpp_df["wind_speed"] = wpp_df["wind_speed"].apply(lambda x: float(x))
return wpp_df
def get_turbine_types(turbine_library="local", print_out=True, filter_=True):
print(turbine_library, print_out, filter_)
msg = (
"\nUse >>from windpowerlib import get_turbine_types<< not"
">>from windpowerlib.wind_turbine import get_turbine_types<<."
)
raise ImportError(msg)