-
Notifications
You must be signed in to change notification settings - Fork 152
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
SciPy Yaw optimization classes: improved efficiency, automated downstream turbine exclusion and turbine-dependent weighing terms #245
Conversation
…bines from the optimization if the lower bound is equal to the upper bound. Also, I took particular care for allowing a non-zero fixed value for special situations, such as having an upstream turbine fixed at a yaw offset or mimicking non-zero vane bias. This functionality significantly speeds up optimizations since one can eliminate turbines beforehand, reducing the dimensionality of the optimization problem.
…. Now one can use a full farm model to optimize only for a subset of turbines. Doing this, one includes the wake effects of other turbines without caring about them in terms of power capture.
…or a select number of yaw angles through specifying the yaw angle bounds. For yaw angles of turbines that should be excluded as control variables, the lower bound should be specified as equal to the upper bound. This lb==ub does not necessarily have to be 0.0, but can also be a nonzero number, if one desires to keep that turbine fixed at that misalignment. Secondly, I implemented a turbine weighing function with which the floris-produced turbine power production values are multiplied in the calculation of the objective/cost function. This allows one to remove turbines from contributing to the cost function. This is particularly useful if one a subset of turbines takes part in a wake steering experiment, whereas the other turbines should be modeled for their wake effects.
…re the most downstreamturbines (i.e., their wakes have no effect on other turbines/the power of the farm). Then, these turbines are excluded in the optimization if exclude_downstream_turbines=True. This allows the user to easily reduce the number of variables to optimize without any loss in performance, potentially leading to significant reductions in computation time.
…in yaw_wind_rose.py (see previous commit)
…dimension did not match because the optimization variables were only reduced if bnds was not None. However, even with bnds=None, we can only be optimizing a subset of turbines due to exclude_downstream_turbines=True. Hence, always reduce variables. Worst case scenario, variables stay the same, negligible computational cost.
… Needs verification.
…so, this facilitates a bug fix where initially the downstream turbines were only calculated for the first wind direction in the array (for _wind_rose.py), while it should be calculated and the solution space is to be modified for every optimization call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey Bart, overall I think this is really nice, thanks for working on this. I like how you included the option to weight individual turbine powers instead of just having a binary option to include or not include turbines in the objective function. Also great that you modified to speed up the optimizations when bounds are set to the same value. I left several comments where I think some changes are needed, and also a bigger comment about whether we should use some existing functionality to determine downstream turbines.
…cified for a downstream machine
…on for exclude_downstream_turbines in yaw.py
…lude_downstream_turbines=True. Also, ensure initial conditions and predicted farm outputs are weighted appropriately with self.turbine_weights.
…e floris when passed to the optimization class. This variable is used to calculate the baseline power production and can differ from the initial guess in the optimization, self.x0. Also, self.x0 is now no longer changed within the optimization class. Rather, the user is forced to specify an appropriate array for self.x0 that meets the bounds.
Hi @ejsimley. Thank you very much for the detailed comments throughout the PR! I processed all your comments and hopefully resolved all of them. Please take a second look whenever is convenient for you. Thanks! |
if x0 is not None: | ||
self.x0 = x0 | ||
if not all(np.array(x0) == np.array(self.x_baseline)): | ||
print("WARNING: Baseline yaw angle in FLORIS differ from initial optimization conditions (self.x0)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this needs to be a warning. For example, I imagine it would be very common for the baseline values to be zero, but the initial guess for optimal offsets to be some other value.
power = -1 * self.fi.get_farm_power_for_yaw_angle( | ||
yaw_angles, | ||
# Create a full yaw angle array | ||
yaw_angles = np.array(self.x0, dtype=float) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing here, where setting the yaw angles for the uncontrolled turbines to their baseline values makes more sense to me. Especially since only a single set of x0 values can be specified for all wind directions. Depending on the wind direction, different turbines will be downstream (assuming downstream turbines are excluded), so it seems safer to set those yaw angles to their baseline values.
@@ -265,25 +296,75 @@ def _optimize(self): | |||
Returns: | |||
opt_yaw_angles (np.array): Optimal yaw angles of each turbine. | |||
""" | |||
opt_yaw_angles = np.array(self.x0, dtype=float) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
flagging this as another place where, to me, baseline yaw angles makes more sense than x0.
Hey @Bartdoekemeijer, the updates are really nice and resolve most of the issues I could think of. I left a few minor comments, though, mostly about whether x0 or x_baseline should be used for the uncontrolled turbines. |
…default wake slope to ensure validity for high turbulence cases
…mplement changes to yaw_wind_rose_parallel.py
Hey @ejsimley. Thank you for these suggestions! I think we are really converging now and the code is definitely a lot clearer and organized after addressing your comments. I have made the desired changes. The biggest change is that I introduced an additional input to the yaw optimization classes called Please review them whenever is convenient for you. Thank you again! |
OK, I think adding the |
Yes, indeed, the values for
This means |
That makes a lot of sense to me Bart! Regarding your first bullet point, I just remember you brought up the point that a user might want to force some turbines to have non-zero yaw angles to represent a yaw error bias or some other fault scenario. But maybe that was more for the baseline yaw offsets, not the optimized offsets. Either way, it is fine with me to set the downstream turbines to 0 yaw. |
…re all np.nan, besides for downstream turbines which are assigned a value closest to 0.0 constraint-allowing, and for equality-constrained turbines which have their value equal to their lower bound (=upper bound). This clarifies which values are assigned to downstream and equality-assigned turbines
…for situations such as where ws > ws_max or ws < ws_min. Namely, in that situation, we should just fall back to the baseline situation where we put every turbines yaw angle as close to 0.0 as possible.
…ose_parallel.py. Specifically, we now create a yaw_angles_template variable that is the default yaw angle array, meeting the equality constraints and also setting the right values for downstream turbines. In optimization, values for the turbines still be to optimized will be overwritten, and the remaining values (for equality-constrained and downstream turbines, if applicable) are unchanged. These changes clarify the default values for turbines, different from x0 (initial guess) and different from yaw_angles_baseline (baseline values for initial power evaluation).
Yes! I do want to facilitate the option to fix downstream turbines (or any turbines for that matter) to a non-zero yaw angle. By setting those turbines' yaw angles to a value closest to |
@ejsimley I think the latest commits should do it. Let me know what you think. Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey Bart, I think your latest changes address all the cases we could think of! It's possible there might be some use cases that aren't handled by the code now, but those can be addressed later if they come up. I left a couple comments where documentation should be cleaned up (one is a suggestion). After you address those, fine with me to merge!
Hi @ejsimley. I just updated the docstrings. I feel like I keep forgetting small things -- thanks for reviewing everything with such eye for detail! Hopefully I didn't miss anything else now and we can get this merged into the |
Thanks @Bartdoekemeijer! The documentation is very clear now. Appreciate all your work on this! I know I'll be using these new features a lot. |
This pull request is ready to be merged.
Feature or improvement description
Improvements to the
SciPy
yaw optimization classes for improved efficiency, automated downstream turbine exclusion and turbine-dependent weighing terms in the objective definition.Specifically, the novel additions are:
bnds
is now expanded to remove control variables in the optimization problem whenever a turbine's specified lower bound equals its upper bound, within a certain margin (0.001 deg
). Removing these turbines from the optimization speeds up the optimization, since the gradients of the objective function w.r.t. these variables need not be calculated and the dimensionality of the problem is smaller. This also circumvents certain numerical issues that I was experiencing in the SciPy optimization with recent versions ofSciPy
andnumpy
. Practically, the usage ofbnds
stays exactly the same as before -- it is just handled in a smarter way behind the scenes.bnds
now allows one to restrict a certain turbine to a nonzero misalignment angle, yet exclude it from the optimization problem. For example, one can opt to fix an upstream turbine to 20 degrees and only optimize the yaw angle for the second turbine in the row, simply bybnds[0] = [20.0, 20.0]
. Previously, in several parts in the code, the yaw angle of such a turbine would fall back to0.0
instead of its assigned value.SciPy
yaw optimization classes calledturbine_weights
. During each objective function call, the turbine power values from floris are multiplied with their corresponding weights as defined in this array. By setting certain turbines' weights to0
, the user can exclude turbines from the objective/cost function. For example, one may optimize a three-turbine array while modeling a larger wind farm. With this new input variable, one can simply assign zero weights to all turbines except the three-turbine array of interest. Yet, the other turbines are still part of each function call's simulation, thereby including their wake effects.turbine_weights
defaults to an array with length equal to the number of turbines, and all values being1.0
.SciPy
yaw optimization classes calledexclude_downstream_turbines
. This is a boolean which, when defined asTrue
, will exclude all turbines that have a wake which does not impinge any other turbine from the optimization problem. Hence, this makes it easy for the user to immediately remove any turbines that do not have any effect on the cost function, rather than having the optimization algorithm figure this out on its own. Here are some figures of the simplified wake model used to derive whether a turbine is waked or not, where the green dots indicate most-downstream turbines:Additionally, this is often more accurate than letting the optimization figure out certain turbines do not affect the cost function. Namely, those turbines' yaw angles do not necessarily converge to zero but instead often converge to a nonzero small value.
Related issue, if one exists
This is a more commonly requested feature within the wind farm controls group at NREL. This also fixes issue #207 .
Impacted areas of the software
The
SciPy
optimization module.Additional supporting information
@ejsimley has volunteered as a reviewer for this PR.
Test results, if applicable
Here is a simple test script based on the
optimize_yaw_wind_rose.py
example:which outputs:
Which seems like the right solutions, namely:
0.0
inturbine_weights
. For thewd=90.0
situation, turbine 0 is most downstream, and turbine 1 is directly impacting exclusively turbine 0. Hence, the yaw angle of turbine 1 is also as small as possible (2.0
, its lower bound). The yaw angle of turbine 2 is as high as possible (7.0
, upper bound) to steer the wake as far as possible away from turbine 1. The yaw angle of the last turbine is fixed at3.0
through the prespecified bounds. This turbine is therefore excluded from the optimization, and3.0
is indeed its found optimal value.wd=180.
andwd=45.
, all turbines operate in freestream. Therefore, all turbines have a minimal yaw angle, according to their bounds.wd=270.
degrees, we experience the traditional aligned optimization problem. Turbines 0, 1 and 2 all have their yaw angle equal to their upper bound. The power of turbine 0 is ignored in the total cost, but this does not significantly impact the optimization: it would only push for more wake steering in turbine 0.I have also tested the parallelized/MPI optimization code on Eagle successfully.