diff --git a/.github/workflows/check-working-examples.yaml b/.github/workflows/check-working-examples.yaml index 26483a4d6..138e70de8 100644 --- a/.github/workflows/check-working-examples.yaml +++ b/.github/workflows/check-working-examples.yaml @@ -36,19 +36,22 @@ jobs: error_found=0 # 0 is false error_results="Error in example:" - # Run each Python script example - for i in *.py; do - - # Skip these examples until the wind rose, optimization package, and - # uncertainty interface are update to v4 - if [[ $i == *20* ]]; then - continue + # Now run the examples in root and subdirectories + echo "Running examples" + for d in . $(find . -type d -name "*examples*"); do + cd $d + echo "========================= Example directory- $d" + for i in *.py; do + echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Running example- $i" + if ! python $i; then + error_results="${error_results}"$'\n'" - ${i}" + error_found=1 + fi + done + if [ "$d" != "." ]; then + cd .. fi - if ! python $i; then - error_results="${error_results}"$'\n'" - ${i}" - error_found=1 - fi done if [[ $error_found ]]; then diff --git a/examples/01_opening_floris_computing_power.py b/examples/001_opening_floris_computing_power.py similarity index 79% rename from examples/01_opening_floris_computing_power.py rename to examples/001_opening_floris_computing_power.py index dcb1987c1..52950c922 100644 --- a/examples/01_opening_floris_computing_power.py +++ b/examples/001_opening_floris_computing_power.py @@ -1,8 +1,8 @@ """Example 1: Opening FLORIS and Computing Power -This first example illustrates several of the key concepts in FLORIS. It: +This example illustrates several of the key concepts in FLORIS. It demonstrates: - 1) Initializing FLORIS + 1) Initializing a FLORIS model 2) Changing the wind farm layout 3) Changing the incoming wind speed, wind direction and turbulence intensity 4) Running the FLORIS simulation @@ -17,22 +17,22 @@ from floris import FlorisModel -# Initialize FLORIS with the given input file. -# The Floris class is the entry point for most usage. +# The FlorisModel class is the entry point for most usage. +# Initialize using an input yaml file fmodel = FlorisModel("inputs/gch.yaml") # Changing the wind farm layout uses FLORIS' set method to a two-turbine layout fmodel.set(layout_x=[0, 500.0], layout_y=[0.0, 0.0]) -# Changing wind speed, wind direction, and turbulence intensity using the set method +# Changing wind speed, wind direction, and turbulence intensity uses the set method # as well. Note that the wind_speeds, wind_directions, and turbulence_intensities # are all specified as arrays of the same length. -fmodel.set(wind_directions=np.array([270.0]), - wind_speeds=[8.0], - turbulence_intensities=np.array([0.06])) +fmodel.set( + wind_directions=np.array([270.0]), wind_speeds=[8.0], turbulence_intensities=np.array([0.06]) +) # Note that typically all 3, wind_directions, wind_speeds and turbulence_intensities -# must be supplied to set. However, the exception is if not changing the lenght +# must be supplied to set. However, the exception is if not changing the length # of the arrays, then only one or two may be supplied. fmodel.set(turbulence_intensities=np.array([0.07])) @@ -42,9 +42,11 @@ # be unique. Internally in FLORIS, most data structures will have the findex as their # 0th dimension. The value n_findex is the total number of conditions to be simulated. # This command would simulate 4 conditions (n_findex = 4). -fmodel.set(wind_directions=np.array([270.0, 270.0, 270.0, 270.0]), - wind_speeds=[8.0, 8.0, 10.0, 10.0], - turbulence_intensities=np.array([0.06, 0.06, 0.06, 0.06])) +fmodel.set( + wind_directions=np.array([270.0, 270.0, 270.0, 270.0]), + wind_speeds=[8.0, 8.0, 10.0, 10.0], + turbulence_intensities=np.array([0.06, 0.06, 0.06, 0.06]), +) # After the set method, the run method is called to perform the simulation fmodel.run() diff --git a/examples/002_visualizations.py b/examples/002_visualizations.py new file mode 100644 index 000000000..f8c946324 --- /dev/null +++ b/examples/002_visualizations.py @@ -0,0 +1,94 @@ +"""Example 2: Visualizations + +This example demonstrates the use of the flow and layout visualizations in FLORIS. +First, an example wind farm layout is plotted, with the turbine names and the directions +and distances between turbines shown in different configurations by subplot. +Next, the horizontal flow field at hub height is plotted for a single wind condition. + +FLORIS includes two modules for visualization: + 1) flow_visualization: for visualizing the flow field + 2) layout_visualization: for visualizing the layout of the wind farm +The two modules can be used together to visualize the flow field and the layout +of the wind farm. + +""" + + +import matplotlib.pyplot as plt + +import floris.layout_visualization as layoutviz +from floris import FlorisModel +from floris.flow_visualization import visualize_cut_plane + + +fmodel = FlorisModel("inputs/gch.yaml") + +# Set the farm layout to have 8 turbines irregularly placed +layout_x = [0, 500, 0, 128, 1000, 900, 1500, 1250] +layout_y = [0, 300, 750, 1400, 0, 567, 888, 1450] +fmodel.set(layout_x=layout_x, layout_y=layout_y) + + +# Layout visualization contains the functions for visualizing the layout: +# plot_turbine_points +# plot_turbine_labels +# plot_turbine_rotors +# plot_waking_directions +# Each of which can be overlaid to provide further information about the layout +# This series of 4 subplots shows the different ways to visualize the layout + +# Create the plotting objects using matplotlib +fig, axarr = plt.subplots(2, 2, figsize=(15, 10), sharex=False) +axarr = axarr.flatten() + +ax = axarr[0] +layoutviz.plot_turbine_points(fmodel, ax=ax) +ax.set_title("Turbine Points") + +ax = axarr[1] +layoutviz.plot_turbine_points(fmodel, ax=ax) +layoutviz.plot_turbine_labels(fmodel, ax=ax) +ax.set_title("Turbine Points and Labels") + +ax = axarr[2] +layoutviz.plot_turbine_points(fmodel, ax=ax) +layoutviz.plot_turbine_labels(fmodel, ax=ax) +layoutviz.plot_waking_directions(fmodel, ax=ax, limit_num=2) +ax.set_title("Turbine Points, Labels, and Waking Directions") + +# In the final subplot, use provided turbine names in place of the t_index +ax = axarr[3] +turbine_names = ["T1", "T2", "T3", "T4", "T9", "T10", "T75", "T78"] +layoutviz.plot_turbine_points(fmodel, ax=ax) +layoutviz.plot_turbine_labels(fmodel, ax=ax, turbine_names=turbine_names) +layoutviz.plot_waking_directions(fmodel, ax=ax, limit_num=2) +ax.set_title("Use Provided Turbine Names") + + +# Visualizations of the flow field are made by using calculate plane methods. In this example +# we show the horizontal plane at hub height, further examples are provided within +# the examples_visualizations folder + +# For flow visualizations, the FlorisModel must be set to run a single condition +# (n_findex = 1) +fmodel.set(wind_speeds=[8.0], wind_directions=[290.0], turbulence_intensities=[0.06]) +horizontal_plane = fmodel.calculate_horizontal_plane( + x_resolution=200, + y_resolution=100, + height=90.0, +) + +# Plot the flow field with rotors +fig, ax = plt.subplots() +visualize_cut_plane( + horizontal_plane, + ax=ax, + label_contours=False, + title="Horizontal Flow with Turbine Rotors and labels", +) + +# Plot the turbine rotors +layoutviz.plot_turbine_rotors(fmodel, ax=ax) +layoutviz.plot_turbine_labels(fmodel, ax=ax, turbine_names=turbine_names) + +plt.show() diff --git a/examples/003_wind_data_objects.py b/examples/003_wind_data_objects.py new file mode 100644 index 000000000..d382d9a29 --- /dev/null +++ b/examples/003_wind_data_objects.py @@ -0,0 +1,239 @@ +"""Example 3: Wind Data Objects + +This example demonstrates the use of wind data objects in FLORIS: + TimeSeries, WindRose, and WindTIRose. + + For each of the WindData objects, examples are shown of: + + 1) Initializing the object + 2) Broadcasting values + 3) Converting between objects + 4) Setting TI and value + 5) Plotting + 6) Setting the FLORIS model using the object + +""" + + +import matplotlib.pyplot as plt +import numpy as np + +from floris import ( + FlorisModel, + TimeSeries, + WindRose, + WindTIRose, +) + + +################################################## +# Initializing +################################################## + +# FLORIS provides a set of wind data objects to hold the ambient wind conditions in a +# convenient classes that include capabilities and methods to manipulate and visualize +# the data. + +# The TimeSeries class is used to hold time series data, such as wind speed, wind direction, +# and turbulence intensity. + +# There is also a "value" wind data variable, which represents the value of the power +# generated at each time step or wind condition (e.g., the price of electricity). This can +# then be used in later optimization methods to optimize for quantities besides AEP. + +# Generate wind speeds, directions, turbulence intensities, and values via random signals +N = 100 +wind_speeds = 8 + 2 * np.random.randn(N) +wind_directions = 270 + 30 * np.random.randn(N) +turbulence_intensities = 0.06 + 0.02 * np.random.randn(N) +values = 25 + 10 * np.random.randn(N) + +time_series = TimeSeries( + wind_directions=wind_directions, + wind_speeds=wind_speeds, + turbulence_intensities=turbulence_intensities, + values=values, +) + +# The WindRose class is used to hold wind rose data, such as wind speed, wind direction, +# and frequency. TI and value are represented as bin averages per wind direction and +# speed bin. +wind_directions = np.arange(0, 360, 3.0) +wind_speeds = np.arange(4, 20, 2.0) + +# Make TI table 6% TI for all wind directions and speeds +ti_table = 0.06 * np.ones((len(wind_directions), len(wind_speeds))) + +# Make value table 25 for all wind directions and speeds +value_table =25 * np.ones((len(wind_directions), len(wind_speeds))) + +# Uniform frequency +freq_table = np.ones((len(wind_directions), len(wind_speeds))) +freq_table = freq_table / np.sum(freq_table) + +wind_rose = WindRose( + wind_directions=wind_directions, + wind_speeds=wind_speeds, + ti_table=ti_table, + freq_table=freq_table, + value_table=value_table, +) + +# The WindTIRose class is similar to the WindRose table except that TI is also binned +# making the frequency table a 3D array. +turbulence_intensities = np.arange(0.05, 0.15, 0.01) + +# Uniform frequency +freq_table = np.ones((len(wind_directions), len(wind_speeds), len(turbulence_intensities))) + +# Uniform value +value_table = 25* np.ones((len(wind_directions), len(wind_speeds), len(turbulence_intensities))) + +wind_ti_rose = WindTIRose( + wind_directions=wind_directions, + wind_speeds=wind_speeds, + turbulence_intensities=turbulence_intensities, + freq_table=freq_table, + value_table=value_table, +) + +################################################## +# Broadcasting +################################################## + +# A convenience method of the wind data objects is that, unlike the lower-level +# FlorisModel.set() method, the wind data objects can broadcast upward data provided +# as a scalar to the full array. This is useful for setting the same wind conditions +# for all turbines in a wind farm. + +# For TimeSeries, as long as one condition is given as an array, the other 2 +# conditions can be given as scalars. The TimeSeries object will broadcast the +# scalars to the full array (uniform) +wind_directions = 270 + 30 * np.random.randn(N) +time_series = TimeSeries( + wind_directions=wind_directions, wind_speeds=8.0, turbulence_intensities=0.06 +) + + +# For WindRose, wind directions and wind speeds must be given as arrays, but the +# ti_table can be supplied as a scalar which will apply uniformly to all wind +# directions and speeds. Not supplying a freq table will similarly generate +# a uniform frequency table. +wind_directions = np.arange(0, 360, 3.0) +wind_speeds = np.arange(4, 20, 2.0) +wind_rose = WindRose(wind_directions=wind_directions, wind_speeds=wind_speeds, ti_table=0.06) + + +################################################## +# Wind Rose from Time Series +################################################## + +# The TimeSeries class has a method to generate a wind rose from a time series based on binning +wind_rose = time_series.to_WindRose(wd_edges=np.arange(0, 360, 3.0), ws_edges=np.arange(2, 20, 2.0)) + +################################################## +# Wind Rose from long CSV FILE +################################################## + +# The WindRose class can also be initialized from a long CSV file. By long what is meant is +# that the file has a column for each wind direction, wind speed combination. The file can +# also specify the mean TI per bin and the frequency of each bin as seperate columns. + +# If the TI is not provided, can specify a fixed TI for all bins using the ti_col_or_value +# input +wind_rose_from_csv = WindRose.read_csv_long( + "inputs/wind_rose.csv", wd_col="wd", ws_col="ws", freq_col="freq_val", ti_col_or_value=0.06 +) + +################################################## +# Setting turbulence intensity +################################################## + +# Each of the wind data objects also has the ability to set the turbulence intensity +# according to a function of wind speed and direction. This can be done using a custom +# function by using the assign_ti_using_wd_ws_function method. There is also a method +# called assign_ti_using_IEC_method which assigns TI based on the IEC 61400-1 standard. +wind_rose.assign_ti_using_IEC_method() # Assign using default settings for Iref and offset + +################################################## +# Setting value +################################################## + +# Similarly, each of the wind data objects also has the ability to set the value according to +# a function of wind speed and direction. This can be done using a custom function by using +# the assign_value_using_wd_ws_function method. There is also a method called +# assign_value_piecewise_linear which assigns value based on a linear piecewise function of +# wind speed. + +# Assign value using default settings. 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. +wind_rose.assign_value_piecewise_linear() + +################################################## +# Plotting Wind Data Objects +################################################## + +# Certain plotting methods are included to enable visualization of the wind data objects +# Plotting a wind rose +wind_rose.plot_wind_rose() + +# Showing TI over wind speed for a WindRose +wind_rose.plot_ti_over_ws() + +# Showing value over wind speed for a WindRose +wind_rose.plot_value_over_ws() + +################################################## +# Setting the FLORIS model via wind data +################################################## + +# Each of the wind data objects can be used to set the FLORIS model by passing +# them in as is to the set method. The FLORIS model will then use the member functions +# of the wind data to extract the wind conditions for the simulation. Frequency tables +# are also extracted for expected power and AEP-like calculations. +# Similarly the value data is extracted and maintained. + +fmodel = FlorisModel("inputs/gch.yaml") + +# Set the wind conditions using the TimeSeries object +fmodel.set(wind_data=time_series) + +# Set the wind conditions using the WindRose object +fmodel.set(wind_data=wind_rose) + +# Note that in the case of the wind_rose, under the default settings, wind direction and wind speed +# bins for which frequency is zero are not simulated. This can be changed by setting the +# compute_zero_freq_occurrence parameter to True. +wind_directions = np.array([200.0, 300.0]) +wind_speeds = np.array([5.0, 1.00]) +freq_table = np.array([[0.5, 0], [0.5, 0]]) +wind_rose = WindRose( + wind_directions=wind_directions, wind_speeds=wind_speeds, ti_table=0.06, freq_table=freq_table +) +fmodel.set(wind_data=wind_rose) + +print( + f"Number of conditions to simulate with compute_zero_freq_occurrence = False: " + f"{fmodel.n_findex}" +) + +wind_rose = WindRose( + wind_directions=wind_directions, + wind_speeds=wind_speeds, + ti_table=0.06, + freq_table=freq_table, + compute_zero_freq_occurrence=True, +) +fmodel.set(wind_data=wind_rose) + +print( + f"Number of conditions to simulate with compute_zero_freq_occurrence = " + f"True: {fmodel.n_findex}" +) + +# Set the wind conditions using the WindTIRose object +fmodel.set(wind_data=wind_ti_rose) + +plt.show() diff --git a/examples/004_set.py b/examples/004_set.py new file mode 100644 index 000000000..ab103098a --- /dev/null +++ b/examples/004_set.py @@ -0,0 +1,105 @@ +"""Example 4: Set + +This example illustrates the use of the set method. The set method is used to +change the wind conditions, the wind farm layout, the turbine type, +and the controls settings. + +This example demonstrates setting each of the following: + 1) Wind conditions + 2) Wind farm layout + 3) Controls settings + +""" + + +import numpy as np + +from floris import ( + FlorisModel, + TimeSeries, + WindRose, +) + + +fmodel = FlorisModel("inputs/gch.yaml") + +###################################################### +# Atmospheric Conditions +###################################################### + + +# Change the wind directions, wind speeds, and turbulence intensities using numpy arrays +fmodel.set( + wind_directions=np.array([270.0, 270.0, 270.0]), + wind_speeds=[8.0, 9.0, 10.0], + turbulence_intensities=np.array([0.06, 0.06, 0.06]), +) + +# Set the wind conditions as above using the TimeSeries object +fmodel.set( + wind_data=TimeSeries( + wind_directions=270.0, wind_speeds=np.array([8.0, 9.0, 10.0]), turbulence_intensities=0.06 + ) +) + +# Set the wind conditions as above using the WindRose object +fmodel.set( + wind_data=WindRose( + wind_directions=np.array([270.0]), + wind_speeds=np.array([8.0, 9.0, 10.0]), + ti_table=0.06, + ) +) + +# Set the wind shear +fmodel.set(wind_shear=0.2) + + +# Set the air density +fmodel.set(air_density=1.1) + +# Set the reference wind height (which is the height at which the wind speed is given) +fmodel.set(reference_wind_height=92.0) + + +###################################################### +# Array Settings +###################################################### + +# Changing the wind farm layout uses FLORIS' set method to a two-turbine layout +fmodel.set(layout_x=[0, 500.0], layout_y=[0.0, 0.0]) + +###################################################### +# Controls Settings +###################################################### + +# Changes to controls settings can be made using the set method +# Note the dimension must match (n_findex, n_turbines) or (number of conditions, number of turbines) +# Above we n_findex = 3 and n_turbines = 2 so the matrix of yaw angles must be 3x2 +yaw_angles = np.array([[0.0, 0.0], [25.0, 0.0], [0.0, 0.0]]) +fmodel.set(yaw_angles=yaw_angles) + +# By default for the turbines in the turbine_library, the power +# thrust model is set to "cosine-loss" which adjusts +# power and thrust according to cos^cosine_loss_exponent(yaw | tilt) +# where the default exponent is 1.88. For other +# control capabilities, the power thrust model can be set to "mixed" +# which provides the same cosine loss model, and +# additionally methods for specifying derating levels for power and disabling turbines. + +# Use the reset operation method to clear out control signals +fmodel.reset_operation() + +# Change to the mixed model turbine +fmodel.set_operation_model("mixed") + +# Shut down the front turbine for the first two findex +disable_turbines = np.array([[True, False], [True, False], [False, False]]) +fmodel.set(disable_turbines=disable_turbines) + +# Derate the front turbine for the first two findex +RATED_POWER = 5e6 # 5MW (Anything above true rated power will still result in rated power) +power_setpoints = np.array( + [[RATED_POWER * 0.3, RATED_POWER], [RATED_POWER * 0.3, RATED_POWER], [RATED_POWER, RATED_POWER]] +) +fmodel.set(power_setpoints=power_setpoints) diff --git a/examples/005_getting_power.py b/examples/005_getting_power.py new file mode 100644 index 000000000..2f4ddd9d2 --- /dev/null +++ b/examples/005_getting_power.py @@ -0,0 +1,144 @@ +"""Example 5: Getting Turbine and Farm Power + +After setting the FlorisModel and running, the next step is typically to get the power output +of the turbines. FLORIS has several methods for getting power: + +1. `get_turbine_powers()`: Returns the power output of each turbine in the farm for each findex + (n_findex, n_turbines) +2. `get_farm_power()`: Returns the total power output of the farm for each findex (n_findex) +3. `get_expected_farm_power()`: Returns the combination of the farm power over each findex + with the frequency of each findex to get the expected farm power +4. `get_farm_AEP()`: Multiplies the expected farm power by the number of hours in a year to get + the expected annual energy production (AEP) of the farm + + +""" + +import matplotlib.pyplot as plt +import numpy as np + +from floris import ( + FlorisModel, + TimeSeries, + WindRose, +) + + +fmodel = FlorisModel("inputs/gch.yaml") + +# Set to a 3-turbine layout +fmodel.set(layout_x=[0, 126 * 5, 126 * 10], layout_y=[0, 0, 0]) + +###################################################### +# Using TimeSeries +###################################################### + +# Set up a time series in which the wind speed and TI are constant but the wind direction +# sweeps the range from 250 to 290 degrees +wind_directions = np.arange(250, 290, 1.0) +time_series = TimeSeries( + wind_directions=wind_directions, wind_speeds=9.9, turbulence_intensities=0.06 +) +fmodel.set(wind_data=time_series) + +# Run the model +fmodel.run() + +# Get the turbine powers +turbine_powers = fmodel.get_turbine_powers() + +# Turbines powers will have shape (n_findex, n_turbines) where n_findex is the number of unique +# wind conditions and n_turbines is the number of turbines in the farm +print(f"Turbine power has shape {turbine_powers.shape}") + +# It is also possible to get the farm power directly +farm_power = fmodel.get_farm_power() + +# Farm power has length n_findex, and is the sum of the turbine powers +print(f"Farm power has shape {farm_power.shape}") + +# It's possible to get these powers with wake losses disabled, this can be useful +# for computing total wake losses +fmodel.run_no_wake() +farm_power_no_wake = fmodel.get_farm_power() + +# Plot the results +fig, axarr = plt.subplots(1, 3, figsize=(15, 5)) + +# Plot the turbine powers +ax = axarr[0] +for i in range(turbine_powers.shape[1]): + ax.plot(wind_directions, turbine_powers[:, i] / 1e3, label=f"Turbine {i+1} ") +ax.set_xlabel("Wind Direction (deg)") +ax.set_ylabel("Power (kW)") +ax.grid(True) +ax.legend() +ax.set_title("Turbine Powers") + +# Plot the farm power +ax = axarr[1] +ax.plot(wind_directions, farm_power / 1e3, label="Farm Power With Wakes", color="k") +ax.plot(wind_directions, farm_power_no_wake / 1e3, label="Farm Power No Wakes", color="r") +ax.set_xlabel("Wind Direction (deg)") +ax.set_ylabel("Power (kW)") +ax.grid(True) +ax.legend() +ax.set_title("Farm Power") + +# Plot the percent wake losses +ax = axarr[2] +percent_wake_losses = 100 * (farm_power_no_wake - farm_power) / farm_power_no_wake +ax.plot(wind_directions, percent_wake_losses, label="Percent Wake Losses", color="k") +ax.set_xlabel("Wind Direction (deg)") +ax.set_ylabel("Percent Wake Losses") +ax.grid(True) +ax.legend() +ax.set_title("Percent Wake Losses") + + +###################################################### +# Using WindRose +###################################################### + +# When running FLORIS using a wind rose, that is when a WindRose or WindTIRose object is +# passed into the set function. The functions get_expected_farm_power and get_farm_AEP +# will operate the same as above, however the functions get_turbine_powers and get_farm_power +# will be reshaped from (n_findex, n_turbines) and +# (n_findex) to (n_wind_dir, n_wind_speed, n_turbines) +# and (n_wind_dir, n_wind_speed) respectively. This is make the powers align more easily with the +# provided wind rose. + +# Declare a WindRose object of 2 wind directions and 3 wind speeds and constant turbulence intensity +wind_rose = WindRose( + wind_directions=np.array([270.0, 280.0]), wind_speeds=np.array([8.0, 9.0, 10.0]), ti_table=0.06 +) + +fmodel.set(wind_data=wind_rose) + +print("==========Wind Rose==========") +print(f"Number of conditions to simulate (2 x 3): {fmodel.n_findex}") + +fmodel.run() + +turbine_powers = fmodel.get_turbine_powers() + +print(f"Shape of turbine powers: {turbine_powers.shape}") + +farm_power = fmodel.get_farm_power() + +print(f"Shape of farm power: {farm_power.shape}") + + +# Plot the farm power +fig, ax = plt.subplots() + +for w_idx, wd in enumerate(wind_rose.wind_directions): + ax.plot(wind_rose.wind_speeds, farm_power[w_idx, :] / 1e3, label=f"WD: {wd}") + +ax.set_xlabel("Wind Speed (m/s)") +ax.set_ylabel("Power (kW)") +ax.grid(True) +ax.legend() +ax.set_title("Farm Power (from Wind Rose)") + +plt.show() diff --git a/examples/006_get_farm_aep.py b/examples/006_get_farm_aep.py new file mode 100644 index 000000000..2d9121be9 --- /dev/null +++ b/examples/006_get_farm_aep.py @@ -0,0 +1,103 @@ +"""Example 6: Getting Expected Power and AEP + +The expected power of a farm is computed by multiplying the power output of the farm by the +frequency of each findex. This is done by the `get_expected_farm_power` method. The expected +AEP is annual energy production is computed by multiplying the expected power by the number of +hours in a year. + +If a wind_data object is provided to the model, the expected power and AEP + can be computed directly by the`get_farm_AEP_with_wind_data` using the frequency table + of the wind data object. If not, a frequency table must be passed into these functions + + +""" + +import numpy as np +import pandas as pd + +from floris import ( + FlorisModel, + TimeSeries, + WindRose, +) + + +fmodel = FlorisModel("inputs/gch.yaml") + + +# Set to a 3-turbine layout +D = 126. +fmodel.set(layout_x=[0.0, 5 * D, 10 * D], + layout_y=[0.0, 0.0, 0.0]) + +# Using TimeSeries + +# Randomly generated a time series with time steps = 365 * 24 +N = 365 * 24 +wind_directions = np.random.uniform(0, 360, N) +wind_speeds = np.random.uniform(5, 25, N) + +# Set up a time series +time_series = TimeSeries( + wind_directions=wind_directions, wind_speeds=wind_speeds, turbulence_intensities=0.06 +) + +# Set the wind data +fmodel.set(wind_data=time_series) + +# Run the model +fmodel.run() + +expected_farm_power = fmodel.get_expected_farm_power() +aep = fmodel.get_farm_AEP() + +# Note this is equivalent to the following +aep_b = fmodel.get_farm_AEP(freq=time_series.unpack_freq()) + +print(f"AEP from time series: {aep}, and re-computed AEP: {aep_b}") + +# Using WindRose============================================== + +# Load the wind rose from csv as in example 003 +wind_rose = WindRose.read_csv_long( + "inputs/wind_rose.csv", wd_col="wd", ws_col="ws", freq_col="freq_val", ti_col_or_value=0.06 +) + + +# Store some values +n_wd = len(wind_rose.wind_directions) +n_ws = len(wind_rose.wind_speeds) + +# Store the number of elements of the freq_table which are 0 +n_zeros = np.sum(wind_rose.freq_table == 0) + +# Set the wind rose +fmodel.set(wind_data=wind_rose) + +# Run the model +fmodel.run() + +# Note that the frequency table contains 0 frequency for some wind directions and wind speeds +# and we've not selected to compute 0 frequency bins, therefore the n_findex will be less than +# the total number of wind directions and wind speed combinations +print(f"Total number of wind direction and wind speed combination: {n_wd * n_ws}") +print(f"Number of 0 frequency bins: {n_zeros}") +print(f"n_findex: {fmodel.n_findex}") + +# Get the AEP +aep = fmodel.get_farm_AEP() + +# Print the AEP +print(f"AEP from wind rose: {aep/1E9:.3f} (GWh)") + +# Run the model again, without wakes, and use the result to compute the wake losses +fmodel.run_no_wake() + +# Get the AEP without wake +aep_no_wake = fmodel.get_farm_AEP() + +# Compute the wake losses +wake_losses = 100 * (aep_no_wake - aep) / aep_no_wake + +# Print the wake losses +print(f"Wake losses: {wake_losses:.2f}%") diff --git a/examples/007_sweeping_variables.py b/examples/007_sweeping_variables.py new file mode 100644 index 000000000..502d961a4 --- /dev/null +++ b/examples/007_sweeping_variables.py @@ -0,0 +1,217 @@ +"""Example 7: Sweeping Variables + +Demonstrate methods for sweeping across variables. Wind directions, wind speeds, +turbulence intensities, as well as control inputs are passed to set() as arrays +and so can be swept and run in one call to run(). + +The example includes demonstrations of sweeping: + + 1) Wind speeds + 2) Wind directions + 3) Turbulence intensities + 4) Yaw angles + 5) Power setpoints + 6) Disabling turbines + +""" + +import matplotlib.pyplot as plt +import numpy as np + +from floris import ( + FlorisModel, + TimeSeries, +) + + +fmodel = FlorisModel("inputs/gch.yaml") + +# Set to a 2 turbine layout +fmodel.set(layout_x=[0.0, 126 * 5], layout_y=[0.0, 0.0]) + +# Start a figure for the results +fig, axarr = plt.subplots(2, 3, figsize=(15, 10), sharey=True) +axarr = axarr.flatten() + +###################################################### +# Sweep wind speeds +###################################################### + + +# The TimeSeries object is the most convenient for sweeping +# wind speeds while keeping the wind direction and turbulence +# intensity constant +wind_speeds = np.arange(5, 10, 0.1) +fmodel.set( + wind_data=TimeSeries( + wind_speeds=wind_speeds, wind_directions=270.0, turbulence_intensities=0.06 + ) +) +fmodel.run() +turbine_powers = fmodel.get_turbine_powers() / 1e3 + +# Plot the results +ax = axarr[0] +ax.plot(wind_speeds, turbine_powers[:, 0], label="Upstream Turbine", color="k") +ax.plot(wind_speeds, turbine_powers[:, 1], label="Downstream Turbine", color="r") +ax.set_ylabel("Power (kW)") +ax.set_xlabel("Wind Speed (m/s)") +ax.legend() + +###################################################### +# Sweep wind directions +###################################################### + + +wind_directions = np.arange(250, 290, 1.0) +fmodel.set( + wind_data=TimeSeries( + wind_speeds=8.0, wind_directions=wind_directions, turbulence_intensities=0.06 + ) +) +fmodel.run() +turbine_powers = fmodel.get_turbine_powers() / 1e3 + +# Plot the results +ax = axarr[1] +ax.plot(wind_directions, turbine_powers[:, 0], label="Upstream Turbine", color="k") +ax.plot(wind_directions, turbine_powers[:, 1], label="Downstream Turbine", color="r") +ax.set_xlabel("Wind Direction (deg)") + +###################################################### +# Sweep turbulence intensities +###################################################### + +turbulence_intensities = np.arange(0.03, 0.2, 0.01) +fmodel.set( + wind_data=TimeSeries( + wind_speeds=8.0, wind_directions=270.0, turbulence_intensities=turbulence_intensities + ) +) +fmodel.run() + +turbine_powers = fmodel.get_turbine_powers() / 1e3 + +# Plot the results +ax = axarr[2] +ax.plot(turbulence_intensities, turbine_powers[:, 0], label="Upstream Turbine", color="k") +ax.plot(turbulence_intensities, turbine_powers[:, 1], label="Downstream Turbine", color="r") +ax.set_xlabel("Turbulence Intensity") + +###################################################### +# Sweep the upstream yaw angle +###################################################### + +# First set the conditions to uniform for N yaw_angles +n_yaw = 100 +wind_directions = np.ones(n_yaw) * 270.0 +fmodel.set( + wind_data=TimeSeries( + wind_speeds=8.0, wind_directions=wind_directions, turbulence_intensities=0.06 + ) +) + +yaw_angles_upstream = np.linspace(-30, 30, n_yaw) +yaw_angles = np.zeros((n_yaw, 2)) +yaw_angles[:, 0] = yaw_angles_upstream + +fmodel.set(yaw_angles=yaw_angles) +fmodel.run() +turbine_powers = fmodel.get_turbine_powers() / 1e3 + +# Plot the results +ax = axarr[3] +ax.plot(yaw_angles_upstream, turbine_powers[:, 0], label="Upstream Turbine", color="k") +ax.plot(yaw_angles_upstream, turbine_powers[:, 1], label="Downstream Turbine", color="r") +ax.set_xlabel("Upstream Yaw Angle (deg)") +ax.set_ylabel("Power (kW)") + +###################################################### +# Sweep the upstream power rating +###################################################### + +# Since we're changing control modes, need to reset the operation +fmodel.reset_operation() + +# To the de-rating need to change the power_thrust_mode to mixed or simple de-rating +fmodel.set_operation_model("simple-derating") + +# Sweep the de-rating levels +RATED_POWER = 5e6 # For NREL 5MW +n_derating_levels = 150 +upstream_power_setpoint = np.linspace(0.0, RATED_POWER * 0.5, n_derating_levels) +power_setpoints = np.ones((n_derating_levels, 2)) * RATED_POWER +power_setpoints[:, 0] = upstream_power_setpoint + +# Set the wind conditions to fixed +wind_directions = np.ones(n_derating_levels) * 270.0 +fmodel.set( + wind_data=TimeSeries( + wind_speeds=8.0, wind_directions=wind_directions, turbulence_intensities=0.06 + ) +) + +# Set the de-rating levels +fmodel.set(power_setpoints=power_setpoints) +fmodel.run() + +# Get the turbine powers +turbine_powers = fmodel.get_turbine_powers() / 1e3 + +# Plot the results +ax = axarr[4] +ax.plot(upstream_power_setpoint / 1e3, turbine_powers[:, 0], label="Upstream Turbine", color="k") +ax.plot(upstream_power_setpoint / 1e3, turbine_powers[:, 1], label="Downstream Turbine", color="r") +ax.plot( + upstream_power_setpoint / 1e3, + upstream_power_setpoint / 1e3, + label="De-Rating Level", + color="b", + linestyle="--", +) +ax.set_xlabel("Upstream Power Setpoint (kW)") +ax.legend() + +###################################################### +# Sweep through disabling turbine combinations +###################################################### + +# Reset the control settings +fmodel.reset_operation() + +# Make a list of possible turbine disable combinations +disable_combinations = np.array([[False, False], [True, False], [False, True], [True, True]]) +n_combinations = disable_combinations.shape[0] + +# Make a list of strings representing the combinations +disable_combination_strings = ["None", "T0", "T1", "T0 & T1"] + +# Set the wind conditions to fixed +wind_directions = np.ones(n_combinations) * 270.0 +fmodel.set( + wind_data=TimeSeries( + wind_speeds=8.0, wind_directions=wind_directions, turbulence_intensities=0.06 + ) +) + +# Assign the disable settings +fmodel.set(disable_turbines=disable_combinations) + +# Run the model +fmodel.run() + +# Get the turbine powers +turbine_powers = fmodel.get_turbine_powers() / 1e3 + +# Plot the results +ax = axarr[5] +ax.plot(disable_combination_strings, turbine_powers[:, 0], "ks-", label="Upstream Turbine") +ax.plot(disable_combination_strings, turbine_powers[:, 1], "ro-", label="Downstream Turbine") +ax.set_xlabel("Turbine Disable Combination") + + +for ax in axarr: + ax.grid(True) + + +plt.show() diff --git a/examples/008_uncertain_models.py b/examples/008_uncertain_models.py new file mode 100644 index 000000000..9d151d687 --- /dev/null +++ b/examples/008_uncertain_models.py @@ -0,0 +1,160 @@ +"""Example 8: Uncertain Models + +UncertainFlorisModel is a class that adds uncertainty to the inflow wind direction +on the FlorisModel class. The UncertainFlorisModel class is interacted with in the +same manner as the FlorisModel class is. This example demonstrates how the +wind farm power production is calculated with and without uncertainty. +Other use cases of UncertainFlorisModel are, e.g., comparing FLORIS to +historical SCADA data and robust optimization. + +For more details on using uncertain models, see further examples within the +examples_uncertain directory. + +""" + +import matplotlib.pyplot as plt +import numpy as np + +from floris import ( + FlorisModel, + TimeSeries, + UncertainFlorisModel, +) + + +# Instantiate FLORIS FLORIS and UncertainFLORIS models +fmodel = FlorisModel("inputs/gch.yaml") # GCH model + +# The instantiation of the UncertainFlorisModel class is similar to the FlorisModel class +# with the addition of the wind direction standard deviation (wd_std) parameter +# and certain resolution parameters. Internally, the UncertainFlorisModel class +# expands the wind direction time series to include the uncertainty but then +# only runs the unique cases. The final result is computed via a gaussian weighting +# of the cases according to wd_std. Here we use the default resolution parameters. +# wd_resolution=1.0, # Degree +# ws_resolution=1.0, # m/s +# ti_resolution=0.01, + +ufmodel_3 = UncertainFlorisModel("inputs/gch.yaml", wd_std=3) +ufmodel_5 = UncertainFlorisModel("inputs/gch.yaml", wd_std=5) + +# Define an inflow where wind direction is swept while +# wind speed and turbulence intensity are held constant +wind_directions = np.arange(240.0, 300.0, 1.0) +time_series = TimeSeries( + wind_directions=wind_directions, + wind_speeds=8.0, + turbulence_intensities=0.06, +) + +# Define a two turbine farm and apply the inflow +D = 126.0 +layout_x = np.array([0, D * 6]) +layout_y = [0, 0] + +fmodel.set( + layout_x=layout_x, + layout_y=layout_y, + wind_data=time_series, +) +ufmodel_3.set( + layout_x=layout_x, + layout_y=layout_y, + wind_data=time_series, +) +ufmodel_5.set( + layout_x=layout_x, + layout_y=layout_y, + wind_data=time_series, +) + + +# Run both models +fmodel.run() +ufmodel_3.run() +ufmodel_5.run() + +# Collect the nominal and uncertain farm power +turbine_powers_nom = fmodel.get_turbine_powers() / 1e3 +turbine_powers_unc_3 = ufmodel_3.get_turbine_powers() / 1e3 +turbine_powers_unc_5 = ufmodel_5.get_turbine_powers() / 1e3 +farm_powers_nom = fmodel.get_farm_power() / 1e3 +farm_powers_unc_3 = ufmodel_3.get_farm_power() / 1e3 +farm_powers_unc_5 = ufmodel_5.get_farm_power() / 1e3 + +# Plot results +fig, axarr = plt.subplots(1, 3, figsize=(15, 5)) +ax = axarr[0] +ax.plot(wind_directions, turbine_powers_nom[:, 0].flatten(), color="k", label="Nominal power") +ax.plot( + wind_directions, + turbine_powers_unc_3[:, 0].flatten(), + color="r", + label="Power with uncertainty = 3 deg", +) +ax.plot( + wind_directions, + turbine_powers_unc_5[:, 0].flatten(), + color="m", + label="Power with uncertainty = 5deg", +) +ax.grid(True) +ax.legend() +ax.set_xlabel("Wind Direction (deg)") +ax.set_ylabel("Power (kW)") +ax.set_title("Upstream Turbine") + +ax = axarr[1] +ax.plot(wind_directions, turbine_powers_nom[:, 1].flatten(), color="k", label="Nominal power") +ax.plot( + wind_directions, + turbine_powers_unc_3[:, 1].flatten(), + color="r", + label="Power with uncertainty = 3 deg", +) +ax.plot( + wind_directions, + turbine_powers_unc_5[:, 1].flatten(), + color="m", + label="Power with uncertainty = 5 deg", +) +ax.set_title("Downstream Turbine") +ax.grid(True) +ax.legend() +ax.set_xlabel("Wind Direction (deg)") +ax.set_ylabel("Power (kW)") + +ax = axarr[2] +ax.plot(wind_directions, farm_powers_nom.flatten(), color="k", label="Nominal farm power") +ax.plot( + wind_directions, + farm_powers_unc_3.flatten(), + color="r", + label="Farm power with uncertainty = 3 deg", +) +ax.plot( + wind_directions, + farm_powers_unc_5.flatten(), + color="m", + label="Farm power with uncertainty = 5 deg", +) +ax.set_title("Farm Power") +ax.grid(True) +ax.legend() +ax.set_xlabel("Wind Direction (deg)") +ax.set_ylabel("Power (kW)") + +# Compare the AEP calculation +freq = np.ones_like(wind_directions) +freq = freq / freq.sum() + +aep_nom = fmodel.get_farm_AEP(freq=freq) +aep_unc_3 = ufmodel_3.get_farm_AEP(freq=freq) +aep_unc_5 = ufmodel_5.get_farm_AEP(freq=freq) + +print(f"AEP without uncertainty {aep_nom}") +print(f"AEP without uncertainty (3 deg) {aep_unc_3} ({100*aep_unc_3/aep_nom:.2f}%)") +print(f"AEP without uncertainty (5 deg) {aep_unc_5} ({100*aep_unc_5/aep_nom:.2f}%)") + + +plt.show() diff --git a/examples/009_compare_farm_power_with_neighbor.py b/examples/009_compare_farm_power_with_neighbor.py new file mode 100644 index 000000000..c67465f31 --- /dev/null +++ b/examples/009_compare_farm_power_with_neighbor.py @@ -0,0 +1,76 @@ +"""Example 9: Compare farm power with neighboring farm + +This example demonstrates how to use turbine_weights to define a set of turbines belonging +to a neighboring farm which impacts the power production of the farm under consideration +via wake losses, but whose own power production is not considered in farm power / aep production + +""" + + +import matplotlib.pyplot as plt +import numpy as np + +from floris import FlorisModel + + +# Instantiate FLORIS using either the GCH or CC model +fmodel = FlorisModel("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 + +# Define a 4 turbine farm turbine farm +D = 126.0 +layout_x = np.array([0, D * 6, 0, D * 6]) +layout_y = [0, 0, D * 3, D * 3] +fmodel.set(layout_x=layout_x, layout_y=layout_y) + +# Define a simple inflow with just 1 wind speed +wd_array = np.arange(0, 360, 4.0) +ws_array = 8.0 * np.ones_like(wd_array) +turbulence_intensities = 0.06 * np.ones_like(wd_array) +fmodel.set( + wind_directions=wd_array, wind_speeds=ws_array, turbulence_intensities=turbulence_intensities +) + + +# Calculate +fmodel.run() + +# Collect the farm power +farm_power_base = fmodel.get_farm_power() / 1e3 # In kW + +# Add a neighbor to the east +layout_x = np.array([0, D * 6, 0, D * 6, D * 12, D * 15, D * 12, D * 15]) +layout_y = np.array([0, 0, D * 3, D * 3, 0, 0, D * 3, D * 3]) +fmodel.set(layout_x=layout_x, layout_y=layout_y) + +# Define the weights to exclude the neighboring farm from calculations of power +turbine_weights = np.zeros(len(layout_x), dtype=int) +turbine_weights[0:4] = 1.0 + +# Calculate +fmodel.run() + +# Collect the farm power with the neighbor +farm_power_neighbor = fmodel.get_farm_power(turbine_weights=turbine_weights) / 1e3 # In kW + +# Show the farms +fig, ax = plt.subplots() +ax.scatter( + layout_x[turbine_weights == 1], layout_y[turbine_weights == 1], color="k", label="Base Farm" +) +ax.scatter( + layout_x[turbine_weights == 0], + layout_y[turbine_weights == 0], + color="r", + label="Neighboring Farm", +) +ax.legend() + +# Plot the power difference +fig, ax = plt.subplots() +ax.plot(wd_array, farm_power_base, color="k", label="Farm Power (no neighbor)") +ax.plot(wd_array, farm_power_neighbor, color="r", label="Farm Power (neighboring farm due east)") +ax.grid(True) +ax.legend() +ax.set_xlabel("Wind Direction (deg)") +ax.set_ylabel("Power (kW)") +plt.show() diff --git a/examples/02_visualizations.py b/examples/02_visualizations.py deleted file mode 100644 index de526328f..000000000 --- a/examples/02_visualizations.py +++ /dev/null @@ -1,149 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np - -import floris.flow_visualization as flowviz -from floris import FlorisModel - - -""" -This example initializes the FLORIS software, and then uses internal -functions to run a simulation and plot the results. In this case, -we are plotting three slices of the resulting flow field: -1. Horizontal slice parallel to the ground and located at the hub height -2. Vertical slice of parallel with the direction of the wind -3. Vertical slice parallel to to the turbine disc plane - -Additionally, an alternative method of plotting a horizontal slice -is shown. Rather than calculating points in the domain behind a turbine, -this method adds an additional turbine to the farm and moves it to -locations throughout the farm while calculating the velocity at it's -rotor. -""" - -# Initialize FLORIS with the given input file via FlorisModel. -# For basic usage, FlorisModel provides a simplified and expressive -# entry point to the simulation routines. -fmodel = FlorisModel("inputs/gch.yaml") - -# The rotor plots show what is happening at each turbine, but we do not -# see what is happening between each turbine. For this, we use a -# grid that has points regularly distributed throughout the fluid domain. -# The FlorisModel contains functions for configuring the new grid, -# running the simulation, and generating plots of 2D slices of the -# flow field. - -# Note this visualization grid created within the calculate_horizontal_plane function will be reset -# to what existed previously at the end of the function - -# Using the FlorisModel functions, get 2D slices. -horizontal_plane = fmodel.calculate_horizontal_plane( - x_resolution=200, - y_resolution=100, - height=90.0, - yaw_angles=np.array([[25.,0.,0.]]), -) - -y_plane = fmodel.calculate_y_plane( - x_resolution=200, - z_resolution=100, - crossstream_dist=0.0, - yaw_angles=np.array([[25.,0.,0.]]), -) -cross_plane = fmodel.calculate_cross_plane( - y_resolution=100, - z_resolution=100, - downstream_dist=630.0, - yaw_angles=np.array([[25.,0.,0.]]), -) - -# Create the plots -fig, ax_list = plt.subplots(3, 1, figsize=(10, 8)) -ax_list = ax_list.flatten() -flowviz.visualize_cut_plane( - horizontal_plane, - ax=ax_list[0], - label_contours=True, - title="Horizontal" -) -flowviz.visualize_cut_plane( - y_plane, - ax=ax_list[1], - label_contours=True, - title="Streamwise profile" -) -flowviz.visualize_cut_plane( - cross_plane, - ax=ax_list[2], - label_contours=True, - title="Spanwise profile" -) - -# Some wake models may not yet have a visualization method included, for these cases can use -# a slower version which scans a turbine model to produce the horizontal flow -horizontal_plane_scan_turbine = flowviz.calculate_horizontal_plane_with_turbines( - fmodel, - x_resolution=20, - y_resolution=10, - yaw_angles=np.array([[25.,0.,0.]]), -) - -fig, ax = plt.subplots() -flowviz.visualize_cut_plane( - horizontal_plane_scan_turbine, - ax=ax, - label_contours=True, - title="Horizontal (coarse turbine scan method)", -) - -# FLORIS further includes visualization methods for visualing the rotor plane of each -# Turbine in the simulation - -# Run the wake calculation to get the turbine-turbine interfactions -# on the turbine grids -fmodel.run() - -# Plot the values at each rotor -fig, axes, _ , _ = flowviz.plot_rotor_values( - fmodel.core.flow_field.u, - findex=0, - n_rows=1, - n_cols=3, - return_fig_objects=True -) -fig.suptitle("Rotor Plane Visualization, Original Resolution") - -# FLORIS supports multiple types of grids for capturing wind speed -# information. The current input file is configured with a square grid -# placed on each rotor plane with 9 points in a 3x3 layout. For visualization, -# this resolution can be increased. Note this operation, unlike the -# calc_x_plane above operations does not automatically reset the grid to -# the initial status as definied by the input file - -# Increase the resolution of points on each turbien plane -solver_settings = { - "type": "turbine_grid", - "turbine_grid_points": 10 -} -fmodel.set(solver_settings=solver_settings) - -# Run the wake calculation to get the turbine-turbine interfactions -# on the turbine grids -fmodel.run() - -# Plot the values at each rotor -fig, axes, _ , _ = flowviz.plot_rotor_values( - fmodel.core.flow_field.u, - findex=0, - n_rows=1, - n_cols=3, - return_fig_objects=True -) -fig.suptitle("Rotor Plane Visualization, 10x10 Resolution") - -# Show plots -plt.show() - -# Note if the user doesn't import matplotlib.pyplot as plt, the user can -# use the following to show the plots: -# flowviz.show() diff --git a/examples/03_making_adjustments.py b/examples/03_making_adjustments.py deleted file mode 100644 index 0bac6e98b..000000000 --- a/examples/03_making_adjustments.py +++ /dev/null @@ -1,114 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np - -import floris.flow_visualization as flowviz -import floris.layout_visualization as layoutviz -from floris import FlorisModel - - -""" -This example makes changes to the given input file through the script. -First, we plot simulation from the input file as given. Then, we make a series -of changes and generate plots from those simulations. -""" - -# Create the plotting objects using matplotlib -fig, axarr = plt.subplots(2, 3, figsize=(12, 5)) -axarr = axarr.flatten() - -MIN_WS = 1.0 -MAX_WS = 8.0 - -# Initialize FLORIS with the given input file via FlorisModel -fmodel = FlorisModel("inputs/gch.yaml") - - -# Plot a horizatonal slice of the initial configuration -horizontal_plane = fmodel.calculate_horizontal_plane(height=90.0) -flowviz.visualize_cut_plane( - horizontal_plane, - ax=axarr[0], - title="Initial setup", - min_speed=MIN_WS, - max_speed=MAX_WS -) - -# Change the wind speed -horizontal_plane = fmodel.calculate_horizontal_plane(ws=[7.0], height=90.0) -flowviz.visualize_cut_plane( - horizontal_plane, - ax=axarr[1], - title="Wind speed at 7 m/s", - min_speed=MIN_WS, - max_speed=MAX_WS -) - - -# Change the wind shear, reset the wind speed, and plot a vertical slice -fmodel.set(wind_shear=0.2, wind_speeds=[8.0]) -y_plane = fmodel.calculate_y_plane(crossstream_dist=0.0) -flowviz.visualize_cut_plane( - y_plane, - ax=axarr[2], - title="Wind shear at 0.2", - min_speed=MIN_WS, - max_speed=MAX_WS -) - -# # Change the farm layout -N = 3 # Number of turbines per row and per column -X, Y = np.meshgrid( - 5.0 * fmodel.core.farm.rotor_diameters[0,0] * np.arange(0, N, 1), - 5.0 * fmodel.core.farm.rotor_diameters[0,0] * np.arange(0, N, 1), -) -fmodel.set(layout_x=X.flatten(), layout_y=Y.flatten(), wind_directions=[270.0]) -horizontal_plane = fmodel.calculate_horizontal_plane(height=90.0) -flowviz.visualize_cut_plane( - horizontal_plane, - ax=axarr[3], - title="3x3 Farm", - min_speed=MIN_WS, - max_speed=MAX_WS -) -layoutviz.plot_turbine_labels(fmodel, axarr[3], plotting_dict={'color':"w"}) #, backgroundcolor="k") -layoutviz.plot_turbine_rotors(fmodel, axarr[3]) - -# Change the yaw angles and configure the plot differently -yaw_angles = np.zeros((1, N * N)) - -## First row -yaw_angles[:,0] = 30.0 -yaw_angles[:,3] = -30.0 -yaw_angles[:,6] = 30.0 - -## Second row -yaw_angles[:,1] = -30.0 -yaw_angles[:,4] = 30.0 -yaw_angles[:,7] = -30.0 - -horizontal_plane = fmodel.calculate_horizontal_plane(yaw_angles=yaw_angles, height=90.0) -flowviz.visualize_cut_plane( - horizontal_plane, - ax=axarr[4], - title="Yawesome art", - cmap="PuOr", - min_speed=MIN_WS, - max_speed=MAX_WS -) - -layoutviz.plot_turbine_rotors(fmodel, axarr[4], yaw_angles=yaw_angles, color="c") - -# Plot the cross-plane of the 3x3 configuration -cross_plane = fmodel.calculate_cross_plane(yaw_angles=yaw_angles, downstream_dist=610.0) -flowviz.visualize_cut_plane( - cross_plane, - ax=axarr[5], - title="Cross section at 610 m", - min_speed=MIN_WS, - max_speed=MAX_WS -) -axarr[5].invert_xaxis() - - -plt.show() diff --git a/examples/04_sweep_wind_directions.py b/examples/04_sweep_wind_directions.py deleted file mode 100644 index d049a0772..000000000 --- a/examples/04_sweep_wind_directions.py +++ /dev/null @@ -1,62 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel - - -""" -04_sweep_wind_directions - -This example sweeps across wind directions while holding wind speed -constant via an array of constant wind speed - -The power of both turbines for each wind direction is then plotted - -""" - -# Instantiate FLORIS using either the GCH or CC model -fmodel = FlorisModel("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 - -# Define a two turbine farm -D = 126. -layout_x = np.array([0, D*6]) -layout_y = [0, 0] -fmodel.set(layout_x=layout_x, layout_y=layout_y) - -# Sweep wind speeds but keep wind direction fixed -wd_array = np.arange(250,291,1.) -ws_array = 8.0 * np.ones_like(wd_array) -ti_array = 0.06 * np.ones_like(wd_array) -fmodel.set(wind_directions=wd_array, wind_speeds=ws_array, turbulence_intensities=ti_array) - -# Define a matrix of yaw angles to be all 0 -# Note that yaw angles is now specified as a matrix whose dimensions are -# wd/ws/turbine -num_wd = len(wd_array) # Number of wind directions -num_ws = len(ws_array) # Number of wind speeds -n_findex = num_wd # Could be either num_wd or num_ws -num_turbine = len(layout_x) # Number of turbines -yaw_angles = np.zeros((n_findex, num_turbine)) -fmodel.set(yaw_angles=yaw_angles) - -# Calculate -fmodel.run() - -# Collect the turbine powers -turbine_powers = fmodel.get_turbine_powers() / 1E3 # In kW - -# Pull out the power values per turbine -pow_t0 = turbine_powers[:,0].flatten() -pow_t1 = turbine_powers[:,1].flatten() - -# Plot -fig, ax = plt.subplots() -ax.plot(wd_array,pow_t0,color='k',label='Upstream Turbine') -ax.plot(wd_array,pow_t1,color='r',label='Downstream Turbine') -ax.grid(True) -ax.legend() -ax.set_xlabel('Wind Direction (deg)') -ax.set_ylabel('Power (kW)') - -plt.show() diff --git a/examples/05_sweep_wind_speeds.py b/examples/05_sweep_wind_speeds.py deleted file mode 100644 index e5cd07c3a..000000000 --- a/examples/05_sweep_wind_speeds.py +++ /dev/null @@ -1,61 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel - - -""" -05_sweep_wind_speeds - -This example sweeps wind speeds while holding wind direction constant - -The power of both turbines for each wind speed is then plotted - -""" - - -# Instantiate FLORIS using either the GCH or CC model -fmodel = FlorisModel("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 - -# Define a two turbine farm -D = 126. -layout_x = np.array([0, D*6]) -layout_y = [0, 0] -fmodel.set(layout_x=layout_x, layout_y=layout_y) - -# Sweep wind speeds but keep wind direction fixed -ws_array = np.arange(5,25,0.5) -wd_array = 270.0 * np.ones_like(ws_array) -ti_array = 0.06 * np.ones_like(ws_array) -fmodel.set(wind_directions=wd_array,wind_speeds=ws_array, turbulence_intensities=ti_array) - -# Define a matrix of yaw angles to be all 0 -# Note that yaw angles is now specified as a matrix whose dimensions are -# wd/ws/turbine -num_wd = len(wd_array) -num_ws = len(ws_array) -n_findex = num_wd # Could be either num_wd or num_ws -num_turbine = len(layout_x) -yaw_angles = np.zeros((n_findex, num_turbine)) -fmodel.set(yaw_angles=yaw_angles) - -# Calculate -fmodel.run() - -# Collect the turbine powers -turbine_powers = fmodel.get_turbine_powers() / 1E3 # In kW - -# Pull out the power values per turbine -pow_t0 = turbine_powers[:,0].flatten() -pow_t1 = turbine_powers[:,1].flatten() - -# Plot -fig, ax = plt.subplots() -ax.plot(ws_array,pow_t0,color='k',label='Upstream Turbine') -ax.plot(ws_array,pow_t1,color='r',label='Downstream Turbine') -ax.grid(True) -ax.legend() -ax.set_xlabel('Wind Speed (m/s)') -ax.set_ylabel('Power (kW)') -plt.show() diff --git a/examples/06_sweep_wind_conditions.py b/examples/06_sweep_wind_conditions.py deleted file mode 100644 index e9f42487b..000000000 --- a/examples/06_sweep_wind_conditions.py +++ /dev/null @@ -1,92 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel - - -""" -This example demonstrates the vectorized wake calculation for -a set of wind speeds and directions combinations. When given -a list of conditions, FLORIS leverages features of the CPU -to perform chunks of the computations at once rather than -looping over each condition. - -This calculation is performed for a single-row 5 turbine farm. In addition -to plotting the powers of the individual turbines, an energy by turbine -calculation is made and plotted by summing over the wind speed and wind direction -axes of the power matrix returned by get_turbine_powers() - -""" - -# Instantiate FLORIS using either the GCH or CC model -fmodel = FlorisModel("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 -# fmodel = FlorisModel("inputs/cc.yaml") # New CumulativeCurl model - -# Define a 5 turbine farm -D = 126.0 -layout_x = np.array([0, D*6, D*12, D*18, D*24]) -layout_y = [0, 0, 0, 0, 0] -fmodel.set(layout_x=layout_x, layout_y=layout_y) - -# In this case we want to check a grid of wind speed and direction combinations -wind_speeds_to_expand = np.arange(6, 9, 1.0) -wind_directions_to_expand = np.arange(250, 295, 1.0) -num_unique_ws = len(wind_speeds_to_expand) -num_unique_wd = len(wind_directions_to_expand) - -# Create grids to make combinations of ws/wd -wind_speeds_grid, wind_directions_grid = np.meshgrid( - wind_speeds_to_expand, - wind_directions_to_expand -) - -# Flatten the grids back to 1D arrays -ws_array = wind_speeds_grid.flatten() -wd_array = wind_directions_grid.flatten() -turbulence_intensities = 0.06 * np.ones_like(wd_array) - -# Now reinitialize FLORIS -fmodel.set( - wind_speeds=ws_array, - wind_directions=wd_array, - turbulence_intensities=turbulence_intensities -) - -# Define a matrix of yaw angles to be all 0 -# Note that yaw angles is now specified as a matrix whose dimensions are -# (findex, turbine) -num_wd = len(wd_array) -num_ws = len(ws_array) -n_findex = num_wd # Could be either num_wd or num_ws -num_turbine = len(layout_x) -yaw_angles = np.zeros((n_findex, num_turbine)) -fmodel.set(yaw_angles=yaw_angles) - -# Calculate -fmodel.run() - -# Collect the turbine powers -turbine_powers = fmodel.get_turbine_powers() / 1e3 # In kW - -# Show results by ws and wd -fig, axarr = plt.subplots(num_unique_ws, 1, sharex=True, sharey=True, figsize=(6, 10)) -for ws_idx, ws in enumerate(wind_speeds_to_expand): - indices = ws_array == ws - ax = axarr[ws_idx] - for t in range(num_turbine): - ax.plot(wd_array[indices], turbine_powers[indices, t].flatten(), label="T%d" % t) - ax.legend() - ax.grid(True) - ax.set_title("Wind Speed = %.1f" % ws) - ax.set_ylabel("Power (kW)") -ax.set_xlabel("Wind Direction (deg)") - -# Sum across wind speeds and directions to show energy produced by turbine as bar plot -# Sum over wind directions and speeds -energy_by_turbine = np.sum(turbine_powers, axis=0) -fig, ax = plt.subplots() -ax.bar(["T%d" % t for t in range(num_turbine)], energy_by_turbine) -ax.set_title("Energy Produced by Turbine") - -plt.show() diff --git a/examples/07_calc_aep_from_rose.py b/examples/07_calc_aep_from_rose.py deleted file mode 100644 index 135a4c119..000000000 --- a/examples/07_calc_aep_from_rose.py +++ /dev/null @@ -1,69 +0,0 @@ - -import numpy as np -import pandas as pd -from scipy.interpolate import NearestNDInterpolator - -from floris import FlorisModel - - -""" -This example demonstrates how to calculate the Annual Energy Production (AEP) -of a wind farm using wind rose information stored in a .csv file. - -The wind rose information is first loaded, after which we initialize our FlorisModel. -A 3 turbine farm is generated, and then the turbine wakes and powers -are calculated across all the wind directions. Finally, the farm power is -converted to AEP and reported out. -""" - -# Read the windrose information file and display -df_wr = pd.read_csv("inputs/wind_rose.csv") -print("The wind rose dataframe looks as follows: \n\n {} \n".format(df_wr)) - -# Derive the wind directions and speeds we need to evaluate in FLORIS -wd_grid, ws_grid = np.meshgrid( - np.array(df_wr["wd"].unique(), dtype=float), # wind directions - np.array(df_wr["ws"].unique(), dtype=float), # wind speeds - indexing="ij" -) -wind_directions = wd_grid.flatten() -wind_speeds = ws_grid.flatten() -turbulence_intensities = np.ones_like(wind_directions) * 0.06 - -# Format the frequency array into the conventional FLORIS v3 format, which is -# an np.array with shape (n_wind_directions, n_wind_speeds). To avoid having -# to manually derive how the variables are sorted and how to reshape the -# one-dimensional frequency array, we use a nearest neighbor interpolant. This -# ensures the frequency values are mapped appropriately to the new 2D array. -freq_interp = NearestNDInterpolator(df_wr[["wd", "ws"]], df_wr["freq_val"]) -freq = freq_interp(wd_grid, ws_grid).flatten() - -# Normalize the frequency array to sum to exactly 1.0 -freq = freq / np.sum(freq) - -# Load the FLORIS object -fmodel = FlorisModel("inputs/gch.yaml") # GCH model -# fmodel = FlorisModel("inputs/cc.yaml") # CumulativeCurl model - -# Assume a three-turbine wind farm with 5D spacing. We reinitialize the -# floris object and assign the layout, wind speed and wind direction arrays. -D = fmodel.core.farm.rotor_diameters[0] # Rotor diameter for the NREL 5 MW -fmodel.set( - layout_x=[0.0, 5 * D, 10 * D], - layout_y=[0.0, 0.0, 0.0], - wind_directions=wind_directions, - wind_speeds=wind_speeds, - turbulence_intensities=turbulence_intensities, -) -fmodel.run() - -# Compute the AEP using the default settings -aep = fmodel.get_farm_AEP(freq=freq) -print("Farm AEP: {:.3f} GWh".format(aep / 1.0e9)) - -# Finally, we can also compute the AEP while ignoring all wake calculations. -# This can be useful to quantity the annual wake losses in the farm. Such -# calculations can be facilitated by first running with run_no_wake(). -fmodel.run_no_wake() -aep_no_wake = fmodel.get_farm_AEP(freq=freq) -print("Farm AEP (no wakes): {:.3f} GWh".format(aep_no_wake / 1.0e9)) diff --git a/examples/09_compare_farm_power_with_neighbor.py b/examples/09_compare_farm_power_with_neighbor.py deleted file mode 100644 index 59e16f841..000000000 --- a/examples/09_compare_farm_power_with_neighbor.py +++ /dev/null @@ -1,85 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel - - -""" -This example demonstrates how to use turbine_wieghts to define a set of turbines belonging -to a neighboring farm which -impacts the power production of the farm under consideration via wake losses, but whose own -power production is not -considered in farm power / aep production - -The use of neighboring farms in the context of wake steering design is considered in example -examples/10_optimize_yaw_with_neighboring_farm.py -""" - - -# Instantiate FLORIS using either the GCH or CC model -fmodel = FlorisModel("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 - -# Define a 4 turbine farm turbine farm -D = 126. -layout_x = np.array([0, D*6, 0, D*6]) -layout_y = [0, 0, D*3, D*3] -fmodel.set(layout_x=layout_x, layout_y=layout_y) - -# Define a simple wind rose with just 1 wind speed -wd_array = np.arange(0,360,4.) -ws_array = 8.0 * np.ones_like(wd_array) -turbulence_intensities = 0.06 * np.ones_like(wd_array) -fmodel.set( - wind_directions=wd_array, - wind_speeds=ws_array, - turbulence_intensities=turbulence_intensities -) - - -# Calculate -fmodel.run() - -# Collect the farm power -farm_power_base = fmodel.get_farm_power() / 1E3 # In kW - -# Add a neighbor to the east -layout_x = np.array([0, D*6, 0, D*6, D*12, D*15, D*12, D*15]) -layout_y = np.array([0, 0, D*3, D*3, 0, 0, D*3, D*3]) -fmodel.set(layout_x=layout_x, layout_y=layout_y) - -# Define the weights to exclude the neighboring farm from calcuations of power -turbine_weights = np.zeros(len(layout_x), dtype=int) -turbine_weights[0:4] = 1.0 - -# Calculate -fmodel.run() - -# Collect the farm power with the neightbor -farm_power_neighbor = fmodel.get_farm_power(turbine_weights=turbine_weights) / 1E3 # In kW - -# Show the farms -fig, ax = plt.subplots() -ax.scatter( - layout_x[turbine_weights==1], - layout_y[turbine_weights==1], - color='k', - label='Base Farm' -) -ax.scatter( - layout_x[turbine_weights==0], - layout_y[turbine_weights==0], - color='r', - label='Neighboring Farm' -) -ax.legend() - -# Plot the power difference -fig, ax = plt.subplots() -ax.plot(wd_array,farm_power_base,color='k',label='Farm Power (no neighbor)') -ax.plot(wd_array,farm_power_neighbor,color='r',label='Farm Power (neighboring farm due east)') -ax.grid(True) -ax.legend() -ax.set_xlabel('Wind Direction (deg)') -ax.set_ylabel('Power (kW)') -plt.show() diff --git a/examples/12_optimize_yaw.py b/examples/12_optimize_yaw.py deleted file mode 100644 index d631d5437..000000000 --- a/examples/12_optimize_yaw.py +++ /dev/null @@ -1,304 +0,0 @@ - -from time import perf_counter as timerpc - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd - -from floris import FlorisModel -from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR - - -""" -This example demonstrates how to perform a yaw optimization and evaluate the performance -over a full wind rose. - -The beginning of the file contains the definition of several functions used in the main part -of the script. - -Within the main part of the script, we first load the wind rose information. We then initialize -our Floris Interface object. We determine the baseline AEP using the wind rose information, and -then perform the yaw optimization over 72 wind directions with 1 wind speed per direction. The -optimal yaw angles are then used to determine yaw angles across all the wind speeds included in -the wind rose. Lastly, the final AEP is calculated and analysis of the results are -shown in several plots. -""" - -def load_floris(): - # Load the default example floris object - fmodel = FlorisModel("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 - # fmodel = FlorisModel("inputs/cc.yaml") # New CumulativeCurl model - - # Specify wind farm layout and update in the floris object - N = 5 # number of turbines per row and per column - X, Y = np.meshgrid( - 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), - 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), - ) - fmodel.set(layout_x=X.flatten(), layout_y=Y.flatten()) - - return fmodel - - -def load_windrose(): - fn = "inputs/wind_rose.csv" - df = pd.read_csv(fn) - df = df[(df["ws"] < 22)].reset_index(drop=True) # Reduce size - df["freq_val"] = df["freq_val"] / df["freq_val"].sum() # Normalize wind rose frequencies - - return df - - -def calculate_aep(fmodel, df_windrose, column_name="farm_power"): - from scipy.interpolate import NearestNDInterpolator - - # Define columns - nturbs = len(fmodel.layout_x) - yaw_cols = ["yaw_{:03d}".format(ti) for ti in range(nturbs)] - - if "yaw_000" not in df_windrose.columns: - df_windrose[yaw_cols] = 0.0 # Add zeros - - # Derive the wind directions and speeds we need to evaluate in FLORIS - wd_array = np.array(df_windrose["wd"], dtype=float) - ws_array = np.array(df_windrose["ws"], dtype=float) - turbulence_intensities = 0.06 * np.ones_like(wd_array) - yaw_angles = np.array(df_windrose[yaw_cols], dtype=float) - fmodel.set( - wind_directions=wd_array, - wind_speeds=ws_array, - turbulence_intensities=turbulence_intensities, - yaw_angles=yaw_angles - ) - - # Calculate FLORIS for every WD and WS combination and get the farm power - fmodel.run() - farm_power_array = fmodel.get_farm_power() - - # Now map FLORIS solutions to dataframe - interpolant = NearestNDInterpolator( - np.vstack([wd_array, ws_array]).T, - farm_power_array.flatten() - ) - df_windrose[column_name] = interpolant(df_windrose[["wd", "ws"]]) # Save to dataframe - df_windrose[column_name] = df_windrose[column_name].fillna(0.0) # Replace NaNs with 0.0 - - # Calculate AEP in GWh - aep = np.dot(df_windrose["freq_val"], df_windrose[column_name]) * 365 * 24 / 1e9 - - return aep - - -if __name__ == "__main__": - # Load a dataframe containing the wind rose information - df_windrose = load_windrose() - - # Load FLORIS - fmodel = load_floris() - ws_array = 8.0 * np.ones_like(fmodel.core.flow_field.wind_directions) - fmodel.set(wind_speeds=ws_array) - nturbs = len(fmodel.layout_x) - - # First, get baseline AEP, without wake steering - start_time = timerpc() - print(" ") - print("===========================================================") - print("Calculating baseline annual energy production (AEP)...") - aep_bl = calculate_aep(fmodel, df_windrose, "farm_power_baseline") - t = timerpc() - start_time - print("Baseline AEP: {:.3f} GWh. Time spent: {:.1f} s.".format(aep_bl, t)) - print("===========================================================") - print(" ") - - # Now optimize the yaw angles using the Serial Refine method - print("Now starting yaw optimization for the entire wind rose...") - start_time = timerpc() - wd_array = np.arange(0.0, 360.0, 5.0) - ws_array = 8.0 * np.ones_like(wd_array) - turbulence_intensities = 0.06 * np.ones_like(wd_array) - fmodel.set( - wind_directions=wd_array, - wind_speeds=ws_array, - turbulence_intensities=turbulence_intensities, - ) - yaw_opt = YawOptimizationSR( - fmodel=fmodel, - minimum_yaw_angle=0.0, # Allowable yaw angles lower bound - maximum_yaw_angle=20.0, # Allowable yaw angles upper bound - Ny_passes=[5, 4], - exclude_downstream_turbines=True, - ) - - df_opt = yaw_opt.optimize() - end_time = timerpc() - t_tot = end_time - start_time - t_fmodel = yaw_opt.time_spent_in_floris - - print("Optimization finished in {:.2f} seconds.".format(t_tot)) - print(" ") - print(df_opt) - print(" ") - - # Now define how the optimal yaw angles for 8 m/s are applied over the other wind speeds - yaw_angles_opt = np.vstack(df_opt["yaw_angles_opt"]) - yaw_angles_wind_rose = np.zeros((df_windrose.shape[0], nturbs)) - for ii, idx in enumerate(df_windrose.index): - wind_speed = df_windrose.loc[idx, "ws"] - wind_direction = df_windrose.loc[idx, "wd"] - - # Interpolate the optimal yaw angles for this wind direction from df_opt - id_opt = df_opt["wind_direction"] == wind_direction - yaw_opt_full = np.array(df_opt.loc[id_opt, "yaw_angles_opt"])[0] - - # Now decide what to do for different wind speeds - if (wind_speed < 4.0) | (wind_speed > 14.0): - yaw_opt = np.zeros(nturbs) # do nothing for very low/high speeds - elif wind_speed < 6.0: - yaw_opt = yaw_opt_full * (6.0 - wind_speed) / 2.0 # Linear ramp up - elif wind_speed > 12.0: - yaw_opt = yaw_opt_full * (14.0 - wind_speed) / 2.0 # Linear ramp down - else: - yaw_opt = yaw_opt_full # Apply full offsets between 6.0 and 12.0 m/s - - # Save to collective array - yaw_angles_wind_rose[ii, :] = yaw_opt - - # Add optimal and interpolated angles to the wind rose dataframe - yaw_cols = ["yaw_{:03d}".format(ti) for ti in range(nturbs)] - df_windrose[yaw_cols] = yaw_angles_wind_rose - - # Now get AEP with optimized yaw angles - start_time = timerpc() - print("==================================================================") - print("Calculating annual energy production (AEP) with wake steering...") - aep_opt = calculate_aep(fmodel, df_windrose, "farm_power_opt") - aep_uplift = 100.0 * (aep_opt / aep_bl - 1) - t = timerpc() - start_time - print("Optimal AEP: {:.3f} GWh. Time spent: {:.1f} s.".format(aep_opt, t)) - print("Relative AEP uplift by wake steering: {:.3f} %.".format(aep_uplift)) - print("==================================================================") - print(" ") - - # Now calculate helpful variables and then plot wind rose information - df = df_windrose.copy() - df["farm_power_relative"] = ( - df["farm_power_opt"] / df["farm_power_baseline"] - ) - df["farm_energy_baseline"] = df["freq_val"] * df["farm_power_baseline"] - df["farm_energy_opt"] = df["freq_val"] * df["farm_power_opt"] - df["energy_uplift"] = df["farm_energy_opt"] - df["farm_energy_baseline"] - df["rel_energy_uplift"] = df["energy_uplift"] / df["energy_uplift"].sum() - - # Plot power and AEP uplift across wind direction - fig, ax = plt.subplots(nrows=3, sharex=True) - - df_8ms = df[df["ws"] == 8.0].reset_index(drop=True) - pow_uplift = 100 * ( - df_8ms["farm_power_opt"] / df_8ms["farm_power_baseline"] - 1 - ) - ax[0].bar( - x=df_8ms["wd"], - height=pow_uplift, - color="darkgray", - edgecolor="black", - width=4.5, - ) - ax[0].set_ylabel("Power uplift \n at 8 m/s (%)") - ax[0].grid(True) - - dist = df.groupby("wd").sum().reset_index() - ax[1].bar( - x=dist["wd"], - height=100 * dist["rel_energy_uplift"], - color="darkgray", - edgecolor="black", - width=4.5, - ) - ax[1].set_ylabel("Contribution to \n AEP uplift (%)") - ax[1].grid(True) - - ax[2].bar( - x=dist["wd"], - height=dist["freq_val"], - color="darkgray", - edgecolor="black", - width=4.5, - ) - ax[2].set_xlabel("Wind direction (deg)") - ax[2].set_ylabel("Frequency of \n occurrence (-)") - ax[2].grid(True) - plt.tight_layout() - - # Plot power and AEP uplift across wind direction - fig, ax = plt.subplots(nrows=3, sharex=True) - - df_avg = df.groupby("ws").mean().reset_index(drop=False) - mean_power_uplift = 100.0 * (df_avg["farm_power_relative"] - 1.0) - ax[0].bar( - x=df_avg["ws"], - height=mean_power_uplift, - color="darkgray", - edgecolor="black", - width=0.95, - ) - ax[0].set_ylabel("Mean power \n uplift (%)") - ax[0].grid(True) - - dist = df.groupby("ws").sum().reset_index() - ax[1].bar( - x=dist["ws"], - height=100 * dist["rel_energy_uplift"], - color="darkgray", - edgecolor="black", - width=0.95, - ) - ax[1].set_ylabel("Contribution to \n AEP uplift (%)") - ax[1].grid(True) - - ax[2].bar( - x=dist["ws"], - height=dist["freq_val"], - color="darkgray", - edgecolor="black", - width=0.95, - ) - ax[2].set_xlabel("Wind speed (m/s)") - ax[2].set_ylabel("Frequency of \n occurrence (-)") - ax[2].grid(True) - plt.tight_layout() - - # Now plot yaw angle distributions over wind direction up to first three turbines - for ti in range(np.min([nturbs, 3])): - fig, ax = plt.subplots(figsize=(6, 3.5)) - ax.plot( - df_opt["wind_direction"], - yaw_angles_opt[:, ti], - "-o", - color="maroon", - markersize=3, - label="For wind speeds between 6 and 12 m/s", - ) - ax.plot( - df_opt["wind_direction"], - 0.5 * yaw_angles_opt[:, ti], - "-v", - color="dodgerblue", - markersize=3, - label="For wind speeds of 5 and 13 m/s", - ) - ax.plot( - df_opt["wind_direction"], - 0.0 * yaw_angles_opt[:, ti], - "-o", - color="grey", - markersize=3, - label="For wind speeds below 4 and above 14 m/s", - ) - ax.set_ylabel("Assigned yaw offsets (deg)") - ax.set_xlabel("Wind direction (deg)") - ax.set_title("Turbine {:d}".format(ti)) - ax.grid(True) - ax.legend() - plt.tight_layout() - - plt.show() diff --git a/examples/12_optimize_yaw_in_parallel.py b/examples/12_optimize_yaw_in_parallel.py deleted file mode 100644 index 8050a8764..000000000 --- a/examples/12_optimize_yaw_in_parallel.py +++ /dev/null @@ -1,300 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from scipy.interpolate import LinearNDInterpolator - -from floris import FlorisModel, ParallelFlorisModel - - -""" -This example demonstrates how to perform a yaw optimization using parallel computing. -... -""" - -def load_floris(): - # Load the default example floris object - fmodel = FlorisModel("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 - # fmodel = FlorisModel("inputs/cc.yaml") # New CumulativeCurl model - - # Specify wind farm layout and update in the floris object - N = 4 # number of turbines per row and per column - X, Y = np.meshgrid( - 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), - 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), - ) - fmodel.set(layout_x=X.flatten(), layout_y=Y.flatten()) - - return fmodel - - -def load_windrose(): - # Grab a linear interpolant from this wind rose - df = pd.read_csv("inputs/wind_rose.csv") - interp = LinearNDInterpolator(points=df[["wd", "ws"]], values=df["freq_val"], fill_value=0.0) - return df, interp - - -if __name__ == "__main__": - # Parallel options - max_workers = 16 - - # Load a dataframe containing the wind rose information - df_windrose, windrose_interpolant = load_windrose() - - # Load a FLORIS object for AEP calculations - fmodel_aep = load_floris() - - # Define arrays of wd/ws - wind_directions_to_expand = np.arange(0.0, 360.0, 1.0) - wind_speeds_to_expand = np.arange(1.0, 25.0, 1.0) - - # Create grids to make combinations of ws/wd - wind_directions_grid, wind_speeds_grid = np.meshgrid( - wind_directions_to_expand, - wind_speeds_to_expand, - ) - - # Flatten the grids back to 1D arrays - wd_array = wind_directions_grid.flatten() - ws_array = wind_speeds_grid.flatten() - turbulence_intensities = 0.08 * np.ones_like(wd_array) - - fmodel_aep.set( - wind_directions=wd_array, - wind_speeds=ws_array, - turbulence_intensities=turbulence_intensities, - ) - - # Pour this into a parallel computing interface - parallel_interface = "concurrent" - pfmodel_aep = ParallelFlorisModel( - fmodel=fmodel_aep, - max_workers=max_workers, - n_wind_condition_splits=max_workers, - interface=parallel_interface, - print_timings=True, - ) - - # Calculate frequency of occurrence for each bin and normalize sum to 1.0 - freq_grid = windrose_interpolant(wd_array, ws_array) - freq_grid = freq_grid / np.sum(freq_grid) # Normalize to 1.0 - - # Calculate farm power baseline - farm_power_bl = pfmodel_aep.get_farm_power() - aep_bl = np.sum(24 * 365 * np.multiply(farm_power_bl, freq_grid)) - - # Alternatively to above code, we could calculate AEP using - # 'pfmodel_aep.get_farm_AEP(...)' but then we would not have the - # farm power productions, which we use later on for plotting. - - # First, get baseline AEP, without wake steering - print(" ") - print("===========================================================") - print("Calculating baseline annual energy production (AEP)...") - print("Baseline AEP: {:.3f} GWh.".format(aep_bl / 1.0e9)) - print("===========================================================") - print(" ") - - # Load a FLORIS object for yaw optimization - fmodel_opt = load_floris() - - # Define arrays of wd/ws - wind_directions_to_expand = np.arange(0.0, 360.0, 3.0) - wind_speeds_to_expand = np.arange(6.0, 14.0, 2.0) - - # Create grids to make combinations of ws/wd - wind_directions_grid, wind_speeds_grid = np.meshgrid( - wind_directions_to_expand, - wind_speeds_to_expand, - ) - - # Flatten the grids back to 1D arrays - wd_array_opt = wind_directions_grid.flatten() - ws_array_opt = wind_speeds_grid.flatten() - turbulence_intensities = 0.08 * np.ones_like(wd_array_opt) - - fmodel_opt.set( - wind_directions=wd_array_opt, - wind_speeds=ws_array_opt, - turbulence_intensities=turbulence_intensities, - ) - - # Pour this into a parallel computing interface - pfmodel_opt = ParallelFlorisModel( - fmodel=fmodel_opt, - max_workers=max_workers, - n_wind_condition_splits=max_workers, - interface=parallel_interface, - print_timings=True, - ) - - # Now optimize the yaw angles using the Serial Refine method - df_opt = pfmodel_opt.optimize_yaw_angles( - minimum_yaw_angle=-25.0, - maximum_yaw_angle=25.0, - Ny_passes=[5, 4], - exclude_downstream_turbines=False, - ) - - - - # Assume linear ramp up at 5-6 m/s and ramp down at 13-14 m/s, - # add to table for linear interpolant - df_copy_lb = df_opt[df_opt["wind_speed"] == 6.0].copy() - df_copy_ub = df_opt[df_opt["wind_speed"] == 13.0].copy() - df_copy_lb["wind_speed"] = 5.0 - df_copy_ub["wind_speed"] = 14.0 - df_copy_lb["yaw_angles_opt"] *= 0.0 - df_copy_ub["yaw_angles_opt"] *= 0.0 - df_opt = pd.concat([df_copy_lb, df_opt, df_copy_ub], axis=0).reset_index(drop=True) - - # Deal with 360 deg wrapping: solutions at 0 deg are also solutions at 360 deg - df_copy_360deg = df_opt[df_opt["wind_direction"] == 0.0].copy() - df_copy_360deg["wind_direction"] = 360.0 - df_opt = pd.concat([df_opt, df_copy_360deg], axis=0).reset_index(drop=True) - - # Derive linear interpolant from solution space - yaw_angles_interpolant = LinearNDInterpolator( - points=df_opt[["wind_direction", "wind_speed"]], - values=np.vstack(df_opt["yaw_angles_opt"]), - fill_value=0.0, - ) - - # Get optimized AEP, with wake steering - yaw_grid = yaw_angles_interpolant(wd_array, ws_array) - farm_power_opt = pfmodel_aep.get_farm_power(yaw_angles=yaw_grid) - aep_opt = np.sum(24 * 365 * np.multiply(farm_power_opt, freq_grid)) - aep_uplift = 100.0 * (aep_opt / aep_bl - 1) - - # Alternatively to above code, we could calculate AEP using - # 'pfmodel_aep.get_farm_AEP(...)' but then we would not have the - # farm power productions, which we use later on for plotting. - - print(" ") - print("===========================================================") - print("Calculating optimized annual energy production (AEP)...") - print("Optimized AEP: {:.3f} GWh.".format(aep_opt / 1.0e9)) - print("Relative AEP uplift by wake steering: {:.3f} %.".format(aep_uplift)) - print("===========================================================") - print(" ") - - # Now calculate helpful variables and then plot wind rose information - farm_energy_bl = np.multiply(freq_grid, farm_power_bl) - farm_energy_opt = np.multiply(freq_grid, farm_power_opt) - df = pd.DataFrame({ - "wd": wd_array.flatten(), - "ws": ws_array.flatten(), - "freq_val": freq_grid.flatten(), - "farm_power_baseline": farm_power_bl.flatten(), - "farm_power_opt": farm_power_opt.flatten(), - "farm_power_relative": farm_power_opt.flatten() / farm_power_bl.flatten(), - "farm_energy_baseline": farm_energy_bl.flatten(), - "farm_energy_opt": farm_energy_opt.flatten(), - "energy_uplift": (farm_energy_opt - farm_energy_bl).flatten(), - "rel_energy_uplift": farm_energy_opt.flatten() / np.sum(farm_energy_bl) - }) - - # Plot power and AEP uplift across wind direction - wd_step = np.diff(fmodel_aep.core.flow_field.wind_directions)[0] # Useful variable for plotting - fig, ax = plt.subplots(nrows=3, sharex=True) - - df_8ms = df[df["ws"] == 8.0].reset_index(drop=True) - pow_uplift = 100 * ( - df_8ms["farm_power_opt"] / df_8ms["farm_power_baseline"] - 1 - ) - ax[0].bar( - x=df_8ms["wd"], - height=pow_uplift, - color="darkgray", - edgecolor="black", - width=wd_step, - ) - ax[0].set_ylabel("Power uplift \n at 8 m/s (%)") - ax[0].grid(True) - - dist = df.groupby("wd").sum().reset_index() - ax[1].bar( - x=dist["wd"], - height=100 * dist["rel_energy_uplift"], - color="darkgray", - edgecolor="black", - width=wd_step, - ) - ax[1].set_ylabel("Contribution to \n AEP uplift (%)") - ax[1].grid(True) - - ax[2].bar( - x=dist["wd"], - height=dist["freq_val"], - color="darkgray", - edgecolor="black", - width=wd_step, - ) - ax[2].set_xlabel("Wind direction (deg)") - ax[2].set_ylabel("Frequency of \n occurrence (-)") - ax[2].grid(True) - plt.tight_layout() - - # Plot power and AEP uplift across wind direction - fig, ax = plt.subplots(nrows=3, sharex=True) - - df_avg = df.groupby("ws").mean().reset_index(drop=False) - mean_power_uplift = 100.0 * (df_avg["farm_power_relative"] - 1.0) - ax[0].bar( - x=df_avg["ws"], - height=mean_power_uplift, - color="darkgray", - edgecolor="black", - width=0.95, - ) - ax[0].set_ylabel("Mean power \n uplift (%)") - ax[0].grid(True) - - dist = df.groupby("ws").sum().reset_index() - ax[1].bar( - x=dist["ws"], - height=100 * dist["rel_energy_uplift"], - color="darkgray", - edgecolor="black", - width=0.95, - ) - ax[1].set_ylabel("Contribution to \n AEP uplift (%)") - ax[1].grid(True) - - ax[2].bar( - x=dist["ws"], - height=dist["freq_val"], - color="darkgray", - edgecolor="black", - width=0.95, - ) - ax[2].set_xlabel("Wind speed (m/s)") - ax[2].set_ylabel("Frequency of \n occurrence (-)") - ax[2].grid(True) - plt.tight_layout() - - # Now plot yaw angle distributions over wind direction up to first three turbines - wd_plot = np.arange(0.0, 360.001, 1.0) - for tindex in range(np.min([fmodel_aep.core.farm.n_turbines, 3])): - fig, ax = plt.subplots(figsize=(6, 3.5)) - ws_to_plot = [6.0, 9.0, 12.0] - colors = ["maroon", "dodgerblue", "grey"] - styles = ["-o", "-v", "-o"] - for ii, ws in enumerate(ws_to_plot): - ax.plot( - wd_plot, - yaw_angles_interpolant(wd_plot, ws * np.ones_like(wd_plot))[:, tindex], - styles[ii], - color=colors[ii], - markersize=3, - label="For wind speed of {:.1f} m/s".format(ws), - ) - ax.set_ylabel("Assigned yaw offsets (deg)") - ax.set_xlabel("Wind direction (deg)") - ax.set_title("Turbine {:d}".format(tindex)) - ax.grid(True) - ax.legend() - plt.tight_layout() - - plt.show() diff --git a/examples/13_optimize_yaw_with_neighboring_farm.py b/examples/13_optimize_yaw_with_neighboring_farm.py deleted file mode 100644 index 300748341..000000000 --- a/examples/13_optimize_yaw_with_neighboring_farm.py +++ /dev/null @@ -1,318 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from scipy.interpolate import NearestNDInterpolator - -from floris import FlorisModel -from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR - - -""" -This example demonstrates how to perform a yaw optimization and evaluate the performance over a -full wind rose. - -The beginning of the file contains the definition of several functions used in the main part of -the script. - -Within the main part of the script, we first load the wind rose information. -We then initialize our Floris Interface object. We determine the baseline AEP using the -wind rose information, and then perform the yaw optimization over 72 wind directions with 1 -wind speed per direction. The optimal yaw angles are then used to determine yaw angles across -all the wind speeds included in the wind rose. Lastly, the final AEP is calculated and analysis -of the results are shown in several plots. -""" - -def load_floris(): - # Load the default example floris object - fmodel = FlorisModel("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 - # fmodel = FlorisModel("inputs/cc.yaml") # New CumulativeCurl model - - # Specify the full wind farm layout: nominal and neighboring wind farms - X = np.array( - [ - 0., 756., 1512., 2268., 3024., 0., 756., 1512., - 2268., 3024., 0., 756., 1512., 2268., 3024., 0., - 756., 1512., 2268., 3024., 4500., 5264., 6028., 4878., - 0., 756., 1512., 2268., 3024., - ] - ) / 1.5 - Y = np.array( - [ - 0., 0., 0., 0., 0., 504., 504., 504., - 504., 504., 1008., 1008., 1008., 1008., 1008., 1512., - 1512., 1512., 1512., 1512., 4500., 4059., 3618., 5155., - -504., -504., -504., -504., -504., - ] - ) / 1.5 - - # Turbine weights: we want to only optimize for the first 10 turbines - turbine_weights = np.zeros(len(X), dtype=int) - turbine_weights[0:10] = 1.0 - - # Now reinitialize FLORIS layout - fmodel.set(layout_x = X, layout_y = Y) - - # And visualize the floris layout - fig, ax = plt.subplots() - ax.plot(X[turbine_weights == 0], Y[turbine_weights == 0], 'ro', label="Neighboring farms") - ax.plot(X[turbine_weights == 1], Y[turbine_weights == 1], 'go', label='Farm subset') - ax.grid(True) - ax.set_xlabel("x coordinate (m)") - ax.set_ylabel("y coordinate (m)") - ax.legend() - - return fmodel, turbine_weights - - -def load_windrose(): - # Load the wind rose information from an external file - df = pd.read_csv("inputs/wind_rose.csv") - df = df[(df["ws"] < 22)].reset_index(drop=True) # Reduce size - df["freq_val"] = df["freq_val"] / df["freq_val"].sum() # Normalize wind rose frequencies - - # Now put the wind rose information in FLORIS format - ws_windrose = df["ws"].unique() - wd_windrose = df["wd"].unique() - - # Use an interpolant to shape the 'freq_val' vector appropriately. You can - # also use np.reshape(), but NearestNDInterpolator is more fool-proof. - freq_interpolant = NearestNDInterpolator( - df[["ws", "wd"]], df["freq_val"] - ) - freq = freq_interpolant(df["wd"], df["ws"]) - freq_windrose = freq / freq.sum() # Normalize to sum to 1.0 - - ws_windrose = df["ws"] - wd_windrose = df["wd"] - - return ws_windrose, wd_windrose, freq_windrose - - -def optimize_yaw_angles(fmodel_opt): - # Specify turbines to optimize - turbs_to_opt = np.zeros(len(fmodel_opt.layout_x), dtype=bool) - turbs_to_opt[0:10] = True - - # Specify turbine weights - turbine_weights = np.zeros(len(fmodel_opt.layout_x)) - turbine_weights[turbs_to_opt] = 1.0 - - # Specify minimum and maximum allowable yaw angle limits - minimum_yaw_angle = np.zeros( - ( - fmodel_opt.core.flow_field.n_findex, - fmodel_opt.core.farm.n_turbines, - ) - ) - maximum_yaw_angle = np.zeros( - ( - fmodel_opt.core.flow_field.n_findex, - fmodel_opt.core.farm.n_turbines, - ) - ) - maximum_yaw_angle[:, turbs_to_opt] = 30.0 - - yaw_opt = YawOptimizationSR( - fmodel=fmodel_opt, - minimum_yaw_angle=minimum_yaw_angle, - maximum_yaw_angle=maximum_yaw_angle, - turbine_weights=turbine_weights, - Ny_passes=[5], - exclude_downstream_turbines=True, - ) - - df_opt = yaw_opt.optimize() - yaw_angles_opt = yaw_opt.yaw_angles_opt - print("Optimization finished.") - print(" ") - print(df_opt) - print(" ") - - # Now create an interpolant from the optimal yaw angles - def yaw_opt_interpolant(wd, ws): - # Format the wind directions and wind speeds accordingly - wd = np.array(wd, dtype=float) - ws = np.array(ws, dtype=float) - - # Interpolate optimal yaw angles - x = yaw_opt.fmodel.core.flow_field.wind_directions - nturbs = fmodel_opt.core.farm.n_turbines - y = np.stack( - [np.interp(wd, x, yaw_angles_opt[:, ti]) for ti in range(nturbs)], - axis=np.ndim(wd) - ) - - # Now, we want to apply a ramp-up region near cut-in and ramp-down - # region near cut-out wind speed for the yaw offsets. - lim = np.ones(np.shape(wd), dtype=float) # Introduce a multiplication factor - - # Dont do wake steering under 4 m/s or above 14 m/s - lim[(ws <= 4.0) | (ws >= 14.0)] = 0.0 - - # Linear ramp up for the maximum yaw offset between 4.0 and 6.0 m/s - ids = (ws > 4.0) & (ws < 6.0) - lim[ids] = (ws[ids] - 4.0) / 2.0 - - # Linear ramp down for the maximum yaw offset between 12.0 and 14.0 m/s - ids = (ws > 12.0) & (ws < 14.0) - lim[ids] = (ws[ids] - 12.0) / 2.0 - - # Copy over multiplication factor to every turbine - lim = np.expand_dims(lim, axis=np.ndim(wd)).repeat(nturbs, axis=np.ndim(wd)) - lim = lim * 30.0 # These are the limits - - # Finally, Return clipped yaw offsets to the limits - return np.clip(a=y, a_min=0.0, a_max=lim) - - # Return the yaw interpolant - return yaw_opt_interpolant - - -if __name__ == "__main__": - # Load FLORIS: full farm including neighboring wind farms - fmodel, turbine_weights = load_floris() - nturbs = len(fmodel.layout_x) - - # Load a dataframe containing the wind rose information - ws_windrose, wd_windrose, freq_windrose = load_windrose() - ws_windrose = ws_windrose + 0.001 # Deal with 0.0 m/s discrepancy - turbulence_intensities_windrose = 0.06 * np.ones_like(wd_windrose) - - # Create a FLORIS object for AEP calculations - fmodel_aep = fmodel.copy() - fmodel_aep.set( - wind_speeds=ws_windrose, - wind_directions=wd_windrose, - turbulence_intensities=turbulence_intensities_windrose - ) - - # And create a separate FLORIS object for optimization - fmodel_opt = fmodel.copy() - wd_array = np.arange(0.0, 360.0, 3.0) - ws_array = 8.0 * np.ones_like(wd_array) - turbulence_intensities = 0.06 * np.ones_like(wd_array) - fmodel_opt.set( - wind_directions=wd_array, - wind_speeds=ws_array, - turbulence_intensities=turbulence_intensities, - ) - - # First, get baseline AEP, without wake steering - print(" ") - print("===========================================================") - print("Calculating baseline annual energy production (AEP)...") - fmodel_aep.run() - aep_bl_subset = 1.0e-9 * fmodel_aep.get_farm_AEP( - freq=freq_windrose, - turbine_weights=turbine_weights - ) - print("Baseline AEP for subset farm: {:.3f} GWh.".format(aep_bl_subset)) - print("===========================================================") - print(" ") - - # Now optimize the yaw angles using the Serial Refine method. We first - # create a copy of the floris object for optimization purposes and assign - # it the atmospheric conditions for which we want to optimize. Typically, - # the optimal yaw angles are very insensitive to the actual wind speed, - # and hence we only optimize for a single wind speed of 8.0 m/s. We assume - # that the optimal yaw angles at 8.0 m/s are also optimal at other wind - # speeds between 4 and 12 m/s. - print("Now starting yaw optimization for the entire wind rose for farm subset...") - - # In this hypothetical case, we can only control the yaw angles of the - # turbines of the wind farm subset (i.e., the first 10 wind turbines). - # Hence, we constrain the yaw angles of the neighboring wind farms to 0.0. - turbs_to_opt = (turbine_weights > 0.0001) - - # Optimize yaw angles while including neighboring farm - yaw_opt_interpolant = optimize_yaw_angles(fmodel_opt=fmodel_opt) - - # Optimize yaw angles while ignoring neighboring farm - fmodel_opt_subset = fmodel_opt.copy() - fmodel_opt_subset.set( - layout_x = fmodel.layout_x[turbs_to_opt], - layout_y = fmodel.layout_y[turbs_to_opt] - ) - yaw_opt_interpolant_nonb = optimize_yaw_angles(fmodel_opt=fmodel_opt_subset) - - # Use interpolant to get optimal yaw angles for fmodel_aep object - wd = fmodel_aep.core.flow_field.wind_directions - ws = fmodel_aep.core.flow_field.wind_speeds - yaw_angles_opt_AEP = yaw_opt_interpolant(wd, ws) - yaw_angles_opt_nonb_AEP = np.zeros_like(yaw_angles_opt_AEP) # nonb = no neighbor - yaw_angles_opt_nonb_AEP[:, turbs_to_opt] = yaw_opt_interpolant_nonb(wd, ws) - - # Now get AEP with optimized yaw angles - print(" ") - print("===========================================================") - print("Calculating annual energy production with wake steering (AEP)...") - fmodel_aep.set(yaw_angles=yaw_angles_opt_nonb_AEP) - fmodel_aep.run() - aep_opt_subset_nonb = 1.0e-9 * fmodel_aep.get_farm_AEP( - freq=freq_windrose, - turbine_weights=turbine_weights, - ) - fmodel_aep.set(yaw_angles=yaw_angles_opt_AEP) - fmodel_aep.run() - aep_opt_subset = 1.0e-9 * fmodel_aep.get_farm_AEP( - freq=freq_windrose, - turbine_weights=turbine_weights, - ) - uplift_subset_nonb = 100.0 * (aep_opt_subset_nonb - aep_bl_subset) / aep_bl_subset - uplift_subset = 100.0 * (aep_opt_subset - aep_bl_subset) / aep_bl_subset - print( - "Optimized AEP for subset farm (including neighbor farms' wakes): " - f"{aep_opt_subset_nonb:.3f} GWh (+{uplift_subset_nonb:.2f}%)." - ) - print( - "Optimized AEP for subset farm (ignoring neighbor farms' wakes): " - f"{aep_opt_subset:.3f} GWh (+{uplift_subset:.2f}%)." - ) - print("===========================================================") - print(" ") - - # Plot power and AEP uplift across wind direction at wind_speed of 8 m/s - wd = fmodel_opt.core.flow_field.wind_directions - ws = fmodel_opt.core.flow_field.wind_speeds - yaw_angles_opt = yaw_opt_interpolant(wd, ws) - - yaw_angles_opt_nonb = np.zeros_like(yaw_angles_opt) # nonb = no neighbor - yaw_angles_opt_nonb[:, turbs_to_opt] = yaw_opt_interpolant_nonb(wd, ws) - - fmodel_opt = fmodel_opt.copy() - fmodel_opt.set(yaw_angles=np.zeros_like(yaw_angles_opt)) - fmodel_opt.run() - farm_power_bl_subset = fmodel_opt.get_farm_power(turbine_weights).flatten() - - fmodel_opt = fmodel_opt.copy() - fmodel_opt.set(yaw_angles=yaw_angles_opt) - fmodel_opt.run() - farm_power_opt_subset = fmodel_opt.get_farm_power(turbine_weights).flatten() - - fmodel_opt = fmodel_opt.copy() - fmodel_opt.set(yaw_angles=yaw_angles_opt_nonb) - fmodel_opt.run() - farm_power_opt_subset_nonb = fmodel_opt.get_farm_power(turbine_weights).flatten() - - fig, ax = plt.subplots() - ax.bar( - x=fmodel_opt.core.flow_field.wind_directions - 0.65, - height=100.0 * (farm_power_opt_subset / farm_power_bl_subset - 1.0), - edgecolor="black", - width=1.3, - label="Including wake effects of neighboring farms" - ) - ax.bar( - x=fmodel_opt.core.flow_field.wind_directions + 0.65, - height=100.0 * (farm_power_opt_subset_nonb / farm_power_bl_subset - 1.0), - edgecolor="black", - width=1.3, - label="Ignoring neighboring farms" - ) - ax.set_ylabel("Power uplift \n at 8 m/s (%)") - ax.legend() - ax.grid(True) - ax.set_xlabel("Wind direction (deg)") - - plt.show() diff --git a/examples/15_optimize_layout.py b/examples/15_optimize_layout.py deleted file mode 100644 index df0f1d460..000000000 --- a/examples/15_optimize_layout.py +++ /dev/null @@ -1,80 +0,0 @@ - -import os - -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel, WindRose -from floris.optimization.layout_optimization.layout_optimization_scipy import ( - LayoutOptimizationScipy, -) - - -""" -This example shows a simple layout optimization using the python module Scipy. - -A 4 turbine array is optimized such that the layout of the turbine produces the -highest annual energy production (AEP) based on the given wind resource. The turbines -are constrained to a square boundary and a random wind resource is supplied. The results -of the optimization show that the turbines are pushed to the outer corners of the boundary, -which makes sense in order to maximize the energy production by minimizing wake interactions. -""" - -# Initialize the FLORIS interface fi -file_dir = os.path.dirname(os.path.abspath(__file__)) -fmodel = FlorisModel('inputs/gch.yaml') - -# Setup 72 wind directions with a 1 wind speed and frequency distribution -wind_directions = np.arange(0, 360.0, 5.0) -wind_speeds = np.array([8.0]) - -# Shape frequency distribution to match number of wind directions and wind speeds -freq_table = np.zeros((len(wind_directions), len(wind_speeds))) -np.random.seed(1) -freq_table[:,0] = (np.abs(np.sort(np.random.randn(len(wind_directions))))) -freq_table = freq_table / freq_table.sum() - -# Establish a TimeSeries object -wind_rose = WindRose( - wind_directions=wind_directions, - wind_speeds=wind_speeds, - freq_table=freq_table, - ti_table=0.06 -) - -fmodel.set(wind_data=wind_rose) - -# The boundaries for the turbines, specified as vertices -boundaries = [(0.0, 0.0), (0.0, 1000.0), (1000.0, 1000.0), (1000.0, 0.0), (0.0, 0.0)] - -# Set turbine locations to 4 turbines in a rectangle -D = 126.0 # rotor diameter for the NREL 5MW -layout_x = [0, 0, 6 * D, 6 * D] -layout_y = [0, 4 * D, 0, 4 * D] -fmodel.set(layout_x=layout_x, layout_y=layout_y) - -# Setup the optimization problem -layout_opt = LayoutOptimizationScipy(fmodel, boundaries) - -# Run the optimization -sol = layout_opt.optimize() - -# Get the resulting improvement in AEP -print('... calcuating improvement in AEP') -fmodel.run() -base_aep = fmodel.get_farm_AEP() / 1e6 -fmodel.set(layout_x=sol[0], layout_y=sol[1]) -fmodel.run() -opt_aep = fmodel.get_farm_AEP() / 1e6 - -percent_gain = 100 * (opt_aep - base_aep) / base_aep - -# Print and plot the results -print(f'Optimal layout: {sol}') -print( - f'Optimal layout improves AEP by {percent_gain:.1f}% ' - f'from {base_aep:.1f} MWh to {opt_aep:.1f} MWh' -) -layout_opt.plot_layout_opt_results() - -plt.show() diff --git a/examples/16b_heterogeneity_multiple_ws_wd.py b/examples/16b_heterogeneity_multiple_ws_wd.py deleted file mode 100644 index c183c4a26..000000000 --- a/examples/16b_heterogeneity_multiple_ws_wd.py +++ /dev/null @@ -1,76 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel -from floris.flow_visualization import visualize_cut_plane - - -""" -This example showcases the heterogeneous inflow capabilities of FLORIS -when multiple wind speeds and direction are considered. -""" - - -# Define the speed ups of the heterogeneous inflow, and their locations. -# For the 2-dimensional case, this requires x and y locations. -# The speed ups are multipliers of the ambient wind speed. -speed_ups = [[2.0, 1.0, 2.0, 1.0]] -x_locs = [-300.0, -300.0, 2600.0, 2600.0] -y_locs = [ -300.0, 300.0, -300.0, 300.0] - -# Initialize FLORIS with the given input. -# Note the heterogeneous inflow is defined in the input file. -fmodel = FlorisModel("inputs/gch_heterogeneous_inflow.yaml") - -# Set shear to 0.0 to highlight the heterogeneous inflow -fmodel.set( - wind_shear=0.0, - wind_speeds=[8.0], - wind_directions=[270.], - turbulence_intensities=[0.06], - layout_x=[0, 0], - layout_y=[-299., 299.], -) -fmodel.run() -turbine_powers = fmodel.get_turbine_powers().flatten() / 1000. - -# Show the initial results -print('------------------------------------------') -print('Given the speedups and turbine locations, ') -print(' the first turbine has an inflow wind speed') -print(' twice that of the second') -print(' Wind Speed = 8., Wind Direction = 270.') -print(f'T0: {turbine_powers[0]:.1f} kW') -print(f'T1: {turbine_powers[1]:.1f} kW') -print() - -# If the number of conditions in the calculation changes, a new heterogeneous map -# must be provided. -speed_multipliers = [[2.0, 1.0, 2.0, 1.0], [2.0, 1.0, 2.0, 1.0]] # Expand to two wind conditions -heterogenous_inflow_config = { - 'speed_multipliers': speed_multipliers, - 'x': x_locs, - 'y': y_locs, -} -fmodel.set( - wind_directions=[270.0, 275.0], - wind_speeds=[8.0, 8.0], - turbulence_intensities=[0.06, 0.06], - heterogenous_inflow_config=heterogenous_inflow_config -) -fmodel.run() -turbine_powers = np.round(fmodel.get_turbine_powers() / 1000.) -print('With wind directions now set to 270 and 275 deg') -print(f'T0: {turbine_powers[:, 0].flatten()} kW') -print(f'T1: {turbine_powers[:, 1].flatten()} kW') - -# # Uncomment if want to see example of error output -# # Note if we change wind directions to 3 without a matching change to het map we get an error -# print() -# print() -# print('~~ Now forcing an error by not matching wd and het_map') - -# fmodel.set(wind_directions=[270, 275, 280], wind_speeds=3*[8.0]) -# fmodel.run() -# turbine_powers = np.round(fmodel.get_turbine_powers() / 1000.) diff --git a/examples/20_calculate_farm_power_with_uncertainty.py b/examples/20_calculate_farm_power_with_uncertainty.py deleted file mode 100644 index f15313c8f..000000000 --- a/examples/20_calculate_farm_power_with_uncertainty.py +++ /dev/null @@ -1,135 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel, UncertainFlorisModel - - -""" -This example demonstrates how one can create an "UncertainFlorisModel" object, -which adds uncertainty on the inflow wind direction on the FlorisModel -class. The UncertainFlorisModel class is interacted with in the exact same -manner as the FlorisModel class is. This example demonstrates how the -wind farm power production is calculated with and without uncertainty. -Other use cases of UncertainFlorisModel are, e.g., comparing FLORIS to -historical SCADA data and robust optimization. -""" - -# Instantiate FLORIS using either the GCH or CC model -fmodel = FlorisModel("inputs/gch.yaml") # GCH model -ufmodel_3 = UncertainFlorisModel( - "inputs/gch.yaml", verbose=True, wd_std=3 -) -ufmodel_5 = UncertainFlorisModel( - "inputs/gch.yaml", verbose=True, wd_std=5 -) - -# Define a two turbine farm -D = 126.0 -layout_x = np.array([0, D * 6]) -layout_y = [0, 0] -wd_array = np.arange(240.0, 300.0, 1.0) -wind_speeds = 8.0 * np.ones_like(wd_array) -ti_array = 0.06 * np.ones_like(wd_array) -fmodel.set( - layout_x=layout_x, - layout_y=layout_y, - wind_directions=wd_array, - wind_speeds=wind_speeds, - turbulence_intensities=ti_array, -) -ufmodel_3.set( - layout_x=layout_x, - layout_y=layout_y, - wind_directions=wd_array, - wind_speeds=wind_speeds, - turbulence_intensities=ti_array, -) -ufmodel_5.set( - layout_x=layout_x, - layout_y=layout_y, - wind_directions=wd_array, - wind_speeds=wind_speeds, - turbulence_intensities=ti_array, -) - - -# Run both models -fmodel.run() -ufmodel_3.run() -ufmodel_5.run() - -# Collect the nominal and uncertain farm power -turbine_powers_nom = fmodel.get_turbine_powers() / 1e3 -turbine_powers_unc_3 = ufmodel_3.get_turbine_powers() / 1e3 -turbine_powers_unc_5 = ufmodel_5.get_turbine_powers() / 1e3 -farm_powers_nom = fmodel.get_farm_power() / 1e3 -farm_powers_unc_3 = ufmodel_3.get_farm_power() / 1e3 -farm_powers_unc_5 = ufmodel_5.get_farm_power() / 1e3 - -# Plot results -fig, axarr = plt.subplots(1, 3, figsize=(15, 5)) -ax = axarr[0] -ax.plot(wd_array, turbine_powers_nom[:, 0].flatten(), color="k", label="Nominal power") -ax.plot( - wd_array, - turbine_powers_unc_3[:, 0].flatten(), - color="r", - label="Power with uncertainty = 3 deg", -) -ax.plot( - wd_array, turbine_powers_unc_5[:, 0].flatten(), color="m", label="Power with uncertainty = 5deg" -) -ax.grid(True) -ax.legend() -ax.set_xlabel("Wind Direction (deg)") -ax.set_ylabel("Power (kW)") -ax.set_title("Upstream Turbine") - -ax = axarr[1] -ax.plot(wd_array, turbine_powers_nom[:, 1].flatten(), color="k", label="Nominal power") -ax.plot( - wd_array, - turbine_powers_unc_3[:, 1].flatten(), - color="r", - label="Power with uncertainty = 3 deg", -) -ax.plot( - wd_array, - turbine_powers_unc_5[:, 1].flatten(), - color="m", - label="Power with uncertainty = 5 deg", -) -ax.set_title("Downstream Turbine") -ax.grid(True) -ax.legend() -ax.set_xlabel("Wind Direction (deg)") -ax.set_ylabel("Power (kW)") - -ax = axarr[2] -ax.plot(wd_array, farm_powers_nom.flatten(), color="k", label="Nominal farm power") -ax.plot( - wd_array, farm_powers_unc_3.flatten(), color="r", label="Farm power with uncertainty = 3 deg" -) -ax.plot( - wd_array, farm_powers_unc_5.flatten(), color="m", label="Farm power with uncertainty = 5 deg" -) -ax.set_title("Farm Power") -ax.grid(True) -ax.legend() -ax.set_xlabel("Wind Direction (deg)") -ax.set_ylabel("Power (kW)") - -# Compare the AEP calculation -freq = np.ones_like(wd_array) -freq = freq / freq.sum() - -aep_nom = fmodel.get_farm_AEP(freq=freq) -aep_unc_3 = ufmodel_3.get_farm_AEP(freq=freq) -aep_unc_5 = ufmodel_5.get_farm_AEP(freq=freq) - -print(f"AEP without uncertainty {aep_nom}") -print(f"AEP without uncertainty (3 deg) {aep_unc_3} ({100*aep_unc_3/aep_nom:.2f}%)") -print(f"AEP without uncertainty (5 deg) {aep_unc_5} ({100*aep_unc_5/aep_nom:.2f}%)") - - -plt.show() diff --git a/examples/21_demo_time_series.py b/examples/21_demo_time_series.py deleted file mode 100644 index 8afa28f2f..000000000 --- a/examples/21_demo_time_series.py +++ /dev/null @@ -1,66 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel - - -""" -This example demonstrates running FLORIS given a time series -of wind direction and wind speed combinations. -""" - -# Initialize FLORIS to simple 4 turbine farm -fmodel = FlorisModel("inputs/gch.yaml") - -# Convert to a simple two turbine layout -fmodel.set(layout_x=[0, 500.], layout_y=[0., 0.]) - -# Create a fake time history where wind speed steps in the middle while wind direction -# Walks randomly -time = np.arange(0, 120, 10.) # Each time step represents a 10-minute average -ws = np.ones_like(time) * 8. -ws[int(len(ws) / 2):] = 9. -wd = np.ones_like(time) * 270. -turbulence_intensities = np.ones_like(time) * 0.06 - -for idx in range(1, len(time)): - wd[idx] = wd[idx - 1] + np.random.randn() * 2. - - -# Now intiialize FLORIS object to this history using time_series flag -fmodel.set(wind_directions=wd, wind_speeds=ws, turbulence_intensities=turbulence_intensities) - -# Collect the powers -fmodel.run() -turbine_powers = fmodel.get_turbine_powers() / 1000. - -# Show the dimensions -num_turbines = len(fmodel.layout_x) -print( - f'There are {len(time)} time samples, and {num_turbines} turbines and ' - f'so the resulting turbine power matrix has the shape {turbine_powers.shape}.' -) - - -fig, axarr = plt.subplots(3, 1, sharex=True, figsize=(7,8)) - -ax = axarr[0] -ax.plot(time, ws, 'o-') -ax.set_ylabel('Wind Speed (m/s)') -ax.grid(True) - -ax = axarr[1] -ax.plot(time, wd, 'o-') -ax.set_ylabel('Wind Direction (Deg)') -ax.grid(True) - -ax = axarr[2] -for t in range(num_turbines): - ax.plot(time,turbine_powers[:, t], 'o-', label='Turbine %d' % t) -ax.legend() -ax.set_ylabel('Turbine Power (kW)') -ax.set_xlabel('Time (minutes)') -ax.grid(True) - -plt.show() diff --git a/examples/22_get_wind_speed_at_turbines.py b/examples/22_get_wind_speed_at_turbines.py deleted file mode 100644 index 7f15a4100..000000000 --- a/examples/22_get_wind_speed_at_turbines.py +++ /dev/null @@ -1,33 +0,0 @@ - -import numpy as np - -from floris import FlorisModel - - -# Initialize FLORIS with the given input file. -# For basic usage, FlorisModel provides a simplified and expressive -# entry point to the simulation routines. -fmodel = FlorisModel("inputs/gch.yaml") - -# Create a 4-turbine layouts -fmodel.set(layout_x=[0, 0., 500., 500.], layout_y=[0., 300., 0., 300.]) - -# Calculate wake -fmodel.run() - -# Collect the wind speed at all the turbine points -u_points = fmodel.core.flow_field.u - -print('U points is 1 findex x 4 turbines x 3 x 3 points (turbine_grid_points=3)') -print(u_points.shape) - -print('turbine_average_velocities is 1 findex x 4 turbines') -print(fmodel.turbine_average_velocities) - -# Show that one is equivalent to the other following averaging -print( - 'turbine_average_velocities is determined by taking the cube root of mean ' - 'of the cubed value across the points ' -) -print(f'turbine_average_velocities: {fmodel.turbine_average_velocities}') -print(f'Recomputed: {np.cbrt(np.mean(u_points**3, axis=(2,3)))}') diff --git a/examples/31_multi_dimensional_cp_ct_2Hs.py b/examples/31_multi_dimensional_cp_ct_2Hs.py deleted file mode 100644 index 56bb6fc20..000000000 --- a/examples/31_multi_dimensional_cp_ct_2Hs.py +++ /dev/null @@ -1,72 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel - - -""" -This example follows after example 30 but shows the effect of changing the Hs setting. - -NOTE: The multi-dimensional Cp/Ct data used in this example is fictional for the purposes of -facilitating this example. The Cp/Ct values for the different wave conditions are scaled -values of the original Cp/Ct data for the IEA 15MW turbine. -""" - -# Initialize FLORIS with the given input file. -fmodel = FlorisModel("inputs/gch_multi_dim_cp_ct.yaml") - -# Make a second Floris instance with a different setting for Hs. -# Note the multi-cp-ct file (iea_15MW_multi_dim_Tp_Hs.csv) -# for the turbine model iea_15MW_floating_multi_dim_cp_ct.yaml -# Defines Hs at 1 and 5. -# The value in gch_multi_dim_cp_ct.yaml is 3.01 which will map -# to 5 as the nearer value, so we set the other case to 1 -# for contrast. -fmodel_dict_mod = fmodel.core.as_dict() -fmodel_dict_mod['flow_field']['multidim_conditions']['Hs'] = 1.0 -fmodel_hs_1 = FlorisModel(fmodel_dict_mod) - -# Set both cases to 3 turbine layout -fmodel.set(layout_x=[0., 500., 1000.], layout_y=[0., 0., 0.]) -fmodel_hs_1.set(layout_x=[0., 500., 1000.], layout_y=[0., 0., 0.]) - -# Use a sweep of wind speeds -wind_speeds = np.arange(5, 20, 1.0) -wind_directions = 270.0 * np.ones_like(wind_speeds) -turbulence_intensities = 0.06 * np.ones_like(wind_speeds) -fmodel.set( - wind_directions=wind_directions, - wind_speeds=wind_speeds, - turbulence_intensities=turbulence_intensities -) -fmodel_hs_1.set( - wind_directions=wind_directions, - wind_speeds=wind_speeds, - turbulence_intensities=turbulence_intensities -) - -# Calculate wakes with baseline yaw -fmodel.run() -fmodel_hs_1.run() - -# Collect the turbine powers in kW -turbine_powers = fmodel.get_turbine_powers()/1000. -turbine_powers_hs_1 = fmodel_hs_1.get_turbine_powers()/1000. - -# Plot the power in each case and the difference in power -fig, axarr = plt.subplots(1,3,sharex=True,figsize=(12,4)) - -for t_idx in range(3): - ax = axarr[t_idx] - ax.plot(wind_speeds, turbine_powers[:,t_idx], color='k', label='Hs=3.1 (5)') - ax.plot(wind_speeds, turbine_powers_hs_1[:,t_idx], color='r', label='Hs=1.0') - ax.grid(True) - ax.set_xlabel('Wind Speed (m/s)') - ax.set_title(f'Turbine {t_idx}') - -axarr[0].set_ylabel('Power (kW)') -axarr[0].legend() -fig.suptitle('Power of each turbine') - -plt.show() diff --git a/examples/35_sweep_ti.py b/examples/35_sweep_ti.py deleted file mode 100644 index 5bf2ffa34..000000000 --- a/examples/35_sweep_ti.py +++ /dev/null @@ -1,49 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np - -from floris import ( - FlorisModel, - TimeSeries, - WindRose, -) -from floris.utilities import wrap_360 - - -""" -Demonstrate the new behavior in V4 where TI is an array rather than a float. -Set up an array of two turbines and sweep TI while holding wd/ws constant. -Use the TimeSeries object to drive the FLORIS calculations. -""" - - -# Generate a random time series of wind speeds, wind directions and turbulence intensities -N = 50 -wd_array = 270.0 * np.ones(N) -ws_array = 8.0 * np.ones(N) -ti_array = np.linspace(0.03, 0.2, N) - - -# Build the time series -time_series = TimeSeries(wd_array, ws_array, turbulence_intensities=ti_array) - - -# Now set up a FLORIS model and initialize it using the time -fmodel = FlorisModel("inputs/gch.yaml") -fmodel.set(layout_x=[0, 500.0], layout_y=[0.0, 0.0], wind_data=time_series) -fmodel.run() -turbine_power = fmodel.get_turbine_powers() - -fig, axarr = plt.subplots(2, 1, sharex=True, figsize=(6, 6)) -ax = axarr[0] -ax.plot(ti_array*100, turbine_power[:, 0]/1000, color="k") -ax.set_ylabel("Front turbine power [kW]") -ax = axarr[1] -ax.plot(ti_array*100, turbine_power[:, 1]/1000, color="k") -ax.set_ylabel("Rear turbine power [kW]") -ax.set_xlabel("Turbulence intensity [%]") - -for ax in axarr: - ax.grid(True) - -plt.show() diff --git a/examples/40_test_derating.py b/examples/40_test_derating.py deleted file mode 100644 index 7d72252b6..000000000 --- a/examples/40_test_derating.py +++ /dev/null @@ -1,112 +0,0 @@ - -import matplotlib.pyplot as plt -import numpy as np -import yaml - -from floris import FlorisModel - - -""" -Example to test out derating of turbines and mixed derating and yawing. Will be refined before -release. TODO: Demonstrate shutting off turbines also, once developed. -""" - -# Grab model of FLORIS and update to deratable turbines -fmodel = FlorisModel("inputs/gch.yaml") - -with open(str( - fmodel.core.as_dict()["farm"]["turbine_library_path"] / - (fmodel.core.as_dict()["farm"]["turbine_type"][0] + ".yaml") -)) as t: - turbine_type = yaml.safe_load(t) -turbine_type["operation_model"] = "simple-derating" - -# Convert to a simple two turbine layout with derating turbines -fmodel.set(layout_x=[0, 1000.0], layout_y=[0.0, 0.0], turbine_type=[turbine_type]) - -# Set the wind directions and speeds to be constant over n_findex = N time steps -N = 50 -fmodel.set( - wind_directions=270 * np.ones(N), - wind_speeds=10.0 * np.ones(N), - turbulence_intensities=0.06 * np.ones(N) -) -fmodel.run() -turbine_powers_orig = fmodel.get_turbine_powers() - -# Add derating -power_setpoints = np.tile(np.linspace(1, 6e6, N), 2).reshape(2, N).T -fmodel.set(power_setpoints=power_setpoints) -fmodel.run() -turbine_powers_derated = fmodel.get_turbine_powers() - -# Compute available power at downstream turbine -power_setpoints_2 = np.array([np.linspace(1, 6e6, N), np.full(N, None)]).T -fmodel.set(power_setpoints=power_setpoints_2) -fmodel.run() -turbine_powers_avail_ds = fmodel.get_turbine_powers()[:,1] - -# Plot the results -fig, ax = plt.subplots(1, 1) -ax.plot(power_setpoints[:, 0]/1000, turbine_powers_derated[:, 0]/1000, color="C0", label="Upstream") -ax.plot( - power_setpoints[:, 1]/1000, - turbine_powers_derated[:, 1]/1000, - color="C1", - label="Downstream" -) -ax.plot( - power_setpoints[:, 0]/1000, - turbine_powers_orig[:, 0]/1000, - color="C0", - linestyle="dotted", - label="Upstream available" -) -ax.plot( - power_setpoints[:, 1]/1000, - turbine_powers_avail_ds/1000, - color="C1", - linestyle="dotted", label="Downstream available" -) -ax.plot( - power_setpoints[:, 1]/1000, - np.ones(N)*np.max(turbine_type["power_thrust_table"]["power"]), - color="k", - linestyle="dashed", - label="Rated power" -) -ax.grid() -ax.legend() -ax.set_xlim([0, 6e3]) -ax.set_xlabel("Power setpoint (kW)") -ax.set_ylabel("Power produced (kW)") - -# Second example showing mixed model use. -turbine_type["operation_model"] = "mixed" -yaw_angles = np.array([ - [0.0, 0.0], - [0.0, 0.0], - [20.0, 10.0], - [0.0, 10.0], - [20.0, 0.0] -]) -power_setpoints = np.array([ - [None, None], - [2e6, 1e6], - [None, None], - [2e6, None,], - [None, 1e6] -]) -fmodel.set( - wind_directions=270 * np.ones(len(yaw_angles)), - wind_speeds=10.0 * np.ones(len(yaw_angles)), - turbulence_intensities=0.06 * np.ones(len(yaw_angles)), - turbine_type=[turbine_type]*2, - yaw_angles=yaw_angles, - power_setpoints=power_setpoints, -) -fmodel.run() -turbine_powers = fmodel.get_turbine_powers() -print(turbine_powers) - -plt.show() diff --git a/examples/10_opt_yaw_single_ws.py b/examples/examples_control_optimization/001_opt_yaw_single_ws.py similarity index 58% rename from examples/10_opt_yaw_single_ws.py rename to examples/examples_control_optimization/001_opt_yaw_single_ws.py index f33878c9e..533347a78 100644 --- a/examples/10_opt_yaw_single_ws.py +++ b/examples/examples_control_optimization/001_opt_yaw_single_ws.py @@ -1,37 +1,36 @@ +"""Example: Optimize yaw for a single wind speed and multiple wind directions + +Use the serial-refine method to optimize the yaw angles for a 3-turbine wind farm + +""" + import matplotlib.pyplot as plt import numpy as np -from floris import FlorisModel +from floris import FlorisModel, TimeSeries from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR -""" -This example demonstrates how to perform a yaw optimization for multiple wind directions -and 1 wind speed. - -First, we initialize our Floris Interface, and then generate a 3 turbine wind farm. -Next, we create the yaw optimization object `yaw_opt` and perform the optimization using the -SerialRefine method. Finally, we plot the results. -""" - # Load the default example floris object -fmodel = FlorisModel("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 -# fmodel = FlorisModel("inputs/cc.yaml") # New CumulativeCurl model +fmodel = FlorisModel("../inputs/gch.yaml") + +# Define an inflow that +# keeps wind speed and TI constant while sweeping the wind directions +wind_directions = np.arange(0.0, 360.0, 3.0) +time_series = TimeSeries( + wind_directions=wind_directions, + wind_speeds=8.0, + turbulence_intensities=0.06, +) -# Reinitialize as a 3-turbine farm with range of WDs and 1 WS -wd_array = np.arange(0.0, 360.0, 3.0) -ws_array = 8.0 * np.ones_like(wd_array) -turbulence_intensities = 0.06 * np.ones_like(wd_array) +# Reinitialize as a 3-turbine using the above inflow D = 126.0 # Rotor diameter for the NREL 5 MW fmodel.set( layout_x=[0.0, 5 * D, 10 * D], layout_y=[0.0, 0.0, 0.0], - wind_directions=wd_array, - wind_speeds=ws_array, - turbulence_intensities=turbulence_intensities, + wind_data=time_series, ) -print(fmodel.core.farm.rotor_diameters) # Initialize optimizer object and run optimization using the Serial-Refine method yaw_opt = YawOptimizationSR(fmodel) diff --git a/examples/examples_control_optimization/002_opt_yaw_single_ws_uncertain.py b/examples/examples_control_optimization/002_opt_yaw_single_ws_uncertain.py new file mode 100644 index 000000000..4b9ceda1e --- /dev/null +++ b/examples/examples_control_optimization/002_opt_yaw_single_ws_uncertain.py @@ -0,0 +1,112 @@ +"""Example: Optimize yaw for a single wind speed and multiple wind directions. +Compare certain and uncertain results. + +Use the serial-refine method to optimize the yaw angles for a 3-turbine wind farm. In one +case use the FlorisModel without uncertainty and in the other use the UncertainFlorisModel +with a wind direction standard deviation of 3 degrees. Compare the results. + +""" + +import matplotlib.pyplot as plt +import numpy as np + +from floris import ( + FlorisModel, + TimeSeries, + UncertainFlorisModel, +) +from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR + + +# Load the floris model and uncertain floris model +fmodel = FlorisModel("../inputs/gch.yaml") +ufmodel = UncertainFlorisModel("../inputs/gch.yaml", wd_std=3) + + +# Define an inflow that +# keeps wind speed and TI constant while sweeping the wind directions +wind_directions = np.arange(250, 290.0, 1.0) +time_series = TimeSeries( + wind_directions=wind_directions, + wind_speeds=8.0, + turbulence_intensities=0.06, +) + +# Reinitialize as a 3-turbine using the above inflow +D = 126.0 # Rotor diameter for the NREL 5 MW +fmodel.set( + layout_x=[0.0, 5 * D, 10 * D], + layout_y=[0.0, 0.0, 0.0], + wind_data=time_series, +) +ufmodel.set( + layout_x=[0.0, 5 * D, 10 * D], + layout_y=[0.0, 0.0, 0.0], + wind_data=time_series, +) + +# Initialize optimizer object and run optimization using the Serial-Refine method +print("++++++++++CERTAIN++++++++++++") +yaw_opt = YawOptimizationSR(fmodel) +df_opt = yaw_opt.optimize() + +# Repeat with uncertain model +print("++++++++++UNCERTAIN++++++++++++") +yaw_opt_u = YawOptimizationSR(ufmodel) +df_opt_uncertain = yaw_opt_u.optimize() + +# Split out the turbine results +for t in range(3): + df_opt["t%d" % t] = df_opt.yaw_angles_opt.apply(lambda x: x[t]) + df_opt_uncertain["t%d" % t] = df_opt_uncertain.yaw_angles_opt.apply(lambda x: x[t]) + +# Show the yaw and turbine results +fig, axarr = plt.subplots(3, sharex=True, sharey=False, figsize=(15, 8)) + +# Yaw results +for tindex in range(3): + ax = axarr[tindex] + ax.plot( + df_opt.wind_direction, df_opt["t%d" % tindex], label="FlorisModel", color="k", marker="o" + ) + ax.plot( + df_opt_uncertain.wind_direction, + df_opt_uncertain["t%d" % tindex], + label="UncertainFlorisModel", + color="r", + marker="x", + ) + ax.set_ylabel("Yaw Offset (deg") + ax.legend() + ax.grid(True) + + +# Power results +fig, axarr = plt.subplots(1, 2, figsize=(15, 5), sharex=True, sharey=True) +ax = axarr[0] +ax.plot(df_opt.wind_direction, df_opt.farm_power_baseline, color="k", label="Baseline Farm Power") +ax.plot(df_opt.wind_direction, df_opt.farm_power_opt, color="r", label="Optimized Farm Power") +ax.set_ylabel("Power (W)") +ax.set_xlabel("Wind Direction (deg)") +ax.legend() +ax.grid(True) +ax.set_title("Certain") +ax = axarr[1] +ax.plot( + df_opt_uncertain.wind_direction, + df_opt_uncertain.farm_power_baseline, + color="k", + label="Baseline Farm Power", +) +ax.plot( + df_opt_uncertain.wind_direction, + df_opt_uncertain.farm_power_opt, + color="r", + label="Optimized Farm Power", +) +ax.set_xlabel("Wind Direction (deg)") +ax.grid(True) +ax.set_title("Uncertain") + + +plt.show() diff --git a/examples/11_opt_yaw_multiple_ws.py b/examples/examples_control_optimization/003_opt_yaw_multiple_ws.py similarity index 78% rename from examples/11_opt_yaw_multiple_ws.py rename to examples/examples_control_optimization/003_opt_yaw_multiple_ws.py index 0a7d9668a..1a2d7e0a0 100644 --- a/examples/11_opt_yaw_multiple_ws.py +++ b/examples/examples_control_optimization/003_opt_yaw_multiple_ws.py @@ -1,47 +1,39 @@ -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel -from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR - - -""" +"""Example: Optimize yaw for multiple wind directions and multiple wind speeds. This example demonstrates how to perform a yaw optimization for multiple wind directions -and multiple wind speeds. +and multiple wind speeds using the WindRose object First, we initialize our Floris Interface, and then generate a 3 turbine wind farm. Next, we create the yaw optimization object `yaw_opt` and perform the optimization using the SerialRefine method. Finally, we plot the results. """ +import matplotlib.pyplot as plt +import numpy as np + +from floris import FlorisModel, WindRose +from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR + + # Load the default example floris object -fmodel = FlorisModel("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 +fmodel = FlorisModel("../inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 # fmodel = FlorisModel("inputs/cc.yaml") # New CumulativeCurl model -# Define arrays of ws/wd -wind_speeds_to_expand = np.arange(2.0, 18.0, 1.0) -wind_directions_to_expand = np.arange(0.0, 360.0, 3.0) - -# Create grids to make combinations of ws/wd -wind_speeds_grid, wind_directions_grid = np.meshgrid( - wind_speeds_to_expand, - wind_directions_to_expand +# Define a WindRose object with uniform TI and frequency table +wind_rose = WindRose( + wind_directions=np.arange(0.0, 360.0, 3.0), + wind_speeds=np.arange(2.0, 18.0, 1.0), + ti_table=0.06, ) -# Flatten the grids back to 1D arrays -wd_array = wind_directions_grid.flatten() -ws_array = wind_speeds_grid.flatten() -turbulence_intensities = 0.06 * np.ones_like(wd_array) + # Reinitialize as a 3-turbine farm with range of WDs and WSs D = 126.0 # Rotor diameter for the NREL 5 MW fmodel.set( layout_x=[0.0, 5 * D, 10 * D], layout_y=[0.0, 0.0, 0.0], - wind_directions=wd_array, - wind_speeds=ws_array, - turbulence_intensities=turbulence_intensities, + wind_data=wind_rose, ) # Initialize optimizer object and run optimization using the Serial-Refine method @@ -49,7 +41,7 @@ # yaw misalignment that increases the wind farm power production by a negligible # amount. For example, at high wind speeds (e.g., 16 m/s), a turbine might yaw # by a substantial amount to increase the power production by less than 1 W. This -# is typically the result of numerical inprecision of the power coefficient curve, +# is typically the result of numerical imprecision of the power coefficient curve, # which slightly differs for different above-rated wind speeds. The option # verify_convergence therefore refines and validates the yaw angle choices # but has no effect on the predicted power uplift from wake steering. @@ -74,7 +66,7 @@ figsize=(10, 8) ) jj = 0 -for ii, ws in enumerate(np.unique(fmodel.core.flow_field.wind_speeds)): +for ii, ws in enumerate(np.unique(fmodel.wind_speeds)): xi = np.remainder(ii, 4) if ((ii > 0) & (xi == 0)): jj += 1 @@ -104,7 +96,7 @@ figsize=(10, 8) ) jj = 0 -for ii, ws in enumerate(np.unique(fmodel.core.flow_field.wind_speeds)): +for ii, ws in enumerate(np.unique(fmodel.wind_speeds)): xi = np.remainder(ii, 4) if ((ii > 0) & (xi == 0)): jj += 1 diff --git a/examples/examples_control_optimization/004_optimize_yaw_aep.py b/examples/examples_control_optimization/004_optimize_yaw_aep.py new file mode 100644 index 000000000..00269e6fe --- /dev/null +++ b/examples/examples_control_optimization/004_optimize_yaw_aep.py @@ -0,0 +1,156 @@ +"""Example: Optimize yaw and compare AEP + +This example demonstrates how to perform a yaw optimization and evaluate the performance +over a full wind rose. + +The script performs the following steps: + 1. Load a wind rose from a csv file + 2. Calculates the optimal yaw angles for a wind speed of 8 m/s across the directions + 3. Applies the optimal yaw angles to the wind rose and calculates the AEP + +""" + +from time import perf_counter as timerpc + +import matplotlib.pyplot as plt +import numpy as np + +from floris import ( + FlorisModel, + TimeSeries, + WindRose, +) +from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR + + +# Load the wind rose from csv +wind_rose = WindRose.read_csv_long( + "../inputs/wind_rose.csv", wd_col="wd", ws_col="ws", freq_col="freq_val", ti_col_or_value=0.06 +) + +# Load FLORIS +fmodel = FlorisModel("../inputs/gch.yaml") + +# Specify wind farm layout and update in the floris object +N = 2 # number of turbines per row and per column +X, Y = np.meshgrid( + 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), + 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), +) +fmodel.set(layout_x=X.flatten(), layout_y=Y.flatten()) + +# Get the number of turbines +n_turbines = len(fmodel.layout_x) + +# Optimize the yaw angles. This could be done for every wind direction and wind speed +# but in practice it is much faster to optimize only for one speed and infer the rest +# using a rule of thumb +time_series = TimeSeries( + wind_directions=wind_rose.wind_directions, wind_speeds=8.0, turbulence_intensities=0.06 +) +fmodel.set(wind_data=time_series) + +# Get the optimal angles +start_time = timerpc() +yaw_opt = YawOptimizationSR( + fmodel=fmodel, + minimum_yaw_angle=0.0, # Allowable yaw angles lower bound + maximum_yaw_angle=20.0, # Allowable yaw angles upper bound + Ny_passes=[5, 4], + exclude_downstream_turbines=True, +) +df_opt = yaw_opt.optimize() +end_time = timerpc() +t_tot = end_time - start_time +print("Optimization finished in {:.2f} seconds.".format(t_tot)) + + +# Calculate the AEP in the baseline case +fmodel.set(wind_data=wind_rose) +fmodel.run() +farm_power_baseline = fmodel.get_farm_power() +aep_baseline = fmodel.get_farm_AEP() + + +# Now need to apply the optimal yaw angles to the wind rose to get the optimized AEP +# do this by applying a rule of thumb where the optimal yaw is applied between 6 and 12 m/s +# and ramped down to 0 above and below this range + +# Grab wind speeds and wind directions from the fmodel. Note that we do this because the +# yaw angles will need to be n_findex long, and accounting for the fact that some wind +# directions and wind speeds may not be present in the wind rose (0 frequency) and aren't +# included in the fmodel +wind_directions = fmodel.wind_directions +wind_speeds = fmodel.wind_speeds +n_findex = fmodel.n_findex + + +# Now define how the optimal yaw angles for 8 m/s are applied over the other wind speeds +yaw_angles_opt = np.vstack(df_opt["yaw_angles_opt"]) +yaw_angles_wind_rose = np.zeros((n_findex, n_turbines)) +for i in range(n_findex): + wind_speed = wind_speeds[i] + wind_direction = wind_directions[i] + + # Interpolate the optimal yaw angles for this wind direction from df_opt + id_opt = df_opt["wind_direction"] == wind_direction + yaw_opt_full = np.array(df_opt.loc[id_opt, "yaw_angles_opt"])[0] + + # Now decide what to do for different wind speeds + if (wind_speed < 4.0) | (wind_speed > 14.0): + yaw_opt = np.zeros(n_turbines) # do nothing for very low/high speeds + elif wind_speed < 6.0: + yaw_opt = yaw_opt_full * (6.0 - wind_speed) / 2.0 # Linear ramp up + elif wind_speed > 12.0: + yaw_opt = yaw_opt_full * (14.0 - wind_speed) / 2.0 # Linear ramp down + else: + yaw_opt = yaw_opt_full # Apply full offsets between 6.0 and 12.0 m/s + + # Save to collective array + yaw_angles_wind_rose[i, :] = yaw_opt + + +# Now apply the optimal yaw angles and get the AEP +fmodel.set(yaw_angles=yaw_angles_wind_rose) +fmodel.run() +aep_opt = fmodel.get_farm_AEP() +aep_uplift = 100.0 * (aep_opt / aep_baseline - 1) +farm_power_opt = fmodel.get_farm_power() + +print("Baseline AEP: {:.2f} GWh.".format(aep_baseline/1E9)) +print("Optimal AEP: {:.2f} GWh.".format(aep_opt/1E9)) +print("Relative AEP uplift by wake steering: {:.3f} %.".format(aep_uplift)) + +# Use farm_power_baseline, farm_power_opt and wind_data to make a heat map of uplift by +# wind direction and wind speed +wind_directions = wind_rose.wind_directions +wind_speeds = wind_rose.wind_speeds +relative_gain = farm_power_opt - farm_power_baseline + +# Plot the heatmap with wind speeds on x, wind directions on y and relative gain as the color +fig, ax = plt.subplots(figsize=(10, 12)) +cax = ax.imshow(relative_gain, cmap='viridis', aspect='auto') +fig.colorbar(cax, ax=ax, label="Relative gain (%)") + +ax.set_yticks(np.arange(len(wind_directions))) +ax.set_yticklabels(wind_directions) +ax.set_xticks(np.arange(len(wind_speeds))) +ax.set_xticklabels(wind_speeds) +ax.set_ylabel("Wind direction (deg)") +ax.set_xlabel("Wind speed (m/s)") + +# Reduce x and y tick font size +for tick in ax.yaxis.get_major_ticks(): + tick.label1.set_fontsize(8) + +for tick in ax.xaxis.get_major_ticks(): + tick.label1.set_fontsize(8) + +# Set y ticks to be horizontal +for tick in ax.get_yticklabels(): + tick.set_rotation(0) + +ax.set_title("Uplift in farm power by wind direction and wind speed", fontsize=12) + +plt.tight_layout() +plt.show() diff --git a/examples/examples_control_optimization/005_optimize_yaw_aep_parallel.py b/examples/examples_control_optimization/005_optimize_yaw_aep_parallel.py new file mode 100644 index 000000000..17e02412b --- /dev/null +++ b/examples/examples_control_optimization/005_optimize_yaw_aep_parallel.py @@ -0,0 +1,149 @@ +"""Example: Optimize yaw and compare AEP in parallel + +This example demonstrates how to perform a yaw optimization and evaluate the performance +over a full wind rose. The example repeats the steps in 04 except using parallel +optimization and evaluation. + +Note that constraints on parallelized operations mean that some syntax is different and +not all operations are possible. Also, rather passing the ParallelFlorisModel +object to a YawOptimizationSR object, the optimization is performed +directly by member functions + +""" + +from time import perf_counter as timerpc + +import numpy as np + +from floris import ( + FlorisModel, + ParallelFlorisModel, + TimeSeries, + WindRose, +) + + +# When using parallel optimization it is importat the "root" script include this +# if __name__ == "__main__": block to avoid problems +if __name__ == "__main__": + + # Load the wind rose from csv + wind_rose = WindRose.read_csv_long( + "../inputs/wind_rose.csv", wd_col="wd", ws_col="ws", freq_col="freq_val", + ti_col_or_value=0.06 + ) + + # Load FLORIS + fmodel = FlorisModel("../inputs/gch.yaml") + + # Specify wind farm layout and update in the floris object + N = 2 # number of turbines per row and per column + X, Y = np.meshgrid( + 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), + 5.0 * fmodel.core.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), + ) + fmodel.set(layout_x=X.flatten(), layout_y=Y.flatten()) + + # Get the number of turbines + n_turbines = len(fmodel.layout_x) + + # Optimize the yaw angles. This could be done for every wind direction and wind speed + # but in practice it is much faster to optimize only for one speed and infer the rest + # using a rule of thumb + time_series = TimeSeries( + wind_directions=wind_rose.wind_directions, wind_speeds=8.0, turbulence_intensities=0.06 + ) + fmodel.set(wind_data=time_series) + + # Set up the parallel model + parallel_interface = "concurrent" + max_workers = 16 + pfmodel = ParallelFlorisModel( + fmodel=fmodel, + max_workers=max_workers, + n_wind_condition_splits=max_workers, + interface=parallel_interface, + print_timings=True, + ) + + # Get the optimal angles using the parallel interface + start_time = timerpc() + # Now optimize the yaw angles using the Serial Refine method + df_opt = pfmodel.optimize_yaw_angles( + minimum_yaw_angle=0.0, + maximum_yaw_angle=20.0, + Ny_passes=[5, 4], + exclude_downstream_turbines=False, + ) + end_time = timerpc() + t_tot = end_time - start_time + print("Optimization finished in {:.2f} seconds.".format(t_tot)) + + + # Calculate the AEP in the baseline case, using the parallel interface + fmodel.set(wind_data=wind_rose) + pfmodel = ParallelFlorisModel( + fmodel=fmodel, + max_workers=max_workers, + n_wind_condition_splits=max_workers, + interface=parallel_interface, + print_timings=True, + ) + + # Note the pfmodel does not use run() but instead uses the get_farm_power() and get_farm_AEP() + # directly, this is necessary for the parallel interface + aep_baseline = pfmodel.get_farm_AEP(freq=wind_rose.unpack_freq()) + + # Now need to apply the optimal yaw angles to the wind rose to get the optimized AEP + # do this by applying a rule of thumb where the optimal yaw is applied between 6 and 12 m/s + # and ramped down to 0 above and below this range + + # Grab wind speeds and wind directions from the fmodel. Note that we do this because the + # yaw angles will need to be n_findex long, and accounting for the fact that some wind + # directions and wind speeds may not be present in the wind rose (0 frequency) and aren't + # included in the fmodel + wind_directions = fmodel.wind_directions + wind_speeds = fmodel.wind_speeds + n_findex = fmodel.n_findex + + + # Now define how the optimal yaw angles for 8 m/s are applied over the other wind speeds + yaw_angles_opt = np.vstack(df_opt["yaw_angles_opt"]) + yaw_angles_wind_rose = np.zeros((n_findex, n_turbines)) + for i in range(n_findex): + wind_speed = wind_speeds[i] + wind_direction = wind_directions[i] + + # Interpolate the optimal yaw angles for this wind direction from df_opt + id_opt = df_opt["wind_direction"] == wind_direction + yaw_opt_full = np.array(df_opt.loc[id_opt, "yaw_angles_opt"])[0] + + # Now decide what to do for different wind speeds + if (wind_speed < 4.0) | (wind_speed > 14.0): + yaw_opt = np.zeros(n_turbines) # do nothing for very low/high speeds + elif wind_speed < 6.0: + yaw_opt = yaw_opt_full * (6.0 - wind_speed) / 2.0 # Linear ramp up + elif wind_speed > 12.0: + yaw_opt = yaw_opt_full * (14.0 - wind_speed) / 2.0 # Linear ramp down + else: + yaw_opt = yaw_opt_full # Apply full offsets between 6.0 and 12.0 m/s + + # Save to collective array + yaw_angles_wind_rose[i, :] = yaw_opt + + + # Now apply the optimal yaw angles and get the AEP + fmodel.set(yaw_angles=yaw_angles_wind_rose) + pfmodel = ParallelFlorisModel( + fmodel=fmodel, + max_workers=max_workers, + n_wind_condition_splits=max_workers, + interface=parallel_interface, + print_timings=True, + ) + aep_opt = pfmodel.get_farm_AEP(freq=wind_rose.unpack_freq(), yaw_angles=yaw_angles_wind_rose) + aep_uplift = 100.0 * (aep_opt / aep_baseline - 1) + + print("Baseline AEP: {:.2f} GWh.".format(aep_baseline/1E9)) + print("Optimal AEP: {:.2f} GWh.".format(aep_opt/1E9)) + print("Relative AEP uplift by wake steering: {:.3f} %.".format(aep_uplift)) diff --git a/examples/14_compare_yaw_optimizers.py b/examples/examples_control_optimization/006_compare_yaw_optimizers.py similarity index 95% rename from examples/14_compare_yaw_optimizers.py rename to examples/examples_control_optimization/006_compare_yaw_optimizers.py index 4e0fa1d99..e0c39bbba 100644 --- a/examples/14_compare_yaw_optimizers.py +++ b/examples/examples_control_optimization/006_compare_yaw_optimizers.py @@ -1,19 +1,7 @@ -from time import perf_counter as timerpc - -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel -from floris.optimization.yaw_optimization.yaw_optimizer_geometric import ( - YawOptimizationGeometric, -) -from floris.optimization.yaw_optimization.yaw_optimizer_scipy import YawOptimizationScipy -from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR - - -""" -This example compares the SciPy-based yaw optimizer with the new Serial-Refine optimizer. +"""Example: Compare yaw optimizers +This example compares the SciPy-based yaw optimizer with the Serial-Refine optimizer +and geometric optimizer. First, we initialize Floris, and then generate a 3 turbine wind farm. Next, we create two yaw optimization objects, `yaw_opt_sr` and `yaw_opt_scipy` for the @@ -30,9 +18,21 @@ """ +from time import perf_counter as timerpc + +import matplotlib.pyplot as plt +import numpy as np + +from floris import FlorisModel +from floris.optimization.yaw_optimization.yaw_optimizer_geometric import ( + YawOptimizationGeometric, +) +from floris.optimization.yaw_optimization.yaw_optimizer_scipy import YawOptimizationScipy +from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR + + # Load the default example floris object -fmodel = FlorisModel("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 -# fmodel = FlorisModel("inputs/cc.yaml") # New CumulativeCurl model +fmodel = FlorisModel("../inputs/gch.yaml") # Reinitialize as a 3-turbine farm with range of WDs and 1 WS D = 126.0 # Rotor diameter for the NREL 5 MW diff --git a/examples/examples_control_optimization/007_optimize_yaw_with_neighbor_farms.py b/examples/examples_control_optimization/007_optimize_yaw_with_neighbor_farms.py new file mode 100644 index 000000000..04b6b65ba --- /dev/null +++ b/examples/examples_control_optimization/007_optimize_yaw_with_neighbor_farms.py @@ -0,0 +1,317 @@ +"""Example: Optimize yaw with neighbor farm + +This example demonstrates how to optimize the yaw angles of a subset of turbines +in order to maximize the annual energy production (AEP) of a wind farm. In this +case, the wind farm is part of a larger collection of turbines, some of which are +part of a neighboring farm. The optimization is performed in two ways: first by +accounting for the wakes of the neighboring farm (while not including those turbines) +in the optimization as a target of yaw angle changes or including their power +in the objective function. In th second method the neighboring farms are removed +from FLORIS for the optimization. The AEP is then calculated for the optimized +yaw angles (accounting for and not accounting for the neighboring farm) and compared +to the baseline AEP. +""" + + +import matplotlib.pyplot as plt +import numpy as np + +from floris import ( + FlorisModel, + TimeSeries, + WindRose, +) +from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR + + +# Load the wind rose from csv +wind_rose = WindRose.read_csv_long( + "../inputs/wind_rose.csv", wd_col="wd", ws_col="ws", freq_col="freq_val", ti_col_or_value=0.06 +) + +# Load FLORIS +fmodel = FlorisModel("../inputs/gch.yaml") + +# Specify a layout of turbines in which only the first 10 turbines are part +# of the farm to be optimized, while the others belong to a neighboring farm +X = ( + np.array( + [ + 0.0, + 756.0, + 1512.0, + 2268.0, + 3024.0, + 0.0, + 756.0, + 1512.0, + 2268.0, + 3024.0, + 0.0, + 756.0, + 1512.0, + 2268.0, + 3024.0, + 0.0, + 756.0, + 1512.0, + 2268.0, + 3024.0, + 4500.0, + 5264.0, + 6028.0, + 4878.0, + 0.0, + 756.0, + 1512.0, + 2268.0, + 3024.0, + ] + ) + / 1.5 +) +Y = ( + np.array( + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 504.0, + 504.0, + 504.0, + 504.0, + 504.0, + 1008.0, + 1008.0, + 1008.0, + 1008.0, + 1008.0, + 1512.0, + 1512.0, + 1512.0, + 1512.0, + 1512.0, + 4500.0, + 4059.0, + 3618.0, + 5155.0, + -504.0, + -504.0, + -504.0, + -504.0, + -504.0, + ] + ) + / 1.5 +) + +# Turbine weights: we want to only optimize for the first 10 turbines +turbine_weights = np.zeros(len(X), dtype=int) +turbine_weights[0:10] = 1.0 + +# Now reinitialize FLORIS layout +fmodel.set(layout_x=X, layout_y=Y) + +# And visualize the floris layout +fig, ax = plt.subplots() +ax.plot(X[turbine_weights == 0], Y[turbine_weights == 0], "ro", label="Neighboring farms") +ax.plot(X[turbine_weights == 1], Y[turbine_weights == 1], "go", label="Farm subset") +ax.grid(True) +ax.set_xlabel("x coordinate (m)") +ax.set_ylabel("y coordinate (m)") +ax.legend() + +# Indicate turbine 0 in the plot above with an annotation arrow +ax.annotate( + "Turbine 0", + (X[0], Y[0]), + xytext=(X[0] + 100, Y[0] + 100), + arrowprops={'facecolor':"black", 'shrink':0.05}, +) + + +# Optimize the yaw angles. This could be done for every wind direction and wind speed +# but in practice it is much faster to optimize only for one speed and infer the rest +# using a rule of thumb +time_series = TimeSeries( + wind_directions=wind_rose.wind_directions, wind_speeds=8.0, turbulence_intensities=0.06 +) +fmodel.set(wind_data=time_series) + +# CASE 1: Optimize the yaw angles of the included farm while accounting for the +# wake effects of the neighboring farm by using turbine weights + +# It's important here to do two things: +# 1. Exclude the downstream turbines from the power optimization goal via +# turbine_weights +# 2. Prevent the optimizer from changing the yaw angles of the turbines in the +# neighboring farm by limiting the yaw angles min max both to 0 + +# Set the yaw angles max min according to point(2) above +minimum_yaw_angle = np.zeros( + ( + fmodel.n_findex, + fmodel.n_turbines, + ) +) +maximum_yaw_angle = np.zeros( + ( + fmodel.n_findex, + fmodel.n_turbines, + ) +) +maximum_yaw_angle[:, :10] = 30.0 + + +yaw_opt = YawOptimizationSR( + fmodel=fmodel, + minimum_yaw_angle=minimum_yaw_angle, # Allowable yaw angles lower bound + maximum_yaw_angle=maximum_yaw_angle, # Allowable yaw angles upper bound + Ny_passes=[5, 4], + exclude_downstream_turbines=True, + turbine_weights=turbine_weights, +) +df_opt_with_neighbor = yaw_opt.optimize() + +# CASE 2: Repeat the optimization, this time ignoring the wakes of the neighboring farm +# by limiting the FLORIS model to only the turbines in the farm to be optimized +f_model_subset = fmodel.copy() +f_model_subset.set( + layout_x=X[:10], + layout_y=Y[:10], +) +yaw_opt = YawOptimizationSR( + fmodel=f_model_subset, + minimum_yaw_angle=0, # Allowable yaw angles lower bound + maximum_yaw_angle=30, # Allowable yaw angles upper bound + Ny_passes=[5, 4], + exclude_downstream_turbines=True, +) +df_opt_without_neighbor = yaw_opt.optimize() + + +# Calculate the AEP in the baseline case +# Use turbine weights again to only consider the first 10 turbines power +fmodel.set(wind_data=wind_rose) +fmodel.run() +farm_power_baseline = fmodel.get_farm_power(turbine_weights=turbine_weights) +aep_baseline = fmodel.get_farm_AEP(turbine_weights=turbine_weights) + + +# Now need to apply the optimal yaw angles to the wind rose to get the optimized AEP +# do this by applying a rule of thumb where the optimal yaw is applied between 6 and 12 m/s +# and ramped down to 0 above and below this range + +# Grab wind speeds and wind directions from the fmodel. Note that we do this because the +# yaw angles will need to be n_findex long, and accounting for the fact that some wind +# directions and wind speeds may not be present in the wind rose (0 frequency) and aren't +# included in the fmodel +wind_directions = fmodel.wind_directions +wind_speeds = fmodel.wind_speeds +n_findex = fmodel.n_findex + +yaw_angles_wind_rose_with_neighbor = np.zeros((n_findex, fmodel.n_turbines)) +yaw_angles_wind_rose_without_neighbor = np.zeros((n_findex, fmodel.n_turbines)) +for i in range(n_findex): + wind_speed = wind_speeds[i] + wind_direction = wind_directions[i] + + # Interpolate the optimal yaw angles for this wind direction from df_opt + id_opt_with_neighbor = df_opt_with_neighbor["wind_direction"] == wind_direction + id_opt_without_neighbor = df_opt_without_neighbor["wind_direction"] == wind_direction + + # Get the yaw angles for this wind direction + yaw_opt_full_with_neighbor = np.array( + df_opt_with_neighbor.loc[id_opt_with_neighbor, "yaw_angles_opt"] + )[0] + yaw_opt_full_without_neighbor = np.array( + df_opt_without_neighbor.loc[id_opt_without_neighbor, "yaw_angles_opt"] + )[0] + + # Extend the yaw angles from 10 turbine to n_turbine by filling with 0s + # in the case of the removed neighboring farms + yaw_opt_full_without_neighbor = np.concatenate( + (yaw_opt_full_without_neighbor, np.zeros(fmodel.n_turbines - 10)) + ) + + # Now decide what to do for different wind speeds + if (wind_speed < 4.0) | (wind_speed > 14.0): + yaw_opt_with_neighbor = np.zeros(fmodel.n_turbines) # do nothing for very low/high speeds + yaw_opt_without_neighbor = np.zeros( + fmodel.n_turbines + ) # do nothing for very low/high speeds + elif wind_speed < 6.0: + yaw_opt_with_neighbor = ( + yaw_opt_full_with_neighbor * (6.0 - wind_speed) / 2.0 + ) # Linear ramp up + yaw_opt_without_neighbor = ( + yaw_opt_full_without_neighbor * (6.0 - wind_speed) / 2.0 + ) # Linear ramp up + elif wind_speed > 12.0: + yaw_opt_with_neighbor = ( + yaw_opt_full_with_neighbor * (14.0 - wind_speed) / 2.0 + ) # Linear ramp down + yaw_opt_without_neighbor = ( + yaw_opt_full_without_neighbor * (14.0 - wind_speed) / 2.0 + ) # Linear ramp down + else: + yaw_opt_with_neighbor = ( + yaw_opt_full_with_neighbor # Apply full offsets between 6.0 and 12.0 m/s + ) + yaw_opt_without_neighbor = ( + yaw_opt_full_without_neighbor # Apply full offsets between 6.0 and 12.0 m/s + ) + + # Save to collective array + yaw_angles_wind_rose_with_neighbor[i, :] = yaw_opt_with_neighbor + yaw_angles_wind_rose_without_neighbor[i, :] = yaw_opt_without_neighbor + + +# Now apply the optimal yaw angles and get the AEP, first accounting for the neighboring farm +fmodel.set(yaw_angles=yaw_angles_wind_rose_with_neighbor) +fmodel.run() +aep_opt_with_neighbor = fmodel.get_farm_AEP(turbine_weights=turbine_weights) +aep_uplift_with_neighbor = 100.0 * (aep_opt_with_neighbor / aep_baseline - 1) +farm_power_opt_with_neighbor = fmodel.get_farm_power(turbine_weights=turbine_weights) + +# Repeat without accounting for neighboring farm +fmodel.set(yaw_angles=yaw_angles_wind_rose_without_neighbor) +fmodel.run() +aep_opt_without_neighbor = fmodel.get_farm_AEP(turbine_weights=turbine_weights) +aep_uplift_without_neighbor = 100.0 * (aep_opt_without_neighbor / aep_baseline - 1) +farm_power_opt_without_neighbor = fmodel.get_farm_power(turbine_weights=turbine_weights) + +print("Baseline AEP: {:.2f} GWh.".format(aep_baseline / 1e9)) +print( + "Optimal AEP (Not accounting for neighboring farm): {:.2f} GWh.".format( + aep_opt_without_neighbor / 1e9 + ) +) +print( + "Optimal AEP (Accounting for neighboring farm): {:.2f} GWh.".format(aep_opt_with_neighbor / 1e9) +) + +# Plot the optimal yaw angles for turbine 0 with and without accounting for the neighboring farm +yaw_angles_0_with_neighbor = np.vstack(df_opt_with_neighbor["yaw_angles_opt"])[:, 0] +yaw_angles_0_without_neighbor = np.vstack(df_opt_without_neighbor["yaw_angles_opt"])[:, 0] + +fig, ax = plt.subplots() +ax.plot( + df_opt_with_neighbor["wind_direction"], + yaw_angles_0_with_neighbor, + label="Accounting for neighboring farm", +) +ax.plot( + df_opt_without_neighbor["wind_direction"], + yaw_angles_0_without_neighbor, + label="Not accounting for neighboring farm", +) +ax.set_xlabel("Wind direction (deg)") +ax.set_ylabel("Yaw angle (deg)") +ax.legend() +ax.grid(True) +ax.set_title("Optimal yaw angles for turbine 0") + +plt.show() diff --git a/examples/examples_control_types/001_derating_control.py b/examples/examples_control_types/001_derating_control.py new file mode 100644 index 000000000..41bf3ea2a --- /dev/null +++ b/examples/examples_control_types/001_derating_control.py @@ -0,0 +1,95 @@ +"""Example of using the simple-derating control model in FLORIS. + +This example demonstrates how to use the simple-derating control model in FLORIS. +The simple-derating control model allows the user to specify a power setpoint for each turbine +in the farm. The power setpoint is used to derate the turbine power output to be at most the +power setpoint. + +In this example: + +1. A simple two-turbine layout is created. +2. The wind conditions are set to be constant. +3. The power setpoint is varied, and set the same for each turbine +4. The power produced by each turbine is computed and plotted +""" + +import matplotlib.pyplot as plt +import numpy as np + +from floris import FlorisModel + + +fmodel = FlorisModel("../inputs/gch.yaml") + +# Change to the simple-derating model turbine +# (Note this could also be done with the mixed model) +fmodel.set_operation_model("simple-derating") + +# Convert to a simple two turbine layout with derating turbines +fmodel.set(layout_x=[0, 1000.0], layout_y=[0.0, 0.0]) + +# For reference, load the turbine type +turbine_type = fmodel.core.farm.turbine_definitions[0] + +# Set the wind directions and speeds to be constant over n_findex = N time steps +N = 50 +fmodel.set( + wind_directions=270 * np.ones(N), + wind_speeds=10.0 * np.ones(N), + turbulence_intensities=0.06 * np.ones(N), +) +fmodel.run() +turbine_powers_orig = fmodel.get_turbine_powers() + +# Add derating level to both turbines +power_setpoints = np.tile(np.linspace(1, 6e6, N), 2).reshape(2, N).T +fmodel.set(power_setpoints=power_setpoints) +fmodel.run() +turbine_powers_derated = fmodel.get_turbine_powers() + +# Compute available power at downstream turbine +power_setpoints_2 = np.array([np.linspace(1, 6e6, N), np.full(N, None)]).T +fmodel.set(power_setpoints=power_setpoints_2) +fmodel.run() +turbine_powers_avail_ds = fmodel.get_turbine_powers()[:, 1] + +# Plot the results +fig, ax = plt.subplots(1, 1) +ax.plot( + power_setpoints[:, 0] / 1000, turbine_powers_derated[:, 0] / 1000, color="C0", label="Upstream" +) +ax.plot( + power_setpoints[:, 1] / 1000, + turbine_powers_derated[:, 1] / 1000, + color="C1", + label="Downstream", +) +ax.plot( + power_setpoints[:, 0] / 1000, + turbine_powers_orig[:, 0] / 1000, + color="C0", + linestyle="dotted", + label="Upstream available", +) +ax.plot( + power_setpoints[:, 1] / 1000, + turbine_powers_avail_ds / 1000, + color="C1", + linestyle="dotted", + label="Downstream available", +) +ax.plot( + power_setpoints[:, 1] / 1000, + np.ones(N) * np.max(turbine_type["power_thrust_table"]["power"]), + color="k", + linestyle="dashed", + label="Rated power", +) +ax.grid() +ax.legend() +ax.set_xlim([0, 6e3]) +ax.set_xlabel("Power setpoint (kW) [Applied to both turbines]") +ax.set_ylabel("Power produced (kW)") + + +plt.show() diff --git a/examples/41_test_disable_turbines.py b/examples/examples_control_types/002_disable_turbines.py similarity index 81% rename from examples/41_test_disable_turbines.py rename to examples/examples_control_types/002_disable_turbines.py index 9dfb2620b..e8cd4b94c 100644 --- a/examples/41_test_disable_turbines.py +++ b/examples/examples_control_types/002_disable_turbines.py @@ -1,30 +1,24 @@ +"""Example 001: Disable turbines + +This example is adapted from https://github.com/NREL/floris/pull/693 +contributed by Elie Kadoche. + +This example demonstrates the ability of FLORIS to shut down some turbines +during a simulation. +""" import matplotlib.pyplot as plt import numpy as np -import yaml from floris import FlorisModel -""" -Adapted from https://github.com/NREL/floris/pull/693 contributed by Elie Kadoche -This example demonstrates the ability of FLORIS to shut down some turbines -during a simulation. -""" - # Initialize FLORIS -fmodel = FlorisModel("inputs/gch.yaml") +fmodel = FlorisModel("../inputs/gch.yaml") # Change to the mixed model turbine -with open( - str( - fmodel.core.as_dict()["farm"]["turbine_library_path"] - / (fmodel.core.as_dict()["farm"]["turbine_type"][0] + ".yaml") - ) -) as t: - turbine_type = yaml.safe_load(t) -turbine_type["operation_model"] = "mixed" -fmodel.set(turbine_type=[turbine_type]) +# (Note this could also be done with the simple-derating model) +fmodel.set_operation_model("mixed") # Consider a wind farm of 3 aligned wind turbines layout = np.array([[0.0, 0.0], [500.0, 0.0], [1000.0, 0.0]]) diff --git a/examples/examples_control_types/003_setting_yaw_and_disabling.py b/examples/examples_control_types/003_setting_yaw_and_disabling.py new file mode 100644 index 000000000..fb526009f --- /dev/null +++ b/examples/examples_control_types/003_setting_yaw_and_disabling.py @@ -0,0 +1,83 @@ +"""Example: Setting yaw angles and disabling turbine + +This example demonstrates how to set yaw angles and disable turbines in FLORIS. +The yaw angles are set to sweep from -20 to 20 degrees for the upstream-most turbine +and to 0 degrees for the downstream-most turbine(s). A two-turbine case is compared +to a three-turbine case where the middle turbine is disabled making the two cases +functionally equivalent. +""" + +import matplotlib.pyplot as plt +import numpy as np + +from floris import FlorisModel, TimeSeries + + +# Initialize 2 FLORIS models, a two-turbine layout +# and three-turbine layout +fmodel_2 = FlorisModel("../inputs/gch.yaml") +fmodel_3 = FlorisModel("../inputs/gch.yaml") + +# Change to the mixed model turbine +# This example sets both yaw angle and power setpoints +fmodel_2.set_operation_model("mixed") +fmodel_3.set_operation_model("mixed") + +# Set the layouts, f_model_3 has an extra turbine in-between the two +# turbines of f_model_2 +fmodel_2.set(layout_x=[0, 1000.0], layout_y=[0.0, 0.0]) +fmodel_3.set(layout_x=[0, 500.0, 1000.0], layout_y=[0.0, 0.0, 0.0]) + +# Set bo + +# Set both to have constant wind conditions +N = 50 +time_series = TimeSeries( + wind_directions=270.0 * np.ones(N), + wind_speeds = 8., + turbulence_intensities=0.06 + ) +fmodel_2.set(wind_data=time_series) +fmodel_3.set(wind_data=time_series) + +# In both cases, set the yaw angles of the upstream-most turbine +# to sweep from -20 to 20 degrees, while other turbines are set to 0 +upstream_yaw_angles = np.linspace(-20, 20, N) +yaw_angles_2 = np.array([upstream_yaw_angles, np.zeros(N)]).T +yaw_angles_3 = np.array([upstream_yaw_angles, np.zeros(N), np.zeros(N)]).T + +# In the three turbine case, also disable the middle turbine +# Declare a np array of booleans that is Nx3 and whose middle column is True +disable_turbines = np.array([np.zeros(N), np.ones(N), np.zeros(N)]).T.astype(bool) + +# Set the yaw angles for both and disable the middle turbine for the +# three turbine case +fmodel_2.set(yaw_angles=yaw_angles_2) +fmodel_3.set(yaw_angles=yaw_angles_3, disable_turbines=disable_turbines) + +# Run both models +fmodel_2.run() +fmodel_3.run() + +# Collect the turbine powers from both +turbine_powers_2 = fmodel_2.get_turbine_powers() +turbine_powers_3 = fmodel_3.get_turbine_powers() + +# Make a 2-panel plot of the turbine powers. For the three-turbine case, +# only plot the first and last turbine +fig, axarr = plt.subplots(2, 1, sharex=True) +axarr[0].plot(upstream_yaw_angles, turbine_powers_2[:, 0] / 1000, label="Two-Turbine", marker='s') +axarr[0].plot(upstream_yaw_angles, turbine_powers_3[:, 0] / 1000, label="Three-Turbine", marker='.') +axarr[0].set_ylabel("Power (kW)") +axarr[0].legend() +axarr[0].grid(True) +axarr[0].set_title("Upstream Turbine") + +axarr[1].plot(upstream_yaw_angles, turbine_powers_2[:, 1] / 1000, label="Two-Turbine", marker='s') +axarr[1].plot(upstream_yaw_angles, turbine_powers_3[:, 2] / 1000, label="Three-Turbine", marker='.') +axarr[1].set_ylabel("Power (kW)") +axarr[1].legend() +axarr[1].grid(True) +axarr[1].set_title("Downstream-most Turbine") + +plt.show() diff --git a/examples/26_empirical_gauss_velocity_deficit_parameters.py b/examples/examples_emgauss/001_empirical_gauss_velocity_deficit_parameters.py similarity index 57% rename from examples/26_empirical_gauss_velocity_deficit_parameters.py rename to examples/examples_emgauss/001_empirical_gauss_velocity_deficit_parameters.py index a3c43343a..4cdf37bea 100644 --- a/examples/26_empirical_gauss_velocity_deficit_parameters.py +++ b/examples/examples_emgauss/001_empirical_gauss_velocity_deficit_parameters.py @@ -1,3 +1,7 @@ +"""Example: Empirical Gaussian velocity deficit parameters +This example illustrates the main parameters of the Empirical Gaussian +velocity deficit model and their effects on the wind turbine wake. +""" import copy @@ -5,20 +9,16 @@ import numpy as np from floris import FlorisModel -from floris.flow_visualization import plot_rotor_values, visualize_cut_plane +from floris.flow_visualization import visualize_cut_plane -""" -This example illustrates the main parameters of the Empirical Gaussian -velocity deficit model and their effects on the wind turbine wake. -""" - # Options show_flow_cuts = True num_in_row = 5 yaw_angles = np.zeros((1, num_in_row)) + # Define function for visualizing wakes def generate_wake_visualization(fmodel: FlorisModel, title=None): # Using the FlorisModel functions, get 2D slices. @@ -38,7 +38,7 @@ def generate_wake_visualization(fmodel: FlorisModel, title=None): height=horizontal_plane_location, x_bounds=x_bounds, y_bounds=y_bounds, - yaw_angles=yaw_angles + yaw_angles=yaw_angles, ) y_plane = fmodel.calculate_y_plane( x_resolution=200, @@ -46,64 +46,67 @@ def generate_wake_visualization(fmodel: FlorisModel, title=None): crossstream_dist=streamwise_plane_location, x_bounds=x_bounds, z_bounds=z_bounds, - yaw_angles=yaw_angles + yaw_angles=yaw_angles, ) cross_planes = [] for cpl in cross_plane_locations: cross_planes.append( - fmodel.calculate_cross_plane( - y_resolution=100, - z_resolution=100, - downstream_dist=cpl - ) + fmodel.calculate_cross_plane(y_resolution=100, z_resolution=100, downstream_dist=cpl) ) # Create the plots # Cutplane settings - cp_ls = "solid" # line style - cp_lw = 0.5 # line width - cp_clr = "black" # line color + cp_ls = "solid" # line style + cp_lw = 0.5 # line width + cp_clr = "black" # line color fig = plt.figure() fig.set_size_inches(12, 12) # Horizontal profile ax = fig.add_subplot(311) - visualize_cut_plane(horizontal_plane, ax=ax, title="Top-down profile", - min_speed=min_ws, max_speed=max_ws) - ax.plot(x_bounds, [streamwise_plane_location]*2, color=cp_clr, - linewidth=cp_lw, linestyle=cp_ls) + visualize_cut_plane( + horizontal_plane, ax=ax, title="Top-down profile", min_speed=min_ws, max_speed=max_ws + ) + ax.plot( + x_bounds, [streamwise_plane_location] * 2, color=cp_clr, linewidth=cp_lw, linestyle=cp_ls + ) for cpl in cross_plane_locations: - ax.plot([cpl]*2, y_bounds, color=cp_clr, linewidth=cp_lw, - linestyle=cp_ls) + ax.plot([cpl] * 2, y_bounds, color=cp_clr, linewidth=cp_lw, linestyle=cp_ls) ax = fig.add_subplot(312) - visualize_cut_plane(y_plane, ax=ax, title="Streamwise profile", - min_speed=min_ws, max_speed=max_ws) - ax.plot(x_bounds, [horizontal_plane_location]*2, color=cp_clr, - linewidth=cp_lw, linestyle=cp_ls) + visualize_cut_plane( + y_plane, ax=ax, title="Streamwise profile", min_speed=min_ws, max_speed=max_ws + ) + ax.plot( + x_bounds, [horizontal_plane_location] * 2, color=cp_clr, linewidth=cp_lw, linestyle=cp_ls + ) for cpl in cross_plane_locations: - ax.plot([cpl, cpl], z_bounds, color=cp_clr, linewidth=cp_lw, - linestyle=cp_ls) + ax.plot([cpl, cpl], z_bounds, color=cp_clr, linewidth=cp_lw, linestyle=cp_ls) # Spanwise profiles for i, (cp, cpl) in enumerate(zip(cross_planes, cross_plane_locations)): - visualize_cut_plane(cp, ax=fig.add_subplot(3, len(cross_planes), i+7), - title="Loc: {:.0f}m".format(cpl), min_speed=min_ws, - max_speed=max_ws) + visualize_cut_plane( + cp, + ax=fig.add_subplot(3, len(cross_planes), i + 7), + title="Loc: {:.0f}m".format(cpl), + min_speed=min_ws, + max_speed=max_ws, + ) # Add overall figure title if title is not None: fig.suptitle(title, fontsize=16) + ## Main script # Load input yaml and define farm layout -fmodel = FlorisModel("inputs/emgauss.yaml") +fmodel = FlorisModel("../inputs/emgauss.yaml") D = fmodel.core.farm.rotor_diameters[0] fmodel.set( - layout_x=[x*5.0*D for x in range(num_in_row)], - layout_y=[0.0]*num_in_row, + layout_x=[x * 5.0 * D for x in range(num_in_row)], + layout_y=[0.0] * num_in_row, wind_speeds=[8.0], - wind_directions=[270.0] + wind_directions=[270.0], ) # Save dictionary to modify later @@ -113,12 +116,12 @@ def generate_wake_visualization(fmodel: FlorisModel, title=None): fmodel.run() # Look at the powers of each turbine -turbine_powers = fmodel.get_turbine_powers().flatten()/1e6 +turbine_powers = fmodel.get_turbine_powers().flatten() / 1e6 -fig0, ax0 = plt.subplots(1,1) +fig0, ax0 = plt.subplots(1, 1) width = 0.1 nw = -2 -x = np.array(range(num_in_row))+width*nw +x = np.array(range(num_in_row)) + width * nw nw += 1 title = "Original" @@ -131,18 +134,17 @@ def generate_wake_visualization(fmodel: FlorisModel, title=None): # Increase the base recovery rate fmodel_dict_mod = copy.deepcopy(fmodel_dict) -fmodel_dict_mod['wake']['wake_velocity_parameters']['empirical_gauss']\ - ['wake_expansion_rates'] = [0.03, 0.015] +fmodel_dict_mod["wake"]["wake_velocity_parameters"]["empirical_gauss"]["wake_expansion_rates"] = [ + 0.03, + 0.015, +] fmodel = FlorisModel(fmodel_dict_mod) -fmodel.set( - wind_speeds=[8.0], - wind_directions=[270.0] -) +fmodel.set(wind_speeds=[8.0], wind_directions=[270.0]) fmodel.run() -turbine_powers = fmodel.get_turbine_powers().flatten()/1e6 +turbine_powers = fmodel.get_turbine_powers().flatten() / 1e6 -x = np.array(range(num_in_row))+width*nw +x = np.array(range(num_in_row)) + width * nw nw += 1 title = "Increase base recovery" @@ -153,23 +155,19 @@ def generate_wake_visualization(fmodel: FlorisModel, title=None): # Add new expansion rate fmodel_dict_mod = copy.deepcopy(fmodel_dict) -fmodel_dict_mod['wake']['wake_velocity_parameters']['empirical_gauss']\ - ['wake_expansion_rates'] = \ - fmodel_dict['wake']['wake_velocity_parameters']['empirical_gauss']\ - ['wake_expansion_rates'] + [0.0] -fmodel_dict_mod['wake']['wake_velocity_parameters']['empirical_gauss']\ - ['breakpoints_D'] = [5, 10] +fmodel_dict_mod["wake"]["wake_velocity_parameters"]["empirical_gauss"]["wake_expansion_rates"] = ( + fmodel_dict["wake"]["wake_velocity_parameters"]["empirical_gauss"]["wake_expansion_rates"] + + [0.0] +) +fmodel_dict_mod["wake"]["wake_velocity_parameters"]["empirical_gauss"]["breakpoints_D"] = [5, 10] fmodel = FlorisModel(fmodel_dict_mod) -fmodel.set( - wind_speeds=[8.0], - wind_directions=[270.0] -) +fmodel.set(wind_speeds=[8.0], wind_directions=[270.0]) fmodel.run() -turbine_powers = fmodel.get_turbine_powers().flatten()/1e6 +turbine_powers = fmodel.get_turbine_powers().flatten() / 1e6 -x = np.array(range(num_in_row))+width*nw +x = np.array(range(num_in_row)) + width * nw nw += 1 title = "Add rate, change breakpoints" @@ -180,18 +178,14 @@ def generate_wake_visualization(fmodel: FlorisModel, title=None): # Increase the wake-induced mixing gain fmodel_dict_mod = copy.deepcopy(fmodel_dict) -fmodel_dict_mod['wake']['wake_velocity_parameters']['empirical_gauss']\ - ['mixing_gain_velocity'] = 3.0 +fmodel_dict_mod["wake"]["wake_velocity_parameters"]["empirical_gauss"]["mixing_gain_velocity"] = 3.0 fmodel = FlorisModel(fmodel_dict_mod) -fmodel.set( - wind_speeds=[8.0], - wind_directions=[270.0] -) +fmodel.set(wind_speeds=[8.0], wind_directions=[270.0]) fmodel.run() -turbine_powers = fmodel.get_turbine_powers().flatten()/1e6 +turbine_powers = fmodel.get_turbine_powers().flatten() / 1e6 -x = np.array(range(num_in_row))+width*nw +x = np.array(range(num_in_row)) + width * nw nw += 1 title = "Increase mixing gain" diff --git a/examples/27_empirical_gauss_deflection_parameters.py b/examples/examples_emgauss/002_empirical_gauss_deflection_parameters.py similarity index 98% rename from examples/27_empirical_gauss_deflection_parameters.py rename to examples/examples_emgauss/002_empirical_gauss_deflection_parameters.py index 79bdee9f8..b945ad8dc 100644 --- a/examples/27_empirical_gauss_deflection_parameters.py +++ b/examples/examples_emgauss/002_empirical_gauss_deflection_parameters.py @@ -1,3 +1,9 @@ +"""Example: Empirical Gaussian deflection parameters + +This example illustrates the main parameters of the Empirical Gaussian +deflection model and their effects on the wind turbine wake. +""" + import copy @@ -8,11 +14,6 @@ from floris.flow_visualization import plot_rotor_values, visualize_cut_plane -""" -This example illustrates the main parameters of the Empirical Gaussian -deflection model and their effects on the wind turbine wake. -""" - # Initialize FLORIS with the given input file. # For basic usage, FlorisModel provides a simplified and expressive # entry point to the simulation routines. @@ -105,7 +106,7 @@ def generate_wake_visualization(fmodel, title=None): ## Main script # Load input yaml and define farm layout -fmodel = FlorisModel("inputs/emgauss.yaml") +fmodel = FlorisModel("../inputs/emgauss.yaml") D = fmodel.core.farm.rotor_diameters[0] fmodel.set( layout_x=[x*5.0*D for x in range(num_in_row)], diff --git a/examples/25_tilt_driven_vertical_wake_deflection.py b/examples/examples_emgauss/003_tilt_driven_vertical_wake_deflection.py similarity index 70% rename from examples/25_tilt_driven_vertical_wake_deflection.py rename to examples/examples_emgauss/003_tilt_driven_vertical_wake_deflection.py index b8d6ffbf5..88049cc7f 100644 --- a/examples/25_tilt_driven_vertical_wake_deflection.py +++ b/examples/examples_emgauss/003_tilt_driven_vertical_wake_deflection.py @@ -1,3 +1,10 @@ +"""Example: Tilt-driven vertical wake deflection +This example demonstrates vertical wake deflections due to the tilt angle when running +with the Empirical Gauss model. Note that only the Empirical Gauss model implements +vertical deflections at this time. Also be aware that this example uses a potentially +unrealistic tilt angle, 15 degrees, to highlight the wake deflection. Moreover, the magnitude +of vertical deflections due to tilt has not been validated. +""" import matplotlib.pyplot as plt import numpy as np @@ -6,19 +13,11 @@ from floris.flow_visualization import visualize_cut_plane -""" -This example demonstrates vertical wake deflections due to the tilt angle when running -with the Empirical Gauss model. Note that only the Empirical Gauss model implements -vertical deflections at this time. Also be aware that this example uses a potentially -unrealistic tilt angle, 15 degrees, to highlight the wake deflection. Moreover, the magnitude -of vertical deflections due to tilt has not been validated. -""" - # Initialize two FLORIS objects: one with 5 degrees of tilt (fixed across all # wind speeds) and one with 15 degrees of tilt (fixed across all wind speeds). -fmodel_5 = FlorisModel("inputs_floating/emgauss_floating_fixedtilt5.yaml") -fmodel_15 = FlorisModel("inputs_floating/emgauss_floating_fixedtilt15.yaml") +fmodel_5 = FlorisModel("../inputs_floating/emgauss_floating_fixedtilt5.yaml") +fmodel_15 = FlorisModel("../inputs_floating/emgauss_floating_fixedtilt15.yaml") D = fmodel_5.core.farm.rotor_diameters[0] @@ -30,14 +29,14 @@ z_bounds = [0.001, 500] cross_plane_locations = [10, 1200, 2500] -horizontal_plane_location=90.0 -streamwise_plane_location=0.0 +horizontal_plane_location = 90.0 +streamwise_plane_location = 0.0 # Create the plots # Cutplane settings -cp_ls = "solid" # line style -cp_lw = 0.5 # line width -cp_clr = "black" # line color +cp_ls = "solid" # line style +cp_lw = 0.5 # line width +cp_clr = "black" # line color min_ws = 4 max_ws = 10 fig = plt.figure() @@ -47,18 +46,17 @@ # Calculate wakes, powers, plot for i, (fmodel, tilt) in enumerate(zip([fmodel_5, fmodel_15], [5, 15])): - # Farm layout and wind conditions fmodel.set( layout_x=[x * 5.0 * D for x in range(num_in_row)], - layout_y=[0.0]*num_in_row, + layout_y=[0.0] * num_in_row, wind_speeds=[8.0], - wind_directions=[270.0] + wind_directions=[270.0], ) # Flow solve and power computation fmodel.run() - powers[i,:] = fmodel.get_turbine_powers().flatten() + powers[i, :] = fmodel.get_turbine_powers().flatten() # Compute flow slices y_plane = fmodel.calculate_y_plane( @@ -66,13 +64,15 @@ z_resolution=100, crossstream_dist=streamwise_plane_location, x_bounds=x_bounds, - z_bounds=z_bounds + z_bounds=z_bounds, ) # Horizontal profile - ax = fig.add_subplot(2, 1, i+1) + ax = fig.add_subplot(2, 1, i + 1) visualize_cut_plane(y_plane, ax=ax, min_speed=min_ws, max_speed=max_ws) - ax.plot(x_bounds, [horizontal_plane_location]*2, color=cp_clr, linewidth=cp_lw, linestyle=cp_ls) + ax.plot( + x_bounds, [horizontal_plane_location] * 2, color=cp_clr, linewidth=cp_lw, linestyle=cp_ls + ) ax.set_title("Tilt angle: {0} degrees".format(tilt)) fig = plt.figure() @@ -80,8 +80,8 @@ ax = fig.add_subplot(1, 1, 1) x_locs = np.arange(num_in_row) width = 0.25 -ax.bar(x_locs-width/2, powers[0,:]/1000, width=width, label="5 degree tilt") -ax.bar(x_locs+width/2, powers[1,:]/1000, width=width, label="15 degree tilt") +ax.bar(x_locs - width / 2, powers[0, :] / 1000, width=width, label="5 degree tilt") +ax.bar(x_locs + width / 2, powers[1, :] / 1000, width=width, label="15 degree tilt") ax.set_xticks(x_locs) ax.set_xticklabels(["T{0}".format(i) for i in range(num_in_row)]) ax.set_xlabel("Turbine number in row") diff --git a/examples/24_floating_turbine_models.py b/examples/examples_floating/001_floating_turbine_models.py similarity index 53% rename from examples/24_floating_turbine_models.py rename to examples/examples_floating/001_floating_turbine_models.py index 76822a76f..75936b09a 100644 --- a/examples/24_floating_turbine_models.py +++ b/examples/examples_floating/001_floating_turbine_models.py @@ -1,11 +1,4 @@ - -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel - - -""" +"""Example: Floating turbines This example demonstrates the impact of floating on turbine power and thrust (not wake behavior). A floating turbine in FLORIS is defined by including a `floating_tilt_table` in the turbine input yaml which sets the steady tilt angle of the turbine based on wind speed. This tilt angle @@ -31,32 +24,36 @@ tilt does not scale cp/ct) """ + +import matplotlib.pyplot as plt +import numpy as np + +from floris import FlorisModel, TimeSeries + + # Create the Floris instances -fmodel_fixed = FlorisModel("inputs_floating/gch_fixed.yaml") -fmodel_floating = FlorisModel("inputs_floating/gch_floating.yaml") -fmodel_floating_defined_floating = FlorisModel("inputs_floating/gch_floating_defined_floating.yaml") - -# Calculate across wind speeds -ws_array = np.arange(3., 25., 1.) -wd_array = 270.0 * np.ones_like(ws_array) -ti_array = 0.06 * np.ones_like(ws_array) -fmodel_fixed.set(wind_speeds=ws_array, wind_directions=wd_array, turbulence_intensities=ti_array) -fmodel_floating.set(wind_speeds=ws_array, wind_directions=wd_array, turbulence_intensities=ti_array) -fmodel_floating_defined_floating.set( - wind_speeds=ws_array, - wind_directions=wd_array, - turbulence_intensities=ti_array +fmodel_fixed = FlorisModel("../inputs_floating/gch_fixed.yaml") +fmodel_floating = FlorisModel("../inputs_floating/gch_floating.yaml") +fmodel_floating_defined_floating = FlorisModel( + "../inputs_floating/gch_floating_defined_floating.yaml" ) +# Calculate across wind speeds, while holding win directions constant +ws_array = np.arange(3.0, 25.0, 1.0) +time_series = TimeSeries(wind_directions=270.0, wind_speeds=ws_array, turbulence_intensities=0.06) +fmodel_fixed.set(wind_data=time_series) +fmodel_floating.set(wind_data=time_series) +fmodel_floating_defined_floating.set(wind_data=time_series) + fmodel_fixed.run() fmodel_floating.run() fmodel_floating_defined_floating.run() # Grab power -power_fixed = fmodel_fixed.get_turbine_powers().flatten()/1000. -power_floating = fmodel_floating.get_turbine_powers().flatten()/1000. +power_fixed = fmodel_fixed.get_turbine_powers().flatten() / 1000.0 +power_floating = fmodel_floating.get_turbine_powers().flatten() / 1000.0 power_floating_defined_floating = ( - fmodel_floating_defined_floating.get_turbine_powers().flatten()/1000. + fmodel_floating_defined_floating.get_turbine_powers().flatten() / 1000.0 ) # Grab Ct @@ -68,62 +65,80 @@ # Grab turbine tilt angles eff_vels = fmodel_fixed.turbine_average_velocities -tilt_angles_fixed = np.squeeze( - fmodel_fixed.core.farm.calculate_tilt_for_eff_velocities(eff_vels) - ) +tilt_angles_fixed = np.squeeze(fmodel_fixed.core.farm.calculate_tilt_for_eff_velocities(eff_vels)) eff_vels = fmodel_floating.turbine_average_velocities tilt_angles_floating = np.squeeze( fmodel_floating.core.farm.calculate_tilt_for_eff_velocities(eff_vels) - ) +) eff_vels = fmodel_floating_defined_floating.turbine_average_velocities tilt_angles_floating_defined_floating = np.squeeze( fmodel_floating_defined_floating.core.farm.calculate_tilt_for_eff_velocities(eff_vels) - ) +) # Plot results -fig, axarr = plt.subplots(4,1, figsize=(8,10), sharex=True) +fig, axarr = plt.subplots(4, 1, figsize=(8, 10), sharex=True) ax = axarr[0] -ax.plot(ws_array, tilt_angles_fixed, color='k',lw=2,label='Fixed Bottom') -ax.plot(ws_array, tilt_angles_floating, color='b',label='Floating') -ax.plot(ws_array, tilt_angles_floating_defined_floating, color='m',ls='--', - label='Floating (cp/ct not scaled by tilt)') +ax.plot(ws_array, tilt_angles_fixed, color="k", lw=2, label="Fixed Bottom") +ax.plot(ws_array, tilt_angles_floating, color="b", label="Floating") +ax.plot( + ws_array, + tilt_angles_floating_defined_floating, + color="m", + ls="--", + label="Floating (cp/ct not scaled by tilt)", +) ax.grid(True) ax.legend() -ax.set_title('Tilt angle (deg)') -ax.set_ylabel('Tlit (deg)') +ax.set_title("Tilt angle (deg)") +ax.set_ylabel("Tlit (deg)") ax = axarr[1] -ax.plot(ws_array, power_fixed, color='k',lw=2,label='Fixed Bottom') -ax.plot(ws_array, power_floating, color='b',label='Floating') -ax.plot(ws_array, power_floating_defined_floating, color='m',ls='--', - label='Floating (cp/ct not scaled by tilt)') +ax.plot(ws_array, power_fixed, color="k", lw=2, label="Fixed Bottom") +ax.plot(ws_array, power_floating, color="b", label="Floating") +ax.plot( + ws_array, + power_floating_defined_floating, + color="m", + ls="--", + label="Floating (cp/ct not scaled by tilt)", +) ax.grid(True) ax.legend() -ax.set_title('Power') -ax.set_ylabel('Power (kW)') +ax.set_title("Power") +ax.set_ylabel("Power (kW)") ax = axarr[2] # ax.plot(ws_array, power_fixed, color='k',label='Fixed Bottom') -ax.plot(ws_array, power_floating - power_fixed, color='b',label='Floating') -ax.plot(ws_array, power_floating_defined_floating - power_fixed, color='m',ls='--', - label='Floating (cp/ct not scaled by tilt)') +ax.plot(ws_array, power_floating - power_fixed, color="b", label="Floating") +ax.plot( + ws_array, + power_floating_defined_floating - power_fixed, + color="m", + ls="--", + label="Floating (cp/ct not scaled by tilt)", +) ax.grid(True) ax.legend() -ax.set_title('Difference from fixed bottom power') -ax.set_ylabel('Power (kW)') +ax.set_title("Difference from fixed bottom power") +ax.set_ylabel("Power (kW)") ax = axarr[3] -ax.plot(ws_array, ct_fixed, color='k',lw=2,label='Fixed Bottom') -ax.plot(ws_array, ct_floating, color='b',label='Floating') -ax.plot(ws_array, ct_floating_defined_floating, color='m',ls='--', - label='Floating (cp/ct not scaled by tilt)') +ax.plot(ws_array, ct_fixed, color="k", lw=2, label="Fixed Bottom") +ax.plot(ws_array, ct_floating, color="b", label="Floating") +ax.plot( + ws_array, + ct_floating_defined_floating, + color="m", + ls="--", + label="Floating (cp/ct not scaled by tilt)", +) ax.grid(True) ax.legend() -ax.set_title('Coefficient of thrust') -ax.set_ylabel('Ct (-)') +ax.set_title("Coefficient of thrust") +ax.set_ylabel("Ct (-)") plt.show() diff --git a/examples/29_floating_vs_fixedbottom_farm.py b/examples/examples_floating/002_floating_vs_fixedbottom_farm.py similarity index 82% rename from examples/29_floating_vs_fixedbottom_farm.py rename to examples/examples_floating/002_floating_vs_fixedbottom_farm.py index ef9745621..0400ac7f1 100644 --- a/examples/29_floating_vs_fixedbottom_farm.py +++ b/examples/examples_floating/002_floating_vs_fixedbottom_farm.py @@ -1,15 +1,5 @@ - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from scipy.interpolate import NearestNDInterpolator - -import floris.flow_visualization as flowviz -from floris import FlorisModel - - -""" -This example demonstrates the impact of floating on turbine power and thurst +"""Example: Floating vs fixed-bottom farm +This example demonstrates the impact of floating on turbine power and thrust and wake behavior. A floating turbine in FLORIS is defined by including a `floating_tilt_table` in the turbine input yaml which sets the steady tilt angle of the turbine based on wind speed. This tilt angle is computed for each @@ -31,9 +21,19 @@ fmodel_floating: Floating turbine (tilt varies with wind speed) """ + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from scipy.interpolate import NearestNDInterpolator + +import floris.flow_visualization as flowviz +from floris import FlorisModel, WindRose + + # Declare the Floris Interface for fixed bottom, provide layout -fmodel_fixed = FlorisModel("inputs_floating/emgauss_fixed.yaml") -fmodel_floating = FlorisModel("inputs_floating/emgauss_floating.yaml") +fmodel_fixed = FlorisModel("../inputs_floating/emgauss_fixed.yaml") +fmodel_floating = FlorisModel("../inputs_floating/emgauss_floating.yaml") x, y = np.meshgrid(np.linspace(0, 4*630., 5), np.linspace(0, 3*630., 4)) x = x.flatten() y = y.flatten() @@ -107,28 +107,22 @@ flowviz.visualize_cut_plane(y_planes[1], ax=ax_list[1], title="Streamwise profile") fig.suptitle("Floating farm") -# Compute AEP (see 07_calc_aep_from_rose.py for details) -df_wr = pd.read_csv("inputs/wind_rose.csv") -wd_grid, ws_grid = np.meshgrid( - np.array(df_wr["wd"].unique(), dtype=float), - np.array(df_wr["ws"].unique(), dtype=float), - indexing="ij" +# Compute AEP +# Load the wind rose from csv as in example 003 +wind_rose = WindRose.read_csv_long( + "../inputs/wind_rose.csv", wd_col="wd", ws_col="ws", freq_col="freq_val", ti_col_or_value=0.06 ) -freq_interp = NearestNDInterpolator(df_wr[["wd", "ws"]], df_wr["freq_val"]) -freq = freq_interp(wd_grid, ws_grid).flatten() -freq = freq / np.sum(freq) + for fmodel in [fmodel_fixed, fmodel_floating]: fmodel.set( - wind_directions=wd_grid.flatten(), - wind_speeds= ws_grid.flatten(), - turbulence_intensities=0.06 * np.ones_like(wd_grid.flatten()) + wind_data=wind_rose, ) fmodel.run() # Compute the AEP -aep_fixed = fmodel_fixed.get_farm_AEP(freq=freq) -aep_floating = fmodel_floating.get_farm_AEP(freq=freq) +aep_fixed = fmodel_fixed.get_farm_AEP() +aep_floating = fmodel_floating.get_farm_AEP() print("Farm AEP (fixed bottom): {:.3f} GWh".format(aep_fixed / 1.0e9)) print("Farm AEP (floating): {:.3f} GWh".format(aep_floating / 1.0e9)) print( diff --git a/examples/examples_get_flow/001_extract_wind_speed_at_turbines.py b/examples/examples_get_flow/001_extract_wind_speed_at_turbines.py new file mode 100644 index 000000000..1eed14e75 --- /dev/null +++ b/examples/examples_get_flow/001_extract_wind_speed_at_turbines.py @@ -0,0 +1,39 @@ +"""Example: Extract wind speed at turbines + +This example demonstrates how to extract the wind speed at the turbine points +from the FLORIS model. Both the u velocities and the turbine average +velocities are grabbed from the model, then the turbine average is +recalculated from the u velocities to show that they are equivalent. +""" + + +import numpy as np + +from floris import FlorisModel + + +# Initialize the FLORIS model +fmodel = FlorisModel("../inputs/gch.yaml") + +# Create a 4-turbine layouts +fmodel.set(layout_x=[0, 0.0, 500.0, 500.0], layout_y=[0.0, 300.0, 0.0, 300.0]) + +# Calculate wake +fmodel.run() + +# Collect the wind speed at all the turbine points +u_points = fmodel.core.flow_field.u + +print("U points is 1 findex x 4 turbines x 3 x 3 points (turbine_grid_points=3)") +print(u_points.shape) + +print("turbine_average_velocities is 1 findex x 4 turbines") +print(fmodel.turbine_average_velocities) + +# Show that one is equivalent to the other following averaging +print( + "turbine_average_velocities is determined by taking the cube root of mean " + "of the cubed value across the points " +) +print(f"turbine_average_velocities: {fmodel.turbine_average_velocities}") +print(f"Recomputed: {np.cbrt(np.mean(u_points**3, axis=(2,3)))}") diff --git a/examples/28_extract_wind_speed_at_points.py b/examples/examples_get_flow/002_extract_wind_speed_at_points.py similarity index 84% rename from examples/28_extract_wind_speed_at_points.py rename to examples/examples_get_flow/002_extract_wind_speed_at_points.py index 7c9b9adbc..aaf086f4b 100644 --- a/examples/28_extract_wind_speed_at_points.py +++ b/examples/examples_get_flow/002_extract_wind_speed_at_points.py @@ -1,11 +1,4 @@ - -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel - - -""" +"""Example: Extract wind speed at points This example demonstrates the use of the sample_flow_at_points method of FlorisModel. sample_flow_at_points extracts the wind speed information at user-specified locations in the flow. @@ -19,21 +12,28 @@ met mast within the two-turbine farm. """ + +import matplotlib.pyplot as plt +import numpy as np + +from floris import FlorisModel + + # User options # FLORIS model to use (limited to Gauss/GCH, Jensen, and empirical Gauss) -floris_model = "gch" # Try "gch", "jensen", "emgauss" +floris_model = "gch" # Try "gch", "jensen", "emgauss" # Option to try different met mast locations -met_mast_option = 0 # Try 0, 1, 2, 3 +met_mast_option = 0 # Try 0, 1, 2, 3 # Instantiate FLORIS model -fmodel = FlorisModel("inputs/"+floris_model+".yaml") +fmodel = FlorisModel("../inputs/" + floris_model + ".yaml") # Set up a two-turbine farm D = 126 fmodel.set(layout_x=[0, 3 * D], layout_y=[0, 3 * D]) -fig, ax = plt.subplots(1,2) -fig.set_size_inches(10,4) +fig, ax = plt.subplots(1, 2) +fig.set_size_inches(10, 4) ax[0].scatter(fmodel.layout_x, fmodel.layout_y, color="black", label="Turbine") # Set the wind direction to run 360 degrees @@ -44,7 +44,7 @@ # Simulate a met mast in between the turbines if met_mast_option == 0: - points_x = 4 * [3*D] + points_x = 4 * [3 * D] points_y = 4 * [0] elif met_mast_option == 1: points_x = 4 * [200.0] @@ -69,10 +69,10 @@ # Plot the velocities for z_idx, z in enumerate(points_z): - ax[1].plot(wd_array, u_at_points[:, z_idx].flatten(), label=f'Speed at z={z} m') + ax[1].plot(wd_array, u_at_points[:, z_idx].flatten(), label=f"Speed at z={z} m") ax[1].grid() ax[1].legend() -ax[1].set_xlabel('Wind Direction (deg)') -ax[1].set_ylabel('Wind Speed (m/s)') +ax[1].set_xlabel("Wind Direction (deg)") +ax[1].set_ylabel("Wind Speed (m/s)") plt.show() diff --git a/examples/32_plot_velocity_deficit_profiles.py b/examples/examples_get_flow/003_plot_velocity_deficit_profiles.py similarity index 75% rename from examples/32_plot_velocity_deficit_profiles.py rename to examples/examples_get_flow/003_plot_velocity_deficit_profiles.py index a0b2949e0..1b8cabc77 100644 --- a/examples/32_plot_velocity_deficit_profiles.py +++ b/examples/examples_get_flow/003_plot_velocity_deficit_profiles.py @@ -1,22 +1,23 @@ +"""Example: Plot velocity deficit profiles + +This example illustrates how to plot velocity deficit profiles at several locations +downstream of a turbine. Here we use the following definition: + velocity_deficit = (homogeneous_wind_speed - u) / homogeneous_wind_speed + , where u is the wake velocity obtained when the incoming wind speed is the + same at all heights and equal to `homogeneous_wind_speed`. +""" + import matplotlib.pyplot as plt import numpy as np from matplotlib import ticker import floris.flow_visualization as flowviz -from floris import cut_plane, FlorisModel +from floris import FlorisModel from floris.flow_visualization import VelocityProfilesFigure from floris.utilities import reverse_rotate_coordinates_rel_west -""" -This example illustrates how to plot velocity deficit profiles at several locations -downstream of a turbine. Here we use the following definition: - velocity_deficit = (homogeneous_wind_speed - u) / homogeneous_wind_speed - , where u is the wake velocity obtained when the incoming wind speed is the - same at all heights and equal to `homogeneous_wind_speed`. -""" - # The first two functions are just used to plot the coordinate system in which the # profiles are sampled. Please go to the main function to begin the example. def plot_coordinate_system(x_origin, y_origin, wind_direction): @@ -27,34 +28,36 @@ def plot_coordinate_system(x_origin, y_origin, wind_direction): [quiver_length, quiver_length], [0, 0], angles=[270 - wind_direction, 360 - wind_direction], - scale_units='x', + scale_units="x", scale=1, ) annotate_coordinate_system(x_origin, y_origin, quiver_length) + def annotate_coordinate_system(x_origin, y_origin, quiver_length): x1 = np.array([quiver_length + 0.35 * D, 0.0]) x2 = np.array([0.0, quiver_length + 0.35 * D]) x3 = np.array([90.0, 90.0]) x, y, _ = reverse_rotate_coordinates_rel_west( - fmodel.core.flow_field.wind_directions, - x1[None, :], - x2[None, :], - x3[None, :], - x_center_of_rotation=0.0, - y_center_of_rotation=0.0, + fmodel.wind_directions, + x1[None, :], + x2[None, :], + x3[None, :], + x_center_of_rotation=0.0, + y_center_of_rotation=0.0, ) x = np.squeeze(x, axis=0) + x_origin y = np.squeeze(y, axis=0) + y_origin - plt.text(x[0], y[0], '$x_1$', bbox={'facecolor': 'white'}) - plt.text(x[1], y[1], '$x_2$', bbox={'facecolor': 'white'}) + plt.text(x[0], y[0], "$x_1$", bbox={"facecolor": "white"}) + plt.text(x[1], y[1], "$x_2$", bbox={"facecolor": "white"}) -if __name__ == '__main__': - D = 125.88 # Turbine diameter + +if __name__ == "__main__": + D = 125.88 # Turbine diameter hub_height = 90.0 homogeneous_wind_speed = 8.0 - fmodel = FlorisModel("inputs/gch.yaml") + fmodel = FlorisModel("../inputs/gch.yaml") fmodel.set(layout_x=[0.0], layout_y=[0.0]) # ------------------------------ Single-turbine layout ------------------------------ @@ -64,7 +67,7 @@ def annotate_coordinate_system(x_origin, y_origin, quiver_length): # Sample three profiles along three corresponding lines that are all parallel to the y-axis # (cross-stream direction). The streamwise location of each line is given in `downstream_dists`. profiles = fmodel.sample_velocity_deficit_profiles( - direction='cross-stream', + direction="cross-stream", downstream_dists=downstream_dists, homogeneous_wind_speed=homogeneous_wind_speed, ) @@ -72,13 +75,13 @@ def annotate_coordinate_system(x_origin, y_origin, quiver_length): horizontal_plane = fmodel.calculate_horizontal_plane(height=hub_height) fig, ax = plt.subplots(figsize=(6.4, 3)) flowviz.visualize_cut_plane(horizontal_plane, ax) - colors = ['b', 'g', 'c'] + colors = ["b", "g", "c"] for i, profile in enumerate(profiles): # Plot profile coordinates on the horizontal plane - ax.plot(profile['x'], profile['y'], colors[i], label=f'x/D={downstream_dists[i] / D:.1f}') - ax.set_xlabel('x [m]') - ax.set_ylabel('y [m]') - ax.set_title('Streamwise velocity in a horizontal plane: gauss velocity model') + ax.plot(profile["x"], profile["y"], colors[i], label=f"x/D={downstream_dists[i] / D:.1f}") + ax.set_xlabel("x [m]") + ax.set_ylabel("y [m]") + ax.set_title("Streamwise velocity in a horizontal plane: gauss velocity model") fig.tight_layout(rect=[0, 0, 0.82, 1]) ax.legend(bbox_to_anchor=[1.29, 1.04]) @@ -86,34 +89,34 @@ def annotate_coordinate_system(x_origin, y_origin, quiver_length): # Initialize it, plot data, and then customize it further if needed. profiles_fig = VelocityProfilesFigure( downstream_dists_D=downstream_dists / D, - layout=['cross-stream'], - coordinate_labels=['x/D', 'y/D'], + layout=["cross-stream"], + coordinate_labels=["x/D", "y/D"], ) # Add profiles to the VelocityProfilesFigure. This method automatically matches the supplied # profiles to the initialized axes in the figure. - profiles_fig.add_profiles(profiles, color='k') + profiles_fig.add_profiles(profiles, color="k") # Change velocity model to jensen, get the velocity deficit profiles, # and add them to the figure. floris_dict = fmodel.core.as_dict() - floris_dict['wake']['model_strings']['velocity_model'] = 'jensen' + floris_dict["wake"]["model_strings"]["velocity_model"] = "jensen" fmodel = FlorisModel(floris_dict) profiles = fmodel.sample_velocity_deficit_profiles( - direction='cross-stream', + direction="cross-stream", downstream_dists=downstream_dists, homogeneous_wind_speed=homogeneous_wind_speed, resolution=400, ) - profiles_fig.add_profiles(profiles, color='r') + profiles_fig.add_profiles(profiles, color="r") # The dashed reference lines show the extent of the rotor profiles_fig.add_ref_lines_x2([-0.5, 0.5]) for ax in profiles_fig.axs[0]: ax.xaxis.set_major_locator(ticker.MultipleLocator(0.2)) - profiles_fig.axs[0,0].legend(['gauss', 'jensen'], fontsize=11) + profiles_fig.axs[0, 0].legend(["gauss", "jensen"], fontsize=11) profiles_fig.fig.suptitle( - 'Velocity deficit profiles from different velocity models', + "Velocity deficit profiles from different velocity models", fontsize=14, ) @@ -123,19 +126,19 @@ def annotate_coordinate_system(x_origin, y_origin, quiver_length): # sampling-coordinate-system (x1, x2, x3) that is rotated such that x1 is always in the # streamwise direction. The user may define the origin of this coordinate system # (i.e. where to start sampling the profiles). - wind_direction = 315.0 # Try to change this + wind_direction = 315.0 # Try to change this downstream_dists = D * np.array([3, 5]) floris_dict = fmodel.core.as_dict() - floris_dict['wake']['model_strings']['velocity_model'] = 'gauss' + floris_dict["wake"]["model_strings"]["velocity_model"] = "gauss" fmodel = FlorisModel(floris_dict) # Let (x_t1, y_t1) be the location of the second turbine - x_t1 = 2 * D + x_t1 = 2 * D y_t1 = -2 * D fmodel.set(wind_directions=[wind_direction], layout_x=[0.0, x_t1], layout_y=[0.0, y_t1]) # Extract profiles at a set of downstream distances from the starting point (x_start, y_start) cross_profiles = fmodel.sample_velocity_deficit_profiles( - direction='cross-stream', + direction="cross-stream", downstream_dists=downstream_dists, homogeneous_wind_speed=homogeneous_wind_speed, x_start=x_t1, @@ -143,21 +146,20 @@ def annotate_coordinate_system(x_origin, y_origin, quiver_length): ) horizontal_plane = fmodel.calculate_horizontal_plane( - height=hub_height, - x_bounds=[-2 * D, 9 * D] + height=hub_height, x_bounds=[-2 * D, 9 * D] ) ax = flowviz.visualize_cut_plane(horizontal_plane) - colors = ['b', 'g', 'c'] + colors = ["b", "g", "c"] for i, profile in enumerate(cross_profiles): ax.plot( - profile['x'], - profile['y'], + profile["x"], + profile["y"], colors[i], - label=f'$x_1/D={downstream_dists[i] / D:.1f}$', + label=f"$x_1/D={downstream_dists[i] / D:.1f}$", ) - ax.set_xlabel('x [m]') - ax.set_ylabel('y [m]') - ax.set_title('Streamwise velocity in a horizontal plane') + ax.set_xlabel("x [m]") + ax.set_ylabel("y [m]") + ax.set_title("Streamwise velocity in a horizontal plane") ax.legend() plot_coordinate_system(x_origin=x_t1, y_origin=y_t1, wind_direction=wind_direction) @@ -166,7 +168,7 @@ def annotate_coordinate_system(x_origin, y_origin, quiver_length): # profiles are almost identical to the cross-stream profiles. However, we now explicitly # set the profile range. The default range is [-2 * D, 2 * D]. vertical_profiles = fmodel.sample_velocity_deficit_profiles( - direction='vertical', + direction="vertical", profile_range=[-1.5 * D, 1.5 * D], downstream_dists=downstream_dists, homogeneous_wind_speed=homogeneous_wind_speed, @@ -176,19 +178,18 @@ def annotate_coordinate_system(x_origin, y_origin, quiver_length): profiles_fig = VelocityProfilesFigure( downstream_dists_D=downstream_dists / D, - layout=['cross-stream', 'vertical'], + layout=["cross-stream", "vertical"], ) - profiles_fig.add_profiles(cross_profiles + vertical_profiles, color='k') + profiles_fig.add_profiles(cross_profiles + vertical_profiles, color="k") profiles_fig.set_xlim([-0.05, 0.85]) - profiles_fig.axs[1,0].set_ylim([-2.2, 2.2]) + profiles_fig.axs[1, 0].set_ylim([-2.2, 2.2]) for ax in profiles_fig.axs[0]: ax.xaxis.set_major_locator(ticker.MultipleLocator(0.4)) profiles_fig.fig.suptitle( - 'Cross-stream profiles at hub-height, and\nvertical profiles at $x_2 = 0$', + "Cross-stream profiles at hub-height, and\nvertical profiles at $x_2 = 0$", fontsize=14, ) - plt.show() diff --git a/examples/examples_heterogeneous/001_heterogeneous_inflow_single.py b/examples/examples_heterogeneous/001_heterogeneous_inflow_single.py new file mode 100644 index 000000000..28f92d238 --- /dev/null +++ b/examples/examples_heterogeneous/001_heterogeneous_inflow_single.py @@ -0,0 +1,79 @@ +"""Example: Heterogeneous Inflow for single case + +This example illustrates how to set up a heterogeneous inflow condition in FLORIS. It: + + 1) Initializes FLORIS + 2) Changes the wind farm layout + 3) Changes the incoming wind speed, wind direction and turbulence intensity + to a single condition + 4) Sets up a heterogeneous inflow condition for that single condition + 5) Runs the FLORIS simulation + 6) Gets the power output of the turbines + 7) Visualizes the horizontal plane at hub height + +""" + +import matplotlib.pyplot as plt +import numpy as np + +from floris import FlorisModel, TimeSeries +from floris.flow_visualization import visualize_cut_plane +from floris.layout_visualization import plot_turbine_labels + + +# Initialize FlorisModel +fmodel = FlorisModel("../inputs/gch.yaml") + +# Change the layout to a 4 turbine layout in a box +fmodel.set(layout_x=[0, 0, 500.0, 500.0], layout_y=[0, 500.0, 0, 500.0]) + +# Set FLORIS to run for a single condition +fmodel.set(wind_speeds=[8.0], wind_directions=[270.0], turbulence_intensities=[0.06]) + +# Define the speed-ups of the heterogeneous inflow, and their locations. +# Note that heterogeneity is only applied within the bounds of the points defined in the +# heterogeneous_inflow_config dictionary. In this case, set the inflow to be 1.25x the ambient +# wind speed for the upper turbines at y = 500m. +speed_ups = [[1.0, 1.25, 1.0, 1.25]] # Note speed-ups has dimensions of n_findex X n_points +x_locs = [-500.0, -500.0, 1000.0, 1000.0] +y_locs = [-500.0, 1000.0, -500.0, 1000.0] + +# Create the configuration dictionary to be used for the heterogeneous inflow. +heterogeneous_inflow_config = { + "speed_multipliers": speed_ups, + "x": x_locs, + "y": y_locs, +} + +# Set the heterogeneous inflow configuration +fmodel.set(heterogeneous_inflow_config=heterogeneous_inflow_config) + +# Run the FLORIS simulation +fmodel.run() + +# Get the power output of the turbines +turbine_powers = fmodel.get_turbine_powers() / 1000.0 + +# Print the turbine powers +print(f"Turbine 0 power = {turbine_powers[0, 0]:.1f} kW") +print(f"Turbine 1 power = {turbine_powers[0, 1]:.1f} kW") +print(f"Turbine 2 power = {turbine_powers[0, 2]:.1f} kW") +print(f"Turbine 3 power = {turbine_powers[0, 3]:.1f} kW") + +# Extract the horizontal plane at hub height +horizontal_plane = fmodel.calculate_horizontal_plane( + x_resolution=200, y_resolution=100, height=90.0 +) + +# Plot the horizontal plane +fig, ax = plt.subplots() +visualize_cut_plane( + horizontal_plane, + ax=ax, + title="Horizontal plane at hub height", + color_bar=True, + label_contours=True, +) +plot_turbine_labels(fmodel, ax) + +plt.show() diff --git a/examples/examples_heterogeneous/002_heterogeneous_inflow_multi.py b/examples/examples_heterogeneous/002_heterogeneous_inflow_multi.py new file mode 100644 index 000000000..fa8b9cfe4 --- /dev/null +++ b/examples/examples_heterogeneous/002_heterogeneous_inflow_multi.py @@ -0,0 +1,123 @@ +"""Example: Heterogeneous Inflow for multiple conditions + +When multiple cases are considered, the heterogeneous inflow conditions can be defined in two ways: + + 1. Passing heterogeneous_inflow_config to the set method, with P points, + and speedups of size n_findex X P + 2. Assigning heterogeneous_inflow_config_by_wd to the wind_data object + used to drive FLORIS. This object includes + n_wd wind_directions, and speedups is of size n_wd X P. When applied + to set, the heterogeneous_inflow_config + is automatically generated by using the nearest wind direction + defined in heterogeneous_inflow_config_by_wd + for each findex. + +This example: + + 1) Implements heterogeneous inflow for a 4 turbine layout using both of the above methods + 2) Compares the results of the two methods and shows that they are equivalent + +""" + + +import matplotlib.pyplot as plt +import numpy as np + +from floris import FlorisModel, TimeSeries + + +# Initialize FlorisModel +fmodel = FlorisModel("../inputs/gch.yaml") + +# Change the layout to a 4 turbine layout in a box +fmodel.set(layout_x=[0, 0, 500.0, 500.0], layout_y=[0, 500.0, 0, 500.0]) + +# Define a TimeSeries object with 4 wind directions and constant wind speed +# and turbulence intensity + +time_series = TimeSeries( + wind_directions=np.array([269.0, 270.0, 271.0, 282.0]), + wind_speeds=8.0, + turbulence_intensities=0.06, +) + +# Apply the time series to the FlorisModel +fmodel.set(wind_data=time_series) + +# Define the x_locs to be used in the heterogeneous inflow configuration that form +# a box around the turbines +x_locs = [-500.0, -500.0, 1000.0, 1000.0] +y_locs = [-500.0, 1000.0, -500.0, 1000.0] + +# Assume the speed-ups are defined such that they are the same 265-275 degrees and 275-285 degrees + +# If defining heterogeneous_inflow_config directly, then the speedups are of size n_findex X P +# where the first 3 rows are identical, and the last row is different +speed_ups = [ + [1.0, 1.25, 1.0, 1.25], + [1.0, 1.25, 1.0, 1.25], + [1.0, 1.25, 1.0, 1.25], + [1.0, 1.35, 1.0, 1.35], +] + +heterogeneous_inflow_config = { + "speed_multipliers": speed_ups, + "x": x_locs, + "y": y_locs, +} + +# Set the heterogeneous inflow configuration +fmodel.set(heterogeneous_inflow_config=heterogeneous_inflow_config) + +# Run the FLORIS simulation +fmodel.run() + +# Get the power output of the turbines +turbine_powers = fmodel.get_turbine_powers() / 1000.0 + +# Now repeat using the wind_data object and heterogeneous_inflow_config_by_wd +# First, create the speedups for the two wind directions +speed_ups = [[1.0, 1.25, 1.0, 1.25], [1.0, 1.35, 1.0, 1.35]] + +# Create the heterogeneous_inflow_config_by_wd dictionary +heterogeneous_inflow_config_by_wd = { + "speed_multipliers": speed_ups, + "x": x_locs, + "y": y_locs, + "wind_directions": [270.0, 280.0], +} + +# Now create a new TimeSeries object including the heterogeneous_inflow_config_by_wd +time_series = TimeSeries( + wind_directions=np.array([269.0, 270.0, 271.0, 282.0]), + wind_speeds=8.0, + turbulence_intensities=0.06, + heterogeneous_inflow_config_by_wd=heterogeneous_inflow_config_by_wd, +) + +# Apply the time series to the FlorisModel +fmodel.set(wind_data=time_series) + +# Run the FLORIS simulation +fmodel.run() + +# Get the power output of the turbines +turbine_powers_by_wd = fmodel.get_turbine_powers() / 1000.0 + +# Plot the results +wind_directions = fmodel.wind_directions +fig, axarr = plt.subplots(2, 2, sharex=True, sharey=True, figsize=(10, 10)) +axarr = axarr.flatten() + +for tindex in range(4): + ax = axarr[tindex] + ax.plot(wind_directions, turbine_powers[:, tindex], "ks-", label="Heterogeneous Inflow") + ax.plot( + wind_directions, turbine_powers_by_wd[:, tindex], ".--", label="Heterogeneous Inflow by WD" + ) + ax.set_title(f"Turbine {tindex}") + ax.set_xlabel("Wind Direction (deg)") + ax.set_ylabel("Power (kW)") + ax.legend() + +plt.show() diff --git a/examples/16_heterogeneous_inflow.py b/examples/examples_heterogeneous/003_heterogeneous_2d_and_3d.py similarity index 62% rename from examples/16_heterogeneous_inflow.py rename to examples/examples_heterogeneous/003_heterogeneous_2d_and_3d.py index 26451ffa5..1d1f3b791 100644 --- a/examples/16_heterogeneous_inflow.py +++ b/examples/examples_heterogeneous/003_heterogeneous_2d_and_3d.py @@ -1,13 +1,8 @@ +"""Example: Heterogeneous Inflow in 2D and 3D -import matplotlib.pyplot as plt - -from floris import FlorisModel -from floris.flow_visualization import visualize_cut_plane - - -""" This example showcases the heterogeneous inflow capabilities of FLORIS. -Heterogeneous flow can be defined in either 2- or 3-dimensions. +Heterogeneous flow can be defined in either 2- or 3-dimensions for a single +condition. For the 2-dimensional case, it can be seen that the freestream velocity only varies in the x direction. For the 3-dimensional case, it can be @@ -18,23 +13,34 @@ For each case, we are plotting three slices of the resulting flow field: 1. Horizontal slice parallel to the ground and located at the hub height 2. Vertical slice parallel with the direction of the wind -3. Veritical slice parallel to to the turbine disc plane +3. Vertical slice parallel to to the turbine disc plane + +Since the intention is for plotting, only a single condition is run and in +this case the heterogeneous_inflow_config is more convenient to use than +heterogeneous_inflow_config_by_wd. However, the latter is more convenient +when running multiple conditions. """ +import matplotlib.pyplot as plt + +from floris import FlorisModel +from floris.flow_visualization import visualize_cut_plane + + # Initialize FLORIS with the given input file via FlorisModel. -# Note that the heterogeneous flow is defined in the input file. The heterogenous_inflow_config +# Note that the heterogeneous flow is defined in the input file. The heterogeneous_inflow_config # dictionary is defined as below. The speed ups are multipliers of the ambient wind speed, # and the x and y are the locations of the speed ups. # -# heterogenous_inflow_config = { +# heterogeneous_inflow_config = { # 'speed_multipliers': [[2.0, 1.0, 2.0, 1.0]], # 'x': [-300.0, -300.0, 2600.0, 2600.0], # 'y': [ -300.0, 300.0, -300.0, 300.0], # } -fmodel_2d = FlorisModel("inputs/gch_heterogeneous_inflow.yaml") +fmodel_2d = FlorisModel("../inputs/gch_heterogeneous_inflow.yaml") # Set shear to 0.0 to highlight the heterogeneous inflow fmodel_2d.set(wind_shear=0.0) @@ -42,47 +48,35 @@ # Using the FlorisModel functions for generating plots, run FLORIS # and extract 2D planes of data. horizontal_plane_2d = fmodel_2d.calculate_horizontal_plane( - x_resolution=200, - y_resolution=100, - height=90.0 + x_resolution=200, y_resolution=100, height=90.0 ) y_plane_2d = fmodel_2d.calculate_y_plane(x_resolution=200, z_resolution=100, crossstream_dist=0.0) cross_plane_2d = fmodel_2d.calculate_cross_plane( - y_resolution=100, - z_resolution=100, - downstream_dist=500.0 + y_resolution=100, z_resolution=100, downstream_dist=500.0 ) # Create the plots fig, ax_list = plt.subplots(3, 1, figsize=(10, 8)) ax_list = ax_list.flatten() visualize_cut_plane( - horizontal_plane_2d, - ax=ax_list[0], - title="Horizontal", - color_bar=True, - label_contours=True + horizontal_plane_2d, ax=ax_list[0], title="Horizontal", color_bar=True, label_contours=True ) -ax_list[0].set_xlabel('x') -ax_list[0].set_ylabel('y') +ax_list[0].set_xlabel("x") +ax_list[0].set_ylabel("y") visualize_cut_plane( - y_plane_2d, - ax=ax_list[1], - title="Streamwise profile", - color_bar=True, - label_contours=True + y_plane_2d, ax=ax_list[1], title="Streamwise profile", color_bar=True, label_contours=True ) -ax_list[1].set_xlabel('x') -ax_list[1].set_ylabel('z') +ax_list[1].set_xlabel("x") +ax_list[1].set_ylabel("z") visualize_cut_plane( cross_plane_2d, ax=ax_list[2], title="Spanwise profile at 500m downstream", color_bar=True, - label_contours=True + label_contours=True, ) -ax_list[2].set_xlabel('y') -ax_list[2].set_ylabel('z') +ax_list[2].set_xlabel("y") +ax_list[2].set_ylabel("z") # Define the speed ups of the heterogeneous inflow, and their locations. @@ -94,18 +88,18 @@ z_locs = [540.0, 540.0, 0.0, 0.0, 540.0, 540.0, 0.0, 0.0] # Create the configuration dictionary to be used for the heterogeneous inflow. -heterogenous_inflow_config = { - 'speed_multipliers': speed_multipliers, - 'x': x_locs, - 'y': y_locs, - 'z': z_locs, +heterogeneous_inflow_config = { + "speed_multipliers": speed_multipliers, + "x": x_locs, + "y": y_locs, + "z": z_locs, } # Initialize FLORIS with the given input file. # Note that we initialize FLORIS with a homogenous flow input file, but # then configure the heterogeneous inflow via the reinitialize method. -fmodel_3d = FlorisModel("inputs/gch.yaml") -fmodel_3d.set(heterogenous_inflow_config=heterogenous_inflow_config) +fmodel_3d = FlorisModel("../inputs/gch.yaml") +fmodel_3d.set(heterogeneous_inflow_config=heterogeneous_inflow_config) # Set shear to 0.0 to highlight the heterogeneous inflow fmodel_3d.set(wind_shear=0.0) @@ -113,50 +107,34 @@ # Using the FlorisModel functions for generating plots, run FLORIS # and extract 2D planes of data. horizontal_plane_3d = fmodel_3d.calculate_horizontal_plane( - x_resolution=200, - y_resolution=100, - height=90.0 -) -y_plane_3d = fmodel_3d.calculate_y_plane( - x_resolution=200, - z_resolution=100, - crossstream_dist=0.0 + x_resolution=200, y_resolution=100, height=90.0 ) +y_plane_3d = fmodel_3d.calculate_y_plane(x_resolution=200, z_resolution=100, crossstream_dist=0.0) cross_plane_3d = fmodel_3d.calculate_cross_plane( - y_resolution=100, - z_resolution=100, - downstream_dist=500.0 + y_resolution=100, z_resolution=100, downstream_dist=500.0 ) # Create the plots fig, ax_list = plt.subplots(3, 1, figsize=(10, 8)) ax_list = ax_list.flatten() visualize_cut_plane( - horizontal_plane_3d, - ax=ax_list[0], - title="Horizontal", - color_bar=True, - label_contours=True + horizontal_plane_3d, ax=ax_list[0], title="Horizontal", color_bar=True, label_contours=True ) -ax_list[0].set_xlabel('x') -ax_list[0].set_ylabel('y') +ax_list[0].set_xlabel("x") +ax_list[0].set_ylabel("y") visualize_cut_plane( - y_plane_3d, - ax=ax_list[1], - title="Streamwise profile", - color_bar=True, - label_contours=True + y_plane_3d, ax=ax_list[1], title="Streamwise profile", color_bar=True, label_contours=True ) -ax_list[1].set_xlabel('x') -ax_list[1].set_ylabel('z') +ax_list[1].set_xlabel("x") +ax_list[1].set_ylabel("z") visualize_cut_plane( cross_plane_3d, ax=ax_list[2], title="Spanwise profile at 500m downstream", color_bar=True, - label_contours=True + label_contours=True, ) -ax_list[2].set_xlabel('y') -ax_list[2].set_ylabel('z') +ax_list[2].set_xlabel("y") +ax_list[2].set_ylabel("z") plt.show() diff --git a/examples/examples_layout_optimization/001_optimize_layout.py b/examples/examples_layout_optimization/001_optimize_layout.py new file mode 100644 index 000000000..809c346d7 --- /dev/null +++ b/examples/examples_layout_optimization/001_optimize_layout.py @@ -0,0 +1,139 @@ + +"""Example: Optimize Layout +This example shows a simple layout optimization using the python module Scipy, optimizing for both +annual energy production (AEP) and annual value production (AVP). + +First, a 4 turbine array is optimized such that the layout of the turbine produces the +highest AEP based on the given wind resource. The turbines +are constrained to a square boundary and a random wind resource is supplied. The results +of the optimization show that the turbines are pushed to near the outer corners of the boundary, +which, given the generally uniform wind rose, makes sense in order to maximize the energy +production by minimizing wake interactions. + +Next, with the same boundary, the same 4 turbine array is optimized to maximize AVP instead of AEP, +using the value table defined in the WindRose object, where value represents the value of the +energy produced for a given wind condition (e.g., the price of electricity). In this example, value +is defined to be significantly higher for northerly and southerly wind directions, and zero when +the wind is from the east or west. Because the value is much higher when the wind is from the north +or south, the turbines are spaced apart roughly evenly in the x direction while being relatively +close in the y direction to avoid wake interactions for northerly and southerly winds. Although the +layout results in large wake losses when the wind is from the east or west, these losses do not +significantly impact the objective function because of the low value for those wind directions. +""" + + +import os + +import matplotlib.pyplot as plt +import numpy as np + +from floris import FlorisModel, WindRose +from floris.optimization.layout_optimization.layout_optimization_scipy import ( + LayoutOptimizationScipy, +) + + +# Define scipy optimization parameters +opt_options = { + "maxiter": 20, + "disp": True, + "iprint": 2, + "ftol": 1e-12, + "eps": 0.05, +} + +# Initialize the FLORIS interface fi +file_dir = os.path.dirname(os.path.abspath(__file__)) +fmodel = FlorisModel('../inputs/gch.yaml') + +# Setup 72 wind directions with a 1 wind speed and frequency distribution +wind_directions = np.arange(0, 360.0, 5.0) +wind_speeds = np.array([8.0]) + +# Shape random frequency distribution to match number of wind directions and wind speeds +freq_table = np.zeros((len(wind_directions), len(wind_speeds))) +np.random.seed(1) +freq_table[:,0] = (np.abs(np.sort(np.random.randn(len(wind_directions))))) +freq_table = freq_table / freq_table.sum() + +# Define the value table such that the value of the energy produced is +# significantly higher when the wind direction is close to the north or +# south, and zero when the wind is from the east or west. Here, value is +# given a mean value of 25 USD/MWh. +value_table = (0.5 + 0.5*np.cos(2*np.radians(wind_directions)))**10 +value_table = 25*value_table/np.mean(value_table) +value_table = value_table.reshape((len(wind_directions),1)) + +# Establish a WindRose object +wind_rose = WindRose( + wind_directions=wind_directions, + wind_speeds=wind_speeds, + freq_table=freq_table, + ti_table=0.06, + value_table=value_table +) + +fmodel.set(wind_data=wind_rose) + +# The boundaries for the turbines, specified as vertices +boundaries = [(0.0, 0.0), (0.0, 1000.0), (1000.0, 1000.0), (1000.0, 0.0), (0.0, 0.0)] + +# Set turbine locations to 4 turbines in a rectangle +D = 126.0 # rotor diameter for the NREL 5MW +layout_x = [0, 0, 6 * D, 6 * D] +layout_y = [0, 4 * D, 0, 4 * D] +fmodel.set(layout_x=layout_x, layout_y=layout_y) + +# Setup the optimization problem to maximize AEP instead of value +layout_opt = LayoutOptimizationScipy(fmodel, boundaries, optOptions=opt_options) + +# Run the optimization +sol = layout_opt.optimize() + +# Get the resulting improvement in AEP +print('... calculating improvement in AEP') +fmodel.run() +base_aep = fmodel.get_farm_AEP() / 1e6 +fmodel.set(layout_x=sol[0], layout_y=sol[1]) +fmodel.run() +opt_aep = fmodel.get_farm_AEP() / 1e6 + +percent_gain = 100 * (opt_aep - base_aep) / base_aep + +# Print and plot the results +print(f'Optimal layout: {sol}') +print( + f'Optimal layout improves AEP by {percent_gain:.1f}% ' + f'from {base_aep:.1f} MWh to {opt_aep:.1f} MWh' +) +layout_opt.plot_layout_opt_results() + +# reset to the original layout +fmodel.set(layout_x=layout_x, layout_y=layout_y) + +# Now set up the optimization problem to maximize annual value production (AVP) +# using the value table provided in the WindRose object. +layout_opt = LayoutOptimizationScipy(fmodel, boundaries, optOptions=opt_options, use_value=True) + +# Run the optimization +sol = layout_opt.optimize() + +# Get the resulting improvement in AVP +print('... calculating improvement in annual value production (AVP)') +fmodel.run() +base_avp = fmodel.get_farm_AVP() / 1e6 +fmodel.set(layout_x=sol[0], layout_y=sol[1]) +fmodel.run() +opt_avp = fmodel.get_farm_AVP() / 1e6 + +percent_gain = 100 * (opt_avp - base_avp) / base_avp + +# Print and plot the results +print(f'Optimal layout: {sol}') +print( + f'Optimal layout improves AVP by {percent_gain:.1f}% ' + f'from {base_avp:.1f} dollars to {opt_avp:.1f} dollars' +) +layout_opt.plot_layout_opt_results() + +plt.show() diff --git a/examples/16c_optimize_layout_with_heterogeneity.py b/examples/examples_layout_optimization/002_optimize_layout_with_heterogeneity.py similarity index 71% rename from examples/16c_optimize_layout_with_heterogeneity.py rename to examples/examples_layout_optimization/002_optimize_layout_with_heterogeneity.py index 069511cd8..e0879b38c 100644 --- a/examples/16c_optimize_layout_with_heterogeneity.py +++ b/examples/examples_layout_optimization/002_optimize_layout_with_heterogeneity.py @@ -1,3 +1,14 @@ +"""Example: Layout optimization with heterogeneous inflow +This example shows a layout optimization using the geometric yaw option. It +combines elements of layout optimization and heterogeneous +inflow for demonstrative purposes. + +Heterogeneity in the inflow provides the necessary driver for coupled yaw +and layout optimization to be worthwhile. First, a layout optimization is +run without coupled yaw optimization; then a coupled optimization is run to +show the benefits of coupled optimization when flows are heterogeneous. +""" + import os @@ -10,25 +21,13 @@ ) -""" -This example shows a layout optimization using the geometric yaw option. It -combines elements of examples 15 (layout optimization) and 16 (heterogeneous -inflow) for demonstrative purposes. If you haven't yet run those examples, -we recommend you try them first. - -Heterogeneity in the inflow provides the necessary driver for coupled yaw -and layout optimization to be worthwhile. First, a layout optimization is -run without coupled yaw optimization; then a coupled optimization is run to -show the benefits of coupled optimization when flows are heterogeneous. -""" - # Initialize FLORIS file_dir = os.path.dirname(os.path.abspath(__file__)) -fmodel = FlorisModel('inputs/gch.yaml') +fmodel = FlorisModel("../inputs/gch.yaml") # Setup 2 wind directions (due east and due west) # and 1 wind speed with uniform probability -wind_directions = np.array([270., 90.]) +wind_directions = np.array([270.0, 90.0]) n_wds = len(wind_directions) wind_speeds = [8.0] * np.ones_like(wind_directions) turbulence_intensities = 0.06 * np.ones_like(wind_directions) @@ -38,32 +37,26 @@ # The boundaries for the turbines, specified as vertices -D = 126.0 # rotor diameter for the NREL 5MW +D = 126.0 # rotor diameter for the NREL 5MW size_D = 12 -boundaries = [ - (0.0, 0.0), - (size_D * D, 0.0), - (size_D * D, 0.1), - (0.0, 0.1), - (0.0, 0.0) -] +boundaries = [(0.0, 0.0), (size_D * D, 0.0), (size_D * D, 0.1), (0.0, 0.1), (0.0, 0.0)] # Set turbine locations to 4 turbines at corners of the rectangle # (optimal without flow heterogeneity) -layout_x = [0.1, 0.3*size_D*D, 0.6*size_D*D] +layout_x = [0.1, 0.3 * size_D * D, 0.6 * size_D * D] layout_y = [0, 0, 0] # Generate exaggerated heterogeneous inflow (same for all wind directions) -speed_multipliers = np.repeat(np.array([0.5, 1.0, 0.5, 1.0])[None,:], n_wds, axis=0) +speed_multipliers = np.repeat(np.array([0.5, 1.0, 0.5, 1.0])[None, :], n_wds, axis=0) x_locs = [0, size_D * D, 0, size_D * D] y_locs = [-D, -D, D, D] # Create the configuration dictionary to be used for the heterogeneous inflow. -heterogenous_inflow_config_by_wd = { - 'speed_multipliers': speed_multipliers, - 'wind_directions': wind_directions, - 'x': x_locs, - 'y': y_locs, +heterogeneous_inflow_config_by_wd = { + "speed_multipliers": speed_multipliers, + "wind_directions": wind_directions, + "x": x_locs, + "y": y_locs, } # Establish a WindRose object @@ -72,7 +65,7 @@ wind_speeds=wind_speeds, freq_table=freq_table, ti_table=0.06, - heterogenous_inflow_config_by_wd=heterogenous_inflow_config_by_wd + heterogeneous_inflow_config_by_wd=heterogeneous_inflow_config_by_wd, ) @@ -85,10 +78,7 @@ # Setup and solve the layout optimization problem without heterogeneity maxiter = 100 layout_opt = LayoutOptimizationScipy( - fmodel, - boundaries, - min_dist=2*D, - optOptions={"maxiter":maxiter} + fmodel, boundaries, min_dist=2 * D, optOptions={"maxiter": maxiter} ) # Run the optimization @@ -96,7 +86,7 @@ sol = layout_opt.optimize() # Get the resulting improvement in AEP -print('... calcuating improvement in AEP') +print("... calcuating improvement in AEP") fmodel.run() base_aep = fmodel.get_farm_AEP() / 1e6 @@ -107,10 +97,10 @@ percent_gain = 100 * (opt_aep - base_aep) / base_aep # Print and plot the results -print(f'Optimal layout: {sol}') +print(f"Optimal layout: {sol}") print( - f'Optimal layout improves AEP by {percent_gain:.1f}% ' - f'from {base_aep:.1f} MWh to {opt_aep:.1f} MWh' + f"Optimal layout improves AEP by {percent_gain:.1f}% " + f"from {base_aep:.1f} MWh to {opt_aep:.1f} MWh" ) layout_opt.plot_layout_opt_results() ax = plt.gca() @@ -125,11 +115,7 @@ print("\nReoptimizing with geometric yaw enabled.") fmodel.set(layout_x=layout_x, layout_y=layout_y) layout_opt = LayoutOptimizationScipy( - fmodel, - boundaries, - min_dist=2*D, - enable_geometric_yaw=True, - optOptions={"maxiter":maxiter} + fmodel, boundaries, min_dist=2 * D, enable_geometric_yaw=True, optOptions={"maxiter": maxiter} ) # Run the optimization @@ -137,7 +123,7 @@ sol = layout_opt.optimize() # Get the resulting improvement in AEP -print('... calcuating improvement in AEP') +print("... calcuating improvement in AEP") fmodel.set(yaw_angles=np.zeros_like(layout_opt.yaw_angles)) fmodel.run() @@ -149,10 +135,10 @@ percent_gain = 100 * (opt_aep - base_aep) / base_aep # Print and plot the results -print(f'Optimal layout: {sol}') +print(f"Optimal layout: {sol}") print( - f'Optimal layout improves AEP by {percent_gain:.1f}% ' - f'from {base_aep:.1f} MWh to {opt_aep:.1f} MWh' + f"Optimal layout improves AEP by {percent_gain:.1f}% " + f"from {base_aep:.1f} MWh to {opt_aep:.1f} MWh" ) layout_opt.plot_layout_opt_results() ax = plt.gca() @@ -163,9 +149,9 @@ ax.set_title("Geometric yaw enabled") print( - 'Turbine geometric yaw angles for wind direction {0:.2f}'.format(wind_directions[1])\ - +' and wind speed {0:.2f} m/s:'.format(wind_speeds[0]), - f'{layout_opt.yaw_angles[1, :]}' + "Turbine geometric yaw angles for wind direction {0:.2f}".format(wind_directions[1]) + + " and wind speed {0:.2f} m/s:".format(wind_speeds[0]), + f"{layout_opt.yaw_angles[1, :]}", ) plt.show() diff --git a/examples/30_multi_dimensional_cp_ct.py b/examples/examples_multidim/001_multi_dimensional_cp_ct.py similarity index 79% rename from examples/30_multi_dimensional_cp_ct.py rename to examples/examples_multidim/001_multi_dimensional_cp_ct.py index e33ca31d2..b1bf0441b 100644 --- a/examples/30_multi_dimensional_cp_ct.py +++ b/examples/examples_multidim/001_multi_dimensional_cp_ct.py @@ -1,14 +1,8 @@ - -import numpy as np - -from floris import FlorisModel - - -""" -This example follows the same setup as example 01 to createa a FLORIS instance and: +"""Example: Multi-dimensional Cp/Ct data +This example creates a FLORIS instance and: 1) Makes a two-turbine layout 2) Demonstrates single ws/wd simulations -3) Demonstrates mulitple ws/wd simulations +3) Demonstrates multiple ws/wd simulations with the modification of using a turbine definition that has a multi-dimensional Cp/Ct table. @@ -19,7 +13,7 @@ height. For every combination of Tp and Hs defined, a Cp/Ct/Wind speed table of values is also defined. It is required for this .csv file to have the last 3 columns be ws, Cp, and Ct. In order for this table to be used, the flag 'multi_dimensional_cp_ct' must be present and set to true in -the turbine definition. With this flag enabled, the solver will downselect to use the +the turbine definition. With this flag enabled, the solver will down-select to use the interpolant defined at the closest conditions. The user must supply these conditions in the main input file under the 'flow_field' section, e.g.: @@ -40,20 +34,25 @@ 'get_turbine_powers_multidim'. The normal 'get_turbine_powers' method will not work. """ +import numpy as np + +from floris import FlorisModel + + # Initialize FLORIS with the given input file. -fmodel = FlorisModel("inputs/gch_multi_dim_cp_ct.yaml") +fmodel = FlorisModel("../inputs/gch_multi_dim_cp_ct.yaml") # Convert to a simple two turbine layout -fmodel.set(layout_x=[0., 500.], layout_y=[0., 0.]) +fmodel.set(layout_x=[0.0, 500.0], layout_y=[0.0, 0.0]) # Single wind speed and wind direction -print('\n========================= Single Wind Direction and Wind Speed =========================') +print("\n========================= Single Wind Direction and Wind Speed =========================") # Get the turbine powers assuming 1 wind speed and 1 wind direction fmodel.set(wind_directions=[270.0], wind_speeds=[8.0], turbulence_intensities=[0.06]) # Set the yaw angles to 0 -yaw_angles = np.zeros([1, 2]) # 1 wind direction and wind speed, 2 turbines +yaw_angles = np.zeros([1, 2]) # 1 wind direction and wind speed, 2 turbines fmodel.set(yaw_angles=yaw_angles) # Calculate @@ -63,10 +62,10 @@ turbine_powers = fmodel.get_turbine_powers() / 1000.0 print("The turbine power matrix should be of dimensions 1 findex X 2 Turbines") print(turbine_powers) -print("Shape: ",turbine_powers.shape) +print("Shape: ", turbine_powers.shape) # Single wind speed and multiple wind directions -print('\n========================= Single Wind Direction and Multiple Wind Speeds ===============') +print("\n========================= Single Wind Direction and Multiple Wind Speeds ===============") wind_speeds = np.array([8.0, 9.0, 10.0]) wind_directions = np.array([270.0, 270.0, 270.0]) @@ -77,16 +76,16 @@ wind_speeds=wind_speeds, wind_directions=wind_directions, turbulence_intensities=turbulence_intensities, - yaw_angles=yaw_angles + yaw_angles=yaw_angles, ) fmodel.run() turbine_powers = fmodel.get_turbine_powers() / 1000.0 print("The turbine power matrix should be of dimensions 3 findex X 2 Turbines") print(turbine_powers) -print("Shape: ",turbine_powers.shape) +print("Shape: ", turbine_powers.shape) # Multiple wind speeds and multiple wind directions -print('\n========================= Multiple Wind Directions and Multiple Wind Speeds ============') +print("\n========================= Multiple Wind Directions and Multiple Wind Speeds ============") wind_speeds = np.tile([8.0, 9.0, 10.0], 3) wind_directions = np.repeat([260.0, 270.0, 280.0], 3) @@ -97,10 +96,10 @@ wind_directions=wind_directions, wind_speeds=wind_speeds, turbulence_intensities=turbulence_intensities, - yaw_angles=yaw_angles + yaw_angles=yaw_angles, ) fmodel.run() -turbine_powers = fmodel.get_turbine_powers()/1000. +turbine_powers = fmodel.get_turbine_powers() / 1000.0 print("The turbine power matrix should be of dimensions 9 WD/WS X 2 Turbines") print(turbine_powers) -print("Shape: ",turbine_powers.shape) +print("Shape: ", turbine_powers.shape) diff --git a/examples/examples_multidim/002_multi_dimensional_cp_ct_2Hs.py b/examples/examples_multidim/002_multi_dimensional_cp_ct_2Hs.py new file mode 100644 index 000000000..8cf206f07 --- /dev/null +++ b/examples/examples_multidim/002_multi_dimensional_cp_ct_2Hs.py @@ -0,0 +1,65 @@ +"""Example: Multi-dimensional Cp/Ct with 2 Hs values +This example follows the previous example but shows the effect of changing the Hs setting. + +NOTE: The multi-dimensional Cp/Ct data used in this example is fictional for the purposes of +facilitating this example. The Cp/Ct values for the different wave conditions are scaled +values of the original Cp/Ct data for the IEA 15MW turbine. +""" + + +import matplotlib.pyplot as plt +import numpy as np + +from floris import FlorisModel, TimeSeries + + +# Initialize FLORIS with the given input file. +fmodel = FlorisModel("../inputs/gch_multi_dim_cp_ct.yaml") + +# Make a second Floris instance with a different setting for Hs. +# Note the multi-cp-ct file (iea_15MW_multi_dim_Tp_Hs.csv) +# for the turbine model iea_15MW_floating_multi_dim_cp_ct.yaml +# Defines Hs at 1 and 5. +# The value in gch_multi_dim_cp_ct.yaml is 3.01 which will map +# to 5 as the nearer value, so we set the other case to 1 +# for contrast. +fmodel_dict_mod = fmodel.core.as_dict() +fmodel_dict_mod["flow_field"]["multidim_conditions"]["Hs"] = 1.0 +fmodel_hs_1 = FlorisModel(fmodel_dict_mod) + +# Set both cases to 3 turbine layout +fmodel.set(layout_x=[0.0, 500.0, 1000.0], layout_y=[0.0, 0.0, 0.0]) +fmodel_hs_1.set(layout_x=[0.0, 500.0, 1000.0], layout_y=[0.0, 0.0, 0.0]) + +# Use a sweep of wind speeds +wind_speeds = np.arange(5, 20, 1.0) +time_series = TimeSeries( + wind_directions=270.0, wind_speeds=wind_speeds, turbulence_intensities=0.06 +) +fmodel.set(wind_data=time_series) +fmodel_hs_1.set(wind_data=time_series) + +# Calculate wakes with baseline yaw +fmodel.run() +fmodel_hs_1.run() + +# Collect the turbine powers in kW +turbine_powers = fmodel.get_turbine_powers() / 1000.0 +turbine_powers_hs_1 = fmodel_hs_1.get_turbine_powers() / 1000.0 + +# Plot the power in each case and the difference in power +fig, axarr = plt.subplots(1, 3, sharex=True, figsize=(12, 4)) + +for t_idx in range(3): + ax = axarr[t_idx] + ax.plot(wind_speeds, turbine_powers[:, t_idx], color="k", label="Hs=3.1 (5)") + ax.plot(wind_speeds, turbine_powers_hs_1[:, t_idx], color="r", label="Hs=1.0") + ax.grid(True) + ax.set_xlabel("Wind Speed (m/s)") + ax.set_title(f"Turbine {t_idx}") + +axarr[0].set_ylabel("Power (kW)") +axarr[0].legend() +fig.suptitle("Power of each turbine") + +plt.show() diff --git a/examples/18_check_turbine.py b/examples/examples_turbine/001_check_turbine.py similarity index 66% rename from examples/18_check_turbine.py rename to examples/examples_turbine/001_check_turbine.py index 258525340..7291ca60c 100644 --- a/examples/18_check_turbine.py +++ b/examples/examples_turbine/001_check_turbine.py @@ -1,5 +1,9 @@ +"""Example: Check turbine power curves + +For each turbine in the turbine library, make a small figure showing that its power +curve and power loss to yaw are reasonable and reasonably smooth +""" -from pathlib import Path import matplotlib.pyplot as plt import numpy as np @@ -7,27 +11,21 @@ from floris import FlorisModel -""" -For each turbine in the turbine library, make a small figure showing that its power -curve and power loss to yaw are reasonable and reasonably smooth -""" -ws_array = np.arange(0.1,30,0.2) +ws_array = np.arange(0.1, 30, 0.2) wd_array = 270.0 * np.ones_like(ws_array) turbulence_intensities = 0.06 * np.ones_like(ws_array) -yaw_angles = np.linspace(-30,30,60) +yaw_angles = np.linspace(-30, 30, 60) wind_speed_to_test_yaw = 11 # Grab the gch model -fmodel = FlorisModel("inputs/gch.yaml") +fmodel = FlorisModel("../inputs/gch.yaml") # Make one turbine simulation fmodel.set(layout_x=[0], layout_y=[0]) # Apply wind directions and wind speeds fmodel.set( - wind_speeds=ws_array, - wind_directions=wd_array, - turbulence_intensities=turbulence_intensities + wind_speeds=ws_array, wind_directions=wd_array, turbulence_intensities=turbulence_intensities ) # Get a list of available turbine models provided through FLORIS, and remove @@ -39,11 +37,10 @@ ] # Declare a set of figures for comparing cp and ct across models -fig_pow_ct, axarr_pow_ct = plt.subplots(2,1,sharex=True,figsize=(10,10)) +fig_pow_ct, axarr_pow_ct = plt.subplots(2, 1, sharex=True, figsize=(10, 10)) # For each turbine model available plot the basic info for t in turbines: - # Set t as the turbine fmodel.set(turbine_type=[t]) @@ -53,26 +50,27 @@ # Plot power and ct onto the fig_pow_ct plot axarr_pow_ct[0].plot( fmodel.core.farm.turbine_map[0].power_thrust_table["wind_speed"], - fmodel.core.farm.turbine_map[0].power_thrust_table["power"],label=t + fmodel.core.farm.turbine_map[0].power_thrust_table["power"], + label=t, ) axarr_pow_ct[0].grid(True) axarr_pow_ct[0].legend() - axarr_pow_ct[0].set_ylabel('Power (kW)') + axarr_pow_ct[0].set_ylabel("Power (kW)") axarr_pow_ct[1].plot( fmodel.core.farm.turbine_map[0].power_thrust_table["wind_speed"], - fmodel.core.farm.turbine_map[0].power_thrust_table["thrust_coefficient"],label=t + fmodel.core.farm.turbine_map[0].power_thrust_table["thrust_coefficient"], + label=t, ) axarr_pow_ct[1].grid(True) axarr_pow_ct[1].legend() - axarr_pow_ct[1].set_ylabel('Ct (-)') - axarr_pow_ct[1].set_xlabel('Wind Speed (m/s)') + axarr_pow_ct[1].set_ylabel("Ct (-)") + axarr_pow_ct[1].set_xlabel("Wind Speed (m/s)") # Create a figure - fig, axarr = plt.subplots(1,2,figsize=(10,5)) + fig, axarr = plt.subplots(1, 2, figsize=(10, 5)) # Try a few density - for density in [1.15,1.225,1.3]: - + for density in [1.15, 1.225, 1.3]: fmodel.set(air_density=density) # POWER CURVE @@ -80,18 +78,18 @@ fmodel.set( wind_speeds=ws_array, wind_directions=wd_array, - turbulence_intensities=turbulence_intensities + turbulence_intensities=turbulence_intensities, ) fmodel.run() turbine_powers = fmodel.get_turbine_powers().flatten() / 1e3 if density == 1.225: - ax.plot(ws_array,turbine_powers,label='Air Density = %.3f' % density, lw=2, color='k') + ax.plot(ws_array, turbine_powers, label="Air Density = %.3f" % density, lw=2, color="k") else: - ax.plot(ws_array,turbine_powers,label='Air Density = %.3f' % density, lw=1) + ax.plot(ws_array, turbine_powers, label="Air Density = %.3f" % density, lw=1) ax.grid(True) ax.legend() - ax.set_xlabel('Wind Speed (m/s)') - ax.set_ylabel('Power (kW)') + ax.set_xlabel("Wind Speed (m/s)") + ax.set_ylabel("Power (kW)") # Power loss to yaw, try a range of yaw angles ax = axarr[1] @@ -99,7 +97,7 @@ fmodel.set( wind_speeds=[wind_speed_to_test_yaw], wind_directions=[270.0], - turbulence_intensities=[0.06] + turbulence_intensities=[0.06], ) yaw_result = [] for yaw in yaw_angles: @@ -108,15 +106,15 @@ turbine_powers = fmodel.get_turbine_powers().flatten() / 1e3 yaw_result.append(turbine_powers[0]) if density == 1.225: - ax.plot(yaw_angles,yaw_result,label='Air Density = %.3f' % density, lw=2, color='k') + ax.plot(yaw_angles, yaw_result, label="Air Density = %.3f" % density, lw=2, color="k") else: - ax.plot(yaw_angles,yaw_result,label='Air Density = %.3f' % density, lw=1) + ax.plot(yaw_angles, yaw_result, label="Air Density = %.3f" % density, lw=1) # ax.plot(yaw_angles,yaw_result,label='Air Density = %.3f' % density) ax.grid(True) ax.legend() - ax.set_xlabel('Yaw Error (deg)') - ax.set_ylabel('Power (kW)') - ax.set_title('Wind Speed = %.1f' % wind_speed_to_test_yaw ) + ax.set_xlabel("Yaw Error (deg)") + ax.set_ylabel("Power (kW)") + ax.set_title("Wind Speed = %.1f" % wind_speed_to_test_yaw) # Give a suptitle fig.suptitle(t) diff --git a/examples/17_multiple_turbine_types.py b/examples/examples_turbine/002_multiple_turbine_types.py similarity index 87% rename from examples/17_multiple_turbine_types.py rename to examples/examples_turbine/002_multiple_turbine_types.py index b7d1c4173..b945d5a0a 100644 --- a/examples/17_multiple_turbine_types.py +++ b/examples/examples_turbine/002_multiple_turbine_types.py @@ -1,3 +1,9 @@ +"""Example: Multiple turbine types + +This example uses an input file where multiple turbine types are defined. +The first two turbines are the NREL 5MW, and the third turbine is the IEA 10MW. +""" + import matplotlib.pyplot as plt @@ -5,24 +11,17 @@ from floris import FlorisModel -""" -This example uses an input file where multiple turbine types are defined. -The first two turbines are the NREL 5MW, and the third turbine is the IEA 10MW. -""" - # Initialize FLORIS with the given input file. # For basic usage, FlorisModel provides a simplified and expressive # entry point to the simulation routines. -fmodel = FlorisModel("inputs/gch_multiple_turbine_types.yaml") +fmodel = FlorisModel("../inputs/gch_multiple_turbine_types.yaml") # Using the FlorisModel functions for generating plots, run FLORIS # and extract 2D planes of data. horizontal_plane = fmodel.calculate_horizontal_plane(x_resolution=200, y_resolution=100, height=90) y_plane = fmodel.calculate_y_plane(x_resolution=200, z_resolution=100, crossstream_dist=0.0) cross_plane = fmodel.calculate_cross_plane( - y_resolution=100, - z_resolution=100, - downstream_dist=500.0 + y_resolution=100, z_resolution=100, downstream_dist=500.0 ) # Create the plots diff --git a/examples/33_specify_turbine_power_curve.py b/examples/examples_turbine/003_specify_turbine_power_curve.py similarity index 66% rename from examples/33_specify_turbine_power_curve.py rename to examples/examples_turbine/003_specify_turbine_power_curve.py index 420f5aeab..1c1b59707 100644 --- a/examples/33_specify_turbine_power_curve.py +++ b/examples/examples_turbine/003_specify_turbine_power_curve.py @@ -1,12 +1,5 @@ +"""Example: Specify turbine power curve -import matplotlib.pyplot as plt -import numpy as np - -from floris import FlorisModel -from floris.turbine_library import build_cosine_loss_turbine_dict - - -""" This example demonstrates how to specify a turbine model based on a power and thrust curve for the wind turbine, as well as possible physical parameters (which default to the parameters of the NREL 5MW reference turbine). @@ -15,14 +8,21 @@ argument to build_turbine_dict is set. """ +import matplotlib.pyplot as plt +import numpy as np + +from floris import FlorisModel +from floris.turbine_library import build_cosine_loss_turbine_dict + + # Generate an example turbine power and thrust curve for use in the FLORIS model powers_orig = np.array([0, 30, 200, 500, 1000, 2000, 4000, 4000, 4000, 4000, 4000]) wind_speeds = np.array([0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]) -power_coeffs = powers_orig[1:]/(0.5*126.**2*np.pi/4*1.225*wind_speeds[1:]**3) +power_coeffs = powers_orig[1:] / (0.5 * 126.0**2 * np.pi / 4 * 1.225 * wind_speeds[1:] ** 3) turbine_data_dict = { - "wind_speed":list(wind_speeds), - "power_coefficient":[0]+list(power_coeffs), - "thrust_coefficient":[0, 0.9, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.25, 0.2] + "wind_speed": list(wind_speeds), + "power_coefficient": [0] + list(power_coeffs), + "thrust_coefficient": [0, 0.9, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.25, 0.2], } turbine_dict = build_cosine_loss_turbine_dict( @@ -36,10 +36,10 @@ rotor_diameter=126, TSR=8, ref_air_density=1.225, - ref_tilt=5 + ref_tilt=5, ) -fmodel = FlorisModel("inputs/gch.yaml") +fmodel = FlorisModel("../inputs/gch.yaml") wind_speeds = np.linspace(1, 15, 100) wind_directions = 270 * np.ones_like(wind_speeds) turbulence_intensities = 0.06 * np.ones_like(wind_speeds) @@ -50,7 +50,7 @@ wind_directions=wind_directions, wind_speeds=wind_speeds, turbulence_intensities=turbulence_intensities, - turbine_type=[turbine_dict] + turbine_type=[turbine_dict], ) fmodel.run() @@ -58,16 +58,20 @@ specified_powers = ( np.array(turbine_data_dict["power_coefficient"]) - *0.5*turbine_dict["power_thrust_table"]["ref_air_density"] - *turbine_dict["rotor_diameter"]**2*np.pi/4 - *np.array(turbine_data_dict["wind_speed"])**3 -)/1000 + * 0.5 + * turbine_dict["power_thrust_table"]["ref_air_density"] + * turbine_dict["rotor_diameter"] ** 2 + * np.pi + / 4 + * np.array(turbine_data_dict["wind_speed"]) ** 3 +) / 1000 -fig, ax = plt.subplots(1,1,sharex=True) +fig, ax = plt.subplots(1, 1, sharex=True) -ax.scatter(wind_speeds, powers/1000, color="C0", s=5, label="Test points") -ax.scatter(turbine_data_dict["wind_speed"], specified_powers, - color="red", s=20, label="Specified points") +ax.scatter(wind_speeds, powers / 1000, color="C0", s=5, label="Test points") +ax.scatter( + turbine_data_dict["wind_speed"], specified_powers, color="red", s=20, label="Specified points" +) ax.grid() ax.set_xlabel("Wind speed [m/s]") diff --git a/examples/examples_uncertain/001_uncertain_model_params.py b/examples/examples_uncertain/001_uncertain_model_params.py new file mode 100644 index 000000000..b03d91500 --- /dev/null +++ b/examples/examples_uncertain/001_uncertain_model_params.py @@ -0,0 +1,170 @@ +"""Example 8: Uncertain Model Parameters + +""" + +import matplotlib.pyplot as plt +import numpy as np + +from floris import ( + FlorisModel, + TimeSeries, + UncertainFlorisModel, +) + + +# Instantiate FlorisModel for comparison +fmodel = FlorisModel("../inputs/gch.yaml") # GCH model + +################################################ +# Resolution parameters +################################################ + +# The resolution parameters are used to define the precision of the wind direction, +# wind speed, and turbulence intensity and control parameters. All the inputs +# passed into the UncertainFlorisModel class are rounded to this resolution. Then +# following expansion, non-unique cases are removed. Here we apply the default +# resolution parameters. +wd_resolution = 1.0 # Degree +ws_resolution = 1.0 # m/s +ti_resolution = 0.01 # Decimal fraction +yaw_resolution = 1.0 # Degree +power_setpoint_resolution = 100.0 # kW + +################################################ +# wd_sample_points +################################################ + +# The wind direction sample points (wd_sample_points) parameter is used to define +# the number of points to sample the wind direction uncertainty. For example, +# if the the single condition to analyze is 270 degrees, and the wd_sample_points +# is [-2, -1, 0, 1 ,2], then the cases to be run and weighted +# will be 268, 269, 270, 271, 272. If not supplied default is +# [-2 * wd_std, -1 * wd_std, 0, wd_std, 2 * wd_std] +wd_sample_points = [-6, -3, 0, 3, 6] + + +################################################ +# WT_STD +################################################ + +# The wind direction standard deviation (wd_std) parameter is the primary input +# to the UncertainFlorisModel class. This parameter is used to weight the points +# following expansion by the wd_sample_points. The smaller the value, the closer +# the weighting will be to the nominal case. +wd_std = 3 # Default is 3 degrees + +################################################ +# Verbosity +################################################ + +# Setting verbose = True will print out the sizes of teh cases run +verbose = True + +################################################ +# Define the UncertainFlorisModel +################################################ +print('*** Instantiating UncertainFlorisModel ***') +ufmodel = UncertainFlorisModel("../inputs/gch.yaml", + wd_resolution=wd_resolution, + ws_resolution=ws_resolution, + ti_resolution=ti_resolution, + yaw_resolution=yaw_resolution, + power_setpoint_resolution=power_setpoint_resolution, + wd_std=wd_std, + wd_sample_points=wd_sample_points, + verbose=verbose) + + +################################################ +# Run the models +################################################ + +# Define an inflow where wind direction is swept while +# wind speed and turbulence intensity are held constant +wind_directions = np.arange(240.0, 300.0, 1.0) +time_series = TimeSeries( + wind_directions=wind_directions, + wind_speeds=8.0, + turbulence_intensities=0.06, +) + +# Define a two turbine farm and apply the inflow +D = 126.0 +layout_x = np.array([0, D * 6]) +layout_y = [0, 0] + +fmodel.set( + layout_x=layout_x, + layout_y=layout_y, + wind_data=time_series, +) +print('*** Setting UncertainFlorisModel to 60 Wind Direction Inflow ***') +ufmodel.set( + layout_x=layout_x, + layout_y=layout_y, + wind_data=time_series, +) + +# Run both models +fmodel.run() +ufmodel.run() + + +# Collect the nominal and uncertain farm power +turbine_powers_nom = fmodel.get_turbine_powers() / 1e3 +turbine_powers_unc = ufmodel.get_turbine_powers() / 1e3 + +farm_powers_nom = fmodel.get_farm_power() / 1e3 +farm_powers_unc_3 = ufmodel.get_farm_power() / 1e3 + + +# Plot results +fig, axarr = plt.subplots(1, 3, figsize=(15, 5)) +ax = axarr[0] +ax.plot(wind_directions, turbine_powers_nom[:, 0].flatten(), color="k", label="Nominal power") +ax.plot( + wind_directions, + turbine_powers_unc[:, 0].flatten(), + color="r", + label="Power with uncertainty", +) + +ax.grid(True) +ax.legend() +ax.set_xlabel("Wind Direction (deg)") +ax.set_ylabel("Power (kW)") +ax.set_title("Upstream Turbine") + +ax = axarr[1] +ax.plot(wind_directions, turbine_powers_nom[:, 1].flatten(), color="k", label="Nominal power") +ax.plot( + wind_directions, + turbine_powers_unc[:, 1].flatten(), + color="r", + label="Power with uncertainty", +) + +ax.set_title("Downstream Turbine") +ax.grid(True) +ax.legend() +ax.set_xlabel("Wind Direction (deg)") +ax.set_ylabel("Power (kW)") + +ax = axarr[2] +ax.plot(wind_directions, farm_powers_nom.flatten(), color="k", label="Nominal farm power") +ax.plot( + wind_directions, + farm_powers_unc_3.flatten(), + color="r", + label="Farm power with uncertainty", +) + + +ax.set_title("Farm Power") +ax.grid(True) +ax.legend() +ax.set_xlabel("Wind Direction (deg)") +ax.set_ylabel("Power (kW)") + + +plt.show() diff --git a/examples/examples_uncertain/002_yaw_inertial_frame.py b/examples/examples_uncertain/002_yaw_inertial_frame.py new file mode 100644 index 000000000..613c0348d --- /dev/null +++ b/examples/examples_uncertain/002_yaw_inertial_frame.py @@ -0,0 +1 @@ +#TODO add example here diff --git a/examples/23_layout_visualizations.py b/examples/examples_visualizations/001_layout_visualizations.py similarity index 69% rename from examples/23_layout_visualizations.py rename to examples/examples_visualizations/001_layout_visualizations.py index 465490e6e..cbf46a52a 100644 --- a/examples/23_layout_visualizations.py +++ b/examples/examples_visualizations/001_layout_visualizations.py @@ -1,3 +1,8 @@ +"""Example: Layout Visualizations + +Demonstrate the use of all the functions within the layout_visualization module + +""" import matplotlib.pyplot as plt import numpy as np @@ -7,10 +12,6 @@ from floris.flow_visualization import visualize_cut_plane -""" -This example shows a number of different ways to visualize a farm layout using FLORIS -""" - # Create the plotting objects using matplotlib fig, axarr = plt.subplots(3, 3, figsize=(16, 10), sharex=False) axarr = axarr.flatten() @@ -19,7 +20,7 @@ MAX_WS = 8.0 # Initialize FLORIS with the given input file. -fmodel = FlorisModel("inputs/gch.yaml") +fmodel = FlorisModel("../inputs/gch.yaml") # Change to 5-turbine layout with a wind direction from northwest fmodel.set( @@ -38,31 +39,25 @@ ) # Plot the turbine points, setting the color to white layoutviz.plot_turbine_points(fmodel, ax=ax, plotting_dict={"color": "w"}) -ax.set_title('Flow visualization and turbine points') +ax.set_title("Flow visualization and turbine points") # Plot 2: Show a particular flow case ax = axarr[1] turbine_names = [f"T{i}" for i in [10, 11, 12, 13, 22]] layoutviz.plot_turbine_points(fmodel, ax=ax) -layoutviz.plot_turbine_labels(fmodel, - ax=ax, - turbine_names=turbine_names, - show_bbox=True, - bbox_dict={'facecolor':'r'}) +layoutviz.plot_turbine_labels( + fmodel, ax=ax, turbine_names=turbine_names, show_bbox=True, bbox_dict={"facecolor": "r"} +) ax.set_title("Show turbine names with a red bounding box") # Plot 2: Show turbine rotors on flow ax = axarr[2] -horizontal_plane = fmodel.calculate_horizontal_plane(height=90.0, - yaw_angles=np.array([[0., 30., 0., 0., 0.]])) -visualize_cut_plane( - horizontal_plane, - ax=ax, - min_speed=MIN_WS, - max_speed=MAX_WS +horizontal_plane = fmodel.calculate_horizontal_plane( + height=90.0, yaw_angles=np.array([[0.0, 30.0, 0.0, 0.0, 0.0]]) ) -layoutviz.plot_turbine_rotors(fmodel,ax=ax,yaw_angles=np.array([[0., 30., 0., 0., 0.]])) +visualize_cut_plane(horizontal_plane, ax=ax, min_speed=MIN_WS, max_speed=MAX_WS) +layoutviz.plot_turbine_rotors(fmodel, ax=ax, yaw_angles=np.array([[0.0, 30.0, 0.0, 0.0, 0.0]])) ax.set_title("Flow visualization with yawed turbine") # Plot 3: Show the layout, including wake directions @@ -74,15 +69,17 @@ # Plot 4: Plot a subset of the layout, and limit directions less than 7D ax = axarr[4] -layoutviz.plot_turbine_points(fmodel, ax=ax, turbine_indices=[0,1,2,3]) -layoutviz.plot_turbine_labels(fmodel, ax=ax, turbine_names=turbine_names, turbine_indices=[0,1,2,3]) -layoutviz.plot_waking_directions(fmodel, ax=ax, turbine_indices=[0,1,2,3], limit_dist_D=7) +layoutviz.plot_turbine_points(fmodel, ax=ax, turbine_indices=[0, 1, 2, 3]) +layoutviz.plot_turbine_labels( + fmodel, ax=ax, turbine_names=turbine_names, turbine_indices=[0, 1, 2, 3] +) +layoutviz.plot_waking_directions(fmodel, ax=ax, turbine_indices=[0, 1, 2, 3], limit_dist_D=7) ax.set_title("Plot a subset and limit wake line distance") # Plot with a shaded region ax = axarr[5] layoutviz.plot_turbine_points(fmodel, ax=ax) -layoutviz.shade_region(np.array([[0,0],[300,0],[300,1000],[0,700]]),ax=ax) +layoutviz.shade_region(np.array([[0, 0], [300, 0], [300, 1000], [0, 700]]), ax=ax) ax.set_title("Plot with a shaded region") # Change hub heights and plot as a proxy for terrain diff --git a/examples/examples_visualizations/002_visualize_y_cut_plane.py b/examples/examples_visualizations/002_visualize_y_cut_plane.py new file mode 100644 index 000000000..7e9ef8cd4 --- /dev/null +++ b/examples/examples_visualizations/002_visualize_y_cut_plane.py @@ -0,0 +1,33 @@ +"""Example: Visualize y cut plane + +Demonstrate visualizing a plane cut vertically through the flow field along the wind direction. + +""" + +import matplotlib.pyplot as plt + +from floris import FlorisModel +from floris.flow_visualization import visualize_cut_plane + + +fmodel = FlorisModel("../inputs/gch.yaml") + +# Set a 3 turbine layout with wind direction along the row +fmodel.set( + layout_x=[0, 500, 1000], + layout_y=[0, 0, 0], + wind_directions=[270], + wind_speeds=[8], + turbulence_intensities=[0.06], +) + +# Collect the yplane +y_plane = fmodel.calculate_y_plane(x_resolution=200, z_resolution=100, crossstream_dist=0.0) + +# Plot the flow field +fig, ax = plt.subplots(figsize=(10, 4)) +visualize_cut_plane( + y_plane, ax=ax, min_speed=3, max_speed=9, label_contours=True, title="Y Cut Plane" +) + +plt.show() diff --git a/examples/examples_visualizations/003_visualize_cross_plane.py b/examples/examples_visualizations/003_visualize_cross_plane.py new file mode 100644 index 000000000..1aa00006e --- /dev/null +++ b/examples/examples_visualizations/003_visualize_cross_plane.py @@ -0,0 +1,37 @@ +"""Example: Visualize cross plane + +Demonstrate visualizing a plane cut vertically through the flow field across the wind direction. + +""" + +import matplotlib.pyplot as plt + +from floris import FlorisModel +from floris.flow_visualization import visualize_cut_plane + + +fmodel = FlorisModel("../inputs/gch.yaml") + +# Set a 1 turbine layout +fmodel.set( + layout_x=[0], + layout_y=[0], + wind_directions=[270], + wind_speeds=[8], + turbulence_intensities=[0.06], +) + +# Collect the cross plane downstream of the turbine +cross_plane = fmodel.calculate_cross_plane( + y_resolution=100, + z_resolution=100, + downstream_dist=500.0, +) + +# Plot the flow field +fig, ax = plt.subplots(figsize=(4, 6)) +visualize_cut_plane( + cross_plane, ax=ax, min_speed=3, max_speed=9, label_contours=True, title="Cross Plane" +) + +plt.show() diff --git a/examples/examples_visualizations/004_visualize_rotor_values.py b/examples/examples_visualizations/004_visualize_rotor_values.py new file mode 100644 index 000000000..e1d40c14b --- /dev/null +++ b/examples/examples_visualizations/004_visualize_rotor_values.py @@ -0,0 +1,33 @@ +"""Example: Visualize rotor velocities + +Demonstrate visualizing the flow velocities at the rotor using plot_rotor_values + +""" + +import matplotlib.pyplot as plt + +import floris.flow_visualization as flowviz +from floris import FlorisModel + + +fmodel = FlorisModel("../inputs/gch.yaml") + +# Set a 2 turbine layout +fmodel.set( + layout_x=[0, 500], + layout_y=[0, 0], + wind_directions=[270], + wind_speeds=[8], + turbulence_intensities=[0.06], +) + +# Run the model +fmodel.run() + +# Plot the values at each rotor +fig, axes, _, _ = flowviz.plot_rotor_values( + fmodel.core.flow_field.u, findex=0, n_rows=1, n_cols=2, return_fig_objects=True +) +fig.suptitle("Rotor Plane Visualization, Original Resolution") + +plt.show() diff --git a/examples/examples_visualizations/005_visualize_flow_by_sweeping_turbines.py b/examples/examples_visualizations/005_visualize_flow_by_sweeping_turbines.py new file mode 100644 index 000000000..3614e74bc --- /dev/null +++ b/examples/examples_visualizations/005_visualize_flow_by_sweeping_turbines.py @@ -0,0 +1,43 @@ +"""Example: Visualize flow by sweeping turbines + +Demonstrate the use calculate_horizontal_plane_with_turbines + +""" + +import matplotlib.pyplot as plt + +import floris.flow_visualization as flowviz +from floris import FlorisModel + + +fmodel = FlorisModel("../inputs/gch.yaml") + +# # Some wake models may not yet have a visualization method included, for these cases can use +# # a slower version which scans a turbine model to produce the horizontal flow + + +# Set a 2 turbine layout +fmodel.set( + layout_x=[0, 500], + layout_y=[0, 0], + wind_directions=[270], + wind_speeds=[8], + turbulence_intensities=[0.06], +) + +horizontal_plane_scan_turbine = flowviz.calculate_horizontal_plane_with_turbines( + fmodel, + x_resolution=20, + y_resolution=10, +) + +fig, ax = plt.subplots(figsize=(10, 4)) +flowviz.visualize_cut_plane( + horizontal_plane_scan_turbine, + ax=ax, + label_contours=True, + title="Horizontal (coarse turbine scan method)", +) + + +plt.show() diff --git a/examples/34_wind_data.py b/examples/examples_wind_data/001_wind_data_comparisons.py similarity index 53% rename from examples/34_wind_data.py rename to examples/examples_wind_data/001_wind_data_comparisons.py index 0d17e7924..9dbbe07c7 100644 --- a/examples/34_wind_data.py +++ b/examples/examples_wind_data/001_wind_data_comparisons.py @@ -1,3 +1,19 @@ +"""Example: Wind Data Comparisons + +In this example, a random time series of wind speeds, wind directions, turbulence +intensities, and values is generated. Value represents the value of the power +generated at each time step or wind condition (e.g., the price of electricity). This +can then be used in later optimization methods to optimize for total value instead of +energy. This time series is then used to instantiate a TimeSeries object. The TimeSeries +object is then used to instantiate a WindRose object and WindTIRose object based on the +same data. The three objects are then each used to drive a FLORIS model of a simple +two-turbine wind farm. The annual energy production (AEP) and annual value production +(AVP) outputs are then compared and printed to the console. + +""" + + + import matplotlib.pyplot as plt import numpy as np @@ -9,21 +25,15 @@ from floris.utilities import wrap_360 -""" -This example is meant to be temporary and may be updated by a later pull request. Before we -release v4, we intend to propagate the TimeSeries and WindRose objects through the other relevant -examples, and change this example to demonstrate more advanced (as yet, not implemented) -functionality of the WindData objects (such as electricity pricing etc). -""" - - -# Generate a random time series of wind speeds, wind directions and turbulence intensities +# Generate a random time series of wind speeds, wind directions, turbulence +# intensities, and values. In this case let's treat value as the dollars per MWh. N = 500 wd_array = wrap_360(270 * np.ones(N) + np.random.randn(N) * 20) ws_array = np.clip(8 * np.ones(N) + np.random.randn(N) * 8, 3, 50) ti_array = np.clip(0.1 * np.ones(N) + np.random.randn(N) * 0.05, 0, 0.25) +value_array = np.clip(25 * np.ones(N) + np.random.randn(N) * 10, 0, 100) -fig, axarr = plt.subplots(3, 1, sharex=True, figsize=(7, 4)) +fig, axarr = plt.subplots(4, 1, sharex=True, figsize=(7, 6)) ax = axarr[0] ax.plot(wd_array, marker=".", ls="None") ax.set_ylabel("Wind Direction") @@ -33,10 +43,13 @@ ax = axarr[2] ax.plot(ti_array, marker=".", ls="None") ax.set_ylabel("Turbulence Intensity") +ax = axarr[3] +ax.plot(value_array, marker=".", ls="None") +ax.set_ylabel("Value") # Build the time series -time_series = TimeSeries(wd_array, ws_array, turbulence_intensities=ti_array) +time_series = TimeSeries(wd_array, ws_array, turbulence_intensities=ti_array, values=value_array) # Now build the wind rose wind_rose = time_series.to_WindRose() @@ -59,7 +72,7 @@ plt.tight_layout() # Now set up a FLORIS model and initialize it using the time series and wind rose -fmodel = FlorisModel("inputs/gch.yaml") +fmodel = FlorisModel("../inputs/gch.yaml") fmodel.set(layout_x=[0, 500.0], layout_y=[0.0, 0.0]) fmodel_time_series = fmodel.copy() @@ -74,9 +87,9 @@ fmodel_wind_rose.run() fmodel_wind_ti_rose.run() -time_series_power = fmodel_time_series.get_farm_power() -wind_rose_power = fmodel_wind_rose.get_farm_power() -wind_ti_rose_power = fmodel_wind_ti_rose.get_farm_power() +# Now, compute AEP using the FLORIS models initialized with the three types of +# WindData objects. The AEP values are very similar but not exactly the same +# because of the effects of binning in the wind roses. time_series_aep = fmodel_time_series.get_farm_AEP() wind_rose_aep = fmodel_wind_rose.get_farm_AEP() @@ -86,4 +99,16 @@ print(f"AEP from WindRose {wind_rose_aep / 1e9:.2f} GWh") print(f"AEP from WindTIRose {wind_ti_rose_aep / 1e9:.2f} GWh") +# Now, compute annual value production (AVP) using the FLORIS models initialized +# with the three types of WindData objects. The AVP values are very similar but +# not exactly the same because of the effects of binning in the wind roses. + +time_series_avp = fmodel_time_series.get_farm_AVP() +wind_rose_avp = fmodel_wind_rose.get_farm_AVP() +wind_ti_rose_avp = fmodel_wind_ti_rose.get_farm_AVP() + +print(f"Annual Value Production (AVP) from TimeSeries {time_series_avp / 1e6:.2f} dollars") +print(f"AVP from WindRose {wind_rose_avp / 1e6:.2f} dollars") +print(f"AVP from WindTIRose {wind_ti_rose_avp / 1e6:.2f} dollars") + plt.show() diff --git a/examples/36_generate_ti.py b/examples/examples_wind_data/002_generate_ti.py similarity index 97% rename from examples/36_generate_ti.py rename to examples/examples_wind_data/002_generate_ti.py index 317bc8dbe..55bf09e4d 100644 --- a/examples/36_generate_ti.py +++ b/examples/examples_wind_data/002_generate_ti.py @@ -1,19 +1,18 @@ +"""Example: Generate TI + +Demonstrate usage of TI generating and plotting functionality in the WindRose +and TimeSeries classes + +""" + import matplotlib.pyplot as plt import numpy as np from floris import ( - FlorisModel, TimeSeries, WindRose, ) -from floris.utilities import wrap_360 - - -""" -Demonstrate usage of TI generating and plotting functionality in the WindRose -and TimeSeries classes -""" # Generate a random time series of wind speeds, wind directions and turbulence intensities diff --git a/examples/examples_wind_data/003_generate_value.py b/examples/examples_wind_data/003_generate_value.py new file mode 100644 index 000000000..af23c5522 --- /dev/null +++ b/examples/examples_wind_data/003_generate_value.py @@ -0,0 +1,81 @@ +"""Example: Generate value + +Demonstrate usage of value generating and plotting functionality in the WindRose +and TimeSeries classes. Value represents the value of the power or energy generated +at each time step or wind condition (e.g., the price of electricity in dollars/MWh). +This can then be used to compute the annual value production (AVP) instead of AEP, +or in later optimization methods to optimize for total value instead of energy. + +""" + + +import matplotlib.pyplot as plt +import numpy as np + +from floris import ( + TimeSeries, + WindRose, +) + + +# Generate a random time series of wind speeds, wind directions and turbulence intensities +wind_directions = np.array([250, 260, 270]) +wind_speeds = np.arange(3.0, 11.0, 1.0) +ti_table = 0.06 + +# Declare a WindRose object +wind_rose = WindRose(wind_directions=wind_directions, wind_speeds=wind_speeds, ti_table=ti_table) + + +# Define a custom function where value = 100 / wind_speed +def custom_value_func(wind_directions, wind_speeds): + return 100 / wind_speeds + + +wind_rose.assign_value_using_wd_ws_function(custom_value_func) + +fig, ax = plt.subplots() +wind_rose.plot_value_over_ws(ax) +ax.set_title("Value defined by custom function") + +# Now assign value using the provided assign_value_piecewise_linear method with the default +# settings. This method assigns value based on a linear piecewise function of wind speed +# (with two line segments). The default arguments produce 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. +wind_rose.assign_value_piecewise_linear( + value_zero_ws=1.425, + ws_knee=4.5, + slope_1=0.0, + slope_2=-0.135 +) +fig, ax = plt.subplots() +wind_rose.plot_value_over_ws(ax) +ax.set_title("Value defined by default piecewise linear function") + +# Demonstrate equivalent usage in time series +N = 100 +wind_directions = 270 * np.ones(N) +wind_speeds = np.linspace(3, 15, N) +turbulence_intensities = 0.06 * np.ones(N) +time_series = TimeSeries( + wind_directions=wind_directions, + wind_speeds=wind_speeds, + turbulence_intensities=turbulence_intensities +) +time_series.assign_value_piecewise_linear() + +fig, axarr = plt.subplots(2, 1, sharex=True, figsize=(7, 8)) +ax = axarr[0] +ax.plot(wind_speeds) +ax.set_ylabel("Wind Speeds (m/s)") +ax.grid(True) +ax = axarr[1] +ax.plot(time_series.values) +ax.set_ylabel("Value (normalized price/MWh)") +ax.grid(True) +fig.suptitle("Generating value in TimeSeries") + + +plt.show() diff --git a/examples/inputs/cc.yaml b/examples/inputs/cc.yaml index af62b0021..de626ff8f 100644 --- a/examples/inputs/cc.yaml +++ b/examples/inputs/cc.yaml @@ -1,7 +1,7 @@ name: CC description: Three turbines using Cumulative Gauss Curl model -floris_version: v3.0.0 +floris_version: v4 logging: console: diff --git a/examples/inputs/emgauss.yaml b/examples/inputs/emgauss.yaml index 73344d5ea..89caef95b 100644 --- a/examples/inputs/emgauss.yaml +++ b/examples/inputs/emgauss.yaml @@ -1,7 +1,7 @@ name: Emperical Gaussian description: Three turbines using emperical Gaussian model -floris_version: v3.x +floris_version: v4 logging: console: diff --git a/examples/inputs/gch.yaml b/examples/inputs/gch.yaml index 3397839da..79b0b8629 100644 --- a/examples/inputs/gch.yaml +++ b/examples/inputs/gch.yaml @@ -12,7 +12,7 @@ description: Three turbines using Gauss Curl Hybrid model ### # The earliest verion of FLORIS this input file supports. # This is not currently only for the user's reference. -floris_version: v3.0.0 +floris_version: v4 ### # Configure the logging level and where to show the logs. diff --git a/examples/inputs/gch_heterogeneous_inflow.yaml b/examples/inputs/gch_heterogeneous_inflow.yaml index 86507e287..121457f15 100644 --- a/examples/inputs/gch_heterogeneous_inflow.yaml +++ b/examples/inputs/gch_heterogeneous_inflow.yaml @@ -1,6 +1,6 @@ name: GCH description: Three turbines using Gauss Curl Hybrid model -floris_version: v3.0.0 +floris_version: v4 logging: console: @@ -27,7 +27,7 @@ farm: flow_field: air_density: 1.225 - heterogenous_inflow_config: + heterogeneous_inflow_config: speed_multipliers: - - 2.0 - 1.0 diff --git a/examples/inputs/gch_multi_dim_cp_ct.yaml b/examples/inputs/gch_multi_dim_cp_ct.yaml index 581dd1f37..236bb63f8 100644 --- a/examples/inputs/gch_multi_dim_cp_ct.yaml +++ b/examples/inputs/gch_multi_dim_cp_ct.yaml @@ -1,7 +1,7 @@ name: GCH multi dimensional Cp/Ct description: Three turbines using GCH model -floris_version: v3.0.0 +floris_version: v4 logging: console: diff --git a/examples/inputs/gch_multiple_turbine_types.yaml b/examples/inputs/gch_multiple_turbine_types.yaml index 0ead479a1..366f4e9c0 100644 --- a/examples/inputs/gch_multiple_turbine_types.yaml +++ b/examples/inputs/gch_multiple_turbine_types.yaml @@ -1,7 +1,7 @@ name: GCH description: Three turbines using Gauss Curl Hybrid model -floris_version: v3.0.0 +floris_version: v4 logging: console: diff --git a/examples/inputs/jensen.yaml b/examples/inputs/jensen.yaml index 6b4ac0dd6..c0f95de6e 100644 --- a/examples/inputs/jensen.yaml +++ b/examples/inputs/jensen.yaml @@ -1,7 +1,7 @@ name: Jensen-Jimenez description: Three turbines using Jensen / Jimenez models -floris_version: v3.0.0 +floris_version: v4 logging: console: diff --git a/examples/inputs/turbopark.yaml b/examples/inputs/turbopark.yaml index 682b1e801..598ed87a0 100644 --- a/examples/inputs/turbopark.yaml +++ b/examples/inputs/turbopark.yaml @@ -1,7 +1,7 @@ name: TurbOPark description: Three turbines using TurbOPark model -floris_version: v3.0.0 +floris_version: v4 logging: console: diff --git a/examples/inputs_floating/emgauss_fixed.yaml b/examples/inputs_floating/emgauss_fixed.yaml index 76c3c4513..026710481 100644 --- a/examples/inputs_floating/emgauss_fixed.yaml +++ b/examples/inputs_floating/emgauss_fixed.yaml @@ -1,7 +1,7 @@ name: Emperical Gaussian description: Example of single fixed-bottom turbine -floris_version: v3.x +floris_version: v4 logging: console: diff --git a/examples/inputs_floating/emgauss_floating.yaml b/examples/inputs_floating/emgauss_floating.yaml index 965ef7549..253944aaf 100644 --- a/examples/inputs_floating/emgauss_floating.yaml +++ b/examples/inputs_floating/emgauss_floating.yaml @@ -1,7 +1,7 @@ name: Emperical Gaussian description: Example of single floating turbine -floris_version: v3.x +floris_version: v4 logging: console: diff --git a/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml b/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml index e8a452325..c34b38250 100644 --- a/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml +++ b/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml @@ -1,7 +1,7 @@ name: Emperical Gaussian floating description: Single turbine using emperical Gaussian model for floating -floris_version: v3.x +floris_version: v4 logging: console: diff --git a/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml b/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml index 7732b6213..398c6eb29 100644 --- a/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml +++ b/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml @@ -1,7 +1,7 @@ name: Emperical Gaussian floating description: Single turbine using emperical Gaussian model for floating -floris_version: v3.x +floris_version: v4 logging: console: diff --git a/examples/inputs_floating/gch_fixed.yaml b/examples/inputs_floating/gch_fixed.yaml index be03460e1..3290d6fa1 100644 --- a/examples/inputs_floating/gch_fixed.yaml +++ b/examples/inputs_floating/gch_fixed.yaml @@ -1,7 +1,7 @@ name: GCH description: Example of single fixed-bottom turbine -floris_version: v3.0.0 +floris_version: v4 logging: console: diff --git a/examples/inputs_floating/gch_floating.yaml b/examples/inputs_floating/gch_floating.yaml index 09aaa5604..c342473f6 100644 --- a/examples/inputs_floating/gch_floating.yaml +++ b/examples/inputs_floating/gch_floating.yaml @@ -2,7 +2,7 @@ name: GCH description: Example of single floating turbine -floris_version: v3.0.0 +floris_version: v4 logging: console: diff --git a/examples/inputs_floating/gch_floating_defined_floating.yaml b/examples/inputs_floating/gch_floating_defined_floating.yaml index d540c8d47..47288c718 100644 --- a/examples/inputs_floating/gch_floating_defined_floating.yaml +++ b/examples/inputs_floating/gch_floating_defined_floating.yaml @@ -1,7 +1,7 @@ name: GCH description: Example of single floating turbine where the cp/ct is calculated with floating tilt included -floris_version: v3.0.0 +floris_version: v4 logging: console: diff --git a/floris/core/core.py b/floris/core/core.py index a31583567..084f0a717 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -93,14 +93,12 @@ def __attrs_post_init__(self) -> None: turbine_diameters=self.farm.rotor_diameters, wind_directions=self.flow_field.wind_directions, grid_resolution=self.solver["turbine_grid_points"], - time_series=self.flow_field.time_series, ) elif self.solver["type"] == "turbine_cubature_grid": self.grid = TurbineCubatureGrid( turbine_coordinates=self.farm.coordinates, turbine_diameters=self.farm.rotor_diameters, wind_directions=self.flow_field.wind_directions, - time_series=self.flow_field.time_series, grid_resolution=self.solver["turbine_grid_points"], ) elif self.solver["type"] == "flow_field_grid": @@ -109,7 +107,6 @@ def __attrs_post_init__(self) -> None: turbine_diameters=self.farm.rotor_diameters, wind_directions=self.flow_field.wind_directions, grid_resolution=self.solver["flow_field_grid_points"], - time_series=self.flow_field.time_series, ) elif self.solver["type"] == "flow_field_planar_grid": self.grid = FlowFieldPlanarGrid( @@ -119,7 +116,6 @@ def __attrs_post_init__(self) -> None: normal_vector=self.solver["normal_vector"], planar_coordinate=self.solver["planar_coordinate"], grid_resolution=self.solver["flow_field_grid_points"], - time_series=self.flow_field.time_series, x1_bounds=self.solver["flow_field_bounds"][0], x2_bounds=self.solver["flow_field_bounds"][1], ) @@ -230,7 +226,6 @@ def solve_for_points(self, x, y, z): turbine_diameters=self.farm.rotor_diameters, wind_directions=self.flow_field.wind_directions, grid_resolution=1, - time_series=self.flow_field.time_series, x_center_of_rotation=self.grid.x_center_of_rotation, y_center_of_rotation=self.grid.y_center_of_rotation ) diff --git a/floris/core/farm.py b/floris/core/farm.py index c92078be6..93bd246b6 100644 --- a/floris/core/farm.py +++ b/floris/core/farm.py @@ -38,7 +38,7 @@ @define class Farm(BaseClass): """Farm is where wind power plants should be instantiated from a YAML configuration - file. The Farm will create a heterogenous set of turbines that compose a wind farm, + file. The Farm will create a heterogeneous set of turbines that compose a wind farm, validate the inputs, and then create a vectorized representation of the the turbine data. diff --git a/floris/core/flow_field.py b/floris/core/flow_field.py index 655f771a9..d28c47f27 100644 --- a/floris/core/flow_field.py +++ b/floris/core/flow_field.py @@ -28,8 +28,7 @@ class FlowField(BaseClass): air_density: float = field(converter=float) turbulence_intensities: NDArrayFloat = field(converter=floris_array_converter) reference_wind_height: float = field(converter=float) - time_series: bool = field(default=False) - heterogenous_inflow_config: dict = field(default=None) + heterogeneous_inflow_config: dict = field(default=None) multidim_conditions: dict = field(default=None) n_findex: int = field(init=False) @@ -97,19 +96,19 @@ def wind_speeds_validator(self, instance: attrs.Attribute, value: NDArrayFloat) f"wind_speeds (length = {len(self.wind_speeds)}) must have the same length" ) - @heterogenous_inflow_config.validator - def heterogenous_config_validator(self, instance: attrs.Attribute, value: dict | None) -> None: - """Using the validator method to check that the heterogenous_inflow_config dictionary has + @heterogeneous_inflow_config.validator + def heterogeneous_config_validator(self, instance: attrs.Attribute, value: dict | None) -> None: + """Using the validator method to check that the heterogeneous_inflow_config dictionary has the correct key-value pairs. """ if value is None: return - # Check that the correct keys are supplied for the heterogenous_inflow_config dict + # Check that the correct keys are supplied for the heterogeneous_inflow_config dict for k in ["speed_multipliers", "x", "y"]: if k not in value.keys(): raise ValueError( - "heterogenous_inflow_config must contain entries for 'speed_multipliers'," + "heterogeneous_inflow_config must contain entries for 'speed_multipliers'," f"'x', and 'y', with 'z' optional. Missing '{k}'." ) if "z" not in value: @@ -131,7 +130,7 @@ def het_map_validator(self, instance: attrs.Attribute, value: list | None) -> No def __attrs_post_init__(self) -> None: - if self.heterogenous_inflow_config is not None: + if self.heterogeneous_inflow_config is not None: self.generate_heterogeneous_wind_map() @@ -165,8 +164,8 @@ def initialize_velocity_field(self, grid: Grid) -> None: # grid locations are determined in either 2 or 3 dimensions. else: bounds = np.array(list(zip( - self.heterogenous_inflow_config['x'], - self.heterogenous_inflow_config['y'] + self.heterogeneous_inflow_config['x'], + self.heterogeneous_inflow_config['y'] ))) hull = ConvexHull(bounds) polygon = Polygon(bounds[hull.vertices]) @@ -273,7 +272,7 @@ def generate_heterogeneous_wind_map(self): map bounds. Args: - heterogenous_inflow_config (dict): The heterogeneous inflow configuration dictionary. + heterogeneous_inflow_config (dict): The heterogeneous inflow configuration dictionary. The configuration should have the following inputs specified. - **speed_multipliers** (list): A list of speed up factors that will multiply the specified freestream wind speed. This 2-dimensional array should have an @@ -282,10 +281,10 @@ def generate_heterogeneous_wind_map(self): - **y**: A list of y locations at which the speed up factors are defined. - **z** (optional): A list of z locations at which the speed up factors are defined. """ - speed_multipliers = self.heterogenous_inflow_config['speed_multipliers'] - x = self.heterogenous_inflow_config['x'] - y = self.heterogenous_inflow_config['y'] - z = self.heterogenous_inflow_config['z'] + speed_multipliers = self.heterogeneous_inflow_config['speed_multipliers'] + x = self.heterogeneous_inflow_config['x'] + y = self.heterogeneous_inflow_config['y'] + z = self.heterogeneous_inflow_config['z'] if z is not None: # Compute the 3-dimensional interpolants for each wind direction diff --git a/floris/core/grid.py b/floris/core/grid.py index 3dc6280ae..9076e01e2 100644 --- a/floris/core/grid.py +++ b/floris/core/grid.py @@ -45,15 +45,12 @@ class Grid(ABC, BaseClass): arrays with shape (N coordinates, 3). turbine_diameters (:py:obj:`NDArrayFloat`): The rotor diameters of each turbine. wind_directions (:py:obj:`NDArrayFloat`): Wind directions supplied by the user. - time_series (:py:obj:`bool`): Flag to indicate whether the supplied wind data is a time - series. grid_resolution (:py:obj:`int` | :py:obj:`Iterable(int,)`): Grid resolution with values specific to each grid type. """ turbine_coordinates: NDArrayFloat = field(converter=floris_array_converter) turbine_diameters: NDArrayFloat = field(converter=floris_array_converter) wind_directions: NDArrayFloat = field(converter=floris_array_converter) - time_series: bool = field() grid_resolution: int | Iterable = field() n_turbines: int = field(init=False) @@ -116,8 +113,6 @@ class TurbineGrid(Grid): arrays with shape (N coordinates, 3). turbine_diameters (:py:obj:`NDArrayFloat`): The rotor diameters of each turbine. wind_directions (:py:obj:`NDArrayFloat`): Wind directions supplied by the user. - time_series (:py:obj:`bool`): Flag to indicate whether the supplied wind data is a time - series. grid_resolution (:py:obj:`int`): The number of points in each direction of the square grid on the rotor plane. For example, grid_resolution=3 creates a 3x3 grid within the rotor swept area. @@ -275,8 +270,6 @@ class TurbineCubatureGrid(Grid): arrays with shape (N coordinates, 3). turbine_diameters (:py:obj:`NDArrayFloat`): The rotor diameters of each turbine. wind_directions (:py:obj:`NDArrayFloat`): Wind directions supplied by the user. - time_series (:py:obj:`bool`): Flag to indicate whether the supplied wind data is a time - series. grid_resolution (:py:obj:`int`): The number of points to include in the cubature method. This value must be in the range [1, 10], and the corresponding cubature weights are set automatically. @@ -438,8 +431,6 @@ class FlowFieldGrid(Grid): arrays with shape (N coordinates, 3). turbine_diameters (:py:obj:`NDArrayFloat`): The rotor diameters of each turbine. wind_directions (:py:obj:`NDArrayFloat`): Wind directions supplied by the user. - time_series (:py:obj:`bool`): Flag to indicate whether the supplied wind data is a time - series. grid_resolution (:py:obj:`Iterable(int,)`): The number of grid points to create in each planar direction. Must be 3 components for resolution in the x, y, and z directions. """ @@ -509,8 +500,6 @@ class FlowFieldPlanarGrid(Grid): arrays with shape (N coordinates, 3). turbine_diameters (:py:obj:`NDArrayFloat`): The rotor diameters of each turbine. wind_directions (:py:obj:`NDArrayFloat`): Wind directions supplied by the user. - time_series (:py:obj:`bool`): Flag to indicate whether the supplied wind data is a time - series. grid_resolution (:py:obj:`Iterable(int,)`): The number of grid points to create in each planar direction. Must be 2 components for resolution in the x and y directions. The z direction is set to 3 planes at -10.0, 0.0, and +10.0 relative to the @@ -626,8 +615,6 @@ class PointsGrid(Grid): turbine_diameters (:py:obj:`NDArrayFloat`): Not used for PointsGrid, but required for the `Grid` super-class. wind_directions (:py:obj:`NDArrayFloat`): Wind directions supplied by the user. - time_series (:py:obj:`bool`): Not used for PointsGrid, but - required for the `Grid` super-class. grid_resolution (:py:obj:`int` | :py:obj:`Iterable(int,)`): Not used for PointsGrid, but required for the `Grid` super-class. diff --git a/floris/core/solver.py b/floris/core/solver.py index 00abcc129..a21978156 100644 --- a/floris/core/solver.py +++ b/floris/core/solver.py @@ -281,7 +281,6 @@ def full_flow_sequential_solver( turbine_diameters=turbine_grid_farm.rotor_diameters, wind_directions=turbine_grid_flow_field.wind_directions, grid_resolution=3, - time_series=turbine_grid_flow_field.time_series, ) turbine_grid_farm.expand_farm_properties( turbine_grid_flow_field.n_findex, @@ -703,7 +702,6 @@ def full_flow_cc_solver( turbine_diameters=turbine_grid_farm.rotor_diameters, wind_directions=turbine_grid_flow_field.wind_directions, grid_resolution=3, - time_series=turbine_grid_flow_field.time_series, ) turbine_grid_farm.expand_farm_properties( turbine_grid_flow_field.n_findex, @@ -1326,7 +1324,6 @@ def full_flow_empirical_gauss_solver( turbine_diameters=turbine_grid_farm.rotor_diameters, wind_directions=turbine_grid_flow_field.wind_directions, grid_resolution=3, - time_series=turbine_grid_flow_field.time_series, ) turbine_grid_farm.expand_farm_properties( turbine_grid_flow_field.n_findex, diff --git a/floris/core/turbine/operation_models.py b/floris/core/turbine/operation_models.py index 88f0f4fac..a4ddfddfe 100644 --- a/floris/core/turbine/operation_models.py +++ b/floris/core/turbine/operation_models.py @@ -396,7 +396,8 @@ def power( power_setpoints: NDArrayFloat, **kwargs ): - yaw_angles_mask = yaw_angles > 0 + # Yaw angles mask all yaw_angles not equal to zero + yaw_angles_mask = yaw_angles != 0.0 power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask) @@ -427,7 +428,7 @@ def thrust_coefficient( power_setpoints: NDArrayFloat, **kwargs ): - yaw_angles_mask = yaw_angles > 0 + yaw_angles_mask = yaw_angles != 0.0 power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask) @@ -458,7 +459,7 @@ def axial_induction( power_setpoints: NDArrayFloat, **kwargs ): - yaw_angles_mask = yaw_angles > 0 + yaw_angles_mask = yaw_angles != 0.0 power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask) diff --git a/floris/floris_model.py b/floris/floris_model.py index d9a7ba7e3..2018a4255 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -128,7 +128,7 @@ def _reinitialize( turbine_type: list | None = None, turbine_library_path: str | Path | None = None, solver_settings: dict | None = None, - heterogenous_inflow_config=None, + heterogeneous_inflow_config=None, wind_data: type[WindDataBase] | None = None, ): """ @@ -157,8 +157,8 @@ def _reinitialize( turbine_library_path (str | Path | None, optional): Path to the turbine library. Defaults to None. solver_settings (dict | None, optional): Solver settings. Defaults to None. - heterogenous_inflow_config (None, optional): Heterogenous inflow configuration. Defaults - to None. + heterogeneous_inflow_config (None, optional): heterogeneous inflow configuration. + Defaults to None. wind_data (type[WindDataBase] | None, optional): Wind data. Defaults to None. """ # Export the floris object recursively as a dictionary @@ -171,13 +171,13 @@ def _reinitialize( (wind_directions is not None) or (wind_speeds is not None) or (turbulence_intensities is not None) - or (heterogenous_inflow_config is not None) + or (heterogeneous_inflow_config is not None) ): if wind_data is not None: raise ValueError( "If wind_data is passed to reinitialize, then do not pass wind_directions, " "wind_speeds, turbulence_intensities or " - "heterogenous_inflow_config as this is redundant" + "heterogeneous_inflow_config as this is redundant" ) elif self.wind_data is not None: self.logger.warning("Deleting stored wind_data information.") @@ -188,7 +188,7 @@ def _reinitialize( wind_directions, wind_speeds, turbulence_intensities, - heterogenous_inflow_config, + heterogeneous_inflow_config, ) = wind_data.unpack_for_reinitialize() self._wind_data = wind_data @@ -207,8 +207,8 @@ def _reinitialize( flow_field_dict["turbulence_intensities"] = turbulence_intensities if air_density is not None: flow_field_dict["air_density"] = air_density - if heterogenous_inflow_config is not None: - flow_field_dict["heterogenous_inflow_config"] = heterogenous_inflow_config + if heterogeneous_inflow_config is not None: + flow_field_dict["heterogeneous_inflow_config"] = heterogeneous_inflow_config ## Farm if layout_x is not None: @@ -302,7 +302,7 @@ def set( turbine_type: list | None = None, turbine_library_path: str | Path | None = None, solver_settings: dict | None = None, - heterogenous_inflow_config=None, + heterogeneous_inflow_config=None, wind_data: type[WindDataBase] | None = None, yaw_angles: NDArrayFloat | list[float] | None = None, power_setpoints: NDArrayFloat | list[float] | list[float, None] | None = None, @@ -330,8 +330,8 @@ def set( turbine_library_path (str | Path | None, optional): Path to the turbine library. Defaults to None. solver_settings (dict | None, optional): Solver settings. Defaults to None. - heterogenous_inflow_config (None, optional): Heterogenous inflow configuration. Defaults - to None. + heterogeneous_inflow_config (None, optional): heterogeneous inflow configuration. + Defaults to None. wind_data (type[WindDataBase] | None, optional): Wind data. Defaults to None. yaw_angles (NDArrayFloat | list[float] | None, optional): Turbine yaw angles. Defaults to None. @@ -357,7 +357,7 @@ def set( turbine_type=turbine_type, turbine_library_path=turbine_library_path, solver_settings=solver_settings, - heterogenous_inflow_config=heterogenous_inflow_config, + heterogeneous_inflow_config=heterogeneous_inflow_config, wind_data=wind_data, ) @@ -1565,6 +1565,56 @@ def layout_y(self): """ return self.core.farm.layout_y + @property + def wind_directions(self): + """ + Wind direction information. + + Returns: + np.array: Wind direction. + """ + return self.core.flow_field.wind_directions + + @property + def wind_speeds(self): + """ + Wind speed information. + + Returns: + np.array: Wind speed. + """ + return self.core.flow_field.wind_speeds + + @property + def turbulence_intensities(self): + """ + Turbulence intensity information. + + Returns: + np.array: Turbulence intensity. + """ + return self.core.flow_field.turbulence_intensities + + @property + def n_findex(self): + """ + Number of floris indices (findex). + + Returns: + int: Number of flow indices. + """ + return self.core.flow_field.n_findex + + @property + def n_turbines(self): + """ + Number of turbines. + + Returns: + int: Number of turbines. + """ + return self.core.farm.n_turbines + @property def turbine_average_velocities(self) -> NDArrayFloat: return average_velocity( diff --git a/floris/flow_visualization.py b/floris/flow_visualization.py index 3afaf1a38..8152be3df 100644 --- a/floris/flow_visualization.py +++ b/floris/flow_visualization.py @@ -297,8 +297,8 @@ def visualize_heterogeneous_cut_plane( points = np.array( list( zip( - fmodel.core.flow_field.heterogenous_inflow_config['x'], - fmodel.core.flow_field.heterogenous_inflow_config['y'], + fmodel.core.flow_field.heterogeneous_inflow_config['x'], + fmodel.core.flow_field.heterogeneous_inflow_config['y'], ) ) ) @@ -442,7 +442,7 @@ def plot_rotor_values( if n_rows == 1 and n_cols == 1: axes = np.array([axes]) - titles = np.array([f"T{i}" for i in t_range]) + titles = np.array([f"tindex: {i}" for i in t_range]) for ax, t, i in zip(axes.flatten(), titles, t_range): diff --git a/floris/optimization/yaw_optimization/yaw_optimization_base.py b/floris/optimization/yaw_optimization/yaw_optimization_base.py index 5608f58f4..07a2f7e11 100644 --- a/floris/optimization/yaw_optimization/yaw_optimization_base.py +++ b/floris/optimization/yaw_optimization/yaw_optimization_base.py @@ -318,7 +318,7 @@ def _calculate_farm_power( turbine_weights (iterable, optional): Array or list of weights to apply to the turbine powers. Defaults to None. heterogeneous_speed_multipliers (iterable, optional): Array or list of speed up factors - for heterogenous inflow. Defaults to None. + for heterogeneous inflow. Defaults to None. Returns: @@ -338,7 +338,7 @@ def _calculate_farm_power( turbine_weights = self._turbine_weights_subset if heterogeneous_speed_multipliers is not None: fmodel_subset.core.flow_field.\ - heterogenous_inflow_config['speed_multipliers'] = heterogeneous_speed_multipliers + heterogeneous_inflow_config['speed_multipliers'] = heterogeneous_speed_multipliers # Ensure format [incompatible with _subset notation] yaw_angles = self._unpack_variable(yaw_angles, subset=True) diff --git a/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py b/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py index b62649117..cdde87656 100644 --- a/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py +++ b/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py @@ -98,10 +98,10 @@ def optimize(self): turbine_weights = np.tile(turbine_weights, (1, 1)) # Handle heterogeneous inflow, if there is one - if (hasattr(self.fmodel.core.flow_field, 'heterogenous_inflow_config') and - self.fmodel.core.flow_field.heterogenous_inflow_config is not None): + if (hasattr(self.fmodel.core.flow_field, 'heterogeneous_inflow_config') and + self.fmodel.core.flow_field.heterogeneous_inflow_config is not None): het_sm_orig = np.array( - self.fmodel.core.flow_field.heterogenous_inflow_config['speed_multipliers'] + self.fmodel.core.flow_field.heterogeneous_inflow_config['speed_multipliers'] ) het_sm = het_sm_orig[i, :].reshape(1, -1) else: diff --git a/floris/optimization/yaw_optimization/yaw_optimizer_sr.py b/floris/optimization/yaw_optimization/yaw_optimizer_sr.py index c6d76b04e..2b5b7ad1b 100644 --- a/floris/optimization/yaw_optimization/yaw_optimizer_sr.py +++ b/floris/optimization/yaw_optimization/yaw_optimizer_sr.py @@ -129,10 +129,10 @@ def _calc_powers_with_memory(self, yaw_angles_subset, use_memory=True): if not np.all(idx): # Now calculate farm powers for conditions we haven't yet evaluated previously start_time = timerpc() - if (hasattr(self.fmodel.core.flow_field, 'heterogenous_inflow_config') and - self.fmodel.core.flow_field.heterogenous_inflow_config is not None): + if (hasattr(self.fmodel.core.flow_field, 'heterogeneous_inflow_config') and + self.fmodel.core.flow_field.heterogeneous_inflow_config is not None): het_sm_orig = np.array( - self.fmodel.core.flow_field.heterogenous_inflow_config['speed_multipliers'] + self.fmodel.core.flow_field.heterogeneous_inflow_config['speed_multipliers'] ) het_sm = np.tile(het_sm_orig, (Ny, 1))[~idx, :] else: diff --git a/floris/parallel_floris_model.py b/floris/parallel_floris_model.py index 86fc3ea08..4de5015df 100644 --- a/floris/parallel_floris_model.py +++ b/floris/parallel_floris_model.py @@ -245,8 +245,7 @@ def _postprocessing(self, output): flowfield_subsets = [p[1] for p in output] # Retrieve and merge turbine power productions - i, j, k = np.shape(power_subsets) - turbine_powers = np.reshape(power_subsets, (i*j, k)) + turbine_powers = np.concatenate(power_subsets, axis=0) # Optionally, also merge flow field dictionaries from individual floris solutions if self.propagate_flowfield_from_workers: @@ -443,7 +442,7 @@ def get_farm_AEP( ) # Finally, calculate AEP in GWh - aep = np.sum(np.multiply(freq, farm_power) * 365 * 24) + aep = np.nansum(np.multiply(freq, farm_power) * 365 * 24) # Reset the FLORIS object to the full wind speed array self.fmodel.set( @@ -532,6 +531,27 @@ def layout_x(self): def layout_y(self): return self.fmodel.layout_y + @property + def wind_speeds(self): + return self.fmodel.wind_speeds + + @property + def wind_directions(self): + return self.fmodel.wind_directions + + @property + def turbulence_intensities(self): + return self.fmodel.turbulence_intensities + + @property + def n_findex(self): + return self.fmodel.n_findex + + @property + def n_turbines(self): + return self.fmodel.n_turbines + + # @property # def floris(self): # return self.fmodel.core diff --git a/floris/uncertain_floris_model.py b/floris/uncertain_floris_model.py index 2242f4075..217dab2e5 100644 --- a/floris/uncertain_floris_model.py +++ b/floris/uncertain_floris_model.py @@ -725,6 +725,56 @@ def layout_y(self): """ return self.fmodel_unexpanded.core.farm.layout_y + @property + def wind_directions(self): + """ + Wind direction information. + + Returns: + np.array: Wind direction. + """ + return self.fmodel_unexpanded.core.flow_field.wind_directions + + @property + def wind_speeds(self): + """ + Wind speed information. + + Returns: + np.array: Wind speed. + """ + return self.fmodel_unexpanded.core.flow_field.wind_speeds + + @property + def turbulence_intensities(self): + """ + Turbulence intensity information. + + Returns: + np.array: Turbulence intensity. + """ + return self.fmodel_unexpanded.core.flow_field.turbulence_intensities + + @property + def n_findex(self): + """ + Number of unique wind conditions. + + Returns: + int: Number of unique wind conditions. + """ + return self.fmodel_unexpanded.core.flow_field.n_findex + + @property + def n_turbines(self): + """ + Number of turbines in the wind farm. + + Returns: + int: Number of turbines in the wind farm. + """ + return self.fmodel_unexpanded.core.farm.n_turbines + @property def core(self): """ diff --git a/floris/wind_data.py b/floris/wind_data.py index 808edc1ee..6ac81f7aa 100644 --- a/floris/wind_data.py +++ b/floris/wind_data.py @@ -36,14 +36,14 @@ def unpack_for_reinitialize(self): ti_table_unpack, _, _, - heterogenous_inflow_config, + heterogeneous_inflow_config, ) = self.unpack() return ( wind_directions_unpack, wind_speeds_unpack, ti_table_unpack, - heterogenous_inflow_config, + heterogeneous_inflow_config, ) def unpack_freq(self): @@ -56,63 +56,63 @@ def unpack_value(self): return self.unpack()[4] - def check_heterogenous_inflow_config_by_wd(self, heterogenous_inflow_config_by_wd): + def check_heterogeneous_inflow_config_by_wd(self, heterogeneous_inflow_config_by_wd): """ - Check that the heterogenous_inflow_config_by_wd dictionary is properly formatted + Check that the heterogeneous_inflow_config_by_wd dictionary is properly formatted Args: - heterogenous_inflow_config_by_wd (dict): A dictionary containing the following keys: + heterogeneous_inflow_config_by_wd (dict): A dictionary containing the following keys: * 'speed_multipliers': A 2D NumPy array (size num_wd x num_points) of speed multipliers. * 'wind_directions': A 1D NumPy array (size num_wd) of wind directions (degrees). * 'x': A 1D NumPy array (size num_points) of x-coordinates (meters). * 'y': A 1D NumPy array (size num_points) of y-coordinates (meters). """ - if heterogenous_inflow_config_by_wd is not None: - if not isinstance(heterogenous_inflow_config_by_wd, dict): - raise TypeError("heterogenous_inflow_config_by_wd must be a dictionary") - if "speed_multipliers" not in heterogenous_inflow_config_by_wd: + if heterogeneous_inflow_config_by_wd is not None: + if not isinstance(heterogeneous_inflow_config_by_wd, dict): + raise TypeError("heterogeneous_inflow_config_by_wd must be a dictionary") + if "speed_multipliers" not in heterogeneous_inflow_config_by_wd: raise ValueError( - "heterogenous_inflow_config_by_wd must contain a key 'speed_multipliers'" + "heterogeneous_inflow_config_by_wd must contain a key 'speed_multipliers'" ) - if "wind_directions" not in heterogenous_inflow_config_by_wd: + if "wind_directions" not in heterogeneous_inflow_config_by_wd: raise ValueError( - "heterogenous_inflow_config_by_wd must contain a key 'wind_directions'" + "heterogeneous_inflow_config_by_wd must contain a key 'wind_directions'" ) - if "x" not in heterogenous_inflow_config_by_wd: - raise ValueError("heterogenous_inflow_config_by_wd must contain a key 'x'") - if "y" not in heterogenous_inflow_config_by_wd: - raise ValueError("heterogenous_inflow_config_by_wd must contain a key 'y'") + if "x" not in heterogeneous_inflow_config_by_wd: + raise ValueError("heterogeneous_inflow_config_by_wd must contain a key 'x'") + if "y" not in heterogeneous_inflow_config_by_wd: + raise ValueError("heterogeneous_inflow_config_by_wd must contain a key 'y'") - def check_heterogenous_inflow_config(self, heterogenous_inflow_config): + def check_heterogeneous_inflow_config(self, heterogeneous_inflow_config): """ - Check that the heterogenous_inflow_config dictionary is properly formatted + Check that the heterogeneous_inflow_config dictionary is properly formatted Args: - heterogenous_inflow_config (dict): A dictionary containing the following keys: + heterogeneous_inflow_config (dict): A dictionary containing the following keys: * 'speed_multipliers': A 2D NumPy array (size n_findex x num_points) of speed multipliers. * 'x': A 1D NumPy array (size num_points) of x-coordinates (meters). * 'y': A 1D NumPy array (size num_points) of y-coordinates (meters). """ - if heterogenous_inflow_config is not None: - if not isinstance(heterogenous_inflow_config, dict): - raise TypeError("heterogenous_inflow_config_by_wd must be a dictionary") - if "speed_multipliers" not in heterogenous_inflow_config: + if heterogeneous_inflow_config is not None: + if not isinstance(heterogeneous_inflow_config, dict): + raise TypeError("heterogeneous_inflow_config_by_wd must be a dictionary") + if "speed_multipliers" not in heterogeneous_inflow_config: raise ValueError( - "heterogenous_inflow_config must contain a key 'speed_multipliers'" + "heterogeneous_inflow_config must contain a key 'speed_multipliers'" ) - if "x" not in heterogenous_inflow_config: - raise ValueError("heterogenous_inflow_config must contain a key 'x'") - if "y" not in heterogenous_inflow_config: - raise ValueError("heterogenous_inflow_config must contain a key 'y'") + if "x" not in heterogeneous_inflow_config: + raise ValueError("heterogeneous_inflow_config must contain a key 'x'") + if "y" not in heterogeneous_inflow_config: + raise ValueError("heterogeneous_inflow_config must contain a key 'y'") - def get_speed_multipliers_by_wd(self, heterogenous_inflow_config_by_wd, wind_directions): + def get_speed_multipliers_by_wd(self, heterogeneous_inflow_config_by_wd, wind_directions): """ - Processes heterogenous inflow configuration data to generate a speed multiplier array + Processes heterogeneous inflow configuration data to generate a speed multiplier array aligned with the wind directions. Accounts for the cyclical nature of wind directions. Args: - heterogenous_inflow_config_by_wd (dict): A dictionary containing the following keys: + heterogeneous_inflow_config_by_wd (dict): A dictionary containing the following keys: * 'speed_multipliers': A 2D NumPy array (size num_wd x num_points) of speed multipliers. * 'wind_directions': A 1D NumPy array (size num_wd) of wind directions (degrees). @@ -128,14 +128,14 @@ def get_speed_multipliers_by_wd(self, heterogenous_inflow_config_by_wd, wind_dir """ # Extract data from the configuration dictionary - speed_multipliers = np.array(heterogenous_inflow_config_by_wd["speed_multipliers"]) - het_wd = np.array(heterogenous_inflow_config_by_wd["wind_directions"]) + speed_multipliers = np.array(heterogeneous_inflow_config_by_wd["speed_multipliers"]) + het_wd = np.array(heterogeneous_inflow_config_by_wd["wind_directions"]) # Confirm 0th dimension of speed_multipliers == len(het_wd) if len(het_wd) != speed_multipliers.shape[0]: raise ValueError( "The legnth of het_wd must equal the number of rows speed_multipliers" - "Within the heterogenous_inflow_config_by_wd dictionary" + "Within the heterogeneous_inflow_config_by_wd dictionary" ) # Calculate closest wind direction indices (accounting for angles) @@ -146,21 +146,21 @@ def get_speed_multipliers_by_wd(self, heterogenous_inflow_config_by_wd, wind_dir # Construct the output array using the calculated indices return speed_multipliers[closest_wd_indices] - def get_heterogenous_inflow_config(self, heterogenous_inflow_config_by_wd, wind_directions): - # If heterogenous_inflow_config_by_wd is None, return None - if heterogenous_inflow_config_by_wd is None: + def get_heterogeneous_inflow_config(self, heterogeneous_inflow_config_by_wd, wind_directions): + # If heterogeneous_inflow_config_by_wd is None, return None + if heterogeneous_inflow_config_by_wd is None: return None - # If heterogenous_inflow_config_by_wd is not None, then process it + # If heterogeneous_inflow_config_by_wd is not None, then process it # Build the n-findex version of the het map speed_multipliers = self.get_speed_multipliers_by_wd( - heterogenous_inflow_config_by_wd, wind_directions + heterogeneous_inflow_config_by_wd, wind_directions ) - # Return heterogenous_inflow_config + # Return heterogeneous_inflow_config return { "speed_multipliers": speed_multipliers, - "x": heterogenous_inflow_config_by_wd["x"], - "y": heterogenous_inflow_config_by_wd["y"], + "x": heterogeneous_inflow_config_by_wd["x"], + "y": heterogeneous_inflow_config_by_wd["y"], } @@ -190,7 +190,7 @@ class WindRose(WindDataBase): each bin to compute the total value of the energy produced compute_zero_freq_occurrence: Flag indicating whether to compute zero frequency occurrences (bool, optional). Defaults to False. - heterogenous_inflow_config_by_wd (dict, optional): A dictionary containing the following + heterogeneous_inflow_config_by_wd (dict, optional): A dictionary containing the following keys. Defaults to None. * 'speed_multipliers': A 2D NumPy array (size num_wd x num_points) of speed multipliers. @@ -208,7 +208,7 @@ def __init__( freq_table: NDArrayFloat | None = None, value_table: NDArrayFloat | None = None, compute_zero_freq_occurrence: bool = False, - heterogenous_inflow_config_by_wd: dict | None = None, + heterogeneous_inflow_config_by_wd: dict | None = None, ): if not isinstance(wind_directions, np.ndarray): raise TypeError("wind_directions must be a NumPy array") @@ -268,12 +268,12 @@ def __init__( ) self.compute_zero_freq_occurrence = compute_zero_freq_occurrence - # Check that heterogenous_inflow_config_by_wd is a dictionary with keys: + # Check that heterogeneous_inflow_config_by_wd is a dictionary with keys: # speed_multipliers, wind_directions, x and y - self.check_heterogenous_inflow_config_by_wd(heterogenous_inflow_config_by_wd) + self.check_heterogeneous_inflow_config_by_wd(heterogeneous_inflow_config_by_wd) # Then save - self.heterogenous_inflow_config_by_wd = heterogenous_inflow_config_by_wd + self.heterogeneous_inflow_config_by_wd = heterogeneous_inflow_config_by_wd # Build the gridded and flatten versions self._build_gridded_and_flattened_version() @@ -339,14 +339,14 @@ def unpack(self): else: value_table_unpack = None - # If heterogenous_inflow_config_by_wd is not None, then update - # heterogenous_inflow_config to match wind_directions_unpack - if self.heterogenous_inflow_config_by_wd is not None: - heterogenous_inflow_config = self.get_heterogenous_inflow_config( - self.heterogenous_inflow_config_by_wd, wind_directions_unpack + # If heterogeneous_inflow_config_by_wd is not None, then update + # heterogeneous_inflow_config to match wind_directions_unpack + if self.heterogeneous_inflow_config_by_wd is not None: + heterogeneous_inflow_config = self.get_heterogeneous_inflow_config( + self.heterogeneous_inflow_config_by_wd, wind_directions_unpack ) else: - heterogenous_inflow_config = None + heterogeneous_inflow_config = None return ( wind_directions_unpack, @@ -354,7 +354,7 @@ def unpack(self): ti_table_unpack, freq_table_unpack, value_table_unpack, - heterogenous_inflow_config, + heterogeneous_inflow_config, ) def resample_wind_rose(self, wd_step=None, ws_step=None): @@ -390,7 +390,7 @@ def resample_wind_rose(self, wd_step=None, ws_step=None): self.ws_flat, self.ti_table_flat, self.value_table_flat, - self.heterogenous_inflow_config_by_wd, + self.heterogeneous_inflow_config_by_wd, ) # Now build a new wind rose using the new steps @@ -753,7 +753,7 @@ class WindTIRose(WindDataBase): to compute the total value of the energy produced. compute_zero_freq_occurrence: Flag indicating whether to compute zero frequency occurrences (bool, optional). Defaults to False. - heterogenous_inflow_config_by_wd (dict, optional): A dictionary containing the following + heterogeneous_inflow_config_by_wd (dict, optional): A dictionary containing the following keys. Defaults to None. * 'speed_multipliers': A 2D NumPy array (size num_wd x num_points) of speed multipliers. @@ -771,7 +771,7 @@ def __init__( freq_table: NDArrayFloat | None = None, value_table: NDArrayFloat | None = None, compute_zero_freq_occurrence: bool = False, - heterogenous_inflow_config_by_wd: dict | None = None, + heterogeneous_inflow_config_by_wd: dict | None = None, ): if not isinstance(wind_directions, np.ndarray): raise TypeError("wind_directions must be a NumPy array") @@ -820,12 +820,12 @@ def __init__( ) self.value_table = value_table - # Check that heterogenous_inflow_config_by_wd is a dictionary with keys: + # Check that heterogeneous_inflow_config_by_wd is a dictionary with keys: # speed_multipliers, wind_directions, x and y - self.check_heterogenous_inflow_config_by_wd(heterogenous_inflow_config_by_wd) + self.check_heterogeneous_inflow_config_by_wd(heterogeneous_inflow_config_by_wd) # Then save - self.heterogenous_inflow_config_by_wd = heterogenous_inflow_config_by_wd + self.heterogeneous_inflow_config_by_wd = heterogeneous_inflow_config_by_wd # Save whether zero occurrence cases should be computed self.compute_zero_freq_occurrence = compute_zero_freq_occurrence @@ -892,14 +892,14 @@ def unpack(self): else: value_table_unpack = None - # If heterogenous_inflow_config_by_wd is not None, then update - # heterogenous_inflow_config to match wind_directions_unpack - if self.heterogenous_inflow_config_by_wd is not None: - heterogenous_inflow_config = self.get_heterogenous_inflow_config( - self.heterogenous_inflow_config_by_wd, wind_directions_unpack + # If heterogeneous_inflow_config_by_wd is not None, then update + # heterogeneous_inflow_config to match wind_directions_unpack + if self.heterogeneous_inflow_config_by_wd is not None: + heterogeneous_inflow_config = self.get_heterogeneous_inflow_config( + self.heterogeneous_inflow_config_by_wd, wind_directions_unpack ) else: - heterogenous_inflow_config = None + heterogeneous_inflow_config = None return ( wind_directions_unpack, @@ -907,7 +907,7 @@ def unpack(self): turbulence_intensities_unpack, freq_table_unpack, value_table_unpack, - heterogenous_inflow_config, + heterogeneous_inflow_config, ) def resample_wind_rose(self, wd_step=None, ws_step=None, ti_step=None): @@ -951,7 +951,7 @@ def resample_wind_rose(self, wd_step=None, ws_step=None, ti_step=None): self.ws_flat, self.ti_flat, self.value_table_flat, - self.heterogenous_inflow_config_by_wd, + self.heterogeneous_inflow_config_by_wd, ) # Now build a new wind rose using the new steps @@ -1295,14 +1295,14 @@ class TimeSeries(WindDataBase): a single value or an array of values. values (NDArrayFloat, optional): Values associated with each wind direction, wind speed, and turbulence intensity. Defaults to None. - heterogenous_inflow_config_by_wd (dict, optional): A dictionary containing the following + heterogeneous_inflow_config_by_wd (dict, optional): A dictionary containing the following keys. Defaults to None. * 'speed_multipliers': A 2D NumPy array (size num_wd x num_points) of speed multipliers. * 'wind_directions': A 1D NumPy array (size num_wd) of wind directions (degrees). * 'x': A 1D NumPy array (size num_points) of x-coordinates (meters). * 'y': A 1D NumPy array (size num_points) of y-coordinates (meters). - heterogenous_inflow_config (dict, optional): A dictionary containing the following keys. + heterogeneous_inflow_config (dict, optional): A dictionary containing the following keys. Defaults to None. * 'speed_multipliers': A 2D NumPy array (size n_findex x num_points) of speed multipliers. @@ -1316,8 +1316,8 @@ def __init__( wind_speeds: float | NDArrayFloat, turbulence_intensities: float | NDArrayFloat, values: NDArrayFloat | None = None, - heterogenous_inflow_config_by_wd: dict | None = None, - heterogenous_inflow_config: dict | None = None, + heterogeneous_inflow_config_by_wd: dict | None = None, + heterogeneous_inflow_config: dict | None = None, ): # At least one of wind_directions, wind_speeds, or turbulence_intensities must be an array if ( @@ -1385,29 +1385,32 @@ def __init__( self.turbulence_intensities = turbulence_intensities self.values = values - # Only one of heterogenous_inflow_config_by_wd and - # heterogenous_inflow_config can be not None - if heterogenous_inflow_config_by_wd is not None and heterogenous_inflow_config is not None: + # Only one of heterogeneous_inflow_config_by_wd and + # heterogeneous_inflow_config can be not None + if ( + heterogeneous_inflow_config_by_wd is not None + and heterogeneous_inflow_config is not None + ): raise ValueError( - "Only one of heterogenous_inflow_config_by_wd and heterogenous_inflow_config " + "Only one of heterogeneous_inflow_config_by_wd and heterogeneous_inflow_config " "can be not None" ) - # if heterogenous_inflow_config is not None, then the speed_multipliers + # if heterogeneous_inflow_config is not None, then the speed_multipliers # must be the same length as wind_directions # in the 0th dimension - if heterogenous_inflow_config is not None: - if len(heterogenous_inflow_config["speed_multipliers"]) != len(wind_directions): + if heterogeneous_inflow_config is not None: + if len(heterogeneous_inflow_config["speed_multipliers"]) != len(wind_directions): raise ValueError("speed_multipliers must be the same length as wind_directions") - # Check that heterogenous_inflow_config_by_wd is a dictionary with keys: + # Check that heterogeneous_inflow_config_by_wd is a dictionary with keys: # speed_multipliers, wind_directions, x and y - self.check_heterogenous_inflow_config_by_wd(heterogenous_inflow_config_by_wd) - self.check_heterogenous_inflow_config(heterogenous_inflow_config) + self.check_heterogeneous_inflow_config_by_wd(heterogeneous_inflow_config_by_wd) + self.check_heterogeneous_inflow_config(heterogeneous_inflow_config) # Then save - self.heterogenous_inflow_config_by_wd = heterogenous_inflow_config_by_wd - self.heterogenous_inflow_config = heterogenous_inflow_config + self.heterogeneous_inflow_config_by_wd = heterogeneous_inflow_config_by_wd + self.heterogeneous_inflow_config = heterogeneous_inflow_config # Record findex self.n_findex = len(self.wind_directions) @@ -1421,14 +1424,14 @@ def unpack(self): uniform_frequency = np.ones_like(self.wind_directions) uniform_frequency = uniform_frequency / uniform_frequency.sum() - # If heterogenous_inflow_config_by_wd is not None, then update - # heterogenous_inflow_config to match wind_directions_unpack - if self.heterogenous_inflow_config_by_wd is not None: - heterogenous_inflow_config = self.get_heterogenous_inflow_config( - self.heterogenous_inflow_config_by_wd, self.wind_directions + # If heterogeneous_inflow_config_by_wd is not None, then update + # heterogeneous_inflow_config to match wind_directions_unpack + if self.heterogeneous_inflow_config_by_wd is not None: + heterogeneous_inflow_config = self.get_heterogeneous_inflow_config( + self.heterogeneous_inflow_config_by_wd, self.wind_directions ) else: - heterogenous_inflow_config = self.heterogenous_inflow_config + heterogeneous_inflow_config = self.heterogeneous_inflow_config return ( self.wind_directions, @@ -1436,7 +1439,7 @@ def unpack(self): self.turbulence_intensities, uniform_frequency, self.values, - heterogenous_inflow_config, + heterogeneous_inflow_config, ) def _wrap_wind_directions_near_360(self, wind_directions, wd_step): @@ -1700,7 +1703,7 @@ def to_WindRose( ti_table, freq_table, value_table, - self.heterogenous_inflow_config_by_wd, + self.heterogeneous_inflow_config_by_wd, ) def to_WindTIRose( @@ -1867,5 +1870,5 @@ def to_WindTIRose( ti_centers, freq_table, value_table, - self.heterogenous_inflow_config_by_wd, + self.heterogeneous_inflow_config_by_wd, ) diff --git a/tests/conftest.py b/tests/conftest.py index 26210c963..b8b70dc7d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -137,7 +137,6 @@ def print_test_values( N_TURBINES = len(X_COORDS) ROTOR_DIAMETER = 126.0 TURBINE_GRID_RESOLUTION = 2 -TIME_SERIES = False ## Unit test fixtures @@ -156,7 +155,6 @@ def turbine_grid_fixture(sample_inputs_fixture) -> TurbineGrid: turbine_diameters=rotor_diameters, wind_directions=np.array(WIND_DIRECTIONS), grid_resolution=TURBINE_GRID_RESOLUTION, - time_series=TIME_SERIES ) @pytest.fixture @@ -182,7 +180,6 @@ def points_grid_fixture(sample_inputs_fixture) -> PointsGrid: turbine_diameters=rotor_diameters, wind_directions=np.array(WIND_DIRECTIONS), grid_resolution=None, - time_series=False, points_x=points_x, points_y=points_y, points_z=points_z, @@ -524,7 +521,7 @@ def __init__(self): }, "name": "conftest", "description": "Inputs used for testing", - "floris_version": "v3.0.0", + "floris_version": "v4", } self.v3type_turbine = { diff --git a/tests/data/input_full.yaml b/tests/data/input_full.yaml index 36a150bdd..d9415db1f 100644 --- a/tests/data/input_full.yaml +++ b/tests/data/input_full.yaml @@ -1,7 +1,7 @@ name: test_input description: Single turbine for testing -floris_version: v3.0.0 +floris_version: v4 logging: console: diff --git a/tests/reg_tests/scipy_layout_opt_regression_test.py b/tests/reg_tests/scipy_layout_opt_regression.py similarity index 100% rename from tests/reg_tests/scipy_layout_opt_regression_test.py rename to tests/reg_tests/scipy_layout_opt_regression.py diff --git a/tests/wind_data_integration_test.py b/tests/wind_data_integration_test.py index c6398a1fa..4cec2eb0c 100644 --- a/tests/wind_data_integration_test.py +++ b/tests/wind_data_integration_test.py @@ -130,7 +130,7 @@ def test_wind_rose_unpack(): ti_table_unpack, freq_table_unpack, value_table_unpack, - heterogenous_inflow_config, + heterogeneous_inflow_config, ) = wind_rose.unpack() # Given the above frequency table with zeros for a few elements, @@ -155,7 +155,7 @@ def test_wind_rose_unpack(): ti_table_unpack, freq_table_unpack, value_table_unpack, - heterogenous_inflow_config, + heterogeneous_inflow_config, ) = wind_rose.unpack() # Expect now to compute all combinations @@ -177,7 +177,7 @@ def test_unpack_for_reinitialize(): wind_directions_unpack, wind_speeds_unpack, ti_table_unpack, - heterogenous_inflow_config, + heterogeneous_inflow_config, ) = wind_rose.unpack_for_reinitialize() # Given the above frequency table, would only expect the @@ -361,7 +361,7 @@ def test_wind_ti_rose_unpack(): turbulence_intensities_unpack, freq_table_unpack, value_table_unpack, - heterogenous_inflow_config, + heterogeneous_inflow_config, ) = wind_rose.unpack() # Given the above frequency table with zeros for a few elements, @@ -391,7 +391,7 @@ def test_wind_ti_rose_unpack(): turbulence_intensities_unpack, freq_table_unpack, value_table_unpack, - heterogenous_inflow_config, + heterogeneous_inflow_config, ) = wind_rose.unpack() # Expect now to compute all combinations @@ -423,7 +423,7 @@ def test_wind_ti_rose_unpack_for_reinitialize(): wind_directions_unpack, wind_speeds_unpack, turbulence_intensities_unpack, - heterogenous_inflow_config, + heterogeneous_inflow_config, ) = wind_rose.unpack_for_reinitialize() # Given the above frequency table with zeros for a few elements, @@ -481,7 +481,7 @@ def test_time_series_to_WindTIRose(): def test_get_speed_multipliers_by_wd(): - heterogenous_inflow_config_by_wd = { + heterogeneous_inflow_config_by_wd = { "speed_multipliers": np.array( [ [1.0, 1.1, 1.2], @@ -497,7 +497,7 @@ def test_get_speed_multipliers_by_wd(): expected_output = np.array([[1.3, 1.4, 1.5], [1.1, 1.1, 1.1], [1.0, 1.1, 1.2]]) wind_data = WindDataBase() result = wind_data.get_speed_multipliers_by_wd( - heterogenous_inflow_config_by_wd, wind_directions + heterogeneous_inflow_config_by_wd, wind_directions ) assert np.allclose(result, expected_output) @@ -505,7 +505,7 @@ def test_get_speed_multipliers_by_wd(): wind_directions = np.array([350, 10]) expected_output = np.array([[1.0, 1.1, 1.2], [1.0, 1.1, 1.2]]) result = wind_data.get_speed_multipliers_by_wd( - heterogenous_inflow_config_by_wd, wind_directions + heterogeneous_inflow_config_by_wd, wind_directions ) assert np.allclose(result, expected_output) @@ -513,17 +513,17 @@ def test_get_speed_multipliers_by_wd(): wind_directions = np.arange(0.0, 360.0, 10.0) num_wd = len(wind_directions) result = wind_data.get_speed_multipliers_by_wd( - heterogenous_inflow_config_by_wd, wind_directions + heterogeneous_inflow_config_by_wd, wind_directions ) assert result.shape[0] == num_wd -def test_gen_heterogenous_inflow_config(): +def test_gen_heterogeneous_inflow_config(): wind_directions = np.array([259.8, 260.2, 260.3, 260.1, 270.0]) wind_speeds = 8 turbulence_intensities = 0.06 - heterogenous_inflow_config_by_wd = { + heterogeneous_inflow_config_by_wd = { "speed_multipliers": np.array( [ [0.9, 0.9], @@ -540,15 +540,15 @@ def test_gen_heterogenous_inflow_config(): wind_directions, wind_speeds, turbulence_intensities=turbulence_intensities, - heterogenous_inflow_config_by_wd=heterogenous_inflow_config_by_wd, + heterogeneous_inflow_config_by_wd=heterogeneous_inflow_config_by_wd, ) - (_, _, _, _, _, heterogenous_inflow_config) = time_series.unpack() + (_, _, _, _, _, heterogeneous_inflow_config) = time_series.unpack() expected_result = np.array([[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.1, 1.2]]) - np.testing.assert_allclose(heterogenous_inflow_config["speed_multipliers"], expected_result) + np.testing.assert_allclose(heterogeneous_inflow_config["speed_multipliers"], expected_result) np.testing.assert_allclose( - heterogenous_inflow_config["x"], heterogenous_inflow_config_by_wd["x"] + heterogeneous_inflow_config["x"], heterogeneous_inflow_config_by_wd["x"] )