-
Notifications
You must be signed in to change notification settings - Fork 157
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
rafmudaf
merged 35 commits into
NREL:develop
from
vallbog:feature/velocity-deficit-profiles
Dec 13, 2023
Merged
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 6688ee2
Preparing inputs in FlorisInterface
vallbog 55281b6
Created VelocityProfileGrid
vallbog b74f0ed
Velocity profiles returned successfully
vallbog 51079f7
Small fixes
vallbog 5c40659
Clarifications
vallbog 0a7ba7b
Draft of init for VelocityProfilesFigure class
vallbog 621556a
Small addition to class init
vallbog ae29d21
Methods for adding profiles and reference lines
vallbog 3b847a1
Improved example
vallbog 158cb99
Corrected self.axs
vallbog 77d0e42
Comments, style check, and updated output coords
vallbog c75c517
Details in example
vallbog 8165bcf
Updated comments
vallbog 79681bb
Merge remote-tracking branch 'nrel/develop' into pr/vallbog/699
rafmudaf d259c05
Change example number and add to docs listing
rafmudaf 21964c0
Replace LinesGrid with PointsGrid
rafmudaf 7c0a2e8
Merge pull request #1 from rafmudaf/feature/velocity-deficit-profiles
vallbog 452c1b7
Rotate sample points with the wind direction, and cleanup
vallbog afe10cd
Remove 5-dimensional indexing in point rotation
rafmudaf 97bc2d2
Return figure axes from CutPlane vis
rafmudaf 68fd463
Plot lines in horizontal plane
rafmudaf 3c74f0b
Merge pull request #2 from rafmudaf/feature/velocity-deficit-profiles
vallbog 782f711
Change coordinate notation and begin to update example
vallbog 947717a
Correct sampling, use solve_for_points, and WIP example
vallbog 8752a70
WIP: example
vallbog 07ca673
WIP: example
vallbog 96e8404
Finalize example
vallbog 299bddb
Details
vallbog b85a9c4
Auto-set xlim
vallbog 2747f72
Fix tabbing
rafmudaf 54fd2a7
Merge branch 'develop' into feature/velocity-deficit-profiles
rafmudaf 8eb47ff
Merge branch 'develop' into pr/vallbog/699
rafmudaf 41e3f1a
Merge branch 'develop' into pr/vallbog/699
rafmudaf ebc69a5
Improve handling the velocity derivate at z=0
rafmudaf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
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 thisif
statementThere 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.
It occurs when
self.wind_shear
is zero andgrid.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 replacingwith
gives the following warnings
This is because the first element in
profile_range_z
makes the method include a sample point at ground level.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.
Thanks for clarifying! I see now: the$z^{(\alpha-1)}$ evaluates to $1/0$ when $z=0$ and $\alpha=0$ . This produces an $\alpha=0$ raises an invalid multiplication warning (0 x
inf
and raises a divide by zero warning; then, the multiplication byinf
) and results in anan
indwind_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?
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.
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 theVec3
class, so that was addressed in #751. In the process of that, I noticed that many of the classes infloris.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 usenp.power
to calculate the derivative since it is a ufunc and supports thewhere=
option: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:
Without the proposed change, this raises a divide by zero warning.
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.
To not belabor the point, @misi9170 if you like it I'll make this commit and merge this pull request.
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.
@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...