Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code for determining which turbines impact the power of a given turbine #70

Merged
merged 9 commits into from
Mar 2, 2023
91 changes: 91 additions & 0 deletions examples/layout/turbine_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import os
import matplotlib.pyplot as plt
import numpy as np

from flasc import floris_tools as fsatools
from flasc import visualization as fsaviz

from floris import tools as wfct

# Demonstrate the turbine dependency functions in floris_tools
# Note a turbine is "dependent" on another if it is affected
# by the wake of the other turbine for a given wind direction.

# A given turbine's dependent turbines are those that depend on it,
# and a turbine's impacting turbines are those turbines that
# it itself depends on.


# Set up FLORIS interface
print('Initializing the FLORIS object for our demo wind farm')
file_path = os.path.dirname(os.path.abspath(__file__))
fi_path = os.path.join(file_path, '../demo_dataset/demo_floris_input.yaml')
fi = wfct.floris_interface.FlorisInterface(fi_path)

# Plot the layout of the farm for reference
fsaviz.plot_layout_only(fi)

# Get the dependencies of turbine 2
check_directions = np.arange(0, 360., 2.)
depend_on_2 = fsatools.get_dependent_turbines_by_wd(fi, 2, check_directions)

print("Turbines that depend on T002 at 226 degrees:",
depend_on_2[round(226/2)]
)

# Can also return all influences as a matrix for other use (not ordered)
depend_on_2, influence_magnitudes = fsatools.get_dependent_turbines_by_wd(
fi, 2, check_directions, return_influence_magnitudes=True)
print("\nArray of all influences of T002 has shape (num_wds x num_turbs): ",
influence_magnitudes.shape)
print("Influence of T002 on T006 at 226 degrees: {0:.4f}".format(
influence_magnitudes[round(226/2), 6]))

df_dependencies = fsatools.get_all_dependent_turbines(fi, check_directions)
print("\nAll turbine dependencies using default threshold "+\
"(first 5 wind directions printed):")
print(df_dependencies.head())

df_dependencies = fsatools.get_all_dependent_turbines(fi, check_directions,
limit_number=2)
print("\nTwo most significant turbine dependencies using default threshold "+\
"(first 5 wind directions printed):")
print(df_dependencies.head())

df_dependencies = fsatools.get_all_dependent_turbines(fi, check_directions,
change_threshold=0.01)
print("\nAll turbine dependencies using higher threshold "+\
"(first 5 wind directions printed):")
print(df_dependencies.head())

print("\nAll upstream turbine impacts using default threshold "+\
"(first 5 wind directions printed):")
df_impacting = fsatools.get_all_impacting_turbines(fi, check_directions)
print(df_impacting.head())
# Inclusion of T005 here as an impact on T000 is surprising; try increasing
# the threshold or reducing the limit_number (see next).

print("\nMost significant upstream turbine impact using default threshold "+\
"(first 5 wind directions printed):")
df_impacting = fsatools.get_all_impacting_turbines(fi, check_directions,
limit_number=1)
print(df_impacting.head())

print("\nAll upstream turbine impacts using higher threshold "+\
"(first 5 wind directions printed):")
df_impacting = fsatools.get_all_impacting_turbines(fi, check_directions,
change_threshold=0.01)
print(df_impacting.head())

# Note that there is no individual turbine version for the "impacting"
# function; instead, compute all impacting turbines and extract desired
# turbine from the output dataframe.

# (compute using defaults again, for example)
df_impacting = fsatools.get_all_impacting_turbines(fi, check_directions)
print("\nTurbines that T006 depends on at 226 degrees:",
df_impacting.loc[226, 6]
)


plt.show()
206 changes: 206 additions & 0 deletions flasc/floris_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pandas.errors import DataError
from scipy import interpolate
from time import perf_counter as timerpc
import copy

from flasc import utilities as fsut

Expand Down Expand Up @@ -670,6 +671,211 @@ def yw_lower(x):

return df_upstream

def get_dependent_turbines_by_wd(fi_in, test_turbine,
wd_array=np.arange(0., 360., 2.), change_threshold=0.001, limit_number=None,
ws_test=9., return_influence_magnitudes=False):
"""
Computes all turbines that depend on the operation of a specified
turbine (test_turbine) for each wind direction in wd_array, using
the FLORIS model specified by fi_in to detect dependencies.

Args:
fi ([floris object]): FLORIS object of the farm of interest.
test_turbine ([int]): Turbine for which dependencies are found.
wd_array ([np.array]): Wind directions at which to determine
dependencies. Defaults to [0, 2, ... , 358].
change_threshold (float): Fractional change in power needed
to denote a dependency. Defaults to 0. (any change in power
is marked as a dependency)
limit_number (int | NoneType): Number of turbines that a
turbine can have as dependencies. If None, returns all
turbines that depend on each turbine. Defaults to None.
ws_test (float): Wind speed at which FLORIS model is run to
determine dependencies. Defaults to 9. m/s.
return_influence_magnitudes (Bool): Flag for whether to return
an array containing the magnitude of the influence of the
test_turbine on all turbines.

Returns:
dep_indices_by_wd (list): A 2-dimensional list. Each element of
the outer level list, which represents wind direction,
contains a list of the turbines that depend on test_turbine
for that wind direction. The second-level list may be empty
if no turbine depends on the test_turbine for that wind
direciton (e.g., the turbine is in the back row).
all_influence_magnitudes ([np.array]): 2-D numpy array of
influences of test_turbine on all other turbines, with size
(number of wind directions) x (number of turbines). Returned
only if return_influence_magnitudes is True.
"""
# Copy fi to a local to not mess with incoming
fi = copy.deepcopy(fi_in)

# Compute the base power
fi.reinitialize(
wind_speeds=[ws_test],
wind_directions=wd_array
)
fi.calculate_wake()
base_power = fi.get_turbine_powers()[:,0,:] # remove unneeded dimension

# Compute the test power
if len(fi.floris.farm.turbine_type) > 1:
# Remove test turbine from list
fi.floris.farm.turbine_type.pop(test_turbine)
else: # Only a single turbine type defined for the whole farm; do nothing
pass
fi.reinitialize(
layout_x=np.delete(fi.layout_x, [test_turbine]),
layout_y=np.delete(fi.layout_y, [test_turbine]),
wind_speeds=[ws_test],
wind_directions=wd_array
) # This will reindex the turbines; undone in following steps.
fi.calculate_wake()
test_power = fi.get_turbine_powers()[:,0,:] # remove unneeded dimension
test_power = np.insert(test_power, test_turbine,
base_power[:,test_turbine], axis=1)

if return_influence_magnitudes:
all_influence_magnitudes = np.zeros_like(test_power)

# Find the indices that have changed
dep_indices_by_wd = [None]*len(wd_array)
for i in range(len(wd_array)):
all_influences = np.abs(test_power[i,:] - base_power[i,:])/\
base_power[i,:]
# Sort with highest influence first; trim to limit_number
influence_order = np.flip(np.argsort(all_influences))[:limit_number]
# Mask to only those that meet the threshold
influence_order = influence_order[
all_influences[influence_order] >= change_threshold
]

# Store in output
dep_indices_by_wd[i] = list(influence_order)
if return_influence_magnitudes:
all_influence_magnitudes[i,:] = all_influences


# Remove the turbines own indice
if return_influence_magnitudes:
return dep_indices_by_wd, all_influence_magnitudes
else:
return dep_indices_by_wd

def get_all_dependent_turbines(fi_in, wd_array=np.arange(0., 360., 2.),
change_threshold=0.001, limit_number=None, ws_test=9.):
"""
Wrapper for get_dependent_turbines_by_wd() that loops over all
turbines in the farm and packages their dependencies as a pandas
dataframe.

Args:
fi ([floris object]): FLORIS object of the farm of interest.
wd_array ([np.array]): Wind directions at which to determine
dependencies. Defaults to [0, 2, ... , 358].
change_threshold (float): Fractional change in power needed
to denote a dependency. Defaults to 0. (any change in power
is marked as a dependency)
limit_number (int | NoneType): Number of turbines that a
turbine can have as dependencies. If None, returns all
turbines that depend on each turbine. Defaults to None.
ws_test (float): Wind speed at which FLORIS model is run to
determine dependencies. Defaults to 9. m/s.

Returns:
df_out ([pd.DataFrame]): A Pandas Dataframe in which each row
contains a wind direction, each column is a turbine, and
each entry is the turbines that depend on the column turbine
at the row wind direction. Dependencies can be extracted
as: For wind direction wd, the turbines that depend on
turbine T are df_out.loc[wd, T]. Dependencies are ordered,
with strongest dependencies appearing first.
"""

results = []
for t_i in range(len(fi_in.layout_x)):
results.append(
get_dependent_turbines_by_wd(
fi_in, t_i, wd_array, change_threshold, limit_number, ws_test
)
)

df_out = (pd.DataFrame(data=results, columns=wd_array)
.transpose()
.reset_index().rename(columns={"index":"wd"}).set_index("wd")
)

return df_out

def get_all_impacting_turbines(fi_in, wd_array=np.arange(0., 360., 2.),
change_threshold=0.001, limit_number=None, ws_test=9.):
"""
Calculate which turbines impact a specified turbine based on the
FLORIS model. Essentially a wrapper for
get_dependent_turbines_by_wd() that loops over all turbines and
extracts their impact magnitudes, then sorts.

Args:
fi ([floris object]): FLORIS object of the farm of interest.
wd_array ([np.array]): Wind directions at which to determine
dependencies. Defaults to [0, 2, ... , 358].
change_threshold (float): Fractional change in power needed
to denote a dependency. Defaults to 0. (any change in power
is marked as a dependency)
limit_number (int | NoneType): Number of turbines that a
turbine can depend on. If None, returns all
turbines that each turbine depends on. Defaults to None.
ws_test (float): Wind speed at which FLORIS model is run to
determine dependencies. Defaults to 9. m/s.

Returns:
df_out ([pd.DataFrame]): A Pandas Dataframe in which each row
contains a wind direction, each column is a turbine, and
each entry is the turbines that the column turbine depends
on at the row wind direction. Dependencies can be extracted
as: For wind direction wd, the turbines that impact turbine
T are df_out.loc[wd, T]. Impacting turbines are simply
ordered by magnitude of impact.
"""

dependency_magnitudes = np.zeros(
(len(wd_array),len(fi_in.layout_x),len(fi_in.layout_x))
)

for t_i in range(len(fi_in.layout_x)):
_, ti_dep_mags = get_dependent_turbines_by_wd(
fi_in, t_i, wd_array, change_threshold, limit_number, ws_test,
return_influence_magnitudes=True
)
dependency_magnitudes[:,:,t_i] = ti_dep_mags

# Sort
impact_order = np.flip(np.argsort(dependency_magnitudes, axis=2), axis=2)

# Truncate to limit_number
impact_order = impact_order[:,:,:limit_number]

# Build up multi-level results list
results = []

for wd in range(len(wd_array)):
wd_results = []
for t_j in range(len(fi_in.layout_x)):
impacts_on_t_j = dependency_magnitudes[wd, t_j, :]
impact_order_t_j = impact_order[wd, t_j, :]
impact_order_t_j = impact_order_t_j[
impacts_on_t_j[impact_order_t_j] >= change_threshold
]
wd_results.append(list(impact_order_t_j))
results.append(wd_results)

# Convert to dataframe
df_out = (pd.DataFrame(data=results, index=wd_array)
.reset_index().rename(columns={"index":"wd"}).set_index("wd")
)

return df_out

# Wrapper function to easily set new TI values
def _fi_set_ws_wd_ti(fi, wd=None, ws=None, ti=None):
Expand Down
21 changes: 20 additions & 1 deletion tests/floris_tools_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import unittest
from flasc.floris_tools import (
calc_floris_approx_table,
interpolate_floris_from_df_approx
interpolate_floris_from_df_approx,
get_dependent_turbines_by_wd
)

from floris import tools as wfct
Expand Down Expand Up @@ -68,3 +69,21 @@ def test_floris_approx_table(self):
# self.assertTrue(("ti_002" in df.columns))
self.assertTrue(("pow_003" in df.columns))
self.assertAlmostEqual(df.shape[0], 3)

def test_get_dependent_turbines_by_wd(self):
# Load FLORIS object
fi = load_floris()

# compute the dependency on turbine 2 at 226 degrees
dep = get_dependent_turbines_by_wd(fi, 2, np.array([226]))
self.assertEqual(dep[0], [1, 6])

# Test the change_threshold
dep = get_dependent_turbines_by_wd(fi, 2, np.array([226]),
change_threshold=0.01)
self.assertEqual(dep[0], [1])

# Test the limit_number
dep = get_dependent_turbines_by_wd(fi, 2, np.array([226]),
limit_number=1)
self.assertEqual(dep[0], [1])