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 capability to sample and plot velocity profiles #699

Merged
merged 35 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c7c0fe3
Moved from other branch
vallbog Jun 28, 2023
6688ee2
Preparing inputs in FlorisInterface
vallbog Jun 28, 2023
55281b6
Created VelocityProfileGrid
vallbog Jun 29, 2023
b74f0ed
Velocity profiles returned successfully
vallbog Jun 29, 2023
51079f7
Small fixes
vallbog Jun 30, 2023
5c40659
Clarifications
vallbog Jul 2, 2023
0a7ba7b
Draft of init for VelocityProfilesFigure class
vallbog Jul 4, 2023
621556a
Small addition to class init
vallbog Jul 6, 2023
ae29d21
Methods for adding profiles and reference lines
vallbog Jul 20, 2023
3b847a1
Improved example
vallbog Jul 21, 2023
158cb99
Corrected self.axs
vallbog Jul 26, 2023
77d0e42
Comments, style check, and updated output coords
vallbog Aug 3, 2023
c75c517
Details in example
vallbog Aug 4, 2023
8165bcf
Updated comments
vallbog Aug 7, 2023
79681bb
Merge remote-tracking branch 'nrel/develop' into pr/vallbog/699
rafmudaf Nov 15, 2023
d259c05
Change example number and add to docs listing
rafmudaf Nov 15, 2023
21964c0
Replace LinesGrid with PointsGrid
rafmudaf Nov 16, 2023
7c0a2e8
Merge pull request #1 from rafmudaf/feature/velocity-deficit-profiles
vallbog Nov 18, 2023
452c1b7
Rotate sample points with the wind direction, and cleanup
vallbog Nov 19, 2023
afe10cd
Remove 5-dimensional indexing in point rotation
rafmudaf Nov 21, 2023
97bc2d2
Return figure axes from CutPlane vis
rafmudaf Nov 21, 2023
68fd463
Plot lines in horizontal plane
rafmudaf Nov 21, 2023
3c74f0b
Merge pull request #2 from rafmudaf/feature/velocity-deficit-profiles
vallbog Nov 24, 2023
782f711
Change coordinate notation and begin to update example
vallbog Nov 24, 2023
947717a
Correct sampling, use solve_for_points, and WIP example
vallbog Nov 24, 2023
8752a70
WIP: example
vallbog Nov 24, 2023
07ca673
WIP: example
vallbog Nov 25, 2023
96e8404
Finalize example
vallbog Nov 25, 2023
299bddb
Details
vallbog Nov 25, 2023
b85a9c4
Auto-set xlim
vallbog Nov 25, 2023
2747f72
Fix tabbing
rafmudaf Nov 28, 2023
54fd2a7
Merge branch 'develop' into feature/velocity-deficit-profiles
rafmudaf Nov 28, 2023
8eb47ff
Merge branch 'develop' into pr/vallbog/699
rafmudaf Dec 6, 2023
41e3f1a
Merge branch 'develop' into pr/vallbog/699
rafmudaf Dec 7, 2023
ebc69a5
Improve handling the velocity derivate at z=0
rafmudaf Dec 13, 2023
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
157 changes: 157 additions & 0 deletions examples/29_plot_velocity_deficit_profiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# 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 matplotlib.pyplot as plt
import numpy as np

from floris.tools import FlorisInterface
from floris.tools.visualization import VelocityProfilesFigure


"""
The first part of this example illustrates how to plot velocity deficit profiles at
several location downstream of a turbine. Here we use the following definition:
velocity_deficit = (homogeneous_wind_speed - u) / homogeneous_wind_speed
, where u is the wake velocity obtained when the incoming wind speed is the
same at all heights and equal to `homogeneous_wind_speed`.
The second part of the example shows a special case of how the profiles are affected
by a change in wind direction as well as a change in turbine location and sampling
starting point.
"""

def get_profiles(direction, resolution=100):
return fi.sample_velocity_deficit_profiles(
direction=direction,
downstream_dists=downstream_dists,
resolution=resolution,
homogeneous_wind_speed=homogeneous_wind_speed,
)

if __name__ == '__main__':
D = 126.0 # Turbine diameter
hub_height = 90.0
fi = FlorisInterface("inputs/gch.yaml")
fi.reinitialize(layout_x=[0.0], layout_y=[0.0])

downstream_dists = D * np.array([3, 5, 7])
homogeneous_wind_speed = 8.0

# Below, `get_profiles('y')` returns three velocity deficit profiles. These are sampled along
# three corresponding lines that are all parallel to the y-axis (cross-stream direction).
# The streamwise location of each line is given in `downstream_dists`.
# Similarly, `get_profiles('z')` samples profiles in the vertical direction (z) at the
# same streamwise locations.
profiles = get_profiles('y') + get_profiles('z')

# Initialize a VelocityProfilesFigure. The workflow is similar to a matplotlib Figure:
# Initialize it, plot data, and then customize it further if needed.
# The provided value of `layout` puts y-profiles on the top row of the figure and
# z-profiles on the bottom row.
profiles_fig = VelocityProfilesFigure(
downstream_dists_D=downstream_dists / D,
layout=['y', 'z'],
)
# Add profiles to the figure. This method automatically determines the direction and
# streamwise location of each profile from the profile coordinates.
profiles_fig.add_profiles(profiles, color='k')

# Change velocity model to jensen, get the velocity deficit profiles,
# and add them to the figure.
floris_dict = fi.floris.as_dict()
floris_dict['wake']['model_strings']['velocity_model'] = 'jensen'
fi = FlorisInterface(floris_dict)
profiles_y = get_profiles('y', resolution=400)
profiles_z = get_profiles('z', resolution=400)
profiles_fig.add_profiles(profiles_y + profiles_z, color='r')

margin = 0.05
profiles_fig.set_xlim([0.0 - margin, 0.6 + margin])
profiles_fig.add_ref_lines_y([-0.5, 0.5])
profiles_fig.add_ref_lines_z([-0.5, 0.5])

profiles_fig.axs[0,0].legend(['gauss', 'jensen'], fontsize=11)
profiles_fig.fig.suptitle(
'Velocity decifit profiles from different velocity models',
fontsize=14,
)

# Second part of example:
# Case 1: Show that, if we have a single turbine, then the profiles are independent of
# the wind direction. This is because x is defined to be in the streamwise direction
# in sample_velocity_deficit_profiles and VelocityProfilesFigure.
# Case 2: Show that the coordinates x/D, y/D and z/D returned by
# sample_velocity_deficit_profiles are relative to the sampling starting point.
# By default, this starting point is at (0.0, 0.0, fi.floris.flow_field.reference_wind_height)
# in inertial coordinates.
downstream_dists = D * np.array([3])
for case in [1, 2]:
# The first added profile is a reference
fi = FlorisInterface("inputs/gch.yaml")
fi.reinitialize(layout_x=[0.0], layout_y=[0.0])
profiles = get_profiles('y', resolution=400)
profiles_fig = VelocityProfilesFigure(
downstream_dists_D=downstream_dists / D,
layout=['y'],
ax_width=3.5,
ax_height=3.5,
)
profiles_fig.add_profiles(profiles, color='k')

if case == 1:
# Change wind direction compared to reference
wind_directions = [315.0]
layout_x, layout_y = [0.0], [0.0]
# Same as the default starting point but specified for completeness
x_inertial_start, y_inertial_start = 0.0, 0.0
elif case == 2:
# Change turbine location compared to reference. Then, set the sampling starting
# point to the new turbine location using the arguments
# `x_inertial_start` and `y_inertial_start`.
wind_directions = [270.0]
layout_x, layout_y = [D], [D]
x_inertial_start, y_inertial_start = D, D

# Plot a second profile to show that it is equivalent to the reference
fi.reinitialize(wind_directions=wind_directions, layout_x=layout_x, layout_y=layout_y)
profiles = fi.sample_velocity_deficit_profiles(
direction='y',
downstream_dists=downstream_dists,
resolution=21,
homogeneous_wind_speed=homogeneous_wind_speed,
x_inertial_start=x_inertial_start,
y_inertial_start=y_inertial_start,
)
profiles_fig.add_profiles(
profiles,
linestyle='None',
marker='o',
color='b',
markerfacecolor='None',
markeredgewidth=1.3,
)

if case == 1:
profiles_fig.axs[0,0].legend(['WD = 270 deg', 'WD = 315 deg'])
elif case == 2:
profiles_fig.fig.suptitle(
'Legend (x, y) locations in inertial coordinates.\n'
'x/D and y/D relative to sampling start point',
)
profiles_fig.axs[0,0].legend([
'turbine location: (0, 0)\nsampling start point: (0, 0)',
'turbine location: (D, D)\nsampling start point: (D, D)',
])

plt.show()
1 change: 1 addition & 0 deletions floris/simulation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
FlowFieldPlanarGrid,
Grid,
PointsGrid,
LinesGrid,
TurbineGrid,
TurbineCubatureGrid
)
Expand Down
86 changes: 86 additions & 0 deletions floris/simulation/floris.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

from pathlib import Path

import numpy as np
import pandas as pd
import yaml
from attrs import define, field

Expand All @@ -33,6 +35,7 @@
full_flow_sequential_solver,
full_flow_turbopark_solver,
Grid,
LinesGrid,
PointsGrid,
sequential_solver,
State,
Expand All @@ -41,6 +44,7 @@
turbopark_solver,
WakeModelManager,
)
from floris.type_dec import NDArrayFloat
from floris.utilities import load_yaml


Expand Down Expand Up @@ -314,6 +318,88 @@ def solve_for_points(self, x, y, z):

return self.flow_field.u_sorted[:,:,:,0,0] # Remove turbine grid dimensions

def solve_for_velocity_deficit_profiles(
self,
direction: str,
downstream_dists: NDArrayFloat | list,
profile_range: NDArrayFloat | list,
resolution: int,
homogeneous_wind_speed: float,
ref_rotor_diameter: float,
x_inertial_start: float,
y_inertial_start: float,
reference_height: float,
) -> list[pd.DataFrame]:
"""
Extract velocity deficit profiles. See
:py:meth:`~floris.tools.floris_interface.FlorisInterface.sample_velocity_deficit_profiles`
for more details.
"""

# Create a grid that contains coordinates for all the sample points in all profiles
field_grid = LinesGrid(
direction=direction,
downstream_dists=downstream_dists,
line_range=profile_range,
resolution=resolution,
x_inertial_start=x_inertial_start,
y_inertial_start=y_inertial_start,
reference_height=reference_height,
x_center_of_rotation=self.grid.x_center_of_rotation,
y_center_of_rotation=self.grid.y_center_of_rotation,
turbine_coordinates=self.farm.coordinates,
reference_turbine_diameter=self.farm.rotor_diameters,
grid_resolution=1,
wind_directions=self.flow_field.wind_directions,
wind_speeds=self.flow_field.wind_speeds,
time_series=self.flow_field.time_series,
)

self.flow_field.initialize_velocity_field(field_grid)

vel_model = self.wake.model_strings["velocity_model"]

if vel_model in ["cc", "turbopark"]:
raise NotImplementedError(
"solve_for_velocity_deficit_profiles is currently only available with the "
"gauss, jensen, and empirical_guass models."
)
elif vel_model == "empirical_gauss":
full_flow_empirical_gauss_solver(self.farm, self.flow_field, field_grid, self.wake)
else:
full_flow_sequential_solver(self.farm, self.flow_field, field_grid, self.wake)

n_profiles = len(downstream_dists)
x_relative_start = np.reshape(
field_grid.x_sorted[0, 0, :, 0, 0] - field_grid.x_start,
(n_profiles, resolution),
)
y_relative_start = np.reshape(
field_grid.y_sorted[0, 0, :, 0, 0] - field_grid.y_start,
(n_profiles, resolution),
)
z_relative_start = np.reshape(
field_grid.z_sorted[0, 0, :, 0, 0] - reference_height,
(n_profiles, resolution),
)
u = np.reshape(self.flow_field.u_sorted[0, 0, :, 0, 0], (n_profiles, resolution))
velocity_deficit = (homogeneous_wind_speed - u) / homogeneous_wind_speed

velocity_deficit_profiles = []

for i in range(n_profiles):
df = pd.DataFrame(
{
"x/D": x_relative_start[i]/ref_rotor_diameter,
"y/D": y_relative_start[i]/ref_rotor_diameter,
"z/D": z_relative_start[i]/ref_rotor_diameter,
"velocity_deficit": velocity_deficit[i],
}
)
velocity_deficit_profiles.append(df)

return velocity_deficit_profiles

def finalize(self):
# Once the wake calculation is finished, unsort the values to match
# the user-supplied order of things.
Expand Down
14 changes: 9 additions & 5 deletions floris/simulation/flow_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,15 @@ def initialize_velocity_field(self, grid: Grid) -> None:
# for height, using it here to apply the shear law makes that dimension store the vertical
# wind profile.
wind_profile_plane = (grid.z_sorted / self.reference_wind_height) ** self.wind_shear
dwind_profile_plane = (
self.wind_shear
* (1 / self.reference_wind_height) ** self.wind_shear
* (grid.z_sorted) ** (self.wind_shear - 1)
)
if self.wind_shear == 0.0:
# Avoid division by zero at z = 0.0 when wind_shear = 0.0
Copy link
Collaborator

@misi9170 misi9170 Aug 8, 2023

Choose a reason for hiding this comment

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

When does the divide by zero issue occur? Is it when self.reference_wind_height is zero? I was able to run the example to completion with and without this if statement

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It occurs when self.wind_shear is zero and grid.z_sorted happens to include the value zero (a sample point at the ground level). For instance, removing the ìf statement and modifing example 29 by replacing

profiles = get_profiles('y') + get_profiles('z')

with

profiles_y = get_profiles('y')
profile_range_z = [-hub_height, 3 * D]
profiles_z = fi.sample_velocity_deficit_profiles(
	direction='z',
	downstream_dists=downstream_dists,
	profile_range=profile_range_z,
	homogeneous_wind_speed=homogeneous_wind_speed,
)
profiles = profiles_y + profiles_z

gives the following warnings

/home/uname/python/floris/floris/simulation/flow_field.py:137: RuntimeWarning: divide by zero encountered in reciprocal
  * (grid.z_sorted) ** (self.wind_shear - 1)
/home/uname/python/floris/floris/simulation/flow_field.py:135: RuntimeWarning: invalid value encountered in multiply
  self.wind_shear

This is because the first element in profile_range_z makes the method include a sample point at ground level.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for clarifying! I see now: the $z^{(\alpha-1)}$ evaluates to $1/0$ when $z=0$ and $\alpha=0$. This produces an inf and raises a divide by zero warning; then, the multiplication by $\alpha=0$ raises an invalid multiplication warning (0 x inf) and results in a nan in dwind_profile_plane (the derivative of the shear profile layer).

However, I actually think we should let these warnings be raised. In my testing, they only appear once, and they let the user know that something strange is happening when one evaluates the derivative of the shear profile at $z=0$ (the derivative is not well defined). In fact the divide by zero problem occurs when $z=0$ for any value of shear less than 1 (not just when shear is 0). The resulting figures appear to be the same regardless.

@rafmudaf @bayc , what are our practices regarding letting warnings be raised in "unusual" circumstances?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok @vallbog @misi9170 I'm just coming up from my obsession on this question for the past couple of weeks. I started by trying to create a small unit test for this situation and realized that the Grid classes weren't able to be created from the from_dict method because they relied on the Vec3 class, so that was addressed in #751. In the process of that, I noticed that many of the classes in floris.simulation weren't proper slotted classes because the base classes weren't attrs-derived, and this exposed quite a bit of monkey patching - all of this was fixed in #750. Finally, I've been able to come back to this and now we can better test this particular situation through the unit testing infrastructure. I've written a general issue for this in #760.

In testing this further, I noticed that the if wind_shear == 0.0 condition is inadequate since the issue comes from the exponent in the derivate being less than 1 which causes the "divide by zero in reciprocal" runtime warning. As @misi9170 pointed out, this will happen if wind_shear is less than 1. However, it's actually the combination of wind shear less than 1 and z=0 that is problematic. To accommodate that, I propose to initialize the derivate to zeros and use np.power to calculate the derivative since it is a ufunc and supports the where= option:

        dwind_profile_plane = (
            self.wind_shear
            * (1 / self.reference_wind_height) ** self.wind_shear
            * np.power(
                grid.z_sorted,
                (self.wind_shear - 1),
                where=grid.z_sorted != 0.0
            )
        )

I initially used np.where, but that evaluates all operations and then picks out the ones that fit the condition so the runtime warning is still raised.

Here's a very simple script to test:

fi = FlorisInterface("inputs/gch.yaml")
x = [0.0] #, 0.0]
y = [0.0] #, 0.0]
z = [0.0] #, 10.0]
u_at_points = fi.sample_flow_at_points(x, y, z)

Without the proposed change, this raises a divide by zero warning.

Copy link
Collaborator

Choose a reason for hiding this comment

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

To not belabor the point, @misi9170 if you like it I'll make this commit and merge this pull request.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@rafmudaf Thanks, this all makes sense. Feel free to merge. That np.where() evaluating both and therefore still raising the issue is something I've run into before, and is a pretty annoying limitation. But that's another story...

dwind_profile_plane = np.zeros_like(grid.z_sorted)
else:
dwind_profile_plane = (
self.wind_shear
* (1 / self.reference_wind_height) ** self.wind_shear
* (grid.z_sorted) ** (self.wind_shear - 1)
)

# If no hetergeneous inflow defined, then set all speeds ups to 1.0
if self.het_map is None:
Expand Down
Loading