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

Add simple turbine clustering functionality to SciPy yaw optimization #261

Merged
merged 33 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2d1a873
Implement yaw optimization functionality to automatically exclude tur…
Jul 22, 2021
5fb31cb
Add turbine weighing option to yaw.py in the scipy optimization suite…
Jul 22, 2021
35455a8
Implement functionality in YawOptimizationWindRose to only optimize f…
Jul 22, 2021
41691b9
Add functionality to yaw.py to automatically estimate what turbines a…
Jul 22, 2021
ae75697
Implement similar functionality for exclusion of downstream turbines …
Jul 22, 2021
108e20f
Quick bug fix for new features in yaw.py and yaw_wind_rose.py. Array …
Jul 22, 2021
fc85499
Implement new features in parallelized yaw optimization function too.…
Jul 22, 2021
c0775ce
Refactor some of the recent improvements into a separate function. Al…
Jul 22, 2021
63183a4
Bug fix for setting bounds closest to 0. when non-zero bounds are spe…
Aug 10, 2021
7dbaa24
Further fix bound fixing for downstream turbines. Also, add descripti…
Aug 10, 2021
6504d50
Add description for exclude_downstream_turbines docstring
Aug 10, 2021
55d39c4
Bug fixes to address applying bounds to a downstream machine when exc…
Aug 10, 2021
64c00b6
Move _reduce_control_variables to private methods
Aug 10, 2021
58ca7af
Add self.x_baseline as being the yaw angles that are inherently insid…
Aug 18, 2021
0bedbc3
Add more elaborate description in simple wake model and set a higher …
Aug 19, 2021
b65e0d2
Implement yaw_angles_baseline as separate variable
Aug 19, 2021
b40097b
Add INFO statement if not all baseline yaw angles are zero and also i…
Aug 19, 2021
ffd61f8
Add a variable called self.yaw_angles_template where the yaw angles a…
Aug 20, 2021
6ecf10a
Update yaw_angles_template to have a non-nan value for every turbine …
Aug 20, 2021
401e53e
Remove unused variables in _reduce_control_variables
Aug 20, 2021
954b336
Implement recent changes in yaw.py to yaw_wind_rose.py and yaw_wind_r…
Aug 20, 2021
6ed1d13
changing imports to be relative
bayc Aug 24, 2021
cddfe91
fixing power_rose plot; changing how string is created
bayc Aug 24, 2021
0e0bf58
Merge remote-tracking branch 'origin/develop' into revised-optimization
bayc Aug 24, 2021
e76fc4b
updating formatting
bayc Aug 24, 2021
c87dc35
Update yaw.py docstring
Aug 27, 2021
8bfa4e9
Update yaw_wind_rose.py and _parallel.py docstring
Aug 27, 2021
b83b67b
Merge branch 'revised-optimization' of github.com:Bartdoekemeijer/flo…
Aug 27, 2021
b8e2de1
Add function to cluster turbines and add a yaw optimization class tha…
Sep 17, 2021
61c54c5
Update docstring in yaw_clustered.py
Sep 17, 2021
dd39322
Add optimization classes for WindRose and WindRoseParallel that inclu…
Sep 17, 2021
4f19cea
Update class names and dependent in clustered and parallelized yaw op…
Sep 17, 2021
4792272
Merge branch 'develop' into revised-optimization-clustering
bayc Sep 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions floris/tools/optimization/scipy/cluster_turbines.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Copyright 2021 NREL

# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.

# See https://floris.readthedocs.io for documentation


import numpy as np
import matplotlib.pyplot as plt


def cluster_turbines(fi, wind_direction=None, wake_slope=0.30, plot_lines=False):
"""Separate a wind farm into separate clusters in which the turbines in
each subcluster only affects the turbines in its cluster and has zero
interaction with turbines from other clusters, both ways (being waked,
generating wake), This allows the user to separate the control setpoint
optimization in several lower-dimensional optimization problems, for
example. This function assumes a very simplified wake function where the
wakes are assumed to have a linearly diverging profile. In comparisons
with the FLORIS GCH model, the wake_slope matches well with the FLORIS'
wake profiles for a value of wake_slope = 0.5 * turbulence_intensity, where
turbulence_intensity is an input to the FLORIS model at the default
GCH parameterization. Note that does not include wind direction variability.
To be conservative, the user is recommended to use the rule of thumb:
`wake_slope = turbulence_intensity`. Hence, the default value for
`wake_slope=0.30` should be conservative for turbulence intensities up to
0.30 and is likely to provide valid estimates of which turbines are
downstream until a turbulence intensity of 0.50. This simple model saves
time compared to FLORIS.

Args:
fi ([floris object]): FLORIS object of the farm of interest.
wind_direction (float): The wind direction in the FLORIS frame
of reference for which the downstream turbines are to be determined.
wake_slope (float, optional): linear slope of the wake (dy/dx)
plot_lines (bool, optional): Enable plotting wakes/turbines.
Defaults to False.

Returns:
clusters (iterable): A list in which each entry contains a list
of turbine numbers that together form a cluster which
exclusively interact with one another and have zero
interaction with turbines outside of this cluster.
"""

if wind_direction is None:
wind_direction = np.mean(fi.floris.farm.wind_direction)

# Get farm layout
x = fi.layout_x
y = fi.layout_y
D = np.array([t.rotor_diameter for t in fi.floris.farm.turbines])
n_turbs = len(x)

# Rotate farm and determine freestream/waked turbines
is_downstream = [False for _ in range(n_turbs)]
x_rot = (
np.cos((wind_direction - 270.0) * np.pi / 180.0) * x
- np.sin((wind_direction - 270.0) * np.pi / 180.0) * y
)
y_rot = (
np.sin((wind_direction - 270.0) * np.pi / 180.0) * x
+ np.cos((wind_direction - 270.0) * np.pi / 180.0) * y
)

if plot_lines:
fig, ax = plt.subplots()
for ii in range(n_turbs):
ax.plot(
x_rot[ii] * np.ones(2),
[y_rot[ii] - D[ii] / 2, y_rot[ii] + D[ii] / 2],
"k",
)
for ii in range(n_turbs):
ax.text(x_rot[ii], y_rot[ii], "T%03d" % ii)
ax.axis("equal")

srt = np.argsort(x_rot)
usrt = np.argsort(srt)
x_rot_srt = x_rot[srt]
y_rot_srt = y_rot[srt]
affected_by_turbs = np.tile(False, (n_turbs, n_turbs))
for ii in range(n_turbs):
x0 = x_rot_srt[ii]
y0 = y_rot_srt[ii]

def wake_profile_ub_turbii(x):
y = (y0 + D[ii]) + (x - x0) * wake_slope
if isinstance(y, (float, np.float64, np.float32)):
if x < (x0 + 0.01):
y = -np.Inf
else:
y[x < x0 + 0.01] = -np.Inf
return y

def wake_profile_lb_turbii(x):
y = (y0 - D[ii]) - (x - x0) * wake_slope
if isinstance(y, (float, np.float64, np.float32)):
if x < (x0 + 0.01):
y = -np.Inf
else:
y[x < x0 + 0.01] = -np.Inf
return y

def determine_if_in_wake(xt, yt):
return (yt < wake_profile_ub_turbii(xt)) & (yt > wake_profile_lb_turbii(xt))

# Get most downstream turbine
is_downstream[ii] = not any(
[
determine_if_in_wake(x_rot_srt[iii], y_rot_srt[iii])
for iii in range(n_turbs)
]
)
# Determine which turbines are affected by this turbine ('ii')
affecting_following_turbs = [
determine_if_in_wake(x_rot_srt[iii], y_rot_srt[iii])
for iii in range(n_turbs)
]

# Determine by which turbines this turbine ('ii') is affected
for aft in np.where(affecting_following_turbs)[0]:
affected_by_turbs[aft, ii] = True

if plot_lines:
x1 = np.max(x_rot_srt) + 500.0
ax.fill_between(
[x0, x1, x1, x0],
[
wake_profile_ub_turbii(x0 + 0.02),
wake_profile_ub_turbii(x1),
wake_profile_lb_turbii(x1),
wake_profile_lb_turbii(x0 + 0.02),
],
alpha=0.1,
color="k",
edgecolor=None,
)

# Rearrange into initial frame of reference
affected_by_turbs = affected_by_turbs[:, usrt][usrt, :]
for ii in range(n_turbs):
affected_by_turbs[ii, ii] = True # Add self to turb_list_affected
affected_by_turbs = [np.where(c)[0] for c in affected_by_turbs]

# List of downstream turbines
turbs_downstream = [is_downstream[i] for i in usrt]
turbs_downstream = list(np.where(turbs_downstream)[0])

# Initialize one cluster for each turbine and all the turbines its affected by
clusters = affected_by_turbs

# Iteratively merge clusters if any overlap between turbines
ci = 0
while ci < len(clusters):
# Compare current row to the ones to the right of it
cj = ci + 1
merged_column = False
while cj < len(clusters):
if any([y in clusters[ci] for y in clusters[cj]]):
# Merge
clusters[ci] = np.hstack([clusters[ci], clusters[cj]])
clusters[ci] = np.array(np.unique(clusters[ci]), dtype=int)
clusters.pop(cj)
merged_column = True
else:
cj = cj + 1
if not merged_column:
ci = ci + 1

if plot_lines:
ax.set_title("wind_direction = %.1f deg" % wind_direction)
ax.set_xlim([np.min(x_rot) - 500.0, x1])
ax.set_ylim([np.min(y_rot) - 500.0, np.max(y_rot) + 500.0])
for ci, cl in enumerate(clusters):
ax.plot(x_rot[cl], y_rot[cl], 'o', label='cluster %d' % ci)
ax.legend()

return clusters
Loading