Wind Data Objects#

FLORIS v4 introduces WindData objects. These include TimeSeries, WindRose, and WindTIRose. These objects are used to hold inputs to FLORIS simulations, such as the ambient wind data, and to provide high-level methods for working with wind data. This notebook provides an overview of the WindData objects and demonstrates how to use them.

WindDataBase#

WindDataBase is the base class for all WindData objects. It provides a common interface for working with wind data. The WindDataBase class is not intended to be used directly, but rather to be subclassed by more specific wind data objects. It is only important to mention that many of the methods in FLORIS that accept wind data as input will accept any WindDataBase object as input. But is not typical to use it directly.

from floris.wind_data import WindDataBase
import matplotlib.pyplot as plt

TimeSeries#

TimeSeries objects are used to represent data which are in a time-series form, or more generally and data which is represented as a list of conditions without frequency weighting (i.e. not a wind rose). In addition to representing time series input conditions, TimeSeries objects are useful for generating sweep inputs where most values are held constant while one input is swept through a range of values. Also useful can be an input of identical repeated inputs which can be useful if some control setting is going to be swept. TimeSeries represents data most similarly to how data structures within FLORIS are represented in that there are N wind_directions, wind_speeds etc., in the TimeSeries, the n_findex value in FLORIS will be N.

TimeSeries Instantiation#

from floris import TimeSeries
import numpy as np

# Like FlorisModel, TimeSeries require wind directions, wind speeds, and turbulence intensities to be of the same length.
N = 50
wind_speeds = np.linspace(3, 15, N)
wind_directions = 270.0 * np.ones(N)
turbulence_intensities = 0.06 * np.ones(N)

# Create a TimeSeries object
time_series = TimeSeries(wind_directions=wind_directions, wind_speeds=wind_speeds, turbulence_intensities=turbulence_intensities)

Broadcasting#

Unlike FlorisModel, TimeSeries objects do allow broadcasting. As long as one of the inputs is a numpy array, the other inputs can be specified as a float, which will be broadcasted to the length of the numpy array.

# Equivalent to the above
time_series = TimeSeries(wind_directions=270.0, wind_speeds=wind_speeds, turbulence_intensities=0.06)

Value#

In addition to wind directions, wind speeds, and turbulence intensities, TimeSeries objects can also hold an array of values. These values can be used for example to represent electricity market prices (e.g., price/MWh). The values are intended to be multiplied by the corresponding wind plant power at each time step or wind condition to determine the total value produced over all conditions.

If values are included in the TimeSeries object, they must be the same length as the wind directions, wind speeds, and turbulence intensities. If included, values enable calculation of Annual Value Production (AVP), in addition to AEP, and certain optimization routines, such as layout, can be configured to maximize value instead of energy production.

# Including value for each indices
time_series = TimeSeries(wind_directions=wind_directions, wind_speeds=wind_speeds, turbulence_intensities=turbulence_intensities, values=np.linspace(0, 1, N))

Generating Turbulence Intensity#

The TimeSeries object also includes functions for generating TI as a function of wind direction and wind speed. This can be accomplished by passing in a custom function, or by taking use of the IEC 61400-1 standard

# Assign TI as a function of wind speed using the IEC method and default parameters.
time_series.assign_ti_using_IEC_method()

fig, ax = plt.subplots()
ax.scatter(time_series.wind_speeds, time_series.turbulence_intensities)
ax.set_xlabel('Wind Speed (m/s)')
ax.set_ylabel('Turbulence Intensity')
Text(0, 0.5, 'Turbulence Intensity')
_images/e5a73da782caf6eced4d4740a5d50b2ea7d665e91f2970af0aaefdedf7e34efe.png

Generating Value#

The TimeSeries object also includes functions for generating value as a function of wind direction and wind speed. This can be accomplished by passing in a custom function using the TimeSeries.assign_value_using_wd_ws_function method, or by using the TimeSeries.assign_value_piecewise_linear method, which approximates value using a two-segment piecewise linear function of wind speed. When using the default parameters, this produces a value vs. wind speed that approximates the normalized mean electricity price vs. wind speed curve for the SPP market in the U.S. for years 2018-2020 from figure 7 in "The value of wake steering wind farm flow control in US energy markets," Wind Energy Science, 2024. https://doi.org/10.5194/wes-9-219-2024.

# Assign value as a function of wind speed using the piecewise linear method and default parameters.
time_series.assign_value_piecewise_linear()

fig, ax = plt.subplots()
ax.scatter(time_series.wind_speeds, time_series.values)
ax.grid()
ax.set_xlabel('Wind Speed (m/s)')
ax.set_ylabel('Value (normalized price/MWh)')
Text(0, 0.5, 'Value (normalized price/MWh)')
_images/36591e5fae7e024182c5831585858c4320caf3e7ad4f9b816be34fe5b1ab58fe.png

WindRose#

A second wind data object is the WindRose, which represents the data as:

  • An array of wind directions

  • An array of wind speeds

  • A table of turbulence intensities of size (n_wind_directions, n_wind_speeds) which represents the TI at each wind direction and wind speed.

  • A table of frequencies of size (n_wind_directions, n_wind_speeds) which represents the frequency of occurance of each wind direction and wind speed.

  • An (optional) table of values of size (n_wind_directions, n_wind_speeds) which represents the value of the wind condition.

from floris import WindRose

wind_directions = np.array([270, 280]) # 2 Wind Directions
wind_speeds = np.array([6.0, 7.0, 8.0]) # 3 Wind Speeds

# Create a WindRose object, not indicating a frequency table indicates uniform frequency
wind_rose = WindRose(
    wind_directions=wind_directions,
    wind_speeds=wind_speeds,
    ti_table=0.06 #As in Time Series, a float indicates a constant table
)

wind_rose.freq_table
array([[0.16666667, 0.16666667, 0.16666667],
       [0.16666667, 0.16666667, 0.16666667]])
# Several of the functions implemented for TimeSeries are likewise implemented for WindRose

wind_rose.assign_ti_using_IEC_method()

print("wind_rose.ti_table")
print(wind_rose.ti_table)

wind_rose.assign_value_piecewise_linear()

print("\nwind_rose.value_table")
print(wind_rose.value_table)
wind_rose.ti_table
[[0.09683333 0.0905     0.08575   ]
 [0.09683333 0.0905     0.08575   ]]

wind_rose.value_table
[[1.2225 1.0875 0.9525]
 [1.2225 1.0875 0.9525]]

WindTIRose#

The WindTIRose is similar to the WindRose except that rather than specififying wind directions and wind speeds as arrays, with TI, frequency, adn value as 2D tables, the WindTIRose specificies wind directions, wind speeds, and turbulence intensities as arrays with the frequency and value tables now 3 dimensional, representing the frequency and value of each wind direction, wind speed, and turbulence intensity occurence.

from floris import WindTIRose

wind_directions = np.array([270, 280]) # 2 Wind Directions
wind_speeds = np.array([6.0, 7.0, 8.0]) # 3 Wind Speeds
turbulence_intensities = np.array([0.06, 0.07, 0.08]) # 3 Turbulence Intensities

# The frequency table therefore is 2 x 3 x 3 and the sum over all entries = 1
freq_table = np.array([
    [[2/18, 0, 1/18], [1/18, 1/18, 1/18], [1/18, 1/18, 1/18]],
    [[1/18, 1/18, 1/18], [1/18, 1/18, 1/18], [1/18, 1/18, 1/18]]
])

# The value table has the same dimensions as frequency
value_table = np.ones_like(freq_table)

wind_ti_rose = WindTIRose(
    wind_directions=wind_directions,
    wind_speeds=wind_speeds,
    turbulence_intensities=turbulence_intensities,
    freq_table=freq_table,
    value_table=value_table
)

# Demonstrate setting value again
wind_ti_rose.assign_value_piecewise_linear()

Conversions#

Several methods for converting between WindData objects and resampling to different bin sizes are provided

# Converting from TimeSeries to WindRose/WindTiRose by binning
wind_rose = time_series.to_WindRose(wd_step=2, ws_step=1)
wind_ti_rose = time_series.to_WindTIRose(wd_step=2, ws_step=1, ti_step=0.01)

Aggregating and Resampling WindRose#

# Generate a wind rose with a few wind directions and speeds
wind_directions=np.array([260,265,270, 275, 280, 285, 290])
wind_speeds=np.array([6.0, 7.0, 8.0, 9.0])
freq_table = np.random.rand(7, 4)
freq_table /= freq_table.sum()

wind_rose = WindRose(
    wind_directions=wind_directions,
    wind_speeds=wind_speeds,
    ti_table=0.06,
    freq_table=freq_table
)

wind_rose.plot()
<PolarAxes: >
_images/0f2a5cf08185451c16250f9dffe3fff9b55afacfe3c62cb5396740543a3f8f27.png
# The aggregate functions of  WindRose/WindTiRose allows for 
# aggregating the data into larger bin sizes 
wind_rose_aggregated = wind_rose.aggregate(wd_step=10, ws_step=2)
wind_rose_aggregated.plot()
<PolarAxes: >
_images/6cf02c9d6f46d37a5d3e2224e92e20839a63bcf3227ba96b7d071388d5381ef2.png
# For upsampling, the resample_by_interpolation method is available which can
# interpolate the data via linear or nearest-neighbor interpolation
wind_rose_resampled = wind_rose.resample_by_interpolation(wd_step=2.5, ws_step=0.5)
wind_rose_resampled.plot()
<PolarAxes: >
_images/d78c89098e5b564711c4006c26357771f9141e80eba8b37b737c22b509b8b410.png

Plotting#

There are several plotting methods available to help visualize wind data objects

wind_directions=np.arange(0, 360, 10)
wind_speeds=np.arange(0.0, 30.0, 5.0)
freq_table=np.random.rand(36, 6)
freq_table = freq_table / freq_table.sum()

wind_rose = WindRose(
    wind_directions=wind_directions,
    wind_speeds=wind_speeds,
    ti_table=0.06,
    freq_table=freq_table
)

# Set value
wind_rose.assign_value_piecewise_linear()

wind_rose.plot()

# Plot with aggregated wind directions
wind_rose.plot(wd_step=30)

wind_rose.plot_ti_over_ws()

wind_rose.plot_value_over_ws()
_images/c28fdeb600980a2eb6143111eb14e9518530dddd3a5f18933e53b059079c8cb5.png _images/401081fed82a9b204f953b44b44d754d6c8a3a1bddc7ffb5778d5845f3eb58f4.png _images/dd91cab9ca0182579fbf3ebdc6aacf23f098e290d264dcf2200e8232b3f4d2d8.png _images/765251d3e3de313717f2cf945d801aa1541f84d6d1ef85438531d4284a4f86e2.png

Setting FLORIS#

WindData objects are used to set wind direction, speed, TI, frequency, and value in a FlorisModel (or UncertainFlorisModel).

TimeSeries#

# TimeSeries

from floris import FlorisModel

# Create a FlorisModel object
fmodel = FlorisModel("../examples/inputs/gch.yaml")

# Set a two-turbine layout
fmodel.set(layout_x=[0, 500], layout_y=[0, 0])

# Make a set of inputs with 5 wind directions, while wind speed and TI are constant
wind_directions = np.array([270, 280, 290, 300, 310])
wind_speeds = 8.0 * np.ones(5)
turbulence_intensities = 0.06 * np.ones(5)

fmodel.set(
    wind_directions=wind_directions,
    wind_speeds=wind_speeds,
    turbulence_intensities=turbulence_intensities
)

# Is equivalent to the following (but now we'll include value as well):
time_series = TimeSeries(wind_directions=wind_directions, wind_speeds=8.0, turbulence_intensities=0.06)

# Scale some of the default parameters to get reasonable values representing USD/MWh
time_series.assign_value_piecewise_linear(value_zero_ws=25*1.425, slope_2=-25*0.135)

fmodel.set(wind_data = time_series)
# Run the model and get outputs
fmodel.run()

# Get the power outputs
turbine_powers = fmodel.get_turbine_powers()
farm_power = fmodel.get_farm_power()
expected_farm_power = fmodel.get_expected_farm_power()
aep = fmodel.get_farm_AEP()

# Get value outputs
expected_farm_value = fmodel.get_expected_farm_value()
avp = fmodel.get_farm_AVP()

# Display
print(f"Turbine power have shape {turbine_powers.shape} and are {turbine_powers}")
print(f"Farm power has shape {farm_power.shape} and is {farm_power}")
print(f"Expected farm power has shape {expected_farm_power.shape} and is {expected_farm_power}")
print(f"Farm AEP is {aep/1e9} GWh")
print(f"Expected farm value has shape {expected_farm_power.shape} and is {expected_farm_value}")
print(f"Farm annual value production (AVP) is {avp/1e6} USD")
floris.floris_model.FlorisModel WARNING Computing AEP with uniform frequencies. Results results may not reflect annual operation.
floris.floris_model.FlorisModel WARNING Computing AVP with uniform frequencies. Results results may not reflect annual operation.
Turbine power have shape (5, 2) and are [[1753954.45917917  354990.76412771]
 [1753954.45917917 1320346.28513924]
 [1753954.45917917 1748551.48278202]
 [1753954.45917917 1753951.95262087]
 [1753954.45917917 1753954.45908051]]
Farm power has shape (5,) and is [2108945.22330688 3074300.74431841 3502505.94196119 3507906.41180004
 3507908.91825968]
Expected farm power has shape () and is 3140313.4479292436
Farm AEP is 27.50914580386017 GWh
Expected farm value has shape () and is 74778713.97881511
Farm annual value production (AVP) is 655061.5344544204 USD

WindRose#

WindRose objects set FLORIS as TimeSeries, but there are some additional considerations.

  • By default, wind direction/speed combinations with 0 frequency are not run

  • The outputs of the functions get_turbine_powers and get_farm_power will be reshaped to have dimensions num_wind_directions x num_wind_speeds ( x num_turbines)

wind_directions = np.array([270, 280]) # 2 Wind Directions
wind_speeds = np.array([6.0, 7.0, 8.0]) # 3 Wind Speeds

# Frequency matrix is 2 x 3, include some 0 frequency results
freq_table = np.array([
    [0, 0, 1/2],
    [1/6, 1/6, 1/6]
])

# Create a WindRose object, not indicating a frequency table indicates uniform frequency
wind_rose = WindRose(
    wind_directions=wind_directions,
    wind_speeds=wind_speeds,
    ti_table=0.06,
    freq_table=freq_table
)

# Set value and scale some of the default parameters to get reasonable values representing USD/MWh
wind_rose.assign_value_piecewise_linear(value_zero_ws=25*1.425, slope_2=-25*0.135)

fmodel.set(wind_data=wind_rose)

print(f"Fmodel has n_findex {fmodel.core.flow_field.n_findex} because two cases have 0 frequency")
Fmodel has n_findex 4 because two cases have 0 frequency
# Run the model and collect the outputs
fmodel.run()

# Get the power outputs
turbine_powers = fmodel.get_turbine_powers()
farm_power = fmodel.get_farm_power()
expected_farm_power = fmodel.get_expected_farm_power()
aep = fmodel.get_farm_AEP()

# Get value outputs
expected_farm_value = fmodel.get_expected_farm_value()
avp = fmodel.get_farm_AVP()

# Note that the nan values in the non-computed cases are expected since these are not run

# Display
print(f"Turbine power have shape {turbine_powers.shape} and are {turbine_powers}")
print(f"Farm power has shape {farm_power.shape} and is {farm_power}")
print(f"Expected farm power has shape {expected_farm_power.shape} and is {expected_farm_power}")
print(f"Farm AEP is {aep/1e9} GWh")
print(f"Expected farm value has shape {expected_farm_power.shape} and is {expected_farm_value}")
print(f"Farm annual value production (AVP) is {avp/1e6} USD")
Turbine power have shape (2, 3, 2) and are [[[             nan              nan]
  [             nan              nan]
  [1753954.45917917  354990.76412771]]

 [[ 731003.41073165  523849.55426108]
  [1176825.66812027  876937.12082426]
  [1753954.45917917 1320346.28513924]]]
Farm power has shape (2, 3) and is [[             nan              nan 2108945.22330688]
 [1254852.96499273 2053762.78894454 3074300.74431841]]
Expected farm power has shape () and is 2118292.028029389
Farm AEP is 18.556238165537447 GWh
Expected farm value has shape () and is 53008780.07184796
Farm annual value production (AVP) is 464356.91342938814 USD
# It's possible however to force the running of 0 frequency cases
wind_rose = WindRose(
    wind_directions=wind_directions,
    wind_speeds=wind_speeds,
    ti_table=0.06,
    freq_table=freq_table,
    compute_zero_freq_occurrence=True
)

# Set value and scale some of the default parameters to get reasonable values representing USD/MWh
wind_rose.assign_value_piecewise_linear(value_zero_ws=25*1.425, slope_2=-25*0.135)

fmodel.set(wind_data=wind_rose)

print(f"Fmodel has n_findex {fmodel.core.flow_field.n_findex}")

# Run the model and collect the outputs
fmodel.run()

# Get the power outputs
turbine_powers = fmodel.get_turbine_powers()
farm_power = fmodel.get_farm_power()
expected_farm_power = fmodel.get_expected_farm_power()
aep = fmodel.get_farm_AEP()

# Get value outputs
expected_farm_value = fmodel.get_expected_farm_value()
avp = fmodel.get_farm_AVP()

# Display
print("Turbine powers and farm power are now computed for all cases")
print(f"Turbine power have shape {turbine_powers.shape} and are {turbine_powers}")
print(f"Farm power has shape {farm_power.shape} and is {farm_power}")

print("Expected farm power and value, AEP, and AVP are the same as before since the new cases are weighted by 0")
print(f"Expected farm power has shape {expected_farm_power.shape} and is {expected_farm_power}")
print(f"Farm AEP is {aep/1e9} GWh")
print(f"Expected farm value has shape {expected_farm_power.shape} and is {expected_farm_value}")
print(f"Farm annual value production (AVP) is {avp/1e6} USD")
Fmodel has n_findex 6
Turbine powers and farm power are now computed for all cases
Turbine power have shape (2, 3, 2) and are [[[ 731003.41073165   80999.08780495]
  [1176825.66812027  191637.98384374]
  [1753954.45917917  354990.76412771]]

 [[ 731003.41073165  523849.55426108]
  [1176825.66812027  876937.12082426]
  [1753954.45917917 1320346.28513924]]]
Farm power has shape (2, 3) and is [[ 812002.4985366  1368463.65196401 2108945.22330688]
 [1254852.96499273 2053762.78894454 3074300.74431841]]
Expected farm power and value, AEP, and AVP are the same as before since the new cases are weighted by 0
Expected farm power has shape () and is 2118292.028029389
Farm AEP is 18.556238165537447 GWh
Expected farm value has shape () and is 53008780.07184796
Farm annual value production (AVP) is 464356.91342938814 USD