Skip to content

Commit

Permalink
Merge 48af5ab into 3e58a6a
Browse files Browse the repository at this point in the history
  • Loading branch information
uvchik committed Sep 2, 2019
2 parents 3e58a6a + 48af5ab commit db6bbde
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 65 deletions.
2 changes: 1 addition & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = []

# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
Expand Down
6 changes: 3 additions & 3 deletions doc/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ The basic usage of the windpowerlib is shown in the ModelChain example that is a
To run the example you need the example weather and turbine data used:

* :download:`Example weather data file <../example/weather.csv>`
* :download:`Example power curve data file <../example/data/example_power_curves.csv>`
* :download:`Example power coefficient curve data file <../example/data/example_power_coefficient_curves.csv>`
* :download:`Example nominal power data file <../example/data/example_turbine_data.csv>`
* :download:`Example power curve data file <../example/data/power_curves.csv>`
* :download:`Example power coefficient curve data file <../example/data/power_coefficient_curves.csv>`
* :download:`Example nominal power data file <../example/data/turbine_data.csv>`

Furthermore, you have to install the windpowerlib and to run the notebook you also need to install `notebook` using pip3. To launch jupyter notebook type ``jupyter notebook`` in the terminal.
This will open a browser window. Navigate to the directory containing the notebook to open it. See the jupyter notebook quick start guide for more information on `how to install <http://jupyter-notebook-beginner-guide.readthedocs.io/en/latest/install.html>`_ and
Expand Down
16 changes: 14 additions & 2 deletions doc/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,26 @@ Wind turbine data
Functions and methods to obtain the nominal power as well as
power curve or power coefficient curve needed by the :py:class:`~.wind_turbine.WindTurbine` class.


.. autosummary::
:toctree: temp/

wind_turbine.get_turbine_data_from_file
wind_turbine.load_turbine_data_from_oedb
wind_turbine.get_turbine_types

.. _create_input_types_label:

Data Container
=====================

Create data container to be used as an input in classes und functions.

.. autosummary::
:toctree: temp/

wind_turbine.WindTurbineGroup
wind_turbine.WindTurbine.to_group

.. _wind_farm_label:

Wind farm calculations
Expand Down Expand Up @@ -234,4 +246,4 @@ TurbineClusterModelChain example
The ``turbine_cluster_modelchain_example`` consists of the following functions
as well as it uses functions of the ``modelchain_example``.

.. include:: example_2.rst
.. include:: example_2.rst
24 changes: 19 additions & 5 deletions tests/test_wind_farm.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ def test_initialization_dataframe(self):

def test_initialization_1(self):
"""test catching error when wind_turbine_fleet not provided as list"""
msg = 'Wind turbine fleet not provided properly.'
msg = 'Wind turbine must be provided as WindTurbine object'
with pytest.raises(ValueError, match=msg):
WindFarm(wind_turbine_fleet=[{'wind_turbine': 'turbine',
'number_of_turbines': 2}, 'dummy'])
WindFarm(wind_turbine_fleet={'wind_turbine': 'turbine',
'number_of_turbines': 2},
name='dummy')

def test_initialization_2(self):
"""test catching error when WindTurbine in wind_turbine_fleet
Expand All @@ -71,7 +72,7 @@ def test_initialization_3(self):
WindTurbine(**self.test_turbine_2)],
'number_of_turbines': [3, 2]})
msg = 'Missing wind_turbine key/column in wind_turbine_fleet'
with pytest.raises(ValueError, match=msg):
with pytest.raises(KeyError, match=msg):
WindFarm(wind_turbine_fleet=wind_turbine_fleet)

def test_initialization_4(self, recwarn):
Expand Down Expand Up @@ -144,4 +145,17 @@ def test_repr(self):
'number_of_turbines': 2}]
assert 'E-126/4200' in repr(WindFarm(wind_turbine_fleet=test_fleet))


def test_aggregation_of_power_curve_with_missing_power_curve(self):
"""Test WindFarm.assign_power_curve() with missing power_curve."""
wt1 = WindTurbine(**self.test_turbine)
wt1.power_curve = None
print(wt1)
wind_turbine_fleet = [
{'wind_turbine': wt1,
'number_of_turbines': 3},
{'wind_turbine': WindTurbine(**self.test_turbine_2),
'number_of_turbines': 2}]
windfarm = WindFarm(wind_turbine_fleet=wind_turbine_fleet)
msg = 'For an aggregated wind farm power curve each wind'
with pytest.raises(ValueError, match=msg):
windfarm.assign_power_curve()
34 changes: 31 additions & 3 deletions tests/test_wind_turbine.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@
from windpowerlib.tools import WindpowerlibUserWarning

from windpowerlib.wind_turbine import (get_turbine_data_from_file, WindTurbine,
get_turbine_types,
get_turbine_types, WindTurbineGroup,
load_turbine_data_from_oedb)


class TestWindTurbine:

@classmethod
def setup_class(cls):
"""Setup default values"""
cls.source = os.path.join(os.path.dirname(__file__), '../example/data')

def test_warning(self, recwarn):
source = os.path.join(os.path.dirname(__file__), '../example/data')
test_turbine_data = {'hub_height': 100,
'rotor_diameter': 80,
'turbine_type': 'turbine_not_in_file',
'path': source}
'path': self.source}
assert(WindTurbine(**test_turbine_data).power_curve is None)
assert recwarn.pop(WindpowerlibUserWarning)

Expand Down Expand Up @@ -50,3 +54,27 @@ def test_wrong_url_load_turbine_data(self):
@pytest.mark.filterwarnings("ignore:The WindTurbine")
def test_string_representation_of_wind_turbine(self):
assert "Wind turbine: ['hub height=120 m'" in repr(WindTurbine(120))

def test_to_group_method(self):
example_turbine = {
'hub_height': 100,
'rotor_diameter': 70,
'turbine_type': 'DUMMY 3',
'path': self.source}
e_t_1 = WindTurbine(**example_turbine)
assert(isinstance(e_t_1.to_group(), WindTurbineGroup))
assert(e_t_1.to_group(5).number_of_turbines == 5)
assert(e_t_1.to_group(number_turbines=5).number_of_turbines == 5)
assert(e_t_1.to_group(total_capacity=3e6).number_of_turbines == 2.0)

def test_wrongly_defined_to_group_method(self):
example_turbine = {
'hub_height': 100,
'rotor_diameter': 70,
'turbine_type': 'DUMMY 3',
'path': self.source}
e_t_1 = WindTurbine(**example_turbine)
with pytest.raises(ValueError,
match="The 'number' and the 'total_capacity"
" parameter are mutually exclusive."):
e_t_1.to_group(5, 3000)
53 changes: 24 additions & 29 deletions windpowerlib/wind_farm.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ class WindFarm(object):
Parameters
----------
wind_turbine_fleet : :pandas:`pandas.DataFrame<frame>` or list(dict)
Wind turbines of wind farm. DataFrame/Dictionaries must have
wind_turbine_fleet : :pandas:`pandas.DataFrame<frame>` or list()
Wind turbines of wind farm. DataFrame must have
'wind_turbine' containing a :class:`~.wind_turbine.WindTurbine` object
and either 'number_of_turbines' (number of wind turbines of the same
turbine type in the wind farm, can be a float) or 'total_capacity'
(installed capacity of wind turbines of the same turbine type in the
wind farm) as columns/keys. See example below.
A list of :class:`~windpowerlib.wind_turbine.WindTurbineGroup` objects.
A WindTurbineGroup can be created from a
:class:`~windpowerlib.wind_turbine.WindTurbine` using the
:func:`~windpowerlib.wind_turbine.WindTurbine.to_group` method.
efficiency : float or :pandas:`pandas.DataFrame<frame>` or None (optional)
Efficiency of the wind farm. Provide as either constant (float) or
power efficiency curve (pd.DataFrame) containing 'wind_speed' and
Expand All @@ -38,11 +43,11 @@ class WindFarm(object):
Attributes
----------
wind_turbine_fleet : list(dict)
Wind turbines of wind farm. Dictionaries must have 'wind_turbine'
wind_turbine_fleet : :pandas:`pandas.DataFrame<frame>`
Wind turbines of wind farm. DataFrame must have 'wind_turbine'
(contains a :class:`~.wind_turbine.WindTurbine` object) and
'number_of_turbines' (number of wind turbines of the same turbine type
in the wind farm) as keys.
in the wind farm) as columns.
efficiency : float or :pandas:`pandas.DataFrame<frame>` or None
Efficiency of the wind farm. Either constant (float) power efficiency
curve (pd.DataFrame) containing 'wind_speed' and 'efficiency'
Expand All @@ -53,9 +58,6 @@ class WindFarm(object):
hub_height : float
The calculated mean hub height of the wind farm. See
:py:func:`mean_hub_height` for more information.
nominal_power : float
The nominal power is the sum of the nominal power of all turbines in
the wind farm in W.
power_curve : :pandas:`pandas.DataFrame<frame>` or None
The calculated power curve of the wind farm. See
:py:func:`assign_power_curve` for more information.
Expand Down Expand Up @@ -93,7 +95,14 @@ class WindFarm(object):
>>> example_farm = wind_farm.WindFarm(**example_farm_data)
>>> print(example_farm.nominal_power)
31200000.0
>>> # turbine fleet as a list of WindTurbineGroup namedtuples using the
>>> # 'to_group' method.
>>> wind_turbine_fleet = [e126.to_group(number_turbines=5),
... e126.to_group(),
... v90.to_group(total_capacity=3 * 2e6)]
>>> example_farm = wind_farm.WindFarm(wind_turbine_fleet)
>>> print(example_farm.nominal_power)
31200000.0
"""

def __init__(self, wind_turbine_fleet, efficiency=None, name='', **kwargs):
Expand All @@ -120,10 +129,7 @@ def check_and_complete_wind_turbine_fleet(self):
"""
# convert list to dataframe if necessary
if isinstance(self.wind_turbine_fleet, list):
try:
self.wind_turbine_fleet = pd.DataFrame(self.wind_turbine_fleet)
except:
raise ValueError("Wind turbine fleet not provided properly.")
self.wind_turbine_fleet = pd.DataFrame(self.wind_turbine_fleet)

# check wind turbines
try:
Expand All @@ -133,8 +139,8 @@ def check_and_complete_wind_turbine_fleet(self):
'Wind turbine must be provided as WindTurbine object '
'but was provided as {}.'.format(type(turbine)))
except KeyError:
raise ValueError('Missing wind_turbine key/column in '
'wind_turbine_fleet parameter.')
raise KeyError('Missing wind_turbine key/column in '
'wind_turbine_fleet parameter.')

# add columns for number of turbines and total capacity if they don't
# yet exist
Expand All @@ -159,7 +165,7 @@ def check_and_complete_wind_turbine_fleet(self):
else:
self.wind_turbine_fleet.loc[ix, 'number_of_turbines'] = \
number_of_turbines
except:
except TypeError:
raise ValueError(msg.format(row['wind_turbine']))

# calculate total capacity if necessary and check that total capacity
Expand All @@ -170,7 +176,7 @@ def check_and_complete_wind_turbine_fleet(self):
self.wind_turbine_fleet.loc[ix, 'total_capacity'] = \
row['number_of_turbines'] * \
row['wind_turbine'].nominal_power
except:
except TypeError:
raise ValueError(
'Total capacity of turbines of type {turbine} cannot '
'be deduced. Please check if the nominal power of the '
Expand Down Expand Up @@ -201,14 +207,7 @@ def __repr__(self):
@property
def nominal_power(self):
r"""
The nominal power of the wind farm.
See :attr:`~.wind_farm.WindFarm.nominal_power` for further information.
Parameters
-----------
nominal_power : float
Nominal power of the wind farm in W.
The nominal power is the sum of the nominal power of all turbines.
Returns
-------
Expand Down Expand Up @@ -308,10 +307,6 @@ def assign_power_curve(self, wake_losses_model='wind_farm_efficiency',
Turbulence intensity at hub height of the wind farm for power curve
smoothing with 'turbulence_intensity' method. Can be calculated
from `roughness_length` instead. Default: None.
roughness_length : float (optional)
Roughness length. If `standard_deviation_method` is
'turbulence_intensity' and `turbulence_intensity` is not given
the turbulence intensity is calculated via the roughness length.
Returns
-------
Expand Down
90 changes: 89 additions & 1 deletion windpowerlib/wind_turbine.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import requests
import os
from windpowerlib.tools import WindpowerlibUserWarning
from typing import NamedTuple


class WindTurbine(object):
Expand Down Expand Up @@ -113,7 +114,6 @@ class WindTurbine(object):
18000.0
>>> print(e_t_1.nominal_power)
1500000.0
"""

def __init__(self, hub_height, nominal_power=None, path='oedb',
Expand Down Expand Up @@ -198,6 +198,94 @@ def __repr__(self):

return turbine_repr

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'.
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.
total_capacity : float
Total capacity of the group of wind turbines of the same type.
Returns
-------
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
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__ = ':class:`~windpowerlib.wind_farm.WindFarm`'
WindTurbineGroup.number_of_turbines.__doc__ = (
'Number of turbines of type WindTurbine')


def get_turbine_data_from_file(turbine_type, path):
r"""
Expand Down
Loading

0 comments on commit db6bbde

Please sign in to comment.