Skip to content

Calculate sector-averaged rotor quantities (wind speed, turbulence intensity) - #1202

Open
bayc wants to merge 21 commits into
NatLabRockies:developfrom
bayc:feature/surrogate_loads
Open

bayc wants to merge 21 commits into
NatLabRockies:developfrom
bayc:feature/surrogate_loads

Conversation

@bayc

@bayc bayc commented Aug 31, 2026 •

Copy link
Copy Markdown
Collaborator

Enable TI and WS calculation to support surrogate models for DELs

This PR includes changes to support the use of FLORIS as part of the DEL surrogate load modeling process described in:

Guilloré, A., Campagnolo, F., & Bottasso, C. L. (2024, June). A control-oriented load surrogate model based on sector-averaged inflow quantities: capturing damage for unwaked, waked, wake-steering and curtailed wind turbines. In Journal of Physics: Conference Series (Vol. 2767, No. 3, p. 032019). IOP Publishing.

The main capabilities added are the calculation of sector-averaged wind speeds (SAWS) and sector-averaged turbulence intensities (SATI), as described in the reference above. In short, the turbine rotor is divided into 4 sectors, centered on the vertical and horizontal axes. The average WS and TI are calculated for each of these sectors and used as inputs into the surrogate model to predict various DELs.

In FLORIS, the averages in these sectors are computed as an average of the respective edge of the 3x3 square grid of points on the turbine's rotor plane. This also requires FLORIS to model TI on the rotor plane as heterogenous vs the current homogenous approach. A flag to enable to resulting change in turbine TI values has been added to preserve the previous method as the default.

Related issue

None.

Impacted areas of the software

sovler.py
deflection/gauss.py

4 new methods in floris_model.py:

  • get_turbine_powers_percent
  • get_turbine_grid_TIs
  • get_turbine_SAWS
  • get_turbine_SATI

Additional supporting information

None.

Test results, if applicable

Passing.

@bayc bayc changed the title Feature/surrogate loads Support calculations for computing surrogate load models Aug 31, 2026
@bayc
bayc requested a review from misi9170 August 31, 2026 20:19

@misi9170 misi9170 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @bayc ! You'll see that I left a laundry list of suggestions below. But overall, there are a couple of bigger things I'd like to get at. The first (and probably biggest) from the source code perspective is, is it possible to avoid the allocation of the new attributes on the FlowField class if the user sets enable_turbine_turbulence_grid to False? I'm finding a non-negligible increase in memory usage being caused by the new arrays, and we've run into memory issues in the past when running FLORIS with large wind farms, so I'd rather not add memory burden for users that won't be using this feature. Computational speed doesn't seem to be affected, so that's good!

Then, I'd like to have some additional information added so that users can understand what has been added here. I think at minimum that'd be an example that calculates the SAWS/SATI for various turbines in a farm and plots the result, but ideally we'd also have a new documentation page on this, too (possibly a new chapter under User reference?

Last, could we get some tests added for the new code?

Comment thread floris/core/flow_field.py Outdated
Comment thread floris/core/flow_field.py Outdated
Comment thread floris/core/flow_field.py Outdated
Comment thread floris/core/flow_field.py Outdated
Comment thread floris/core/flow_field.py Outdated
Comment thread floris/core/solver.py Outdated
Comment thread floris/floris_model.py Outdated
Comment thread floris/floris_model.py

return turbine_powers

def get_turbine_powers_percent(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I quite understand this method? It seems to compute the percentage of rated power that the power setpoint is, but the docstring states that it calculates the percentage of power that each turbine is producing, relative to its setpoint? Is this method being used (if so, maybe the docstring needs updating)?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo, good catch. It returns the percent of the max power for that turbine that the turbine is currently operating at. Updated to reflect the correct wording.

@misi9170 misi9170 Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe self.core.farm.power_setpoints / turbine_max_powers will return the percentage of max power that the setpoint is, regardless of whether the turbine is actually operating at that setpoint or not.

For instance, for a 5MW rated turbine, let's say the power setpoint is 3MW but the turbine is only operating at 2MW (due to the wind resource it is experiencing).

I think this method will return 60% (3/5). To me though, the docstring and name of the method seem to imply that the "answer" should be 40% (2/5). Could you confirm on that?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bayc , this is my only remaining concern. Once we're clear on this we'll be good to merge. However, if I'm honest, my preference is simply to remove this method entirely and push this operation to the user's software, since it doesn't have anything to do with the solve (just computes the ratio between two user inputs: the turbine power rating and the power setpoint).

Comment thread floris/floris_model.py Outdated
Comment thread floris/floris_model.py
@misi9170

misi9170 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

@bayc ,

I've made a few commits here to try to address my own concerns about increased memory usage. For reference, the last commit before my changes is 4f15fb8.

I've mostly tried to avoid declaring new attributes on FlowField. I've removed SAWS, SETI, turbulence_wake_mixing, and turbulence_intensity_field, and left only turbulence_wake_mixing_sorted as a new attribute; and I've made it so that turbulence_wake_mixing_sorted is only ever instantiated if the user runs Floris with the enable_turbine_turbulence_grid set to True. I'm using the following script to determine the size of both the FlowField object in isolation, and the entire Floris Core, for a solve with 1800 conditions and 100 turbines.

import copy
import sys

from conftest import SampleInputs
from pympler import asizeof # Will need to install, not part of the FLORIS requirements

from floris.core import Core

def run_floris():
    sample_inputs = SampleInputs()

    sample_inputs.core["wake"]["model_strings"]["velocity_model"] = "gauss"
    sample_inputs.core["wake"]["model_strings"]["deflection_model"] = "gauss"
    sample_inputs.core["wake"]["enable_secondary_steering"] = True
    sample_inputs.core["wake"]["enable_yaw_added_recovery"] = True
    sample_inputs.core["wake"]["enable_transverse_velocities"] = True
    sample_inputs.core["wake"]["enable_turbine_turbulence_grid"] = False # Comment out this line when running on develop

    N_TURBINES = 100
    N_FINDEX = 72 * 25  # Size of a characteristic wind rose

    TURBINE_DIAMETER = sample_inputs.core["farm"]["turbine_type"][0]["rotor_diameter"]
    sample_inputs.core["farm"]["layout_x"] = [5 * TURBINE_DIAMETER * i for i in range(N_TURBINES)]
    sample_inputs.core["farm"]["layout_y"] = [0.0 for i in range(N_TURBINES)]

    sample_inputs.core["flow_field"]["wind_directions"] = N_FINDEX * [270.0]
    sample_inputs.core["flow_field"]["wind_speeds"] = N_FINDEX * [8.0]
    sample_inputs.core["flow_field"]["turbulence_intensities"] = N_FINDEX * [0.06]

    N = 1
    for i in range(N):
        core = Core.from_dict(copy.deepcopy(sample_inputs.core))
        core.initialize_domain()
        core.steady_state_atmospheric_condition()

    print(asizeof.asizeof(core.flow_field)/1e6) # ~MB
    print("\n", asizeof.asizeof(core)/1e6) # ~MB

if __name__=="__main__":
    run_floris()

Running on develop, I get 145 MB for FlowField and 303 MB for Core, which sets the benchmark. Running before my changes to minimize memory usage, I get 197 / 354 MB, regardless of whether enable_turbine_turbulence_grid is set to True or False (since the memory is allocated either way). After my changes, I get 160 / 317 MB when enable_turbine_turbulence_grid is True and 146 / 304 MB when enable_turbine_turbulence_grid is False. That last quantity I am satisfied with---this is a <1% increase in memory over the existing code on develop.

Could you take a look over my changes and see if you are happy with them? I also went ahead and renamed the methods on FlorisModel to be more explicit, which will mean that anything calling Floris will need to be updated.

After this, I think a minimal product would be to add some tests; documentation and examples could wait (I'd just open an issue and tag you in it to do that at a later date). And maybe you could weigh in on whether the FlorisModel.get_turbine_powers_percent() is needed (and if so, update the doc string).

After these updates, I'd probably rereview to comb through minor bits and pieces.

@misi9170 misi9170 changed the title Support calculations for computing surrogate load models Calculate sector-averaged rotor quantities (wind speed, turbulence intensity) Sep 1, 2026
@bayc

bayc commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator Author

@bayc ,

I've made a few commits here to try to address my own concerns about increased memory usage. For reference, the last commit before my changes is 4f15fb8.

I've mostly tried to avoid declaring new attributes on FlowField. I've removed SAWS, SETI, turbulence_wake_mixing, and turbulence_intensity_field, and left only turbulence_wake_mixing_sorted as a new attribute; and I've made it so that turbulence_wake_mixing_sorted is only ever instantiated if the user runs Floris with the enable_turbine_turbulence_grid set to True. I'm using the following script to determine the size of both the FlowField object in isolation, and the entire Floris Core, for a solve with 1800 conditions and 100 turbines.

import copy
import sys

from conftest import SampleInputs
from pympler import asizeof # Will need to install, not part of the FLORIS requirements

from floris.core import Core

def run_floris():
    sample_inputs = SampleInputs()

    sample_inputs.core["wake"]["model_strings"]["velocity_model"] = "gauss"
    sample_inputs.core["wake"]["model_strings"]["deflection_model"] = "gauss"
    sample_inputs.core["wake"]["enable_secondary_steering"] = True
    sample_inputs.core["wake"]["enable_yaw_added_recovery"] = True
    sample_inputs.core["wake"]["enable_transverse_velocities"] = True
    sample_inputs.core["wake"]["enable_turbine_turbulence_grid"] = False # Comment out this line when running on develop

    N_TURBINES = 100
    N_FINDEX = 72 * 25  # Size of a characteristic wind rose

    TURBINE_DIAMETER = sample_inputs.core["farm"]["turbine_type"][0]["rotor_diameter"]
    sample_inputs.core["farm"]["layout_x"] = [5 * TURBINE_DIAMETER * i for i in range(N_TURBINES)]
    sample_inputs.core["farm"]["layout_y"] = [0.0 for i in range(N_TURBINES)]

    sample_inputs.core["flow_field"]["wind_directions"] = N_FINDEX * [270.0]
    sample_inputs.core["flow_field"]["wind_speeds"] = N_FINDEX * [8.0]
    sample_inputs.core["flow_field"]["turbulence_intensities"] = N_FINDEX * [0.06]

    N = 1
    for i in range(N):
        core = Core.from_dict(copy.deepcopy(sample_inputs.core))
        core.initialize_domain()
        core.steady_state_atmospheric_condition()

    print(asizeof.asizeof(core.flow_field)/1e6) # ~MB
    print("\n", asizeof.asizeof(core)/1e6) # ~MB

if __name__=="__main__":
    run_floris()

Running on develop, I get 145 MB for FlowField and 303 MB for Core, which sets the benchmark. Running before my changes to minimize memory usage, I get 197 / 354 MB, regardless of whether enable_turbine_turbulence_grid is set to True or False (since the memory is allocated either way). After my changes, I get 160 / 317 MB when enable_turbine_turbulence_grid is True and 146 / 304 MB when enable_turbine_turbulence_grid is False. That last quantity I am satisfied with---this is a <1% increase in memory over the existing code on develop.

Could you take a look over my changes and see if you are happy with them? I also went ahead and renamed the methods on FlorisModel to be more explicit, which will mean that anything calling Floris will need to be updated.

After this, I think a minimal product would be to add some tests; documentation and examples could wait (I'd just open an issue and tag you in it to do that at a later date). And maybe you could weigh in on whether the FlorisModel.get_turbine_powers_percent() is needed (and if so, update the doc string).

After these updates, I'd probably rereview to comb through minor bits and pieces.

@misi9170 Thanks for making these changes and tackling the memory issue! I reviewed your changes and believe they should all return the same values, so that looks good to me. I've also worked to address your comments and believe the PR is ready for your re-review.

@bayc
bayc requested a review from misi9170 September 24, 2026 21:21

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants