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

Enabling vectorized time series calculation of wind conditions #400

Merged
merged 8 commits into from
Jun 3, 2022
Merged
89 changes: 89 additions & 0 deletions examples/18_demo_time_series.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# 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

"""
This example demonstrates running FLORIS in time series mode.

Typically when an array of wind directions and wind speeds are passed in FLORIS,
it is assumed these are defining a grid of wd/ws points to consider, as in a wind rose.
All combinations of wind direction and wind speed are therefore computed, and resulting
matrices, for example of turbine power are returned with martrices whose dimensions are
wind direction, wind speed and turbine number.

In time series mode, specified by setting the time_series flag of the FLORIS interface to True
each wd/ws pair is assumed to constitute a single point in time and each pair is computed.
Results are returned still as a 3 dimensional matrix, however the index of the (wd/ws) pair
is provided in the first dimension, the second dimension is fixed at 1, and the thrid is
turbine number again for consistency.

Note by not specifying yaw, the assumption is that all turbines are always pointing into the
current wind direction with no offset.
"""

# Initialize FLORIS to simple 4 turbine farm
fi = FlorisInterface("inputs/gch.yaml")

# Convert to a simple two turbine layout
fi.reinitialize(layout=([0, 500.], [0., 0.]))

# Create a fake time history where wind speed steps in the middle while wind direction
# Walks randomly
time = np.arange(0, 120, 10.) # Each time step represents a 10-minute average
ws = np.ones_like(time) * 8.
ws[int(len(ws) / 2):] = 9.
wd = np.ones_like(time) * 270.

for idx in range(1, len(time)):
wd[idx] = wd[idx - 1] + np.random.randn() * 2.


# Now intiialize FLORIS object to this history using time_series flag
fi.reinitialize(wind_directions=wd, wind_speeds=ws, time_series=True)

# Collect the powers
fi.calculate_wake()
turbine_powers = fi.get_turbine_powers() / 1000.

# Show the dimensions
num_turbines = len(fi.layout_x)
print('There are %d time samples, and %d turbines and so the resulting turbine power matrix has the shape:' % (len(time), num_turbines), turbine_powers.shape)


fig, axarr = plt.subplots(3, 1, sharex=True, figsize=(7,8))

ax = axarr[0]
ax.plot(time, ws, 'o-')
ax.set_ylabel('Wind Speed (m/s)')
ax.grid(True)

ax = axarr[1]
ax.plot(time, wd, 'o-')
ax.set_ylabel('Wind Direction (Deg)')
ax.grid(True)

ax = axarr[2]
for t in range(num_turbines):
ax.plot(time,turbine_powers[:, 0, t], 'o-', label='Turbine %d' % t)
ax.legend()
ax.set_ylabel('Turbine Power (kW)')
ax.set_xlabel('Time (minutes)')
ax.grid(True)

plt.show()
3 changes: 3 additions & 0 deletions floris/simulation/floris.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def __attrs_post_init__(self) -> None:
wind_directions=self.flow_field.wind_directions,
wind_speeds=self.flow_field.wind_speeds,
grid_resolution=self.solver["turbine_grid_points"],
time_series=self.flow_field.time_series,
)
elif self.solver["type"] == "flow_field_grid":
self.grid = FlowFieldGrid(
Expand All @@ -91,6 +92,7 @@ def __attrs_post_init__(self) -> None:
wind_directions=self.flow_field.wind_directions,
wind_speeds=self.flow_field.wind_speeds,
grid_resolution=self.solver["flow_field_grid_points"],
time_series=self.flow_field.time_series,
)
elif self.solver["type"] == "flow_field_planar_grid":
self.grid = FlowFieldPlanarGrid(
Expand All @@ -103,6 +105,7 @@ def __attrs_post_init__(self) -> None:
grid_resolution=self.solver["flow_field_grid_points"],
x1_bounds=self.solver["flow_field_bounds"][0],
x2_bounds=self.solver["flow_field_bounds"][1],
time_series=self.flow_field.time_series,
)
else:
raise ValueError(
Expand Down
13 changes: 10 additions & 3 deletions floris/simulation/flow_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class FlowField(FromDictMixin):
wind_shear: float = field(converter=float)
air_density: float = field(converter=float)
turbulence_intensity: float = field(converter=float)
reference_wind_height: float = field(converter=float)
reference_wind_height: int = field(converter=int)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm guessing this is by mistake. I think we want to keep reference_wind_height a float

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes good catch @Bartdoekemeijer

time_series : bool = field(default=False)

n_wind_speeds: int = field(init=False)
n_wind_directions: int = field(init=False)
Expand All @@ -55,7 +56,10 @@ class FlowField(FromDictMixin):
@wind_speeds.validator
def wind_speeds_validator(self, instance: attrs.Attribute, value: NDArrayFloat) -> None:
"""Using the validator method to keep the `n_wind_speeds` attribute up to date."""
self.n_wind_speeds = value.size
if self.time_series:
self.n_wind_speeds = 1
else:
self.n_wind_speeds = value.size

@wind_directions.validator
def wind_directions_validator(self, instance: attrs.Attribute, value: NDArrayFloat) -> None:
Expand Down Expand Up @@ -93,7 +97,10 @@ def initialize_velocity_field(self, grid: Grid) -> None:
# here to do broadcasting from left to right (transposed), and then transpose back.
# The result is an array the wind speed and wind direction dimensions on the left side
# of the shape and the grid.template array on the right
self.u_initial_sorted = (self.wind_speeds[None, :].T * wind_profile_plane.T).T * speed_ups
if self.time_series:
self.u_initial_sorted = (self.wind_speeds[:].T * wind_profile_plane.T).T * speed_ups
else:
self.u_initial_sorted = (self.wind_speeds[None, :].T * wind_profile_plane.T).T * speed_ups
self.v_initial_sorted = np.zeros(np.shape(self.u_initial_sorted), dtype=self.u_initial_sorted.dtype)
self.w_initial_sorted = np.zeros(np.shape(self.u_initial_sorted), dtype=self.u_initial_sorted.dtype)

Expand Down
6 changes: 5 additions & 1 deletion floris/simulation/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class Grid(ABC):
grid_resolution: int | Iterable = field()
wind_directions: NDArrayFloat = field(converter=floris_array_converter)
wind_speeds: NDArrayFloat = field(converter=floris_array_converter)
time_series: bool = field()

n_turbines: int = field(init=False)
n_wind_speeds: int = field(init=False)
Expand Down Expand Up @@ -88,7 +89,10 @@ def check_coordinates(self, instance: attrs.Attribute, value: list[Vec3]) -> Non
@wind_speeds.validator
def wind_speeds_validator(self, instance: attrs.Attribute, value: NDArrayFloat) -> None:
"""Using the validator method to keep the `n_wind_speeds` attribute up to date."""
self.n_wind_speeds = value.size
if self.time_series:
self.n_wind_speeds = 1
else:
self.n_wind_speeds = value.size

@wind_directions.validator
def wind_directions_validator(self, instance: attrs.Attribute, value: NDArrayFloat) -> None:
Expand Down
1 change: 1 addition & 0 deletions floris/simulation/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ def full_flow_sequential_solver(farm: Farm, flow_field: FlowField, flow_field_gr
wind_directions=turbine_grid_flow_field.wind_directions,
wind_speeds=turbine_grid_flow_field.wind_speeds,
grid_resolution=3,
time_series=turbine_grid_flow_field.time_series,
)
turbine_grid_farm.expand_farm_properties(
turbine_grid_flow_field.n_wind_directions, turbine_grid_flow_field.n_wind_speeds, turbine_grid.sorted_coord_indices
Expand Down
8 changes: 7 additions & 1 deletion floris/tools/floris_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ def reinitialize(
# turbine_id: list[str] | None = None,
# wtg_id: list[str] | None = None,
# with_resolution: float | None = None,
solver_settings: dict | None = None
solver_settings: dict | None = None,
time_series: bool | None = False
):
# Export the floris object recursively as a dictionary
floris_dict = self.floris.as_dict()
Expand Down Expand Up @@ -217,6 +218,11 @@ def reinitialize(
if turbine_type is not None:
farm_dict["turbine_type"] = turbine_type

if time_series:
flow_field_dict["time_series"] = True
else:
flow_field_dict["time_series"] = False

## Wake
# if wake is not None:
# self.floris.wake = wake
Expand Down
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def print_test_values(average_velocities: list, thrusts: list, powers: list, axi
N_TURBINES = len(X_COORDS)
ROTOR_DIAMETER = 126.0
TURBINE_GRID_RESOLUTION = 2
TIME_SERIES = False


## Unit test fixtures
Expand All @@ -116,7 +117,8 @@ def turbine_grid_fixture(sample_inputs_fixture) -> TurbineGrid:
reference_turbine_diameter=rotor_diameters,
wind_directions=np.array(WIND_DIRECTIONS),
wind_speeds=np.array(WIND_SPEEDS),
grid_resolution=TURBINE_GRID_RESOLUTION
grid_resolution=TURBINE_GRID_RESOLUTION,
time_series=TIME_SERIES
)

@pytest.fixture
Expand Down