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 34 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
8 changes: 8 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ mast across all wind directions (at a fixed free stream wind speed).
Try different values for met_mast_option to vary the location of the met mast within
the two-turbine farm.

### 32_plot_velocity_deficit_profiles.py
This example illustrates how to plot velocity deficit profiles at several locations
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`.

### 29_floating_vs_fixedbottom_farm.py

Compares a fixed-bottom wind farm (with a gridded layout) to a floating
Expand Down
205 changes: 205 additions & 0 deletions examples/32_plot_velocity_deficit_profiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# 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 matplotlib import ticker

import floris.tools.visualization as wakeviz
from floris.tools import cut_plane, FlorisInterface
from floris.tools.visualization import VelocityProfilesFigure
from floris.utilities import reverse_rotate_coordinates_rel_west


"""
This example illustrates how to plot velocity deficit profiles at several locations
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 first two functions are just used to plot the coordinate system in which the
# profiles are sampled. Please go to the main function to begin the example.
def plot_coordinate_system(x_origin, y_origin, wind_direction):
quiver_length = 1.4 * D
plt.quiver(
[x_origin, x_origin],
[y_origin, y_origin],
[quiver_length, quiver_length],
[0, 0],
angles=[270 - wind_direction, 360 - wind_direction],
scale_units='x',
scale=1,
)
annotate_coordinate_system(x_origin, y_origin, quiver_length)

def annotate_coordinate_system(x_origin, y_origin, quiver_length):
x1 = np.array([quiver_length + 0.35 * D, 0.0])
x2 = np.array([0.0, quiver_length + 0.35 * D])
x3 = np.array([90.0, 90.0])
x, y, _ = reverse_rotate_coordinates_rel_west(
fi.floris.flow_field.wind_directions,
x1[None, :],
x2[None, :],
x3[None, :],
x_center_of_rotation=0.0,
y_center_of_rotation=0.0,
)
x = np.squeeze(x, axis=0) + x_origin
y = np.squeeze(y, axis=0) + y_origin
plt.text(x[0], y[0], '$x_1$', bbox={'facecolor': 'white'})
plt.text(x[1], y[1], '$x_2$', bbox={'facecolor': 'white'})

if __name__ == '__main__':
D = 126.0 # Turbine diameter
hub_height = 90.0
homogeneous_wind_speed = 8.0

fi = FlorisInterface("inputs/gch.yaml")
fi.reinitialize(layout_x=[0.0], layout_y=[0.0])

# ------------------------------ Single-turbine layout ------------------------------
# We first show how to sample and plot velocity deficit profiles on a single-turbine layout.
# Lines are drawn on a horizontal plane to indicate were the velocity is sampled.
downstream_dists = D * np.array([3, 5, 7])
# Sample three profiles 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`.
profiles = fi.sample_velocity_deficit_profiles(
direction='cross-stream',
downstream_dists=downstream_dists,
homogeneous_wind_speed=homogeneous_wind_speed,
)

horizontal_plane = fi.calculate_horizontal_plane(height=hub_height)
fig, ax = plt.subplots(figsize=(6.4, 3))
wakeviz.visualize_cut_plane(horizontal_plane, ax)
colors = ['b', 'g', 'c']
for i, profile in enumerate(profiles):
# Plot profile coordinates on the horizontal plane
ax.plot(profile['x'], profile['y'], colors[i], label=f'x/D={downstream_dists[i] / D:.1f}')
ax.set_xlabel('x [m]')
ax.set_ylabel('y [m]')
ax.set_title('Streamwise velocity in a horizontal plane: gauss velocity model')
fig.tight_layout(rect=[0, 0, 0.82, 1])
ax.legend(bbox_to_anchor=[1.29, 1.04])

# Initialize a VelocityProfilesFigure. The workflow is similar to a matplotlib Figure:
# Initialize it, plot data, and then customize it further if needed.
profiles_fig = VelocityProfilesFigure(
downstream_dists_D=downstream_dists / D,
layout=['cross-stream'],
coordinate_labels=['x/D', 'y/D'],
)
# Add profiles to the VelocityProfilesFigure. This method automatically matches the supplied
# profiles to the initialized axes in the figure.
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 = fi.sample_velocity_deficit_profiles(
direction='cross-stream',
downstream_dists=downstream_dists,
homogeneous_wind_speed=homogeneous_wind_speed,
resolution=400,
)
profiles_fig.add_profiles(profiles, color='r')

# The dashed reference lines show the extent of the rotor
profiles_fig.add_ref_lines_x2([-0.5, 0.5])
for ax in profiles_fig.axs[0]:
ax.xaxis.set_major_locator(ticker.MultipleLocator(0.2))

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

# -------------------------------- Two-turbine layout --------------------------------
# This is a two-turbine case where the wind direction is north-west. Velocity profiles
# are sampled behind the second turbine. This illustrates the need for a
# sampling-coordinate-system (x1, x2, x3) that is rotated such that x1 is always in the
# streamwise direction. The user may define the origin of this coordinate system
# (i.e. where to start sampling the profiles).
wind_direction = 315.0 # Try to change this
downstream_dists = D * np.array([3, 5])
floris_dict = fi.floris.as_dict()
floris_dict['wake']['model_strings']['velocity_model'] = 'gauss'
fi = FlorisInterface(floris_dict)
# Let (x_t1, y_t1) be the location of the second turbine
x_t1 = 2 * D
y_t1 = -2 * D
fi.reinitialize(wind_directions=[wind_direction], layout_x=[0.0, x_t1], layout_y=[0.0, y_t1])

# Extract profiles at a set of downstream distances from the starting point (x_start, y_start)
cross_profiles = fi.sample_velocity_deficit_profiles(
direction='cross-stream',
downstream_dists=downstream_dists,
homogeneous_wind_speed=homogeneous_wind_speed,
x_start=x_t1,
y_start=y_t1,
)

horizontal_plane = fi.calculate_horizontal_plane(height=hub_height, x_bounds=[-2 * D, 9 * D])
ax = wakeviz.visualize_cut_plane(horizontal_plane)
colors = ['b', 'g', 'c']
for i, profile in enumerate(cross_profiles):
ax.plot(
profile['x'],
profile['y'],
colors[i],
label=f'$x_1/D={downstream_dists[i] / D:.1f}$',
)
ax.set_xlabel('x [m]')
ax.set_ylabel('y [m]')
ax.set_title('Streamwise velocity in a horizontal plane')
ax.legend()
plot_coordinate_system(x_origin=x_t1, y_origin=y_t1, wind_direction=wind_direction)

# Sample velocity deficit profiles in the vertical direction at the same downstream
# locations as before. We stay directly downstream of the turbine (i.e. x2 = 0). These
# profiles are almost identical to the cross-stream profiles. However, we now explicitly
# set the profile range. The default range is [-2 * D, 2 * D].
vertical_profiles = fi.sample_velocity_deficit_profiles(
direction='vertical',
profile_range=[-1.5 * D, 1.5 * D],
downstream_dists=downstream_dists,
homogeneous_wind_speed=homogeneous_wind_speed,
x_start=x_t1,
y_start=y_t1,
)

profiles_fig = VelocityProfilesFigure(
downstream_dists_D=downstream_dists / D,
layout=['cross-stream', 'vertical'],
)
profiles_fig.add_profiles(cross_profiles + vertical_profiles, color='k')

profiles_fig.set_xlim([-0.05, 0.85])
profiles_fig.axs[1,0].set_ylim([-2.2, 2.2])
for ax in profiles_fig.axs[0]:
ax.xaxis.set_major_locator(ticker.MultipleLocator(0.4))

profiles_fig.fig.suptitle(
'Cross-stream profiles at hub-height, and\nvertical profiles at $x_2 = 0$',
fontsize=14,
)


plt.show()
83 changes: 82 additions & 1 deletion 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 @@ -42,7 +44,11 @@
turbopark_solver,
WakeModelManager,
)
from floris.utilities import load_yaml
from floris.type_dec import NDArrayFloat
from floris.utilities import (
load_yaml,
reverse_rotate_coordinates_rel_west,
)


@define
Expand Down Expand Up @@ -325,6 +331,81 @@ 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_start: float,
y_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.
# Effectively, this is a grid of parallel lines.
n_lines = len(downstream_dists)

# Coordinate system (x1, x2, x3) is used to define the sample points. The origin is at
# (x_start, y_start, reference_height) and x1 is in the streamwise direction.
# The x1-coordinate is fixed for every line (every row in `x1`).
x1 = np.atleast_2d(downstream_dists).T * np.ones((n_lines, resolution))

if resolution == 1:
single_line = [0.0]
else:
single_line = np.linspace(profile_range[0], profile_range[1], resolution)

if direction == 'cross-stream':
x2 = single_line * np.ones((n_lines, resolution))
x3 = np.zeros((n_lines, resolution))
elif direction == 'vertical':
x3 = single_line * np.ones((n_lines, resolution))
x2 = np.zeros((n_lines, resolution))

# Find the coordinates of the sample points in the inertial frame (x, y, z). This is done
# through one rotation and one translation.
x, y, z = reverse_rotate_coordinates_rel_west(
self.flow_field.wind_directions,
x1[None, :, :],
x2[None, :, :],
x3[None, :, :],
x_center_of_rotation=0.0,
y_center_of_rotation=0.0,
)
x = np.squeeze(x, axis=0) + x_start
y = np.squeeze(y, axis=0) + y_start
z = np.squeeze(z, axis=0) + reference_height

u = self.solve_for_points(x.flatten(), y.flatten(), z.flatten())
u = np.reshape(u[0, 0, :], (n_lines, resolution))
velocity_deficit = (homogeneous_wind_speed - u) / homogeneous_wind_speed

velocity_deficit_profiles = []

for i in range(n_lines):
df = pd.DataFrame(
{
'x': x[i],
'y': y[i],
'z': z[i],
'x1/D': x1[i]/ref_rotor_diameter,
'x2/D': x2[i]/ref_rotor_diameter,
'x3/D': x3[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 @@ -128,11 +128,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 heterogeneous inflow defined, then set all speeds ups to 1.0
if self.het_map is None:
Expand Down
Loading