NREL / floris

A controls-oriented engineering wake model.
http://nrel.github.io/floris
BSD 3-Clause "New" or "Revised" License
215 stars 156 forks source link

Allow yaw optimization with disabled turbines #1027

Closed misi9170 closed 4 days ago

misi9170 commented 1 week ago

In #1016, @MiguelMarante raised the issue that the Serial Refine yaw optimizer ignores that some turbines are disabled. I converted this to an issue in #1022 .

This pull request enables that behavior, both for the Serial Refine routine as well as the Geometric yaw optimizer. Note that yaw optimization using Scipy is still not supported for the "mixed-operation" model (I looked into enabling this as well, but this will take more time and is not the priority).

Affected areas of the code

In making the necessary changes to the yaw optimization routines, I also had to make the following changes:

I've also added a new example, examples/examples_controloptimization/008....py, which is based heavily on @MiguelMarante 's example script in #1016.

Notes

In the yaw optimization routines, the solution involves passing power_setpoints through from the users fmodel to the fmodel_subset attributes on the yaw optimization routines. While this works fine, it is yet another example of where control setpoints have to be piped through various pieces of the FLORIS code. To enable other setpoints to be used while optimizing yaw, this process will have to be repeated. We could consider moving to some sort of high-level data structure (possible just a dictionary) for the control setpoints, so that they can be lumped together and passed through the FLORIS code more readily.

Results

Running the new example produces (removing the progress output from Serial Refine):

Serial Refine optimized yaw angles (all turbines active) [deg]:
 0    [25.0, 22.65625, 0.0]
1    [25.0, 22.65625, 0.0]
Name: yaw_angles_opt, dtype: object

Geometric optimized yaw angles (all turbines active) [deg]:
 0    [19.9952335557674, 19.9952335557674, 0.0]
1    [19.9952335557674, 19.9952335557674, 0.0]
Name: yaw_angles_opt, dtype: object

Serial Refine optimized yaw angles (some turbines disabled) [deg]:
 0    [20.3125, 0.0, 0.0]
1      [0.0, 25.0, 0.0]
Name: yaw_angles_opt, dtype: object

Geometric optimized yaw angles (some turbines disabled) [deg]:
 0    [14.990467111534794, 0.0, 0.0]
1      [0.0, 19.9952335557674, 0.0]

which is in line with expectations, see #1022.

misi9170 commented 4 days ago

This all looks good @misi9170 , I left a few small comments. Going to approve but one additional thought I had is we could perhaps add a small test or two to ensure we keep this desired behavior, but isn't clearly necessary

Thanks @paulf81 --- testing is a good idea, I'll write a small test to check it. I don't see any comments though---perhaps those got lost somehow?

paulf81 commented 4 days ago

This all looks good @misi9170 , I left a few small comments. Going to approve but one additional thought I had is we could perhaps add a small test or two to ensure we keep this desired behavior, but isn't clearly necessary

Thanks @paulf81 --- testing is a good idea, I'll write a small test to check it. I don't see any comments though---perhaps those got lost somehow?

Huh! They did all disappear, bummer! I don't think they were particularly insightful, from memory they were like: 1) Is the difference between floris.copy() versus copy.deepcopy() important? 2) Just flagging we may need to one day route the helix setpoints through the same architecture (but not this PR) 3) Checking my understanding of the example outputs that the yaw angles of disabled turbines are meaningless and can be anything

misi9170 commented 4 days ago

@paulf81 on those points:

  1. The deepcopy() is needed to copy over the power_setpoints. Currently, FlorisModel.copy() doesn't copy the control setpoints (yaw_angles, power_setpoints, etc) because it creates a FLORIS dictionary, which doesn't contain those values.
  2. Agreed. This is what I was trying to get at in the Notes section of the PR description---I think we need a better solution for passing around the control setpoints generally. Depending on what this looks like, it may also solve the copy() problem above.
  3. Yes, that's right. I toyed around with having special handling that would automatically set the yaw angles to zero for disabled turbines in the output dictionary. This happens somewhat naturally in the YawOptimizerGeometric code, because yaw angles are initialized at zero, but it was not immediately obvious how this should be done in YawOptimizerSR and besides, it's not really true that the "optimal" yaw angle is zero for a disabled turbine anyway. Still, this may confuse users, and perhaps having special handling to set those yaw angles to zero would be good (alternatively, set them to NaN, which could be done in YawOptimizerGeometric too).

I've now added tests for:

  1. setting simultaneously vs in multiple calls when using the "mixed" operation model
  2. Basic yaw optimization behavior for Serial Refine
  3. Yaw optimization behavior for Serial Refine when turbines are disabled
  4. The above two for the geometric yaw optimizer also
paulf81 commented 4 days ago

Thanks @misi9170 good to go on my end!

misi9170 commented 4 days ago

@paulf81 I have now added a small piece of code in the constructor of the base YawOptimization class that automatically assigns minimum and maximum yaw angles of zero to disabled turbines. This does help make the output yaw angles more intuitive---thanks for the suggestion.

misi9170 commented 4 days ago

I incorrectly merged this PR directly into the main branch. I have forced pushed the reversion back to the previous commit on main, so main no longer contains this PR's changes. I've opened a new PR #1031 to merge this code into develop, as intended.