From 5b6abadb3dd70d251776fc030c22de105c165455 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 11 Jan 2024 12:58:34 -0700 Subject: [PATCH 001/101] move files around to get started --- floris/tools/time_series.py | 13 + floris/tools/{ => to_delete}/power_rose.py | 0 floris/tools/to_delete/wind_rose.py | 1597 +++++++++++++++++++ floris/tools/wind_rose.py | 1615 +------------------- 4 files changed, 1611 insertions(+), 1614 deletions(-) create mode 100644 floris/tools/time_series.py rename floris/tools/{ => to_delete}/power_rose.py (100%) create mode 100644 floris/tools/to_delete/wind_rose.py diff --git a/floris/tools/time_series.py b/floris/tools/time_series.py new file mode 100644 index 000000000..fcce3af6c --- /dev/null +++ b/floris/tools/time_series.py @@ -0,0 +1,13 @@ +# Copyright 2024 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 diff --git a/floris/tools/power_rose.py b/floris/tools/to_delete/power_rose.py similarity index 100% rename from floris/tools/power_rose.py rename to floris/tools/to_delete/power_rose.py diff --git a/floris/tools/to_delete/wind_rose.py b/floris/tools/to_delete/wind_rose.py new file mode 100644 index 000000000..c0996369d --- /dev/null +++ b/floris/tools/to_delete/wind_rose.py @@ -0,0 +1,1597 @@ +# 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 + +# TODO +# 1: reorganize into private and public methods +# 2: Include smoothing? + +import os +import pickle + +import dateutil +import matplotlib.cm as cm +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from pyproj import Proj +from scipy.interpolate import LinearNDInterpolator, NearestNDInterpolator + +import floris.utilities as geo + + +class WindRose: + """ + The WindRose class is used to organize information about the frequency of + occurance of different combinations of wind speed and wind direction (and + other optimal wind variables). A WindRose object can be used to help + calculate annual energy production (AEP) when combined with Floris power + calculations for different wind conditions. Several methods exist for + populating a WindRose object with wind data. WindRose also contains methods + for visualizing wind roses. + + References: + .. bibliography:: /references.bib + :style: unsrt + :filter: docname in docnames + :keyprefix: wr- + """ + + def __init__( + self, + ): + """ + Instantiate a WindRose object and set some initial parameter values. + No explicit arguments required, and an additional method will need to + be called to populate the WindRose object with data. + """ + # Initialize some varibles + self.num_wd = 0 + self.num_ws = 0 + self.wd_step = 1.0 + self.ws_step = 5.0 + self.wd = np.array([]) + self.ws = np.array([]) + self.df = pd.DataFrame() + + def save(self, filename): + """ + This method saves the WindRose data as a pickle file so that it can be + imported into a WindRose object later. + + Args: + filename (str): Path and filename of pickle file to save. + """ + pickle.dump( + [ + self.num_wd, + self.num_ws, + self.wd_step, + self.ws_step, + self.wd, + self.ws, + self.df, + ], + open(filename, "wb"), + ) + + def load(self, filename): + """ + This method loads data from a previously saved WindRose pickle file + into a WindRose object. + + Args: + filename (str): Path and filename of pickle file to load. + + Returns: + int, int, float, float, np.array, np.array, pandas.DataFrame: + + - Number of wind direction bins. + - Number of wind speed bins. + - Wind direction bin size (deg). + - Wind speed bin size (m/s). + - List of wind direction bin center values (deg). + - List of wind speed bin center values (m/s). + - DataFrame containing at least the following columns: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - Wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of + the wind conditions in the other columns. + """ + ( + self.num_wd, + self.num_ws, + self.wd_step, + self.ws_step, + self.wd, + self.ws, + self.df, + ) = pickle.load(open(filename, "rb")) + + return self.df + + def resample_wind_speed(self, df, ws=np.arange(0, 26, 1.0)): + """ + This method resamples the wind speed bins using the specified wind + speed bin center values. The frequency values are adjusted accordingly. + + Args: + df (pandas.DataFrame): Wind rose DataFrame containing at least the + following columns: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - Wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + wind conditions in the other columns. + + ws (np.array, optional): List of new wind speed center bins (m/s). + Defaults to np.arange(0, 26, 1.). + + Returns: + pandas.DataFrame: Wind rose DataFrame with the resampled wind speed + bins and frequencies containing at least the following columns: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - New wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + new wind conditions in the other columns. + """ + # Make a copy of incoming dataframe + df = df.copy(deep=True) + + # Get the wind step + ws_step = ws[1] - ws[0] + + # Ws + ws_edges = ws - ws_step / 2.0 + ws_edges = np.append(ws_edges, np.array(ws[-1] + ws_step / 2.0)) + + # Cut wind speed onto bins + df["ws"] = pd.cut(df.ws, ws_edges, labels=ws) + + # Regroup + df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() + + # Fill nans + df = df.fillna(0) + + # Reset the index + df = df.reset_index() + + # Set to float + for c in [c for c in df.columns if c != "freq_val"]: + df[c] = df[c].astype(float) + df[c] = df[c].astype(float) + + return df + + def internal_resample_wind_speed(self, ws=np.arange(0, 26, 1.0)): + """ + Internal method for resampling wind speed into desired bins. The + frequency values are adjusted accordingly. Modifies data within + WindRose object without explicit return. + + TODO: make a private method + + Args: + ws (np.array, optional): Vector of wind speed bin centers for + the wind rose (m/s). Defaults to np.arange(0, 26, 1.). + """ + # Update ws and wd binning + self.ws = ws + self.num_ws = len(ws) + self.ws_step = ws[1] - ws[0] + + # Update internal data frame + self.df = self.resample_wind_speed(self.df, ws) + + def resample_wind_direction(self, df, wd=np.arange(0, 360, 5.0)): + """ + This method resamples the wind direction bins using the specified wind + direction bin center values. The frequency values are adjusted + accordingly. + + Args: + df (pandas.DataFrame): Wind rose DataFrame containing at least the + following columns: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - Wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + wind conditions in the other columns. + + wd (np.array, optional): List of new wind direction center bins + (deg). Defaults to np.arange(0, 360, 5.). + + Returns: + pandas.DataFrame: Wind rose DataFrame with the resampled wind + direction bins and frequencies containing at least the following + columns: + + - **wd** (*float*) - New wind direction bin center values (deg). + - **ws** (*float*) - Wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + new wind conditions in the other columns. + """ + # Make a copy of incoming dataframe + df = df.copy(deep=True) + + # Get the wind step + wd_step = wd[1] - wd[0] + + # Get bin edges + wd_edges = wd - wd_step / 2.0 + wd_edges = np.append(wd_edges, np.array(wd[-1] + wd_step / 2.0)) + + # Get the overhangs + negative_overhang = wd_edges[0] + positive_overhang = wd_edges[-1] - 360.0 + + # Need potentially to wrap high angle direction to negative for correct + # binning + df["wd"] = geo.wrap_360(df.wd) + if negative_overhang < 0: + print("Correcting negative Overhang:%.1f" % negative_overhang) + df["wd"] = np.where( + df.wd.values >= 360.0 + negative_overhang, + df.wd.values - 360.0, + df.wd.values, + ) + + # Check on other side + if positive_overhang > 0: + print("Correcting positive Overhang:%.1f" % positive_overhang) + df["wd"] = np.where( + df.wd.values <= positive_overhang, df.wd.values + 360.0, df.wd.values + ) + + # Cut into bins + df["wd"] = pd.cut(df.wd, wd_edges, labels=wd) + + # Regroup + df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() + + # Fill nans + df = df.fillna(0) + + # Reset the index + df = df.reset_index() + + # Set to float Re-wrap + for c in [c for c in df.columns if c != "freq_val"]: + df[c] = df[c].astype(float) + df[c] = df[c].astype(float) + df["wd"] = geo.wrap_360(df.wd) + + return df + + def internal_resample_wind_direction(self, wd=np.arange(0, 360, 5.0)): + """ + Internal method for resampling wind direction into desired bins. The + frequency values are adjusted accordingly. Modifies data within + WindRose object without explicit return. + + TODO: make a private method + + Args: + wd (np.array, optional): Vector of wind direction bin centers for + the wind rose (deg). Defaults to np.arange(0, 360, 5.). + """ + # Update ws and wd binning + self.wd = wd + self.num_wd = len(wd) + self.wd_step = wd[1] - wd[0] + + # Update internal data frame + self.df = self.resample_wind_direction(self.df, wd) + + def resample_column(self, df, col, bins): + """ + This method resamples the specified wind parameter column using the + specified bin center values. The frequency values are adjusted + accordingly. + + Args: + df (pandas.DataFrame): Wind rose DataFrame containing at least the + following columns as well as *col*: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - Wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + wind conditions in the other columns. + + col (str): The name of the column to resample. + bins (np.array): List of new bin center values for the specified + column. + + Returns: + pandas.DataFrame: Wind rose DataFrame with the resampled wind + parameter bins and frequencies containing at least the following + columns as well as *col*: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - Wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + new wind conditions in the other columns. + """ + # Make a copy of incoming dataframe + df = df.copy(deep=True) + + # Cut into bins, make first and last bins extend to -/+ infinity + var_edges = np.append(0.5 * (bins[1:] + bins[:-1]), np.inf) + var_edges = np.append(-np.inf, var_edges) + df[col] = pd.cut(df[col], var_edges, labels=bins) + + # Regroup + df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() + + # Fill nans + df = df.fillna(0) + + # Reset the index + df = df.reset_index() + + # Set to float + for c in [c for c in df.columns if c != "freq_val"]: + df[c] = df[c].astype(float) + + return df + + def internal_resample_column(self, col, bins): + """ + Internal method for resampling column into desired bins. The frequency + values are adjusted accordingly. Modifies data within WindRose object + without explicit return. + + TODO: make a private method + + Args: + col (str): Name of column to resample. + bins (np.array): Vector of bins for the WindRose column. + """ + # Update internal data frame + self.df = self.resample_column(self.df, col, bins) + + def resample_average_ws_by_wd(self, df): + """ + This method calculates the mean wind speed for each wind direction bin + and resamples the wind rose, resulting in a single mean wind speed per + wind direction bin. The frequency values are adjusted accordingly. + + Args: + df (pandas.DataFrame): Wind rose DataFrame containing at least the + following columns: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - Wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + wind conditions in the other columns. + + Returns: + pandas.DataFrame: Wind rose DataFrame with the resampled wind speed + bins and frequencies containing at least the following columns: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - The average wind speed for each wind + direction bin (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + new wind conditions in the other columns. + """ + # Make a copy of incoming dataframe + df = df.copy(deep=True) + + ws_avg = [] + + for val in df.wd.unique(): + ws_avg.append( + np.array(df.loc[df["wd"] == val]["ws"] * df.loc[df["wd"] == val]["freq_val"]).sum() + / df.loc[df["wd"] == val]["freq_val"].sum() + ) + + # Regroup + df = df.groupby("wd").sum() + + df["ws"] = ws_avg + + # Reset the index + df = df.reset_index() + + # Set to float + df["ws"] = df.ws.astype(float) + df["wd"] = df.wd.astype(float) + + return df + + def internal_resample_average_ws_by_wd(self, wd=np.arange(0, 360, 5.0)): + """ + This internal method calculates the mean wind speed for each specified + wind direction bin and resamples the wind rose, resulting in a single + mean wind speed per wind direction bin. The frequency values are + adjusted accordingly. + + TODO: make an internal method + + Args: + wd (np.arange, optional): Wind direction bin centers (deg). + Defaults to np.arange(0, 360, 5.). + + Returns: + pandas.DataFrame: Wind rose DataFrame with the resampled wind speed + bins and frequencies containing at least the following columns: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - The average wind speed for each wind + direction bin (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + new wind conditions in the other columns. + """ + # Update ws and wd binning + self.wd = wd + self.num_wd = len(wd) + self.wd_step = wd[1] - wd[0] + + # Update internal data frame + self.df = self.resample_average_ws_by_wd(self.df) + + def interpolate( + self, + wind_directions: np.ndarray, + wind_speeds: np.ndarray, + mirror_0_to_360=True, + fill_value=0.0, + method="linear", + ): + """ + This method returns a linear interpolant that will return the occurrence + frequency for any given wind direction and wind speed combination(s). + This can be particularly useful when evaluating the wind rose at a + higher frequency than the input data is provided. + + Args: + wind_directions (np.ndarray): One or multi-dimensional array containing + the wind direction values at which the wind rose frequency of occurrence + should be evaluated. + wind_speeds (np.ndarray): One or multi-dimensional array containing + the wind speed values at which the wind rose frequency of occurrence + should be evaluated. + mirror_0_to_360 (bool, optional): This function copies the wind rose + frequency values from 0 deg to 360 deg. This can be useful when, for example, + the wind rose is only calculated until 357 deg but then interpolant is + requesting values at 359 deg. Defaults to True. + fill_value (float, optional): Fill value for the interpolant when + interpolating values outside of the data region. Defaults to 0.0. + method (str, optional): The interpolation method. Options are 'linear' and + 'nearest'. Recommended usage is 'linear'. Defaults to 'linear'. + + Returns: + scipy.interpolate.LinearNDInterpolant: Linear interpolant for the + wind rose currently available in the class (self.df). + + Example: + wr = wind_rose.WindRose() + wr.make_wind_rose_from_user_data(...) + freq_floris = wr.interpolate(floris_wind_direction_grid, floris_wind_speed_grid) + """ + if method == "linear": + interpolator = LinearNDInterpolator + elif method == "nearest": + interpolator = NearestNDInterpolator + else: + UserWarning("Unknown interpolation method: '{:s}'".format(method)) + + # Load windrose information from self + df = self.df.copy() + + if mirror_0_to_360: + # Copy values from 0 deg over to 360 deg + df_copy = df[df["wd"] == 0.0].copy() + df_copy["wd"] = 360.0 + df = pd.concat([df, df_copy], axis=0) + + interp = interpolator(points=df[["wd", "ws"]], values=df["freq_val"], fill_value=fill_value) + return interp(wind_directions, wind_speeds) + + def weibull(self, x, k=2.5, lam=8.0): + """ + This method returns a Weibull distribution corresponding to the input + data array (typically wind speed) using the specified Weibull + parameters. + + Args: + x (np.array): List of input data (typically binned wind speed + observations). + k (float, optional): Weibull shape parameter. Defaults to 2.5. + lam (float, optional): Weibull scale parameter. Defaults to 8.0. + + Returns: + np.array: Weibull distribution probabilities corresponding to + values in the input array. + """ + return (k / lam) * (x / lam) ** (k - 1) * np.exp(-((x / lam) ** k)) + + def make_wind_rose_from_weibull(self, wd=np.arange(0, 360, 5.0), ws=np.arange(0, 26, 1.0)): + """ + Populate WindRose object with an example wind rose with wind speed + frequencies given by a Weibull distribution. The wind direction + frequencies are initialized according to an example distribution. + + Args: + wd (np.array, optional): Wind direciton bin centers (deg). Defaults + to np.arange(0, 360, 5.). + ws (np.array, optional): Wind speed bin centers (m/s). Defaults to + np.arange(0, 26, 1.). + + Returns: + pandas.DataFrame: Wind rose DataFrame containing at least the + following columns: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - Wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + wind conditions in the other columns. + """ + # Use an assumed wind-direction for dir frequency + wind_dir = [ + 0, + 22.5, + 45, + 67.5, + 90, + 112.5, + 135, + 157.5, + 180, + 202.5, + 225, + 247.5, + 270, + 292.5, + 315, + 337.5, + ] + freq_dir = [ + 0.064, + 0.04, + 0.038, + 0.036, + 0.045, + 0.05, + 0.07, + 0.08, + 0.11, + 0.08, + 0.05, + 0.036, + 0.048, + 0.058, + 0.095, + 0.10, + ] + + freq_wd = np.interp(wd, wind_dir, freq_dir) + freq_ws = self.weibull(ws) + + freq_tot = np.zeros(len(wd) * len(ws)) + wd_tot = np.zeros(len(wd) * len(ws)) + ws_tot = np.zeros(len(wd) * len(ws)) + + count = 0 + for i in range(len(wd)): + for j in range(len(ws)): + wd_tot[count] = wd[i] + ws_tot[count] = ws[j] + + freq_tot[count] = freq_wd[i] * freq_ws[j] + count = count + 1 + + # renormalize + freq_tot = freq_tot / np.sum(freq_tot) + + # Load the wind toolkit data into a dataframe + df = pd.DataFrame() + + # Start by simply round and wrapping the wind direction and wind speed + # columns + df["wd"] = wd_tot + df["ws"] = ws_tot + + # Now group up + df["freq_val"] = freq_tot + + # Save the df at this point + self.df = df + # TODO is there a reason self.df is updated AND returned? + return self.df + + def make_wind_rose_from_user_data( + self, wd_raw, ws_raw, *args, wd=np.arange(0, 360, 5.0), ws=np.arange(0, 26, 1.0) + ): + """ + This method populates the WindRose object given user-specified + observations of wind direction, wind speed, and additional optional + variables. The wind parameters are binned and the frequencies of + occurance of each binned wind condition combination are calculated. + + Args: + wd_raw (array-like): An array-like list of all wind direction + observations used to calculate the normalized frequencies (deg). + ws_raw (array-like): An array-like list of all wind speed + observations used to calculate the normalized frequencies (m/s). + *args: Variable length argument list consisting of a sequence of + the following alternating arguments: + + - string - Name of additional wind parameters to include in + wind rose. + - array-like - Values of the additional wind parameters used + to calculate the frequencies of occurance + - np.array - Bin center values for binning the additional + wind parameters. + + wd (np.array, optional): Wind direction bin centers (deg). Defaults + to np.arange(0, 360, 5.). + ws (np.array, optional): Wind speed bin limits (m/s). Defaults to + np.arange(0, 26, 1.). + + Returns: + pandas.DataFrame: Wind rose DataFrame containing at least the + following columns: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - Wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + wind conditions in the other columns. + """ + df = pd.DataFrame() + + # convert inputs to np.array + wd_raw = np.array(wd_raw) + ws_raw = np.array(ws_raw) + + # Start by simply round and wrapping the wind direction and wind speed + # columns + df["wd"] = geo.wrap_360(wd_raw.round()) + df["ws"] = ws_raw.round() + + # Loop through *args and assign new dataframe columns after cutting + # into possibly irregularly-spaced bins + for in_var in range(0, len(args), 3): + df[args[in_var]] = np.array(args[in_var + 1]) + + # Cut into bins, make first and last bins extend to -/+ infinity + var_edges = np.append(0.5 * (args[in_var + 2][1:] + args[in_var + 2][:-1]), np.inf) + var_edges = np.append(-np.inf, var_edges) + df[args[in_var]] = pd.cut(df[args[in_var]], var_edges, labels=args[in_var + 2]) + + # Now group up + df["freq_val"] = 1.0 + df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() + df["freq_val"] = df.freq_val.astype(float) / df.freq_val.sum() + df = df.reset_index() + + # Save the df at this point + self.df = df + + # Resample onto the provided wind speed and wind direction binnings + self.internal_resample_wind_speed(ws=ws) + self.internal_resample_wind_direction(wd=wd) + + return self.df + + def read_wind_rose_csv(self, filename): + # Read in the csv + self.df = pd.read_csv(filename) + + # Renormalize the frequency column + self.df["freq_val"] = self.df["freq_val"] / self.df["freq_val"].sum() + + # Call the resample function in order to set all the internal variables + self.internal_resample_wind_speed(ws=self.df.ws.unique()) + self.internal_resample_wind_direction(wd=self.df.wd.unique()) + + def make_wind_rose_from_user_dist( + self, + wd_raw, + ws_raw, + freq_val, + *args, + wd=np.arange(0, 360, 5.0), + ws=np.arange(0, 26, 1.0), + ): + """ + This method populates the WindRose object given user-specified + combinations of wind direction, wind speed, additional optional + variables, and the corresponding frequencies of occurance. The wind + parameters are binned using the specified wind parameter bin center + values and the corresponding frequencies of occrance are calculated. + + Args: + wd_raw (array-like): An array-like list of wind directions + corresponding to the specified frequencies of occurance (deg). + wd_raw (array-like): An array-like list of wind speeds + corresponding to the specified frequencies of occurance (m/s). + freq_val (array-like): An array-like list of normalized frequencies + corresponding to the provided wind parameter combinations. + *args: Variable length argument list consisting of a sequence of + the following alternating arguments: + + - string - Name of additional wind parameters to include in + wind rose. + - array-like - Values of the additional wind parameters + corresponding to the specified frequencies of occurance. + - np.array - Bin center values for binning the additional + wind parameters. + + wd (np.array, optional): Wind direction bin centers (deg). Defaults + to np.arange(0, 360, 5.). + ws (np.array, optional): Wind speed bin centers (m/s). Defaults to + np.arange(0, 26, 1.). + + Returns: + pandas.DataFrame: Wind rose DataFrame containing at least the + following columns: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - Wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + wind conditions in the other columns. + """ + df = pd.DataFrame() + + # convert inputs to np.array + wd_raw = np.array(wd_raw) + ws_raw = np.array(ws_raw) + + # Start by simply wrapping the wind direction column + df["wd"] = geo.wrap_360(wd_raw) + df["ws"] = ws_raw + + # Loop through *args and assign new dataframe columns + for in_var in range(0, len(args), 3): + df[args[in_var]] = np.array(args[in_var + 1]) + + # Assign frequency column + df["freq_val"] = np.array(freq_val) + df["freq_val"] = df["freq_val"] / df["freq_val"].sum() + + # Save the df at this point + self.df = df + + # Resample onto the provided wind variable binnings + self.internal_resample_wind_speed(ws=ws) + self.internal_resample_wind_direction(wd=wd) + + # Loop through *args and resample using provided binnings + for in_var in range(0, len(args), 3): + self.internal_resample_column(args[in_var], args[in_var + 2]) + + return self.df + + def parse_wind_toolkit_folder( + self, + folder_name, + wd=np.arange(0, 360, 5.0), + ws=np.arange(0, 26, 1.0), + limit_month=None, + ): + """ + This method populates the WindRose object given raw wind direction and + wind speed data saved in csv files downloaded from the WIND Toolkit + application (see https://www.nrel.gov/grid/wind-toolkit.html for more + information). The wind parameters are binned using the specified wind + parameter bin center values and the corresponding frequencies of + occurance are calculated. + + Args: + folder_name (str): Path to the folder containing the WIND Toolkit + data files. + wd (np.array, optional): Wind direction bin centers (deg). Defaults + to np.arange(0, 360, 5.). + ws (np.array, optional): Wind speed bin centers (m/s). Defaults to + np.arange(0, 26, 1.). + limit_month (list, optional): List of ints of month(s) (e.g., 1, 2 + 3...) to consider when calculating the wind condition + frequencies. If none are specified, all months will be used. + Defaults to None. + + Returns: + pandas.DataFrame: Wind rose DataFrame containing the following + columns: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - Wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + wind conditions in the other columns. + """ + # Load the wind toolkit data into a dataframe + df = self.load_wind_toolkit_folder(folder_name, limit_month=limit_month) + + # Start by simply round and wrapping the wind direction and wind speed + # columns + df["wd"] = geo.wrap_360(df.wd.round()) + df["ws"] = geo.wrap_360(df.ws.round()) + + # Now group up + df["freq_val"] = 1.0 + df = df.groupby(["ws", "wd"]).sum() + df["freq_val"] = df.freq_val.astype(float) / df.freq_val.sum() + df = df.reset_index() + + # Save the df at this point + self.df = df + + # Resample onto the provided wind speed and wind direction binnings + self.internal_resample_wind_speed(ws=ws) + self.internal_resample_wind_direction(wd=wd) + + return self.df + + def load_wind_toolkit_folder(self, folder_name, limit_month=None): + """ + This method imports raw wind direction and wind speed data saved in csv + files in the specified folder downloaded from the WIND Toolkit + application (see https://www.nrel.gov/grid/wind-toolkit.html for more + information). + + TODO: make private method? + + Args: + folder_name (str): Path to the folder containing the WIND Toolkit + csv data files. + limit_month (list, optional): List of ints of month(s) (e.g., 1, 2, + 3...) to consider when calculating the wind condition + frequencies. If none are specified, all months will be used. + Defaults to None. + + Returns: + pandas.DataFrame: DataFrame containing the following columns: + + - **wd** (*float*) - Raw wind direction data (deg). + - **ws** (*float*) - Raw wind speed data (m/s). + """ + file_list = os.listdir(folder_name) + file_list = [os.path.join(folder_name, f) for f in file_list if ".csv" in f] + + df = pd.DataFrame() + for f_idx, f in enumerate(file_list): + print("%d of %d: %s" % (f_idx, len(file_list), f)) + df_temp = self.load_wind_toolkit_file(f, limit_month=limit_month) + df = df.append(df_temp) + + return df + + def load_wind_toolkit_file(self, filename, limit_month=None): + """ + This method imports raw wind direction and wind speed data saved in the + specified csv file downloaded from the WIND Toolkit application (see + https://www.nrel.gov/grid/wind-toolkit.html for more information). + + TODO: make private method? + + Args: + filename (str): Path to the WIND Toolkit csv file. + limit_month (list, optional): List of ints of month(s) (e.g., 1, 2, + 3...) to consider when calculating the wind condition + frequencies. If none are specified, all months will be used. + Defaults to None. + + Returns: + pandas.DataFrame: DataFrame containing the following columns with + data from the WIND Toolkit file: + + - **wd** (*float*) - Raw wind direction data (deg). + - **ws** (*float*) - Raw wind speed data (m/s). + """ + df = pd.read_csv(filename, header=3, sep=",") + + # If asked to limit to particular months + if limit_month is not None: + df = df[df.Month.isin(limit_month)] + + # Save just what I want + speed_column = [c for c in df.columns if "speed" in c][0] + direction_column = [c for c in df.columns if "direction" in c][0] + df = df.rename(index=str, columns={speed_column: "ws", direction_column: "wd"})[ + ["wd", "ws"] + ] + + return df + + def import_from_wind_toolkit_hsds( + self, + lat, + lon, + ht=100, + wd=np.arange(0, 360, 5.0), + ws=np.arange(0, 26, 1.0), + include_ti=False, + limit_month=None, + limit_hour=None, + st_date=None, + en_date=None, + ): + """ + This method populates the WindRose object using wind data from the WIND + Toolkit dataset (https://www.nrel.gov/grid/wind-toolkit.html) for the + specified lat/long coordinate in the continental US. The wind data + are obtained from the WIND Toolkit dataset using the HSDS service (see + https://github.com/NREL/hsds-examples). The wind data returned is + obtained from the nearest 2km x 2km grid point to the input + coordinate and is limited to the years 2007-2013. The wind parameters + are binned using the specified wind parameter bin center values and the + corresponding frequencies of occrance are calculated. + + Requires h5pyd package, which can be installed using: + pip install --user git+http://github.com/HDFGroup/h5pyd.git + + Then, make a configuration file at ~/.hscfg containing: + + hs_endpoint = https://developer.nrel.gov/api/hsds + + hs_username = None + + hs_password = None + + hs_api_key = 3K3JQbjZmWctY0xmIfSYvYgtIcM3CN0cb1Y2w9bf + + The example API key above is for demonstation and is + rate-limited per IP. To get your own API key, visit + https://developer.nrel.gov/signup/. + + More information can be found at: https://github.com/NREL/hsds-examples. + + Args: + lat (float): Latitude in degrees. + lon (float): Longitude in degrees. + ht (int, optional): The height above ground where wind + information is obtained (m). Defaults to 100. + wd (np.array, optional): Wind direction bin centers (deg). Defaults + to np.arange(0, 360, 5.). + ws (np.array, optional): Wind speed bin centers (m/s). Defaults to + np.arange(0, 26, 1.). + include_ti (bool, optional): Determines whether turbulence + intensity is included as an additional parameter. If True, TI + is added as an additional wind rose variable, estimated based + on the Obukhov length from WIND Toolkit. Defaults to False. + limit_month (list, optional): List of ints of month(s) (e.g., 1, 2, + 3...) to consider when calculating the wind condition + frequencies. If none are specified, all months will be used. + Defaults to None. + limit_hour (list, optional): List of ints of hour(s) (e.g., 0, 1, + ... 23) to consider when calculating the wind condition + frequencies. If none are specified, all hours will be used. + Defaults to None. + st_date (str, optional): The start date to consider when creating + the wind rose, formatted as 'MM-DD-YYYY'. If not specified data + beginning in 2007 will be used. Defaults to None. + en_date (str, optional): The end date to consider when creating + the wind rose, formatted as 'MM-DD-YYYY'. If not specified data + through 2013 will be used. Defaults to None. + + Returns: + pandas.DataFrame: Wind rose DataFrame containing at least the + following columns: + + - **wd** (*float*) - Wind direction bin center values (deg). + - **ws** (*float*) - Wind speed bin center values (m/s). + - **freq_val** (*float*) - The frequency of occurance of the + wind conditions in the other columns. + """ + # Check inputs + + # Array of hub height data avaliable on Toolkit + h_range = [10, 40, 60, 80, 100, 120, 140, 160, 200] + + if st_date is not None: + if dateutil.parser.parse(st_date) > dateutil.parser.parse("12-13-2013 23:00"): + print("Error, invalid date range. Valid range: 01-01-2007 - " + "12/31/2013") + return None + + if en_date is not None: + if dateutil.parser.parse(en_date) < dateutil.parser.parse("01-01-2007 00:00"): + print("Error, invalid date range. Valid range: 01-01-2007 - " + "12/31/2013") + return None + + if h_range[0] > ht: + print( + "Error, height is not in the range of avaliable " + + "WindToolKit data. Minimum height = 10m" + ) + return None + + if h_range[-1] < ht: + print( + "Error, height is not in the range of avaliable " + + "WindToolKit data. Maxiumum height = 200m" + ) + return None + + # Load wind speeds and directions from WimdToolkit + + # Case for turbine height (ht) matching discrete avaliable height + # (h_range) + if ht in h_range: + d = self.load_wind_toolkit_hsds( + lat, + lon, + ht, + include_ti=include_ti, + limit_month=limit_month, + limit_hour=limit_hour, + st_date=st_date, + en_date=en_date, + ) + + ws_new = d["ws"] + wd_new = d["wd"] + if include_ti: + ti_new = d["ti"] + + # Case for ht not matching discete height + else: + h_range_up = next(x[0] for x in enumerate(h_range) if x[1] > ht) + h_range_low = h_range_up - 1 + h_up = h_range[h_range_up] + h_low = h_range[h_range_low] + + # Load data for boundary cases of ht + d_low = self.load_wind_toolkit_hsds( + lat, + lon, + h_low, + include_ti=include_ti, + limit_month=limit_month, + limit_hour=limit_hour, + st_date=st_date, + en_date=en_date, + ) + + d_up = self.load_wind_toolkit_hsds( + lat, + lon, + h_up, + include_ti=include_ti, + limit_month=limit_month, + limit_hour=limit_hour, + st_date=st_date, + en_date=en_date, + ) + + # Wind Speed interpolation + ws_low = d_low["ws"] + ws_high = d_up["ws"] + + ws_new = np.array(ws_low) * (1 - ((ht - h_low) / (h_up - h_low))) + np.array( + ws_high + ) * ((ht - h_low) / (h_up - h_low)) + + # Wind Direction interpolation using Circular Mean method + wd_low = d_low["wd"] + wd_high = d_up["wd"] + + sin0 = np.sin(np.array(wd_low) * (np.pi / 180)) + cos0 = np.cos(np.array(wd_low) * (np.pi / 180)) + sin1 = np.sin(np.array(wd_high) * (np.pi / 180)) + cos1 = np.cos(np.array(wd_high) * (np.pi / 180)) + + sin_wd = sin0 * (1 - ((ht - h_low) / (h_up - h_low))) + sin1 * ( + (ht - h_low) / (h_up - h_low) + ) + cos_wd = cos0 * (1 - ((ht - h_low) / (h_up - h_low))) + cos1 * ( + (ht - h_low) / (h_up - h_low) + ) + + # Interpolated wind direction + wd_new = 180 / np.pi * np.arctan2(sin_wd, cos_wd) + + # TI is independent of height + if include_ti: + ti_new = d_up["ti"] + + # Create a dataframe named df + if include_ti: + df = pd.DataFrame({"ws": ws_new, "wd": wd_new, "ti": ti_new}) + else: + df = pd.DataFrame({"ws": ws_new, "wd": wd_new}) + + # Start by simply round and wrapping the wind direction and wind speed + # columns + df["wd"] = geo.wrap_360(df.wd.round()) + df["ws"] = df.ws.round() + + # Now group up + df["freq_val"] = 1.0 + df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() + df["freq_val"] = df.freq_val.astype(float) / df.freq_val.sum() + df = df.reset_index() + + # Save the df at this point + self.df = df + + # Resample onto the provided wind speed and wind direction binnings + self.internal_resample_wind_speed(ws=ws) + self.internal_resample_wind_direction(wd=wd) + + return self.df + + def load_wind_toolkit_hsds( + self, + lat, + lon, + ht=100, + include_ti=False, + limit_month=None, + limit_hour=None, + st_date=None, + en_date=None, + ): + """ + This method returns a pandas DataFrame containing hourly wind speed, + wind direction, and optionally estimated turbulence intensity data + using wind data from the WIND Toolkit dataset + (https://www.nrel.gov/grid/wind-toolkit.html) for the specified + lat/long coordinate in the continental US. The wind data are obtained + from the WIND Toolkit dataset using the HSDS service + (see https://github.com/NREL/hsds-examples). The wind data returned is + obtained from the nearest 2km x 2km grid point to the input coordinate + and is limited to the years 2007-2013. + + TODO: make private method? + + Args: + lat (float): Latitude in degrees. + lon (float): Longitude in degrees + ht (int, optional): The height above ground where wind + information is obtained (m). Defaults to 100. + include_ti (bool, optional): Determines whether turbulence + intensity is included as an additional parameter. If True, TI + is added as an additional wind rose variable, estimated based + on the Obukhov length from WIND Toolkit. Defaults to False. + limit_month (list, optional): List of ints of month(s) (e.g., 1, 2, + 3...) to consider when calculating the wind condition + frequencies. If none are specified, all months will be used. + Defaults to None. + limit_hour (list, optional): List of ints of hour(s) (e.g., 0, 1, + ... 23) to consider when calculating the wind condition + frequencies. If none are specified, all hours will be used. + Defaults to None. + st_date (str, optional): The start date to consider, formatted as + 'MM-DD-YYYY'. If not specified data beginning in 2007 will be + used. Defaults to None. + en_date (str, optional): The end date to consider, formatted as + 'MM-DD-YYYY'. If not specified data through 2013 will be used. + Defaults to None. + + Returns: + pandas.DataFrame: DataFrame containing the following columns(abd + optionally turbulence intensity) with hourly data from WIND Toolkit: + + - **wd** (*float*) - Raw wind direction data (deg). + - **ws** (*float*) - Raw wind speed data (m/s). + """ + import h5pyd + + # Open the wind data "file" + # server endpoint, username, password is found via a config file + f = h5pyd.File("/nrel/wtk-us.h5", "r") + + # assign wind direction, wind speed, optional ti, and time datasets for + # the desired height + wd_dset = f["winddirection_" + str(ht) + "m"] + ws_dset = f["windspeed_" + str(ht) + "m"] + if include_ti: + obkv_dset = f["inversemoninobukhovlength_2m"] + dt = f["datetime"] + dt = pd.DataFrame({"datetime": dt[:]}, index=range(0, dt.shape[0])) + dt["datetime"] = dt["datetime"].apply(dateutil.parser.parse) + + # find dataset indices from lat/long + Location_idx = self.indices_for_coord(f, lat, lon) + + # check if in bounds + if ( + (Location_idx[0] < 0) + | (Location_idx[0] >= wd_dset.shape[1]) + | (Location_idx[1] < 0) + | (Location_idx[1] >= wd_dset.shape[2]) + ): + print( + "Error, coordinates out of bounds. WIND Toolkit database " + + "covers the continental United States." + ) + return None + + # create dataframe with wind direction and wind speed + df = pd.DataFrame() + df["wd"] = wd_dset[:, Location_idx[0], Location_idx[1]] + df["ws"] = ws_dset[:, Location_idx[0], Location_idx[1]] + if include_ti: + L = self.obkv_dset_to_L(obkv_dset, Location_idx) + ti = self.ti_calculator_IU2(L) + df["ti"] = ti + df["datetime"] = dt["datetime"] + + # limit dates if start and end dates are provided + if st_date is not None: + df = df[df.datetime >= st_date] + + if en_date is not None: + df = df[df.datetime < en_date] + + # limit to certain months if specified + if limit_month is not None: + df["month"] = df["datetime"].map(lambda x: x.month) + df = df[df.month.isin(limit_month)] + if limit_hour is not None: + df["hour"] = df["datetime"].map(lambda x: x.hour) + df = df[df.hour.isin(limit_hour)] + if include_ti: + df = df[["wd", "ws", "ti"]] + else: + df = df[["wd", "ws"]] + + return df + + def obkv_dset_to_L(self, obkv_dset, Location_idx): + """ + This function returns an array containing hourly Obukhov lengths from + the WIND Toolkit dataset for the specified Lat/Lon coordinate indices. + + Args: + obkv_dset (np.ndarray): Dataset for Obukhov lengths from WIND + Toolkit. + Location_idx (tuple): A tuple containing the Lat/Lon coordinate + indices of interest in the Obukhov length dataset. + + Returns: + np.array: An array containing Obukhov lengths for each time index + in the Wind Toolkit dataset (m). + """ + linv = obkv_dset[:, Location_idx[0], Location_idx[1]] + # avoid divide by zero + linv[linv == 0.0] = 0.0003 + L = 1 / linv + return L + + def ti_calculator_IU2(self, L): + """ + This function estimates the turbulence intensity corresponding to each + Obukhov length value in the input list using the relationship between + Obukhov length bins and TI given in the I_U2SODAR column in Table 2 of + :cite:`wr-wharton2010assessing`. + + Args: + L (iterable): A list of Obukhov Length values (m). + + Returns: + list: A list of turbulence intensity values expressed as fractions. + """ + ti_set = [] + for i in L: + # Strongly Stable + if 0 < i < 100: + TI = 0.04 # paper says < 8%, so using 4% + # Stable + elif 100 < i < 600: + TI = 0.09 + # Neutral + elif abs(i) > 600: + TI = 0.115 + # Convective + elif -600 < i < -50: + TI = 0.165 + # Strongly Convective + elif -50 < i < 0: + # no upper bound given, so using the lowest + # value from the paper for this stability bin + TI = 0.2 + ti_set.append(TI) + return ti_set + + def indices_for_coord(self, f, lat_index, lon_index): + """ + This method finds the nearest x/y indices of the WIND Toolkit dataset + for a given lat/lon coordinate in the continental US. Rather than + fetching the entire coordinates database, which is 500+ MB, this uses + the Proj4 library to find a nearby point and then converts to x/y + indices. + + **Note**: This method is obtained directly from: + https://github.com/NREL/hsds-examples/blob/master/notebooks/01_WTK_introduction.ipynb, + where it is called "indicesForCoord." + + Args: + f (h5pyd.File): A HDF5 "file" used to access the WIND Toolkit data. + lat_index (float): Latitude coordinate for which dataset indices + are to be found (degrees). + lon_index (float): Longitude coordinate for which dataset indices + are to be found (degrees). + + Returns: + tuple: A tuple containing the Lat/Lon coordinate indices of + interest in the WIND Toolkit dataset. + """ + dset_coords = f["coordinates"] + projstring = """+proj=lcc +lat_1=30 +lat_2=60 + +lat_0=38.47240422490422 +lon_0=-96.0 + +x_0=0 +y_0=0 +ellps=sphere + +units=m +no_defs """ + projectLcc = Proj(projstring) + origin_ll = reversed(dset_coords[0][0]) # Grab origin directly from database + origin = projectLcc(*origin_ll) + + coords = (lon_index, lat_index) + coords = projectLcc(*coords) + delta = np.subtract(coords, origin) + ij = [int(round(x / 2000)) for x in delta] + return tuple(reversed(ij)) + + def plot_wind_speed_all(self, ax=None, label=None): + """ + This method plots the wind speed frequency distribution of the WindRose + object averaged across all wind directions. If no axis is provided, a + new one is created. + + Args: + ax (:py:class:`matplotlib.pyplot.axes`, optional): Figure axes on + which data should be plotted. Defaults to None. + """ + if ax is None: + _, ax = plt.subplots() + + df_plot = self.df.groupby("ws").sum() + ax.plot(self.ws, df_plot.freq_val, label=label) + + def plot_wind_speed_by_direction(self, dirs, ax=None): + """ + This method plots the wind speed frequency distribution of the WindRose + object for each specified wind direction bin center. The wind + directions are resampled using the specified bin centers and the + frequencies of occurance of the wind conditions are modified + accordingly. If no axis is provided, a new one is created. + + Args: + dirs (np.array): A list of wind direction bin centers for which + wind speed distributions are plotted (deg). + ax (:py:class:`matplotlib.pyplot.axes`, optional): Figure axes on + which data should be plotted. Defaults to None. + """ + # Get a downsampled frame + df_plot = self.resample_wind_direction(self.df, wd=dirs) + + if ax is None: + _, ax = plt.subplots() + + for wd in dirs: + df_plot_sub = df_plot[df_plot.wd == wd] + ax.plot(df_plot_sub.ws, df_plot_sub["freq_val"], label=wd) + ax.legend() + + def plot_wind_rose( + self, + ax=None, + color_map="viridis_r", + ws_right_edges=np.array([5, 10, 15, 20, 25]), + wd_bins=np.arange(0, 360, 15.0), + legend_kwargs={}, + ): + """ + This method creates a wind rose plot showing the frequency of occurance + of the specified wind direction and wind speed bins. If no axis is + provided, a new one is created. + + **Note**: Based on code provided by Patrick Murphy from the University + of Colorado Boulder. + + Args: + ax (:py:class:`matplotlib.pyplot.axes`, optional): The figure axes + on which the wind rose is plotted. Defaults to None. + color_map (str, optional): Colormap to use. Defaults to 'viridis_r'. + ws_right_edges (np.array, optional): The upper bounds of the wind + speed bins (m/s). The first bin begins at 0. Defaults to + np.array([5, 10, 15, 20, 25]). + wd_bins (np.array, optional): The wind direction bin centers used + for plotting (deg). Defaults to np.arange(0, 360, 15.). + legend_kwargs (dict, optional): Keyword arguments to be passed to + ax.legend(). + + Returns: + :py:class:`matplotlib.pyplot.axes`: A figure axes object containing + the plotted wind rose. + """ + # Resample data onto bins + df_plot = self.resample_wind_direction(self.df, wd=wd_bins) + + # Make labels for wind speed based on edges + ws_step = ws_right_edges[1] - ws_right_edges[0] + ws_labels = ["%d-%d m/s" % (w - ws_step, w) for w in ws_right_edges] + + # Grab the wd_step + wd_step = wd_bins[1] - wd_bins[0] + + # Set up figure + if ax is None: + _, ax = plt.subplots(subplot_kw={"polar": True}) + + # Get a color array + color_array = cm.get_cmap(color_map, len(ws_right_edges)) + + for wd in wd_bins: + rects = [] + df_plot_sub = df_plot[df_plot.wd == wd] + for ws_idx, ws in enumerate(ws_right_edges[::-1]): + plot_val = df_plot_sub[ + df_plot_sub.ws <= ws + ].freq_val.sum() # Get the sum of frequency up to this wind speed + rects.append( + ax.bar( + np.radians(wd), + plot_val, + width=0.9 * np.radians(wd_step), + color=color_array(ws_idx), + edgecolor="k", + ) + ) + # break + + # Configure the plot + ax.legend(reversed(rects), ws_labels, **legend_kwargs) + ax.set_theta_direction(-1) + ax.set_theta_offset(np.pi / 2.0) + ax.set_theta_zero_location("N") + ax.set_xticks(np.arange(0, 2 * np.pi, np.pi / 4)) + ax.set_xticklabels(["N", "NE", "E", "SE", "S", "SW", "W", "NW"]) + + return ax + + def plot_wind_rose_ti( + self, + ax=None, + color_map="viridis_r", + ti_right_edges=np.array([0.06, 0.1, 0.14, 0.18, 0.22]), + wd_bins=np.arange(0, 360, 15.0), + ): + """ + This method creates a wind rose plot showing the frequency of occurance + of the specified wind direction and turbulence intensity bins. This + requires turbulence intensity to already be included as a parameter in + the wind rose. If no axis is provided,a new one is created. + + **Note**: Based on code provided by Patrick Murphy from the University + of Colorado Boulder. + + Args: + ax (:py:class:`matplotlib.pyplot.axes`, optional): The figure axes + on which the wind rose is plotted. Defaults to None. + color_map (str, optional): Colormap to use. Defaults to 'viridis_r'. + ti_right_edges (np.array, optional): The upper bounds of the + turbulence intensity bins. The first bin begins at 0. Defaults + to np.array([0.06, 0.1, 0.14, 0.18,0.22]). + wd_bins (np.array, optional): The wind direction bin centers used + for plotting (deg). Defaults to np.arange(0, 360, 15.). + + Returns: + :py:class:`matplotlib.pyplot.axes`: A figure axes object containing + the plotted wind rose. + """ + + # Resample data onto bins + df_plot = self.resample_wind_direction(self.df, wd=wd_bins) + + # Make labels for TI based on edges + ti_step = ti_right_edges[1] - ti_right_edges[0] + ti_labels = ["%.2f-%.2f " % (w - ti_step, w) for w in ti_right_edges] + + # Grab the wd_step + wd_step = wd_bins[1] - wd_bins[0] + + # Set up figure + if ax is None: + _, ax = plt.subplots(subplot_kw={"polar": True}) + + # Get a color array + color_array = cm.get_cmap(color_map, len(ti_right_edges)) + + for wd in wd_bins: + rects = [] + df_plot_sub = df_plot[df_plot.wd == wd] + for ti_idx, ti in enumerate(ti_right_edges[::-1]): + plot_val = df_plot_sub[ + df_plot_sub.ti <= ti + ].freq_val.sum() # Get the sum of frequency up to this wind speed + rects.append( + ax.bar( + np.radians(wd), + plot_val, + width=0.9 * np.radians(wd_step), + color=color_array(ti_idx), + edgecolor="k", + ) + ) + + # Configure the plot + ax.legend(reversed(rects), ti_labels, loc="lower right", title="TI") + ax.set_theta_direction(-1) + ax.set_theta_offset(np.pi / 2.0) + ax.set_theta_zero_location("N") + ax.set_xticks(np.arange(0, 2 * np.pi, np.pi / 4)) + ax.set_xticklabels(["N", "NE", "E", "SE", "S", "SW", "W", "NW"]) + + return ax + + def plot_ti_ws(self, ax=None, ws_bins=np.arange(0, 26, 1.0)): + """ + This method plots the wind speed frequency distribution of the WindRose + object for each turbulence intensity bin. The wind speeds are resampled + using the specified bin centers and the frequencies of occurance of the + wind conditions are modified accordingly. This method assumes there are + five TI bins. If no axis is provided, a new one is created. + + Args: + ax (:py:class:`matplotlib.pyplot.axes`, optional): Figure axes on + which data should be plotted. Defaults to None. + ws_bins (np.array, optional): A list of wind speed bin centers on + which the wind speeds are resampled before plotting (m/s). + Defaults to np.arange(0, 26, 1.). + + Returns: + :py:class:`matplotlib.pyplot.axes`: A figure axes object containing + the plotted wind speed distributions. + """ + + # Resample data onto bins + df_plot = self.resample_wind_speed(self.df, ws=ws_bins) + + df_plot = df_plot.groupby(["ws", "ti"]).sum() + df_plot = df_plot.reset_index() + + if ax is None: + _, ax = plt.subplots(figsize=(10, 7)) + + tis = df_plot["ti"].drop_duplicates() + margin_bottom = np.zeros(len(df_plot["ws"].drop_duplicates())) + colors = ["#1e5631", "#a4de02", "#76ba1b", "#4c9a2a", "#acdf87"] + + for num, ti in enumerate(tis): + values = list(df_plot[df_plot["ti"] == ti].loc[:, "freq_val"]) + + df_plot[df_plot["ti"] == ti].plot.bar( + x="ws", + y="freq_val", + ax=ax, + bottom=margin_bottom, + color=colors[num], + label=ti, + ) + + margin_bottom += values + + plt.title("Turbulence Intensity Frequencies as Function of Wind Speed") + plt.xlabel("Wind Speed (m/s)") + plt.ylabel("Frequency") + + return ax + + def export_for_floris_opt(self): + """ + This method returns a list of tuples of at least wind speed, wind + direction, and frequency of occurance, which can be used to help loop + through different wind conditions for Floris power calculations. + + Returns: + list: A list of tuples containing all combinations of wind + parameters and frequencies of occurance in the WindRose object's + wind rose DataFrame values. + """ + # Return a list of tuples, where each tuple is (ws,wd,freq) + return [tuple(x) for x in self.df.values] diff --git a/floris/tools/wind_rose.py b/floris/tools/wind_rose.py index 6725af485..fcce3af6c 100644 --- a/floris/tools/wind_rose.py +++ b/floris/tools/wind_rose.py @@ -1,4 +1,4 @@ -# Copyright 2021 NREL +# Copyright 2024 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 @@ -11,1616 +11,3 @@ # the License. # See https://floris.readthedocs.io for documentation - -# TODO -# 1: reorganize into private and public methods -# 2: Include smoothing? - -import os -import pickle - -import dateutil -import matplotlib.cm as cm -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from scipy.interpolate import LinearNDInterpolator, NearestNDInterpolator - -import floris.utilities as geo - - -# from pyproj import Proj - - - -class WindRose: - """ - The WindRose class is used to organize information about the frequency of - occurance of different combinations of wind speed and wind direction (and - other optimal wind variables). A WindRose object can be used to help - calculate annual energy production (AEP) when combined with Floris power - calculations for different wind conditions. Several methods exist for - populating a WindRose object with wind data. WindRose also contains methods - for visualizing wind roses. - - References: - .. bibliography:: /references.bib - :style: unsrt - :filter: docname in docnames - :keyprefix: wr- - """ - - def __init__(self,): - """ - Instantiate a WindRose object and set some initial parameter values. - No explicit arguments required, and an additional method will need to - be called to populate the WindRose object with data. - """ - # Initialize some varibles - self.num_wd = 0 - self.num_ws = 0 - self.wd_step = 1.0 - self.ws_step = 5.0 - self.wd = np.array([]) - self.ws = np.array([]) - self.df = pd.DataFrame() - - def save(self, filename): - """ - This method saves the WindRose data as a pickle file so that it can be - imported into a WindRose object later. - - Args: - filename (str): Path and filename of pickle file to save. - """ - pickle.dump( - [ - self.num_wd, - self.num_ws, - self.wd_step, - self.ws_step, - self.wd, - self.ws, - self.df, - ], - open(filename, "wb"), - ) - - def load(self, filename): - """ - This method loads data from a previously saved WindRose pickle file - into a WindRose object. - - Args: - filename (str): Path and filename of pickle file to load. - - Returns: - int, int, float, float, np.array, np.array, pandas.DataFrame: - - - Number of wind direction bins. - - Number of wind speed bins. - - Wind direction bin size (deg). - - Wind speed bin size (m/s). - - List of wind direction bin center values (deg). - - List of wind speed bin center values (m/s). - - DataFrame containing at least the following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of - the wind conditions in the other columns. - """ - ( - self.num_wd, - self.num_ws, - self.wd_step, - self.ws_step, - self.wd, - self.ws, - self.df, - ) = pickle.load(open(filename, "rb")) - - return self.df - - def resample_wind_speed(self, df, ws=np.arange(0, 26, 1.0)): - """ - This method resamples the wind speed bins using the specified wind - speed bin center values. The frequency values are adjusted accordingly. - - Args: - df (pandas.DataFrame): Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - - ws (np.array, optional): List of new wind speed center bins (m/s). - Defaults to np.arange(0, 26, 1.). - - Returns: - pandas.DataFrame: Wind rose DataFrame with the resampled wind speed - bins and frequencies containing at least the following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - New wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - new wind conditions in the other columns. - """ - # Make a copy of incoming dataframe - df = df.copy(deep=True) - - # Get the wind step - ws_step = ws[1] - ws[0] - - # Ws - ws_edges = ws - ws_step / 2.0 - ws_edges = np.append(ws_edges, np.array(ws[-1] + ws_step / 2.0)) - - # Cut wind speed onto bins - df["ws"] = pd.cut(df.ws, ws_edges, labels=ws) - - # Regroup - df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() - - # Fill nans - df = df.fillna(0) - - # Reset the index - df = df.reset_index() - - # Set to float - for c in [c for c in df.columns if c != "freq_val"]: - df[c] = df[c].astype(float) - df[c] = df[c].astype(float) - - return df - - def internal_resample_wind_speed(self, ws=np.arange(0, 26, 1.0)): - """ - Internal method for resampling wind speed into desired bins. The - frequency values are adjusted accordingly. Modifies data within - WindRose object without explicit return. - - TODO: make a private method - - Args: - ws (np.array, optional): Vector of wind speed bin centers for - the wind rose (m/s). Defaults to np.arange(0, 26, 1.). - """ - # Update ws and wd binning - self.ws = ws - self.num_ws = len(ws) - self.ws_step = ws[1] - ws[0] - - # Update internal data frame - self.df = self.resample_wind_speed(self.df, ws) - - def resample_wind_direction(self, df, wd=np.arange(0, 360, 5.0)): - """ - This method resamples the wind direction bins using the specified wind - direction bin center values. The frequency values are adjusted - accordingly. - - Args: - df (pandas.DataFrame): Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - - wd (np.array, optional): List of new wind direction center bins - (deg). Defaults to np.arange(0, 360, 5.). - - Returns: - pandas.DataFrame: Wind rose DataFrame with the resampled wind - direction bins and frequencies containing at least the following - columns: - - - **wd** (*float*) - New wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - new wind conditions in the other columns. - """ - # Make a copy of incoming dataframe - df = df.copy(deep=True) - - # Get the wind step - wd_step = wd[1] - wd[0] - - # Get bin edges - wd_edges = wd - wd_step / 2.0 - wd_edges = np.append(wd_edges, np.array(wd[-1] + wd_step / 2.0)) - - # Get the overhangs - negative_overhang = wd_edges[0] - positive_overhang = wd_edges[-1] - 360.0 - - # Need potentially to wrap high angle direction to negative for correct - # binning - df["wd"] = geo.wrap_360(df.wd) - if negative_overhang < 0: - print("Correcting negative Overhang:%.1f" % negative_overhang) - df["wd"] = np.where( - df.wd.values >= 360.0 + negative_overhang, - df.wd.values - 360.0, - df.wd.values, - ) - - # Check on other side - if positive_overhang > 0: - print("Correcting positive Overhang:%.1f" % positive_overhang) - df["wd"] = np.where( - df.wd.values <= positive_overhang, df.wd.values + 360.0, df.wd.values - ) - - # Cut into bins - df["wd"] = pd.cut(df.wd, wd_edges, labels=wd) - - # Regroup - df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() - - # Fill nans - df = df.fillna(0) - - # Reset the index - df = df.reset_index() - - # Set to float Re-wrap - for c in [c for c in df.columns if c != "freq_val"]: - df[c] = df[c].astype(float) - df[c] = df[c].astype(float) - df["wd"] = geo.wrap_360(df.wd) - - return df - - def internal_resample_wind_direction(self, wd=np.arange(0, 360, 5.0)): - """ - Internal method for resampling wind direction into desired bins. The - frequency values are adjusted accordingly. Modifies data within - WindRose object without explicit return. - - TODO: make a private method - - Args: - wd (np.array, optional): Vector of wind direction bin centers for - the wind rose (deg). Defaults to np.arange(0, 360, 5.). - """ - # Update ws and wd binning - self.wd = wd - self.num_wd = len(wd) - self.wd_step = wd[1] - wd[0] - - # Update internal data frame - self.df = self.resample_wind_direction(self.df, wd) - - def resample_column(self, df, col, bins): - """ - This method resamples the specified wind parameter column using the - specified bin center values. The frequency values are adjusted - accordingly. - - Args: - df (pandas.DataFrame): Wind rose DataFrame containing at least the - following columns as well as *col*: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - - col (str): The name of the column to resample. - bins (np.array): List of new bin center values for the specified - column. - - Returns: - pandas.DataFrame: Wind rose DataFrame with the resampled wind - parameter bins and frequencies containing at least the following - columns as well as *col*: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - new wind conditions in the other columns. - """ - # Make a copy of incoming dataframe - df = df.copy(deep=True) - - # Cut into bins, make first and last bins extend to -/+ infinity - var_edges = np.append(0.5 * (bins[1:] + bins[:-1]), np.inf) - var_edges = np.append(-np.inf, var_edges) - df[col] = pd.cut(df[col], var_edges, labels=bins) - - # Regroup - df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() - - # Fill nans - df = df.fillna(0) - - # Reset the index - df = df.reset_index() - - # Set to float - for c in [c for c in df.columns if c != "freq_val"]: - df[c] = df[c].astype(float) - - return df - - def internal_resample_column(self, col, bins): - """ - Internal method for resampling column into desired bins. The frequency - values are adjusted accordingly. Modifies data within WindRose object - without explicit return. - - TODO: make a private method - - Args: - col (str): Name of column to resample. - bins (np.array): Vector of bins for the WindRose column. - """ - # Update internal data frame - self.df = self.resample_column(self.df, col, bins) - - def resample_average_ws_by_wd(self, df): - """ - This method calculates the mean wind speed for each wind direction bin - and resamples the wind rose, resulting in a single mean wind speed per - wind direction bin. The frequency values are adjusted accordingly. - - Args: - df (pandas.DataFrame): Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - - Returns: - pandas.DataFrame: Wind rose DataFrame with the resampled wind speed - bins and frequencies containing at least the following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - The average wind speed for each wind - direction bin (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - new wind conditions in the other columns. - """ - # Make a copy of incoming dataframe - df = df.copy(deep=True) - - ws_avg = [] - - for val in df.wd.unique(): - ws_avg.append( - np.array( - df.loc[df["wd"] == val]["ws"] * df.loc[df["wd"] == val]["freq_val"] - ).sum() - / df.loc[df["wd"] == val]["freq_val"].sum() - ) - - # Regroup - df = df.groupby("wd").sum() - - df["ws"] = ws_avg - - # Reset the index - df = df.reset_index() - - # Set to float - df["ws"] = df.ws.astype(float) - df["wd"] = df.wd.astype(float) - - return df - - def internal_resample_average_ws_by_wd(self, wd=np.arange(0, 360, 5.0)): - """ - This internal method calculates the mean wind speed for each specified - wind direction bin and resamples the wind rose, resulting in a single - mean wind speed per wind direction bin. The frequency values are - adjusted accordingly. - - TODO: make an internal method - - Args: - wd (np.arange, optional): Wind direction bin centers (deg). - Defaults to np.arange(0, 360, 5.). - - Returns: - pandas.DataFrame: Wind rose DataFrame with the resampled wind speed - bins and frequencies containing at least the following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - The average wind speed for each wind - direction bin (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - new wind conditions in the other columns. - """ - # Update ws and wd binning - self.wd = wd - self.num_wd = len(wd) - self.wd_step = wd[1] - wd[0] - - # Update internal data frame - self.df = self.resample_average_ws_by_wd(self.df) - - def interpolate( - self, - wind_directions: np.ndarray, - wind_speeds: np.ndarray, - mirror_0_to_360=True, - fill_value=0.0, - method="linear" - ): - """ - This method returns a linear interpolant that will return the occurrence - frequency for any given wind direction and wind speed combination(s). - This can be particularly useful when evaluating the wind rose at a - higher frequency than the input data is provided. - - Args: - wind_directions (np.ndarray): One or multi-dimensional array containing - the wind direction values at which the wind rose frequency of occurrence - should be evaluated. - wind_speeds (np.ndarray): One or multi-dimensional array containing - the wind speed values at which the wind rose frequency of occurrence - should be evaluated. - mirror_0_to_360 (bool, optional): This function copies the wind rose - frequency values from 0 deg to 360 deg. This can be useful when, for example, - the wind rose is only calculated until 357 deg but then interpolant is - requesting values at 359 deg. Defaults to True. - fill_value (float, optional): Fill value for the interpolant when - interpolating values outside of the data region. Defaults to 0.0. - method (str, optional): The interpolation method. Options are 'linear' and - 'nearest'. Recommended usage is 'linear'. Defaults to 'linear'. - - Returns: - scipy.interpolate.LinearNDInterpolant: Linear interpolant for the - wind rose currently available in the class (self.df). - - Example: - wr = wind_rose.WindRose() - wr.make_wind_rose_from_user_data(...) - freq_floris = wr.interpolate(floris_wind_direction_grid, floris_wind_speed_grid) - """ - if method == "linear": - interpolator = LinearNDInterpolator - elif method == "nearest": - interpolator = NearestNDInterpolator - else: - UserWarning("Unknown interpolation method: '{:s}'".format(method)) - - # Load windrose information from self - df = self.df.copy() - - if mirror_0_to_360: - # Copy values from 0 deg over to 360 deg - df_copy = df[df["wd"] == 0.0].copy() - df_copy["wd"] = 360.0 - df = pd.concat([df, df_copy], axis=0) - - interp = interpolator( - points=df[["wd", "ws"]], - values=df["freq_val"], - fill_value=fill_value - ) - return interp(wind_directions, wind_speeds) - - def weibull(self, x, k=2.5, lam=8.0): - """ - This method returns a Weibull distribution corresponding to the input - data array (typically wind speed) using the specified Weibull - parameters. - - Args: - x (np.array): List of input data (typically binned wind speed - observations). - k (float, optional): Weibull shape parameter. Defaults to 2.5. - lam (float, optional): Weibull scale parameter. Defaults to 8.0. - - Returns: - np.array: Weibull distribution probabilities corresponding to - values in the input array. - """ - return (k / lam) * (x / lam) ** (k - 1) * np.exp(-((x / lam) ** k)) - - def make_wind_rose_from_weibull( - self, wd=np.arange(0, 360, 5.0), ws=np.arange(0, 26, 1.0) - ): - """ - Populate WindRose object with an example wind rose with wind speed - frequencies given by a Weibull distribution. The wind direction - frequencies are initialized according to an example distribution. - - Args: - wd (np.array, optional): Wind direciton bin centers (deg). Defaults - to np.arange(0, 360, 5.). - ws (np.array, optional): Wind speed bin centers (m/s). Defaults to - np.arange(0, 26, 1.). - - Returns: - pandas.DataFrame: Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - """ - # Use an assumed wind-direction for dir frequency - wind_dir = [ - 0, - 22.5, - 45, - 67.5, - 90, - 112.5, - 135, - 157.5, - 180, - 202.5, - 225, - 247.5, - 270, - 292.5, - 315, - 337.5, - ] - freq_dir = [ - 0.064, - 0.04, - 0.038, - 0.036, - 0.045, - 0.05, - 0.07, - 0.08, - 0.11, - 0.08, - 0.05, - 0.036, - 0.048, - 0.058, - 0.095, - 0.10, - ] - - freq_wd = np.interp(wd, wind_dir, freq_dir) - freq_ws = self.weibull(ws) - - freq_tot = np.zeros(len(wd) * len(ws)) - wd_tot = np.zeros(len(wd) * len(ws)) - ws_tot = np.zeros(len(wd) * len(ws)) - - count = 0 - for i in range(len(wd)): - for j in range(len(ws)): - wd_tot[count] = wd[i] - ws_tot[count] = ws[j] - - freq_tot[count] = freq_wd[i] * freq_ws[j] - count = count + 1 - - # renormalize - freq_tot = freq_tot / np.sum(freq_tot) - - # Load the wind toolkit data into a dataframe - df = pd.DataFrame() - - # Start by simply round and wrapping the wind direction and wind speed - # columns - df["wd"] = wd_tot - df["ws"] = ws_tot - - # Now group up - df["freq_val"] = freq_tot - - # Save the df at this point - self.df = df - # TODO is there a reason self.df is updated AND returned? - return self.df - - def make_wind_rose_from_user_data( - self, wd_raw, ws_raw, *args, wd=np.arange(0, 360, 5.0), ws=np.arange(0, 26, 1.0) - ): - """ - This method populates the WindRose object given user-specified - observations of wind direction, wind speed, and additional optional - variables. The wind parameters are binned and the frequencies of - occurance of each binned wind condition combination are calculated. - - Args: - wd_raw (array-like): An array-like list of all wind direction - observations used to calculate the normalized frequencies (deg). - ws_raw (array-like): An array-like list of all wind speed - observations used to calculate the normalized frequencies (m/s). - *args: Variable length argument list consisting of a sequence of - the following alternating arguments: - - - string - Name of additional wind parameters to include in - wind rose. - - array-like - Values of the additional wind parameters used - to calculate the frequencies of occurance - - np.array - Bin center values for binning the additional - wind parameters. - - wd (np.array, optional): Wind direction bin centers (deg). Defaults - to np.arange(0, 360, 5.). - ws (np.array, optional): Wind speed bin limits (m/s). Defaults to - np.arange(0, 26, 1.). - - Returns: - pandas.DataFrame: Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - """ - df = pd.DataFrame() - - # convert inputs to np.array - wd_raw = np.array(wd_raw) - ws_raw = np.array(ws_raw) - - # Start by simply round and wrapping the wind direction and wind speed - # columns - df["wd"] = geo.wrap_360(wd_raw.round()) - df["ws"] = ws_raw.round() - - # Loop through *args and assign new dataframe columns after cutting - # into possibly irregularly-spaced bins - for in_var in range(0, len(args), 3): - df[args[in_var]] = np.array(args[in_var + 1]) - - # Cut into bins, make first and last bins extend to -/+ infinity - var_edges = np.append( - 0.5 * (args[in_var + 2][1:] + args[in_var + 2][:-1]), np.inf - ) - var_edges = np.append(-np.inf, var_edges) - df[args[in_var]] = pd.cut( - df[args[in_var]], var_edges, labels=args[in_var + 2] - ) - - # Now group up - df["freq_val"] = 1.0 - df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() - df["freq_val"] = df.freq_val.astype(float) / df.freq_val.sum() - df = df.reset_index() - - # Save the df at this point - self.df = df - - # Resample onto the provided wind speed and wind direction binnings - self.internal_resample_wind_speed(ws=ws) - self.internal_resample_wind_direction(wd=wd) - - return self.df - - def read_wind_rose_csv( - self, - filename - ): - - #Read in the csv - self.df = pd.read_csv(filename) - - # Renormalize the frequency column - self.df["freq_val"] = self.df["freq_val"] / self.df["freq_val"].sum() - - # Call the resample function in order to set all the internal variables - self.internal_resample_wind_speed(ws=self.df.ws.unique()) - self.internal_resample_wind_direction(wd=self.df.wd.unique()) - - - def make_wind_rose_from_user_dist( - self, - wd_raw, - ws_raw, - freq_val, - *args, - wd=np.arange(0, 360, 5.0), - ws=np.arange(0, 26, 1.0), - ): - """ - This method populates the WindRose object given user-specified - combinations of wind direction, wind speed, additional optional - variables, and the corresponding frequencies of occurance. The wind - parameters are binned using the specified wind parameter bin center - values and the corresponding frequencies of occrance are calculated. - - Args: - wd_raw (array-like): An array-like list of wind directions - corresponding to the specified frequencies of occurance (deg). - wd_raw (array-like): An array-like list of wind speeds - corresponding to the specified frequencies of occurance (m/s). - freq_val (array-like): An array-like list of normalized frequencies - corresponding to the provided wind parameter combinations. - *args: Variable length argument list consisting of a sequence of - the following alternating arguments: - - - string - Name of additional wind parameters to include in - wind rose. - - array-like - Values of the additional wind parameters - corresponding to the specified frequencies of occurance. - - np.array - Bin center values for binning the additional - wind parameters. - - wd (np.array, optional): Wind direction bin centers (deg). Defaults - to np.arange(0, 360, 5.). - ws (np.array, optional): Wind speed bin centers (m/s). Defaults to - np.arange(0, 26, 1.). - - Returns: - pandas.DataFrame: Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - """ - df = pd.DataFrame() - - # convert inputs to np.array - wd_raw = np.array(wd_raw) - ws_raw = np.array(ws_raw) - - # Start by simply wrapping the wind direction column - df["wd"] = geo.wrap_360(wd_raw) - df["ws"] = ws_raw - - # Loop through *args and assign new dataframe columns - for in_var in range(0, len(args), 3): - df[args[in_var]] = np.array(args[in_var + 1]) - - # Assign frequency column - df["freq_val"] = np.array(freq_val) - df["freq_val"] = df["freq_val"] / df["freq_val"].sum() - - # Save the df at this point - self.df = df - - # Resample onto the provided wind variable binnings - self.internal_resample_wind_speed(ws=ws) - self.internal_resample_wind_direction(wd=wd) - - # Loop through *args and resample using provided binnings - for in_var in range(0, len(args), 3): - self.internal_resample_column(args[in_var], args[in_var + 2]) - - return self.df - - def parse_wind_toolkit_folder( - self, - folder_name, - wd=np.arange(0, 360, 5.0), - ws=np.arange(0, 26, 1.0), - limit_month=None, - ): - """ - This method populates the WindRose object given raw wind direction and - wind speed data saved in csv files downloaded from the WIND Toolkit - application (see https://www.nrel.gov/grid/wind-toolkit.html for more - information). The wind parameters are binned using the specified wind - parameter bin center values and the corresponding frequencies of - occurance are calculated. - - Args: - folder_name (str): Path to the folder containing the WIND Toolkit - data files. - wd (np.array, optional): Wind direction bin centers (deg). Defaults - to np.arange(0, 360, 5.). - ws (np.array, optional): Wind speed bin centers (m/s). Defaults to - np.arange(0, 26, 1.). - limit_month (list, optional): List of ints of month(s) (e.g., 1, 2 - 3...) to consider when calculating the wind condition - frequencies. If none are specified, all months will be used. - Defaults to None. - - Returns: - pandas.DataFrame: Wind rose DataFrame containing the following - columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - """ - # Load the wind toolkit data into a dataframe - df = self.load_wind_toolkit_folder(folder_name, limit_month=limit_month) - - # Start by simply round and wrapping the wind direction and wind speed - # columns - df["wd"] = geo.wrap_360(df.wd.round()) - df["ws"] = geo.wrap_360(df.ws.round()) - - # Now group up - df["freq_val"] = 1.0 - df = df.groupby(["ws", "wd"]).sum() - df["freq_val"] = df.freq_val.astype(float) / df.freq_val.sum() - df = df.reset_index() - - # Save the df at this point - self.df = df - - # Resample onto the provided wind speed and wind direction binnings - self.internal_resample_wind_speed(ws=ws) - self.internal_resample_wind_direction(wd=wd) - - return self.df - - def load_wind_toolkit_folder(self, folder_name, limit_month=None): - """ - This method imports raw wind direction and wind speed data saved in csv - files in the specified folder downloaded from the WIND Toolkit - application (see https://www.nrel.gov/grid/wind-toolkit.html for more - information). - - TODO: make private method? - - Args: - folder_name (str): Path to the folder containing the WIND Toolkit - csv data files. - limit_month (list, optional): List of ints of month(s) (e.g., 1, 2, - 3...) to consider when calculating the wind condition - frequencies. If none are specified, all months will be used. - Defaults to None. - - Returns: - pandas.DataFrame: DataFrame containing the following columns: - - - **wd** (*float*) - Raw wind direction data (deg). - - **ws** (*float*) - Raw wind speed data (m/s). - """ - file_list = os.listdir(folder_name) - file_list = [os.path.join(folder_name, f) for f in file_list if ".csv" in f] - - df = pd.DataFrame() - for f_idx, f in enumerate(file_list): - print("%d of %d: %s" % (f_idx, len(file_list), f)) - df_temp = self.load_wind_toolkit_file(f, limit_month=limit_month) - df = df.append(df_temp) - - return df - - def load_wind_toolkit_file(self, filename, limit_month=None): - """ - This method imports raw wind direction and wind speed data saved in the - specified csv file downloaded from the WIND Toolkit application (see - https://www.nrel.gov/grid/wind-toolkit.html for more information). - - TODO: make private method? - - Args: - filename (str): Path to the WIND Toolkit csv file. - limit_month (list, optional): List of ints of month(s) (e.g., 1, 2, - 3...) to consider when calculating the wind condition - frequencies. If none are specified, all months will be used. - Defaults to None. - - Returns: - pandas.DataFrame: DataFrame containing the following columns with - data from the WIND Toolkit file: - - - **wd** (*float*) - Raw wind direction data (deg). - - **ws** (*float*) - Raw wind speed data (m/s). - """ - df = pd.read_csv(filename, header=3, sep=",") - - # If asked to limit to particular months - if limit_month is not None: - df = df[df.Month.isin(limit_month)] - - # Save just what I want - speed_column = [c for c in df.columns if "speed" in c][0] - direction_column = [c for c in df.columns if "direction" in c][0] - df = df.rename(index=str, columns={speed_column: "ws", direction_column: "wd"})[ - ["wd", "ws"] - ] - - return df - - def import_from_wind_toolkit_hsds( - self, - lat, - lon, - ht=100, - wd=np.arange(0, 360, 5.0), - ws=np.arange(0, 26, 1.0), - include_ti=False, - limit_month=None, - limit_hour=None, - st_date=None, - en_date=None, - ): - """ - This method populates the WindRose object using wind data from the WIND - Toolkit dataset (https://www.nrel.gov/grid/wind-toolkit.html) for the - specified lat/long coordinate in the continental US. The wind data - are obtained from the WIND Toolkit dataset using the HSDS service (see - https://github.com/NREL/hsds-examples). The wind data returned is - obtained from the nearest 2km x 2km grid point to the input - coordinate and is limited to the years 2007-2013. The wind parameters - are binned using the specified wind parameter bin center values and the - corresponding frequencies of occrance are calculated. - - Requires h5pyd package, which can be installed using: - pip install --user git+http://github.com/HDFGroup/h5pyd.git - - Then, make a configuration file at ~/.hscfg containing: - - hs_endpoint = https://developer.nrel.gov/api/hsds - - hs_username = None - - hs_password = None - - hs_api_key = 3K3JQbjZmWctY0xmIfSYvYgtIcM3CN0cb1Y2w9bf - - The example API key above is for demonstation and is - rate-limited per IP. To get your own API key, visit - https://developer.nrel.gov/signup/. - - More information can be found at: https://github.com/NREL/hsds-examples. - - Args: - lat (float): Latitude in degrees. - lon (float): Longitude in degrees. - ht (int, optional): The height above ground where wind - information is obtained (m). Defaults to 100. - wd (np.array, optional): Wind direction bin centers (deg). Defaults - to np.arange(0, 360, 5.). - ws (np.array, optional): Wind speed bin centers (m/s). Defaults to - np.arange(0, 26, 1.). - include_ti (bool, optional): Determines whether turbulence - intensity is included as an additional parameter. If True, TI - is added as an additional wind rose variable, estimated based - on the Obukhov length from WIND Toolkit. Defaults to False. - limit_month (list, optional): List of ints of month(s) (e.g., 1, 2, - 3...) to consider when calculating the wind condition - frequencies. If none are specified, all months will be used. - Defaults to None. - limit_hour (list, optional): List of ints of hour(s) (e.g., 0, 1, - ... 23) to consider when calculating the wind condition - frequencies. If none are specified, all hours will be used. - Defaults to None. - st_date (str, optional): The start date to consider when creating - the wind rose, formatted as 'MM-DD-YYYY'. If not specified data - beginning in 2007 will be used. Defaults to None. - en_date (str, optional): The end date to consider when creating - the wind rose, formatted as 'MM-DD-YYYY'. If not specified data - through 2013 will be used. Defaults to None. - - Returns: - pandas.DataFrame: Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - """ - # Check inputs - - # Array of hub height data avaliable on Toolkit - h_range = [10, 40, 60, 80, 100, 120, 140, 160, 200] - - if st_date is not None: - if dateutil.parser.parse(st_date) > dateutil.parser.parse( - "12-13-2013 23:00" - ): - print( - "Error, invalid date range. Valid range: 01-01-2007 - " - + "12/31/2013" - ) - return None - - if en_date is not None: - if dateutil.parser.parse(en_date) < dateutil.parser.parse( - "01-01-2007 00:00" - ): - print( - "Error, invalid date range. Valid range: 01-01-2007 - " - + "12/31/2013" - ) - return None - - if h_range[0] > ht: - print( - "Error, height is not in the range of avaliable " - + "WindToolKit data. Minimum height = 10m" - ) - return None - - if h_range[-1] < ht: - print( - "Error, height is not in the range of avaliable " - + "WindToolKit data. Maxiumum height = 200m" - ) - return None - - # Load wind speeds and directions from WimdToolkit - - # Case for turbine height (ht) matching discrete avaliable height - # (h_range) - if ht in h_range: - - d = self.load_wind_toolkit_hsds( - lat, - lon, - ht, - include_ti=include_ti, - limit_month=limit_month, - limit_hour=limit_hour, - st_date=st_date, - en_date=en_date, - ) - - ws_new = d["ws"] - wd_new = d["wd"] - if include_ti: - ti_new = d["ti"] - - # Case for ht not matching discete height - else: - h_range_up = next(x[0] for x in enumerate(h_range) if x[1] > ht) - h_range_low = h_range_up - 1 - h_up = h_range[h_range_up] - h_low = h_range[h_range_low] - - # Load data for boundary cases of ht - d_low = self.load_wind_toolkit_hsds( - lat, - lon, - h_low, - include_ti=include_ti, - limit_month=limit_month, - limit_hour=limit_hour, - st_date=st_date, - en_date=en_date, - ) - - d_up = self.load_wind_toolkit_hsds( - lat, - lon, - h_up, - include_ti=include_ti, - limit_month=limit_month, - limit_hour=limit_hour, - st_date=st_date, - en_date=en_date, - ) - - # Wind Speed interpolation - ws_low = d_low["ws"] - ws_high = d_up["ws"] - - ws_new = np.array(ws_low) * ( - 1 - ((ht - h_low) / (h_up - h_low)) - ) + np.array(ws_high) * ((ht - h_low) / (h_up - h_low)) - - # Wind Direction interpolation using Circular Mean method - wd_low = d_low["wd"] - wd_high = d_up["wd"] - - sin0 = np.sin(np.array(wd_low) * (np.pi / 180)) - cos0 = np.cos(np.array(wd_low) * (np.pi / 180)) - sin1 = np.sin(np.array(wd_high) * (np.pi / 180)) - cos1 = np.cos(np.array(wd_high) * (np.pi / 180)) - - sin_wd = sin0 * (1 - ((ht - h_low) / (h_up - h_low))) + sin1 * ( - (ht - h_low) / (h_up - h_low) - ) - cos_wd = cos0 * (1 - ((ht - h_low) / (h_up - h_low))) + cos1 * ( - (ht - h_low) / (h_up - h_low) - ) - - # Interpolated wind direction - wd_new = 180 / np.pi * np.arctan2(sin_wd, cos_wd) - - # TI is independent of height - if include_ti: - ti_new = d_up["ti"] - - # Create a dataframe named df - if include_ti: - df = pd.DataFrame({"ws": ws_new, "wd": wd_new, "ti": ti_new}) - else: - df = pd.DataFrame({"ws": ws_new, "wd": wd_new}) - - # Start by simply round and wrapping the wind direction and wind speed - # columns - df["wd"] = geo.wrap_360(df.wd.round()) - df["ws"] = df.ws.round() - - # Now group up - df["freq_val"] = 1.0 - df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() - df["freq_val"] = df.freq_val.astype(float) / df.freq_val.sum() - df = df.reset_index() - - # Save the df at this point - self.df = df - - # Resample onto the provided wind speed and wind direction binnings - self.internal_resample_wind_speed(ws=ws) - self.internal_resample_wind_direction(wd=wd) - - return self.df - - def load_wind_toolkit_hsds( - self, - lat, - lon, - ht=100, - include_ti=False, - limit_month=None, - limit_hour=None, - st_date=None, - en_date=None, - ): - """ - This method returns a pandas DataFrame containing hourly wind speed, - wind direction, and optionally estimated turbulence intensity data - using wind data from the WIND Toolkit dataset - (https://www.nrel.gov/grid/wind-toolkit.html) for the specified - lat/long coordinate in the continental US. The wind data are obtained - from the WIND Toolkit dataset using the HSDS service - (see https://github.com/NREL/hsds-examples). The wind data returned is - obtained from the nearest 2km x 2km grid point to the input coordinate - and is limited to the years 2007-2013. - - TODO: make private method? - - Args: - lat (float): Latitude in degrees. - lon (float): Longitude in degrees - ht (int, optional): The height above ground where wind - information is obtained (m). Defaults to 100. - include_ti (bool, optional): Determines whether turbulence - intensity is included as an additional parameter. If True, TI - is added as an additional wind rose variable, estimated based - on the Obukhov length from WIND Toolkit. Defaults to False. - limit_month (list, optional): List of ints of month(s) (e.g., 1, 2, - 3...) to consider when calculating the wind condition - frequencies. If none are specified, all months will be used. - Defaults to None. - limit_hour (list, optional): List of ints of hour(s) (e.g., 0, 1, - ... 23) to consider when calculating the wind condition - frequencies. If none are specified, all hours will be used. - Defaults to None. - st_date (str, optional): The start date to consider, formatted as - 'MM-DD-YYYY'. If not specified data beginning in 2007 will be - used. Defaults to None. - en_date (str, optional): The end date to consider, formatted as - 'MM-DD-YYYY'. If not specified data through 2013 will be used. - Defaults to None. - - Returns: - pandas.DataFrame: DataFrame containing the following columns(abd - optionally turbulence intensity) with hourly data from WIND Toolkit: - - - **wd** (*float*) - Raw wind direction data (deg). - - **ws** (*float*) - Raw wind speed data (m/s). - """ - import h5pyd - - # Open the wind data "file" - # server endpoint, username, password is found via a config file - f = h5pyd.File("/nrel/wtk-us.h5", "r") - - # assign wind direction, wind speed, optional ti, and time datasets for - # the desired height - wd_dset = f["winddirection_" + str(ht) + "m"] - ws_dset = f["windspeed_" + str(ht) + "m"] - if include_ti: - obkv_dset = f["inversemoninobukhovlength_2m"] - dt = f["datetime"] - dt = pd.DataFrame({"datetime": dt[:]}, index=range(0, dt.shape[0])) - dt["datetime"] = dt["datetime"].apply(dateutil.parser.parse) - - # find dataset indices from lat/long - Location_idx = self.indices_for_coord(f, lat, lon) - - # check if in bounds - if ( - (Location_idx[0] < 0) - | (Location_idx[0] >= wd_dset.shape[1]) - | (Location_idx[1] < 0) - | (Location_idx[1] >= wd_dset.shape[2]) - ): - print( - "Error, coordinates out of bounds. WIND Toolkit database " - + "covers the continental United States." - ) - return None - - # create dataframe with wind direction and wind speed - df = pd.DataFrame() - df["wd"] = wd_dset[:, Location_idx[0], Location_idx[1]] - df["ws"] = ws_dset[:, Location_idx[0], Location_idx[1]] - if include_ti: - L = self.obkv_dset_to_L(obkv_dset, Location_idx) - ti = self.ti_calculator_IU2(L) - df["ti"] = ti - df["datetime"] = dt["datetime"] - - # limit dates if start and end dates are provided - if st_date is not None: - df = df[df.datetime >= st_date] - - if en_date is not None: - df = df[df.datetime < en_date] - - # limit to certain months if specified - if limit_month is not None: - df["month"] = df["datetime"].map(lambda x: x.month) - df = df[df.month.isin(limit_month)] - if limit_hour is not None: - df["hour"] = df["datetime"].map(lambda x: x.hour) - df = df[df.hour.isin(limit_hour)] - if include_ti: - df = df[["wd", "ws", "ti"]] - else: - df = df[["wd", "ws"]] - - return df - - def obkv_dset_to_L(self, obkv_dset, Location_idx): - """ - This function returns an array containing hourly Obukhov lengths from - the WIND Toolkit dataset for the specified Lat/Lon coordinate indices. - - Args: - obkv_dset (np.ndarray): Dataset for Obukhov lengths from WIND - Toolkit. - Location_idx (tuple): A tuple containing the Lat/Lon coordinate - indices of interest in the Obukhov length dataset. - - Returns: - np.array: An array containing Obukhov lengths for each time index - in the Wind Toolkit dataset (m). - """ - linv = obkv_dset[:, Location_idx[0], Location_idx[1]] - # avoid divide by zero - linv[linv == 0.0] = 0.0003 - L = 1 / linv - return L - - def ti_calculator_IU2(self, L): - """ - This function estimates the turbulence intensity corresponding to each - Obukhov length value in the input list using the relationship between - Obukhov length bins and TI given in the I_U2SODAR column in Table 2 of - :cite:`wr-wharton2010assessing`. - - Args: - L (iterable): A list of Obukhov Length values (m). - - Returns: - list: A list of turbulence intensity values expressed as fractions. - """ - ti_set = [] - for i in L: - # Strongly Stable - if 0 < i < 100: - TI = 0.04 # paper says < 8%, so using 4% - # Stable - elif 100 < i < 600: - TI = 0.09 - # Neutral - elif abs(i) > 600: - TI = 0.115 - # Convective - elif -600 < i < -50: - TI = 0.165 - # Strongly Convective - elif -50 < i < 0: - # no upper bound given, so using the lowest - # value from the paper for this stability bin - TI = 0.2 - ti_set.append(TI) - return ti_set - - def indices_for_coord(self, f, lat_index, lon_index): - """ - This method finds the nearest x/y indices of the WIND Toolkit dataset - for a given lat/lon coordinate in the continental US. Rather than - fetching the entire coordinates database, which is 500+ MB, this uses - the Proj4 library to find a nearby point and then converts to x/y - indices. - - **Note**: This method is obtained directly from: - https://github.com/NREL/hsds-examples/blob/master/notebooks/01_WTK_introduction.ipynb, - where it is called "indicesForCoord." - - Args: - f (h5pyd.File): A HDF5 "file" used to access the WIND Toolkit data. - lat_index (float): Latitude coordinate for which dataset indices - are to be found (degrees). - lon_index (float): Longitude coordinate for which dataset indices - are to be found (degrees). - - Returns: - tuple: A tuple containing the Lat/Lon coordinate indices of - interest in the WIND Toolkit dataset. - """ - dset_coords = f["coordinates"] - projstring = """+proj=lcc +lat_1=30 +lat_2=60 - +lat_0=38.47240422490422 +lon_0=-96.0 - +x_0=0 +y_0=0 +ellps=sphere - +units=m +no_defs """ - projectLcc = Proj(projstring) - origin_ll = reversed(dset_coords[0][0]) # Grab origin directly from database - origin = projectLcc(*origin_ll) - - coords = (lon_index, lat_index) - coords = projectLcc(*coords) - delta = np.subtract(coords, origin) - ij = [int(round(x / 2000)) for x in delta] - return tuple(reversed(ij)) - - def plot_wind_speed_all(self, ax=None, label=None): - """ - This method plots the wind speed frequency distribution of the WindRose - object averaged across all wind directions. If no axis is provided, a - new one is created. - - Args: - ax (:py:class:`matplotlib.pyplot.axes`, optional): Figure axes on - which data should be plotted. Defaults to None. - """ - if ax is None: - _, ax = plt.subplots() - - df_plot = self.df.groupby("ws").sum() - ax.plot(self.ws, df_plot.freq_val, label=label) - - def plot_wind_speed_by_direction(self, dirs, ax=None): - """ - This method plots the wind speed frequency distribution of the WindRose - object for each specified wind direction bin center. The wind - directions are resampled using the specified bin centers and the - frequencies of occurance of the wind conditions are modified - accordingly. If no axis is provided, a new one is created. - - Args: - dirs (np.array): A list of wind direction bin centers for which - wind speed distributions are plotted (deg). - ax (:py:class:`matplotlib.pyplot.axes`, optional): Figure axes on - which data should be plotted. Defaults to None. - """ - # Get a downsampled frame - df_plot = self.resample_wind_direction(self.df, wd=dirs) - - if ax is None: - _, ax = plt.subplots() - - for wd in dirs: - df_plot_sub = df_plot[df_plot.wd == wd] - ax.plot(df_plot_sub.ws, df_plot_sub["freq_val"], label=wd) - ax.legend() - - def plot_wind_rose( - self, - ax=None, - color_map="viridis_r", - ws_right_edges=np.array([5, 10, 15, 20, 25]), - wd_bins=np.arange(0, 360, 15.0), - legend_kwargs={}, - ): - """ - This method creates a wind rose plot showing the frequency of occurance - of the specified wind direction and wind speed bins. If no axis is - provided, a new one is created. - - **Note**: Based on code provided by Patrick Murphy from the University - of Colorado Boulder. - - Args: - ax (:py:class:`matplotlib.pyplot.axes`, optional): The figure axes - on which the wind rose is plotted. Defaults to None. - color_map (str, optional): Colormap to use. Defaults to 'viridis_r'. - ws_right_edges (np.array, optional): The upper bounds of the wind - speed bins (m/s). The first bin begins at 0. Defaults to - np.array([5, 10, 15, 20, 25]). - wd_bins (np.array, optional): The wind direction bin centers used - for plotting (deg). Defaults to np.arange(0, 360, 15.). - legend_kwargs (dict, optional): Keyword arguments to be passed to - ax.legend(). - - Returns: - :py:class:`matplotlib.pyplot.axes`: A figure axes object containing - the plotted wind rose. - """ - # Resample data onto bins - df_plot = self.resample_wind_direction(self.df, wd=wd_bins) - - # Make labels for wind speed based on edges - ws_step = ws_right_edges[1] - ws_right_edges[0] - ws_labels = ["%d-%d m/s" % (w - ws_step, w) for w in ws_right_edges] - - # Grab the wd_step - wd_step = wd_bins[1] - wd_bins[0] - - # Set up figure - if ax is None: - _, ax = plt.subplots(subplot_kw={"polar": True}) - - # Get a color array - color_array = cm.get_cmap(color_map, len(ws_right_edges)) - - for wd in wd_bins: - rects = [] - df_plot_sub = df_plot[df_plot.wd == wd] - for ws_idx, ws in enumerate(ws_right_edges[::-1]): - plot_val = df_plot_sub[ - df_plot_sub.ws <= ws - ].freq_val.sum() # Get the sum of frequency up to this wind speed - rects.append( - ax.bar( - np.radians(wd), - plot_val, - width=0.9 * np.radians(wd_step), - color=color_array(ws_idx), - edgecolor="k", - ) - ) - # break - - # Configure the plot - ax.legend(reversed(rects), ws_labels, **legend_kwargs) - ax.set_theta_direction(-1) - ax.set_theta_offset(np.pi / 2.0) - ax.set_theta_zero_location("N") - ax.set_xticks(np.arange(0, 2*np.pi, np.pi/4)) - ax.set_xticklabels(["N", "NE", "E", "SE", "S", "SW", "W", "NW"]) - - return ax - - def plot_wind_rose_ti( - self, - ax=None, - color_map="viridis_r", - ti_right_edges=np.array([0.06, 0.1, 0.14, 0.18, 0.22]), - wd_bins=np.arange(0, 360, 15.0), - ): - """ - This method creates a wind rose plot showing the frequency of occurance - of the specified wind direction and turbulence intensity bins. This - requires turbulence intensity to already be included as a parameter in - the wind rose. If no axis is provided,a new one is created. - - **Note**: Based on code provided by Patrick Murphy from the University - of Colorado Boulder. - - Args: - ax (:py:class:`matplotlib.pyplot.axes`, optional): The figure axes - on which the wind rose is plotted. Defaults to None. - color_map (str, optional): Colormap to use. Defaults to 'viridis_r'. - ti_right_edges (np.array, optional): The upper bounds of the - turbulence intensity bins. The first bin begins at 0. Defaults - to np.array([0.06, 0.1, 0.14, 0.18,0.22]). - wd_bins (np.array, optional): The wind direction bin centers used - for plotting (deg). Defaults to np.arange(0, 360, 15.). - - Returns: - :py:class:`matplotlib.pyplot.axes`: A figure axes object containing - the plotted wind rose. - """ - - # Resample data onto bins - df_plot = self.resample_wind_direction(self.df, wd=wd_bins) - - # Make labels for TI based on edges - ti_step = ti_right_edges[1] - ti_right_edges[0] - ti_labels = ["%.2f-%.2f " % (w - ti_step, w) for w in ti_right_edges] - - # Grab the wd_step - wd_step = wd_bins[1] - wd_bins[0] - - # Set up figure - if ax is None: - _, ax = plt.subplots(subplot_kw={"polar": True}) - - # Get a color array - color_array = cm.get_cmap(color_map, len(ti_right_edges)) - - for wd in wd_bins: - rects = [] - df_plot_sub = df_plot[df_plot.wd == wd] - for ti_idx, ti in enumerate(ti_right_edges[::-1]): - plot_val = df_plot_sub[ - df_plot_sub.ti <= ti - ].freq_val.sum() # Get the sum of frequency up to this wind speed - rects.append( - ax.bar( - np.radians(wd), - plot_val, - width=0.9 * np.radians(wd_step), - color=color_array(ti_idx), - edgecolor="k", - ) - ) - - # Configure the plot - ax.legend(reversed(rects), ti_labels, loc="lower right", title="TI") - ax.set_theta_direction(-1) - ax.set_theta_offset(np.pi / 2.0) - ax.set_theta_zero_location("N") - ax.set_xticks(np.arange(0, 2*np.pi, np.pi/4)) - ax.set_xticklabels(["N", "NE", "E", "SE", "S", "SW", "W", "NW"]) - - return ax - - def plot_ti_ws(self, ax=None, ws_bins=np.arange(0, 26, 1.0)): - """ - This method plots the wind speed frequency distribution of the WindRose - object for each turbulence intensity bin. The wind speeds are resampled - using the specified bin centers and the frequencies of occurance of the - wind conditions are modified accordingly. This method assumes there are - five TI bins. If no axis is provided, a new one is created. - - Args: - ax (:py:class:`matplotlib.pyplot.axes`, optional): Figure axes on - which data should be plotted. Defaults to None. - ws_bins (np.array, optional): A list of wind speed bin centers on - which the wind speeds are resampled before plotting (m/s). - Defaults to np.arange(0, 26, 1.). - - Returns: - :py:class:`matplotlib.pyplot.axes`: A figure axes object containing - the plotted wind speed distributions. - """ - - # Resample data onto bins - df_plot = self.resample_wind_speed(self.df, ws=ws_bins) - - df_plot = df_plot.groupby(["ws", "ti"]).sum() - df_plot = df_plot.reset_index() - - if ax is None: - _, ax = plt.subplots(figsize=(10, 7)) - - tis = df_plot["ti"].drop_duplicates() - margin_bottom = np.zeros(len(df_plot["ws"].drop_duplicates())) - colors = ["#1e5631", "#a4de02", "#76ba1b", "#4c9a2a", "#acdf87"] - - for num, ti in enumerate(tis): - values = list(df_plot[df_plot["ti"] == ti].loc[:, "freq_val"]) - - df_plot[df_plot["ti"] == ti].plot.bar( - x="ws", - y="freq_val", - ax=ax, - bottom=margin_bottom, - color=colors[num], - label=ti, - ) - - margin_bottom += values - - plt.title("Turbulence Intensity Frequencies as Function of Wind Speed") - plt.xlabel("Wind Speed (m/s)") - plt.ylabel("Frequency") - - return ax - - def export_for_floris_opt(self): - """ - This method returns a list of tuples of at least wind speed, wind - direction, and frequency of occurance, which can be used to help loop - through different wind conditions for Floris power calculations. - - Returns: - list: A list of tuples containing all combinations of wind - parameters and frequencies of occurance in the WindRose object's - wind rose DataFrame values. - """ - # Return a list of tuples, where each tuple is (ws,wd,freq) - return [tuple(x) for x in self.df.values] From b5152747d2187dc84420b580cb716555bc1d1660 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 11 Jan 2024 13:14:16 -0700 Subject: [PATCH 002/101] move files around to get started --- floris/tools/__init__.py | 21 +++++++++++---------- floris/tools/time_series.py | 17 +++++++++++++++++ floris/tools/wind_rose.py | 17 +++++++++++++++++ tests/wind_rose_time_series_test.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 tests/wind_rose_time_series_test.py diff --git a/floris/tools/__init__.py b/floris/tools/__init__.py index 6a2cca91b..a6dbe5b73 100644 --- a/floris/tools/__init__.py +++ b/floris/tools/__init__.py @@ -39,6 +39,7 @@ from .floris_interface import FlorisInterface from .floris_interface_legacy_reader import FlorisInterfaceLegacyV2 from .parallel_computing_interface import ParallelComputingInterface +from .time_series import TimeSeries from .uncertainty_interface import UncertaintyInterface from .visualization import ( plot_rotor_values, @@ -50,14 +51,14 @@ # from floris.tools import ( - # cut_plane, - # floris_interface, - # interface_utilities, - # layout_functions, - # optimization, - # plotting, - # power_rose, - # rews, - # visualization, - # wind_rose, +# cut_plane, +# floris_interface, +# interface_utilities, +# layout_functions, +# optimization, +# plotting, +# power_rose, +# rews, +# visualization, +# wind_rose, # ) diff --git a/floris/tools/time_series.py b/floris/tools/time_series.py index fcce3af6c..54d70c650 100644 --- a/floris/tools/time_series.py +++ b/floris/tools/time_series.py @@ -11,3 +11,20 @@ # the License. # See https://floris.readthedocs.io for documentation + + +class TimeSeries: + """ + In FLORIS v4, the TimeSeries class is used to drive FLORIS and optimization + operations in which the inflow is by a sequence of wind speed, wind directino + and turbulence intensitity values + + """ + + def __init__( + self, + ): + """ + TODO: Write this later + """ + pass diff --git a/floris/tools/wind_rose.py b/floris/tools/wind_rose.py index fcce3af6c..05201cf54 100644 --- a/floris/tools/wind_rose.py +++ b/floris/tools/wind_rose.py @@ -11,3 +11,20 @@ # the License. # See https://floris.readthedocs.io for documentation + + +class WindRose: + """ + In FLORIS v4, the WindRose class is used to drive FLORIS and optimization + operations in which the inflow is characterized by the frequency of + binned wind speed, wind direction and turbulence intensity values + + """ + + def __init__( + self, + ): + """ + TODO: Write this later + """ + pass diff --git a/tests/wind_rose_time_series_test.py b/tests/wind_rose_time_series_test.py new file mode 100644 index 000000000..454cee4db --- /dev/null +++ b/tests/wind_rose_time_series_test.py @@ -0,0 +1,28 @@ +# Copyright 2024 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 + +from floris.tools import TimeSeries, WindRose + + +# import pytest + + +def test_wind_rose_instantiation(): + wind_rose = WindRose() + wind_rose + + +def test_time_series_instantiation(): + time_series = TimeSeries + time_series From f595720957ce7ea4b99c597ee511950baa02546d Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 11 Jan 2024 13:15:19 -0700 Subject: [PATCH 003/101] ignore unused in __init__ files --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2bb5fdcf5..27ea791e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,6 +122,8 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" "floris/simulation/wake_velocity/jensen.py" = ["F841"] "floris/simulation/wake_velocity/gauss.py" = ["F841"] "floris/simulation/wake_velocity/empirical_gauss.py" = ["F841"] +# Ignore `F401` (import violations) in all `__init__.py` files, and in `path/to/file.py`. +"__init__.py" = ["F401"] # I001 unsorted-imports: ignore because the import order is meaningful to navigate # import dependencies From 2096d9c0dc26e9606ec49a115524b5c612bea5b2 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 14:13:50 -0700 Subject: [PATCH 004/101] Consolidate wind rose and time series into one module --- floris/tools/time_series.py | 30 ---- floris/tools/wind_rose.py | 30 ---- floris/tools/wind_rose_time_series.py | 231 ++++++++++++++++++++++++++ 3 files changed, 231 insertions(+), 60 deletions(-) delete mode 100644 floris/tools/time_series.py delete mode 100644 floris/tools/wind_rose.py create mode 100644 floris/tools/wind_rose_time_series.py diff --git a/floris/tools/time_series.py b/floris/tools/time_series.py deleted file mode 100644 index 54d70c650..000000000 --- a/floris/tools/time_series.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2024 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 - - -class TimeSeries: - """ - In FLORIS v4, the TimeSeries class is used to drive FLORIS and optimization - operations in which the inflow is by a sequence of wind speed, wind directino - and turbulence intensitity values - - """ - - def __init__( - self, - ): - """ - TODO: Write this later - """ - pass diff --git a/floris/tools/wind_rose.py b/floris/tools/wind_rose.py deleted file mode 100644 index 05201cf54..000000000 --- a/floris/tools/wind_rose.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2024 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 - - -class WindRose: - """ - In FLORIS v4, the WindRose class is used to drive FLORIS and optimization - operations in which the inflow is characterized by the frequency of - binned wind speed, wind direction and turbulence intensity values - - """ - - def __init__( - self, - ): - """ - TODO: Write this later - """ - pass diff --git a/floris/tools/wind_rose_time_series.py b/floris/tools/wind_rose_time_series.py new file mode 100644 index 000000000..512c8971c --- /dev/null +++ b/floris/tools/wind_rose_time_series.py @@ -0,0 +1,231 @@ +# Copyright 2024 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 numpy as np +import pandas as pd +from pandas.api.types import CategoricalDtype + + +class WindRose: + """ + In FLORIS v4, the WindRose class is used to drive FLORIS and optimization + operations in which the inflow is characterized by the frequency of + binned wind speed, wind direction and turbulence intensity values + + """ + + def __init__( + self, + wind_directions, + wind_speeds, + freq_table=None, + ti_table=None, + price_table=None, + ): + """ + TODO: Write this later + """ + + # Save the wind speeds and directions + self.wind_directions = wind_directions + self.wind_speeds = wind_speeds + + # If freq_table is not None, confirm it has correct dimension, + # otherwise initialze to uniform probability + if freq_table is not None: + if not freq_table.shape[0] == len(wind_directions): + raise ValueError("freq_table first dimension must equal len(wind_directions)") + if not freq_table.shape[1] == len(wind_speeds): + raise ValueError("freq_table second dimension must equal len(wind_speeds)") + self.freq_table = freq_table + else: + self.freq_table = np.ones((len(wind_directions), len(wind_speeds))) + + # Normalize freq table + self.freq_table = self.freq_table / np.sum(self.freq_table) + + # If TI table is not None, confirm dimension + # otherwise leave it None + if ti_table is not None: + if not ti_table.shape[0] == len(wind_directions): + raise ValueError("ti_table first dimension must equal len(wind_directions)") + if not ti_table.shape[1] == len(wind_speeds): + raise ValueError("ti_table second dimension must equal len(wind_speeds)") + self.ti_table = ti_table + + # If price_table is not None, confirm it has correct dimension, + # otherwise initialze to all ones + if price_table is not None: + if not price_table.shape[0] == len(wind_directions): + raise ValueError("price_table first dimension must equal len(wind_directions)") + if not price_table.shape[1] == len(wind_speeds): + raise ValueError("price_table second dimension must equal len(wind_speeds)") + self.price_table = price_table + else: + self.price_table = np.ones((len(wind_directions), len(wind_speeds))) + + +class TimeSeries: + """ + In FLORIS v4, the TimeSeries class is used to drive FLORIS and optimization + operations in which the inflow is by a sequence of wind speed, wind directino + and turbulence intensitity values + + """ + + def __init__( + self, + wind_directions, + wind_speeds, + turbulence_intensity=None, + prices=None, + ): + """ + TODO: Write this later + """ + + # Wind speeds and wind directions must be the same length + if len(wind_directions) != len(wind_speeds): + raise ValueError("wind_directions and wind_speeds must be the same length") + + self.wind_directions = wind_directions + self.wind_speeds = wind_speeds + self.turbulence_intensity = turbulence_intensity + self.prices = prices + + # Record findex + self.n_findex = len(self.wind_directions) + + def _wrap_wind_directions_near_360(self, wind_directions, wd_step): + """ + use wd_step to produce a wrapped version of wind_directions + where values that are between [360 - wd_step/2.0,360] get mapped + to negative numbers for binning + """ + wind_directions_wrapped = wind_directions.copy() + mask = wind_directions_wrapped >= 360 - wd_step / 2.0 + wind_directions_wrapped[mask] = wind_directions_wrapped[mask] - 360.0 + return wind_directions_wrapped + + def to_wind_rose(self, wd_step=2.0, ws_step=1.0, wd_edges=None, ws_edges=None): + """ + TODO: Write this later + """ + + # If wd_edges is defined, then use it to produce the bin centers + if wd_edges is not None: + wd_step = wd_edges[1] - wd_edges[0] + + # use wd_step to produce a wrapped version of wind_directions + wind_directions_wrapped = self._wrap_wind_directions_near_360( + self.wind_directions, wd_step + ) + + # Else, determine wd_edges from the step and data + else: + wd_edges = np.arange(0.0 - wd_step / 2.0, 360.0, wd_step) + + # use wd_step to produce a wrapped version of wind_directions + wind_directions_wrapped = self._wrap_wind_directions_near_360( + self.wind_directions, wd_step + ) + + # Only keep the range with values in it + wd_edges = wd_edges[wd_edges + wd_step >= wind_directions_wrapped.min()] + wd_edges = wd_edges[wd_edges - wd_step <= wind_directions_wrapped.max()] + + # Define the centers from the edges + wd_centers = wd_edges[:-1] + wd_step / 2.0 + + # Repeat for wind speeds + if ws_edges is not None: + ws_step = ws_edges[1] - ws_edges[0] + + else: + ws_edges = np.arange(0.0 - ws_step / 2.0, 50.0, ws_step) + + # Only keep the range with values in it + ws_edges = ws_edges[ws_edges + ws_step >= self.wind_speeds.min()] + ws_edges = ws_edges[ws_edges - ws_step <= self.wind_speeds.max()] + + # Define the centers from the edges + ws_centers = ws_edges[:-1] + ws_step / 2.0 + + # Now use pandas to get the tables need for wind rose + df = pd.DataFrame( + { + "wd": wind_directions_wrapped, + "ws": self.wind_speeds, + "freq_val": np.ones(len(wind_directions_wrapped)), + } + ) + + # If turbulence_intensity is not none, add to dataframe + if self.turbulence_intensity is not None: + df = df.assign(turbulence_intensity=self.turbulence_intensity) + + # If prices is not none, add to dataframe + if self.prices is not None: + df = df.assign(prices=self.prices) + + # Bin wind speed and wind direction and then group things up + df = ( + df.assign( + wd_bin=pd.cut( + df.wd, bins=wd_edges, labels=wd_centers, right=False, include_lowest=True + ) + ) + .assign( + ws_bin=pd.cut( + df.ws, bins=ws_edges, labels=ws_centers, right=False, include_lowest=True + ) + ) + .drop(["wd", "ws"], axis=1) + ) + + # Convert wd_bin and ws_bin to categoricals to ensure all combinations + # are considered and then group + wd_cat = CategoricalDtype(categories=wd_centers, ordered=True) + ws_cat = CategoricalDtype(categories=ws_centers, ordered=True) + + df = ( + df.assign(wd_bin=df["wd_bin"].astype(wd_cat)) + .assign(ws_bin=df["ws_bin"].astype(ws_cat)) + .groupby(["wd_bin", "ws_bin"]) + .agg([np.sum, np.mean]) + ) + # Flatten and combine levels using an underscore + df.columns = ["_".join(col) for col in df.columns] + + # Collect the frequency table and reshape + freq_table = df["freq_val_sum"].values.copy() + freq_table = freq_table / freq_table.sum() + freq_table = freq_table.reshape((len(wd_centers), len(ws_centers))) + + # If turbulence intensity is not none, compute the table + if self.turbulence_intensity is not None: + ti_table = df["turbulence_intensity_mean"].values.copy() + ti_table = ti_table.reshape((len(wd_centers), len(ws_centers))) + else: + ti_table = None + + # If prices is not none, compute the table + if self.prices is not None: + price_table = df["prices_mean"].values.copy() + price_table = price_table.reshape((len(wd_centers), len(ws_centers))) + else: + price_table = None + + # Return a WindRose + return WindRose(wd_centers, ws_centers, freq_table, ti_table, price_table) From 9f94b014b2e72cf5b09fa77763b9b3be3cf271c8 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 14:14:04 -0700 Subject: [PATCH 005/101] Update init --- floris/tools/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/floris/tools/__init__.py b/floris/tools/__init__.py index a6dbe5b73..ae7c7e2fc 100644 --- a/floris/tools/__init__.py +++ b/floris/tools/__init__.py @@ -39,7 +39,6 @@ from .floris_interface import FlorisInterface from .floris_interface_legacy_reader import FlorisInterfaceLegacyV2 from .parallel_computing_interface import ParallelComputingInterface -from .time_series import TimeSeries from .uncertainty_interface import UncertaintyInterface from .visualization import ( plot_rotor_values, @@ -47,7 +46,7 @@ visualize_cut_plane, visualize_quiver, ) -from .wind_rose import WindRose +from .wind_rose_time_series import TimeSeries, WindRose # from floris.tools import ( From a9bb92cdff2e2450e4157c628b2b5280b32e1798 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 14:14:27 -0700 Subject: [PATCH 006/101] Update tests --- tests/wind_rose_time_series_test.py | 116 ++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 7 deletions(-) diff --git a/tests/wind_rose_time_series_test.py b/tests/wind_rose_time_series_test.py index 454cee4db..2fb4c31fb 100644 --- a/tests/wind_rose_time_series_test.py +++ b/tests/wind_rose_time_series_test.py @@ -12,17 +12,119 @@ # See https://floris.readthedocs.io for documentation +import numpy as np +import pytest + from floris.tools import TimeSeries, WindRose -# import pytest +def test_time_series_instantiation(): + wind_directions = np.array([6, 7, 8]) + wind_speeds = np.array([100, 120, 130]) + time_series = TimeSeries(wind_directions, wind_speeds) + time_series -def test_wind_rose_instantiation(): - wind_rose = WindRose() - wind_rose +def test_time_series_wrong_dimensions(): + wind_directions = np.array([6, 7]) + wind_speeds = np.array([100, 120, 130]) + with pytest.raises(ValueError): + TimeSeries(wind_directions, wind_speeds) -def test_time_series_instantiation(): - time_series = TimeSeries - time_series +def test_wind_rose_wrong_dimensions(): + wind_directions = np.array([6, 7]) + wind_speeds = np.array([100, 120, 130]) + + # This should be ok: + _ = WindRose(wind_directions, wind_speeds) + + # This should be ok + _ = WindRose(wind_directions, wind_speeds, np.ones((2, 3))) + + # This should raise an error + with pytest.raises(ValueError): + WindRose(wind_directions, wind_speeds, np.ones((3, 3))) + + +def test_wrap_wind_directions_near_360(): + wd_step = 5.0 + wd_values = np.array([0, 180, 357, 357.5, 358]) + time_series = TimeSeries(np.array([0]), np.array([0])) + + wd_wrapped = time_series._wrap_wind_directions_near_360(wd_values, wd_step) + + expected_result = np.array([0, 180, 357, -wd_step / 2.0, -2.0]) + assert np.allclose(wd_wrapped, expected_result) + + +def test_time_series_to_wind_rose(): + # Test just 1 wind speed + wind_directions = np.array([259.8, 260.2, 264.3]) + wind_speeds = np.array([5.0, 5.0, 5.1]) + time_series = TimeSeries(wind_directions, wind_speeds) + wind_rose = time_series.to_wind_rose(wd_step=2.0, ws_step=1.0) + + # The wind directions should be 260, 262 and 264 + assert np.allclose(wind_rose.wind_directions, [260, 262, 264]) + + # Freq table should have dimension of 3 wd x 1 ws + freq_table = wind_rose.freq_table + assert freq_table.shape[0] == 3 + assert freq_table.shape[1] == 1 + + # The frequencies should [2/3, 0, 1/3] + assert np.allclose(freq_table.squeeze(), [2 / 3, 0, 1 / 3]) + + # Test just 2 wind speeds + wind_directions = np.array([259.8, 260.2, 264.3]) + wind_speeds = np.array([5.0, 5.0, 6.1]) + time_series = TimeSeries(wind_directions, wind_speeds) + wind_rose = time_series.to_wind_rose(wd_step=2.0, ws_step=1.0) + + # The wind directions should be 260, 262 and 264 + assert np.allclose(wind_rose.wind_directions, [260, 262, 264]) + + # The wind speeds should be 5 and 6 + assert np.allclose(wind_rose.wind_speeds, [5, 6]) + + # Freq table should have dimension of 3 wd x 2 ws + freq_table = wind_rose.freq_table + assert freq_table.shape[0] == 3 + assert freq_table.shape[1] == 2 + + # The frequencies should [2/3, 0, 1/3] + assert freq_table[0, 0] == 2 / 3 + assert freq_table[2, 1] == 1 / 3 + + +def test_time_series_to_wind_rose_wrapping(): + wind_directions = np.arange(0.0, 360.0, 0.25) + wind_speeds = 8.0 * np.ones_like(wind_directions) + time_series = TimeSeries(wind_directions, wind_speeds) + wind_rose = time_series.to_wind_rose(wd_step=2.0, ws_step=1.0) + + # Expert for the first bin in this case to be 0, and the final to be 358 + # and both to have equal numbers of points + np.testing.assert_almost_equal(wind_rose.wind_directions[0], 0) + np.testing.assert_almost_equal(wind_rose.wind_directions[-1], 358) + np.testing.assert_almost_equal(wind_rose.freq_table[0, 0], wind_rose.freq_table[-1, 0]) + + +def test_time_series_to_wind_rose_with_ti(): + wind_directions = np.array([259.8, 260.2, 260.3, 260.1]) + wind_speeds = np.array([5.0, 5.0, 5.1, 7.2]) + turbulence_intensity = np.array([0.5, 1.0, 1.5, 2.0]) + time_series = TimeSeries( + wind_directions, wind_speeds, turbulence_intensity=turbulence_intensity + ) + wind_rose = time_series.to_wind_rose(wd_step=2.0, ws_step=1.0) + + # Turbulence intensity should average to 1 in the 5 m/s bin and 2 in the 7 m/s bin + ti_table = wind_rose.ti_table + np.testing.assert_almost_equal(ti_table[0, 0], 1) + np.testing.assert_almost_equal(ti_table[0, 2], 2) + + # The 6 m/s bin should be empty + freq_table = wind_rose.freq_table + np.testing.assert_almost_equal(freq_table[0, 1], 0) From f70c41113c5d28f9ae776d09263ef13db009408e Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 15:22:28 -0700 Subject: [PATCH 007/101] Add unpack functions --- floris/tools/wind_rose_time_series.py | 66 ++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/floris/tools/wind_rose_time_series.py b/floris/tools/wind_rose_time_series.py index 512c8971c..ea7848125 100644 --- a/floris/tools/wind_rose_time_series.py +++ b/floris/tools/wind_rose_time_series.py @@ -41,6 +41,15 @@ def __init__( self.wind_directions = wind_directions self.wind_speeds = wind_speeds + # Also save gridded versions + self.wd_grid, self.ws_grid = np.meshgrid( + self.wind_directions, self.wind_speeds, indexing="ij" + ) + + # Save flat versions of each as well + self.wd_flat = self.wd_grid.flatten() + self.ws_flat = self.ws_grid.flatten() + # If freq_table is not None, confirm it has correct dimension, # otherwise initialze to uniform probability if freq_table is not None: @@ -55,6 +64,9 @@ def __init__( # Normalize freq table self.freq_table = self.freq_table / np.sum(self.freq_table) + # Save a flatten version + self.freq_table_flat = self.freq_table.flatten() + # If TI table is not None, confirm dimension # otherwise leave it None if ti_table is not None: @@ -62,7 +74,11 @@ def __init__( raise ValueError("ti_table first dimension must equal len(wind_directions)") if not ti_table.shape[1] == len(wind_speeds): raise ValueError("ti_table second dimension must equal len(wind_speeds)") - self.ti_table = ti_table + self.ti_table = ti_table + self.ti_table_flat = self.ti_table.flatten() + else: + self.ti_table = None + self.ti_table_flat = None # If price_table is not None, confirm it has correct dimension, # otherwise initialze to all ones @@ -74,6 +90,43 @@ def __init__( self.price_table = price_table else: self.price_table = np.ones((len(wind_directions), len(wind_speeds))) + # Save a flatten version + self.price_table_flat = self.price_table.flatten() + + def _unpack(self): + """ + Unpack the values in a form which is ready for FLORIS' reinitialize function + """ + + # The unpacked versions start as the flat version of each + wind_directions_unpack = self.wd_flat.copy() + wind_speeds_unpack = self.ws_flat.copy() + freq_table_unpack = self.freq_table_flat.copy() + + # Get a mask of combinations that are more than 0 occurences + self.unpack_mask = freq_table_unpack > 0.0 + + # Now mask thes values to as to only compute values with occurence over 0 + wind_directions_unpack = wind_directions_unpack[self.unpack_mask] + wind_speeds_unpack = wind_speeds_unpack[self.unpack_mask] + freq_table_unpack = freq_table_unpack[self.unpack_mask] + + # Repeat for turbulence intensity if not none + if self.ti_table_flat is not None: + ti_table_unpack = self.ti_table_flat[self.unpack_mask] + else: + ti_table_unpack = None + + # Now get unpacked price table + price_table_unpack = self.price_table_flat[self.unpack_mask] + + return ( + wind_directions_unpack, + wind_speeds_unpack, + freq_table_unpack, + ti_table_unpack, + price_table_unpack, + ) class TimeSeries: @@ -107,6 +160,17 @@ def __init__( # Record findex self.n_findex = len(self.wind_directions) + def _unpack(self): + """ + Unpack the time series data to floris' reinitialize function + """ + return ( + self.wind_directions.copy(), + self.wind_speeds.copy(), + self.turbulence_intensity.copy(), + self.prices.copy(), + ) + def _wrap_wind_directions_near_360(self, wind_directions, wd_step): """ use wd_step to produce a wrapped version of wind_directions From 1fbb6b580fa840e30d64c11a8da6b0bda02d01db Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 15:22:42 -0700 Subject: [PATCH 008/101] Add grid and unpack tests --- tests/wind_rose_time_series_test.py | 49 ++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/tests/wind_rose_time_series_test.py b/tests/wind_rose_time_series_test.py index 2fb4c31fb..3f228c4f6 100644 --- a/tests/wind_rose_time_series_test.py +++ b/tests/wind_rose_time_series_test.py @@ -19,34 +19,69 @@ def test_time_series_instantiation(): - wind_directions = np.array([6, 7, 8]) - wind_speeds = np.array([100, 120, 130]) + wind_directions = np.array([270, 280, 290]) + wind_speeds = np.array([5, 5, 5]) time_series = TimeSeries(wind_directions, wind_speeds) time_series def test_time_series_wrong_dimensions(): - wind_directions = np.array([6, 7]) - wind_speeds = np.array([100, 120, 130]) + wind_directions = np.array([270, 280, 290]) + wind_speeds = np.array([5, 5]) with pytest.raises(ValueError): TimeSeries(wind_directions, wind_speeds) def test_wind_rose_wrong_dimensions(): - wind_directions = np.array([6, 7]) - wind_speeds = np.array([100, 120, 130]) + wind_directions = np.array([270, 280, 290]) + wind_speeds = np.array([6, 7]) # This should be ok: _ = WindRose(wind_directions, wind_speeds) # This should be ok - _ = WindRose(wind_directions, wind_speeds, np.ones((2, 3))) + _ = WindRose(wind_directions, wind_speeds, np.ones((3, 2))) # This should raise an error with pytest.raises(ValueError): WindRose(wind_directions, wind_speeds, np.ones((3, 3))) +def test_wind_rose_grid(): + wind_directions = np.array([270, 280, 290]) + wind_speeds = np.array([6, 7]) + + wind_rose = WindRose(wind_directions, wind_speeds) + + # Wd grid has same dimensions as freq table + assert wind_rose.wd_grid.shape == wind_rose.freq_table.shape + + # Flattening process occurs wd first + np.testing.assert_allclose(wind_rose.wd_flat, [270, 270, 280, 280, 290, 290]) + + +def test_wind_rose_unpack(): + wind_directions = np.array([270, 280, 290]) + wind_speeds = np.array([6, 7]) + freq_table = np.array([[1.0, 0.0], [0, 1.0], [0, 0]]) + + wind_rose = WindRose(wind_directions, wind_speeds, freq_table) + + ( + wind_directions_unpack, + wind_speeds_unpack, + freq_table_unpack, + ti_table_unpack, + price_table_unpack, + ) = wind_rose._unpack() + + # Given the above frequency table, would only expect the + # (270 deg, 6 m/s) and (280 deg, 7 m/s) rows + np.testing.assert_allclose(wind_directions_unpack, [270, 280]) + np.testing.assert_allclose(wind_speeds_unpack, [6, 7]) + np.testing.assert_allclose(freq_table_unpack, [0.5, 0.5]) + + def test_wrap_wind_directions_near_360(): wd_step = 5.0 wd_values = np.array([0, 180, 357, 357.5, 358]) From 1c5485374626a4cf340341593e5bac6a3b69a800 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 15:43:32 -0700 Subject: [PATCH 009/101] Small refactor --- floris/tools/wind_rose_time_series.py | 60 +++++++++++++++++++-------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/floris/tools/wind_rose_time_series.py b/floris/tools/wind_rose_time_series.py index ea7848125..8a617b3c8 100644 --- a/floris/tools/wind_rose_time_series.py +++ b/floris/tools/wind_rose_time_series.py @@ -41,15 +41,6 @@ def __init__( self.wind_directions = wind_directions self.wind_speeds = wind_speeds - # Also save gridded versions - self.wd_grid, self.ws_grid = np.meshgrid( - self.wind_directions, self.wind_speeds, indexing="ij" - ) - - # Save flat versions of each as well - self.wd_flat = self.wd_grid.flatten() - self.ws_flat = self.ws_grid.flatten() - # If freq_table is not None, confirm it has correct dimension, # otherwise initialze to uniform probability if freq_table is not None: @@ -64,9 +55,6 @@ def __init__( # Normalize freq table self.freq_table = self.freq_table / np.sum(self.freq_table) - # Save a flatten version - self.freq_table_flat = self.freq_table.flatten() - # If TI table is not None, confirm dimension # otherwise leave it None if ti_table is not None: @@ -75,10 +63,9 @@ def __init__( if not ti_table.shape[1] == len(wind_speeds): raise ValueError("ti_table second dimension must equal len(wind_speeds)") self.ti_table = ti_table - self.ti_table_flat = self.ti_table.flatten() + else: self.ti_table = None - self.ti_table_flat = None # If price_table is not None, confirm it has correct dimension, # otherwise initialze to all ones @@ -89,9 +76,35 @@ def __init__( raise ValueError("price_table second dimension must equal len(wind_speeds)") self.price_table = price_table else: - self.price_table = np.ones((len(wind_directions), len(wind_speeds))) - # Save a flatten version - self.price_table_flat = self.price_table.flatten() + self.price_table = None + + # Build the gridded and flatten versions + self._build_gridded_and_flattened_version() + + def _build_gridded_and_flattened_version(self): + # Gridded wind speed and direction + self.wd_grid, self.ws_grid = np.meshgrid( + self.wind_directions, self.wind_speeds, indexing="ij" + ) + + # Flat wind speed and direction + self.wd_flat = self.wd_grid.flatten() + self.ws_flat = self.ws_grid.flatten() + + # Flat frequency table + self.freq_table_flat = self.freq_table.flatten() + + # TI table + if self.ti_table is not None: + self.ti_table_flat = self.ti_table.flatten() + else: + self.ti_table_flat = None + + # Price table + if self.price_table is not None: + self.price_table_flat = self.price_table.flatten() + else: + self.price_table_flat = None def _unpack(self): """ @@ -118,7 +131,10 @@ def _unpack(self): ti_table_unpack = None # Now get unpacked price table - price_table_unpack = self.price_table_flat[self.unpack_mask] + if self.price_table_flat is not None: + price_table_unpack = self.price_table_flat[self.unpack_mask] + else: + price_table_unpack = None return ( wind_directions_unpack, @@ -128,6 +144,14 @@ def _unpack(self): price_table_unpack, ) + def resample_wind_speeds(self): + # TODO: Need to figure this out + pass + + def resample_wind_direction(self): + # TODO: Need to figure this out + pass + class TimeSeries: """ From 96b477b371b1e504c52dbed07b7ed850010d40a6 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 16:48:31 -0700 Subject: [PATCH 010/101] Add resample function --- floris/tools/wind_rose_time_series.py | 57 ++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/floris/tools/wind_rose_time_series.py b/floris/tools/wind_rose_time_series.py index 8a617b3c8..394f44442 100644 --- a/floris/tools/wind_rose_time_series.py +++ b/floris/tools/wind_rose_time_series.py @@ -36,6 +36,11 @@ def __init__( """ TODO: Write this later """ + if not isinstance(wind_directions, np.ndarray): + raise TypeError("wind_directions must be a NumPy array") + + if not isinstance(wind_speeds, np.ndarray): + raise TypeError("wind_directions must be a NumPy array") # Save the wind speeds and directions self.wind_directions = wind_directions @@ -106,7 +111,7 @@ def _build_gridded_and_flattened_version(self): else: self.price_table_flat = None - def _unpack(self): + def unpack(self): """ Unpack the values in a form which is ready for FLORIS' reinitialize function """ @@ -144,13 +149,34 @@ def _unpack(self): price_table_unpack, ) - def resample_wind_speeds(self): - # TODO: Need to figure this out - pass + def resample_wind_rose(self, ws_step=None, wd_step=None): + # Returns a resampled version of the wind rose using new ws_step and wd_step + + # Use the bin weights feature in TimeSeries to resample the wind rose + + # If ws_step or wd_step, not specied use current values + if ws_step is None: + if len(self.wind_speeds) >= 2: + ws_step = self.wind_speeds[1] - self.wind_speeds[0] + else: + # It doesn't matter, just set to 1 + ws_step = 1 + if wd_step is None: + if len(self.wind_directions) >= 2: + wd_step = self.wind_directions[1] - self.wind_directions[0] + else: + # It doesn't matter, just set to 1 + wd_step = 1 + + # Pass the flat versions of each quantity to build a TimeSeries model + time_series = TimeSeries( + self.wd_flat, self.ws_flat, self.ti_table_flat, self.price_table_flat + ) - def resample_wind_direction(self): - # TODO: Need to figure this out - pass + # Now build a new wind rose using the new steps + return time_series.to_wind_rose( + wd_step=wd_step, ws_step=ws_step, bin_weights=self.freq_table_flat + ) class TimeSeries: @@ -184,13 +210,19 @@ def __init__( # Record findex self.n_findex = len(self.wind_directions) - def _unpack(self): + def unpack(self): """ Unpack the time series data to floris' reinitialize function """ + + # to match wind_rose, make a uniform frequency + uniform_frequency = np.ones_like(self.wind_directions) + uniform_frequency = uniform_frequency / uniform_frequency.sum() + return ( self.wind_directions.copy(), self.wind_speeds.copy(), + uniform_frequency, self.turbulence_intensity.copy(), self.prices.copy(), ) @@ -206,7 +238,9 @@ def _wrap_wind_directions_near_360(self, wind_directions, wd_step): wind_directions_wrapped[mask] = wind_directions_wrapped[mask] - 360.0 return wind_directions_wrapped - def to_wind_rose(self, wd_step=2.0, ws_step=1.0, wd_edges=None, ws_edges=None): + def to_wind_rose( + self, wd_step=2.0, ws_step=1.0, wd_edges=None, ws_edges=None, bin_weights=None + ): """ TODO: Write this later """ @@ -259,6 +293,11 @@ def to_wind_rose(self, wd_step=2.0, ws_step=1.0, wd_edges=None, ws_edges=None): } ) + # If bin_weights are passed in, apply these to the frequency + # this is mostly used when resampling the wind rose + if bin_weights is not None: + df = df.assign(freq_val=df["freq_val"] * bin_weights) + # If turbulence_intensity is not none, add to dataframe if self.turbulence_intensity is not None: df = df.assign(turbulence_intensity=self.turbulence_intensity) From d9be4ace3b340b78d7fc9af474f293a82a55fc81 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 16:48:45 -0700 Subject: [PATCH 011/101] Test resample --- tests/wind_rose_time_series_test.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/wind_rose_time_series_test.py b/tests/wind_rose_time_series_test.py index 3f228c4f6..d68faf832 100644 --- a/tests/wind_rose_time_series_test.py +++ b/tests/wind_rose_time_series_test.py @@ -73,7 +73,7 @@ def test_wind_rose_unpack(): freq_table_unpack, ti_table_unpack, price_table_unpack, - ) = wind_rose._unpack() + ) = wind_rose.unpack() # Given the above frequency table, would only expect the # (270 deg, 6 m/s) and (280 deg, 7 m/s) rows @@ -82,6 +82,26 @@ def test_wind_rose_unpack(): np.testing.assert_allclose(freq_table_unpack, [0.5, 0.5]) +def test_wind_rose_resample(): + wind_directions = np.array([0, 2, 4, 6, 8, 10]) + wind_speeds = np.array([8]) + freq_table = np.array([[1.0], [1.0], [1.0], [1.0], [1.0], [1.0]]) + + wind_rose = WindRose(wind_directions, wind_speeds, freq_table) + + # Test that resampling with a new step size returns the same + wind_rose_resample = wind_rose.resample_wind_rose() + + np.testing.assert_allclose(wind_rose.wind_directions, wind_rose_resample.wind_directions) + np.testing.assert_allclose(wind_rose.wind_speeds, wind_rose_resample.wind_speeds) + np.testing.assert_allclose(wind_rose.freq_table_flat, wind_rose_resample.freq_table_flat) + + # Now test resampling the wind direction to 5 deg bins + wind_rose_resample = wind_rose.resample_wind_rose(wd_step=5.0) + np.testing.assert_allclose(wind_rose_resample.wind_directions, [0, 5, 10]) + np.testing.assert_allclose(wind_rose_resample.freq_table_flat, [2 / 6, 2 / 6, 2 / 6]) + + def test_wrap_wind_directions_near_360(): wd_step = 5.0 wd_values = np.array([0, 180, 357, 357.5, 358]) From bd0e7f3650bf103544fb77d53ae880e35d4538d0 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 22:28:45 -0700 Subject: [PATCH 012/101] Add plot wind rose function --- floris/tools/wind_rose_time_series.py | 79 ++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/floris/tools/wind_rose_time_series.py b/floris/tools/wind_rose_time_series.py index 394f44442..7070f7de5 100644 --- a/floris/tools/wind_rose_time_series.py +++ b/floris/tools/wind_rose_time_series.py @@ -12,6 +12,8 @@ # See https://floris.readthedocs.io for documentation +import matplotlib.cm as cm +import matplotlib.pyplot as plt import numpy as np import pandas as pd from pandas.api.types import CategoricalDtype @@ -149,7 +151,7 @@ def unpack(self): price_table_unpack, ) - def resample_wind_rose(self, ws_step=None, wd_step=None): + def resample_wind_rose(self, wd_step=None, ws_step=None): # Returns a resampled version of the wind rose using new ws_step and wd_step # Use the bin weights feature in TimeSeries to resample the wind rose @@ -178,6 +180,77 @@ def resample_wind_rose(self, ws_step=None, wd_step=None): wd_step=wd_step, ws_step=ws_step, bin_weights=self.freq_table_flat ) + def plot_wind_rose( + self, + ax=None, + color_map="viridis_r", + wd_step=15.0, + ws_step=5.0, + legend_kwargs={}, + ): + """ + This method creates a wind rose plot showing the frequency of occurance + of the specified wind direction and wind speed bins. If no axis is + provided, a new one is created. + + **Note**: Based on code provided by Patrick Murphy from the University + of Colorado Boulder. + + Args: + ax (:py:class:`matplotlib.pyplot.axes`, optional): The figure axes + on which the wind rose is plotted. Defaults to None. + color_map (str, optional): Colormap to use. Defaults to 'viridis_r'. + ws_step + wd_step + legend_kwargs (dict, optional): Keyword arguments to be passed to + ax.legend(). + + Returns: + :py:class:`matplotlib.pyplot.axes`: A figure axes object containing + the plotted wind rose. + """ + + # Get a resampled wind_rose + wind_rose_resample = self.resample_wind_rose(wd_step, ws_step) + wd_bins = wind_rose_resample.wind_directions + ws_bins = wind_rose_resample.wind_speeds + freq_table = wind_rose_resample.freq_table + + print(ws_bins) + + # Set up figure + if ax is None: + _, ax = plt.subplots(subplot_kw={"polar": True}) + + # Get a color array + color_array = cm.get_cmap(color_map, len(ws_bins)) + + for wd_idx, wd in enumerate(wd_bins): + rects = [] + freq_table_sub = freq_table[wd_idx, :].flatten() + for ws_idx, ws in reversed(list(enumerate(ws_bins))): + plot_val = freq_table_sub[:ws_idx].sum() + rects.append( + ax.bar( + np.radians(wd), + plot_val, + width=0.9 * np.radians(wd_step), + color=color_array(ws_idx), + edgecolor="k", + ) + ) + # break + + # Configure the plot + ax.legend(reversed(rects), ws_bins, **legend_kwargs) + ax.set_theta_direction(-1) + ax.set_theta_offset(np.pi / 2.0) + ax.set_theta_zero_location("N") + ax.set_xticks(np.arange(0, 2 * np.pi, np.pi / 4)) + ax.set_xticklabels(["N", "NE", "E", "SE", "S", "SW", "W", "NW"]) + + return ax + class TimeSeries: """ @@ -329,8 +402,8 @@ def to_wind_rose( df = ( df.assign(wd_bin=df["wd_bin"].astype(wd_cat)) .assign(ws_bin=df["ws_bin"].astype(ws_cat)) - .groupby(["wd_bin", "ws_bin"]) - .agg([np.sum, np.mean]) + .groupby(["wd_bin", "ws_bin"], observed=False) + .agg(["sum", "mean"]) ) # Flatten and combine levels using an underscore df.columns = ["_".join(col) for col in df.columns] From c76f4796787893eaccf89dd8b668c510495e4451 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 22:29:06 -0700 Subject: [PATCH 013/101] Add new wind rose usage example --- examples/34_wind_rose_examples.py | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 examples/34_wind_rose_examples.py diff --git a/examples/34_wind_rose_examples.py b/examples/34_wind_rose_examples.py new file mode 100644 index 000000000..174846790 --- /dev/null +++ b/examples/34_wind_rose_examples.py @@ -0,0 +1,50 @@ +# Copyright 2024 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 TimeSeries, WindRose +from floris.utilities import wrap_360 + + +# Generate a random time series of wind speeds, wind directions and turbulence intensities +N = 500 +wd_array = wrap_360(270 * np.ones(N) + np.random.randn(N) * 20) +ws_array = np.clip(8 * np.ones(N) + np.random.randn(N) * 8, 0, 50) +ti_array = np.clip(0.1 * np.ones(N) + np.random.randn(N) * 0.05, 0, 0.25) + +fig, axarr = plt.subplots(3, 1, sharex=True, figsize=(7, 4)) +ax = axarr[0] +ax.plot(wd_array, marker=".", ls="None") +ax.set_ylabel("Wind Direction") +ax = axarr[1] +ax.plot(ws_array, marker=".", ls="None") +ax.set_ylabel("Wind Speed") +ax = axarr[2] +ax.plot(ti_array, marker=".", ls="None") +ax.set_ylabel("Turbulence Intensity") + + +# Build the time series +time_series = TimeSeries(wd_array, ws_array, turbulence_intensity=ti_array) + +# Now build the wind rose +wind_rose = time_series.to_wind_rose() + +# Plot the wind rose +fig, ax = plt.subplots(subplot_kw={"polar": True}) +wind_rose.plot_wind_rose(ax=ax) + +plt.show() From 18a4884b72d73396aaf754ab12515a48550218c4 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 22:46:48 -0700 Subject: [PATCH 014/101] Delete old code --- floris/tools/to_delete/power_rose.py | 500 -------- floris/tools/to_delete/wind_rose.py | 1597 -------------------------- 2 files changed, 2097 deletions(-) delete mode 100644 floris/tools/to_delete/power_rose.py delete mode 100644 floris/tools/to_delete/wind_rose.py diff --git a/floris/tools/to_delete/power_rose.py b/floris/tools/to_delete/power_rose.py deleted file mode 100644 index 579d5e783..000000000 --- a/floris/tools/to_delete/power_rose.py +++ /dev/null @@ -1,500 +0,0 @@ -# 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 os -import pickle - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd - -from floris.utilities import wrap_180 - - -# TODO: organize by private and public methods - - -class PowerRose: - """ - The PowerRose class is used to organize information about wind farm power - production for different wind conditions (e.g., wind speed, wind direction) - along with their frequencies of occurance to calculate the resulting annual - energy production (AEP). Power production and AEP are considered for - baseline operation, ideal operation without wake losses, and optionally - optimal operation with wake steering. The primary purpose of the PowerRose - class is for visualizing and reporting energy production and energy gains - from wake steering. A PowerRose object can be populated with user-specified - wind rose and power data (for example, using a :py:class:`~.tools - WindRose` object) or data from a previously saved PowerRose object can be - loaded. - """ - - def __init__(self,): - """ - Instantiate a PowerRose object. No explicit arguments required, and an - additional method will need to be called to populate the PowerRose - object with data. - """ - - def load(self, filename): - """ - This method loads data from a previously saved PowerRose pickle file - into a PowerRose object. - - Args: - filename (str): Path and filename of pickle file to load. - """ - - ( - self.name, - self.df_windrose, - self.power_no_wake, - self.power_baseline, - self.power_opt, - self.use_opt, - ) = pickle.load(open(filename, "rb")) - - # Compute energies - self.df_power = pd.DataFrame( - {"wd": self.df_windrose["wd"], "ws": self.df_windrose["ws"]} - ) - self._compute_energy() - - # Compute totals - self._compute_totals() - - def save(self, filename): - """ - This method saves PowerRose data as a pickle file so that it can be - imported into a PowerRose object later. - - Args: - filename (str): Path and filename of pickle file to save. - """ - pickle.dump( - [ - self.name, - self.df_windrose, - self.power_no_wake, - self.power_baseline, - self.power_opt, - self.use_opt, - ], - open(filename, "wb"), - ) - - # def _all_combine(self): - # df_power = self.df_power.copy(deep=True) - # df_yaw = self.df_yaw.copy(deep=True) - # df_turbine_power_no_wake = self.df_turbine_power_no_wake.copy( - # deep=True) - # df_turbine_power_baseline = self.df_turbine_power_baseline.copy( - # deep=True) - # df_turbine_power_opt = self.df_turbine_power_opt.copy(deep=True) - - # # Adjust the column names for uniqunes - # df_yaw.columns = [ - # 'yaw_%d' % c if type(c) is int else c for c in df_yaw.columns - # ] - # df_turbine_power_no_wake.columns = [ - # 'tnw_%d' % c if type(c) is int else c - # for c in df_turbine_power_no_wake.columns - # ] - # df_turbine_power_baseline.columns = [ - # 'tb_%d' % c if type(c) is int else c - # for c in df_turbine_power_baseline.columns - # ] - # df_turbine_power_opt.columns = [ - # 'topt_%d' % c if type(c) is int else c - # for c in df_turbine_power_opt.columns - # ] - - # # Merge - # df_combine = df_power.merge(df_yaw, on=['ws', 'wd']) - # df_combine = df_combine.merge(df_turbine_power_no_wake, - # on=['ws', 'wd']) - # df_combine = df_combine.merge(df_turbine_power_baseline, - # on=['ws', 'wd']) - # df_combine = df_combine.merge(df_turbine_power_opt, on=['ws', 'wd']) - - # return df_combine - - def _norm_frequency(self, df): - print("Norming frequency total of %.2f to 1.0" % df.freq_val.sum()) - df["freq_val"] = df.freq_val / df.freq_val.sum() - return df - - def _compute_energy(self): - self.df_power["energy_no_wake"] = self.df_windrose.freq_val * self.power_no_wake - self.df_power["energy_baseline"] = ( - self.df_windrose.freq_val * self.power_baseline - ) - if self.use_opt: - self.df_power["energy_opt"] = self.df_windrose.freq_val * self.power_opt - - def _compute_totals(self): - df = self.df_power.copy(deep=True) - df = df.sum() - - # Get total annual energy amounts - self.total_no_wake = (8760 / 1e9) * df.energy_no_wake - self.total_baseline = (8760 / 1e9) * df.energy_baseline - if self.use_opt: - self.total_opt = (8760 / 1e9) * df.energy_opt - - # Get wake loss amounts - self.baseline_percent = self.total_baseline / self.total_no_wake - self.baseline_wake_loss = 1 - self.baseline_percent - - if self.use_opt: - self.opt_percent = self.total_opt / self.total_no_wake - self.opt_wake_loss = 1 - self.opt_percent - - # Percent gain - if self.use_opt: - self.percent_gain = ( - self.total_opt - self.total_baseline - ) / self.total_baseline - self.reduction_in_wake_loss = ( - -1 - * (self.opt_wake_loss - self.baseline_wake_loss) - / self.baseline_wake_loss - ) - - def make_power_rose_from_user_data( - self, name, df_windrose, power_no_wake, power_baseline, power_opt=None - ): - """ - This method populates the PowerRose object with a user-specified wind - rose containing wind direction, wind speed, and additional optional - variables, as well as baseline wind farm power, ideal wind farm power - without wake losses, and optionally optimal wind farm power with wake - steering corresponding to each wind condition. - - TODO: Add inputs for turbine-level power and optimal yaw offsets. - - Args: - name (str): The name of the PowerRose object. - df_windrose (pandas.DataFrame): A DataFrame with wind rose - information containing at least - the following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - - power_no_wake (iterable): A list of wind farm power without wake - losses corresponding to the wind conditions in df_windrose (W). - power_baseline (iterable): A list of baseline wind farm power with - wake losses corresponding to the wind conditions in df_windrose - (W). - power_opt (iterable, optional): A list of optimal wind farm power - with wake steering corresponding to the wind conditions in - df_windrose (W). Defaults to None. - """ - self.name = name - if df_windrose is not None: - self.df_windrose = self._norm_frequency(df_windrose) - self.power_no_wake = power_no_wake - self.power_baseline = power_baseline - self.power_opt = power_opt - - # Only use_opt data if provided - if power_opt is None: - self.use_opt = False - else: - self.use_opt = True - - # # Make a single combined frame in case it's useful (Set aside for now) - # self.df_combine = self._all_combine() - - # Compute energies - self.df_power = pd.DataFrame({"wd": df_windrose["wd"], "ws": df_windrose["ws"]}) - self._compute_energy() - - # Compute totals - self._compute_totals() - - def report(self): - """ - This method prints information about annual energy production (AEP) - using the PowerRose object data. The AEP in GWh is listed for ideal - operation without wake losses, baseline operation, and optimal - operation with wake steering, if optimal power data are stored. The - wind farm efficiency (% of ideal energy production) and wake loss - percentages are listed for baseline and optimal operation (if optimal - power is stored), along with the AEP gain from wake steering (again, if - optimal power is stored). The AEP gain from wake steering is also - listed as a percentage of wake losses recovered, if applicable. - """ - if self.use_opt: - print("=============================================") - print("Case %s has results:" % self.name) - print("=============================================") - print("-\tNo-Wake\t\tBaseline\tOpt ") - print("---------------------------------------------") - print( - "AEP (GWh)\t%.1E\t\t%.1E\t\t%.1E" - % (self.total_no_wake, self.total_baseline, self.total_opt) - ) - print( - "%%\t--\t\t%.1f%%\t\t%.1f%%" - % (100.0 * self.baseline_percent, 100.0 * self.opt_percent) - ) - print( - "Wk Loss\t--\t\t%.1f%%\t\t%.1f%%" - % (100.0 * self.baseline_wake_loss, 100.0 * self.opt_wake_loss) - ) - print("AEP Gain --\t\t--\t\t%.1f%%" % (100.0 * self.percent_gain)) - print("Loss Red --\t\t--\t\t%.1f%%" % (100.0 * self.reduction_in_wake_loss)) - else: - print("=============================================") - print("Case %s has results:" % self.name) - print("=============================================") - print("-\tNo-Wake\t\tBaseline ") - print("---------------------------------------------") - print("AEP (GWh)\t%.1E\t\t%.1E" % (self.total_no_wake, self.total_baseline)) - print("%%\t--\t\t%.1f%%" % (100.0 * self.baseline_percent)) - print("Wk Loss\t--\t\t%.1f%%" % (100.0 * self.baseline_wake_loss)) - - def plot_by_direction(self, axarr=None): - """ - This method plots energy production, wind farm efficiency, and energy - gains from wake steering (if applicable) as a function of wind - direction. If axes are not provided, new ones are created. The plots - include: - - 1) The energy production as a function of wind direction for the - baseline and, if applicable, optimal wake steering cases normalized by - the maximum energy production. - 2) The wind farm efficiency (energy production relative to energy - production without wake losses) as a function of wind direction for the - baseline and, if applicable, optimal wake steering cases. - 3) Percent gain in energy production with optimal wake steering as a - function of wind direction. This third plot is only created if optimal - power data are stored in the PowerRose object. - - Args: - axarr (numpy.ndarray, optional): An array of 2 or 3 - :py:class:`matplotlib.axes._subplots.AxesSubplot` axes objects - on which data are plotted. Three axes are rquired if the - PowerRose object contains optimal power data. Default is None. - - Returns: - numpy.ndarray: An array of 2 or 3 - :py:class:`matplotlib.axes._subplots.AxesSubplot` axes objects on - which the data are plotted. - """ - - df = self.df_power.copy(deep=True) - df = df.groupby("wd").sum().reset_index() - - if self.use_opt: - - if axarr is None: - fig, axarr = plt.subplots(3, 1, sharex=True) - - ax = axarr[0] - ax.plot( - df.wd, - df.energy_baseline / np.max(df.energy_opt), - label="Baseline", - color="k", - ) - ax.axhline( - np.mean(df.energy_baseline / np.max(df.energy_opt)), color="r", ls="--" - ) - ax.plot( - df.wd, - df.energy_opt / np.max(df.energy_opt), - label="Optimized", - color="r", - ) - ax.axhline( - np.mean(df.energy_opt / np.max(df.energy_opt)), color="r", ls="--" - ) - ax.set_ylabel("Normalized Energy") - ax.grid(True) - ax.legend() - ax.set_title(self.name) - - ax = axarr[1] - ax.plot( - df.wd, - df.energy_baseline / df.energy_no_wake, - label="Baseline", - color="k", - ) - ax.axhline( - np.mean(df.energy_baseline) / np.mean(df.energy_no_wake), - color="k", - ls="--", - ) - ax.plot( - df.wd, df.energy_opt / df.energy_no_wake, label="Optimized", color="r" - ) - ax.axhline( - np.mean(df.energy_opt) / np.mean(df.energy_no_wake), color="r", ls="--" - ) - ax.set_ylabel("Wind Farm Efficiency") - ax.grid(True) - ax.legend() - - ax = axarr[2] - ax.plot( - df.wd, - 100.0 * (df.energy_opt - df.energy_baseline) / df.energy_baseline, - "r", - ) - ax.axhline( - 100.0 - * (df.energy_opt.mean() - df.energy_baseline.mean()) - / df.energy_baseline.mean(), - df.energy_baseline.mean(), - color="r", - ls="--", - ) - ax.set_ylabel("Percent Gain") - ax.set_xlabel("Wind Direction (deg)") - - return axarr - - else: - - if axarr is None: - fig, axarr = plt.subplots(2, 1, sharex=True) - - ax = axarr[0] - ax.plot( - df.wd, - df.energy_baseline / np.max(df.energy_baseline), - label="Baseline", - color="k", - ) - ax.axhline( - np.mean(df.energy_baseline / np.max(df.energy_baseline)), - color="r", - ls="--", - ) - ax.set_ylabel("Normalized Energy") - ax.grid(True) - ax.legend() - ax.set_title(self.name) - - ax = axarr[1] - ax.plot( - df.wd, - df.energy_baseline / df.energy_no_wake, - label="Baseline", - color="k", - ) - ax.axhline( - np.mean(df.energy_baseline) / np.mean(df.energy_no_wake), - color="k", - ls="--", - ) - ax.set_ylabel("Wind Farm Efficiency") - ax.grid(True) - ax.legend() - - ax.set_xlabel("Wind Direction (deg)") - - return axarr - - # def wake_loss_at_direction(self, wd): - # """ - # Calculate wake losses for a given direction. Plot rose figures - # for Power, Energy, Baseline power, Optimal gain, Total Gain, - # Percent Gain, etc. - - # Args: - # wd (float): Wind direction of interest. - - # Returns: - # tuple: tuple containing: - - # - **fig** (*plt.figure*): Figure handle. - # - **axarr** (*list*): list of axis handles. - # """ - - # df = self.df_power.copy(deep=True) - - # # Choose the nearest direction - # # Find nearest wind direction - # df['dist'] = np.abs(wrap_180(df.wd - wd)) - # wd_select = df[df.dist == df.dist.min()]['wd'].unique()[0] - # print('Nearest wd to %.1f is %.1f' % (wd, wd_select)) - # df = df[df.wd == wd_select] - - # df = df.groupby('ws').sum().reset_index() - - # fig, axarr = plt.subplots(4, 2, sharex=True, figsize=(14, 12)) - - # ax = axarr[0, 0] - # ax.set_title('Power') - # ax.plot(df.ws, df.power_no_wake, 'k', label='No Wake') - # ax.plot(df.ws, df.power_baseline, 'b', label='Baseline') - # ax.plot(df.ws, df.power_opt, 'r', label='Opt') - # ax.set_ylabel('Total') - # ax.grid() - - # ax = axarr[0, 1] - # ax.set_title('Energy') - # ax.plot(df.ws, df.energy_no_wake, 'k', label='No Wake') - # ax.plot(df.ws, df.energy_baseline, 'b', label='Baseline') - # ax.plot(df.ws, df.energy_opt, 'r', label='Opt') - # ax.legend() - # ax.grid() - - # ax = axarr[1, 0] - # ax.plot(df.ws, - # df.power_baseline / df.power_no_wake, - # 'b', - # label='Baseline') - # ax.plot(df.ws, df.power_opt / df.power_no_wake, 'r', label='Opt') - # ax.set_ylabel('Percent') - # ax.grid() - - # ax = axarr[1, 1] - # ax.plot(df.ws, - # df.energy_baseline / df.energy_no_wake, - # 'b', - # label='Baseline') - # ax.plot(df.ws, df.energy_opt / df.energy_no_wake, 'r', label='Opt') - # ax.grid() - - # ax = axarr[2, 0] - # ax.plot(df.ws, (df.power_opt - df.power_baseline), 'r') - # ax.set_ylabel('Total Gain') - # ax.grid() - - # ax = axarr[2, 1] - # ax.plot(df.ws, (df.energy_opt - df.energy_baseline), 'r') - # ax.grid() - - # ax = axarr[3, 0] - # ax.plot(df.ws, (df.power_opt - df.power_baseline) / df.power_baseline, - # 'r') - # ax.set_ylabel('Percent Gain') - # ax.grid() - # ax.set_xlabel('Wind Speed (m/s)') - - # ax = axarr[3, 1] - # ax.plot(df.ws, - # (df.energy_opt - df.energy_baseline) / df.energy_baseline, 'r') - # ax.grid() - # ax.set_xlabel('Wind Speed (m/s)') - - # return fig, axarr diff --git a/floris/tools/to_delete/wind_rose.py b/floris/tools/to_delete/wind_rose.py deleted file mode 100644 index c0996369d..000000000 --- a/floris/tools/to_delete/wind_rose.py +++ /dev/null @@ -1,1597 +0,0 @@ -# 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 - -# TODO -# 1: reorganize into private and public methods -# 2: Include smoothing? - -import os -import pickle - -import dateutil -import matplotlib.cm as cm -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from pyproj import Proj -from scipy.interpolate import LinearNDInterpolator, NearestNDInterpolator - -import floris.utilities as geo - - -class WindRose: - """ - The WindRose class is used to organize information about the frequency of - occurance of different combinations of wind speed and wind direction (and - other optimal wind variables). A WindRose object can be used to help - calculate annual energy production (AEP) when combined with Floris power - calculations for different wind conditions. Several methods exist for - populating a WindRose object with wind data. WindRose also contains methods - for visualizing wind roses. - - References: - .. bibliography:: /references.bib - :style: unsrt - :filter: docname in docnames - :keyprefix: wr- - """ - - def __init__( - self, - ): - """ - Instantiate a WindRose object and set some initial parameter values. - No explicit arguments required, and an additional method will need to - be called to populate the WindRose object with data. - """ - # Initialize some varibles - self.num_wd = 0 - self.num_ws = 0 - self.wd_step = 1.0 - self.ws_step = 5.0 - self.wd = np.array([]) - self.ws = np.array([]) - self.df = pd.DataFrame() - - def save(self, filename): - """ - This method saves the WindRose data as a pickle file so that it can be - imported into a WindRose object later. - - Args: - filename (str): Path and filename of pickle file to save. - """ - pickle.dump( - [ - self.num_wd, - self.num_ws, - self.wd_step, - self.ws_step, - self.wd, - self.ws, - self.df, - ], - open(filename, "wb"), - ) - - def load(self, filename): - """ - This method loads data from a previously saved WindRose pickle file - into a WindRose object. - - Args: - filename (str): Path and filename of pickle file to load. - - Returns: - int, int, float, float, np.array, np.array, pandas.DataFrame: - - - Number of wind direction bins. - - Number of wind speed bins. - - Wind direction bin size (deg). - - Wind speed bin size (m/s). - - List of wind direction bin center values (deg). - - List of wind speed bin center values (m/s). - - DataFrame containing at least the following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of - the wind conditions in the other columns. - """ - ( - self.num_wd, - self.num_ws, - self.wd_step, - self.ws_step, - self.wd, - self.ws, - self.df, - ) = pickle.load(open(filename, "rb")) - - return self.df - - def resample_wind_speed(self, df, ws=np.arange(0, 26, 1.0)): - """ - This method resamples the wind speed bins using the specified wind - speed bin center values. The frequency values are adjusted accordingly. - - Args: - df (pandas.DataFrame): Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - - ws (np.array, optional): List of new wind speed center bins (m/s). - Defaults to np.arange(0, 26, 1.). - - Returns: - pandas.DataFrame: Wind rose DataFrame with the resampled wind speed - bins and frequencies containing at least the following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - New wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - new wind conditions in the other columns. - """ - # Make a copy of incoming dataframe - df = df.copy(deep=True) - - # Get the wind step - ws_step = ws[1] - ws[0] - - # Ws - ws_edges = ws - ws_step / 2.0 - ws_edges = np.append(ws_edges, np.array(ws[-1] + ws_step / 2.0)) - - # Cut wind speed onto bins - df["ws"] = pd.cut(df.ws, ws_edges, labels=ws) - - # Regroup - df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() - - # Fill nans - df = df.fillna(0) - - # Reset the index - df = df.reset_index() - - # Set to float - for c in [c for c in df.columns if c != "freq_val"]: - df[c] = df[c].astype(float) - df[c] = df[c].astype(float) - - return df - - def internal_resample_wind_speed(self, ws=np.arange(0, 26, 1.0)): - """ - Internal method for resampling wind speed into desired bins. The - frequency values are adjusted accordingly. Modifies data within - WindRose object without explicit return. - - TODO: make a private method - - Args: - ws (np.array, optional): Vector of wind speed bin centers for - the wind rose (m/s). Defaults to np.arange(0, 26, 1.). - """ - # Update ws and wd binning - self.ws = ws - self.num_ws = len(ws) - self.ws_step = ws[1] - ws[0] - - # Update internal data frame - self.df = self.resample_wind_speed(self.df, ws) - - def resample_wind_direction(self, df, wd=np.arange(0, 360, 5.0)): - """ - This method resamples the wind direction bins using the specified wind - direction bin center values. The frequency values are adjusted - accordingly. - - Args: - df (pandas.DataFrame): Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - - wd (np.array, optional): List of new wind direction center bins - (deg). Defaults to np.arange(0, 360, 5.). - - Returns: - pandas.DataFrame: Wind rose DataFrame with the resampled wind - direction bins and frequencies containing at least the following - columns: - - - **wd** (*float*) - New wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - new wind conditions in the other columns. - """ - # Make a copy of incoming dataframe - df = df.copy(deep=True) - - # Get the wind step - wd_step = wd[1] - wd[0] - - # Get bin edges - wd_edges = wd - wd_step / 2.0 - wd_edges = np.append(wd_edges, np.array(wd[-1] + wd_step / 2.0)) - - # Get the overhangs - negative_overhang = wd_edges[0] - positive_overhang = wd_edges[-1] - 360.0 - - # Need potentially to wrap high angle direction to negative for correct - # binning - df["wd"] = geo.wrap_360(df.wd) - if negative_overhang < 0: - print("Correcting negative Overhang:%.1f" % negative_overhang) - df["wd"] = np.where( - df.wd.values >= 360.0 + negative_overhang, - df.wd.values - 360.0, - df.wd.values, - ) - - # Check on other side - if positive_overhang > 0: - print("Correcting positive Overhang:%.1f" % positive_overhang) - df["wd"] = np.where( - df.wd.values <= positive_overhang, df.wd.values + 360.0, df.wd.values - ) - - # Cut into bins - df["wd"] = pd.cut(df.wd, wd_edges, labels=wd) - - # Regroup - df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() - - # Fill nans - df = df.fillna(0) - - # Reset the index - df = df.reset_index() - - # Set to float Re-wrap - for c in [c for c in df.columns if c != "freq_val"]: - df[c] = df[c].astype(float) - df[c] = df[c].astype(float) - df["wd"] = geo.wrap_360(df.wd) - - return df - - def internal_resample_wind_direction(self, wd=np.arange(0, 360, 5.0)): - """ - Internal method for resampling wind direction into desired bins. The - frequency values are adjusted accordingly. Modifies data within - WindRose object without explicit return. - - TODO: make a private method - - Args: - wd (np.array, optional): Vector of wind direction bin centers for - the wind rose (deg). Defaults to np.arange(0, 360, 5.). - """ - # Update ws and wd binning - self.wd = wd - self.num_wd = len(wd) - self.wd_step = wd[1] - wd[0] - - # Update internal data frame - self.df = self.resample_wind_direction(self.df, wd) - - def resample_column(self, df, col, bins): - """ - This method resamples the specified wind parameter column using the - specified bin center values. The frequency values are adjusted - accordingly. - - Args: - df (pandas.DataFrame): Wind rose DataFrame containing at least the - following columns as well as *col*: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - - col (str): The name of the column to resample. - bins (np.array): List of new bin center values for the specified - column. - - Returns: - pandas.DataFrame: Wind rose DataFrame with the resampled wind - parameter bins and frequencies containing at least the following - columns as well as *col*: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - new wind conditions in the other columns. - """ - # Make a copy of incoming dataframe - df = df.copy(deep=True) - - # Cut into bins, make first and last bins extend to -/+ infinity - var_edges = np.append(0.5 * (bins[1:] + bins[:-1]), np.inf) - var_edges = np.append(-np.inf, var_edges) - df[col] = pd.cut(df[col], var_edges, labels=bins) - - # Regroup - df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() - - # Fill nans - df = df.fillna(0) - - # Reset the index - df = df.reset_index() - - # Set to float - for c in [c for c in df.columns if c != "freq_val"]: - df[c] = df[c].astype(float) - - return df - - def internal_resample_column(self, col, bins): - """ - Internal method for resampling column into desired bins. The frequency - values are adjusted accordingly. Modifies data within WindRose object - without explicit return. - - TODO: make a private method - - Args: - col (str): Name of column to resample. - bins (np.array): Vector of bins for the WindRose column. - """ - # Update internal data frame - self.df = self.resample_column(self.df, col, bins) - - def resample_average_ws_by_wd(self, df): - """ - This method calculates the mean wind speed for each wind direction bin - and resamples the wind rose, resulting in a single mean wind speed per - wind direction bin. The frequency values are adjusted accordingly. - - Args: - df (pandas.DataFrame): Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - - Returns: - pandas.DataFrame: Wind rose DataFrame with the resampled wind speed - bins and frequencies containing at least the following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - The average wind speed for each wind - direction bin (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - new wind conditions in the other columns. - """ - # Make a copy of incoming dataframe - df = df.copy(deep=True) - - ws_avg = [] - - for val in df.wd.unique(): - ws_avg.append( - np.array(df.loc[df["wd"] == val]["ws"] * df.loc[df["wd"] == val]["freq_val"]).sum() - / df.loc[df["wd"] == val]["freq_val"].sum() - ) - - # Regroup - df = df.groupby("wd").sum() - - df["ws"] = ws_avg - - # Reset the index - df = df.reset_index() - - # Set to float - df["ws"] = df.ws.astype(float) - df["wd"] = df.wd.astype(float) - - return df - - def internal_resample_average_ws_by_wd(self, wd=np.arange(0, 360, 5.0)): - """ - This internal method calculates the mean wind speed for each specified - wind direction bin and resamples the wind rose, resulting in a single - mean wind speed per wind direction bin. The frequency values are - adjusted accordingly. - - TODO: make an internal method - - Args: - wd (np.arange, optional): Wind direction bin centers (deg). - Defaults to np.arange(0, 360, 5.). - - Returns: - pandas.DataFrame: Wind rose DataFrame with the resampled wind speed - bins and frequencies containing at least the following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - The average wind speed for each wind - direction bin (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - new wind conditions in the other columns. - """ - # Update ws and wd binning - self.wd = wd - self.num_wd = len(wd) - self.wd_step = wd[1] - wd[0] - - # Update internal data frame - self.df = self.resample_average_ws_by_wd(self.df) - - def interpolate( - self, - wind_directions: np.ndarray, - wind_speeds: np.ndarray, - mirror_0_to_360=True, - fill_value=0.0, - method="linear", - ): - """ - This method returns a linear interpolant that will return the occurrence - frequency for any given wind direction and wind speed combination(s). - This can be particularly useful when evaluating the wind rose at a - higher frequency than the input data is provided. - - Args: - wind_directions (np.ndarray): One or multi-dimensional array containing - the wind direction values at which the wind rose frequency of occurrence - should be evaluated. - wind_speeds (np.ndarray): One or multi-dimensional array containing - the wind speed values at which the wind rose frequency of occurrence - should be evaluated. - mirror_0_to_360 (bool, optional): This function copies the wind rose - frequency values from 0 deg to 360 deg. This can be useful when, for example, - the wind rose is only calculated until 357 deg but then interpolant is - requesting values at 359 deg. Defaults to True. - fill_value (float, optional): Fill value for the interpolant when - interpolating values outside of the data region. Defaults to 0.0. - method (str, optional): The interpolation method. Options are 'linear' and - 'nearest'. Recommended usage is 'linear'. Defaults to 'linear'. - - Returns: - scipy.interpolate.LinearNDInterpolant: Linear interpolant for the - wind rose currently available in the class (self.df). - - Example: - wr = wind_rose.WindRose() - wr.make_wind_rose_from_user_data(...) - freq_floris = wr.interpolate(floris_wind_direction_grid, floris_wind_speed_grid) - """ - if method == "linear": - interpolator = LinearNDInterpolator - elif method == "nearest": - interpolator = NearestNDInterpolator - else: - UserWarning("Unknown interpolation method: '{:s}'".format(method)) - - # Load windrose information from self - df = self.df.copy() - - if mirror_0_to_360: - # Copy values from 0 deg over to 360 deg - df_copy = df[df["wd"] == 0.0].copy() - df_copy["wd"] = 360.0 - df = pd.concat([df, df_copy], axis=0) - - interp = interpolator(points=df[["wd", "ws"]], values=df["freq_val"], fill_value=fill_value) - return interp(wind_directions, wind_speeds) - - def weibull(self, x, k=2.5, lam=8.0): - """ - This method returns a Weibull distribution corresponding to the input - data array (typically wind speed) using the specified Weibull - parameters. - - Args: - x (np.array): List of input data (typically binned wind speed - observations). - k (float, optional): Weibull shape parameter. Defaults to 2.5. - lam (float, optional): Weibull scale parameter. Defaults to 8.0. - - Returns: - np.array: Weibull distribution probabilities corresponding to - values in the input array. - """ - return (k / lam) * (x / lam) ** (k - 1) * np.exp(-((x / lam) ** k)) - - def make_wind_rose_from_weibull(self, wd=np.arange(0, 360, 5.0), ws=np.arange(0, 26, 1.0)): - """ - Populate WindRose object with an example wind rose with wind speed - frequencies given by a Weibull distribution. The wind direction - frequencies are initialized according to an example distribution. - - Args: - wd (np.array, optional): Wind direciton bin centers (deg). Defaults - to np.arange(0, 360, 5.). - ws (np.array, optional): Wind speed bin centers (m/s). Defaults to - np.arange(0, 26, 1.). - - Returns: - pandas.DataFrame: Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - """ - # Use an assumed wind-direction for dir frequency - wind_dir = [ - 0, - 22.5, - 45, - 67.5, - 90, - 112.5, - 135, - 157.5, - 180, - 202.5, - 225, - 247.5, - 270, - 292.5, - 315, - 337.5, - ] - freq_dir = [ - 0.064, - 0.04, - 0.038, - 0.036, - 0.045, - 0.05, - 0.07, - 0.08, - 0.11, - 0.08, - 0.05, - 0.036, - 0.048, - 0.058, - 0.095, - 0.10, - ] - - freq_wd = np.interp(wd, wind_dir, freq_dir) - freq_ws = self.weibull(ws) - - freq_tot = np.zeros(len(wd) * len(ws)) - wd_tot = np.zeros(len(wd) * len(ws)) - ws_tot = np.zeros(len(wd) * len(ws)) - - count = 0 - for i in range(len(wd)): - for j in range(len(ws)): - wd_tot[count] = wd[i] - ws_tot[count] = ws[j] - - freq_tot[count] = freq_wd[i] * freq_ws[j] - count = count + 1 - - # renormalize - freq_tot = freq_tot / np.sum(freq_tot) - - # Load the wind toolkit data into a dataframe - df = pd.DataFrame() - - # Start by simply round and wrapping the wind direction and wind speed - # columns - df["wd"] = wd_tot - df["ws"] = ws_tot - - # Now group up - df["freq_val"] = freq_tot - - # Save the df at this point - self.df = df - # TODO is there a reason self.df is updated AND returned? - return self.df - - def make_wind_rose_from_user_data( - self, wd_raw, ws_raw, *args, wd=np.arange(0, 360, 5.0), ws=np.arange(0, 26, 1.0) - ): - """ - This method populates the WindRose object given user-specified - observations of wind direction, wind speed, and additional optional - variables. The wind parameters are binned and the frequencies of - occurance of each binned wind condition combination are calculated. - - Args: - wd_raw (array-like): An array-like list of all wind direction - observations used to calculate the normalized frequencies (deg). - ws_raw (array-like): An array-like list of all wind speed - observations used to calculate the normalized frequencies (m/s). - *args: Variable length argument list consisting of a sequence of - the following alternating arguments: - - - string - Name of additional wind parameters to include in - wind rose. - - array-like - Values of the additional wind parameters used - to calculate the frequencies of occurance - - np.array - Bin center values for binning the additional - wind parameters. - - wd (np.array, optional): Wind direction bin centers (deg). Defaults - to np.arange(0, 360, 5.). - ws (np.array, optional): Wind speed bin limits (m/s). Defaults to - np.arange(0, 26, 1.). - - Returns: - pandas.DataFrame: Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - """ - df = pd.DataFrame() - - # convert inputs to np.array - wd_raw = np.array(wd_raw) - ws_raw = np.array(ws_raw) - - # Start by simply round and wrapping the wind direction and wind speed - # columns - df["wd"] = geo.wrap_360(wd_raw.round()) - df["ws"] = ws_raw.round() - - # Loop through *args and assign new dataframe columns after cutting - # into possibly irregularly-spaced bins - for in_var in range(0, len(args), 3): - df[args[in_var]] = np.array(args[in_var + 1]) - - # Cut into bins, make first and last bins extend to -/+ infinity - var_edges = np.append(0.5 * (args[in_var + 2][1:] + args[in_var + 2][:-1]), np.inf) - var_edges = np.append(-np.inf, var_edges) - df[args[in_var]] = pd.cut(df[args[in_var]], var_edges, labels=args[in_var + 2]) - - # Now group up - df["freq_val"] = 1.0 - df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() - df["freq_val"] = df.freq_val.astype(float) / df.freq_val.sum() - df = df.reset_index() - - # Save the df at this point - self.df = df - - # Resample onto the provided wind speed and wind direction binnings - self.internal_resample_wind_speed(ws=ws) - self.internal_resample_wind_direction(wd=wd) - - return self.df - - def read_wind_rose_csv(self, filename): - # Read in the csv - self.df = pd.read_csv(filename) - - # Renormalize the frequency column - self.df["freq_val"] = self.df["freq_val"] / self.df["freq_val"].sum() - - # Call the resample function in order to set all the internal variables - self.internal_resample_wind_speed(ws=self.df.ws.unique()) - self.internal_resample_wind_direction(wd=self.df.wd.unique()) - - def make_wind_rose_from_user_dist( - self, - wd_raw, - ws_raw, - freq_val, - *args, - wd=np.arange(0, 360, 5.0), - ws=np.arange(0, 26, 1.0), - ): - """ - This method populates the WindRose object given user-specified - combinations of wind direction, wind speed, additional optional - variables, and the corresponding frequencies of occurance. The wind - parameters are binned using the specified wind parameter bin center - values and the corresponding frequencies of occrance are calculated. - - Args: - wd_raw (array-like): An array-like list of wind directions - corresponding to the specified frequencies of occurance (deg). - wd_raw (array-like): An array-like list of wind speeds - corresponding to the specified frequencies of occurance (m/s). - freq_val (array-like): An array-like list of normalized frequencies - corresponding to the provided wind parameter combinations. - *args: Variable length argument list consisting of a sequence of - the following alternating arguments: - - - string - Name of additional wind parameters to include in - wind rose. - - array-like - Values of the additional wind parameters - corresponding to the specified frequencies of occurance. - - np.array - Bin center values for binning the additional - wind parameters. - - wd (np.array, optional): Wind direction bin centers (deg). Defaults - to np.arange(0, 360, 5.). - ws (np.array, optional): Wind speed bin centers (m/s). Defaults to - np.arange(0, 26, 1.). - - Returns: - pandas.DataFrame: Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - """ - df = pd.DataFrame() - - # convert inputs to np.array - wd_raw = np.array(wd_raw) - ws_raw = np.array(ws_raw) - - # Start by simply wrapping the wind direction column - df["wd"] = geo.wrap_360(wd_raw) - df["ws"] = ws_raw - - # Loop through *args and assign new dataframe columns - for in_var in range(0, len(args), 3): - df[args[in_var]] = np.array(args[in_var + 1]) - - # Assign frequency column - df["freq_val"] = np.array(freq_val) - df["freq_val"] = df["freq_val"] / df["freq_val"].sum() - - # Save the df at this point - self.df = df - - # Resample onto the provided wind variable binnings - self.internal_resample_wind_speed(ws=ws) - self.internal_resample_wind_direction(wd=wd) - - # Loop through *args and resample using provided binnings - for in_var in range(0, len(args), 3): - self.internal_resample_column(args[in_var], args[in_var + 2]) - - return self.df - - def parse_wind_toolkit_folder( - self, - folder_name, - wd=np.arange(0, 360, 5.0), - ws=np.arange(0, 26, 1.0), - limit_month=None, - ): - """ - This method populates the WindRose object given raw wind direction and - wind speed data saved in csv files downloaded from the WIND Toolkit - application (see https://www.nrel.gov/grid/wind-toolkit.html for more - information). The wind parameters are binned using the specified wind - parameter bin center values and the corresponding frequencies of - occurance are calculated. - - Args: - folder_name (str): Path to the folder containing the WIND Toolkit - data files. - wd (np.array, optional): Wind direction bin centers (deg). Defaults - to np.arange(0, 360, 5.). - ws (np.array, optional): Wind speed bin centers (m/s). Defaults to - np.arange(0, 26, 1.). - limit_month (list, optional): List of ints of month(s) (e.g., 1, 2 - 3...) to consider when calculating the wind condition - frequencies. If none are specified, all months will be used. - Defaults to None. - - Returns: - pandas.DataFrame: Wind rose DataFrame containing the following - columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - """ - # Load the wind toolkit data into a dataframe - df = self.load_wind_toolkit_folder(folder_name, limit_month=limit_month) - - # Start by simply round and wrapping the wind direction and wind speed - # columns - df["wd"] = geo.wrap_360(df.wd.round()) - df["ws"] = geo.wrap_360(df.ws.round()) - - # Now group up - df["freq_val"] = 1.0 - df = df.groupby(["ws", "wd"]).sum() - df["freq_val"] = df.freq_val.astype(float) / df.freq_val.sum() - df = df.reset_index() - - # Save the df at this point - self.df = df - - # Resample onto the provided wind speed and wind direction binnings - self.internal_resample_wind_speed(ws=ws) - self.internal_resample_wind_direction(wd=wd) - - return self.df - - def load_wind_toolkit_folder(self, folder_name, limit_month=None): - """ - This method imports raw wind direction and wind speed data saved in csv - files in the specified folder downloaded from the WIND Toolkit - application (see https://www.nrel.gov/grid/wind-toolkit.html for more - information). - - TODO: make private method? - - Args: - folder_name (str): Path to the folder containing the WIND Toolkit - csv data files. - limit_month (list, optional): List of ints of month(s) (e.g., 1, 2, - 3...) to consider when calculating the wind condition - frequencies. If none are specified, all months will be used. - Defaults to None. - - Returns: - pandas.DataFrame: DataFrame containing the following columns: - - - **wd** (*float*) - Raw wind direction data (deg). - - **ws** (*float*) - Raw wind speed data (m/s). - """ - file_list = os.listdir(folder_name) - file_list = [os.path.join(folder_name, f) for f in file_list if ".csv" in f] - - df = pd.DataFrame() - for f_idx, f in enumerate(file_list): - print("%d of %d: %s" % (f_idx, len(file_list), f)) - df_temp = self.load_wind_toolkit_file(f, limit_month=limit_month) - df = df.append(df_temp) - - return df - - def load_wind_toolkit_file(self, filename, limit_month=None): - """ - This method imports raw wind direction and wind speed data saved in the - specified csv file downloaded from the WIND Toolkit application (see - https://www.nrel.gov/grid/wind-toolkit.html for more information). - - TODO: make private method? - - Args: - filename (str): Path to the WIND Toolkit csv file. - limit_month (list, optional): List of ints of month(s) (e.g., 1, 2, - 3...) to consider when calculating the wind condition - frequencies. If none are specified, all months will be used. - Defaults to None. - - Returns: - pandas.DataFrame: DataFrame containing the following columns with - data from the WIND Toolkit file: - - - **wd** (*float*) - Raw wind direction data (deg). - - **ws** (*float*) - Raw wind speed data (m/s). - """ - df = pd.read_csv(filename, header=3, sep=",") - - # If asked to limit to particular months - if limit_month is not None: - df = df[df.Month.isin(limit_month)] - - # Save just what I want - speed_column = [c for c in df.columns if "speed" in c][0] - direction_column = [c for c in df.columns if "direction" in c][0] - df = df.rename(index=str, columns={speed_column: "ws", direction_column: "wd"})[ - ["wd", "ws"] - ] - - return df - - def import_from_wind_toolkit_hsds( - self, - lat, - lon, - ht=100, - wd=np.arange(0, 360, 5.0), - ws=np.arange(0, 26, 1.0), - include_ti=False, - limit_month=None, - limit_hour=None, - st_date=None, - en_date=None, - ): - """ - This method populates the WindRose object using wind data from the WIND - Toolkit dataset (https://www.nrel.gov/grid/wind-toolkit.html) for the - specified lat/long coordinate in the continental US. The wind data - are obtained from the WIND Toolkit dataset using the HSDS service (see - https://github.com/NREL/hsds-examples). The wind data returned is - obtained from the nearest 2km x 2km grid point to the input - coordinate and is limited to the years 2007-2013. The wind parameters - are binned using the specified wind parameter bin center values and the - corresponding frequencies of occrance are calculated. - - Requires h5pyd package, which can be installed using: - pip install --user git+http://github.com/HDFGroup/h5pyd.git - - Then, make a configuration file at ~/.hscfg containing: - - hs_endpoint = https://developer.nrel.gov/api/hsds - - hs_username = None - - hs_password = None - - hs_api_key = 3K3JQbjZmWctY0xmIfSYvYgtIcM3CN0cb1Y2w9bf - - The example API key above is for demonstation and is - rate-limited per IP. To get your own API key, visit - https://developer.nrel.gov/signup/. - - More information can be found at: https://github.com/NREL/hsds-examples. - - Args: - lat (float): Latitude in degrees. - lon (float): Longitude in degrees. - ht (int, optional): The height above ground where wind - information is obtained (m). Defaults to 100. - wd (np.array, optional): Wind direction bin centers (deg). Defaults - to np.arange(0, 360, 5.). - ws (np.array, optional): Wind speed bin centers (m/s). Defaults to - np.arange(0, 26, 1.). - include_ti (bool, optional): Determines whether turbulence - intensity is included as an additional parameter. If True, TI - is added as an additional wind rose variable, estimated based - on the Obukhov length from WIND Toolkit. Defaults to False. - limit_month (list, optional): List of ints of month(s) (e.g., 1, 2, - 3...) to consider when calculating the wind condition - frequencies. If none are specified, all months will be used. - Defaults to None. - limit_hour (list, optional): List of ints of hour(s) (e.g., 0, 1, - ... 23) to consider when calculating the wind condition - frequencies. If none are specified, all hours will be used. - Defaults to None. - st_date (str, optional): The start date to consider when creating - the wind rose, formatted as 'MM-DD-YYYY'. If not specified data - beginning in 2007 will be used. Defaults to None. - en_date (str, optional): The end date to consider when creating - the wind rose, formatted as 'MM-DD-YYYY'. If not specified data - through 2013 will be used. Defaults to None. - - Returns: - pandas.DataFrame: Wind rose DataFrame containing at least the - following columns: - - - **wd** (*float*) - Wind direction bin center values (deg). - - **ws** (*float*) - Wind speed bin center values (m/s). - - **freq_val** (*float*) - The frequency of occurance of the - wind conditions in the other columns. - """ - # Check inputs - - # Array of hub height data avaliable on Toolkit - h_range = [10, 40, 60, 80, 100, 120, 140, 160, 200] - - if st_date is not None: - if dateutil.parser.parse(st_date) > dateutil.parser.parse("12-13-2013 23:00"): - print("Error, invalid date range. Valid range: 01-01-2007 - " + "12/31/2013") - return None - - if en_date is not None: - if dateutil.parser.parse(en_date) < dateutil.parser.parse("01-01-2007 00:00"): - print("Error, invalid date range. Valid range: 01-01-2007 - " + "12/31/2013") - return None - - if h_range[0] > ht: - print( - "Error, height is not in the range of avaliable " - + "WindToolKit data. Minimum height = 10m" - ) - return None - - if h_range[-1] < ht: - print( - "Error, height is not in the range of avaliable " - + "WindToolKit data. Maxiumum height = 200m" - ) - return None - - # Load wind speeds and directions from WimdToolkit - - # Case for turbine height (ht) matching discrete avaliable height - # (h_range) - if ht in h_range: - d = self.load_wind_toolkit_hsds( - lat, - lon, - ht, - include_ti=include_ti, - limit_month=limit_month, - limit_hour=limit_hour, - st_date=st_date, - en_date=en_date, - ) - - ws_new = d["ws"] - wd_new = d["wd"] - if include_ti: - ti_new = d["ti"] - - # Case for ht not matching discete height - else: - h_range_up = next(x[0] for x in enumerate(h_range) if x[1] > ht) - h_range_low = h_range_up - 1 - h_up = h_range[h_range_up] - h_low = h_range[h_range_low] - - # Load data for boundary cases of ht - d_low = self.load_wind_toolkit_hsds( - lat, - lon, - h_low, - include_ti=include_ti, - limit_month=limit_month, - limit_hour=limit_hour, - st_date=st_date, - en_date=en_date, - ) - - d_up = self.load_wind_toolkit_hsds( - lat, - lon, - h_up, - include_ti=include_ti, - limit_month=limit_month, - limit_hour=limit_hour, - st_date=st_date, - en_date=en_date, - ) - - # Wind Speed interpolation - ws_low = d_low["ws"] - ws_high = d_up["ws"] - - ws_new = np.array(ws_low) * (1 - ((ht - h_low) / (h_up - h_low))) + np.array( - ws_high - ) * ((ht - h_low) / (h_up - h_low)) - - # Wind Direction interpolation using Circular Mean method - wd_low = d_low["wd"] - wd_high = d_up["wd"] - - sin0 = np.sin(np.array(wd_low) * (np.pi / 180)) - cos0 = np.cos(np.array(wd_low) * (np.pi / 180)) - sin1 = np.sin(np.array(wd_high) * (np.pi / 180)) - cos1 = np.cos(np.array(wd_high) * (np.pi / 180)) - - sin_wd = sin0 * (1 - ((ht - h_low) / (h_up - h_low))) + sin1 * ( - (ht - h_low) / (h_up - h_low) - ) - cos_wd = cos0 * (1 - ((ht - h_low) / (h_up - h_low))) + cos1 * ( - (ht - h_low) / (h_up - h_low) - ) - - # Interpolated wind direction - wd_new = 180 / np.pi * np.arctan2(sin_wd, cos_wd) - - # TI is independent of height - if include_ti: - ti_new = d_up["ti"] - - # Create a dataframe named df - if include_ti: - df = pd.DataFrame({"ws": ws_new, "wd": wd_new, "ti": ti_new}) - else: - df = pd.DataFrame({"ws": ws_new, "wd": wd_new}) - - # Start by simply round and wrapping the wind direction and wind speed - # columns - df["wd"] = geo.wrap_360(df.wd.round()) - df["ws"] = df.ws.round() - - # Now group up - df["freq_val"] = 1.0 - df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() - df["freq_val"] = df.freq_val.astype(float) / df.freq_val.sum() - df = df.reset_index() - - # Save the df at this point - self.df = df - - # Resample onto the provided wind speed and wind direction binnings - self.internal_resample_wind_speed(ws=ws) - self.internal_resample_wind_direction(wd=wd) - - return self.df - - def load_wind_toolkit_hsds( - self, - lat, - lon, - ht=100, - include_ti=False, - limit_month=None, - limit_hour=None, - st_date=None, - en_date=None, - ): - """ - This method returns a pandas DataFrame containing hourly wind speed, - wind direction, and optionally estimated turbulence intensity data - using wind data from the WIND Toolkit dataset - (https://www.nrel.gov/grid/wind-toolkit.html) for the specified - lat/long coordinate in the continental US. The wind data are obtained - from the WIND Toolkit dataset using the HSDS service - (see https://github.com/NREL/hsds-examples). The wind data returned is - obtained from the nearest 2km x 2km grid point to the input coordinate - and is limited to the years 2007-2013. - - TODO: make private method? - - Args: - lat (float): Latitude in degrees. - lon (float): Longitude in degrees - ht (int, optional): The height above ground where wind - information is obtained (m). Defaults to 100. - include_ti (bool, optional): Determines whether turbulence - intensity is included as an additional parameter. If True, TI - is added as an additional wind rose variable, estimated based - on the Obukhov length from WIND Toolkit. Defaults to False. - limit_month (list, optional): List of ints of month(s) (e.g., 1, 2, - 3...) to consider when calculating the wind condition - frequencies. If none are specified, all months will be used. - Defaults to None. - limit_hour (list, optional): List of ints of hour(s) (e.g., 0, 1, - ... 23) to consider when calculating the wind condition - frequencies. If none are specified, all hours will be used. - Defaults to None. - st_date (str, optional): The start date to consider, formatted as - 'MM-DD-YYYY'. If not specified data beginning in 2007 will be - used. Defaults to None. - en_date (str, optional): The end date to consider, formatted as - 'MM-DD-YYYY'. If not specified data through 2013 will be used. - Defaults to None. - - Returns: - pandas.DataFrame: DataFrame containing the following columns(abd - optionally turbulence intensity) with hourly data from WIND Toolkit: - - - **wd** (*float*) - Raw wind direction data (deg). - - **ws** (*float*) - Raw wind speed data (m/s). - """ - import h5pyd - - # Open the wind data "file" - # server endpoint, username, password is found via a config file - f = h5pyd.File("/nrel/wtk-us.h5", "r") - - # assign wind direction, wind speed, optional ti, and time datasets for - # the desired height - wd_dset = f["winddirection_" + str(ht) + "m"] - ws_dset = f["windspeed_" + str(ht) + "m"] - if include_ti: - obkv_dset = f["inversemoninobukhovlength_2m"] - dt = f["datetime"] - dt = pd.DataFrame({"datetime": dt[:]}, index=range(0, dt.shape[0])) - dt["datetime"] = dt["datetime"].apply(dateutil.parser.parse) - - # find dataset indices from lat/long - Location_idx = self.indices_for_coord(f, lat, lon) - - # check if in bounds - if ( - (Location_idx[0] < 0) - | (Location_idx[0] >= wd_dset.shape[1]) - | (Location_idx[1] < 0) - | (Location_idx[1] >= wd_dset.shape[2]) - ): - print( - "Error, coordinates out of bounds. WIND Toolkit database " - + "covers the continental United States." - ) - return None - - # create dataframe with wind direction and wind speed - df = pd.DataFrame() - df["wd"] = wd_dset[:, Location_idx[0], Location_idx[1]] - df["ws"] = ws_dset[:, Location_idx[0], Location_idx[1]] - if include_ti: - L = self.obkv_dset_to_L(obkv_dset, Location_idx) - ti = self.ti_calculator_IU2(L) - df["ti"] = ti - df["datetime"] = dt["datetime"] - - # limit dates if start and end dates are provided - if st_date is not None: - df = df[df.datetime >= st_date] - - if en_date is not None: - df = df[df.datetime < en_date] - - # limit to certain months if specified - if limit_month is not None: - df["month"] = df["datetime"].map(lambda x: x.month) - df = df[df.month.isin(limit_month)] - if limit_hour is not None: - df["hour"] = df["datetime"].map(lambda x: x.hour) - df = df[df.hour.isin(limit_hour)] - if include_ti: - df = df[["wd", "ws", "ti"]] - else: - df = df[["wd", "ws"]] - - return df - - def obkv_dset_to_L(self, obkv_dset, Location_idx): - """ - This function returns an array containing hourly Obukhov lengths from - the WIND Toolkit dataset for the specified Lat/Lon coordinate indices. - - Args: - obkv_dset (np.ndarray): Dataset for Obukhov lengths from WIND - Toolkit. - Location_idx (tuple): A tuple containing the Lat/Lon coordinate - indices of interest in the Obukhov length dataset. - - Returns: - np.array: An array containing Obukhov lengths for each time index - in the Wind Toolkit dataset (m). - """ - linv = obkv_dset[:, Location_idx[0], Location_idx[1]] - # avoid divide by zero - linv[linv == 0.0] = 0.0003 - L = 1 / linv - return L - - def ti_calculator_IU2(self, L): - """ - This function estimates the turbulence intensity corresponding to each - Obukhov length value in the input list using the relationship between - Obukhov length bins and TI given in the I_U2SODAR column in Table 2 of - :cite:`wr-wharton2010assessing`. - - Args: - L (iterable): A list of Obukhov Length values (m). - - Returns: - list: A list of turbulence intensity values expressed as fractions. - """ - ti_set = [] - for i in L: - # Strongly Stable - if 0 < i < 100: - TI = 0.04 # paper says < 8%, so using 4% - # Stable - elif 100 < i < 600: - TI = 0.09 - # Neutral - elif abs(i) > 600: - TI = 0.115 - # Convective - elif -600 < i < -50: - TI = 0.165 - # Strongly Convective - elif -50 < i < 0: - # no upper bound given, so using the lowest - # value from the paper for this stability bin - TI = 0.2 - ti_set.append(TI) - return ti_set - - def indices_for_coord(self, f, lat_index, lon_index): - """ - This method finds the nearest x/y indices of the WIND Toolkit dataset - for a given lat/lon coordinate in the continental US. Rather than - fetching the entire coordinates database, which is 500+ MB, this uses - the Proj4 library to find a nearby point and then converts to x/y - indices. - - **Note**: This method is obtained directly from: - https://github.com/NREL/hsds-examples/blob/master/notebooks/01_WTK_introduction.ipynb, - where it is called "indicesForCoord." - - Args: - f (h5pyd.File): A HDF5 "file" used to access the WIND Toolkit data. - lat_index (float): Latitude coordinate for which dataset indices - are to be found (degrees). - lon_index (float): Longitude coordinate for which dataset indices - are to be found (degrees). - - Returns: - tuple: A tuple containing the Lat/Lon coordinate indices of - interest in the WIND Toolkit dataset. - """ - dset_coords = f["coordinates"] - projstring = """+proj=lcc +lat_1=30 +lat_2=60 - +lat_0=38.47240422490422 +lon_0=-96.0 - +x_0=0 +y_0=0 +ellps=sphere - +units=m +no_defs """ - projectLcc = Proj(projstring) - origin_ll = reversed(dset_coords[0][0]) # Grab origin directly from database - origin = projectLcc(*origin_ll) - - coords = (lon_index, lat_index) - coords = projectLcc(*coords) - delta = np.subtract(coords, origin) - ij = [int(round(x / 2000)) for x in delta] - return tuple(reversed(ij)) - - def plot_wind_speed_all(self, ax=None, label=None): - """ - This method plots the wind speed frequency distribution of the WindRose - object averaged across all wind directions. If no axis is provided, a - new one is created. - - Args: - ax (:py:class:`matplotlib.pyplot.axes`, optional): Figure axes on - which data should be plotted. Defaults to None. - """ - if ax is None: - _, ax = plt.subplots() - - df_plot = self.df.groupby("ws").sum() - ax.plot(self.ws, df_plot.freq_val, label=label) - - def plot_wind_speed_by_direction(self, dirs, ax=None): - """ - This method plots the wind speed frequency distribution of the WindRose - object for each specified wind direction bin center. The wind - directions are resampled using the specified bin centers and the - frequencies of occurance of the wind conditions are modified - accordingly. If no axis is provided, a new one is created. - - Args: - dirs (np.array): A list of wind direction bin centers for which - wind speed distributions are plotted (deg). - ax (:py:class:`matplotlib.pyplot.axes`, optional): Figure axes on - which data should be plotted. Defaults to None. - """ - # Get a downsampled frame - df_plot = self.resample_wind_direction(self.df, wd=dirs) - - if ax is None: - _, ax = plt.subplots() - - for wd in dirs: - df_plot_sub = df_plot[df_plot.wd == wd] - ax.plot(df_plot_sub.ws, df_plot_sub["freq_val"], label=wd) - ax.legend() - - def plot_wind_rose( - self, - ax=None, - color_map="viridis_r", - ws_right_edges=np.array([5, 10, 15, 20, 25]), - wd_bins=np.arange(0, 360, 15.0), - legend_kwargs={}, - ): - """ - This method creates a wind rose plot showing the frequency of occurance - of the specified wind direction and wind speed bins. If no axis is - provided, a new one is created. - - **Note**: Based on code provided by Patrick Murphy from the University - of Colorado Boulder. - - Args: - ax (:py:class:`matplotlib.pyplot.axes`, optional): The figure axes - on which the wind rose is plotted. Defaults to None. - color_map (str, optional): Colormap to use. Defaults to 'viridis_r'. - ws_right_edges (np.array, optional): The upper bounds of the wind - speed bins (m/s). The first bin begins at 0. Defaults to - np.array([5, 10, 15, 20, 25]). - wd_bins (np.array, optional): The wind direction bin centers used - for plotting (deg). Defaults to np.arange(0, 360, 15.). - legend_kwargs (dict, optional): Keyword arguments to be passed to - ax.legend(). - - Returns: - :py:class:`matplotlib.pyplot.axes`: A figure axes object containing - the plotted wind rose. - """ - # Resample data onto bins - df_plot = self.resample_wind_direction(self.df, wd=wd_bins) - - # Make labels for wind speed based on edges - ws_step = ws_right_edges[1] - ws_right_edges[0] - ws_labels = ["%d-%d m/s" % (w - ws_step, w) for w in ws_right_edges] - - # Grab the wd_step - wd_step = wd_bins[1] - wd_bins[0] - - # Set up figure - if ax is None: - _, ax = plt.subplots(subplot_kw={"polar": True}) - - # Get a color array - color_array = cm.get_cmap(color_map, len(ws_right_edges)) - - for wd in wd_bins: - rects = [] - df_plot_sub = df_plot[df_plot.wd == wd] - for ws_idx, ws in enumerate(ws_right_edges[::-1]): - plot_val = df_plot_sub[ - df_plot_sub.ws <= ws - ].freq_val.sum() # Get the sum of frequency up to this wind speed - rects.append( - ax.bar( - np.radians(wd), - plot_val, - width=0.9 * np.radians(wd_step), - color=color_array(ws_idx), - edgecolor="k", - ) - ) - # break - - # Configure the plot - ax.legend(reversed(rects), ws_labels, **legend_kwargs) - ax.set_theta_direction(-1) - ax.set_theta_offset(np.pi / 2.0) - ax.set_theta_zero_location("N") - ax.set_xticks(np.arange(0, 2 * np.pi, np.pi / 4)) - ax.set_xticklabels(["N", "NE", "E", "SE", "S", "SW", "W", "NW"]) - - return ax - - def plot_wind_rose_ti( - self, - ax=None, - color_map="viridis_r", - ti_right_edges=np.array([0.06, 0.1, 0.14, 0.18, 0.22]), - wd_bins=np.arange(0, 360, 15.0), - ): - """ - This method creates a wind rose plot showing the frequency of occurance - of the specified wind direction and turbulence intensity bins. This - requires turbulence intensity to already be included as a parameter in - the wind rose. If no axis is provided,a new one is created. - - **Note**: Based on code provided by Patrick Murphy from the University - of Colorado Boulder. - - Args: - ax (:py:class:`matplotlib.pyplot.axes`, optional): The figure axes - on which the wind rose is plotted. Defaults to None. - color_map (str, optional): Colormap to use. Defaults to 'viridis_r'. - ti_right_edges (np.array, optional): The upper bounds of the - turbulence intensity bins. The first bin begins at 0. Defaults - to np.array([0.06, 0.1, 0.14, 0.18,0.22]). - wd_bins (np.array, optional): The wind direction bin centers used - for plotting (deg). Defaults to np.arange(0, 360, 15.). - - Returns: - :py:class:`matplotlib.pyplot.axes`: A figure axes object containing - the plotted wind rose. - """ - - # Resample data onto bins - df_plot = self.resample_wind_direction(self.df, wd=wd_bins) - - # Make labels for TI based on edges - ti_step = ti_right_edges[1] - ti_right_edges[0] - ti_labels = ["%.2f-%.2f " % (w - ti_step, w) for w in ti_right_edges] - - # Grab the wd_step - wd_step = wd_bins[1] - wd_bins[0] - - # Set up figure - if ax is None: - _, ax = plt.subplots(subplot_kw={"polar": True}) - - # Get a color array - color_array = cm.get_cmap(color_map, len(ti_right_edges)) - - for wd in wd_bins: - rects = [] - df_plot_sub = df_plot[df_plot.wd == wd] - for ti_idx, ti in enumerate(ti_right_edges[::-1]): - plot_val = df_plot_sub[ - df_plot_sub.ti <= ti - ].freq_val.sum() # Get the sum of frequency up to this wind speed - rects.append( - ax.bar( - np.radians(wd), - plot_val, - width=0.9 * np.radians(wd_step), - color=color_array(ti_idx), - edgecolor="k", - ) - ) - - # Configure the plot - ax.legend(reversed(rects), ti_labels, loc="lower right", title="TI") - ax.set_theta_direction(-1) - ax.set_theta_offset(np.pi / 2.0) - ax.set_theta_zero_location("N") - ax.set_xticks(np.arange(0, 2 * np.pi, np.pi / 4)) - ax.set_xticklabels(["N", "NE", "E", "SE", "S", "SW", "W", "NW"]) - - return ax - - def plot_ti_ws(self, ax=None, ws_bins=np.arange(0, 26, 1.0)): - """ - This method plots the wind speed frequency distribution of the WindRose - object for each turbulence intensity bin. The wind speeds are resampled - using the specified bin centers and the frequencies of occurance of the - wind conditions are modified accordingly. This method assumes there are - five TI bins. If no axis is provided, a new one is created. - - Args: - ax (:py:class:`matplotlib.pyplot.axes`, optional): Figure axes on - which data should be plotted. Defaults to None. - ws_bins (np.array, optional): A list of wind speed bin centers on - which the wind speeds are resampled before plotting (m/s). - Defaults to np.arange(0, 26, 1.). - - Returns: - :py:class:`matplotlib.pyplot.axes`: A figure axes object containing - the plotted wind speed distributions. - """ - - # Resample data onto bins - df_plot = self.resample_wind_speed(self.df, ws=ws_bins) - - df_plot = df_plot.groupby(["ws", "ti"]).sum() - df_plot = df_plot.reset_index() - - if ax is None: - _, ax = plt.subplots(figsize=(10, 7)) - - tis = df_plot["ti"].drop_duplicates() - margin_bottom = np.zeros(len(df_plot["ws"].drop_duplicates())) - colors = ["#1e5631", "#a4de02", "#76ba1b", "#4c9a2a", "#acdf87"] - - for num, ti in enumerate(tis): - values = list(df_plot[df_plot["ti"] == ti].loc[:, "freq_val"]) - - df_plot[df_plot["ti"] == ti].plot.bar( - x="ws", - y="freq_val", - ax=ax, - bottom=margin_bottom, - color=colors[num], - label=ti, - ) - - margin_bottom += values - - plt.title("Turbulence Intensity Frequencies as Function of Wind Speed") - plt.xlabel("Wind Speed (m/s)") - plt.ylabel("Frequency") - - return ax - - def export_for_floris_opt(self): - """ - This method returns a list of tuples of at least wind speed, wind - direction, and frequency of occurance, which can be used to help loop - through different wind conditions for Floris power calculations. - - Returns: - list: A list of tuples containing all combinations of wind - parameters and frequencies of occurance in the WindRose object's - wind rose DataFrame values. - """ - # Return a list of tuples, where each tuple is (ws,wd,freq) - return [tuple(x) for x in self.df.values] From cb3a7e1efc19154951099647e97249c33fb0f7a9 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 22:47:06 -0700 Subject: [PATCH 015/101] add super class to import --- floris/tools/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/floris/tools/__init__.py b/floris/tools/__init__.py index ae7c7e2fc..435a4d107 100644 --- a/floris/tools/__init__.py +++ b/floris/tools/__init__.py @@ -46,7 +46,11 @@ visualize_cut_plane, visualize_quiver, ) -from .wind_rose_time_series import TimeSeries, WindRose +from .wind_rose_time_series import ( + TimeSeries, + WindData, + WindRose, +) # from floris.tools import ( From 8685b5f1ea92eaa15afdee11c9bac9c66b89bfdf Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 22:58:38 -0700 Subject: [PATCH 016/101] Add a super class and inheritance --- floris/tools/wind_rose_time_series.py | 58 ++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/floris/tools/wind_rose_time_series.py b/floris/tools/wind_rose_time_series.py index 7070f7de5..ef585fe95 100644 --- a/floris/tools/wind_rose_time_series.py +++ b/floris/tools/wind_rose_time_series.py @@ -19,7 +19,16 @@ from pandas.api.types import CategoricalDtype -class WindRose: +# Define the super lass that WindRose and TimeSeries inherit +class WindData: + def __init__(): + pass + + def unpack_for_reinitialize(self): + pass + + +class WindRose(WindData): """ In FLORIS v4, the WindRose class is used to drive FLORIS and optimization operations in which the inflow is characterized by the frequency of @@ -113,7 +122,7 @@ def _build_gridded_and_flattened_version(self): else: self.price_table_flat = None - def unpack(self): + def unpack(self, mask_0_occurence=True): """ Unpack the values in a form which is ready for FLORIS' reinitialize function """ @@ -124,7 +133,10 @@ def unpack(self): freq_table_unpack = self.freq_table_flat.copy() # Get a mask of combinations that are more than 0 occurences - self.unpack_mask = freq_table_unpack > 0.0 + if mask_0_occurence: + self.unpack_mask = freq_table_unpack > 0.0 + else: + self.unpack_mask = [True for i in range(len(wind_directions_unpack))] # Now mask thes values to as to only compute values with occurence over 0 wind_directions_unpack = wind_directions_unpack[self.unpack_mask] @@ -133,13 +145,13 @@ def unpack(self): # Repeat for turbulence intensity if not none if self.ti_table_flat is not None: - ti_table_unpack = self.ti_table_flat[self.unpack_mask] + ti_table_unpack = self.ti_table_flat[self.unpack_mask].copy() else: ti_table_unpack = None # Now get unpacked price table if self.price_table_flat is not None: - price_table_unpack = self.price_table_flat[self.unpack_mask] + price_table_unpack = self.price_table_flat[self.unpack_mask].copy() else: price_table_unpack = None @@ -151,6 +163,20 @@ def unpack(self): price_table_unpack, ) + def unpack_for_reinitialize(self, mask_0_occurence=True): + """ + Return only the variables need for reinitialize + """ + ( + wind_directions_unpack, + wind_speeds_unpack, + _, + ti_table_unpack, + _, + ) = self.unpack(mask_0_occurence) + + return wind_directions_unpack, wind_speeds_unpack, ti_table_unpack + def resample_wind_rose(self, wd_step=None, ws_step=None): # Returns a resampled version of the wind rose using new ws_step and wd_step @@ -216,8 +242,6 @@ def plot_wind_rose( ws_bins = wind_rose_resample.wind_speeds freq_table = wind_rose_resample.freq_table - print(ws_bins) - # Set up figure if ax is None: _, ax = plt.subplots(subplot_kw={"polar": True}) @@ -252,7 +276,7 @@ def plot_wind_rose( return ax -class TimeSeries: +class TimeSeries(WindData): """ In FLORIS v4, the TimeSeries class is used to drive FLORIS and optimization operations in which the inflow is by a sequence of wind speed, wind directino @@ -296,10 +320,24 @@ def unpack(self): self.wind_directions.copy(), self.wind_speeds.copy(), uniform_frequency, - self.turbulence_intensity.copy(), - self.prices.copy(), + self.turbulence_intensity, # can be none so can't copy + self.prices, # can be none so can't copy ) + def unpack_for_reinitialize(self): + """ + Return only the variables need for reinitialize + """ + ( + wind_directions_unpack, + wind_speeds_unpack, + _, + ti_table_unpack, + _, + ) = self.unpack() + + return wind_directions_unpack, wind_speeds_unpack, ti_table_unpack + def _wrap_wind_directions_near_360(self, wind_directions, wd_step): """ use wd_step to produce a wrapped version of wind_directions From d087abdc2f09d055f94cfb9aeba3de8b66af9803 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 22:59:11 -0700 Subject: [PATCH 017/101] Add wind_data to reinitialize (also ruffing) --- floris/tools/floris_interface.py | 97 ++++++++++++++------------------ 1 file changed, 41 insertions(+), 56 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index ef5b992b0..7ba74ef94 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -32,6 +32,9 @@ from floris.type_dec import NDArrayFloat +# from floris.tools import WindData + + class FlorisInterface(LoggingManager): """ FlorisInterface provides a high-level user interface to many of the @@ -76,10 +79,9 @@ def __init__(self, configuration: dict | str | Path): # Make a check on reference height and provide a helpful warning unique_heights = np.unique(np.round(self.floris.farm.hub_heights, decimals=6)) - if (( - len(unique_heights) == 1) and - (np.abs(self.floris.flow_field.reference_wind_height - unique_heights[0]) > 1.0e-6 - )): + if (len(unique_heights) == 1) and ( + np.abs(self.floris.flow_field.reference_wind_height - unique_heights[0]) > 1.0e-6 + ): err_msg = ( "The only unique hub-height is not the equal to the specified reference " "wind height. If this was unintended use -1 as the reference hub height to " @@ -99,7 +101,6 @@ def __init__(self, configuration: dict | str | Path): raise ValueError("turbine_grid_points must be less than or equal to 3.") def assign_hub_height_to_ref_height(self): - # Confirm can do this operation unique_heights = np.unique(self.floris.farm.hub_heights) if len(unique_heights) > 1: @@ -130,12 +131,7 @@ def calculate_wake( """ if yaw_angles is None: - yaw_angles = np.zeros( - ( - self.floris.flow_field.n_findex, - self.floris.farm.n_turbines - ) - ) + yaw_angles = np.zeros((self.floris.flow_field.n_findex, self.floris.farm.n_turbines)) self.floris.farm.yaw_angles = yaw_angles # # TODO is this required? @@ -169,12 +165,7 @@ def calculate_no_wake( """ if yaw_angles is None: - yaw_angles = np.zeros( - ( - self.floris.flow_field.n_findex, - self.floris.farm.n_turbines - ) - ) + yaw_angles = np.zeros((self.floris.flow_field.n_findex, self.floris.farm.n_turbines)) self.floris.farm.yaw_angles = yaw_angles # Initialize solution space @@ -200,6 +191,7 @@ def reinitialize( turbine_library_path: str | Path | None = None, solver_settings: dict | None = None, heterogenous_inflow_config=None, + wind_data=None, ): # Export the floris object recursively as a dictionary floris_dict = self.floris.as_dict() @@ -208,6 +200,13 @@ def reinitialize( # Make the given changes + # First check if wind data is not None, + # if not, get wind speeds, wind direction and + # turbulence intensity using the unpack_for_reinitialize + # method + if wind_data is not None: + wind_speeds, wind_directions, turbulence_intensity = wind_data.unpack_for_reinitialize() + ## FlowField if wind_speeds is not None: flow_field_dict["wind_speeds"] = wind_speeds @@ -271,7 +270,7 @@ def get_plane_of_points( :py:class:`pandas.DataFrame`: containing values of x1, x2, x3, u, v, w """ # Get results vectors - if (normal_vector == "z"): + if normal_vector == "z": x_flat = self.floris.grid.x_sorted_inertial_frame[0].flatten() y_flat = self.floris.grid.y_sorted_inertial_frame[0].flatten() z_flat = self.floris.grid.z_sorted_inertial_frame[0].flatten() @@ -401,10 +400,7 @@ def calculate_horizontal_plane( # Compute the cutplane horizontal_plane = CutPlane( - df, - self.floris.grid.grid_resolution[0], - self.floris.grid.grid_resolution[1], - "z" + df, self.floris.grid.grid_resolution[0], self.floris.grid.grid_resolution[1], "z" ) # Reset the fi object back to the turbine grid configuration @@ -599,7 +595,7 @@ def get_turbine_powers(self) -> NDArrayFloat: ) # Check for negative velocities, which could indicate bad model # parameters or turbines very closely spaced. - if (self.floris.flow_field.u < 0.).any(): + if (self.floris.flow_field.u < 0.0).any(): self.logger.warning("Some velocities at the rotor are negative.") turbine_powers = power( @@ -612,7 +608,7 @@ def get_turbine_powers(self) -> NDArrayFloat: turbine_type_map=self.floris.farm.turbine_type_map, turbine_power_thrust_tables=self.floris.farm.turbine_power_thrust_tables, correct_cp_ct_for_tilt=self.floris.farm.correct_cp_ct_for_tilt, - multidim_condition=self.floris.flow_field.multidim_conditions + multidim_condition=self.floris.flow_field.multidim_conditions, ) return turbine_powers @@ -628,7 +624,7 @@ def get_turbine_thrust_coefficients(self) -> NDArrayFloat: turbine_power_thrust_tables=self.floris.farm.turbine_power_thrust_tables, average_method=self.floris.grid.average_method, cubature_weights=self.floris.grid.cubature_weights, - multidim_condition=self.floris.flow_field.multidim_conditions + multidim_condition=self.floris.flow_field.multidim_conditions, ) return turbine_thrust_coefficients @@ -644,7 +640,7 @@ def get_turbine_ais(self) -> NDArrayFloat: turbine_power_thrust_tables=self.floris.farm.turbine_power_thrust_tables, average_method=self.floris.grid.average_method, cubature_weights=self.floris.grid.cubature_weights, - multidim_condition=self.floris.flow_field.multidim_conditions + multidim_condition=self.floris.flow_field.multidim_conditions, ) return turbine_ais @@ -653,7 +649,7 @@ def turbine_average_velocities(self) -> NDArrayFloat: return average_velocity( velocities=self.floris.flow_field.u, method=self.floris.grid.average_method, - cubature_weights=self.floris.grid.cubature_weights + cubature_weights=self.floris.grid.cubature_weights, ) def get_turbine_TIs(self) -> NDArrayFloat: @@ -709,20 +705,11 @@ def get_farm_power( if turbine_weights is None: # Default to equal weighing of all turbines when turbine_weights is None turbine_weights = np.ones( - ( - self.floris.flow_field.n_findex, - self.floris.farm.n_turbines - ) + (self.floris.flow_field.n_findex, self.floris.farm.n_turbines) ) elif len(np.shape(turbine_weights)) == 1: # Deal with situation when 1D array is provided - turbine_weights = np.tile( - turbine_weights, - ( - self.floris.flow_field.n_findex, - 1 - ) - ) + turbine_weights = np.tile(turbine_weights, (self.floris.flow_field.n_findex, 1)) # Calculate all turbine powers and apply weights turbine_powers = self.get_turbine_powers() @@ -796,8 +783,7 @@ def get_farm_AEP( # Check if frequency vector sums to 1.0. If not, raise a warning if np.abs(np.sum(freq) - 1.0) > 0.001: self.logger.warning( - "WARNING: The frequency array provided to get_farm_AEP() " - "does not sum to 1.0." + "WARNING: The frequency array provided to get_farm_AEP() " "does not sum to 1.0." ) # Copy the full wind speed array from the floris object and initialize @@ -819,15 +805,14 @@ def get_farm_AEP( if yaw_angles is not None: yaw_angles_subset = yaw_angles[conditions_to_evaluate] self.reinitialize( - wind_speeds=wind_speeds_subset, - wind_directions=wind_directions_subset + wind_speeds=wind_speeds_subset, wind_directions=wind_directions_subset ) if no_wake: self.calculate_no_wake(yaw_angles=yaw_angles_subset) else: self.calculate_wake(yaw_angles=yaw_angles_subset) - farm_power[conditions_to_evaluate] = ( - self.get_farm_power(turbine_weights=turbine_weights) + farm_power[conditions_to_evaluate] = self.get_farm_power( + turbine_weights=turbine_weights ) # Finally, calculate AEP in GWh @@ -859,17 +844,17 @@ def sample_flow_at_points(self, x: NDArrayFloat, y: NDArrayFloat, z: NDArrayFloa return self.floris.solve_for_points(x, y, z) def sample_velocity_deficit_profiles( - self, - direction: str = 'cross-stream', - downstream_dists: NDArrayFloat | list = None, - profile_range: NDArrayFloat | list = None, - resolution: int = 100, - wind_direction: float = None, - homogeneous_wind_speed: float = None, - ref_rotor_diameter: float = None, - x_start: float = 0.0, - y_start: float = 0.0, - reference_height: float = None, + self, + direction: str = "cross-stream", + downstream_dists: NDArrayFloat | list = None, + profile_range: NDArrayFloat | list = None, + resolution: int = 100, + wind_direction: float = None, + homogeneous_wind_speed: float = None, + ref_rotor_diameter: float = None, + x_start: float = 0.0, + y_start: float = 0.0, + reference_height: float = None, ) -> list[pd.DataFrame]: """ Extract velocity deficit profiles at a set of downstream distances from a starting point @@ -903,7 +888,7 @@ def sample_velocity_deficit_profiles( profile. """ - if direction not in ['cross-stream', 'vertical']: + if direction not in ["cross-stream", "vertical"]: raise ValueError("`direction` must be either `cross-stream` or `vertical`.") if ref_rotor_diameter is None: From c75c23c47e7a3c16b10a9500fccc4d786b33675c Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 22:59:34 -0700 Subject: [PATCH 018/101] Show example of reinit off wind_data objects --- examples/34_wind_rose_examples.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/examples/34_wind_rose_examples.py b/examples/34_wind_rose_examples.py index 174846790..1c059b352 100644 --- a/examples/34_wind_rose_examples.py +++ b/examples/34_wind_rose_examples.py @@ -15,7 +15,11 @@ import matplotlib.pyplot as plt import numpy as np -from floris.tools import TimeSeries, WindRose +from floris.tools import ( + FlorisInterface, + TimeSeries, + WindRose, +) from floris.utilities import wrap_360 @@ -38,7 +42,7 @@ # Build the time series -time_series = TimeSeries(wd_array, ws_array, turbulence_intensity=ti_array) +time_series = TimeSeries(wd_array, ws_array) # , turbulence_intensity=ti_array) # Now build the wind rose wind_rose = time_series.to_wind_rose() @@ -47,4 +51,21 @@ fig, ax = plt.subplots(subplot_kw={"polar": True}) wind_rose.plot_wind_rose(ax=ax) -plt.show() +# Now set up a FLORIS model and initialize it using the time series and wind rose +fi = FlorisInterface("inputs/gch.yaml") +fi.reinitialize(layout_x=[0, 500.0], layout_y=[0.0, 0.0]) + +fi_time_series = fi.copy() +fi_wind_rose = fi.copy() + +fi_time_series.reinitialize(wind_data=time_series) +fi_wind_rose.reinitialize(wind_data=wind_rose) + +fi_time_series.calculate_wake() +fi_wind_rose.calculate_wake() + +time_series_power = fi_time_series.get_farm_power() +wind_rose_power = fi_wind_rose.get_farm_power() + +print(time_series_power.shape) +print(wind_rose_power.shape) From d419e0d7c3944ba18297fc912da96f8c411de8b4 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 16 Jan 2024 13:01:53 -0700 Subject: [PATCH 019/101] Update how compute 0 freq works --- floris/tools/wind_rose_time_series.py | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/floris/tools/wind_rose_time_series.py b/floris/tools/wind_rose_time_series.py index ef585fe95..b173730a8 100644 --- a/floris/tools/wind_rose_time_series.py +++ b/floris/tools/wind_rose_time_series.py @@ -43,6 +43,7 @@ def __init__( freq_table=None, ti_table=None, price_table=None, + compute_zero_freq_occurence=False, ): """ TODO: Write this later @@ -94,6 +95,9 @@ def __init__( else: self.price_table = None + # Save whether zero occurence cases should be computed + self.compute_zero_freq_occurence = compute_zero_freq_occurence + # Build the gridded and flatten versions self._build_gridded_and_flattened_version() @@ -122,7 +126,14 @@ def _build_gridded_and_flattened_version(self): else: self.price_table_flat = None - def unpack(self, mask_0_occurence=True): + # Set mask to non-zero frequency cases depending on compute_zero_freq_occurence + if self.compute_zero_freq_occurence: + # If computing zero freq occurences, then this is all True + self.non_zero_freq_mask = [True for i in range(len(self.freq_table_flat))] + else: + self.non_zero_freq_mask = self.freq_table_flat > 0.0 + + def unpack(self): """ Unpack the values in a form which is ready for FLORIS' reinitialize function """ @@ -132,26 +143,20 @@ def unpack(self, mask_0_occurence=True): wind_speeds_unpack = self.ws_flat.copy() freq_table_unpack = self.freq_table_flat.copy() - # Get a mask of combinations that are more than 0 occurences - if mask_0_occurence: - self.unpack_mask = freq_table_unpack > 0.0 - else: - self.unpack_mask = [True for i in range(len(wind_directions_unpack))] - - # Now mask thes values to as to only compute values with occurence over 0 - wind_directions_unpack = wind_directions_unpack[self.unpack_mask] - wind_speeds_unpack = wind_speeds_unpack[self.unpack_mask] - freq_table_unpack = freq_table_unpack[self.unpack_mask] + # Now mask thes values according to self.non_zero_freq_mask + wind_directions_unpack = wind_directions_unpack[self.non_zero_freq_mask] + wind_speeds_unpack = wind_speeds_unpack[self.non_zero_freq_mask] + freq_table_unpack = freq_table_unpack[self.non_zero_freq_mask] # Repeat for turbulence intensity if not none if self.ti_table_flat is not None: - ti_table_unpack = self.ti_table_flat[self.unpack_mask].copy() + ti_table_unpack = self.ti_table_flat[self.non_zero_freq_mask].copy() else: ti_table_unpack = None # Now get unpacked price table if self.price_table_flat is not None: - price_table_unpack = self.price_table_flat[self.unpack_mask].copy() + price_table_unpack = self.price_table_flat[self.non_zero_freq_mask].copy() else: price_table_unpack = None @@ -163,7 +168,7 @@ def unpack(self, mask_0_occurence=True): price_table_unpack, ) - def unpack_for_reinitialize(self, mask_0_occurence=True): + def unpack_for_reinitialize(self): """ Return only the variables need for reinitialize """ @@ -173,7 +178,7 @@ def unpack_for_reinitialize(self, mask_0_occurence=True): _, ti_table_unpack, _, - ) = self.unpack(mask_0_occurence) + ) = self.unpack() return wind_directions_unpack, wind_speeds_unpack, ti_table_unpack From f6df165b23e8d765f942e2b67aefeb6f398b09aa Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 16 Jan 2024 13:05:06 -0700 Subject: [PATCH 020/101] Test computing all cases --- tests/wind_rose_time_series_test.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/wind_rose_time_series_test.py b/tests/wind_rose_time_series_test.py index d68faf832..b9c9cd18e 100644 --- a/tests/wind_rose_time_series_test.py +++ b/tests/wind_rose_time_series_test.py @@ -65,6 +65,7 @@ def test_wind_rose_unpack(): wind_speeds = np.array([6, 7]) freq_table = np.array([[1.0, 0.0], [0, 1.0], [0, 0]]) + # First test using default assumption only non-zero frequency cases computed wind_rose = WindRose(wind_directions, wind_speeds, freq_table) ( @@ -81,6 +82,20 @@ def test_wind_rose_unpack(): np.testing.assert_allclose(wind_speeds_unpack, [6, 7]) np.testing.assert_allclose(freq_table_unpack, [0.5, 0.5]) + # Now test computing 0-freq cases too + wind_rose = WindRose(wind_directions, wind_speeds, freq_table, compute_zero_freq_occurence=True) + + ( + wind_directions_unpack, + wind_speeds_unpack, + freq_table_unpack, + ti_table_unpack, + price_table_unpack, + ) = wind_rose.unpack() + + # Expect now to compute all combinations + np.testing.assert_allclose(wind_directions_unpack, [270, 270, 280, 280, 290, 290]) + def test_wind_rose_resample(): wind_directions = np.array([0, 2, 4, 6, 8, 10]) From c819fe4a7abe16afaaf3aee60159d27aa87c295c Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 16 Jan 2024 13:25:04 -0700 Subject: [PATCH 021/101] add n_findex calculation and test --- floris/tools/wind_rose_time_series.py | 3 +++ tests/wind_rose_time_series_test.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/floris/tools/wind_rose_time_series.py b/floris/tools/wind_rose_time_series.py index b173730a8..ae0ff12dc 100644 --- a/floris/tools/wind_rose_time_series.py +++ b/floris/tools/wind_rose_time_series.py @@ -133,6 +133,9 @@ def _build_gridded_and_flattened_version(self): else: self.non_zero_freq_mask = self.freq_table_flat > 0.0 + # N_findex should only be the calculated cases + self.n_findex = np.sum(self.non_zero_freq_mask) + def unpack(self): """ Unpack the values in a form which is ready for FLORIS' reinitialize function diff --git a/tests/wind_rose_time_series_test.py b/tests/wind_rose_time_series_test.py index b9c9cd18e..9bbd7d046 100644 --- a/tests/wind_rose_time_series_test.py +++ b/tests/wind_rose_time_series_test.py @@ -82,6 +82,9 @@ def test_wind_rose_unpack(): np.testing.assert_allclose(wind_speeds_unpack, [6, 7]) np.testing.assert_allclose(freq_table_unpack, [0.5, 0.5]) + # In this case n_findex == 2 + assert wind_rose.n_findex == 2 + # Now test computing 0-freq cases too wind_rose = WindRose(wind_directions, wind_speeds, freq_table, compute_zero_freq_occurence=True) @@ -96,6 +99,9 @@ def test_wind_rose_unpack(): # Expect now to compute all combinations np.testing.assert_allclose(wind_directions_unpack, [270, 270, 280, 280, 290, 290]) + # In this case n_findex == 6 + assert wind_rose.n_findex == 6 + def test_wind_rose_resample(): wind_directions = np.array([0, 2, 4, 6, 8, 10]) From 09bf38cb415ae56c4eb27bbada99a9f87e484dcb Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 16 Jan 2024 13:31:37 -0700 Subject: [PATCH 022/101] Add unpack_freq function --- floris/tools/wind_rose_time_series.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/floris/tools/wind_rose_time_series.py b/floris/tools/wind_rose_time_series.py index ae0ff12dc..b8e155372 100644 --- a/floris/tools/wind_rose_time_series.py +++ b/floris/tools/wind_rose_time_series.py @@ -185,6 +185,19 @@ def unpack_for_reinitialize(self): return wind_directions_unpack, wind_speeds_unpack, ti_table_unpack + def unpack_freq(self): + """Unpack frequency weighting""" + + ( + _, + _, + freq_table_unpack, + _, + _, + ) = self.unpack() + + return freq_table_unpack + def resample_wind_rose(self, wd_step=None, ws_step=None): # Returns a resampled version of the wind rose using new ws_step and wd_step @@ -346,6 +359,19 @@ def unpack_for_reinitialize(self): return wind_directions_unpack, wind_speeds_unpack, ti_table_unpack + def unpack_freq(self): + """Unpack frequency weighting""" + + ( + _, + _, + freq_table_unpack, + _, + _, + ) = self.unpack() + + return freq_table_unpack + def _wrap_wind_directions_near_360(self, wind_directions, wd_step): """ use wd_step to produce a wrapped version of wind_directions From feb01638a94e8a6cc0ecda0a50210816b2eb8e69 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 16 Jan 2024 13:31:48 -0700 Subject: [PATCH 023/101] Get aep using wind data --- floris/tools/floris_interface.py | 117 +++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 7ba74ef94..b1be75f57 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -767,12 +767,129 @@ def get_farm_AEP( the flow field. This can be useful when quantifying the loss in AEP due to wakes. Defaults to *False*. + + Returns: + float: + The Annual Energy Production (AEP) for the wind farm in + watt-hours. + """ + + # Verify dimensions of the variable "freq" + if np.shape(freq)[0] != self.floris.flow_field.n_findex: + raise UserWarning( + "'freq' should be a one-dimensional array with dimensions (n_findex). " + f"Given shape is {np.shape(freq)}" + ) + + # Check if frequency vector sums to 1.0. If not, raise a warning + if np.abs(np.sum(freq) - 1.0) > 0.001: + self.logger.warning( + "WARNING: The frequency array provided to get_farm_AEP() " "does not sum to 1.0." + ) + + # Copy the full wind speed array from the floris object and initialize + # the the farm_power variable as an empty array. + wind_speeds = np.array(self.floris.flow_field.wind_speeds, copy=True) + wind_directions = np.array(self.floris.flow_field.wind_directions, copy=True) + farm_power = np.zeros(self.floris.flow_field.n_findex) + + # Determine which wind speeds we must evaluate + conditions_to_evaluate = wind_speeds >= cut_in_wind_speed + if cut_out_wind_speed is not None: + conditions_to_evaluate = conditions_to_evaluate & (wind_speeds < cut_out_wind_speed) + + # Evaluate the conditions in floris + if np.any(conditions_to_evaluate): + wind_speeds_subset = wind_speeds[conditions_to_evaluate] + wind_directions_subset = wind_directions[conditions_to_evaluate] + yaw_angles_subset = None + if yaw_angles is not None: + yaw_angles_subset = yaw_angles[conditions_to_evaluate] + self.reinitialize( + wind_speeds=wind_speeds_subset, wind_directions=wind_directions_subset + ) + if no_wake: + self.calculate_no_wake(yaw_angles=yaw_angles_subset) + else: + self.calculate_wake(yaw_angles=yaw_angles_subset) + farm_power[conditions_to_evaluate] = self.get_farm_power( + turbine_weights=turbine_weights + ) + + # Finally, calculate AEP in GWh + aep = np.sum(np.multiply(freq, farm_power) * 365 * 24) + + # Reset the FLORIS object to the full wind speed array + self.reinitialize(wind_speeds=wind_speeds, wind_directions=wind_directions) + + return aep + + def get_farm_AEP_with_wind_data( + self, + wind_data, + cut_in_wind_speed=0.001, + cut_out_wind_speed=None, + yaw_angles=None, + turbine_weights=None, + no_wake=False, + ) -> float: + """ + Estimate annual energy production (AEP) for distributions of wind speed, wind + direction, frequency of occurrence, and yaw offset. + + Args: + freq (NDArrayFloat): NumPy array with shape (n_findex) + with the frequencies of each wind direction and + wind speed combination. These frequencies should typically sum + up to 1.0 and are used to weigh the wind farm power for every + condition in calculating the wind farm's AEP. + cut_in_wind_speed (float, optional): Wind speed in m/s below which + any calculations are ignored and the wind farm is known to + produce 0.0 W of power. Note that to prevent problems with the + wake models at negative / zero wind speeds, this variable must + always have a positive value. Defaults to 0.001 [m/s]. + cut_out_wind_speed (float, optional): Wind speed above which the + wind farm is known to produce 0.0 W of power. If None is + specified, will assume that the wind farm does not cut out + at high wind speeds. Defaults to None. + yaw_angles (NDArrayFloat | list[float] | None, optional): + The relative turbine yaw angles in degrees. If None is + specified, will assume that the turbine yaw angles are all + zero degrees for all conditions. Defaults to None. + turbine_weights (NDArrayFloat | list[float] | None, optional): + weighing terms that allow the user to emphasize power at + particular turbines and/or completely ignore the power + from other turbines. This is useful when, for example, you are + modeling multiple wind farms in a single floris object. If you + only want to calculate the power production for one of those + farms and include the wake effects of the neighboring farms, + you can set the turbine_weights for the neighboring farms' + turbines to 0.0. The array of turbine powers from floris + is multiplied with this array in the calculation of the + objective function. If None, this is an array with all values + 1.0 and with shape equal to (n_findex, + n_turbines). Defaults to None. + no_wake: (bool, optional): When *True* updates the turbine + quantities without calculating the wake or adding the wake to + the flow field. This can be useful when quantifying the loss + in AEP due to wakes. Defaults to *False*. + wind_data: (WindData, optional): Should be the same same object + passed to reinitialize + Returns: float: The Annual Energy Production (AEP) for the wind farm in watt-hours. """ + # Verify the wind_data object matches FLORIS' initialization + if wind_data is not None: + if wind_data.n_findex != self.floris.flow_field.n_findex: + raise ValueError("WindData object and floris do not have same findex") + + # Get freq directly from wind_data + freq = wind_data.unpack_freq() + # Verify dimensions of the variable "freq" if np.shape(freq)[0] != self.floris.flow_field.n_findex: raise UserWarning( From dc8bc8094c4dbbd0b9680c59bdc8d640ebf06f7a Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 16 Jan 2024 14:39:58 -0700 Subject: [PATCH 024/101] Move unpack_for functions to super class --- floris/tools/wind_rose_time_series.py | 82 +++++++++------------------ tests/wind_rose_time_series_test.py | 20 +++++++ 2 files changed, 47 insertions(+), 55 deletions(-) diff --git a/floris/tools/wind_rose_time_series.py b/floris/tools/wind_rose_time_series.py index b8e155372..d0aea1533 100644 --- a/floris/tools/wind_rose_time_series.py +++ b/floris/tools/wind_rose_time_series.py @@ -20,12 +20,38 @@ # Define the super lass that WindRose and TimeSeries inherit +# Define functions here that are either the same for both WindRose and +# TimeSeries or will be overloaded class WindData: def __init__(): pass def unpack_for_reinitialize(self): - pass + """ + Return only the variables need for reinitialize + """ + ( + wind_directions_unpack, + wind_speeds_unpack, + _, + ti_table_unpack, + _, + ) = self.unpack() + + return wind_directions_unpack, wind_speeds_unpack, ti_table_unpack + + def unpack_freq(self): + """Unpack frequency weighting""" + + ( + _, + _, + freq_table_unpack, + _, + _, + ) = self.unpack() + + return freq_table_unpack class WindRose(WindData): @@ -171,33 +197,6 @@ def unpack(self): price_table_unpack, ) - def unpack_for_reinitialize(self): - """ - Return only the variables need for reinitialize - """ - ( - wind_directions_unpack, - wind_speeds_unpack, - _, - ti_table_unpack, - _, - ) = self.unpack() - - return wind_directions_unpack, wind_speeds_unpack, ti_table_unpack - - def unpack_freq(self): - """Unpack frequency weighting""" - - ( - _, - _, - freq_table_unpack, - _, - _, - ) = self.unpack() - - return freq_table_unpack - def resample_wind_rose(self, wd_step=None, ws_step=None): # Returns a resampled version of the wind rose using new ws_step and wd_step @@ -345,33 +344,6 @@ def unpack(self): self.prices, # can be none so can't copy ) - def unpack_for_reinitialize(self): - """ - Return only the variables need for reinitialize - """ - ( - wind_directions_unpack, - wind_speeds_unpack, - _, - ti_table_unpack, - _, - ) = self.unpack() - - return wind_directions_unpack, wind_speeds_unpack, ti_table_unpack - - def unpack_freq(self): - """Unpack frequency weighting""" - - ( - _, - _, - freq_table_unpack, - _, - _, - ) = self.unpack() - - return freq_table_unpack - def _wrap_wind_directions_near_360(self, wind_directions, wd_step): """ use wd_step to produce a wrapped version of wind_directions diff --git a/tests/wind_rose_time_series_test.py b/tests/wind_rose_time_series_test.py index 9bbd7d046..c7954d903 100644 --- a/tests/wind_rose_time_series_test.py +++ b/tests/wind_rose_time_series_test.py @@ -103,6 +103,26 @@ def test_wind_rose_unpack(): assert wind_rose.n_findex == 6 +def test_unpack_for_reinitialize(): + wind_directions = np.array([270, 280, 290]) + wind_speeds = np.array([6, 7]) + freq_table = np.array([[1.0, 0.0], [0, 1.0], [0, 0]]) + + # First test using default assumption only non-zero frequency cases computed + wind_rose = WindRose(wind_directions, wind_speeds, freq_table) + + ( + wind_directions_unpack, + wind_speeds_unpack, + ti_table_unpack, + ) = wind_rose.unpack_for_reinitialize() + + # Given the above frequency table, would only expect the + # (270 deg, 6 m/s) and (280 deg, 7 m/s) rows + np.testing.assert_allclose(wind_directions_unpack, [270, 280]) + np.testing.assert_allclose(wind_speeds_unpack, [6, 7]) + + def test_wind_rose_resample(): wind_directions = np.array([0, 2, 4, 6, 8, 10]) wind_speeds = np.array([8]) From 48730a9091678a3b5e7fa67a17b230fa11b09cf9 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 16 Jan 2024 14:42:30 -0700 Subject: [PATCH 025/101] simplify get_farm_AEP_with_wind_data --- floris/tools/floris_interface.py | 57 +++++--------------------------- 1 file changed, 8 insertions(+), 49 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index b1be75f57..d2f39b212 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -890,55 +890,14 @@ def get_farm_AEP_with_wind_data( # Get freq directly from wind_data freq = wind_data.unpack_freq() - # Verify dimensions of the variable "freq" - if np.shape(freq)[0] != self.floris.flow_field.n_findex: - raise UserWarning( - "'freq' should be a one-dimensional array with dimensions (n_findex). " - f"Given shape is {np.shape(freq)}" - ) - - # Check if frequency vector sums to 1.0. If not, raise a warning - if np.abs(np.sum(freq) - 1.0) > 0.001: - self.logger.warning( - "WARNING: The frequency array provided to get_farm_AEP() " "does not sum to 1.0." - ) - - # Copy the full wind speed array from the floris object and initialize - # the the farm_power variable as an empty array. - wind_speeds = np.array(self.floris.flow_field.wind_speeds, copy=True) - wind_directions = np.array(self.floris.flow_field.wind_directions, copy=True) - farm_power = np.zeros(self.floris.flow_field.n_findex) - - # Determine which wind speeds we must evaluate - conditions_to_evaluate = wind_speeds >= cut_in_wind_speed - if cut_out_wind_speed is not None: - conditions_to_evaluate = conditions_to_evaluate & (wind_speeds < cut_out_wind_speed) - - # Evaluate the conditions in floris - if np.any(conditions_to_evaluate): - wind_speeds_subset = wind_speeds[conditions_to_evaluate] - wind_directions_subset = wind_directions[conditions_to_evaluate] - yaw_angles_subset = None - if yaw_angles is not None: - yaw_angles_subset = yaw_angles[conditions_to_evaluate] - self.reinitialize( - wind_speeds=wind_speeds_subset, wind_directions=wind_directions_subset - ) - if no_wake: - self.calculate_no_wake(yaw_angles=yaw_angles_subset) - else: - self.calculate_wake(yaw_angles=yaw_angles_subset) - farm_power[conditions_to_evaluate] = self.get_farm_power( - turbine_weights=turbine_weights - ) - - # Finally, calculate AEP in GWh - aep = np.sum(np.multiply(freq, farm_power) * 365 * 24) - - # Reset the FLORIS object to the full wind speed array - self.reinitialize(wind_speeds=wind_speeds, wind_directions=wind_directions) - - return aep + return self.get_farm_AEP( + freq, + cut_in_wind_speed=cut_in_wind_speed, + cut_out_wind_speed=cut_out_wind_speed, + yaw_angles=yaw_angles, + turbine_weights=turbine_weights, + no_wake=no_wake, + ) def sample_flow_at_points(self, x: NDArrayFloat, y: NDArrayFloat, z: NDArrayFloat): """ From 4bcea143dee974cd4d2ae95ffee6257a4b648e9b Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 17 Jan 2024 11:22:17 -0700 Subject: [PATCH 026/101] Add docstrings --- floris/tools/wind_rose_time_series.py | 94 +++++++++++++++++++++------ 1 file changed, 74 insertions(+), 20 deletions(-) diff --git a/floris/tools/wind_rose_time_series.py b/floris/tools/wind_rose_time_series.py index d0aea1533..b1152f49b 100644 --- a/floris/tools/wind_rose_time_series.py +++ b/floris/tools/wind_rose_time_series.py @@ -60,6 +60,18 @@ class WindRose(WindData): operations in which the inflow is characterized by the frequency of binned wind speed, wind direction and turbulence intensity values + Args: + wind_directions: NumPy array of wind directions (NDArrayFloat). + wind_speeds: NumPy array of wind speeds (NDArrayFloat). + freq_table: Frequency table for binned wind direction, wind speed + values (NDArrayFloat, optional). Defaults to None. + ti_table: Turbulence intensity table for binned wind direction, wind + speed values (NDArrayFloat, optional). Defaults to None. + price_table: Price table for binned binned wind direction, wind + speed values (NDArrayFloat, optional). Defaults to None. + compute_zero_freq_occurrence: Flag indicating whether to compute zero + frequency occurrences (bool, optional). Defaults to False. + """ def __init__( @@ -71,9 +83,6 @@ def __init__( price_table=None, compute_zero_freq_occurence=False, ): - """ - TODO: Write this later - """ if not isinstance(wind_directions, np.ndarray): raise TypeError("wind_directions must be a NumPy array") @@ -128,6 +137,11 @@ def __init__( self._build_gridded_and_flattened_version() def _build_gridded_and_flattened_version(self): + """ + Given the wind direction and speed array, build the gridded versions + covering all combinations, and then flatten versions which put all + combinations into 1D array + """ # Gridded wind speed and direction self.wd_grid, self.ws_grid = np.meshgrid( self.wind_directions, self.wind_speeds, indexing="ij" @@ -164,7 +178,8 @@ def _build_gridded_and_flattened_version(self): def unpack(self): """ - Unpack the values in a form which is ready for FLORIS' reinitialize function + Unpack the flattened versions of the matrices and return the values + accounting for the non_zero_freq_mask """ # The unpacked versions start as the flat version of each @@ -198,11 +213,21 @@ def unpack(self): ) def resample_wind_rose(self, wd_step=None, ws_step=None): - # Returns a resampled version of the wind rose using new ws_step and wd_step + """ + Resamples the wind rose by by wd_step and/or ws_step - # Use the bin weights feature in TimeSeries to resample the wind rose + Args: + wd_step: Step size for wind direction resampling (float, optional). + ws_step: Step size for wind speed resampling (float, optional). + + Returns: + WindRose: Resampled wind rose based on the provided or default step sizes. - # If ws_step or wd_step, not specied use current values + Notes: + - Returns a resampled version of the wind rose using new `ws_step` and `wd_step`. + - Uses the bin weights feature in TimeSeries to resample the wind rose. + - If `ws_step` or `wd_step` is not specified, it uses the current values. + """ if ws_step is None: if len(self.wind_speeds) >= 2: ws_step = self.wind_speeds[1] - self.wind_speeds[0] @@ -246,8 +271,8 @@ def plot_wind_rose( ax (:py:class:`matplotlib.pyplot.axes`, optional): The figure axes on which the wind rose is plotted. Defaults to None. color_map (str, optional): Colormap to use. Defaults to 'viridis_r'. - ws_step - wd_step + wd_step: Step size for wind direction (float, optional). + ws_step: Step size for wind speed (float, optional). legend_kwargs (dict, optional): Keyword arguments to be passed to ax.legend(). @@ -283,7 +308,6 @@ def plot_wind_rose( edgecolor="k", ) ) - # break # Configure the plot ax.legend(reversed(rects), ws_bins, **legend_kwargs) @@ -299,9 +323,17 @@ def plot_wind_rose( class TimeSeries(WindData): """ In FLORIS v4, the TimeSeries class is used to drive FLORIS and optimization - operations in which the inflow is by a sequence of wind speed, wind directino + operations in which the inflow is by a sequence of wind direction, wind speed and turbulence intensitity values + Args: + wind_directions: NumPy array of wind directions (NDArrayFloat). + wind_speeds: NumPy array of wind speeds (NDArrayFloat). + turbulence_intensity: NumPy array of wind speeds (NDArrayFloat, optional). + Defatuls to None + prices: NumPy array of electricity prices (NDArrayFloat, optional). + Defatuls to None + """ def __init__( @@ -311,10 +343,6 @@ def __init__( turbulence_intensity=None, prices=None, ): - """ - TODO: Write this later - """ - # Wind speeds and wind directions must be the same length if len(wind_directions) != len(wind_speeds): raise ValueError("wind_directions and wind_speeds must be the same length") @@ -329,7 +357,7 @@ def __init__( def unpack(self): """ - Unpack the time series data to floris' reinitialize function + Unpack the time series data in a manner consistent with wind rose unpack """ # to match wind_rose, make a uniform frequency @@ -346,9 +374,17 @@ def unpack(self): def _wrap_wind_directions_near_360(self, wind_directions, wd_step): """ - use wd_step to produce a wrapped version of wind_directions - where values that are between [360 - wd_step/2.0,360] get mapped - to negative numbers for binning + Wraps the wind directions using `wd_step` to produce a wrapped version + where values between [360 - wd_step/2.0, 360] get mapped to negative numbers + for binning. + + Args: + wind_directions (NDArrayFloat): NumPy array of wind directions. + wd_step (float): Step size for wind direction. + + Returns: + NDArrayFloat: Wrapped version of wind directions. + """ wind_directions_wrapped = wind_directions.copy() mask = wind_directions_wrapped >= 360 - wd_step / 2.0 @@ -359,7 +395,25 @@ def to_wind_rose( self, wd_step=2.0, ws_step=1.0, wd_edges=None, ws_edges=None, bin_weights=None ): """ - TODO: Write this later + Converts the TimeSeries data to a WindRose. + + Args: + wd_step (float, optional): Step size for wind direction (default is 2.0). + ws_step (float, optional): Step size for wind speed (default is 1.0). + wd_edges (NDArrayFloat, optional): Custom wind direction edges. Defaults to None. + ws_edges (NDArrayFloat, optional): Custom wind speed edges. Defaults to None. + bin_weights (NDArrayFloat, optional): Bin weights for resampling. Note these + are primarily used by the resample resample_wind_rose function. + Defaults to None. + + Returns: + WindRose: A WindRose object based on the TimeSeries data. + + Notes: + - If `wd_edges` is defined, it uses it to produce the bin centers. + - If `wd_edges` is not defined, it determines `wd_edges` from the step and data. + - If `ws_edges` is defined, it uses it for wind speed edges. + - If `ws_edges` is not defined, it determines `ws_edges` from the step and data. """ # If wd_edges is defined, then use it to produce the bin centers From 793d50a4a644ad797de87d7d2cbcd6dbdfb2783c Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 17 Jan 2024 11:36:03 -0700 Subject: [PATCH 027/101] bugfix --- floris/tools/floris_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index d2f39b212..0b9c9ed03 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -205,7 +205,7 @@ def reinitialize( # turbulence intensity using the unpack_for_reinitialize # method if wind_data is not None: - wind_speeds, wind_directions, turbulence_intensity = wind_data.unpack_for_reinitialize() + wind_directions, wind_speeds, turbulence_intensity = wind_data.unpack_for_reinitialize() ## FlowField if wind_speeds is not None: From b56a09628377a23f363defc22e05adc00e88c337 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 17 Jan 2024 11:36:21 -0700 Subject: [PATCH 028/101] Finalize example --- examples/34_wind_rose_examples.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/34_wind_rose_examples.py b/examples/34_wind_rose_examples.py index 1c059b352..5d5885ab6 100644 --- a/examples/34_wind_rose_examples.py +++ b/examples/34_wind_rose_examples.py @@ -26,7 +26,7 @@ # Generate a random time series of wind speeds, wind directions and turbulence intensities N = 500 wd_array = wrap_360(270 * np.ones(N) + np.random.randn(N) * 20) -ws_array = np.clip(8 * np.ones(N) + np.random.randn(N) * 8, 0, 50) +ws_array = np.clip(8 * np.ones(N) + np.random.randn(N) * 8, 3, 50) ti_array = np.clip(0.1 * np.ones(N) + np.random.randn(N) * 0.05, 0, 0.25) fig, axarr = plt.subplots(3, 1, sharex=True, figsize=(7, 4)) @@ -67,5 +67,10 @@ time_series_power = fi_time_series.get_farm_power() wind_rose_power = fi_wind_rose.get_farm_power() -print(time_series_power.shape) -print(wind_rose_power.shape) +time_series_aep = fi_time_series.get_farm_AEP_with_wind_data(time_series) +wind_rose_aep = fi_wind_rose.get_farm_AEP_with_wind_data(wind_rose) + +print(f"AEP from TimeSeries {time_series_aep / 1e9:.2f} GWh") +print(f"AEP from WindRose {wind_rose_aep / 1e9:.2f} GWh") + +plt.show() From 81e00ee52d8fba60f9a2931ad9afc312dbdfb715 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 17 Jan 2024 16:46:24 -0700 Subject: [PATCH 029/101] Rename module file and base class --- floris/tools/__init__.py | 4 ++-- floris/tools/{wind_rose_time_series.py => wind_data.py} | 6 +++--- tests/{wind_rose_time_series_test.py => wind_data_test.py} | 0 3 files changed, 5 insertions(+), 5 deletions(-) rename floris/tools/{wind_rose_time_series.py => wind_data.py} (99%) rename tests/{wind_rose_time_series_test.py => wind_data_test.py} (100%) diff --git a/floris/tools/__init__.py b/floris/tools/__init__.py index 435a4d107..89fc0b856 100644 --- a/floris/tools/__init__.py +++ b/floris/tools/__init__.py @@ -46,9 +46,9 @@ visualize_cut_plane, visualize_quiver, ) -from .wind_rose_time_series import ( +from .wind_data import ( TimeSeries, - WindData, + WindDataBase, WindRose, ) diff --git a/floris/tools/wind_rose_time_series.py b/floris/tools/wind_data.py similarity index 99% rename from floris/tools/wind_rose_time_series.py rename to floris/tools/wind_data.py index b1152f49b..d51c60769 100644 --- a/floris/tools/wind_rose_time_series.py +++ b/floris/tools/wind_data.py @@ -22,7 +22,7 @@ # Define the super lass that WindRose and TimeSeries inherit # Define functions here that are either the same for both WindRose and # TimeSeries or will be overloaded -class WindData: +class WindDataBase: def __init__(): pass @@ -54,7 +54,7 @@ def unpack_freq(self): return freq_table_unpack -class WindRose(WindData): +class WindRose(WindDataBase): """ In FLORIS v4, the WindRose class is used to drive FLORIS and optimization operations in which the inflow is characterized by the frequency of @@ -320,7 +320,7 @@ def plot_wind_rose( return ax -class TimeSeries(WindData): +class TimeSeries(WindDataBase): """ In FLORIS v4, the TimeSeries class is used to drive FLORIS and optimization operations in which the inflow is by a sequence of wind direction, wind speed diff --git a/tests/wind_rose_time_series_test.py b/tests/wind_data_test.py similarity index 100% rename from tests/wind_rose_time_series_test.py rename to tests/wind_data_test.py From 64693625ab029568b59ef270b873a5ef9dc4f763 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 17 Jan 2024 16:58:45 -0700 Subject: [PATCH 030/101] Add description to example explaining plan for updates. --- ...34_wind_rose_examples.py => 34_wind_data_temporary.py} | 8 ++++++++ 1 file changed, 8 insertions(+) rename examples/{34_wind_rose_examples.py => 34_wind_data_temporary.py} (87%) diff --git a/examples/34_wind_rose_examples.py b/examples/34_wind_data_temporary.py similarity index 87% rename from examples/34_wind_rose_examples.py rename to examples/34_wind_data_temporary.py index 5d5885ab6..f3e87686d 100644 --- a/examples/34_wind_rose_examples.py +++ b/examples/34_wind_data_temporary.py @@ -23,6 +23,14 @@ from floris.utilities import wrap_360 +""" +This example is meant to be temporary and may be updated by a later pull request. Before we +release v4, we intend to propagate the TimeSeries and WindRose objects through the other relevant +examples, and change this example to demonstrate more advanced (as yet, not implemented) +functionality of the WindData objects (such as electricity pricing etc). +""" + + # Generate a random time series of wind speeds, wind directions and turbulence intensities N = 500 wd_array = wrap_360(270 * np.ones(N) + np.random.randn(N) * 20) From ccdf2c562df9e792d8c38a39cc832f69777b9067 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 18 Jan 2024 10:27:13 -0700 Subject: [PATCH 031/101] providing unpack() on base class; renaming example. --- .../{34_wind_data_temporary.py => 34_wind_data.py} | 0 floris/tools/wind_data.py | 9 +++++++++ tests/wind_data_test.py | 10 +++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) rename examples/{34_wind_data_temporary.py => 34_wind_data.py} (100%) diff --git a/examples/34_wind_data_temporary.py b/examples/34_wind_data.py similarity index 100% rename from examples/34_wind_data_temporary.py rename to examples/34_wind_data.py diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index d51c60769..fa81fc6a3 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -12,6 +12,7 @@ # See https://floris.readthedocs.io for documentation +from abc import abstractmethod import matplotlib.cm as cm import matplotlib.pyplot as plt import numpy as np @@ -26,6 +27,14 @@ class WindDataBase: def __init__(): pass + @abstractmethod + def unpack(self): + """ + Placeholder for child classes of WindDataBase, which each need to implement the unpack() + method. + """ + raise NotImplementedError("unpack() not implemented on {0}".format(self.__class__.__name__)) + def unpack_for_reinitialize(self): """ Return only the variables need for reinitialize diff --git a/tests/wind_data_test.py b/tests/wind_data_test.py index c7954d903..9b76baebc 100644 --- a/tests/wind_data_test.py +++ b/tests/wind_data_test.py @@ -15,8 +15,16 @@ import numpy as np import pytest -from floris.tools import TimeSeries, WindRose +from floris.tools import TimeSeries, WindRose, WindDataBase +class TestClass(WindDataBase): + def __init__(self): + pass + +def test_bad_inheritance(): + test_class = TestClass() + with pytest.raises(NotImplementedError): + test_class.unpack() def test_time_series_instantiation(): wind_directions = np.array([270, 280, 290]) From df8694578a062f5c4e3e05859c054a3f28df289e Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 18 Jan 2024 11:14:01 -0700 Subject: [PATCH 032/101] Inheritance clarified; some cleanup. --- floris/tools/floris_interface.py | 13 +++++++------ floris/tools/wind_data.py | 11 +++-------- tests/wind_data_test.py | 4 ++-- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 0b9c9ed03..8b3e72459 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -28,13 +28,11 @@ power, thrust_coefficient, ) +from floris.tools.wind_data import WindDataBase from floris.tools.cut_plane import CutPlane from floris.type_dec import NDArrayFloat -# from floris.tools import WindData - - class FlorisInterface(LoggingManager): """ FlorisInterface provides a high-level user interface to many of the @@ -191,7 +189,7 @@ def reinitialize( turbine_library_path: str | Path | None = None, solver_settings: dict | None = None, heterogenous_inflow_config=None, - wind_data=None, + wind_data: type[WindDataBase] | None = None, ): # Export the floris object recursively as a dictionary floris_dict = self.floris.as_dict() @@ -400,7 +398,10 @@ def calculate_horizontal_plane( # Compute the cutplane horizontal_plane = CutPlane( - df, self.floris.grid.grid_resolution[0], self.floris.grid.grid_resolution[1], "z" + df, + self.floris.grid.grid_resolution[0], + self.floris.grid.grid_resolution[1], + "z", ) # Reset the fi object back to the turbine grid configuration @@ -784,7 +785,7 @@ def get_farm_AEP( # Check if frequency vector sums to 1.0. If not, raise a warning if np.abs(np.sum(freq) - 1.0) > 0.001: self.logger.warning( - "WARNING: The frequency array provided to get_farm_AEP() " "does not sum to 1.0." + "WARNING: The frequency array provided to get_farm_AEP() does not sum to 1.0." ) # Copy the full wind speed array from the floris object and initialize diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index fa81fc6a3..1152b31b5 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -123,10 +123,7 @@ def __init__( raise ValueError("ti_table first dimension must equal len(wind_directions)") if not ti_table.shape[1] == len(wind_speeds): raise ValueError("ti_table second dimension must equal len(wind_speeds)") - self.ti_table = ti_table - - else: - self.ti_table = None + self.ti_table = ti_table # If price_table is not None, confirm it has correct dimension, # otherwise initialze to all ones @@ -135,10 +132,8 @@ def __init__( raise ValueError("price_table first dimension must equal len(wind_directions)") if not price_table.shape[1] == len(wind_speeds): raise ValueError("price_table second dimension must equal len(wind_speeds)") - self.price_table = price_table - else: - self.price_table = None - + self.price_table = price_table + # Save whether zero occurence cases should be computed self.compute_zero_freq_occurence = compute_zero_freq_occurence diff --git a/tests/wind_data_test.py b/tests/wind_data_test.py index 9b76baebc..666748055 100644 --- a/tests/wind_data_test.py +++ b/tests/wind_data_test.py @@ -17,12 +17,12 @@ from floris.tools import TimeSeries, WindRose, WindDataBase -class TestClass(WindDataBase): +class ChildClassTest(WindDataBase): def __init__(self): pass def test_bad_inheritance(): - test_class = TestClass() + test_class = ChildClassTest() with pytest.raises(NotImplementedError): test_class.unpack() From 2f2b15d8de01bf4a9a43ea91d45e449dcb0815e2 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 18 Jan 2024 11:16:21 -0700 Subject: [PATCH 033/101] Remove copy()s (can point to same memory). --- floris/tools/wind_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 1152b31b5..ff3080335 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -369,8 +369,8 @@ def unpack(self): uniform_frequency = uniform_frequency / uniform_frequency.sum() return ( - self.wind_directions.copy(), - self.wind_speeds.copy(), + self.wind_directions, + self.wind_speeds, uniform_frequency, self.turbulence_intensity, # can be none so can't copy self.prices, # can be none so can't copy From 19b647eeb5449ba343a5558cfbea8b44b3b235a0 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 18 Jan 2024 13:31:06 -0700 Subject: [PATCH 034/101] Small fixes throughout. --- floris/tools/floris_interface.py | 17 ++++-------- floris/tools/wind_data.py | 47 ++++++++++++++++---------------- tests/wind_data_test.py | 7 ++++- 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 8b3e72459..65b4f7413 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -28,8 +28,8 @@ power, thrust_coefficient, ) -from floris.tools.wind_data import WindDataBase from floris.tools.cut_plane import CutPlane +from floris.tools.wind_data import WindDataBase from floris.type_dec import NDArrayFloat @@ -839,11 +839,9 @@ def get_farm_AEP_with_wind_data( direction, frequency of occurrence, and yaw offset. Args: - freq (NDArrayFloat): NumPy array with shape (n_findex) - with the frequencies of each wind direction and - wind speed combination. These frequencies should typically sum - up to 1.0 and are used to weigh the wind farm power for every - condition in calculating the wind farm's AEP. + wind_data: (type(WindDataBase)): TimeSeries or WindRose object containing + the wind conditions over which to calculate the AEP. Should match the wind_data + object passed to reinitialize(). cut_in_wind_speed (float, optional): Wind speed in m/s below which any calculations are ignored and the wind farm is known to produce 0.0 W of power. Note that to prevent problems with the @@ -874,8 +872,6 @@ def get_farm_AEP_with_wind_data( quantities without calculating the wake or adding the wake to the flow field. This can be useful when quantifying the loss in AEP due to wakes. Defaults to *False*. - wind_data: (WindData, optional): Should be the same same object - passed to reinitialize Returns: float: @@ -884,9 +880,8 @@ def get_farm_AEP_with_wind_data( """ # Verify the wind_data object matches FLORIS' initialization - if wind_data is not None: - if wind_data.n_findex != self.floris.flow_field.n_findex: - raise ValueError("WindData object and floris do not have same findex") + if wind_data.n_findex != self.floris.flow_field.n_findex: + raise ValueError("WindData object and floris do not have same findex") # Get freq directly from wind_data freq = wind_data.unpack_freq() diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index ff3080335..f79214135 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -13,19 +13,22 @@ # See https://floris.readthedocs.io for documentation from abc import abstractmethod + import matplotlib.cm as cm import matplotlib.pyplot as plt import numpy as np import pandas as pd from pandas.api.types import CategoricalDtype +from floris.type_dec import NDArrayFloat + -# Define the super lass that WindRose and TimeSeries inherit -# Define functions here that are either the same for both WindRose and -# TimeSeries or will be overloaded class WindDataBase: - def __init__(): - pass + """ + Super class that WindRose and TimeSeries inherit from, enforcing the implmentaton of + unpack() on the child classes and providing the general functions unpack_for_reinitialize() and + unpack_freq(). + """ @abstractmethod def unpack(self): @@ -85,18 +88,18 @@ class WindRose(WindDataBase): def __init__( self, - wind_directions, - wind_speeds, - freq_table=None, - ti_table=None, - price_table=None, - compute_zero_freq_occurence=False, + wind_directions: NDArrayFloat, + wind_speeds: NDArrayFloat, + freq_table: NDArrayFloat | None=None, + ti_table: NDArrayFloat | None=None, + price_table: NDArrayFloat | None=None, + compute_zero_freq_occurence: bool=False, ): if not isinstance(wind_directions, np.ndarray): raise TypeError("wind_directions must be a NumPy array") if not isinstance(wind_speeds, np.ndarray): - raise TypeError("wind_directions must be a NumPy array") + raise TypeError("wind_speeds must be a NumPy array") # Save the wind speeds and directions self.wind_directions = wind_directions @@ -133,7 +136,7 @@ def __init__( if not price_table.shape[1] == len(wind_speeds): raise ValueError("price_table second dimension must equal len(wind_speeds)") self.price_table = price_table - + # Save whether zero occurence cases should be computed self.compute_zero_freq_occurence = compute_zero_freq_occurence @@ -235,15 +238,13 @@ def resample_wind_rose(self, wd_step=None, ws_step=None): if ws_step is None: if len(self.wind_speeds) >= 2: ws_step = self.wind_speeds[1] - self.wind_speeds[0] - else: - # It doesn't matter, just set to 1 - ws_step = 1 + else: # wind rose will have only a single wind speed, and we assume a ws_step of 1 + ws_step = 1.0 if wd_step is None: if len(self.wind_directions) >= 2: wd_step = self.wind_directions[1] - self.wind_directions[0] - else: - # It doesn't matter, just set to 1 - wd_step = 1 + else: # wind rose will have only a single wind direction, and we assume a wd_step of 1 + wd_step = 1.0 # Pass the flat versions of each quantity to build a TimeSeries model time_series = TimeSeries( @@ -342,10 +343,10 @@ class TimeSeries(WindDataBase): def __init__( self, - wind_directions, - wind_speeds, - turbulence_intensity=None, - prices=None, + wind_directions: NDArrayFloat, + wind_speeds: NDArrayFloat, + turbulence_intensity: NDArrayFloat | None=None, + prices: NDArrayFloat | None=None, ): # Wind speeds and wind directions must be the same length if len(wind_directions) != len(wind_speeds): diff --git a/tests/wind_data_test.py b/tests/wind_data_test.py index 666748055..064702f7e 100644 --- a/tests/wind_data_test.py +++ b/tests/wind_data_test.py @@ -15,7 +15,12 @@ import numpy as np import pytest -from floris.tools import TimeSeries, WindRose, WindDataBase +from floris.tools import ( + TimeSeries, + WindDataBase, + WindRose, +) + class ChildClassTest(WindDataBase): def __init__(self): From efd3a9589d30abd516856a36a4b2ca874214917f Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 18 Jan 2024 13:39:50 -0700 Subject: [PATCH 035/101] Python back compatibility type-hinting issue. --- floris/tools/wind_data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index f79214135..34f6bdf7d 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -12,6 +12,8 @@ # See https://floris.readthedocs.io for documentation +from __future__ import annotations + from abc import abstractmethod import matplotlib.cm as cm From 0445472c70cd38d63b47fc9132c0d34927a3a770 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Thu, 18 Jan 2024 16:26:14 -0600 Subject: [PATCH 036/101] Maintain consistent formatting This preserves the style defined in v3 style guide (https://github.com/NREL/floris/discussions/292) --- floris/tools/__init__.py | 20 ++++++++++---------- floris/tools/floris_interface.py | 27 ++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/floris/tools/__init__.py b/floris/tools/__init__.py index 89fc0b856..eb73d8fc1 100644 --- a/floris/tools/__init__.py +++ b/floris/tools/__init__.py @@ -54,14 +54,14 @@ # from floris.tools import ( -# cut_plane, -# floris_interface, -# interface_utilities, -# layout_functions, -# optimization, -# plotting, -# power_rose, -# rews, -# visualization, -# wind_rose, +# cut_plane, +# floris_interface, +# interface_utilities, +# layout_functions, +# optimization, +# plotting, +# power_rose, +# rews, +# visualization, +# wind_rose, # ) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 65b4f7413..2944d5838 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -129,7 +129,12 @@ def calculate_wake( """ if yaw_angles is None: - yaw_angles = np.zeros((self.floris.flow_field.n_findex, self.floris.farm.n_turbines)) + yaw_angles = np.zeros( + ( + self.floris.flow_field.n_findex, + self.floris.farm.n_turbines, + ) + ) self.floris.farm.yaw_angles = yaw_angles # # TODO is this required? @@ -163,7 +168,12 @@ def calculate_no_wake( """ if yaw_angles is None: - yaw_angles = np.zeros((self.floris.flow_field.n_findex, self.floris.farm.n_turbines)) + yaw_angles = np.zeros( + ( + self.floris.flow_field.n_findex, + self.floris.farm.n_turbines, + ) + ) self.floris.farm.yaw_angles = yaw_angles # Initialize solution space @@ -706,11 +716,17 @@ def get_farm_power( if turbine_weights is None: # Default to equal weighing of all turbines when turbine_weights is None turbine_weights = np.ones( - (self.floris.flow_field.n_findex, self.floris.farm.n_turbines) + ( + self.floris.flow_field.n_findex, + self.floris.farm.n_turbines, + ) ) elif len(np.shape(turbine_weights)) == 1: # Deal with situation when 1D array is provided - turbine_weights = np.tile(turbine_weights, (self.floris.flow_field.n_findex, 1)) + turbine_weights = np.tile( + turbine_weights, + (self.floris.flow_field.n_findex, 1), + ) # Calculate all turbine powers and apply weights turbine_powers = self.get_turbine_powers() @@ -807,7 +823,8 @@ def get_farm_AEP( if yaw_angles is not None: yaw_angles_subset = yaw_angles[conditions_to_evaluate] self.reinitialize( - wind_speeds=wind_speeds_subset, wind_directions=wind_directions_subset + wind_speeds=wind_speeds_subset, + wind_directions=wind_directions_subset, ) if no_wake: self.calculate_no_wake(yaw_angles=yaw_angles_subset) From bb83eadad4d1f05639d994ff5c450784e1f90051 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 18 Jan 2024 15:29:19 -0700 Subject: [PATCH 037/101] update ti to array --- examples/12_optimize_yaw_in_parallel.py | 39 ++++---- examples/19_streamlit_demo.py | 90 ++++++++----------- examples/inputs/cc.yaml | 3 +- examples/inputs/emgauss.yaml | 3 +- examples/inputs/gch.yaml | 3 +- examples/inputs/gch_heterogeneous_inflow.yaml | 3 +- examples/inputs/gch_multi_dim_cp_ct.yaml | 3 +- .../inputs/gch_multiple_turbine_types.yaml | 3 +- examples/inputs/jensen.yaml | 3 +- examples/inputs/turbopark.yaml | 3 +- examples/inputs_floating/emgauss_fixed.yaml | 3 +- .../inputs_floating/emgauss_floating.yaml | 3 +- .../emgauss_floating_fixedtilt15.yaml | 3 +- .../emgauss_floating_fixedtilt5.yaml | 3 +- examples/inputs_floating/gch_fixed.yaml | 3 +- examples/inputs_floating/gch_floating.yaml | 3 +- .../gch_floating_defined_floating.yaml | 3 +- tests/data/input_full_v3.yaml | 3 +- 18 files changed, 89 insertions(+), 88 deletions(-) diff --git a/examples/12_optimize_yaw_in_parallel.py b/examples/12_optimize_yaw_in_parallel.py index 33c996dc1..26351f0a7 100644 --- a/examples/12_optimize_yaw_in_parallel.py +++ b/examples/12_optimize_yaw_in_parallel.py @@ -26,9 +26,10 @@ ... """ + def load_floris(): # Load the default example floris object - fi = FlorisInterface("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 + fi = FlorisInterface("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 # fi = FlorisInterface("inputs/cc.yaml") # New CumulativeCurl model # Specify wind farm layout and update in the floris object @@ -63,7 +64,7 @@ def load_windrose(): fi_aep.reinitialize( wind_directions=wind_directions, wind_speeds=wind_speeds, - turbulence_intensity=0.08 # Assume 8% turbulence intensity + turbulence_intensity=[0.08], # Assume 8% turbulence intensity ) # Pour this into a parallel computing interface @@ -105,7 +106,7 @@ def load_windrose(): fi_opt.reinitialize( wind_directions=wind_directions, wind_speeds=wind_speeds, - turbulence_intensity=0.08 # Assume 8% turbulence intensity + turbulence_intensity=[0.08], # Assume 8% turbulence intensity ) # Pour this into a parallel computing interface @@ -127,8 +128,6 @@ def load_windrose(): exploit_layout_symmetry=False, ) - - # Assume linear ramp up at 5-6 m/s and ramp down at 13-14 m/s, # add to table for linear interpolant df_copy_lb = df_opt[df_opt["wind_speed"] == 6.0].copy() @@ -172,27 +171,27 @@ def load_windrose(): # Now calculate helpful variables and then plot wind rose information farm_energy_bl = np.multiply(freq_grid, farm_power_bl) farm_energy_opt = np.multiply(freq_grid, farm_power_opt) - df = pd.DataFrame({ - "wd": wd_grid.flatten(), - "ws": ws_grid.flatten(), - "freq_val": freq_grid.flatten(), - "farm_power_baseline": farm_power_bl.flatten(), - "farm_power_opt": farm_power_opt.flatten(), - "farm_power_relative": farm_power_opt.flatten() / farm_power_bl.flatten(), - "farm_energy_baseline": farm_energy_bl.flatten(), - "farm_energy_opt": farm_energy_opt.flatten(), - "energy_uplift": (farm_energy_opt - farm_energy_bl).flatten(), - "rel_energy_uplift": farm_energy_opt.flatten() / np.sum(farm_energy_bl) - }) + df = pd.DataFrame( + { + "wd": wd_grid.flatten(), + "ws": ws_grid.flatten(), + "freq_val": freq_grid.flatten(), + "farm_power_baseline": farm_power_bl.flatten(), + "farm_power_opt": farm_power_opt.flatten(), + "farm_power_relative": farm_power_opt.flatten() / farm_power_bl.flatten(), + "farm_energy_baseline": farm_energy_bl.flatten(), + "farm_energy_opt": farm_energy_opt.flatten(), + "energy_uplift": (farm_energy_opt - farm_energy_bl).flatten(), + "rel_energy_uplift": farm_energy_opt.flatten() / np.sum(farm_energy_bl), + } + ) # Plot power and AEP uplift across wind direction wd_step = np.diff(fi_aep.floris.flow_field.wind_directions)[0] # Useful variable for plotting fig, ax = plt.subplots(nrows=3, sharex=True) df_8ms = df[df["ws"] == 8.0].reset_index(drop=True) - pow_uplift = 100 * ( - df_8ms["farm_power_opt"] / df_8ms["farm_power_baseline"] - 1 - ) + pow_uplift = 100 * (df_8ms["farm_power_opt"] / df_8ms["farm_power_baseline"] - 1) ax[0].bar( x=df_8ms["wd"], height=pow_uplift, diff --git a/examples/19_streamlit_demo.py b/examples/19_streamlit_demo.py index d40296c19..baa04603f 100644 --- a/examples/19_streamlit_demo.py +++ b/examples/19_streamlit_demo.py @@ -24,7 +24,6 @@ # import seaborn as sns - # """ # This example demonstrates an interactive visual comparison of FLORIS # wake models using streamlit @@ -45,21 +44,16 @@ st.set_page_config(layout="wide") # Parameters -D = 126. # Assume for convenience -floris_model_list = ['jensen','gch','cc','turbopark'] -color_dict = { - 'jensen':'k', - 'gch':'b', - 'cc':'r', - 'turbopark':'c' -} +D = 126.0 # Assume for convenience +floris_model_list = ["jensen", "gch", "cc", "turbopark"] +color_dict = {"jensen": "k", "gch": "b", "cc": "r", "turbopark": "c"} # Streamlit inputs n_turbine_per_row = st.sidebar.slider("Turbines per row", 1, 8, 2, step=1) -n_row = st.sidebar.slider("Number of rows", 1, 8,1, step=1) -spacing = st.sidebar.slider("Turbine spacing (D)", 3., 10., 6., step=0.5) -wind_direction = st.sidebar.slider("Wind Direction", 240., 300., 270., step=1.) -wind_speed = st.sidebar.slider("Wind Speed", 4., 15., 8., step=0.25) +n_row = st.sidebar.slider("Number of rows", 1, 8, 1, step=1) +spacing = st.sidebar.slider("Turbine spacing (D)", 3.0, 10.0, 6.0, step=0.5) +wind_direction = st.sidebar.slider("Wind Direction", 240.0, 300.0, 270.0, step=1.0) +wind_speed = st.sidebar.slider("Wind Speed", 4.0, 15.0, 8.0, step=0.25) turbulence_intensity = st.sidebar.slider("Turbulence Intensity", 0.01, 0.25, 0.06, step=0.01) floris_models = st.sidebar.multiselect("FLORIS Models", floris_model_list, floris_model_list) # floris_models_viz = st.sidebar.multiselect( @@ -67,8 +61,8 @@ # floris_model_list, # floris_model_list # ) -desc_yaw = st.sidebar.checkbox("Descending yaw pattern?",value=False) -front_turbine_yaw = st.sidebar.slider("Upstream yaw angle", -30., 30., 0., step=0.5) +desc_yaw = st.sidebar.checkbox("Descending yaw pattern?", value=False) +front_turbine_yaw = st.sidebar.slider("Upstream yaw angle", -30.0, 30.0, 0.0, step=0.5) # Define the layout X = [] @@ -79,19 +73,18 @@ X.append(D * spacing * x_idx) Y.append(D * spacing * y_idx) -turbine_labels = ['T%02d' % i for i in range(len(X))] +turbine_labels = ["T%02d" % i for i in range(len(X))] # Set up the yaw angle values -yaw_angles_base = np.zeros([1,1,len(X)]) +yaw_angles_base = np.zeros([1, 1, len(X)]) -yaw_angles_yaw = np.zeros([1,1,len(X)]) +yaw_angles_yaw = np.zeros([1, 1, len(X)]) if not desc_yaw: - yaw_angles_yaw[:,:,:n_row] = front_turbine_yaw + yaw_angles_yaw[:, :, :n_row] = front_turbine_yaw else: - decreasing_pattern = np.linspace(front_turbine_yaw,0,n_turbine_per_row) + decreasing_pattern = np.linspace(front_turbine_yaw, 0, n_turbine_per_row) for i in range(n_turbine_per_row): - yaw_angles_yaw[:,:,i*n_row:(i+1)*n_row] = decreasing_pattern[i] - + yaw_angles_yaw[:, :, i * n_row : (i + 1) * n_row] = decreasing_pattern[i] # Get a few quanitities @@ -103,7 +96,7 @@ num_models_to_viz = len(floris_models_viz) # Set up the visualization plot -fig_viz, axarr_viz = plt.subplots(num_models_to_viz,2) +fig_viz, axarr_viz = plt.subplots(num_models_to_viz, 2) # Set up the turbine power plot fig_turb_pow, ax_turb_pow = plt.subplots() @@ -113,9 +106,8 @@ # Now complete all these plots in a loop for fm in floris_models: - # Analyze the base case================================================== - print('Loading: ',fm) + print("Loading: ", fm) fi = FlorisInterface("inputs/%s.yaml" % fm) # Set the layout, wind direction and wind speed @@ -124,26 +116,26 @@ layout_y=Y, wind_speeds=[wind_speed], wind_directions=[wind_direction], - turbulence_intensity=turbulence_intensity + turbulence_intensity=[turbulence_intensity], ) fi.calculate_wake(yaw_angles=yaw_angles_base) - turbine_powers = fi.get_turbine_powers() / 1000. + turbine_powers = fi.get_turbine_powers() / 1000.0 ax_turb_pow.plot( turbine_labels, turbine_powers.flatten(), color=color_dict[fm], - ls='-', - marker='s', - label='%s - baseline' % fm + ls="-", + marker="s", + label="%s - baseline" % fm, ) ax_turb_pow.grid(True) ax_turb_pow.legend() - ax_turb_pow.set_xlabel('Turbine') - ax_turb_pow.set_ylabel('Power (kW)') + ax_turb_pow.set_xlabel("Turbine") + ax_turb_pow.set_ylabel("Power (kW)") # Save the farm power - farm_power_results.append((fm,'base',np.sum(turbine_powers))) + farm_power_results.append((fm, "base", np.sum(turbine_powers))) # If in viz list also visualize if fm in floris_models_viz: @@ -151,15 +143,12 @@ ax = axarr_viz[ax_idx, 0] horizontal_plane_gch = fi.calculate_horizontal_plane( - x_resolution=100, - y_resolution=100, - yaw_angles=yaw_angles_base, - height=90.0 + x_resolution=100, y_resolution=100, yaw_angles=yaw_angles_base, height=90.0 ) - visualize_cut_plane(horizontal_plane_gch, ax=ax, title='%s - baseline' % fm) + visualize_cut_plane(horizontal_plane_gch, ax=ax, title="%s - baseline" % fm) # Analyze the yawed case================================================== - print('Loading: ',fm) + print("Loading: ", fm) fi = FlorisInterface("inputs/%s.yaml" % fm) # Set the layout, wind direction and wind speed @@ -168,26 +157,26 @@ layout_y=Y, wind_speeds=[wind_speed], wind_directions=[wind_direction], - turbulence_intensity=turbulence_intensity + turbulence_intensity=[turbulence_intensity], ) fi.calculate_wake(yaw_angles=yaw_angles_yaw) - turbine_powers = fi.get_turbine_powers() / 1000. + turbine_powers = fi.get_turbine_powers() / 1000.0 ax_turb_pow.plot( turbine_labels, turbine_powers.flatten(), color=color_dict[fm], - ls='--', - marker='o', - label='%s - yawed' % fm + ls="--", + marker="o", + label="%s - yawed" % fm, ) ax_turb_pow.grid(True) ax_turb_pow.legend() - ax_turb_pow.set_xlabel('Turbine') - ax_turb_pow.set_ylabel('Power (kW)') + ax_turb_pow.set_xlabel("Turbine") + ax_turb_pow.set_ylabel("Power (kW)") # Save the farm power - farm_power_results.append((fm,'yawed',np.sum(turbine_powers))) + farm_power_results.append((fm, "yawed", np.sum(turbine_powers))) # If in viz list also visualize if fm in floris_models_viz: @@ -195,12 +184,9 @@ ax = axarr_viz[ax_idx, 1] horizontal_plane_gch = fi.calculate_horizontal_plane( - x_resolution=100, - y_resolution=100, - yaw_angles=yaw_angles_yaw, - height=90.0 + x_resolution=100, y_resolution=100, yaw_angles=yaw_angles_yaw, height=90.0 ) - visualize_cut_plane(horizontal_plane_gch, ax=ax, title='%s - yawed' % fm) + visualize_cut_plane(horizontal_plane_gch, ax=ax, title="%s - yawed" % fm) st.header("Visualizations") st.write(fig_viz) diff --git a/examples/inputs/cc.yaml b/examples/inputs/cc.yaml index 922fadd05..e04c67f34 100644 --- a/examples/inputs/cc.yaml +++ b/examples/inputs/cc.yaml @@ -30,7 +30,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs/emgauss.yaml b/examples/inputs/emgauss.yaml index f984f421d..3bef1c14e 100644 --- a/examples/inputs/emgauss.yaml +++ b/examples/inputs/emgauss.yaml @@ -30,7 +30,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs/gch.yaml b/examples/inputs/gch.yaml index 220fafeac..9a12a08c1 100644 --- a/examples/inputs/gch.yaml +++ b/examples/inputs/gch.yaml @@ -112,7 +112,8 @@ flow_field: ### # The level of turbulence intensity level in the wind. - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 ### # The wind directions to include in the simulation. diff --git a/examples/inputs/gch_heterogeneous_inflow.yaml b/examples/inputs/gch_heterogeneous_inflow.yaml index d7cffa0d5..7bbc2f7e4 100644 --- a/examples/inputs/gch_heterogeneous_inflow.yaml +++ b/examples/inputs/gch_heterogeneous_inflow.yaml @@ -44,7 +44,8 @@ flow_field: - -300. - 300. reference_wind_height: -1 - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs/gch_multi_dim_cp_ct.yaml b/examples/inputs/gch_multi_dim_cp_ct.yaml index 8709fbcc7..6bd32a6bf 100644 --- a/examples/inputs/gch_multi_dim_cp_ct.yaml +++ b/examples/inputs/gch_multi_dim_cp_ct.yaml @@ -33,7 +33,8 @@ flow_field: Hs: 3.01 air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs/gch_multiple_turbine_types.yaml b/examples/inputs/gch_multiple_turbine_types.yaml index ca2d86ea5..47767d999 100644 --- a/examples/inputs/gch_multiple_turbine_types.yaml +++ b/examples/inputs/gch_multiple_turbine_types.yaml @@ -29,7 +29,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: 90.0 # Since multiple defined turbines, must specify explicitly the reference wind height - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs/jensen.yaml b/examples/inputs/jensen.yaml index abb889e0a..0c77a86dd 100644 --- a/examples/inputs/jensen.yaml +++ b/examples/inputs/jensen.yaml @@ -30,7 +30,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs/turbopark.yaml b/examples/inputs/turbopark.yaml index 85bda5fef..10f667a8e 100644 --- a/examples/inputs/turbopark.yaml +++ b/examples/inputs/turbopark.yaml @@ -30,7 +30,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: 90.0 - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/emgauss_fixed.yaml b/examples/inputs_floating/emgauss_fixed.yaml index 9d0b23960..59362d966 100644 --- a/examples/inputs_floating/emgauss_fixed.yaml +++ b/examples/inputs_floating/emgauss_fixed.yaml @@ -30,7 +30,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/emgauss_floating.yaml b/examples/inputs_floating/emgauss_floating.yaml index 1fd66d217..10ffd8d4e 100644 --- a/examples/inputs_floating/emgauss_floating.yaml +++ b/examples/inputs_floating/emgauss_floating.yaml @@ -30,7 +30,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml b/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml index dfb4e3155..3e2f3d7d6 100644 --- a/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml +++ b/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml @@ -26,7 +26,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml b/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml index 67be5dfd3..cf3cba71a 100644 --- a/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml +++ b/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml @@ -26,7 +26,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/gch_fixed.yaml b/examples/inputs_floating/gch_fixed.yaml index 497cecc95..6de82e887 100644 --- a/examples/inputs_floating/gch_fixed.yaml +++ b/examples/inputs_floating/gch_fixed.yaml @@ -26,7 +26,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/gch_floating.yaml b/examples/inputs_floating/gch_floating.yaml index 31ff7c606..be175052e 100644 --- a/examples/inputs_floating/gch_floating.yaml +++ b/examples/inputs_floating/gch_floating.yaml @@ -27,7 +27,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/gch_floating_defined_floating.yaml b/examples/inputs_floating/gch_floating_defined_floating.yaml index 3096e4c2a..79aa9614a 100644 --- a/examples/inputs_floating/gch_floating_defined_floating.yaml +++ b/examples/inputs_floating/gch_floating_defined_floating.yaml @@ -26,7 +26,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/tests/data/input_full_v3.yaml b/tests/data/input_full_v3.yaml index 5cace12df..fe0e31105 100644 --- a/tests/data/input_full_v3.yaml +++ b/tests/data/input_full_v3.yaml @@ -26,7 +26,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: 90.0 - turbulence_intensity: 0.06 + turbulence_intensity: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 From 4fbbced9b971ff4a7fcc0a2788a7334f111aa506 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 18 Jan 2024 15:29:56 -0700 Subject: [PATCH 038/101] ti to array and ruff formatting --- tests/conftest.py | 91 ++++++++++++++++++----------------------------- 1 file changed, 35 insertions(+), 56 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 65a0144a4..6a0f4bcb6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,7 +56,7 @@ def print_test_values( thrusts: list, powers: list, axial_inductions: list, - max_findex_print: int | None =None + max_findex_print: int | None = None, ): n_findex, n_turb = np.shape(average_velocities) if max_findex_print is not None: @@ -66,8 +66,7 @@ def print_test_values( for j in range(n_turb): print( " [{:.7f}, {:.7f}, {:.7f}, {:.7f}],".format( - average_velocities[i,j], thrusts[i,j], powers[i,j], - axial_inductions[i,j] + average_velocities[i, j], thrusts[i, j], powers[i, j], axial_inductions[i, j] ) ) print("],") @@ -114,21 +113,9 @@ def print_test_values( # len(WIND_DIRECTIONS) or len(WIND_SPEEDS N_FINDEX = len(WIND_DIRECTIONS) -X_COORDS = [ - 0.0, - 5 * 126.0, - 10 * 126.0 -] -Y_COORDS = [ - 0.0, - 0.0, - 0.0 -] -Z_COORDS = [ - 90.0, - 90.0, - 90.0 -] +X_COORDS = [0.0, 5 * 126.0, 10 * 126.0] +Y_COORDS = [0.0, 0.0, 0.0] +Z_COORDS = [90.0, 90.0, 90.0] N_TURBINES = len(X_COORDS) ROTOR_DIAMETER = 126.0 TURBINE_GRID_RESOLUTION = 2 @@ -137,38 +124,42 @@ def print_test_values( ## Unit test fixtures + @pytest.fixture def flow_field_fixture(sample_inputs_fixture): flow_field_dict = sample_inputs_fixture.flow_field return FlowField.from_dict(flow_field_dict) + @pytest.fixture def turbine_grid_fixture(sample_inputs_fixture) -> TurbineGrid: turbine_coordinates = np.array(list(zip(X_COORDS, Y_COORDS, Z_COORDS))) - rotor_diameters = ROTOR_DIAMETER * np.ones( (N_TURBINES) ) + rotor_diameters = ROTOR_DIAMETER * np.ones((N_TURBINES)) return TurbineGrid( turbine_coordinates=turbine_coordinates, turbine_diameters=rotor_diameters, wind_directions=np.array(WIND_DIRECTIONS), grid_resolution=TURBINE_GRID_RESOLUTION, - time_series=TIME_SERIES + time_series=TIME_SERIES, ) + @pytest.fixture def flow_field_grid_fixture(sample_inputs_fixture) -> FlowFieldGrid: turbine_coordinates = np.array(list(zip(X_COORDS, Y_COORDS, Z_COORDS))) - rotor_diameters = ROTOR_DIAMETER * np.ones( (N_FINDEX, N_TURBINES) ) + rotor_diameters = ROTOR_DIAMETER * np.ones((N_FINDEX, N_TURBINES)) return FlowFieldGrid( turbine_coordinates=turbine_coordinates, turbine_diameters=rotor_diameters, wind_directions=np.array(WIND_DIRECTIONS), - grid_resolution=[3,2,2] + grid_resolution=[3, 2, 2], ) + @pytest.fixture def points_grid_fixture(sample_inputs_fixture) -> PointsGrid: turbine_coordinates = np.array(list(zip(X_COORDS, Y_COORDS, Z_COORDS))) - rotor_diameters = ROTOR_DIAMETER * np.ones( (N_FINDEX, N_TURBINES) ) + rotor_diameters = ROTOR_DIAMETER * np.ones((N_FINDEX, N_TURBINES)) points_x = [0.0, 10.0] points_y = [0.0, 0.0] points_z = [1.0, 2.0] @@ -183,11 +174,13 @@ def points_grid_fixture(sample_inputs_fixture) -> PointsGrid: points_z=points_z, ) + @pytest.fixture def floris_fixture(): sample_inputs = SampleInputs() return Floris(sample_inputs.floris) + @pytest.fixture def sample_inputs_fixture(): return SampleInputs() @@ -379,7 +372,7 @@ def __init__(self): 50.0, ], }, - "TSR": 8.0 + "TSR": 8.0, } self.turbine_floating = copy.deepcopy(self.turbine) @@ -398,22 +391,18 @@ def __init__(self): self.turbine_floating["correct_cp_ct_for_tilt"] = True self.turbine_multi_dim = copy.deepcopy(self.turbine) - del self.turbine_multi_dim['power_thrust_table']['power'] - del self.turbine_multi_dim['power_thrust_table']['thrust_coefficient'] - del self.turbine_multi_dim['power_thrust_table']['wind_speed'] + del self.turbine_multi_dim["power_thrust_table"]["power"] + del self.turbine_multi_dim["power_thrust_table"]["thrust_coefficient"] + del self.turbine_multi_dim["power_thrust_table"]["wind_speed"] self.turbine_multi_dim["multi_dimensional_cp_ct"] = True - self.turbine_multi_dim['power_thrust_table']["power_thrust_data_file"] = "" + self.turbine_multi_dim["power_thrust_table"]["power_thrust_data_file"] = "" - self.farm = { - "layout_x": X_COORDS, - "layout_y": Y_COORDS, - "turbine_type": [self.turbine] - } + self.farm = {"layout_x": X_COORDS, "layout_y": Y_COORDS, "turbine_type": [self.turbine]} self.flow_field = { "wind_speeds": WIND_SPEEDS, "wind_directions": WIND_DIRECTIONS, - "turbulence_intensity": 0.1, + "turbulence_intensity": [0.1], "wind_shear": 0.12, "wind_veer": 0.0, "air_density": 1.225, @@ -435,7 +424,7 @@ def __init__(self): "beta": 0.077, "dm": 1.0, "ka": 0.38, - "kb": 0.004 + "kb": 0.004, }, "jimenez": { "ad": 0.0, @@ -443,20 +432,15 @@ def __init__(self): "kd": 0.05, }, "empirical_gauss": { - "horizontal_deflection_gain_D": 3.0, - "vertical_deflection_gain_D": -1, - "deflection_rate": 30, - "mixing_gain_deflection": 0.0, - "yaw_added_mixing_gain": 0.0 + "horizontal_deflection_gain_D": 3.0, + "vertical_deflection_gain_D": -1, + "deflection_rate": 30, + "mixing_gain_deflection": 0.0, + "yaw_added_mixing_gain": 0.0, }, }, "wake_velocity_parameters": { - "gauss": { - "alpha": 0.58, - "beta": 0.077, - "ka": 0.38, - "kb": 0.004 - }, + "gauss": {"alpha": 0.58, "beta": 0.077, "ka": 0.38, "kb": 0.004}, "jensen": { "we": 0.05, }, @@ -468,18 +452,15 @@ def __init__(self): "a_f": 3.11, "b_f": -0.68, "c_f": 2.41, - "alpha_mod": 1.0 - }, - "turbopark": { - "A": 0.04, - "sigma_max_rel": 4.0 + "alpha_mod": 1.0, }, + "turbopark": {"A": 0.04, "sigma_max_rel": 4.0}, "empirical_gauss": { "wake_expansion_rates": [0.023, 0.008], "breakpoints_D": [10], "sigma_0_D": 0.28, "smoothing_length_D": 2.0, - "mixing_gain_velocity": 2.0 + "mixing_gain_velocity": 2.0, }, }, "wake_turbulence_parameters": { @@ -487,11 +468,9 @@ def __init__(self): "initial": 0.1, "constant": 0.5, "ai": 0.8, - "downstream": -0.32 + "downstream": -0.32, }, - "wake_induced_mixing": { - "atmospheric_ti_gain": 0.0 - } + "wake_induced_mixing": {"atmospheric_ti_gain": 0.0}, }, "enable_secondary_steering": False, "enable_yaw_added_recovery": False, From 84ae7978c3b3d2a476e94f9fc53e6edf2aec1d22 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 18 Jan 2024 15:30:09 -0700 Subject: [PATCH 039/101] ti to array --- floris/tools/floris_interface.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 0b9c9ed03..7a3f0d3ba 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -181,7 +181,7 @@ def reinitialize( wind_shear: float | None = None, wind_veer: float | None = None, reference_wind_height: float | None = None, - turbulence_intensity: float | None = None, + turbulence_intensity: list[float] | NDArrayFloat | None = None, # turbulence_kinetic_energy=None, air_density: float | None = None, # wake: WakeModelManager = None, @@ -225,6 +225,24 @@ def reinitialize( if heterogenous_inflow_config is not None: flow_field_dict["heterogenous_inflow_config"] = heterogenous_inflow_config + # Handle a special case where: + # wind_speeds | wind_directions are not None + # turbulence_intensity is None + # len(turbulence intensity) != len(wind_directions) + # turbulence_intensity is uniform + # in this case, automatically resize turbulence intensity + # This is the case where user is assuming same ti across all findex + if ((wind_speeds is not None) or (wind_directions is not None)) and ( + turbulence_intensity is None + ): + if len(flow_field_dict["turbulence_intensity"]) != len( + flow_field_dict["wind_directions"] + ): + if len(np.unique(flow_field_dict["turbulence_intensity"])) == 1: + flow_field_dict["turbulence_intensity"] = flow_field_dict[ + "turbulence_intensity" + ][0] * np.ones_like(flow_field_dict["wind_directions"]) + ## Farm if layout_x is not None: farm_dict["layout_x"] = layout_x From 0f011d80ca302540c801fb72a1e35cd71a1e866a Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 18 Jan 2024 15:30:30 -0700 Subject: [PATCH 040/101] ti to array and ruff format --- floris/simulation/flow_field.py | 84 ++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index a53db1fa9..c4bcaf24b 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -39,7 +39,7 @@ class FlowField(BaseClass): wind_veer: float = field(converter=float) wind_shear: float = field(converter=float) air_density: float = field(converter=float) - turbulence_intensity: float = field(converter=float) + turbulence_intensity: float = field(converter=floris_array_converter) reference_wind_height: float = field(converter=float) time_series: bool = field(default=False) heterogenous_inflow_config: dict = field(default=None) @@ -66,6 +66,22 @@ class FlowField(BaseClass): init=False, factory=lambda: np.array([]) ) + @turbulence_intensity.validator + def turbulence_intensity_validator( + self, instance: attrs.Attribute, value: NDArrayFloat + ) -> None: + try: + # Check the turbulence intensity is either length 1 or n_findex + if len(value) != 1 and len(value) != self.n_findex: + raise ValueError("turbulence_intensities should either be length 1 or n_findex") + except TypeError as te: + # Handle the TypeError here + print(f"Caught a TypeError: {te}") + raise TypeError( + "turbulence_intensities must be provided as a list or array. To specify a uniform", + " turbulence intensity, specify as an array of legnth 1", + ) + @wind_directions.validator def wind_directions_validator(self, instance: attrs.Attribute, value: NDArrayFloat) -> None: """Using the validator method to keep the `n_findex` attribute up to date.""" @@ -103,14 +119,16 @@ def het_map_validator(self, instance: attrs.Attribute, value: list | None) -> No "The het_map's first dimension not equal to the FLORIS first dimension." ) - def __attrs_post_init__(self) -> None: if self.heterogenous_inflow_config is not None: self.generate_heterogeneous_wind_map() + # If turbulence_intensity is length 1, then convert it to a uniform array of + # length n_findex + if len(self.turbulence_intensity) == 1: + self.turbulence_intensity = self.turbulence_intensity * np.ones(self.n_findex) def initialize_velocity_field(self, grid: Grid) -> None: - # Create an initial wind profile as a function of height. The values here will # be multiplied with the wind speeds to give the initial wind field. # Since we use grid.z, this is a vertical plane for each turbine @@ -125,11 +143,7 @@ def initialize_velocity_field(self, grid: Grid) -> None: 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 - ) + * np.power(grid.z_sorted, (self.wind_shear - 1), where=grid.z_sorted != 0.0) ) # If no heterogeneous inflow defined, then set all speeds ups to 1.0 if self.het_map is None: @@ -138,10 +152,11 @@ def initialize_velocity_field(self, grid: Grid) -> None: # If heterogeneous flow data is given, the speed ups at the defined # grid locations are determined in either 2 or 3 dimensions. else: - bounds = np.array(list(zip( - self.heterogenous_inflow_config['x'], - self.heterogenous_inflow_config['y'] - ))) + bounds = np.array( + list( + zip(self.heterogenous_inflow_config["x"], self.heterogenous_inflow_config["y"]) + ) + ) hull = ConvexHull(bounds) polygon = Polygon(bounds[hull.vertices]) path = mpltPath.Path(polygon.boundary.coords) @@ -163,16 +178,14 @@ def initialize_velocity_field(self, grid: Grid) -> None: if len(self.het_map[0].points[0]) == 2: speed_ups = self.calculate_speed_ups( - self.het_map, - grid.x_sorted_inertial_frame, - grid.y_sorted_inertial_frame + self.het_map, grid.x_sorted_inertial_frame, grid.y_sorted_inertial_frame ) elif len(self.het_map[0].points[0]) == 3: speed_ups = self.calculate_speed_ups( self.het_map, grid.x_sorted_inertial_frame, grid.y_sorted_inertial_frame, - grid.z_sorted + grid.z_sorted, ) # Create the sheer-law wind profile @@ -185,26 +198,23 @@ def initialize_velocity_field(self, grid: Grid) -> None: self.dudz_initial_sorted = (self.wind_speeds.T * dwind_profile_plane.T).T * speed_ups self.v_initial_sorted = np.zeros( - np.shape(self.u_initial_sorted), - dtype=self.u_initial_sorted.dtype + 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 + np.shape(self.u_initial_sorted), dtype=self.u_initial_sorted.dtype ) self.u_sorted = self.u_initial_sorted.copy() self.v_sorted = self.v_initial_sorted.copy() self.w_sorted = self.w_initial_sorted.copy() - self.turbulence_intensity_field = self.turbulence_intensity * np.ones( - ( - self.n_findex, - grid.n_turbines, - 1, - 1, - ) + self.turbulence_intensity_field = self.turbulence_intensity[ + :, np.newaxis, np.newaxis, np.newaxis + ] + self.turbulence_intensity_field = np.repeat( + self.turbulence_intensity_field, grid.n_turbines, axis=1 ) + self.turbulence_intensity_field_sorted = self.turbulence_intensity_field.copy() def finalize(self, unsorted_indices): @@ -213,12 +223,8 @@ def finalize(self, unsorted_indices): self.w = np.take_along_axis(self.w_sorted, unsorted_indices, axis=1) self.turbulence_intensity_field = np.mean( - np.take_along_axis( - self.turbulence_intensity_field_sorted, - unsorted_indices, - axis=1 - ), - axis=(2,3) + np.take_along_axis(self.turbulence_intensity_field_sorted, unsorted_indices, axis=1), + axis=(2, 3), ) def calculate_speed_ups(self, het_map, x, y, z=None): @@ -226,7 +232,7 @@ def calculate_speed_ups(self, het_map, x, y, z=None): # Calculate the 3-dimensional speed ups; squeeze is needed as the generator # adds an extra dimension speed_ups = np.squeeze( - [het_map[i](x[i:i+1], y[i:i+1], z[i:i+1]) for i in range( len(het_map))], + [het_map[i](x[i : i + 1], y[i : i + 1], z[i : i + 1]) for i in range(len(het_map))], axis=1, ) @@ -234,7 +240,7 @@ def calculate_speed_ups(self, het_map, x, y, z=None): # Calculate the 2-dimensional speed ups; squeeze is needed as the generator # adds an extra dimension speed_ups = np.squeeze( - [het_map[i](x[i:i+1], y[i:i+1]) for i in range(len(het_map))], + [het_map[i](x[i : i + 1], y[i : i + 1]) for i in range(len(het_map))], axis=1, ) @@ -257,10 +263,10 @@ def generate_heterogeneous_wind_map(self): - **y**: A list of y locations at which the speed up factors are defined. - **z** (optional): A list of z locations at which the speed up factors are defined. """ - speed_multipliers = self.heterogenous_inflow_config['speed_multipliers'] - x = self.heterogenous_inflow_config['x'] - y = self.heterogenous_inflow_config['y'] - z = self.heterogenous_inflow_config['z'] + speed_multipliers = self.heterogenous_inflow_config["speed_multipliers"] + x = self.heterogenous_inflow_config["x"] + y = self.heterogenous_inflow_config["y"] + z = self.heterogenous_inflow_config["z"] if z is not None: # Compute the 3-dimensional interpolants for each wind direction From 390e5836e59cdc56b414d5b7372b772608e05ba2 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 18 Jan 2024 15:31:02 -0700 Subject: [PATCH 041/101] ti to array and ruff format --- floris/simulation/solver.py | 509 +++++++++++++++++------------------- 1 file changed, 240 insertions(+), 269 deletions(-) diff --git a/floris/simulation/solver.py b/floris/simulation/solver.py index d32ef9d15..2e848a302 100644 --- a/floris/simulation/solver.py +++ b/floris/simulation/solver.py @@ -56,10 +56,7 @@ def calculate_area_overlap(wake_velocities, freestream_velocities, y_ngrid, z_ng # @profile def sequential_solver( - farm: Farm, - flow_field: FlowField, - grid: TurbineGrid, - model_manager: WakeModelManager + farm: Farm, flow_field: FlowField, grid: TurbineGrid, model_manager: WakeModelManager ) -> None: # Algorithm # For each turbine, calculate its effect on every downstream turbine. @@ -76,25 +73,36 @@ def sequential_solver( v_wake = np.zeros_like(flow_field.v_initial_sorted) w_wake = np.zeros_like(flow_field.w_initial_sorted) - turbine_turbulence_intensity = ( - flow_field.turbulence_intensity - * np.ones((flow_field.n_findex, farm.n_turbines, 1, 1)) - ) - ambient_turbulence_intensity = flow_field.turbulence_intensity + # Set up turbulence arrays + turbine_turbulence_intensity = flow_field.turbulence_intensity[ + :, np.newaxis, np.newaxis, np.newaxis + ] + turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) + + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity + # with extra dimension to reach 4d + ambient_turbulence_intensity = flow_field.turbulence_intensity.copy()[ + :, np.newaxis, np.newaxis, np.newaxis + ] + + # Old code + # turbine_turbulence_intensity = flow_field.turbulence_intensity * np.ones( + # (flow_field.n_findex, farm.n_turbines, 1, 1) + # ) + # ambient_turbulence_intensity = flow_field.turbulence_intensity # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): - # Get the current turbine quantities - x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3)) + x_i = np.mean(grid.x_sorted[:, i : i + 1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3)) + y_i = np.mean(grid.y_sorted[:, i : i + 1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3)) + z_i = np.mean(grid.z_sorted[:, i : i + 1], axis=(2, 3)) z_i = z_i[:, :, None, None] - u_i = flow_field.u_sorted[:, i:i+1] - v_i = flow_field.v_sorted[:, i:i+1] + u_i = flow_field.u_sorted[:, i : i + 1] + v_i = flow_field.v_sorted[:, i : i + 1] ct_i = thrust_coefficient( velocities=flow_field.u_sorted, @@ -108,7 +116,7 @@ def sequential_solver( ix_filter=[i], average_method=grid.average_method, cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions + multidim_condition=flow_field.multidim_conditions, ) # Since we are filtering for the i'th turbine in the thrust coefficient function, # get the first index here (0:1) @@ -125,16 +133,16 @@ def sequential_solver( ix_filter=[i], average_method=grid.average_method, cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions + multidim_condition=flow_field.multidim_conditions, ) # Since we are filtering for the i'th turbine in the axial induction function, # get the first index here (0:1) axial_induction_i = axial_induction_i[:, 0:1, None, None] - turbulence_intensity_i = turbine_turbulence_intensity[:, i:i+1] - yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] - hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] - TSR_i = farm.TSRs_sorted[:, i:i+1, None, None] + turbulence_intensity_i = turbine_turbulence_intensity[:, i : i + 1] + yaw_angle_i = farm.yaw_angles_sorted[:, i : i + 1, None, None] + hub_height_i = farm.hub_heights_sorted[:, i : i + 1, None, None] + rotor_diameter_i = farm.rotor_diameters_sorted[:, i : i + 1, None, None] + TSR_i = farm.TSRs_sorted[:, i : i + 1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i @@ -144,8 +152,8 @@ def sequential_solver( u_i, v_i, flow_field.u_initial_sorted, - grid.y_sorted[:, i:i+1] - y_i, - grid.z_sorted[:, i:i+1], + grid.y_sorted[:, i : i + 1] - y_i, + grid.z_sorted[:, i : i + 1], rotor_diameter_i, hub_height_i, ct_i, @@ -189,12 +197,14 @@ def sequential_solver( u_i, turbulence_intensity_i, v_i, - flow_field.w_sorted[:, i:i+1], - v_wake[:, i:i+1], - w_wake[:, i:i+1], + flow_field.w_sorted[:, i : i + 1], + v_wake[:, i : i + 1], + w_wake[:, i : i + 1], ) gch_gain = 2 - turbine_turbulence_intensity[:, i:i+1] = turbulence_intensity_i + gch_gain * I_mixing + turbine_turbulence_intensity[:, i : i + 1] = ( + turbulence_intensity_i + gch_gain * I_mixing + ) # NOTE: exponential velocity_deficit = model_manager.velocity_model.function( @@ -212,8 +222,7 @@ def sequential_solver( ) wake_field = model_manager.combination_model.function( - wake_field, - velocity_deficit * flow_field.u_initial_sorted + wake_field, velocity_deficit * flow_field.u_initial_sorted ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( @@ -225,10 +234,9 @@ def sequential_solver( ) # Calculate wake overlap for wake-added turbulence (WAT) - area_overlap = ( - np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3)) - / (grid.grid_resolution * grid.grid_resolution) - ) + area_overlap = np.sum( + velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3) + ) / (grid.grid_resolution * grid.grid_resolution) area_overlap = area_overlap[:, :, None, None] # Modify wake added turbulence by wake area overlap @@ -243,8 +251,7 @@ def sequential_solver( # Combine turbine TIs with WAT turbine_turbulence_intensity = np.maximum( - np.sqrt( ti_added ** 2 + ambient_turbulence_intensity ** 2 ), - turbine_turbulence_intensity + np.sqrt(ti_added**2 + ambient_turbulence_intensity**2), turbine_turbulence_intensity ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field @@ -253,8 +260,7 @@ def sequential_solver( flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity flow_field.turbulence_intensity_field_sorted_avg = np.mean( - turbine_turbulence_intensity, - axis=(2,3) + turbine_turbulence_intensity, axis=(2, 3) )[:, :, None, None] @@ -262,9 +268,8 @@ def full_flow_sequential_solver( farm: Farm, flow_field: FlowField, flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, - model_manager: WakeModelManager + model_manager: WakeModelManager, ) -> None: - # Get the flow quantities and turbine performance turbine_grid_farm = copy.deepcopy(farm) turbine_grid_flow_field = copy.deepcopy(flow_field) @@ -300,13 +305,9 @@ def full_flow_sequential_solver( # Use full flow_field here to use the full grid in the wake models deflection_model_args = model_manager.deflection_model.prepare_function( - flow_field_grid, - flow_field - ) - deficit_model_args = model_manager.velocity_model.prepare_function( - flow_field_grid, - flow_field + flow_field_grid, flow_field ) + deficit_model_args = model_manager.velocity_model.prepare_function(flow_field_grid, flow_field) wake_field = np.zeros_like(flow_field.u_initial_sorted) v_wake = np.zeros_like(flow_field.v_initial_sorted) @@ -314,17 +315,16 @@ def full_flow_sequential_solver( # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(flow_field_grid.n_turbines): - # Get the current turbine quantities - x_i = np.mean(turbine_grid.x_sorted[:, i:i+1], axis=(2, 3)) + x_i = np.mean(turbine_grid.x_sorted[:, i : i + 1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(turbine_grid.y_sorted[:, i:i+1], axis=(2, 3)) + y_i = np.mean(turbine_grid.y_sorted[:, i : i + 1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(turbine_grid.z_sorted[:, i:i+1], axis=(2, 3)) + z_i = np.mean(turbine_grid.z_sorted[:, i : i + 1], axis=(2, 3)) z_i = z_i[:, :, None, None] - u_i = turbine_grid_flow_field.u_sorted[:, i:i+1] - v_i = turbine_grid_flow_field.v_sorted[:, i:i+1] + u_i = turbine_grid_flow_field.u_sorted[:, i : i + 1] + v_i = turbine_grid_flow_field.v_sorted[:, i : i + 1] ct_i = thrust_coefficient( velocities=turbine_grid_flow_field.u_sorted, @@ -354,12 +354,13 @@ def full_flow_sequential_solver( # Since we are filtering for the i'th turbine in the axial induction function, # get the first index here (0:1) axial_induction_i = axial_induction_i[:, 0:1, None, None] - turbulence_intensity_i = \ - turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] - yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i:i+1, None, None] - hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i:i+1, None, None] - rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i:i+1, None, None] - TSR_i = turbine_grid_farm.TSRs_sorted[:, i:i+1, None, None] + turbulence_intensity_i = turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[ + :, i : i + 1 + ] + yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i : i + 1, None, None] + hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i : i + 1, None, None] + rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i : i + 1, None, None] + TSR_i = turbine_grid_farm.TSRs_sorted[:, i : i + 1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i @@ -369,8 +370,8 @@ def full_flow_sequential_solver( u_i, v_i, turbine_grid_flow_field.u_initial_sorted, - turbine_grid.y_sorted[:, i:i+1] - y_i, - turbine_grid.z_sorted[:, i:i+1], + turbine_grid.y_sorted[:, i : i + 1] - y_i, + turbine_grid.z_sorted[:, i : i + 1], rotor_diameter_i, hub_height_i, ct_i, @@ -425,8 +426,7 @@ def full_flow_sequential_solver( ) wake_field = model_manager.combination_model.function( - wake_field, - velocity_deficit * flow_field.u_initial_sorted + wake_field, velocity_deficit * flow_field.u_initial_sorted ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field @@ -435,10 +435,7 @@ def full_flow_sequential_solver( def cc_solver( - farm: Farm, - flow_field: FlowField, - grid: TurbineGrid, - model_manager: WakeModelManager + farm: Farm, flow_field: FlowField, grid: TurbineGrid, model_manager: WakeModelManager ) -> None: # <> deflection_model_args = model_manager.deflection_model.prepare_function(grid, flow_field) @@ -450,10 +447,17 @@ def cc_solver( turb_u_wake = np.zeros_like(flow_field.u_initial_sorted) turb_inflow_field = copy.deepcopy(flow_field.u_initial_sorted) - turbine_turbulence_intensity = ( - flow_field.turbulence_intensity * np.ones((flow_field.n_findex, farm.n_turbines, 1, 1)) - ) - ambient_turbulence_intensity = flow_field.turbulence_intensity + # Set up turbulence arrays + turbine_turbulence_intensity = flow_field.turbulence_intensity[ + :, np.newaxis, np.newaxis, np.newaxis + ] + turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) + + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity + # with extra dimension to reach 4d + ambient_turbulence_intensity = flow_field.turbulence_intensity.copy()[ + :, np.newaxis, np.newaxis, np.newaxis + ] shape = (farm.n_turbines,) + np.shape(flow_field.u_initial_sorted) Ctmp = np.zeros((shape)) @@ -464,16 +468,15 @@ def cc_solver( # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): - # Get the current turbine quantities - x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3)) + x_i = np.mean(grid.x_sorted[:, i : i + 1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3)) + y_i = np.mean(grid.y_sorted[:, i : i + 1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3)) + z_i = np.mean(grid.z_sorted[:, i : i + 1], axis=(2, 3)) z_i = z_i[:, :, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] + rotor_diameter_i = farm.rotor_diameters_sorted[:, i : i + 1, None, None] mask2 = ( (grid.x_sorted < x_i + 0.01) @@ -482,8 +485,7 @@ def cc_solver( * (grid.y_sorted > y_i - 0.51 * rotor_diameter_i) ) turb_inflow_field = ( - turb_inflow_field * ~mask2 - + (flow_field.u_initial_sorted - turb_u_wake) * mask2 + turb_inflow_field * ~mask2 + (flow_field.u_initial_sorted - turb_u_wake) * mask2 ) turb_avg_vels = average_velocity(turb_inflow_field) @@ -497,7 +499,7 @@ def cc_solver( turbine_type_map=farm.turbine_type_map_sorted, turbine_power_thrust_tables=farm.turbine_power_thrust_tables, average_method=grid.average_method, - cubature_weights=grid.cubature_weights + cubature_weights=grid.cubature_weights, ) turb_Cts = turb_Cts[:, :, None, None] turb_aIs = axial_induction( @@ -511,12 +513,12 @@ def cc_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[i], average_method=grid.average_method, - cubature_weights=grid.cubature_weights + cubature_weights=grid.cubature_weights, ) turb_aIs = turb_aIs[:, :, None, None] - u_i = turb_inflow_field[:, i:i+1] - v_i = flow_field.v_sorted[:, i:i+1] + u_i = turb_inflow_field[:, i : i + 1] + v_i = flow_field.v_sorted[:, i : i + 1] axial_induction_i = axial_induction( velocities=flow_field.u_sorted, @@ -529,15 +531,15 @@ def cc_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[i], average_method=grid.average_method, - cubature_weights=grid.cubature_weights + cubature_weights=grid.cubature_weights, ) axial_induction_i = axial_induction_i[:, :, None, None] - turbulence_intensity_i = turbine_turbulence_intensity[:, i:i+1] - yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] - hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] - TSR_i = farm.TSRs_sorted[:, i:i+1, None, None] + turbulence_intensity_i = turbine_turbulence_intensity[:, i : i + 1] + yaw_angle_i = farm.yaw_angles_sorted[:, i : i + 1, None, None] + hub_height_i = farm.hub_heights_sorted[:, i : i + 1, None, None] + TSR_i = farm.TSRs_sorted[:, i : i + 1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i @@ -547,11 +549,11 @@ def cc_solver( u_i, v_i, flow_field.u_initial_sorted, - grid.y_sorted[:, i:i+1] - y_i, - grid.z_sorted[:, i:i+1], + grid.y_sorted[:, i : i + 1] - y_i, + grid.z_sorted[:, i : i + 1], rotor_diameter_i, hub_height_i, - turb_Cts[:, i:i+1], + turb_Cts[:, i : i + 1], TSR_i, axial_induction_i, flow_field.wind_shear, @@ -566,7 +568,7 @@ def cc_solver( y_i, effective_yaw_i, turbulence_intensity_i, - turb_Cts[:, i:i+1], + turb_Cts[:, i : i + 1], rotor_diameter_i, **deflection_model_args, ) @@ -582,7 +584,7 @@ def cc_solver( rotor_diameter_i, hub_height_i, yaw_angle_i, - turb_Cts[:, i:i+1], + turb_Cts[:, i : i + 1], TSR_i, axial_induction_i, flow_field.wind_shear, @@ -594,12 +596,14 @@ def cc_solver( u_i, turbulence_intensity_i, v_i, - flow_field.w_sorted[:, i:i+1], - v_wake[:, i:i+1], - w_wake[:, i:i+1], + flow_field.w_sorted[:, i : i + 1], + v_wake[:, i : i + 1], + w_wake[:, i : i + 1], ) gch_gain = 1.0 - turbine_turbulence_intensity[:, i:i+1] = turbulence_intensity_i + gch_gain * I_mixing + turbine_turbulence_intensity[:, i : i + 1] = ( + turbulence_intensity_i + gch_gain * I_mixing + ) turb_u_wake, Ctmp = model_manager.velocity_model.function( i, @@ -618,17 +622,12 @@ def cc_solver( ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensity, - grid.x_sorted, - x_i, - rotor_diameter_i, - turb_aIs + ambient_turbulence_intensity, grid.x_sorted, x_i, rotor_diameter_i, turb_aIs ) # Calculate wake overlap for wake-added turbulence (WAT) area_overlap = 1 - ( - np.sum(turb_u_wake <= 0.05, axis=(2, 3)) - / (grid.grid_resolution * grid.grid_resolution) + np.sum(turb_u_wake <= 0.05, axis=(2, 3)) / (grid.grid_resolution * grid.grid_resolution) ) area_overlap = area_overlap[:, :, None, None] @@ -644,8 +643,7 @@ def cc_solver( # Combine turbine TIs with WAT turbine_turbulence_intensity = np.maximum( - np.sqrt(ti_added ** 2 + ambient_turbulence_intensity ** 2), - turbine_turbulence_intensity + np.sqrt(ti_added**2 + ambient_turbulence_intensity**2), turbine_turbulence_intensity ) flow_field.v_sorted += v_wake @@ -654,8 +652,7 @@ def cc_solver( flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity flow_field.turbulence_intensity_field_sorted_avg = np.mean( - turbine_turbulence_intensity, - axis=(2,3) + turbine_turbulence_intensity, axis=(2, 3) ) @@ -700,13 +697,9 @@ def full_flow_cc_solver( # Use full flow_field here to use the full grid in the wake models deflection_model_args = model_manager.deflection_model.prepare_function( - flow_field_grid, - flow_field - ) - deficit_model_args = model_manager.velocity_model.prepare_function( - flow_field_grid, - flow_field + flow_field_grid, flow_field ) + deficit_model_args = model_manager.velocity_model.prepare_function(flow_field_grid, flow_field) v_wake = np.zeros_like(flow_field.v_initial_sorted) w_wake = np.zeros_like(flow_field.w_initial_sorted) @@ -717,17 +710,16 @@ def full_flow_cc_solver( # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(flow_field_grid.n_turbines): - # Get the current turbine quantities - x_i = np.mean(turbine_grid.x_sorted[:, i:i+1], axis=(2, 3)) + x_i = np.mean(turbine_grid.x_sorted[:, i : i + 1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(turbine_grid.y_sorted[:, i:i+1], axis=(2, 3)) + y_i = np.mean(turbine_grid.y_sorted[:, i : i + 1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(turbine_grid.z_sorted[:, i:i+1], axis=(2, 3)) + z_i = np.mean(turbine_grid.z_sorted[:, i : i + 1], axis=(2, 3)) z_i = z_i[:, :, None, None] - u_i = turbine_grid_flow_field.u_sorted[:, i:i+1] - v_i = turbine_grid_flow_field.v_sorted[:, i:i+1] + u_i = turbine_grid_flow_field.u_sorted[:, i : i + 1] + v_i = turbine_grid_flow_field.v_sorted[:, i : i + 1] turb_avg_vels = average_velocity(turbine_grid_flow_field.u_sorted) turb_Cts = thrust_coefficient( @@ -740,7 +732,7 @@ def full_flow_cc_solver( turbine_type_map=turbine_grid_farm.turbine_type_map_sorted, turbine_power_thrust_tables=turbine_grid_farm.turbine_power_thrust_tables, average_method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights + cubature_weights=turbine_grid.cubature_weights, ) turb_Cts = turb_Cts[:, :, None, None] @@ -755,16 +747,17 @@ def full_flow_cc_solver( turbine_power_thrust_tables=turbine_grid_farm.turbine_power_thrust_tables, ix_filter=[i], average_method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights + cubature_weights=turbine_grid.cubature_weights, ) axial_induction_i = axial_induction_i[:, :, None, None] - turbulence_intensity_i = \ - turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] - yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i:i+1, None, None] - hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i:i+1, None, None] - rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i:i+1, None, None] - TSR_i = turbine_grid_farm.TSRs_sorted[:, i:i+1, None, None] + turbulence_intensity_i = turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[ + :, i : i + 1 + ] + yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i : i + 1, None, None] + hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i : i + 1, None, None] + rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i : i + 1, None, None] + TSR_i = turbine_grid_farm.TSRs_sorted[:, i : i + 1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i @@ -774,11 +767,11 @@ def full_flow_cc_solver( u_i, v_i, turbine_grid_flow_field.u_initial_sorted, - turbine_grid.y_sorted[:, i:i+1] - y_i, - turbine_grid.z_sorted[:, i:i+1], + turbine_grid.y_sorted[:, i : i + 1] - y_i, + turbine_grid.z_sorted[:, i : i + 1], rotor_diameter_i, hub_height_i, - turb_Cts[:, i:i+1], + turb_Cts[:, i : i + 1], TSR_i, axial_induction_i, flow_field.wind_shear, @@ -793,7 +786,7 @@ def full_flow_cc_solver( y_i, effective_yaw_i, turbulence_intensity_i, - turb_Cts[:, i:i+1], + turb_Cts[:, i : i + 1], rotor_diameter_i, **deflection_model_args, ) @@ -809,7 +802,7 @@ def full_flow_cc_solver( rotor_diameter_i, hub_height_i, yaw_angle_i, - turb_Cts[:, i:i+1], + turb_Cts[:, i : i + 1], TSR_i, axial_induction_i, flow_field.wind_shear, @@ -839,10 +832,7 @@ def full_flow_cc_solver( def turbopark_solver( - farm: Farm, - flow_field: FlowField, - grid: TurbineGrid, - model_manager: WakeModelManager + farm: Farm, flow_field: FlowField, grid: TurbineGrid, model_manager: WakeModelManager ) -> None: # Algorithm # For each turbine, calculate its effect on every downstream turbine. @@ -862,24 +852,35 @@ def turbopark_solver( velocity_deficit = np.zeros(shape) deflection_field = np.zeros_like(flow_field.u_initial_sorted) - turbine_turbulence_intensity = ( - flow_field.turbulence_intensity - * np.ones((flow_field.n_findex, farm.n_turbines, 1, 1)) - ) - ambient_turbulence_intensity = flow_field.turbulence_intensity + # Set up turbulence arrays + turbine_turbulence_intensity = flow_field.turbulence_intensity[ + :, np.newaxis, np.newaxis, np.newaxis + ] + turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) + + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity + # with extra dimension to reach 4d + ambient_turbulence_intensity = flow_field.turbulence_intensity.copy()[ + :, np.newaxis, np.newaxis, np.newaxis + ] + + # turbine_turbulence_intensity = flow_field.turbulence_intensity * np.ones( + # (flow_field.n_findex, farm.n_turbines, 1, 1) + # ) + # ambient_turbulence_intensity = flow_field.turbulence_intensity # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): # Get the current turbine quantities - x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3)) + x_i = np.mean(grid.x_sorted[:, i : i + 1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3)) + y_i = np.mean(grid.y_sorted[:, i : i + 1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3)) + z_i = np.mean(grid.z_sorted[:, i : i + 1], axis=(2, 3)) z_i = z_i[:, :, None, None] - u_i = flow_field.u_sorted[:, :, i:i+1] - v_i = flow_field.v_sorted[:, :, i:i+1] + u_i = flow_field.u_sorted[:, :, i : i + 1] + v_i = flow_field.v_sorted[:, :, i : i + 1] Cts = thrust_coefficient( velocities=flow_field.u_sorted, @@ -891,7 +892,7 @@ def turbopark_solver( turbine_type_map=farm.turbine_type_map_sorted, turbine_power_thrust_tables=farm.turbine_power_thrust_tables, average_method=grid.average_method, - cubature_weights=grid.cubature_weights + cubature_weights=grid.cubature_weights, ) ct_i = thrust_coefficient( @@ -905,7 +906,7 @@ def turbopark_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[i], average_method=grid.average_method, - cubature_weights=grid.cubature_weights + cubature_weights=grid.cubature_weights, ) # Since we are filtering for the i'th turbine in the thrust coefficient function, # get the first index here (0:1) @@ -921,28 +922,27 @@ def turbopark_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[i], average_method=grid.average_method, - cubature_weights=grid.cubature_weights + cubature_weights=grid.cubature_weights, ) # Since we are filtering for the i'th turbine in the axial induction function, # get the first index here (0:1) axial_induction_i = axial_induction_i[:, 0:1, None, None] - turbulence_intensity_i = turbine_turbulence_intensity[:, i:i+1] - yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] - hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] - TSR_i = farm.TSRs_sorted[:, i:i+1, None, None] + turbulence_intensity_i = turbine_turbulence_intensity[:, i : i + 1] + yaw_angle_i = farm.yaw_angles_sorted[:, i : i + 1, None, None] + hub_height_i = farm.hub_heights_sorted[:, i : i + 1, None, None] + rotor_diameter_i = farm.rotor_diameters_sorted[:, i : i + 1, None, None] + TSR_i = farm.TSRs_sorted[:, i : i + 1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i - if model_manager.enable_secondary_steering: added_yaw = wake_added_yaw( u_i, v_i, flow_field.u_initial_sorted, - grid.y_sorted[:, i:i+1] - y_i, - grid.z_sorted[:, i:i+1], + grid.y_sorted[:, i : i + 1] - y_i, + grid.z_sorted[:, i : i + 1], rotor_diameter_i, hub_height_i, ct_i, @@ -961,13 +961,13 @@ def turbopark_solver( "and perform a thorough examination of the results." ) for ii in range(i): - x_ii = np.mean(grid.x_sorted[:, ii:ii+1], axis=(2, 3)) + x_ii = np.mean(grid.x_sorted[:, ii : ii + 1], axis=(2, 3)) x_ii = x_ii[:, :, None, None] - y_ii = np.mean(grid.y_sorted[:, ii:ii+1], axis=(2, 3)) + y_ii = np.mean(grid.y_sorted[:, ii : ii + 1], axis=(2, 3)) y_ii = y_ii[:, :, None, None] - yaw_ii = farm.yaw_angles_sorted[:, ii:ii+1, None, None] - turbulence_intensity_ii = turbine_turbulence_intensity[:, ii:ii+1] + yaw_ii = farm.yaw_angles_sorted[:, ii : ii + 1, None, None] + turbulence_intensity_ii = turbine_turbulence_intensity[:, ii : ii + 1] ct_ii = thrust_coefficient( velocities=flow_field.u_sorted, yaw_angles=farm.yaw_angles_sorted, @@ -979,10 +979,10 @@ def turbopark_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[ii], average_method=grid.average_method, - cubature_weights=grid.cubature_weights + cubature_weights=grid.cubature_weights, ) ct_ii = ct_ii[:, 0:1, None, None] - rotor_diameter_ii = farm.rotor_diameters_sorted[:, ii:ii+1, None, None] + rotor_diameter_ii = farm.rotor_diameters_sorted[:, ii : ii + 1, None, None] deflection_field_ii = model_manager.deflection_model.function( x_ii, @@ -994,7 +994,7 @@ def turbopark_solver( **deflection_model_args, ) - deflection_field[:, ii:ii+1, :, :] = deflection_field_ii[:, i:i+1, :, :] + deflection_field[:, ii : ii + 1, :, :] = deflection_field_ii[:, i : i + 1, :, :] if model_manager.enable_transverse_velocities: v_wake, w_wake = calculate_transverse_velocity( @@ -1018,12 +1018,14 @@ def turbopark_solver( u_i, turbulence_intensity_i, v_i, - flow_field.w_sorted[:, :, i:i+1], - v_wake[:, :, i:i+1], - w_wake[:, :, i:i+1], + flow_field.w_sorted[:, :, i : i + 1], + v_wake[:, :, i : i + 1], + w_wake[:, :, i : i + 1], ) gch_gain = 2 - turbine_turbulence_intensity[:, :, i:i+1] = turbulence_intensity_i + gch_gain * I_mixing + turbine_turbulence_intensity[:, :, i : i + 1] = ( + turbulence_intensity_i + gch_gain * I_mixing + ) # NOTE: exponential velocity_deficit = model_manager.velocity_model.function( @@ -1040,26 +1042,20 @@ def turbopark_solver( ) wake_field = model_manager.combination_model.function( - wake_field, - velocity_deficit * flow_field.u_initial_sorted + wake_field, velocity_deficit * flow_field.u_initial_sorted ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensity, - grid.x_sorted, - x_i, - rotor_diameter_i, - axial_induction_i + ambient_turbulence_intensity, grid.x_sorted, x_i, rotor_diameter_i, axial_induction_i ) # TODO: leaving this in for GCH quantities; will need to find another way to # compute area_overlap as the current wake deficit is solved for only upstream # turbines; could use WAT_upstream # Calculate wake overlap for wake-added turbulence (WAT) - area_overlap = ( - np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3)) - / (grid.grid_resolution * grid.grid_resolution) - ) + area_overlap = np.sum( + velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3) + ) / (grid.grid_resolution * grid.grid_resolution) area_overlap = area_overlap[:, :, None, None] # Modify wake added turbulence by wake area overlap @@ -1074,8 +1070,7 @@ def turbopark_solver( # Combine turbine TIs with WAT turbine_turbulence_intensity = np.maximum( - np.sqrt( ti_added ** 2 + ambient_turbulence_intensity ** 2 ), - turbine_turbulence_intensity + np.sqrt(ti_added**2 + ambient_turbulence_intensity**2), turbine_turbulence_intensity ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field @@ -1084,8 +1079,7 @@ def turbopark_solver( flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity flow_field.turbulence_intensity_field_sorted_avg = np.mean( - turbine_turbulence_intensity, - axis=(2, 3) + turbine_turbulence_intensity, axis=(2, 3) ) @@ -1093,16 +1087,13 @@ def full_flow_turbopark_solver( farm: Farm, flow_field: FlowField, flow_field_grid: FlowFieldGrid, - model_manager: WakeModelManager + model_manager: WakeModelManager, ) -> None: raise NotImplementedError("Plotting for the TurbOPark model is not currently implemented.") def empirical_gauss_solver( - farm: Farm, - flow_field: FlowField, - grid: TurbineGrid, - model_manager: WakeModelManager + farm: Farm, flow_field: FlowField, grid: TurbineGrid, model_manager: WakeModelManager ) -> NDArrayFloat: """ Algorithm: @@ -1125,7 +1116,6 @@ def empirical_gauss_solver( NDArrayFloat: wake induced mixing field primarily for use in the full-flow EmGauss solver """ - # <> deflection_model_args = model_manager.deflection_model.prepare_function(grid, flow_field) deficit_model_args = model_manager.velocity_model.prepare_function(grid, flow_field) @@ -1135,33 +1125,31 @@ def empirical_gauss_solver( v_wake = np.zeros_like(flow_field.v_initial_sorted) w_wake = np.zeros_like(flow_field.w_initial_sorted) - x_locs = np.mean(grid.x_sorted, axis=(2, 3))[:,:,None] - downstream_distance_D = x_locs - np.transpose(x_locs, axes=(0,2,1)) - downstream_distance_D = downstream_distance_D / \ - np.repeat(farm.rotor_diameters_sorted[:,:,None], grid.n_turbines, axis=-1) - downstream_distance_D = np.maximum(downstream_distance_D, 0.1) # For ease + x_locs = np.mean(grid.x_sorted, axis=(2, 3))[:, :, None] + downstream_distance_D = x_locs - np.transpose(x_locs, axes=(0, 2, 1)) + downstream_distance_D = downstream_distance_D / np.repeat( + farm.rotor_diameters_sorted[:, :, None], grid.n_turbines, axis=-1 + ) + downstream_distance_D = np.maximum(downstream_distance_D, 0.1) # For ease # Initialize the mixing factor model using TI if specified - initial_mixing_factor = model_manager.turbulence_model.atmospheric_ti_gain*\ - flow_field.turbulence_intensity*np.eye(grid.n_turbines) - mixing_factor = np.repeat( - initial_mixing_factor[None,:,:], - flow_field.n_findex, - axis=0 + initial_mixing_factor = model_manager.turbulence_model.atmospheric_ti_gain * np.eye( + grid.n_turbines ) + mixing_factor = np.repeat(initial_mixing_factor[None, :, :], flow_field.n_findex, axis=0) + mixing_factor = mixing_factor * flow_field.turbulence_intensity[:, None, None] # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): - # Get the current turbine quantities - x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3)) + x_i = np.mean(grid.x_sorted[:, i : i + 1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3)) + y_i = np.mean(grid.y_sorted[:, i : i + 1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3)) + z_i = np.mean(grid.z_sorted[:, i : i + 1], axis=(2, 3)) z_i = z_i[:, :, None, None] - flow_field.u_sorted[:, i:i+1] - flow_field.v_sorted[:, i:i+1] + flow_field.u_sorted[:, i : i + 1] + flow_field.v_sorted[:, i : i + 1] ct_i = thrust_coefficient( velocities=flow_field.u_sorted, @@ -1174,7 +1162,7 @@ def empirical_gauss_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[i], average_method=grid.average_method, - cubature_weights=grid.cubature_weights + cubature_weights=grid.cubature_weights, ) # Since we are filtering for the i'th turbine in the thrust coefficient function, # get the first index here (0:1) @@ -1190,49 +1178,43 @@ def empirical_gauss_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[i], average_method=grid.average_method, - cubature_weights=grid.cubature_weights + cubature_weights=grid.cubature_weights, ) # Since we are filtering for the i'th turbine in the axial induction function, # get the first index here (0:1) axial_induction_i = axial_induction_i[:, 0:1, None, None] - yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] - hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] + yaw_angle_i = farm.yaw_angles_sorted[:, i : i + 1, None, None] + hub_height_i = farm.hub_heights_sorted[:, i : i + 1, None, None] + rotor_diameter_i = farm.rotor_diameters_sorted[:, i : i + 1, None, None] # Secondary steering not currently implemented in EmGauss model # effective_yaw_i = np.zeros_like(yaw_angle_i) # effective_yaw_i += yaw_angle_i average_velocities = average_velocity( - flow_field.u_sorted, - method=grid.average_method, - cubature_weights=grid.cubature_weights + flow_field.u_sorted, method=grid.average_method, cubature_weights=grid.cubature_weights ) tilt_angle_i = farm.calculate_tilt_for_eff_velocities(average_velocities) - tilt_angle_i = tilt_angle_i[:, i:i+1, None, None] + tilt_angle_i = tilt_angle_i[:, i : i + 1, None, None] if model_manager.enable_secondary_steering: - raise NotImplementedError( - "Secondary steering not available for this model.") + raise NotImplementedError("Secondary steering not available for this model.") if model_manager.enable_transverse_velocities: - raise NotImplementedError( - "Transverse velocities not used in this model.") + raise NotImplementedError("Transverse velocities not used in this model.") if model_manager.enable_yaw_added_recovery: # Influence of yawing on turbine's own wake - mixing_factor[:, i:i+1, i] += \ - yaw_added_wake_mixing( - axial_induction_i, - yaw_angle_i, - 1, - model_manager.deflection_model.yaw_added_mixing_gain - ) + mixing_factor[:, i : i + 1, i] += yaw_added_wake_mixing( + axial_induction_i, + yaw_angle_i, + 1, + model_manager.deflection_model.yaw_added_mixing_gain, + ) # Extract total wake induced mixing for turbine i mixing_i = np.linalg.norm( - mixing_factor[:, i:i+1, :, None], - ord=2, axis=2, keepdims=True + mixing_factor[:, i : i + 1, :, None], ord=2, axis=2, keepdims=True ) # Model calculations @@ -1245,7 +1227,7 @@ def empirical_gauss_solver( mixing_i, ct_i, rotor_diameter_i, - **deflection_model_args + **deflection_model_args, ) # NOTE: exponential @@ -1262,30 +1244,28 @@ def empirical_gauss_solver( ct_i, hub_height_i, rotor_diameter_i, - **deficit_model_args + **deficit_model_args, ) wake_field = model_manager.combination_model.function( - wake_field, - velocity_deficit * flow_field.u_initial_sorted + wake_field, velocity_deficit * flow_field.u_initial_sorted ) # Calculate wake overlap for wake-added turbulence (WAT) - area_overlap = np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3))\ - / (grid.grid_resolution * grid.grid_resolution) + area_overlap = np.sum( + velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3) + ) / (grid.grid_resolution * grid.grid_resolution) # Compute wake induced mixing factor - mixing_factor[:,:,i] += \ - area_overlap * model_manager.turbulence_model.function( - axial_induction_i, downstream_distance_D[:,:,i] - ) + mixing_factor[:, :, i] += area_overlap * model_manager.turbulence_model.function( + axial_induction_i, downstream_distance_D[:, :, i] + ) if model_manager.enable_yaw_added_recovery: - mixing_factor[:,:,i] += \ - area_overlap * yaw_added_wake_mixing( + mixing_factor[:, :, i] += area_overlap * yaw_added_wake_mixing( axial_induction_i, yaw_angle_i, - downstream_distance_D[:,:,i], - model_manager.deflection_model.yaw_added_mixing_gain + downstream_distance_D[:, :, i], + model_manager.deflection_model.yaw_added_mixing_gain, ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field @@ -1299,9 +1279,8 @@ def full_flow_empirical_gauss_solver( farm: Farm, flow_field: FlowField, flow_field_grid: FlowFieldGrid, - model_manager: WakeModelManager + model_manager: WakeModelManager, ) -> None: - # Get the flow quantities and turbine performance turbine_grid_farm = copy.deepcopy(farm) turbine_grid_flow_field = copy.deepcopy(flow_field) @@ -1326,16 +1305,12 @@ def full_flow_empirical_gauss_solver( time_series=turbine_grid_flow_field.time_series, ) turbine_grid_farm.expand_farm_properties( - turbine_grid_flow_field.n_findex, - turbine_grid.sorted_coord_indices + turbine_grid_flow_field.n_findex, turbine_grid.sorted_coord_indices ) turbine_grid_flow_field.initialize_velocity_field(turbine_grid) turbine_grid_farm.initialize(turbine_grid.sorted_indices) wim_field = empirical_gauss_solver( - turbine_grid_farm, - turbine_grid_flow_field, - turbine_grid, - model_manager + turbine_grid_farm, turbine_grid_flow_field, turbine_grid, model_manager ) ### Referring to the quantities from above, calculate the wake in the full grid @@ -1352,17 +1327,16 @@ def full_flow_empirical_gauss_solver( # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(flow_field_grid.n_turbines): - # Get the current turbine quantities - x_i = np.mean(turbine_grid.x_sorted[:, i:i+1], axis=(2,3)) + x_i = np.mean(turbine_grid.x_sorted[:, i : i + 1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(turbine_grid.y_sorted[:, i:i+1], axis=(2,3)) + y_i = np.mean(turbine_grid.y_sorted[:, i : i + 1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(turbine_grid.z_sorted[:, i:i+1], axis=(2,3)) + z_i = np.mean(turbine_grid.z_sorted[:, i : i + 1], axis=(2, 3)) z_i = z_i[:, :, None, None] - turbine_grid_flow_field.u_sorted[:, i:i+1] - turbine_grid_flow_field.v_sorted[:, i:i+1] + turbine_grid_flow_field.u_sorted[:, i : i + 1] + turbine_grid_flow_field.v_sorted[:, i : i + 1] ct_i = thrust_coefficient( velocities=turbine_grid_flow_field.u_sorted, @@ -1392,28 +1366,26 @@ def full_flow_empirical_gauss_solver( # Since we are filtering for the i'th turbine in the axial induction function, # get the first index here (0:1) axial_induction_i = axial_induction_i[:, 0:1, None, None] - yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i:i+1, None, None] - hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i:i+1, None, None] - rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i:i+1, None, None] - wake_induced_mixing_i = wim_field[:, i:i+1, :, None].sum(axis=2, keepdims=1) + yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i : i + 1, None, None] + hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i : i + 1, None, None] + rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i : i + 1, None, None] + wake_induced_mixing_i = wim_field[:, i : i + 1, :, None].sum(axis=2, keepdims=1) effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i average_velocities = average_velocity( turbine_grid_flow_field.u_sorted, method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights + cubature_weights=turbine_grid.cubature_weights, ) tilt_angle_i = turbine_grid_farm.calculate_tilt_for_eff_velocities(average_velocities) - tilt_angle_i = tilt_angle_i[:, i:i+1, None, None] + tilt_angle_i = tilt_angle_i[:, i : i + 1, None, None] if model_manager.enable_secondary_steering: - raise NotImplementedError( - "Secondary steering not available for this model.") + raise NotImplementedError("Secondary steering not available for this model.") if model_manager.enable_transverse_velocities: - raise NotImplementedError( - "Transverse velocities not used in this model.") + raise NotImplementedError("Transverse velocities not used in this model.") # Model calculations # NOTE: exponential @@ -1425,7 +1397,7 @@ def full_flow_empirical_gauss_solver( wake_induced_mixing_i, ct_i, rotor_diameter_i, - **deflection_model_args + **deflection_model_args, ) # NOTE: exponential @@ -1442,12 +1414,11 @@ def full_flow_empirical_gauss_solver( ct_i, hub_height_i, rotor_diameter_i, - **deficit_model_args + **deficit_model_args, ) wake_field = model_manager.combination_model.function( - wake_field, - velocity_deficit * flow_field.u_initial_sorted + wake_field, velocity_deficit * flow_field.u_initial_sorted ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field From 9bb615a895e3c1497665dc72980ca7e7b5dd9bc9 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 18 Jan 2024 15:31:19 -0700 Subject: [PATCH 042/101] add ti test --- tests/flow_field_unit_test.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/flow_field_unit_test.py b/tests/flow_field_unit_test.py index 9b0c9a724..7101f996e 100644 --- a/tests/flow_field_unit_test.py +++ b/tests/flow_field_unit_test.py @@ -41,15 +41,13 @@ def test_initialize_velocity_field(flow_field_fixture, turbine_grid_fixture: Tur # which is the input wind speed. shape = np.shape(flow_field_fixture.u_sorted[0, 0, :, :]) n_elements = shape[0] * shape[1] - average = ( - np.sum(flow_field_fixture.u_sorted[:, 0, :, :], axis=(-2, -1)) - / np.array([n_elements]) + average = np.sum(flow_field_fixture.u_sorted[:, 0, :, :], axis=(-2, -1)) / np.array( + [n_elements] ) assert np.array_equal(average, flow_field_fixture.wind_speeds) def test_asdict(flow_field_fixture: FlowField, turbine_grid_fixture: TurbineGrid): - flow_field_fixture.initialize_velocity_field(turbine_grid_fixture) dict1 = flow_field_fixture.as_dict() @@ -58,3 +56,20 @@ def test_asdict(flow_field_fixture: FlowField, turbine_grid_fixture: TurbineGrid dict2 = new_ff.as_dict() assert dict1 == dict2 + + +def test_turbulence_intensity_to_n_findex(flow_field_fixture, turbine_grid_fixture): + # Assert tubulence intensity has same length as n_findex + assert len(flow_field_fixture.turbulence_intensity) == flow_field_fixture.n_findex + + # Assert turbulence_intensity_field is the correct shape + flow_field_fixture.initialize_velocity_field(turbine_grid_fixture) + assert flow_field_fixture.turbulence_intensity_field.shape == (N_FINDEX, N_TURBINES, 1, 1) + + # Assert that turbulence_intensity_field has values matched to turbulence_intensity + for findex in range(N_FINDEX): + for t in range(N_TURBINES): + assert ( + flow_field_fixture.turbulence_intensity[findex] + == flow_field_fixture.turbulence_intensity_field[findex, t, 0, 0] + ) From 8493b59989276c88767d26aa0ab1c147122a77f9 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Thu, 18 Jan 2024 16:32:50 -0600 Subject: [PATCH 043/101] Spell check --- floris/tools/wind_data.py | 22 +++++++++++----------- tests/wind_data_test.py | 7 ++++++- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 34f6bdf7d..6602305db 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -27,7 +27,7 @@ class WindDataBase: """ - Super class that WindRose and TimeSeries inherit from, enforcing the implmentaton of + Super class that WindRose and TimeSeries inherit from, enforcing the implementation of unpack() on the child classes and providing the general functions unpack_for_reinitialize() and unpack_freq(). """ @@ -95,7 +95,7 @@ def __init__( freq_table: NDArrayFloat | None=None, ti_table: NDArrayFloat | None=None, price_table: NDArrayFloat | None=None, - compute_zero_freq_occurence: bool=False, + compute_zero_freq_occurrence: bool=False, ): if not isinstance(wind_directions, np.ndarray): raise TypeError("wind_directions must be a NumPy array") @@ -139,8 +139,8 @@ def __init__( raise ValueError("price_table second dimension must equal len(wind_speeds)") self.price_table = price_table - # Save whether zero occurence cases should be computed - self.compute_zero_freq_occurence = compute_zero_freq_occurence + # Save whether zero occurrence cases should be computed + self.compute_zero_freq_occurrence = compute_zero_freq_occurrence # Build the gridded and flatten versions self._build_gridded_and_flattened_version() @@ -175,9 +175,9 @@ def _build_gridded_and_flattened_version(self): else: self.price_table_flat = None - # Set mask to non-zero frequency cases depending on compute_zero_freq_occurence - if self.compute_zero_freq_occurence: - # If computing zero freq occurences, then this is all True + # Set mask to non-zero frequency cases depending on compute_zero_freq_occurrence + if self.compute_zero_freq_occurrence: + # If computing zero freq occurrences, then this is all True self.non_zero_freq_mask = [True for i in range(len(self.freq_table_flat))] else: self.non_zero_freq_mask = self.freq_table_flat > 0.0 @@ -267,7 +267,7 @@ def plot_wind_rose( legend_kwargs={}, ): """ - This method creates a wind rose plot showing the frequency of occurance + This method creates a wind rose plot showing the frequency of occurrence of the specified wind direction and wind speed bins. If no axis is provided, a new one is created. @@ -331,15 +331,15 @@ class TimeSeries(WindDataBase): """ In FLORIS v4, the TimeSeries class is used to drive FLORIS and optimization operations in which the inflow is by a sequence of wind direction, wind speed - and turbulence intensitity values + and turbulence intensity values Args: wind_directions: NumPy array of wind directions (NDArrayFloat). wind_speeds: NumPy array of wind speeds (NDArrayFloat). turbulence_intensity: NumPy array of wind speeds (NDArrayFloat, optional). - Defatuls to None + Defaults to None prices: NumPy array of electricity prices (NDArrayFloat, optional). - Defatuls to None + Defaults to None """ diff --git a/tests/wind_data_test.py b/tests/wind_data_test.py index 064702f7e..7b0f1ec8d 100644 --- a/tests/wind_data_test.py +++ b/tests/wind_data_test.py @@ -99,7 +99,12 @@ def test_wind_rose_unpack(): assert wind_rose.n_findex == 2 # Now test computing 0-freq cases too - wind_rose = WindRose(wind_directions, wind_speeds, freq_table, compute_zero_freq_occurence=True) + wind_rose = WindRose( + wind_directions, + wind_speeds, + freq_table, + compute_zero_freq_occurrence=True + ) ( wind_directions_unpack, From 27a131cc3b3d1f55541f81c7fbd0cee745f2bab4 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Thu, 18 Jan 2024 17:05:08 -0600 Subject: [PATCH 044/101] Remove outdated comments --- floris/tools/wind_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 6602305db..c7f49e0c2 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -375,8 +375,8 @@ def unpack(self): self.wind_directions, self.wind_speeds, uniform_frequency, - self.turbulence_intensity, # can be none so can't copy - self.prices, # can be none so can't copy + self.turbulence_intensity, + self.prices, ) def _wrap_wind_directions_near_360(self, wind_directions, wd_step): From a6dc0799d0ab27495060293913507c777482b698 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Thu, 18 Jan 2024 17:05:23 -0600 Subject: [PATCH 045/101] Expand docs for wind data unit tests --- tests/wind_data_test.py | 48 +++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/tests/wind_data_test.py b/tests/wind_data_test.py index 7b0f1ec8d..d875b5e86 100644 --- a/tests/wind_data_test.py +++ b/tests/wind_data_test.py @@ -27,35 +27,48 @@ def __init__(self): pass def test_bad_inheritance(): + """ + Verifies that a child class of WindDataBase must implement the unpack method. + """ test_class = ChildClassTest() with pytest.raises(NotImplementedError): test_class.unpack() + def test_time_series_instantiation(): wind_directions = np.array([270, 280, 290]) wind_speeds = np.array([5, 5, 5]) - time_series = TimeSeries(wind_directions, wind_speeds) - time_series + TimeSeries(wind_directions, wind_speeds) def test_time_series_wrong_dimensions(): + """ + Verifies that the TimeSeries class errors when the input wind directions and wind speeds + have different lengths. + """ wind_directions = np.array([270, 280, 290]) wind_speeds = np.array([5, 5]) with pytest.raises(ValueError): TimeSeries(wind_directions, wind_speeds) -def test_wind_rose_wrong_dimensions(): +def test_wind_rose_init(): + """ + The wind directions and wind speeds can have any length, but the frequency + array must have shape (n wind directions, n wind speeds) + """ wind_directions = np.array([270, 280, 290]) wind_speeds = np.array([6, 7]) - # This should be ok: + # This should be ok _ = WindRose(wind_directions, wind_speeds) - # This should be ok + # This should be ok since the frequency array shape matches the wind directions + # and wind speeds _ = WindRose(wind_directions, wind_speeds, np.ones((3, 2))) - # This should raise an error + # This should raise an error since the frequency array shape does not + # match the wind directions and wind speeds with pytest.raises(ValueError): WindRose(wind_directions, wind_speeds, np.ones((3, 3))) @@ -66,10 +79,11 @@ def test_wind_rose_grid(): wind_rose = WindRose(wind_directions, wind_speeds) - # Wd grid has same dimensions as freq table + # Wind direction grid has the same dimensions as the frequency table assert wind_rose.wd_grid.shape == wind_rose.freq_table.shape # Flattening process occurs wd first + # This is each wind direction for each wind speed: np.testing.assert_allclose(wind_rose.wd_flat, [270, 270, 280, 280, 290, 290]) @@ -89,13 +103,14 @@ def test_wind_rose_unpack(): price_table_unpack, ) = wind_rose.unpack() - # Given the above frequency table, would only expect the - # (270 deg, 6 m/s) and (280 deg, 7 m/s) rows + # Given the above frequency table with zeros for a few elements, + # we expect only the (270 deg, 6 m/s) and (280 deg, 7 m/s) rows np.testing.assert_allclose(wind_directions_unpack, [270, 280]) np.testing.assert_allclose(wind_speeds_unpack, [6, 7]) np.testing.assert_allclose(freq_table_unpack, [0.5, 0.5]) - # In this case n_findex == 2 + # In this case n_findex is the length of the wind combinations that are + # non-zero frequency assert wind_rose.n_findex == 2 # Now test computing 0-freq cases too @@ -117,7 +132,7 @@ def test_wind_rose_unpack(): # Expect now to compute all combinations np.testing.assert_allclose(wind_directions_unpack, [270, 270, 280, 280, 290, 290]) - # In this case n_findex == 6 + # In this case n_findex is the total number of wind combinations assert wind_rose.n_findex == 6 @@ -179,15 +194,18 @@ def test_time_series_to_wind_rose(): time_series = TimeSeries(wind_directions, wind_speeds) wind_rose = time_series.to_wind_rose(wd_step=2.0, ws_step=1.0) - # The wind directions should be 260, 262 and 264 + # The wind directions should be 260, 262 and 264 because they're binned + # to the nearest 2 deg increment assert np.allclose(wind_rose.wind_directions, [260, 262, 264]) - # Freq table should have dimension of 3 wd x 1 ws + # Freq table should have dimension of 3 wd x 1 ws because the wind speeds + # are all binned to the same value given the `ws_step` size freq_table = wind_rose.freq_table assert freq_table.shape[0] == 3 assert freq_table.shape[1] == 1 # The frequencies should [2/3, 0, 1/3] + # TODO this one I dont follow, @paul or @misha? assert np.allclose(freq_table.squeeze(), [2 / 3, 0, 1 / 3]) # Test just 2 wind speeds @@ -230,7 +248,9 @@ def test_time_series_to_wind_rose_with_ti(): wind_speeds = np.array([5.0, 5.0, 5.1, 7.2]) turbulence_intensity = np.array([0.5, 1.0, 1.5, 2.0]) time_series = TimeSeries( - wind_directions, wind_speeds, turbulence_intensity=turbulence_intensity + wind_directions, + wind_speeds, + turbulence_intensity=turbulence_intensity, ) wind_rose = time_series.to_wind_rose(wd_step=2.0, ws_step=1.0) From 2e765b14cf92b55ccd1771e7683738e9fe1fae11 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 18 Jan 2024 16:16:51 -0700 Subject: [PATCH 046/101] Add dimensions to doc string --- floris/tools/wind_data.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index c7f49e0c2..a3c583309 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -78,11 +78,14 @@ class WindRose(WindDataBase): wind_directions: NumPy array of wind directions (NDArrayFloat). wind_speeds: NumPy array of wind speeds (NDArrayFloat). freq_table: Frequency table for binned wind direction, wind speed - values (NDArrayFloat, optional). Defaults to None. + values (NDArrayFloat, optional). Must have dimension + (n_wind_directions, n_wind_speeds). Defaults to None. ti_table: Turbulence intensity table for binned wind direction, wind - speed values (NDArrayFloat, optional). Defaults to None. + speed values (NDArrayFloat, optional). Must have dimension + (n_wind_directions, n_wind_speeds). Defaults to None. price_table: Price table for binned binned wind direction, wind - speed values (NDArrayFloat, optional). Defaults to None. + speed values (NDArrayFloat, optional). Must have dimension + (n_wind_directions, n_wind_speeds). Defaults to None. compute_zero_freq_occurrence: Flag indicating whether to compute zero frequency occurrences (bool, optional). Defaults to False. @@ -92,10 +95,10 @@ def __init__( self, wind_directions: NDArrayFloat, wind_speeds: NDArrayFloat, - freq_table: NDArrayFloat | None=None, - ti_table: NDArrayFloat | None=None, - price_table: NDArrayFloat | None=None, - compute_zero_freq_occurrence: bool=False, + freq_table: NDArrayFloat | None = None, + ti_table: NDArrayFloat | None = None, + price_table: NDArrayFloat | None = None, + compute_zero_freq_occurrence: bool = False, ): if not isinstance(wind_directions, np.ndarray): raise TypeError("wind_directions must be a NumPy array") @@ -240,12 +243,12 @@ def resample_wind_rose(self, wd_step=None, ws_step=None): if ws_step is None: if len(self.wind_speeds) >= 2: ws_step = self.wind_speeds[1] - self.wind_speeds[0] - else: # wind rose will have only a single wind speed, and we assume a ws_step of 1 + else: # wind rose will have only a single wind speed, and we assume a ws_step of 1 ws_step = 1.0 if wd_step is None: if len(self.wind_directions) >= 2: wd_step = self.wind_directions[1] - self.wind_directions[0] - else: # wind rose will have only a single wind direction, and we assume a wd_step of 1 + else: # wind rose will have only a single wind direction, and we assume a wd_step of 1 wd_step = 1.0 # Pass the flat versions of each quantity to build a TimeSeries model @@ -347,8 +350,8 @@ def __init__( self, wind_directions: NDArrayFloat, wind_speeds: NDArrayFloat, - turbulence_intensity: NDArrayFloat | None=None, - prices: NDArrayFloat | None=None, + turbulence_intensity: NDArrayFloat | None = None, + prices: NDArrayFloat | None = None, ): # Wind speeds and wind directions must be the same length if len(wind_directions) != len(wind_speeds): From 41af717b6ad277d296fe98a6ec838714c9e8a33a Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 18 Jan 2024 16:19:16 -0700 Subject: [PATCH 047/101] Add context to to_wind_rose test comments --- tests/wind_data_test.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/wind_data_test.py b/tests/wind_data_test.py index d875b5e86..c003ebe22 100644 --- a/tests/wind_data_test.py +++ b/tests/wind_data_test.py @@ -26,6 +26,7 @@ class ChildClassTest(WindDataBase): def __init__(self): pass + def test_bad_inheritance(): """ Verifies that a child class of WindDataBase must implement the unpack method. @@ -115,10 +116,7 @@ def test_wind_rose_unpack(): # Now test computing 0-freq cases too wind_rose = WindRose( - wind_directions, - wind_speeds, - freq_table, - compute_zero_freq_occurrence=True + wind_directions, wind_speeds, freq_table, compute_zero_freq_occurrence=True ) ( @@ -204,8 +202,8 @@ def test_time_series_to_wind_rose(): assert freq_table.shape[0] == 3 assert freq_table.shape[1] == 1 - # The frequencies should [2/3, 0, 1/3] - # TODO this one I dont follow, @paul or @misha? + # The frequencies should [2/3, 0, 1/3] given that 2 of the data points + # fall in the 260 deg bin, 0 in the 262 deg bin and 1 in the 264 deg bin assert np.allclose(freq_table.squeeze(), [2 / 3, 0, 1 / 3]) # Test just 2 wind speeds From 1d07c57259b7ff73f4ba258ca09ef868b9b4297f Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 18 Jan 2024 16:23:41 -0700 Subject: [PATCH 048/101] Add error to reinitialize --- floris/tools/floris_interface.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 2944d5838..c63a172ce 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -213,6 +213,15 @@ def reinitialize( # turbulence intensity using the unpack_for_reinitialize # method if wind_data is not None: + if ( + (wind_directions is not None) + or (wind_speeds is not None) + or (turbulence_intensity is not None) + ): + raise ValueError( + "If wind_data is passed to reinitialize, then do not pass wind_directions, " + "wind_speeds or turbulence_intensity as this is redundant" + ) wind_directions, wind_speeds, turbulence_intensity = wind_data.unpack_for_reinitialize() ## FlowField From 957304025c38dff55e67e25aaeaad50faa64e552 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 11:19:05 -0700 Subject: [PATCH 049/101] pluralize ti --- examples/inputs/cc.yaml | 2 +- examples/inputs/emgauss.yaml | 2 +- examples/inputs/gch.yaml | 2 +- examples/inputs/gch_heterogeneous_inflow.yaml | 2 +- examples/inputs/gch_multi_dim_cp_ct.yaml | 2 +- examples/inputs/gch_multiple_turbine_types.yaml | 2 +- examples/inputs/jensen.yaml | 2 +- examples/inputs/turbopark.yaml | 2 +- examples/inputs_floating/emgauss_fixed.yaml | 2 +- examples/inputs_floating/emgauss_floating.yaml | 2 +- examples/inputs_floating/emgauss_floating_fixedtilt15.yaml | 2 +- examples/inputs_floating/emgauss_floating_fixedtilt5.yaml | 2 +- examples/inputs_floating/gch_fixed.yaml | 2 +- examples/inputs_floating/gch_floating.yaml | 2 +- examples/inputs_floating/gch_floating_defined_floating.yaml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/inputs/cc.yaml b/examples/inputs/cc.yaml index e04c67f34..af62b0021 100644 --- a/examples/inputs/cc.yaml +++ b/examples/inputs/cc.yaml @@ -30,7 +30,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs/emgauss.yaml b/examples/inputs/emgauss.yaml index 3bef1c14e..73344d5ea 100644 --- a/examples/inputs/emgauss.yaml +++ b/examples/inputs/emgauss.yaml @@ -30,7 +30,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs/gch.yaml b/examples/inputs/gch.yaml index 9a12a08c1..2cd76c7f5 100644 --- a/examples/inputs/gch.yaml +++ b/examples/inputs/gch.yaml @@ -112,7 +112,7 @@ flow_field: ### # The level of turbulence intensity level in the wind. - turbulence_intensity: + turbulence_intensities: - 0.06 ### diff --git a/examples/inputs/gch_heterogeneous_inflow.yaml b/examples/inputs/gch_heterogeneous_inflow.yaml index 7bbc2f7e4..86507e287 100644 --- a/examples/inputs/gch_heterogeneous_inflow.yaml +++ b/examples/inputs/gch_heterogeneous_inflow.yaml @@ -44,7 +44,7 @@ flow_field: - -300. - 300. reference_wind_height: -1 - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs/gch_multi_dim_cp_ct.yaml b/examples/inputs/gch_multi_dim_cp_ct.yaml index 6bd32a6bf..e14976050 100644 --- a/examples/inputs/gch_multi_dim_cp_ct.yaml +++ b/examples/inputs/gch_multi_dim_cp_ct.yaml @@ -33,7 +33,7 @@ flow_field: Hs: 3.01 air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs/gch_multiple_turbine_types.yaml b/examples/inputs/gch_multiple_turbine_types.yaml index 47767d999..0ead479a1 100644 --- a/examples/inputs/gch_multiple_turbine_types.yaml +++ b/examples/inputs/gch_multiple_turbine_types.yaml @@ -29,7 +29,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: 90.0 # Since multiple defined turbines, must specify explicitly the reference wind height - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs/jensen.yaml b/examples/inputs/jensen.yaml index 0c77a86dd..6b4ac0dd6 100644 --- a/examples/inputs/jensen.yaml +++ b/examples/inputs/jensen.yaml @@ -30,7 +30,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs/turbopark.yaml b/examples/inputs/turbopark.yaml index 10f667a8e..682b1e801 100644 --- a/examples/inputs/turbopark.yaml +++ b/examples/inputs/turbopark.yaml @@ -30,7 +30,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: 90.0 - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs_floating/emgauss_fixed.yaml b/examples/inputs_floating/emgauss_fixed.yaml index 59362d966..76c3c4513 100644 --- a/examples/inputs_floating/emgauss_fixed.yaml +++ b/examples/inputs_floating/emgauss_fixed.yaml @@ -30,7 +30,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs_floating/emgauss_floating.yaml b/examples/inputs_floating/emgauss_floating.yaml index 10ffd8d4e..965ef7549 100644 --- a/examples/inputs_floating/emgauss_floating.yaml +++ b/examples/inputs_floating/emgauss_floating.yaml @@ -30,7 +30,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml b/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml index 3e2f3d7d6..e8a452325 100644 --- a/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml +++ b/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml @@ -26,7 +26,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml b/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml index cf3cba71a..7732b6213 100644 --- a/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml +++ b/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml @@ -26,7 +26,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs_floating/gch_fixed.yaml b/examples/inputs_floating/gch_fixed.yaml index 6de82e887..be03460e1 100644 --- a/examples/inputs_floating/gch_fixed.yaml +++ b/examples/inputs_floating/gch_fixed.yaml @@ -26,7 +26,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs_floating/gch_floating.yaml b/examples/inputs_floating/gch_floating.yaml index be175052e..09aaa5604 100644 --- a/examples/inputs_floating/gch_floating.yaml +++ b/examples/inputs_floating/gch_floating.yaml @@ -27,7 +27,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/examples/inputs_floating/gch_floating_defined_floating.yaml b/examples/inputs_floating/gch_floating_defined_floating.yaml index 79aa9614a..d540c8d47 100644 --- a/examples/inputs_floating/gch_floating_defined_floating.yaml +++ b/examples/inputs_floating/gch_floating_defined_floating.yaml @@ -26,7 +26,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 From 9be01c23562d836ec1ddda3e9bc7473edd66e3c9 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 11:19:19 -0700 Subject: [PATCH 050/101] pluraralize ti --- examples/12_optimize_yaw_in_parallel.py | 4 ++-- examples/19_streamlit_demo.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/12_optimize_yaw_in_parallel.py b/examples/12_optimize_yaw_in_parallel.py index 26351f0a7..d4be8b8ec 100644 --- a/examples/12_optimize_yaw_in_parallel.py +++ b/examples/12_optimize_yaw_in_parallel.py @@ -64,7 +64,7 @@ def load_windrose(): fi_aep.reinitialize( wind_directions=wind_directions, wind_speeds=wind_speeds, - turbulence_intensity=[0.08], # Assume 8% turbulence intensity + turbulence_intensities=[0.08], # Assume 8% turbulence intensity ) # Pour this into a parallel computing interface @@ -106,7 +106,7 @@ def load_windrose(): fi_opt.reinitialize( wind_directions=wind_directions, wind_speeds=wind_speeds, - turbulence_intensity=[0.08], # Assume 8% turbulence intensity + turbulence_intensities=[0.08], # Assume 8% turbulence intensity ) # Pour this into a parallel computing interface diff --git a/examples/19_streamlit_demo.py b/examples/19_streamlit_demo.py index baa04603f..2e00f6161 100644 --- a/examples/19_streamlit_demo.py +++ b/examples/19_streamlit_demo.py @@ -116,7 +116,7 @@ layout_y=Y, wind_speeds=[wind_speed], wind_directions=[wind_direction], - turbulence_intensity=[turbulence_intensity], + turbulence_intensities=[turbulence_intensity], ) fi.calculate_wake(yaw_angles=yaw_angles_base) @@ -157,7 +157,7 @@ layout_y=Y, wind_speeds=[wind_speed], wind_directions=[wind_direction], - turbulence_intensity=[turbulence_intensity], + turbulence_intensities=[turbulence_intensity], ) fi.calculate_wake(yaw_angles=yaw_angles_yaw) From f93ae9f3ecff316d241bd8361a7f2e8088a098e1 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 11:19:32 -0700 Subject: [PATCH 051/101] pluralize ti --- floris/simulation/flow_field.py | 12 ++--- floris/simulation/solver.py | 41 ++++++--------- floris/simulation/wake_velocity/turbopark.py | 53 +++++++++++--------- 3 files changed, 51 insertions(+), 55 deletions(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index c4bcaf24b..c47baad83 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -39,7 +39,7 @@ class FlowField(BaseClass): wind_veer: float = field(converter=float) wind_shear: float = field(converter=float) air_density: float = field(converter=float) - turbulence_intensity: float = field(converter=floris_array_converter) + turbulence_intensities: float = field(converter=floris_array_converter) reference_wind_height: float = field(converter=float) time_series: bool = field(default=False) heterogenous_inflow_config: dict = field(default=None) @@ -66,8 +66,8 @@ class FlowField(BaseClass): init=False, factory=lambda: np.array([]) ) - @turbulence_intensity.validator - def turbulence_intensity_validator( + @turbulence_intensities.validator + def turbulence_intensities_validator( self, instance: attrs.Attribute, value: NDArrayFloat ) -> None: try: @@ -125,8 +125,8 @@ def __attrs_post_init__(self) -> None: # If turbulence_intensity is length 1, then convert it to a uniform array of # length n_findex - if len(self.turbulence_intensity) == 1: - self.turbulence_intensity = self.turbulence_intensity * np.ones(self.n_findex) + if len(self.turbulence_intensities) == 1: + self.turbulence_intensities = self.turbulence_intensities[0] * np.ones(self.n_findex) def initialize_velocity_field(self, grid: Grid) -> None: # Create an initial wind profile as a function of height. The values here will @@ -208,7 +208,7 @@ def initialize_velocity_field(self, grid: Grid) -> None: self.v_sorted = self.v_initial_sorted.copy() self.w_sorted = self.w_initial_sorted.copy() - self.turbulence_intensity_field = self.turbulence_intensity[ + self.turbulence_intensity_field = self.turbulence_intensities[ :, np.newaxis, np.newaxis, np.newaxis ] self.turbulence_intensity_field = np.repeat( diff --git a/floris/simulation/solver.py b/floris/simulation/solver.py index 2e848a302..e428cd8e3 100644 --- a/floris/simulation/solver.py +++ b/floris/simulation/solver.py @@ -74,23 +74,17 @@ def sequential_solver( w_wake = np.zeros_like(flow_field.w_initial_sorted) # Set up turbulence arrays - turbine_turbulence_intensity = flow_field.turbulence_intensity[ + turbine_turbulence_intensity = flow_field.turbulence_intensities[ :, np.newaxis, np.newaxis, np.newaxis ] turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity # with extra dimension to reach 4d - ambient_turbulence_intensity = flow_field.turbulence_intensity.copy()[ + ambient_turbulence_intensities = flow_field.turbulence_intensities.copy()[ :, np.newaxis, np.newaxis, np.newaxis ] - # Old code - # turbine_turbulence_intensity = flow_field.turbulence_intensity * np.ones( - # (flow_field.n_findex, farm.n_turbines, 1, 1) - # ) - # ambient_turbulence_intensity = flow_field.turbulence_intensity - # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): # Get the current turbine quantities @@ -226,7 +220,7 @@ def sequential_solver( ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensity, + ambient_turbulence_intensities, grid.x_sorted, x_i, rotor_diameter_i, @@ -251,7 +245,7 @@ def sequential_solver( # Combine turbine TIs with WAT turbine_turbulence_intensity = np.maximum( - np.sqrt(ti_added**2 + ambient_turbulence_intensity**2), turbine_turbulence_intensity + np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbine_turbulence_intensity ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field @@ -448,14 +442,14 @@ def cc_solver( turb_inflow_field = copy.deepcopy(flow_field.u_initial_sorted) # Set up turbulence arrays - turbine_turbulence_intensity = flow_field.turbulence_intensity[ + turbine_turbulence_intensity = flow_field.turbulence_intensities[ :, np.newaxis, np.newaxis, np.newaxis ] turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) - # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensities # with extra dimension to reach 4d - ambient_turbulence_intensity = flow_field.turbulence_intensity.copy()[ + ambient_turbulence_intensities = flow_field.turbulence_intensities.copy()[ :, np.newaxis, np.newaxis, np.newaxis ] @@ -622,7 +616,7 @@ def cc_solver( ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensity, grid.x_sorted, x_i, rotor_diameter_i, turb_aIs + ambient_turbulence_intensities, grid.x_sorted, x_i, rotor_diameter_i, turb_aIs ) # Calculate wake overlap for wake-added turbulence (WAT) @@ -643,7 +637,7 @@ def cc_solver( # Combine turbine TIs with WAT turbine_turbulence_intensity = np.maximum( - np.sqrt(ti_added**2 + ambient_turbulence_intensity**2), turbine_turbulence_intensity + np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbine_turbulence_intensity ) flow_field.v_sorted += v_wake @@ -853,22 +847,17 @@ def turbopark_solver( deflection_field = np.zeros_like(flow_field.u_initial_sorted) # Set up turbulence arrays - turbine_turbulence_intensity = flow_field.turbulence_intensity[ + turbine_turbulence_intensity = flow_field.turbulence_intensities[ :, np.newaxis, np.newaxis, np.newaxis ] turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) - # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensities # with extra dimension to reach 4d - ambient_turbulence_intensity = flow_field.turbulence_intensity.copy()[ + ambient_turbulence_intensities = flow_field.turbulence_intensities.copy()[ :, np.newaxis, np.newaxis, np.newaxis ] - # turbine_turbulence_intensity = flow_field.turbulence_intensity * np.ones( - # (flow_field.n_findex, farm.n_turbines, 1, 1) - # ) - # ambient_turbulence_intensity = flow_field.turbulence_intensity - # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): # Get the current turbine quantities @@ -1046,7 +1035,7 @@ def turbopark_solver( ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensity, grid.x_sorted, x_i, rotor_diameter_i, axial_induction_i + ambient_turbulence_intensities, grid.x_sorted, x_i, rotor_diameter_i, axial_induction_i ) # TODO: leaving this in for GCH quantities; will need to find another way to @@ -1070,7 +1059,7 @@ def turbopark_solver( # Combine turbine TIs with WAT turbine_turbulence_intensity = np.maximum( - np.sqrt(ti_added**2 + ambient_turbulence_intensity**2), turbine_turbulence_intensity + np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbine_turbulence_intensity ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field @@ -1136,7 +1125,7 @@ def empirical_gauss_solver( grid.n_turbines ) mixing_factor = np.repeat(initial_mixing_factor[None, :, :], flow_field.n_findex, axis=0) - mixing_factor = mixing_factor * flow_field.turbulence_intensity[:, None, None] + mixing_factor = mixing_factor * flow_field.turbulence_intensities[:, None, None] # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): diff --git a/floris/simulation/wake_velocity/turbopark.py b/floris/simulation/wake_velocity/turbopark.py index 0b52c0476..d9f6057ff 100644 --- a/floris/simulation/wake_velocity/turbopark.py +++ b/floris/simulation/wake_velocity/turbopark.py @@ -50,14 +50,11 @@ class TurbOParkVelocityDeficit(BaseModel): def __attrs_post_init__(self) -> None: lookup_table_matlab_file = Path(__file__).parent / "turbopark_lookup_table.mat" lookup_table_file = scipy.io.loadmat(lookup_table_matlab_file) - dist = lookup_table_file['overlap_lookup_table'][0][0][0][0] - radius_down = lookup_table_file['overlap_lookup_table'][0][0][1][0] - overlap_gauss = lookup_table_file['overlap_lookup_table'][0][0][2] + dist = lookup_table_file["overlap_lookup_table"][0][0][0][0] + radius_down = lookup_table_file["overlap_lookup_table"][0][0][1][0] + overlap_gauss = lookup_table_file["overlap_lookup_table"][0][0][2] self.overlap_gauss_interp = RegularGridInterpolator( - (dist, radius_down), - overlap_gauss, - method='linear', - bounds_error=False + (dist, radius_down), overlap_gauss, method="linear", bounds_error=False ) def prepare_function( @@ -65,7 +62,6 @@ def prepare_function( grid: Grid, flow_field: FlowField, ) -> Dict[str, Any]: - kwargs = { "x": grid.x_sorted, "y": grid.y_sorted, @@ -80,7 +76,7 @@ def function( x_i: np.ndarray, y_i: np.ndarray, z_i: np.ndarray, - ambient_turbulence_intensity: np.ndarray, + ambient_turbulence_intensities: np.ndarray, Cts: np.ndarray, rotor_diameter_i: np.ndarray, rotor_diameters: np.ndarray, @@ -101,7 +97,7 @@ def function( # subsequent runtime warnings. # Here self.NUM_EPS is to avoid precision issues with masking, and is slightly # larger than 0.0 - downstream_mask = (x_i - x >= self.NUM_EPS) + downstream_mask = x_i - x >= self.NUM_EPS x_dist = (x_i - x) * downstream_mask / rotor_diameters # Radial distance between turbine i and the center lines of wakes from all @@ -112,9 +108,9 @@ def function( Cts[:, i:, :, :] = 0.00001 # Characteristic wake widths from all turbines relative to turbine i - dw = characteristic_wake_width(x_dist, ambient_turbulence_intensity, Cts, self.A) + dw = characteristic_wake_width(x_dist, ambient_turbulence_intensities, Cts, self.A) epsilon = 0.25 * np.sqrt( - np.min( 0.5 * (1 + np.sqrt(1 - Cts)) / np.sqrt(1 - Cts), 3, keepdims=True ) + np.min(0.5 * (1 + np.sqrt(1 - Cts)) / np.sqrt(1 - Cts), 3, keepdims=True) ) sigma = rotor_diameters * (epsilon + dw) @@ -131,11 +127,15 @@ def function( delta_image = np.empty(np.shape(u_initial)) * np.nan # Compute deficits for real turbines and for mirrored (image) turbines - delta_real = C * wtg_overlapping * self.overlap_gauss_interp( - (r_dist / sigma, rotor_diameter_i / 2 / sigma) + delta_real = ( + C + * wtg_overlapping + * self.overlap_gauss_interp((r_dist / sigma, rotor_diameter_i / 2 / sigma)) ) - delta_image = C * wtg_overlapping * self.overlap_gauss_interp( - (r_dist_image / sigma, rotor_diameter_i / 2 / sigma) + delta_image = ( + C + * wtg_overlapping + * self.overlap_gauss_interp((r_dist_image / sigma, rotor_diameter_i / 2 / sigma)) ) delta = np.concatenate((delta_real, delta_image), axis=1) @@ -156,10 +156,12 @@ def precalculate_overlap(): for i in range(len(dist)): for j in range(len(radius_down)): if radius_down[j] > 0: + def fun(r, theta): return r * np.exp( - -1 * (r ** 2 + dist[i] ** 2 - 2 * dist[i] * r * np.cos(theta)) / 2 + -1 * (r**2 + dist[i] ** 2 - 2 * dist[i] * r * np.cos(theta)) / 2 ) + out = integrate.dblquad(fun, 0, radius_down[j], lambda x: 0, lambda x: 2 * np.pi)[0] out = out / (np.pi * radius_down[j] ** 2) else: @@ -179,12 +181,17 @@ def characteristic_wake_width(x_dist, TI, Cts, A): alpha = TI * c1 beta = c2 * TI / np.sqrt(Cts) - dw = A * TI / beta * ( - np.sqrt((alpha + beta * x_dist) ** 2 + 1) - - np.sqrt(1 + alpha ** 2) - - np.log( - ((np.sqrt((alpha + beta * x_dist) ** 2 + 1) + 1) * alpha) - / ((np.sqrt(1 + alpha ** 2) + 1) * (alpha + beta * x_dist)) + dw = ( + A + * TI + / beta + * ( + np.sqrt((alpha + beta * x_dist) ** 2 + 1) + - np.sqrt(1 + alpha**2) + - np.log( + ((np.sqrt((alpha + beta * x_dist) ** 2 + 1) + 1) * alpha) + / ((np.sqrt(1 + alpha**2) + 1) * (alpha + beta * x_dist)) + ) ) ) From 1fca2df5a9fc5ebd04a8c6a30c22189dbd054612 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 11:19:49 -0700 Subject: [PATCH 052/101] plluralize ti --- floris/tools/floris_interface.py | 30 ++++--- .../tools/floris_interface_legacy_reader.py | 35 +++----- floris/tools/parallel_computing_interface.py | 82 +++++++++---------- floris/tools/uncertainty_interface.py | 50 +++++------ floris/tools/wind_data.py | 16 ++-- 5 files changed, 96 insertions(+), 117 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index cc980cdfd..a9df9937f 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -189,7 +189,7 @@ def reinitialize( wind_shear: float | None = None, wind_veer: float | None = None, reference_wind_height: float | None = None, - turbulence_intensity: list[float] | NDArrayFloat | None = None, + turbulence_intensities: list[float] | NDArrayFloat | None = None, # turbulence_kinetic_energy=None, air_density: float | None = None, # wake: WakeModelManager = None, @@ -216,13 +216,17 @@ def reinitialize( if ( (wind_directions is not None) or (wind_speeds is not None) - or (turbulence_intensity is not None) + or (turbulence_intensities is not None) ): raise ValueError( "If wind_data is passed to reinitialize, then do not pass wind_directions, " - "wind_speeds or turbulence_intensity as this is redundant" + "wind_speeds or turbulence_intensities as this is redundant" ) - wind_directions, wind_speeds, turbulence_intensity = wind_data.unpack_for_reinitialize() + ( + wind_directions, + wind_speeds, + turbulence_intensities, + ) = wind_data.unpack_for_reinitialize() ## FlowField if wind_speeds is not None: @@ -235,8 +239,8 @@ def reinitialize( flow_field_dict["wind_veer"] = wind_veer if reference_wind_height is not None: flow_field_dict["reference_wind_height"] = reference_wind_height - if turbulence_intensity is not None: - flow_field_dict["turbulence_intensity"] = turbulence_intensity + if turbulence_intensities is not None: + flow_field_dict["turbulence_intensities"] = turbulence_intensities if air_density is not None: flow_field_dict["air_density"] = air_density if heterogenous_inflow_config is not None: @@ -244,20 +248,20 @@ def reinitialize( # Handle a special case where: # wind_speeds | wind_directions are not None - # turbulence_intensity is None + # turbulence_intensities is None # len(turbulence intensity) != len(wind_directions) - # turbulence_intensity is uniform + # turbulence_intensities is uniform # in this case, automatically resize turbulence intensity # This is the case where user is assuming same ti across all findex if ((wind_speeds is not None) or (wind_directions is not None)) and ( - turbulence_intensity is None + turbulence_intensities is None ): - if len(flow_field_dict["turbulence_intensity"]) != len( + if len(flow_field_dict["turbulence_intensities"]) != len( flow_field_dict["wind_directions"] ): - if len(np.unique(flow_field_dict["turbulence_intensity"])) == 1: - flow_field_dict["turbulence_intensity"] = flow_field_dict[ - "turbulence_intensity" + if len(np.unique(flow_field_dict["turbulence_intensities"])) == 1: + flow_field_dict["turbulence_intensities"] = flow_field_dict[ + "turbulence_intensities" ][0] * np.ones_like(flow_field_dict["wind_directions"]) ## Farm diff --git a/floris/tools/floris_interface_legacy_reader.py b/floris/tools/floris_interface_legacy_reader.py index 300b3566c..644f33597 100644 --- a/floris/tools/floris_interface_legacy_reader.py +++ b/floris/tools/floris_interface_legacy_reader.py @@ -64,7 +64,6 @@ class FlorisInterfaceLegacyV2(FlorisInterface): """ def __init__(self, configuration: dict | str | Path, het_map=None): - if not isinstance(configuration, (str, Path, dict)): raise TypeError("The Floris `configuration` must of type 'dict', 'str', or 'Path'.") @@ -113,7 +112,7 @@ def _convert_v24_dictionary_to_v3(dict_legacy): dict_floris["farm"] = { "layout_x": fp["layout_x"], "layout_y": fp["layout_y"], - "turbine_type": ["nrel_5MW"] # Placeholder + "turbine_type": ["nrel_5MW"], # Placeholder } ref_height = fp["specified_wind_height"] @@ -123,7 +122,7 @@ def _convert_v24_dictionary_to_v3(dict_legacy): dict_floris["flow_field"] = { "air_density": fp["air_density"], "reference_wind_height": ref_height, - "turbulence_intensity": fp["turbulence_intensity"][0], + "turbulence_intensities": fp["turbulence_intensities"][0], "wind_directions": [fp["wind_direction"]], "wind_shear": fp["wind_shear"], "wind_speeds": [fp["wind_speed"]], @@ -168,15 +167,9 @@ def _convert_v24_dictionary_to_v3(dict_legacy): turbulence_subdict = copy.deepcopy(wtp) # Save parameter settings to wake dictionary - dict_floris["wake"]["wake_velocity_parameters"] = { - velocity_model_str: velocity_subdict - } - dict_floris["wake"]["wake_deflection_parameters"] = { - deflection_model: deflection_subdict - } - dict_floris["wake"]["wake_turbulence_parameters"] = { - turbulence_model: turbulence_subdict - } + dict_floris["wake"]["wake_velocity_parameters"] = {velocity_model_str: velocity_subdict} + dict_floris["wake"]["wake_deflection_parameters"] = {deflection_model: deflection_subdict} + dict_floris["wake"]["wake_turbulence_parameters"] = {turbulence_model: turbulence_subdict} # Finally add turbine information dict_turbine = { @@ -188,7 +181,7 @@ def _convert_v24_dictionary_to_v3(dict_legacy): "rotor_diameter": tp["rotor_diameter"], "TSR": tp["TSR"], "power_thrust_table": tp["power_thrust_table"], - "ref_air_density": 1.225 # This was implicit in the former input file + "ref_air_density": 1.225, # This was implicit in the former input file } return dict_floris, dict_turbine @@ -208,16 +201,12 @@ def _convert_v24_dictionary_to_v3(dict_legacy): The file format is changed from JSON to YAML and all inputs are mapped, as needed." parser = argparse.ArgumentParser(description=description) - parser.add_argument("-i", - "--input-file", - nargs=1, - required=True, - help="Path to the legacy input file") - parser.add_argument("-o", - "--output-file", - nargs="?", - default=None, - help="Path to write the output file") + parser.add_argument( + "-i", "--input-file", nargs=1, required=True, help="Path to the legacy input file" + ) + parser.add_argument( + "-o", "--output-file", nargs="?", default=None, help="Path to write the output file" + ) args = parser.parse_args() # Specify paths diff --git a/floris/tools/parallel_computing_interface.py b/floris/tools/parallel_computing_interface.py index 1192fcfdb..f17093395 100644 --- a/floris/tools/parallel_computing_interface.py +++ b/floris/tools/parallel_computing_interface.py @@ -11,11 +11,7 @@ from floris.tools.uncertainty_interface import FlorisInterface, UncertaintyInterface -def _load_local_floris_object( - fi_dict, - unc_pmfs=None, - fix_yaw_in_relative_frame=False -): +def _load_local_floris_object(fi_dict, unc_pmfs=None, fix_yaw_in_relative_frame=False): # Load local FLORIS object if unc_pmfs is None: fi = FlorisInterface(fi_dict) @@ -76,7 +72,7 @@ def __init__( interface="multiprocessing", # Options are 'multiprocessing', 'mpi4py' or 'concurrent' use_mpi4py=None, propagate_flowfield_from_workers=False, - print_timings=False + print_timings=False, ): """A wrapper around the nominal floris_interface class that adds parallel computing to common FlorisInterface properties. @@ -116,14 +112,17 @@ def __init__( if interface == "mpi4py": import mpi4py.futures as mp + self._PoolExecutor = mp.MPIPoolExecutor elif interface == "multiprocessing": import multiprocessing as mp + self._PoolExecutor = mp.Pool if max_workers is None: max_workers = mp.cpu_count() elif interface == "concurrent": from concurrent.futures import ProcessPoolExecutor + self._PoolExecutor = ProcessPoolExecutor else: raise UserWarning( @@ -166,7 +165,7 @@ def reinitialize( wind_shear=None, wind_veer=None, reference_wind_height=None, - turbulence_intensity=None, + turbulence_intensities=None, air_density=None, layout=None, layout_x=None, @@ -193,7 +192,7 @@ def reinitialize( wind_shear=wind_shear, wind_veer=wind_veer, reference_wind_height=reference_wind_height, - turbulence_intensity=turbulence_intensity, + turbulence_intensities=turbulence_intensities, air_density=air_density, layout_x=layout_x, layout_y=layout_y, @@ -215,11 +214,13 @@ def reinitialize( def _preprocessing(self, yaw_angles=None): # Format yaw angles if yaw_angles is None: - yaw_angles = np.zeros(( - self.fi.floris.flow_field.n_wind_directions, - self.fi.floris.flow_field.n_wind_speeds, - self.fi.floris.farm.n_turbines - )) + yaw_angles = np.zeros( + ( + self.fi.floris.flow_field.n_wind_directions, + self.fi.floris.flow_field.n_wind_speeds, + self.fi.floris.farm.n_turbines, + ) + ) # Prepare settings n_wind_direction_splits = self.n_wind_direction_splits @@ -232,12 +233,10 @@ def _preprocessing(self, yaw_angles=None): # Prepare the input arguments for parallel execution fi_dict = self.fi.floris.as_dict() wind_direction_id_splits = np.array_split( - np.arange(self.fi.floris.flow_field.n_wind_directions), - n_wind_direction_splits + np.arange(self.fi.floris.flow_field.n_wind_directions), n_wind_direction_splits ) wind_speed_id_splits = np.array_split( - np.arange(self.fi.floris.flow_field.n_wind_speeds), - n_wind_speed_splits + np.arange(self.fi.floris.flow_field.n_wind_speeds), n_wind_speed_splits ) multiargs = [] for wd_id_split in wind_direction_id_splits: @@ -245,7 +244,7 @@ def _preprocessing(self, yaw_angles=None): fi_dict_split = copy.deepcopy(fi_dict) wind_directions = self.fi.floris.flow_field.wind_directions[wd_id_split] wind_speeds = self.fi.floris.flow_field.wind_speeds[ws_id_split] - yaw_angles_subset = yaw_angles[wd_id_split[0]:wd_id_split[-1]+1, ws_id_split, :] + yaw_angles_subset = yaw_angles[wd_id_split[0] : wd_id_split[-1] + 1, ws_id_split, :] fi_dict_split["flow_field"]["wind_directions"] = wind_directions fi_dict_split["flow_field"]["wind_speeds"] = wind_speeds @@ -257,7 +256,7 @@ def _preprocessing(self, yaw_angles=None): fi_dict_split, self.fi.fi.het_map, self.fi.unc_pmfs, - self.fi.fix_yaw_in_relative_frame + self.fi.fix_yaw_in_relative_frame, ) multiargs.append((fi_information, yaw_angles_subset)) @@ -271,16 +270,15 @@ def _merge_subsets(self, field, subset): [ eval("f.{:s}".format(field)) for f in subset[ - wii - * self.n_wind_direction_splits:(wii+1) + wii * self.n_wind_direction_splits : (wii + 1) * self.n_wind_direction_splits ] ], - axis=0 + axis=0, ) for wii in range(self.n_wind_speed_splits) ], - axis=1 + axis=1, ) def _postprocessing(self, output): @@ -292,12 +290,14 @@ def _postprocessing(self, output): turbine_powers = np.concatenate( [ np.concatenate( - power_subsets[self.n_wind_speed_splits*(ii):self.n_wind_speed_splits*(ii+1)], - axis=1 + power_subsets[ + self.n_wind_speed_splits * (ii) : self.n_wind_speed_splits * (ii + 1) + ], + axis=1, ) for ii in range(self.n_wind_direction_splits) ], - axis=0 + axis=0, ) # Optionally, also merge flow field dictionaries from individual floris solutions @@ -310,8 +310,7 @@ def _postprocessing(self, output): self.floris.flow_field.v = self._merge_subsets("v", flowfield_subsets) self.floris.flow_field.w = self._merge_subsets("w", flowfield_subsets) self.floris.flow_field.turbulence_intensity_field = self._merge_subsets( - "turbulence_intensity_field", - flowfield_subsets + "turbulence_intensity_field", flowfield_subsets ) return turbine_powers @@ -334,9 +333,7 @@ def get_turbine_powers(self, yaw_angles=None): out = p.starmap(_get_turbine_powers_serial, multiargs) else: out = p.map( - _get_turbine_powers_serial, - [j[0] for j in multiargs], - [j[1] for j in multiargs] + _get_turbine_powers_serial, [j[0] for j in multiargs], [j[1] for j in multiargs] ) # out = list(out) t_execution = timerpc() - t1 @@ -366,7 +363,7 @@ def get_farm_power(self, yaw_angles=None, turbine_weights=None): ( self.fi.floris.flow_field.n_wind_directions, self.fi.floris.flow_field.n_wind_speeds, - self.fi.floris.farm.n_turbines + self.fi.floris.farm.n_turbines, ) ) elif len(np.shape(turbine_weights)) == 1: @@ -376,8 +373,8 @@ def get_farm_power(self, yaw_angles=None, turbine_weights=None): ( self.fi.floris.flow_field.n_wind_directions, self.fi.floris.flow_field.n_wind_speeds, - 1 - ) + 1, + ), ) # Calculate all turbine powers and apply weights @@ -450,7 +447,7 @@ def get_farm_AEP( cut_out_wind_speed=cut_out_wind_speed, yaw_angles=yaw_angles, turbine_weights=turbine_weights, - no_wake=no_wake + no_wake=no_wake, ) # Verify dimensions of the variable "freq" @@ -487,8 +484,8 @@ def get_farm_AEP( if yaw_angles is not None: yaw_angles_subset = yaw_angles[:, conditions_to_evaluate] self.fi.reinitialize(wind_speeds=wind_speeds_subset) - farm_power[:, conditions_to_evaluate] = ( - self.get_farm_power(yaw_angles=yaw_angles_subset, turbine_weights=turbine_weights) + farm_power[:, conditions_to_evaluate] = self.get_farm_power( + yaw_angles=yaw_angles_subset, turbine_weights=turbine_weights ) # Finally, calculate AEP in GWh @@ -505,14 +502,13 @@ def optimize_yaw_angles( maximum_yaw_angle=25.0, yaw_angles_baseline=None, x0=None, - Ny_passes=[5,4], + Ny_passes=[5, 4], turbine_weights=None, exclude_downstream_turbines=True, exploit_layout_symmetry=True, verify_convergence=False, print_worker_progress=False, # Recommended disabled to avoid clutter. Useful for debugging ): - # Prepare the inputs to each core for multiprocessing module t0 = timerpc() multiargs = self._preprocessing() @@ -550,13 +546,15 @@ def optimize_yaw_angles( [j[7] for j in multiargs], [j[8] for j in multiargs], [j[9] for j in multiargs], - [j[10] for j in multiargs] + [j[10] for j in multiargs], ) t2 = timerpc() # Combine all solutions from multiprocessing into single dataframe - df_opt = pd.concat(df_opt_splits, axis=0).reset_index(drop=True).sort_values( - by=["wind_direction", "wind_speed", "turbulence_intensity"] + df_opt = ( + pd.concat(df_opt_splits, axis=0) + .reset_index(drop=True) + .sort_values(by=["wind_direction", "wind_speed", "turbulence_intensity"]) ) t3 = timerpc() diff --git a/floris/tools/uncertainty_interface.py b/floris/tools/uncertainty_interface.py index 7f2b833ef..70ac24d56 100644 --- a/floris/tools/uncertainty_interface.py +++ b/floris/tools/uncertainty_interface.py @@ -183,10 +183,9 @@ def _expand_wind_directions_and_yaw_angles(self): # Expand wind direction and yaw angle array into the direction # of uncertainty over the ambient wind direction. - wd_array_probablistic = np.vstack([ - np.expand_dims(wd_array_nominal, axis=0) + dy - for dy in unc_pmfs["wd_unc"] - ]) + wd_array_probablistic = np.vstack( + [np.expand_dims(wd_array_nominal, axis=0) + dy for dy in unc_pmfs["wd_unc"]] + ) if self.fix_yaw_in_relative_frame: # The relative yaw angle is fixed and always has the nominal @@ -196,10 +195,9 @@ def _expand_wind_directions_and_yaw_angles(self): # wind directions. This can also be really fast, since it would # not require any additional calculations compared to the # non-uncertainty FLORIS evaluation. - yaw_angles_probablistic = np.vstack([ - np.expand_dims(yaw_angles_nominal, axis=0) - for _ in unc_pmfs["wd_unc"] - ]) + yaw_angles_probablistic = np.vstack( + [np.expand_dims(yaw_angles_nominal, axis=0) for _ in unc_pmfs["wd_unc"]] + ) else: # Fix yaw angles in the absolute (compass) reference frame, # meaning that for each probablistic wind direction evaluation, @@ -208,10 +206,9 @@ def _expand_wind_directions_and_yaw_angles(self): # direction 3 deg above the nominal value means that we evaluate # it with a relative yaw angle that is 3 deg below its nominal # value. - yaw_angles_probablistic = np.vstack([ - np.expand_dims(yaw_angles_nominal, axis=0) - dy - for dy in unc_pmfs["wd_unc"] - ]) + yaw_angles_probablistic = np.vstack( + [np.expand_dims(yaw_angles_nominal, axis=0) - dy for dy in unc_pmfs["wd_unc"]] + ) self.wd_array_probablistic = wd_array_probablistic self.yaw_angles_probablistic = yaw_angles_probablistic @@ -231,10 +228,7 @@ def copy(self): return fi_unc_copy def reinitialize_uncertainty( - self, - unc_options=None, - unc_pmfs=None, - fix_yaw_in_relative_frame=None + self, unc_options=None, unc_pmfs=None, fix_yaw_in_relative_frame=None ): """Reinitialize the wind direction and yaw angle probability distributions used in evaluating FLORIS. Must either specify @@ -332,7 +326,7 @@ def reinitialize( wind_shear=None, wind_veer=None, reference_wind_height=None, - turbulence_intensity=None, + turbulence_intensities=None, air_density=None, layout_x=None, layout_y=None, @@ -350,7 +344,7 @@ def reinitialize( wind_shear=wind_shear, wind_veer=wind_veer, reference_wind_height=reference_wind_height, - turbulence_intensity=turbulence_intensity, + turbulence_intensities=turbulence_intensities, air_density=air_density, layout_x=layout_x, layout_y=layout_y, @@ -417,8 +411,7 @@ def get_turbine_powers(self): # Format into conventional floris format by reshaping wd_array_probablistic = np.reshape(self.wd_array_probablistic, -1) yaw_angles_probablistic = np.reshape( - self.yaw_angles_probablistic, - (-1, num_ws, num_turbines) + self.yaw_angles_probablistic, (-1, num_ws, num_turbines) ) # Wrap wind direction array around 360 deg @@ -430,7 +423,7 @@ def get_turbine_powers(self): np.append(yaw_angles_probablistic, wd_exp, axis=2), axis=0, return_index=True, - return_inverse=True + return_inverse=True, ) wd_array_probablistic_min = wd_array_probablistic[id_unq] yaw_angles_probablistic_min = yaw_angles_probablistic[id_unq, :, :] @@ -449,8 +442,7 @@ def get_turbine_powers(self): # Reshape solutions back to full set power_probablistic = turbine_powers[id_unq_rev, :] power_probablistic = np.reshape( - power_probablistic, - (num_wd_unc, num_wd, num_ws, num_turbines) + power_probablistic, (num_wd_unc, num_wd, num_ws, num_turbines) ) # Calculate probability weighing terms @@ -495,18 +487,14 @@ def get_farm_power(self, turbine_weights=None): ( self.floris.flow_field.n_wind_directions, self.floris.flow_field.n_wind_speeds, - self.floris.farm.n_turbines + self.floris.farm.n_turbines, ) ) elif len(np.shape(turbine_weights)) == 1: # Deal with situation when 1D array is provided turbine_weights = np.tile( turbine_weights, - ( - self.floris.flow_field.n_wind_directions, - self.floris.flow_field.n_wind_speeds, - 1 - ) + (self.floris.flow_field.n_wind_directions, self.floris.flow_field.n_wind_speeds, 1), ) # Calculate all turbine powers and apply weights @@ -609,8 +597,8 @@ def get_farm_AEP( self.calculate_no_wake(yaw_angles=yaw_angles_subset) else: self.calculate_wake(yaw_angles=yaw_angles_subset) - farm_power[:, conditions_to_evaluate] = ( - self.get_farm_power(turbine_weights=turbine_weights) + farm_power[:, conditions_to_evaluate] = self.get_farm_power( + turbine_weights=turbine_weights ) # Finally, calculate AEP in GWh diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index a3c583309..db9bf8934 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -339,7 +339,7 @@ class TimeSeries(WindDataBase): Args: wind_directions: NumPy array of wind directions (NDArrayFloat). wind_speeds: NumPy array of wind speeds (NDArrayFloat). - turbulence_intensity: NumPy array of wind speeds (NDArrayFloat, optional). + turbulence_intensities: NumPy array of wind speeds (NDArrayFloat, optional). Defaults to None prices: NumPy array of electricity prices (NDArrayFloat, optional). Defaults to None @@ -350,7 +350,7 @@ def __init__( self, wind_directions: NDArrayFloat, wind_speeds: NDArrayFloat, - turbulence_intensity: NDArrayFloat | None = None, + turbulence_intensities: NDArrayFloat | None = None, prices: NDArrayFloat | None = None, ): # Wind speeds and wind directions must be the same length @@ -359,7 +359,7 @@ def __init__( self.wind_directions = wind_directions self.wind_speeds = wind_speeds - self.turbulence_intensity = turbulence_intensity + self.turbulence_intensities = turbulence_intensities self.prices = prices # Record findex @@ -378,7 +378,7 @@ def unpack(self): self.wind_directions, self.wind_speeds, uniform_frequency, - self.turbulence_intensity, + self.turbulence_intensities, self.prices, ) @@ -480,8 +480,8 @@ def to_wind_rose( df = df.assign(freq_val=df["freq_val"] * bin_weights) # If turbulence_intensity is not none, add to dataframe - if self.turbulence_intensity is not None: - df = df.assign(turbulence_intensity=self.turbulence_intensity) + if self.turbulence_intensities is not None: + df = df.assign(turbulence_intensities=self.turbulence_intensities) # If prices is not none, add to dataframe if self.prices is not None: @@ -522,8 +522,8 @@ def to_wind_rose( freq_table = freq_table.reshape((len(wd_centers), len(ws_centers))) # If turbulence intensity is not none, compute the table - if self.turbulence_intensity is not None: - ti_table = df["turbulence_intensity_mean"].values.copy() + if self.turbulence_intensities is not None: + ti_table = df["turbulence_intensities_mean"].values.copy() ti_table = ti_table.reshape((len(wd_centers), len(ws_centers))) else: ti_table = None From d81f427f856b2d9468a70a443aa2c838a6df6250 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 11:20:03 -0700 Subject: [PATCH 053/101] pluralize ti --- tests/conftest.py | 2 +- tests/data/input_full_v3.yaml | 2 +- tests/flow_field_unit_test.py | 6 +++--- tests/wind_data_test.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6a0f4bcb6..cbbcd32ed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -402,7 +402,7 @@ def __init__(self): self.flow_field = { "wind_speeds": WIND_SPEEDS, "wind_directions": WIND_DIRECTIONS, - "turbulence_intensity": [0.1], + "turbulence_intensities": [0.1], "wind_shear": 0.12, "wind_veer": 0.0, "air_density": 1.225, diff --git a/tests/data/input_full_v3.yaml b/tests/data/input_full_v3.yaml index fe0e31105..36a150bdd 100644 --- a/tests/data/input_full_v3.yaml +++ b/tests/data/input_full_v3.yaml @@ -26,7 +26,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: 90.0 - turbulence_intensity: + turbulence_intensities: - 0.06 wind_directions: - 270.0 diff --git a/tests/flow_field_unit_test.py b/tests/flow_field_unit_test.py index 7101f996e..1ce53b89c 100644 --- a/tests/flow_field_unit_test.py +++ b/tests/flow_field_unit_test.py @@ -58,9 +58,9 @@ def test_asdict(flow_field_fixture: FlowField, turbine_grid_fixture: TurbineGrid assert dict1 == dict2 -def test_turbulence_intensity_to_n_findex(flow_field_fixture, turbine_grid_fixture): +def test_turbulence_intensities_to_n_findex(flow_field_fixture, turbine_grid_fixture): # Assert tubulence intensity has same length as n_findex - assert len(flow_field_fixture.turbulence_intensity) == flow_field_fixture.n_findex + assert len(flow_field_fixture.turbulence_intensities) == flow_field_fixture.n_findex # Assert turbulence_intensity_field is the correct shape flow_field_fixture.initialize_velocity_field(turbine_grid_fixture) @@ -70,6 +70,6 @@ def test_turbulence_intensity_to_n_findex(flow_field_fixture, turbine_grid_fixtu for findex in range(N_FINDEX): for t in range(N_TURBINES): assert ( - flow_field_fixture.turbulence_intensity[findex] + flow_field_fixture.turbulence_intensities[findex] == flow_field_fixture.turbulence_intensity_field[findex, t, 0, 0] ) diff --git a/tests/wind_data_test.py b/tests/wind_data_test.py index c003ebe22..d1bc10854 100644 --- a/tests/wind_data_test.py +++ b/tests/wind_data_test.py @@ -244,11 +244,11 @@ def test_time_series_to_wind_rose_wrapping(): def test_time_series_to_wind_rose_with_ti(): wind_directions = np.array([259.8, 260.2, 260.3, 260.1]) wind_speeds = np.array([5.0, 5.0, 5.1, 7.2]) - turbulence_intensity = np.array([0.5, 1.0, 1.5, 2.0]) + turbulence_intensities = np.array([0.5, 1.0, 1.5, 2.0]) time_series = TimeSeries( wind_directions, wind_speeds, - turbulence_intensity=turbulence_intensity, + turbulence_intensities=turbulence_intensities, ) wind_rose = time_series.to_wind_rose(wd_step=2.0, ws_step=1.0) From 36f6e28fff7a8da48d5959bd333555694708198f Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 11:20:15 -0700 Subject: [PATCH 054/101] pluralize ti --- .../legacy/scipy/yaw_wind_rose.py | 57 +++----- .../legacy/scipy/yaw_wind_rose_clustered.py | 19 +-- .../legacy/scipy/yaw_wind_rose_parallel.py | 22 +-- .../scipy/yaw_wind_rose_parallel_clustered.py | 36 ++--- .../yaw_optimization/yaw_optimization_base.py | 133 +++++++++--------- 5 files changed, 102 insertions(+), 165 deletions(-) diff --git a/floris/tools/optimization/legacy/scipy/yaw_wind_rose.py b/floris/tools/optimization/legacy/scipy/yaw_wind_rose.py index c6b2219a3..4341ed2a7 100644 --- a/floris/tools/optimization/legacy/scipy/yaw_wind_rose.py +++ b/floris/tools/optimization/legacy/scipy/yaw_wind_rose.py @@ -243,7 +243,7 @@ def _get_initial_farm_power(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensity=self.ti[i], + turbulence_intensities=[self.ti[i]], ) # initial power @@ -262,7 +262,7 @@ def _get_initial_farm_power(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensity=self.ti[i], + turbulence_intensities=[self.ti[i]], ) self.fi.calculate_wake(yaw_angles=self.yaw_angles_baseline) power_init = self.fi.get_turbine_power( @@ -303,11 +303,7 @@ def _get_power_for_yaw_angle_opt(self, yaw_angles_subset_norm): unc_options=self.unc_options, ) - return ( - -1.0 - * np.dot(self.turbine_weights, turbine_powers) - / self.initial_farm_power - ) + return -1.0 * np.dot(self.turbine_weights, turbine_powers) / self.initial_farm_power def _set_opt_bounds(self, minimum_yaw_angle, maximum_yaw_angle): """ @@ -344,7 +340,7 @@ def _optimize(self): self.fi.reinitialize_flow_field( wind_speed=wind_map.input_speed, wind_direction=wind_map.input_direction, - turbulence_intensity=wind_map.input_ti, + turbulence_intensities=wind_map.input_ti, ) return opt_yaw_angles @@ -369,9 +365,7 @@ def _reduce_control_variables(self): fi=self.fi, wind_direction=self.fi.floris.farm.wind_direction[0] ) downstream_turbines = np.array(downstream_turbines, dtype=int) - self.turbs_to_opt = [ - i for i in self.turbs_to_opt if i not in downstream_turbines - ] + self.turbs_to_opt = [i for i in self.turbs_to_opt if i not in downstream_turbines] # Set up a template yaw angles array with default solutions. The default # solutions are either 0.0 or the allowable yaw angle closest to 0.0 deg. @@ -381,9 +375,7 @@ def _reduce_control_variables(self): yaw_angles_template = np.zeros(self.nturbs, dtype=float) for ti in range(self.nturbs): if (self.bnds[ti][0] > 0.0) | (self.bnds[ti][1] < 0.0): - yaw_angles_template[ti] = self.bnds[ti][ - np.argmin(np.abs(self.bnds[ti])) - ] + yaw_angles_template[ti] = self.bnds[ti][np.argmin(np.abs(self.bnds[ti]))] self.yaw_angles_template = yaw_angles_template # Derive normalized initial condition and bounds @@ -393,12 +385,8 @@ def _reduce_control_variables(self): ) self.bnds_norm = [ ( - self._norm( - self.bnds[i][0], self.minimum_yaw_angle, self.maximum_yaw_angle - ), - self._norm( - self.bnds[i][1], self.minimum_yaw_angle, self.maximum_yaw_angle - ), + self._norm(self.bnds[i][0], self.minimum_yaw_angle, self.maximum_yaw_angle), + self._norm(self.bnds[i][1], self.minimum_yaw_angle, self.maximum_yaw_angle), ) for i in self.turbs_to_opt ] @@ -572,17 +560,14 @@ def reinitialize_opt_wind_rose( self.yaw_angles_baseline = yaw_angles_baseline else: self.yaw_angles_baseline = [ - turbine.yaw_angle - for turbine in self.fi.floris.farm.turbine_map.turbines + turbine.yaw_angle for turbine in self.fi.floris.farm.turbine_map.turbines ] if any(np.abs(self.yaw_angles_baseline) > 0.0): print( "INFO: Baseline yaw angles were not specified and were derived " "from the floris object." ) - print( - "INFO: The inherent yaw angles in the floris object are not all 0.0 degrees." - ) + print("INFO: The inherent yaw angles in the floris object are not all 0.0 degrees.") self.bnds = bnds if bnds is not None: @@ -599,13 +584,9 @@ def reinitialize_opt_wind_rose( if (self.bnds[ti][0] > 0.0) | (self.bnds[ti][1] < 0.0): self.x0[ti] = np.mean(self.bnds[ti]) - if any( - np.array(self.yaw_angles_baseline) < np.array([b[0] for b in self.bnds]) - ): + if any(np.array(self.yaw_angles_baseline) < np.array([b[0] for b in self.bnds])): print("INFO: yaw_angles_baseline exceed lower bound constraints.") - if any( - np.array(self.yaw_angles_baseline) > np.array([b[1] for b in self.bnds]) - ): + if any(np.array(self.yaw_angles_baseline) > np.array([b[1] for b in self.bnds])): print("INFO: yaw_angles_baseline in FLORIS exceed upper bound constraints.") if any(np.array(self.x0) < np.array([b[0] for b in self.bnds])): raise ValueError("Initial guess x0 exceeds lower bound constraints.") @@ -770,13 +751,11 @@ def calc_baseline_power(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensity=self.ti[i], + turbulence_intensities=[self.ti[i]], ) # calculate baseline power - self.fi.calculate_wake( - yaw_angles=self.yaw_angles_baseline, no_wake=False - ) + self.fi.calculate_wake(yaw_angles=self.yaw_angles_baseline, no_wake=False) power_base = self.fi.get_turbine_power( include_unc=self.include_unc, unc_pmfs=self.unc_pmfs, @@ -784,9 +763,7 @@ def calc_baseline_power(self): ) # calculate power for no wake case - self.fi.calculate_wake( - yaw_angles=self.yaw_angles_baseline, no_wake=True - ) + self.fi.calculate_wake(yaw_angles=self.yaw_angles_baseline, no_wake=True) power_no_wake = self.fi.get_turbine_power( include_unc=self.include_unc, unc_pmfs=self.unc_pmfs, @@ -913,7 +890,7 @@ def optimize(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensity=self.ti[i], + turbulence_intensities=[self.ti[i]], ) self.initial_farm_power = self.initial_farm_powers[i] @@ -945,7 +922,7 @@ def optimize(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensity=self.ti[i], + turbulence_intensities=[self.ti[i]], ) opt_yaw_angles = np.array(self.yaw_angles_template, copy=True) self.fi.calculate_wake(yaw_angles=opt_yaw_angles) diff --git a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_clustered.py b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_clustered.py index 0c5d5a8e3..b72851b47 100644 --- a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_clustered.py +++ b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_clustered.py @@ -223,15 +223,14 @@ def __init__( ) self.clustering_wake_slope = clustering_wake_slope - def _cluster_turbines(self): wind_directions = self.fi.floris.farm.wind_direction - if (np.std(wind_directions) > 0.001): + if np.std(wind_directions) > 0.001: raise ValueError("Wind directions must be uniform for clustering algorithm.") self.clusters = cluster_turbines( fi=self.fi, wind_direction=self.fi.floris.farm.wind_direction[0], - wake_slope=self.clustering_wake_slope + wake_slope=self.clustering_wake_slope, ) def plot_clusters(self): @@ -240,10 +239,9 @@ def plot_clusters(self): fi=self.fi, wind_direction=wd, wake_slope=self.clustering_wake_slope, - plot_lines=True + plot_lines=True, ) - def optimize(self): """ This method solves for the optimum turbine yaw angles for power @@ -319,7 +317,7 @@ def optimize(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensity=self.ti[i], + turbulence_intensities=[self.ti[i]], ) # Set initial farm power @@ -355,7 +353,7 @@ def optimize(self): self.fi.reinitialize_flow_field( layout_array=[ np.array(fi_full.layout_x)[cl], - np.array(fi_full.layout_y)[cl] + np.array(fi_full.layout_y)[cl], ] ) opt_yaw_angles[cl] = self._optimize() @@ -368,10 +366,7 @@ def optimize(self): self.x0 = x0_full self.fi = fi_full self.fi.reinitialize_flow_field( - layout_array=[ - np.array(fi_full.layout_x), - np.array(fi_full.layout_y) - ] + layout_array=[np.array(fi_full.layout_x), np.array(fi_full.layout_y)] ) if np.sum(np.abs(opt_yaw_angles)) == 0: @@ -400,7 +395,7 @@ def optimize(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensity=self.ti[i], + turbulence_intensities=[self.ti[i]], ) opt_yaw_angles = np.array(self.yaw_angles_template, copy=True) self.fi.calculate_wake(yaw_angles=opt_yaw_angles) diff --git a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel.py b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel.py index ec46763a5..450e19e58 100644 --- a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel.py +++ b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel.py @@ -240,11 +240,7 @@ def _calc_baseline_power_one_case(self, ws, wd, ti=None): """ if ti is None: print( - "Computing wind speed = " - + str(ws) - + " m/s, wind direction = " - + str(wd) - + " deg." + "Computing wind speed = " + str(ws) + " m/s, wind direction = " + str(wd) + " deg." ) else: print( @@ -264,7 +260,7 @@ def _calc_baseline_power_one_case(self, ws, wd, ti=None): self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) else: self.fi.reinitialize_flow_field( - wind_direction=wd, wind_speed=ws, turbulence_intensity=ti + wind_direction=wd, wind_speed=ws, turbulence_intensities=ti ) # calculate baseline power self.fi.calculate_wake(yaw_angles=self.yaw_angles_baseline) @@ -348,11 +344,7 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): """ if ti is None: print( - "Computing wind speed = " - + str(ws) - + " m/s, wind direction = " - + str(wd) - + " deg." + "Computing wind speed = " + str(ws) + " m/s, wind direction = " + str(wd) + " deg." ) else: print( @@ -372,7 +364,7 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) else: self.fi.reinitialize_flow_field( - wind_direction=wd, wind_speed=ws, turbulence_intensity=ti + wind_direction=wd, wind_speed=ws, turbulence_intensities=ti ) self.initial_farm_power = initial_farm_power @@ -400,7 +392,7 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) else: self.fi.reinitialize_flow_field( - wind_direction=wd, wind_speed=ws, turbulence_intensity=ti + wind_direction=wd, wind_speed=ws, turbulence_intensities=ti ) opt_yaw_angles = np.array(self.yaw_angles_template, copy=True) self.fi.calculate_wake(yaw_angles=opt_yaw_angles) @@ -500,7 +492,6 @@ def calc_baseline_power(self): for df_base_one in executor.map( self._calc_baseline_power_one_case, self.ws.values, self.wd.values ): - # add variables to dataframe df_base = df_base.append(df_base_one) else: @@ -510,7 +501,6 @@ def calc_baseline_power(self): self.wd.values, self.ti.values, ): - # add variables to dataframe df_base = df_base.append(df_base_one) @@ -575,7 +565,6 @@ def optimize(self): self.wd.values, self.df_base.power_baseline.values, ): - # add variables to dataframe df_opt = df_opt.append(df_opt_one) else: @@ -586,7 +575,6 @@ def optimize(self): self.df_base.power_baseline.values, self.ti.values, ): - # add variables to dataframe df_opt = df_opt.append(df_opt_one) diff --git a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel_clustered.py b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel_clustered.py index caacc0429..e748daba4 100644 --- a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel_clustered.py +++ b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel_clustered.py @@ -60,7 +60,7 @@ def __init__( unc_options=None, turbine_weights=None, exclude_downstream_turbines=False, - clustering_wake_slope=0.30 + clustering_wake_slope=0.30, ): """ Instantiate YawOptimizationWindRoseParallel object with a @@ -217,7 +217,7 @@ def __init__( turbine_weights=turbine_weights, calc_init_power=False, exclude_downstream_turbines=exclude_downstream_turbines, - clustering_wake_slope=clustering_wake_slope + clustering_wake_slope=clustering_wake_slope, ) self.clustering_wake_slope = clustering_wake_slope @@ -258,11 +258,7 @@ def _calc_baseline_power_one_case(self, ws, wd, ti=None): """ if ti is None: print( - "Computing wind speed = " - + str(ws) - + " m/s, wind direction = " - + str(wd) - + " deg." + "Computing wind speed = " + str(ws) + " m/s, wind direction = " + str(wd) + " deg." ) else: print( @@ -282,7 +278,7 @@ def _calc_baseline_power_one_case(self, ws, wd, ti=None): self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) else: self.fi.reinitialize_flow_field( - wind_direction=wd, wind_speed=ws, turbulence_intensity=ti + wind_direction=wd, wind_speed=ws, turbulence_intensities=ti ) # calculate baseline power self.fi.calculate_wake(yaw_angles=self.yaw_angles_baseline) @@ -366,11 +362,7 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): """ if ti is None: print( - "Computing wind speed = " - + str(ws) - + " m/s, wind direction = " - + str(wd) - + " deg." + "Computing wind speed = " + str(ws) + " m/s, wind direction = " + str(wd) + " deg." ) else: print( @@ -390,7 +382,7 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) else: self.fi.reinitialize_flow_field( - wind_direction=wd, wind_speed=ws, turbulence_intensity=ti + wind_direction=wd, wind_speed=ws, turbulence_intensities=ti ) self.initial_farm_power = initial_farm_power @@ -420,10 +412,7 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): self.x0 = np.array(x0_full)[cl] self.fi = copy.deepcopy(fi_full) self.fi.reinitialize_flow_field( - layout_array=[ - np.array(fi_full.layout_x)[cl], - np.array(fi_full.layout_y)[cl] - ] + layout_array=[np.array(fi_full.layout_x)[cl], np.array(fi_full.layout_y)[cl]] ) opt_yaw_angles[cl] = self._optimize() @@ -435,10 +424,7 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): self.x0 = x0_full self.fi = fi_full self.fi.reinitialize_flow_field( - layout_array=[ - np.array(fi_full.layout_x), - np.array(fi_full.layout_y) - ] + layout_array=[np.array(fi_full.layout_x), np.array(fi_full.layout_y)] ) if np.sum(np.abs(opt_yaw_angles)) == 0: @@ -463,7 +449,7 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) else: self.fi.reinitialize_flow_field( - wind_direction=wd, wind_speed=ws, turbulence_intensity=ti + wind_direction=wd, wind_speed=ws, turbulence_intensities=ti ) opt_yaw_angles = np.array(self.yaw_angles_template, copy=True) self.fi.calculate_wake(yaw_angles=opt_yaw_angles) @@ -563,7 +549,6 @@ def calc_baseline_power(self): for df_base_one in executor.map( self._calc_baseline_power_one_case, self.ws.values, self.wd.values ): - # add variables to dataframe df_base = df_base.append(df_base_one) else: @@ -573,7 +558,6 @@ def calc_baseline_power(self): self.wd.values, self.ti.values, ): - # add variables to dataframe df_base = df_base.append(df_base_one) @@ -638,7 +622,6 @@ def optimize(self): self.wd.values, self.df_base.power_baseline.values, ): - # add variables to dataframe df_opt = df_opt.append(df_opt_one) else: @@ -649,7 +632,6 @@ def optimize(self): self.df_base.power_baseline.values, self.ti.values, ): - # add variables to dataframe df_opt = df_opt.append(df_opt_one) diff --git a/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py b/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py index baffb9822..2fd417f75 100644 --- a/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py +++ b/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py @@ -139,8 +139,7 @@ def __init__( "were derived from the floris object." ) print( - "INFO: The inherent yaw angles in the floris object " - "are not all 0.0 degrees." + "INFO: The inherent yaw angles in the floris object " "are not all 0.0 degrees." ) # Set optimization bounds @@ -237,8 +236,8 @@ def _unpack_variable(self, variable, subset=False): ( self.fi.floris.flow_field.n_wind_directions, self.fi.floris.flow_field.n_wind_speeds, - 1 - ) + 1, + ), ) if len(np.shape(variable)) == 2: @@ -260,7 +259,7 @@ def _reduce_control_problem(self): exploit_layout_symmetry == True. """ # Initialize which turbines to optimize for - self.turbs_to_opt = (self.maximum_yaw_angle - self.minimum_yaw_angle >= 0.001) + self.turbs_to_opt = self.maximum_yaw_angle - self.minimum_yaw_angle >= 0.001 # Initialize subset variables as full set self.fi_subset = self.fi.copy() @@ -311,9 +310,9 @@ def _reduce_control_problem(self): combined_bounds = np.concatenate( ( np.expand_dims(minimum_yaw_angle_subset, axis=3), - np.expand_dims(maximum_yaw_angle_subset, axis=3) + np.expand_dims(maximum_yaw_angle_subset, axis=3), ), - axis=3 + axis=3, ) # Overwrite all values that are not allowed to be 0.0 with bound value closest to zero ids_closest = np.expand_dims(np.argmin(np.abs(combined_bounds), axis=3), axis=3) @@ -339,20 +338,22 @@ def _normalize_control_problem(self): """ lb = np.min(self._minimum_yaw_angle_subset) ub = np.max(self._maximum_yaw_angle_subset) - self._normalization_length = (ub - lb) + self._normalization_length = ub - lb self._x0_subset_norm = self._x0_subset / self._normalization_length self._minimum_yaw_angle_subset_norm = ( - self._minimum_yaw_angle_subset - / self._normalization_length + self._minimum_yaw_angle_subset / self._normalization_length ) self._maximum_yaw_angle_subset_norm = ( - self._maximum_yaw_angle_subset - / self._normalization_length + self._maximum_yaw_angle_subset / self._normalization_length ) - def _calculate_farm_power(self, yaw_angles=None, wd_array=None, turbine_weights=None, - heterogeneous_speed_multipliers=None - ): + def _calculate_farm_power( + self, + yaw_angles=None, + wd_array=None, + turbine_weights=None, + heterogeneous_speed_multipliers=None, + ): """ Calculate the wind farm power production assuming the predefined probability distribution (self.unc_options/unc_pmf), with the @@ -373,8 +374,9 @@ def _calculate_farm_power(self, yaw_angles=None, wd_array=None, turbine_weights= if turbine_weights is None: turbine_weights = self._turbine_weights_subset if heterogeneous_speed_multipliers is not None: - fi_subset.floris.flow_field.\ - heterogenous_inflow_config['speed_multipliers'] = heterogeneous_speed_multipliers + fi_subset.floris.flow_field.heterogenous_inflow_config[ + "speed_multipliers" + ] = heterogeneous_speed_multipliers # Ensure format [incompatible with _subset notation] yaw_angles = self._unpack_variable(yaw_angles, subset=True) @@ -451,9 +453,11 @@ def _derive_layout_symmetry(self): wd_array = self.fi.floris.flow_field.wind_directions sym_step = df.iloc[0]["wd_range"][1] - if ((0.0 not in wd_array) or(sym_step not in wd_array)): - print("Floris wind direction array does not " + - "intersect {:.1f} and {:.1f}.".format(0.0, sym_step)) + if (0.0 not in wd_array) or (sym_step not in wd_array): + print( + "Floris wind direction array does not " + + "intersect {:.1f} and {:.1f}.".format(0.0, sym_step) + ) print("Exploitation of symmetry has been disabled.") return @@ -466,8 +470,9 @@ def _derive_layout_symmetry(self): print("Exploitation of symmetry has been disabled.") self._sym_mapping_extrap = np.array( - [np.where(np.abs(x - wd_array_min) < 0.0001)[0][0] - for x in wd_array_remn], dtype=int) + [np.where(np.abs(x - wd_array_min) < 0.0001)[0][0] for x in wd_array_remn], + dtype=int, + ) self._sym_mapping_reduce = copy.deepcopy(ids_minimal) self._sym_df = df @@ -498,10 +503,7 @@ def _unreduce_variable(self, variable): # Now process turbine mapping wd_array = self.fi.floris.flow_field.wind_directions for ii, dfrow in self._sym_df.iloc[1::].iterrows(): - ids = ( - (wd_array >= dfrow["wd_range"][0]) & - (wd_array < dfrow["wd_range"][1]) - ) + ids = (wd_array >= dfrow["wd_range"][0]) & (wd_array < dfrow["wd_range"][1]) tmap = np.argsort(dfrow["turbine_mapping"]) full_array[ids, :, :] = full_array[ids, :, :][:, :, tmap] else: @@ -518,11 +520,8 @@ def _finalize(self, farm_power_opt_subset=None, yaw_angles_opt_subset=None): # Now verify solutions for convergence, if necessary if self.verify_convergence: - yaw_angles_opt_subset, farm_power_opt_subset = ( - self._verify_solutions_for_convergence( - farm_power_opt_subset, - yaw_angles_opt_subset - ) + yaw_angles_opt_subset, farm_power_opt_subset = self._verify_solutions_for_convergence( + farm_power_opt_subset, yaw_angles_opt_subset ) # Finalization step for optimization: undo reduction step @@ -530,20 +529,26 @@ def _finalize(self, farm_power_opt_subset=None, yaw_angles_opt_subset=None): self.yaw_angles_opt = self._unreduce_variable(yaw_angles_opt_subset) # Produce output table - ti = np.min(self.fi.floris.flow_field.turbulence_intensity) + ti = np.min(self.fi.floris.flow_field.turbulence_intensities) df_list = [] num_wind_directions = len(self.fi.floris.flow_field.wind_directions) for ii, wind_speed in enumerate(self.fi.floris.flow_field.wind_speeds): - df_list.append(pd.DataFrame({ - "wind_direction": self.fi.floris.flow_field.wind_directions, - "wind_speed": wind_speed * np.ones(num_wind_directions), - "turbulence_intensity": ti * np.ones(num_wind_directions), - "yaw_angles_opt": list(self.yaw_angles_opt[:, ii, :]), - "farm_power_opt": None if self.farm_power_opt is None \ - else self.farm_power_opt[:, ii], - "farm_power_baseline": None if self.farm_power_baseline is None \ - else self.farm_power_baseline[:, ii], - })) + df_list.append( + pd.DataFrame( + { + "wind_direction": self.fi.floris.flow_field.wind_directions, + "wind_speed": wind_speed * np.ones(num_wind_directions), + "turbulence_intensities": ti * np.ones(num_wind_directions), + "yaw_angles_opt": list(self.yaw_angles_opt[:, ii, :]), + "farm_power_opt": None + if self.farm_power_opt is None + else self.farm_power_opt[:, ii], + "farm_power_baseline": None + if self.farm_power_baseline is None + else self.farm_power_baseline[:, ii], + } + ) + ) df_opt = pd.concat(df_list, axis=0) return df_opt @@ -640,13 +645,12 @@ def _verify_solutions_for_convergence( farm_power = self._calculate_farm_power( yaw_angles=yaw_angles_verify, wd_array=np.tile(wd_array_nominal, n_turbs), - turbine_weights=np.tile(self._turbs_to_opt_subset, sp) + turbine_weights=np.tile(self._turbs_to_opt_subset, sp), ) # Calculate power uplift for optimal solutions uplift_o = 100 * ( - np.tile(farm_power_opt_subset, (n_turbs, 1)) / - farm_power_baseline_verify - 1.0 + np.tile(farm_power_opt_subset, (n_turbs, 1)) / farm_power_baseline_verify - 1.0 ) # Calculate power uplift for all cases we evaluated @@ -664,29 +668,19 @@ def _verify_solutions_for_convergence( ) # Overwrite yaw angles that insufficiently increased farm power with baseline values - yaw_angles_opt_subset[ids_to_simplify] = ( - yaw_angles_baseline_subset[ids_to_simplify] - ) + yaw_angles_opt_subset[ids_to_simplify] = yaw_angles_baseline_subset[ids_to_simplify] n = len(ids_to_simplify[0]) if n > 0: # Yaw angles notably changed: recalculate farm powers - farm_power_opt_subset_new = ( - self._calculate_farm_power(yaw_angles_opt_subset) - ) + farm_power_opt_subset_new = self._calculate_farm_power(yaw_angles_opt_subset) if verbose: # Calculate old uplift for all conditions - dP_old = 100.0 * ( - farm_power_opt_subset / - farm_power_baseline_subset - ) - 100.0 + dP_old = 100.0 * (farm_power_opt_subset / farm_power_baseline_subset) - 100.0 # Calculate new uplift for all conditions - dP_new = 100.0 * ( - farm_power_opt_subset_new / - farm_power_baseline_subset - ) - 100.0 + dP_new = 100.0 * (farm_power_opt_subset_new / farm_power_baseline_subset) - 100.0 # Calculate differences in power uplift diff_uplift = dP_old - dP_new @@ -694,22 +688,23 @@ def _verify_solutions_for_convergence( jj = (ids_max_loss[0][0], ids_max_loss[1][0]) ws_array_nominal = self.fi_subset.floris.flow_field.wind_speeds print( - "Nullified the optimal yaw offset for {:d}".format(n) + - " conditions and turbines." - ) + "Nullified the optimal yaw offset for {:d}".format(n) + + " conditions and turbines." + ) print( - "Simplifying the yaw angles for these conditions lead " + - "to a maximum change in wake-steering power uplift from " - + "{:.5f}% to {:.5f}% at ".format(dP_old[jj], dP_new[jj]) - + " WD = {:.1f} deg and WS = {:.1f} m/s.".format( - wd_array_nominal[jj[0]], ws_array_nominal[jj[1]], + "Simplifying the yaw angles for these conditions lead " + + "to a maximum change in wake-steering power uplift from " + + "{:.5f}% to {:.5f}% at ".format(dP_old[jj], dP_new[jj]) + + " WD = {:.1f} deg and WS = {:.1f} m/s.".format( + wd_array_nominal[jj[0]], + ws_array_nominal[jj[1]], ) ) t = timerpc() - start_time print( - "Time spent to verify the convergence of the optimal " + - "yaw angles: {:.3f} s.".format(t) + "Time spent to verify the convergence of the optimal " + + "yaw angles: {:.3f} s.".format(t) ) # Return optimal solutions to the user From f6a54837fb45214c05c884c560103e2f0b0c1850 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 12:38:18 -0700 Subject: [PATCH 055/101] Add new test of turbulence intensity --- tests/floris_interface_test.py | 52 +++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/floris_interface_test.py b/tests/floris_interface_test.py index 0196af5fc..56b49b07f 100644 --- a/tests/floris_interface_test.py +++ b/tests/floris_interface_test.py @@ -1,6 +1,7 @@ from pathlib import Path import numpy as np +import pytest from floris.tools.floris_interface import FlorisInterface @@ -198,5 +199,54 @@ def test_get_farm_aep_with_conditions(): # In this case farm_aep should match farm powers np.testing.assert_allclose(farm_aep, aep) - #Confirm n_findex reset after the operation + # Confirm n_findex reset after the operation assert n_findex == fi.floris.flow_field.n_findex + + +def test_reinitailize_ti(): + fi = FlorisInterface(configuration=YAML_INPUT) + + # Set wind directions and wind speeds and turbulence intensitities + # with n_findex = 3 + fi.reinitialize( + wind_speeds=[8.0, 8.0, 8.0], + wind_directions=[240.0, 250.0, 260.0], + turbulence_intensities=[0.1, 0.1, 0.1], + ) + + # Now confirm can change wind speeds and directions shape without changing + # turbulence intensity since it is uniform this allowed + # raises n_findex to 4 + fi.reinitialize( + wind_speeds=[8.0, 8.0, 8.0, 8.0], + wind_directions=[ + 240.0, + 250.0, + 260.0, + 270.0, + ], + ) + + # Confirm turbulence_intensities now length 4 with single unique value + np.testing.assert_allclose(fi.floris.flow_field.turbulence_intensities, [0.1, 0.1, 0.1, 0.1]) + + # Now should be able to change turbulence intensity to changing, so long as length 4 + fi.reinitialize(turbulence_intensities=[0.08, 0.09, 0.1, 0.11]) + + # However the wrong length should raise an error + with pytest.raises(ValueError): + fi.reinitialize(turbulence_intensities=[0.08, 0.09, 0.1]) + + # Also, now that TI is not a single unique value, it can not be left default when changing + # shape of wind speeds and directions + with pytest.raises(ValueError): + fi.reinitialize( + wind_speeds=[8.0, 8.0, 8.0, 8.0, 8.0], + wind_directions=[ + 240.0, + 250.0, + 260.0, + 270.0, + 280.0, + ], + ) From 2b64318deac5b01eeda8070cf75c283d7d5088d9 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 12:49:29 -0700 Subject: [PATCH 056/101] Update example 34 --- examples/34_wind_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/34_wind_data.py b/examples/34_wind_data.py index f3e87686d..5da902880 100644 --- a/examples/34_wind_data.py +++ b/examples/34_wind_data.py @@ -50,7 +50,7 @@ # Build the time series -time_series = TimeSeries(wd_array, ws_array) # , turbulence_intensity=ti_array) +time_series = TimeSeries(wd_array, ws_array, turbulence_intensities=ti_array) # Now build the wind rose wind_rose = time_series.to_wind_rose() From f997f12cf80c45b5d19651396f979b143513093a Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 12:49:46 -0700 Subject: [PATCH 057/101] Update to ti array --- floris/tools/wind_data.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index db9bf8934..9448d88f4 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -357,6 +357,15 @@ def __init__( if len(wind_directions) != len(wind_speeds): raise ValueError("wind_directions and wind_speeds must be the same length") + # If turbulence intensity is not none, must be same length as wind speed and + # wind directions + if (turbulence_intensities is not None) and ( + len(turbulence_intensities) != len(wind_directions) + ): + raise ValueError( + "wind_directions and wind_speeds and turbulence_intensities must be the same length" + ) + self.wind_directions = wind_directions self.wind_speeds = wind_speeds self.turbulence_intensities = turbulence_intensities From b4df579cf09165eb2dd26be55a41f3e6d51c0c9c Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 12:50:01 -0700 Subject: [PATCH 058/101] Add example of sweeping ti --- examples/35_sweep_ti.py | 68 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 examples/35_sweep_ti.py diff --git a/examples/35_sweep_ti.py b/examples/35_sweep_ti.py new file mode 100644 index 000000000..fcae472db --- /dev/null +++ b/examples/35_sweep_ti.py @@ -0,0 +1,68 @@ +# Copyright 2024 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, + TimeSeries, + WindRose, +) +from floris.utilities import wrap_360 + + +""" +Demonstrate the new behavior in T4 where TI is an array rather than a float +""" + + +# Generate a random time series of wind speeds, wind directions and turbulence intensities +N = 50 +wd_array = 270.0 * np.ones(N) +ws_array = 8.0 * np.ones(N) +ti_array = np.linspace(0.03, 0.2, N) + + +# Build the time series +time_series = TimeSeries(wd_array, ws_array, turbulence_intensities=ti_array) + + +# Now set up a FLORIS model and initialize it using the time +fi = FlorisInterface("inputs/gch.yaml") +fi.reinitialize(layout_x=[0, 500.0], layout_y=[0.0, 0.0], wind_data=time_series) +fi.calculate_wake() +turbine_power = fi.get_turbine_powers() + +fig, axarr = plt.subplots(5, 1, sharex=True, figsize=(5, 9)) +ax = axarr[0] +ax.plot(wd_array, color="k") +ax.set_ylabel("Wind Direction") +ax = axarr[1] +ax.plot(ws_array, color="k") +ax.set_ylabel("Wind Speed") +ax = axarr[2] +ax.plot(ti_array, color="k") +ax.set_ylabel("Turbulence Intensity") +ax = axarr[3] +ax.plot(turbine_power[:, 0], color="k") +ax.set_ylabel("Front Turbine") +ax = axarr[4] +ax.plot(turbine_power[:, 1], color="k") +ax.set_ylabel("Rear Turbine") + +for ax in axarr: + ax.grid(True) + +plt.show() From 3fbc2940356fddb3ca3806e6f20ca05e72b77863 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 14:34:14 -0700 Subject: [PATCH 059/101] Add tools for generating TI to windrose and timeseries --- floris/tools/wind_data.py | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 9448d88f4..b7d8135c4 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -329,6 +329,65 @@ def plot_wind_rose( return ax + def assign_ti_using_wd_ws_function(self, func): + """ + Use the passed in function to new assign values to turbulence_intensities + + Args: + func (function): Function which accepts wind_directions as its + first argument and wind_speeds as second argument and returns + turbulence_intensities + """ + self.ti_table = func(self.wd_grid, self.ws_grid) + self._build_gridded_and_flattened_version() + + def assign_ti_using_Iref(self, Iref): + """ + Define TI as a function of wind speed by specifying an Iref value as in the + IEC standard appraoch + + Args: + Iref (float): Reference turbulence level, values range [0,1] + """ + if (Iref < 0) or (Iref > 1): + raise ValueError("Iref must be >= 0 and <=1") + + def iref_func(wind_directions, wind_speeds): + sigma_1 = Iref * (0.75 * wind_speeds + 5.6) + return sigma_1 / wind_speeds + + self.assign_ti_using_wd_ws_function(iref_func) + + def plot_ti_over_ws( + self, + ax=None, + marker=".", + ls="None", + color="k", + ): + """ + Scatter plot the turbulence_intensities against wind_speeds + + Args: + ax (:py:class:`matplotlib.pyplot.axes`, optional): The figure axes + on which the wind rose is plotted. Defaults to None. + plot_kwargs (dict, optional): Keyword arguments to be passed to + ax.plot(). + + Returns: + :py:class:`matplotlib.pyplot.axes`: A figure axes object containing + the plotted wind rose. + """ + + # Set up figure + if ax is None: + _, ax = plt.subplots() + + ax.plot(self.ws_flat, self.ti_table_flat, marker=marker, ls=ls, color=color) + ax.set_xlabel("Wind Speed (m/s)") + ax.set_ylabel("Turbulence Intensity (%)") + ax.grid(True) + class TimeSeries(WindDataBase): """ @@ -410,6 +469,34 @@ def _wrap_wind_directions_near_360(self, wind_directions, wd_step): wind_directions_wrapped[mask] = wind_directions_wrapped[mask] - 360.0 return wind_directions_wrapped + def assign_ti_using_wd_ws_function(self, func): + """ + Use the passed in function to new assign values to turbulence_intensities + + Args: + func (function): Function which accepts wind_directions as its + first argument and wind_speeds as second argument and returns + turbulence_intensities + """ + self.turbulence_intensities = func(self.wind_directions, self.wind_speeds) + + def assign_ti_using_Iref(self, Iref): + """ + Define TI as a function of wind speed by specifying an Iref value as in the + IEC standard appraoch + + Args: + Iref (float): Reference turbulence level, values range [0,1] + """ + if (Iref < 0) or (Iref > 1): + raise ValueError("Iref must be >= 0 and <=1") + + def iref_func(wind_directions, wind_speeds): + sigma_1 = Iref * (0.75 * wind_speeds + 5.6) + return sigma_1 / wind_speeds + + self.assign_ti_using_wd_ws_function(iref_func) + def to_wind_rose( self, wd_step=2.0, ws_step=1.0, wd_edges=None, ws_edges=None, bin_weights=None ): From 594f6ab046c6da16f6e27eb03046f976dce252bf Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 14:34:28 -0700 Subject: [PATCH 060/101] Add example for generating TI --- examples/36_generate_ti.py | 76 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 examples/36_generate_ti.py diff --git a/examples/36_generate_ti.py b/examples/36_generate_ti.py new file mode 100644 index 000000000..fca54a348 --- /dev/null +++ b/examples/36_generate_ti.py @@ -0,0 +1,76 @@ +# Copyright 2024 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, + TimeSeries, + WindRose, +) +from floris.utilities import wrap_360 + + +""" +Demonstrate usage of ti generating and plotting functionality +""" + + +# Generate a random time series of wind speeds, wind directions and turbulence intensities +wind_directions = np.array([250, 260, 270]) +wind_speeds = np.array([5, 6, 7, 8, 9, 10]) + +wind_rose = WindRose(wind_directions=wind_directions, wind_speeds=wind_speeds) + + +# Define a custom function where TI = 1 / wind_speed +def custom_ti_func(wind_directions, wind_speeds): + return 1 / wind_speeds + + +wind_rose.assign_ti_using_wd_ws_function(custom_ti_func) + +fig, ax = plt.subplots() +wind_rose.plot_ti_over_ws(ax) +ax.set_title("Turbulence Intensity defined by custom function") + +# Now use the IEC Iref approach: +Iref = 0.08 +wind_rose.assign_ti_using_Iref(Iref) +fig, ax = plt.subplots() +wind_rose.plot_ti_over_ws(ax) +ax.set_title(f"Turbulence Intensity defined by Iref = {Iref:0.2}") + + +# Demonstrate equivalent usage in time series +N = 100 +wind_directions = 270 * np.ones(N) +wind_speeds = np.linspace(5, 15, N) +time_series = TimeSeries(wind_directions=wind_directions, wind_speeds=wind_speeds) +time_series.assign_ti_using_Iref(Iref=Iref) + +fig, axarr = plt.subplots(2, 1, sharex=True, figsize=(7, 8)) +ax = axarr[0] +ax.plot(wind_speeds) +ax.set_ylabel("Wind Speeds (m/s)") +ax.grid(True) +ax = axarr[1] +ax.plot(time_series.turbulence_intensities) +ax.set_ylabel("Turbulence Intensity (-)") +ax.grid(True) +fig.suptitle("Generating TI in TimeSeries") + + +plt.show() From 2053c6373b3087734feeddbbb043b62fb62fd36f Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 22:00:17 -0700 Subject: [PATCH 061/101] Explain what happens in default cases for WindRose --- floris/tools/wind_data.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index a3c583309..54b288ebf 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -79,13 +79,16 @@ class WindRose(WindDataBase): wind_speeds: NumPy array of wind speeds (NDArrayFloat). freq_table: Frequency table for binned wind direction, wind speed values (NDArrayFloat, optional). Must have dimension - (n_wind_directions, n_wind_speeds). Defaults to None. + (n_wind_directions, n_wind_speeds). Defaults to None in which case + uniform frequency of all bins is assumed. ti_table: Turbulence intensity table for binned wind direction, wind speed values (NDArrayFloat, optional). Must have dimension - (n_wind_directions, n_wind_speeds). Defaults to None. + (n_wind_directions, n_wind_speeds). Defaults to None (no change to + turbulence intensity) price_table: Price table for binned binned wind direction, wind speed values (NDArrayFloat, optional). Must have dimension - (n_wind_directions, n_wind_speeds). Defaults to None. + (n_wind_directions, n_wind_speeds). Defaults to None in which case + uniform prices are assumed. compute_zero_freq_occurrence: Flag indicating whether to compute zero frequency occurrences (bool, optional). Defaults to False. From 126f4ad38ddcdd9583d8c2154d17b80869fd1e74 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 22:04:50 -0700 Subject: [PATCH 062/101] Rename price to value --- floris/tools/wind_data.py | 67 ++++++++++++++++++++------------------- tests/wind_data_test.py | 4 +-- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 54b288ebf..542aa77e0 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -85,10 +85,11 @@ class WindRose(WindDataBase): speed values (NDArrayFloat, optional). Must have dimension (n_wind_directions, n_wind_speeds). Defaults to None (no change to turbulence intensity) - price_table: Price table for binned binned wind direction, wind + value_table: Value table for binned wind direction, wind speed values (NDArrayFloat, optional). Must have dimension (n_wind_directions, n_wind_speeds). Defaults to None in which case - uniform prices are assumed. + uniform values are assumed. Value can be used to weight power in + each bin to compute the total value of the energy produced compute_zero_freq_occurrence: Flag indicating whether to compute zero frequency occurrences (bool, optional). Defaults to False. @@ -100,7 +101,7 @@ def __init__( wind_speeds: NDArrayFloat, freq_table: NDArrayFloat | None = None, ti_table: NDArrayFloat | None = None, - price_table: NDArrayFloat | None = None, + value_table: NDArrayFloat | None = None, compute_zero_freq_occurrence: bool = False, ): if not isinstance(wind_directions, np.ndarray): @@ -136,14 +137,14 @@ def __init__( raise ValueError("ti_table second dimension must equal len(wind_speeds)") self.ti_table = ti_table - # If price_table is not None, confirm it has correct dimension, + # If value_table is not None, confirm it has correct dimension, # otherwise initialze to all ones - if price_table is not None: - if not price_table.shape[0] == len(wind_directions): - raise ValueError("price_table first dimension must equal len(wind_directions)") - if not price_table.shape[1] == len(wind_speeds): - raise ValueError("price_table second dimension must equal len(wind_speeds)") - self.price_table = price_table + if value_table is not None: + if not value_table.shape[0] == len(wind_directions): + raise ValueError("value_table first dimension must equal len(wind_directions)") + if not value_table.shape[1] == len(wind_speeds): + raise ValueError("value_table second dimension must equal len(wind_speeds)") + self.value_table = value_table # Save whether zero occurrence cases should be computed self.compute_zero_freq_occurrence = compute_zero_freq_occurrence @@ -175,11 +176,11 @@ def _build_gridded_and_flattened_version(self): else: self.ti_table_flat = None - # Price table - if self.price_table is not None: - self.price_table_flat = self.price_table.flatten() + # value table + if self.value_table is not None: + self.value_table_flat = self.value_table.flatten() else: - self.price_table_flat = None + self.value_table_flat = None # Set mask to non-zero frequency cases depending on compute_zero_freq_occurrence if self.compute_zero_freq_occurrence: @@ -213,18 +214,18 @@ def unpack(self): else: ti_table_unpack = None - # Now get unpacked price table - if self.price_table_flat is not None: - price_table_unpack = self.price_table_flat[self.non_zero_freq_mask].copy() + # Now get unpacked value table + if self.value_table_flat is not None: + value_table_unpack = self.value_table_flat[self.non_zero_freq_mask].copy() else: - price_table_unpack = None + value_table_unpack = None return ( wind_directions_unpack, wind_speeds_unpack, freq_table_unpack, ti_table_unpack, - price_table_unpack, + value_table_unpack, ) def resample_wind_rose(self, wd_step=None, ws_step=None): @@ -256,7 +257,7 @@ def resample_wind_rose(self, wd_step=None, ws_step=None): # Pass the flat versions of each quantity to build a TimeSeries model time_series = TimeSeries( - self.wd_flat, self.ws_flat, self.ti_table_flat, self.price_table_flat + self.wd_flat, self.ws_flat, self.ti_table_flat, self.value_table_flat ) # Now build a new wind rose using the new steps @@ -344,7 +345,7 @@ class TimeSeries(WindDataBase): wind_speeds: NumPy array of wind speeds (NDArrayFloat). turbulence_intensity: NumPy array of wind speeds (NDArrayFloat, optional). Defaults to None - prices: NumPy array of electricity prices (NDArrayFloat, optional). + values: NumPy array of electricity values (NDArrayFloat, optional). Defaults to None """ @@ -354,7 +355,7 @@ def __init__( wind_directions: NDArrayFloat, wind_speeds: NDArrayFloat, turbulence_intensity: NDArrayFloat | None = None, - prices: NDArrayFloat | None = None, + values: NDArrayFloat | None = None, ): # Wind speeds and wind directions must be the same length if len(wind_directions) != len(wind_speeds): @@ -363,7 +364,7 @@ def __init__( self.wind_directions = wind_directions self.wind_speeds = wind_speeds self.turbulence_intensity = turbulence_intensity - self.prices = prices + self.values = values # Record findex self.n_findex = len(self.wind_directions) @@ -382,7 +383,7 @@ def unpack(self): self.wind_speeds, uniform_frequency, self.turbulence_intensity, - self.prices, + self.values, ) def _wrap_wind_directions_near_360(self, wind_directions, wd_step): @@ -486,9 +487,9 @@ def to_wind_rose( if self.turbulence_intensity is not None: df = df.assign(turbulence_intensity=self.turbulence_intensity) - # If prices is not none, add to dataframe - if self.prices is not None: - df = df.assign(prices=self.prices) + # If values is not none, add to dataframe + if self.values is not None: + df = df.assign(values=self.values) # Bin wind speed and wind direction and then group things up df = ( @@ -531,12 +532,12 @@ def to_wind_rose( else: ti_table = None - # If prices is not none, compute the table - if self.prices is not None: - price_table = df["prices_mean"].values.copy() - price_table = price_table.reshape((len(wd_centers), len(ws_centers))) + # If values is not none, compute the table + if self.values is not None: + value_table = df["values_mean"].values.copy() + value_table = value_table.reshape((len(wd_centers), len(ws_centers))) else: - price_table = None + value_table = None # Return a WindRose - return WindRose(wd_centers, ws_centers, freq_table, ti_table, price_table) + return WindRose(wd_centers, ws_centers, freq_table, ti_table, value_table) diff --git a/tests/wind_data_test.py b/tests/wind_data_test.py index c003ebe22..f9503fe1c 100644 --- a/tests/wind_data_test.py +++ b/tests/wind_data_test.py @@ -101,7 +101,7 @@ def test_wind_rose_unpack(): wind_speeds_unpack, freq_table_unpack, ti_table_unpack, - price_table_unpack, + value_table_unpack, ) = wind_rose.unpack() # Given the above frequency table with zeros for a few elements, @@ -124,7 +124,7 @@ def test_wind_rose_unpack(): wind_speeds_unpack, freq_table_unpack, ti_table_unpack, - price_table_unpack, + value_table_unpack, ) = wind_rose.unpack() # Expect now to compute all combinations From 4dba98d00066dfcd53554ee2b9a4c09aa69f0b04 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 22:06:55 -0700 Subject: [PATCH 063/101] Add check on ti and value --- floris/tools/wind_data.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 542aa77e0..b86f5edb3 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -361,6 +361,16 @@ def __init__( if len(wind_directions) != len(wind_speeds): raise ValueError("wind_directions and wind_speeds must be the same length") + # If turbulence_intensity is not None, must be same length as wind_directions + if turbulence_intensity is not None: + if len(wind_directions) != len(turbulence_intensity): + raise ValueError("wind_directions and turbulence_intensity must be the same length") + + # If turbulence_intensity is not None, must be same length as wind_directions + if values is not None: + if len(wind_directions) != len(values): + raise ValueError("wind_directions and values must be the same length") + self.wind_directions = wind_directions self.wind_speeds = wind_speeds self.turbulence_intensity = turbulence_intensity From 6049894a2af4da6ff431c923c612f44dae8afa67 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Jan 2024 22:10:47 -0700 Subject: [PATCH 064/101] Fix bin minimum --- floris/tools/wind_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index b86f5edb3..1e2c657a5 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -459,7 +459,7 @@ def to_wind_rose( ) # Only keep the range with values in it - wd_edges = wd_edges[wd_edges + wd_step >= wind_directions_wrapped.min()] + wd_edges = wd_edges[wd_edges + wd_step > wind_directions_wrapped.min()] wd_edges = wd_edges[wd_edges - wd_step <= wind_directions_wrapped.max()] # Define the centers from the edges @@ -473,7 +473,7 @@ def to_wind_rose( ws_edges = np.arange(0.0 - ws_step / 2.0, 50.0, ws_step) # Only keep the range with values in it - ws_edges = ws_edges[ws_edges + ws_step >= self.wind_speeds.min()] + ws_edges = ws_edges[ws_edges + ws_step > self.wind_speeds.min()] ws_edges = ws_edges[ws_edges - ws_step <= self.wind_speeds.max()] # Define the centers from the edges From cdb987599e8f0d48381a3609f2a09cc8bc03a918 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 23 Jan 2024 19:11:29 -0800 Subject: [PATCH 065/101] Revert changes not connected to ti --- floris/simulation/wake_velocity/turbopark.py | 49 +++++++++----------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/floris/simulation/wake_velocity/turbopark.py b/floris/simulation/wake_velocity/turbopark.py index d9f6057ff..637c30d34 100644 --- a/floris/simulation/wake_velocity/turbopark.py +++ b/floris/simulation/wake_velocity/turbopark.py @@ -50,11 +50,14 @@ class TurbOParkVelocityDeficit(BaseModel): def __attrs_post_init__(self) -> None: lookup_table_matlab_file = Path(__file__).parent / "turbopark_lookup_table.mat" lookup_table_file = scipy.io.loadmat(lookup_table_matlab_file) - dist = lookup_table_file["overlap_lookup_table"][0][0][0][0] - radius_down = lookup_table_file["overlap_lookup_table"][0][0][1][0] - overlap_gauss = lookup_table_file["overlap_lookup_table"][0][0][2] + dist = lookup_table_file['overlap_lookup_table'][0][0][0][0] + radius_down = lookup_table_file['overlap_lookup_table'][0][0][1][0] + overlap_gauss = lookup_table_file['overlap_lookup_table'][0][0][2] self.overlap_gauss_interp = RegularGridInterpolator( - (dist, radius_down), overlap_gauss, method="linear", bounds_error=False + (dist, radius_down), + overlap_gauss, + method='linear', + bounds_error=False ) def prepare_function( @@ -62,6 +65,7 @@ def prepare_function( grid: Grid, flow_field: FlowField, ) -> Dict[str, Any]: + kwargs = { "x": grid.x_sorted, "y": grid.y_sorted, @@ -97,7 +101,7 @@ def function( # subsequent runtime warnings. # Here self.NUM_EPS is to avoid precision issues with masking, and is slightly # larger than 0.0 - downstream_mask = x_i - x >= self.NUM_EPS + downstream_mask = (x_i - x >= self.NUM_EPS) x_dist = (x_i - x) * downstream_mask / rotor_diameters # Radial distance between turbine i and the center lines of wakes from all @@ -110,7 +114,7 @@ def function( # Characteristic wake widths from all turbines relative to turbine i dw = characteristic_wake_width(x_dist, ambient_turbulence_intensities, Cts, self.A) epsilon = 0.25 * np.sqrt( - np.min(0.5 * (1 + np.sqrt(1 - Cts)) / np.sqrt(1 - Cts), 3, keepdims=True) + np.min( 0.5 * (1 + np.sqrt(1 - Cts)) / np.sqrt(1 - Cts), 3, keepdims=True ) ) sigma = rotor_diameters * (epsilon + dw) @@ -127,15 +131,11 @@ def function( delta_image = np.empty(np.shape(u_initial)) * np.nan # Compute deficits for real turbines and for mirrored (image) turbines - delta_real = ( - C - * wtg_overlapping - * self.overlap_gauss_interp((r_dist / sigma, rotor_diameter_i / 2 / sigma)) + delta_real = C * wtg_overlapping * self.overlap_gauss_interp( + (r_dist / sigma, rotor_diameter_i / 2 / sigma) ) - delta_image = ( - C - * wtg_overlapping - * self.overlap_gauss_interp((r_dist_image / sigma, rotor_diameter_i / 2 / sigma)) + delta_image = C * wtg_overlapping * self.overlap_gauss_interp( + (r_dist_image / sigma, rotor_diameter_i / 2 / sigma) ) delta = np.concatenate((delta_real, delta_image), axis=1) @@ -156,12 +156,10 @@ def precalculate_overlap(): for i in range(len(dist)): for j in range(len(radius_down)): if radius_down[j] > 0: - def fun(r, theta): return r * np.exp( - -1 * (r**2 + dist[i] ** 2 - 2 * dist[i] * r * np.cos(theta)) / 2 + -1 * (r ** 2 + dist[i] ** 2 - 2 * dist[i] * r * np.cos(theta)) / 2 ) - out = integrate.dblquad(fun, 0, radius_down[j], lambda x: 0, lambda x: 2 * np.pi)[0] out = out / (np.pi * radius_down[j] ** 2) else: @@ -181,17 +179,12 @@ def characteristic_wake_width(x_dist, TI, Cts, A): alpha = TI * c1 beta = c2 * TI / np.sqrt(Cts) - dw = ( - A - * TI - / beta - * ( - np.sqrt((alpha + beta * x_dist) ** 2 + 1) - - np.sqrt(1 + alpha**2) - - np.log( - ((np.sqrt((alpha + beta * x_dist) ** 2 + 1) + 1) * alpha) - / ((np.sqrt(1 + alpha**2) + 1) * (alpha + beta * x_dist)) - ) + dw = A * TI / beta * ( + np.sqrt((alpha + beta * x_dist) ** 2 + 1) + - np.sqrt(1 + alpha ** 2) + - np.log( + ((np.sqrt((alpha + beta * x_dist) ** 2 + 1) + 1) * alpha) + / ((np.sqrt(1 + alpha ** 2) + 1) * (alpha + beta * x_dist)) ) ) From cbfb48ddb4b866ed860ca3ca50614f837ac35b8e Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 23 Jan 2024 19:18:19 -0800 Subject: [PATCH 066/101] Remove pure format changes --- floris/simulation/flow_field.py | 49 +++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index c47baad83..0e81fdcba 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -119,6 +119,7 @@ def het_map_validator(self, instance: attrs.Attribute, value: list | None) -> No "The het_map's first dimension not equal to the FLORIS first dimension." ) + def __attrs_post_init__(self) -> None: if self.heterogenous_inflow_config is not None: self.generate_heterogeneous_wind_map() @@ -129,6 +130,7 @@ def __attrs_post_init__(self) -> None: self.turbulence_intensities = self.turbulence_intensities[0] * np.ones(self.n_findex) def initialize_velocity_field(self, grid: Grid) -> None: + # Create an initial wind profile as a function of height. The values here will # be multiplied with the wind speeds to give the initial wind field. # Since we use grid.z, this is a vertical plane for each turbine @@ -143,7 +145,11 @@ def initialize_velocity_field(self, grid: Grid) -> None: 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) + * np.power( + grid.z_sorted, + (self.wind_shear - 1), + where=grid.z_sorted != 0.0 + ) ) # If no heterogeneous inflow defined, then set all speeds ups to 1.0 if self.het_map is None: @@ -152,11 +158,10 @@ def initialize_velocity_field(self, grid: Grid) -> None: # If heterogeneous flow data is given, the speed ups at the defined # grid locations are determined in either 2 or 3 dimensions. else: - bounds = np.array( - list( - zip(self.heterogenous_inflow_config["x"], self.heterogenous_inflow_config["y"]) - ) - ) + bounds = np.array(list(zip( + self.heterogenous_inflow_config['x'], + self.heterogenous_inflow_config['y'] + ))) hull = ConvexHull(bounds) polygon = Polygon(bounds[hull.vertices]) path = mpltPath.Path(polygon.boundary.coords) @@ -178,14 +183,16 @@ def initialize_velocity_field(self, grid: Grid) -> None: if len(self.het_map[0].points[0]) == 2: speed_ups = self.calculate_speed_ups( - self.het_map, grid.x_sorted_inertial_frame, grid.y_sorted_inertial_frame + self.het_map, + grid.x_sorted_inertial_frame, + grid.y_sorted_inertial_frame ) elif len(self.het_map[0].points[0]) == 3: speed_ups = self.calculate_speed_ups( self.het_map, grid.x_sorted_inertial_frame, grid.y_sorted_inertial_frame, - grid.z_sorted, + grid.z_sorted ) # Create the sheer-law wind profile @@ -198,10 +205,12 @@ def initialize_velocity_field(self, grid: Grid) -> None: self.dudz_initial_sorted = (self.wind_speeds.T * dwind_profile_plane.T).T * speed_ups self.v_initial_sorted = np.zeros( - np.shape(self.u_initial_sorted), dtype=self.u_initial_sorted.dtype + 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 + np.shape(self.u_initial_sorted), + dtype=self.u_initial_sorted.dtype ) self.u_sorted = self.u_initial_sorted.copy() @@ -223,8 +232,12 @@ def finalize(self, unsorted_indices): self.w = np.take_along_axis(self.w_sorted, unsorted_indices, axis=1) self.turbulence_intensity_field = np.mean( - np.take_along_axis(self.turbulence_intensity_field_sorted, unsorted_indices, axis=1), - axis=(2, 3), + np.take_along_axis( + self.turbulence_intensity_field_sorted, + unsorted_indices, + axis=1 + ), + axis=(2,3) ) def calculate_speed_ups(self, het_map, x, y, z=None): @@ -232,7 +245,7 @@ def calculate_speed_ups(self, het_map, x, y, z=None): # Calculate the 3-dimensional speed ups; squeeze is needed as the generator # adds an extra dimension speed_ups = np.squeeze( - [het_map[i](x[i : i + 1], y[i : i + 1], z[i : i + 1]) for i in range(len(het_map))], + [het_map[i](x[i:i+1], y[i:i+1], z[i:i+1]) for i in range( len(het_map))], axis=1, ) @@ -240,7 +253,7 @@ def calculate_speed_ups(self, het_map, x, y, z=None): # Calculate the 2-dimensional speed ups; squeeze is needed as the generator # adds an extra dimension speed_ups = np.squeeze( - [het_map[i](x[i : i + 1], y[i : i + 1]) for i in range(len(het_map))], + [het_map[i](x[i:i+1], y[i:i+1]) for i in range(len(het_map))], axis=1, ) @@ -263,10 +276,10 @@ def generate_heterogeneous_wind_map(self): - **y**: A list of y locations at which the speed up factors are defined. - **z** (optional): A list of z locations at which the speed up factors are defined. """ - speed_multipliers = self.heterogenous_inflow_config["speed_multipliers"] - x = self.heterogenous_inflow_config["x"] - y = self.heterogenous_inflow_config["y"] - z = self.heterogenous_inflow_config["z"] + speed_multipliers = self.heterogenous_inflow_config['speed_multipliers'] + x = self.heterogenous_inflow_config['x'] + y = self.heterogenous_inflow_config['y'] + z = self.heterogenous_inflow_config['z'] if z is not None: # Compute the 3-dimensional interpolants for each wind direction From 20aebb9bb8f583e301b968dd8ee7d4a12979d965 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 23 Jan 2024 19:28:19 -0800 Subject: [PATCH 067/101] Remove pure formatting changes --- floris/simulation/solver.py | 420 ++++++++++++++++++++---------------- 1 file changed, 233 insertions(+), 187 deletions(-) diff --git a/floris/simulation/solver.py b/floris/simulation/solver.py index e428cd8e3..b930070df 100644 --- a/floris/simulation/solver.py +++ b/floris/simulation/solver.py @@ -56,7 +56,10 @@ def calculate_area_overlap(wake_velocities, freestream_velocities, y_ngrid, z_ng # @profile def sequential_solver( - farm: Farm, flow_field: FlowField, grid: TurbineGrid, model_manager: WakeModelManager + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + model_manager: WakeModelManager ) -> None: # Algorithm # For each turbine, calculate its effect on every downstream turbine. @@ -87,16 +90,17 @@ def sequential_solver( # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): + # Get the current turbine quantities - x_i = np.mean(grid.x_sorted[:, i : i + 1], axis=(2, 3)) + x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(grid.y_sorted[:, i : i + 1], axis=(2, 3)) + y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(grid.z_sorted[:, i : i + 1], axis=(2, 3)) + z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3)) z_i = z_i[:, :, None, None] - u_i = flow_field.u_sorted[:, i : i + 1] - v_i = flow_field.v_sorted[:, i : i + 1] + u_i = flow_field.u_sorted[:, i:i+1] + v_i = flow_field.v_sorted[:, i:i+1] ct_i = thrust_coefficient( velocities=flow_field.u_sorted, @@ -110,7 +114,7 @@ def sequential_solver( ix_filter=[i], average_method=grid.average_method, cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions, + multidim_condition=flow_field.multidim_conditions ) # Since we are filtering for the i'th turbine in the thrust coefficient function, # get the first index here (0:1) @@ -127,16 +131,16 @@ def sequential_solver( ix_filter=[i], average_method=grid.average_method, cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions, + multidim_condition=flow_field.multidim_conditions ) # Since we are filtering for the i'th turbine in the axial induction function, # get the first index here (0:1) axial_induction_i = axial_induction_i[:, 0:1, None, None] - turbulence_intensity_i = turbine_turbulence_intensity[:, i : i + 1] - yaw_angle_i = farm.yaw_angles_sorted[:, i : i + 1, None, None] - hub_height_i = farm.hub_heights_sorted[:, i : i + 1, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[:, i : i + 1, None, None] - TSR_i = farm.TSRs_sorted[:, i : i + 1, None, None] + turbulence_intensity_i = turbine_turbulence_intensity[:, i:i+1] + yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] + hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] + rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] + TSR_i = farm.TSRs_sorted[:, i:i+1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i @@ -146,8 +150,8 @@ def sequential_solver( u_i, v_i, flow_field.u_initial_sorted, - grid.y_sorted[:, i : i + 1] - y_i, - grid.z_sorted[:, i : i + 1], + grid.y_sorted[:, i:i+1] - y_i, + grid.z_sorted[:, i:i+1], rotor_diameter_i, hub_height_i, ct_i, @@ -191,14 +195,12 @@ def sequential_solver( u_i, turbulence_intensity_i, v_i, - flow_field.w_sorted[:, i : i + 1], - v_wake[:, i : i + 1], - w_wake[:, i : i + 1], + flow_field.w_sorted[:, i:i+1], + v_wake[:, i:i+1], + w_wake[:, i:i+1], ) gch_gain = 2 - turbine_turbulence_intensity[:, i : i + 1] = ( - turbulence_intensity_i + gch_gain * I_mixing - ) + turbine_turbulence_intensity[:, i:i+1] = turbulence_intensity_i + gch_gain * I_mixing # NOTE: exponential velocity_deficit = model_manager.velocity_model.function( @@ -216,7 +218,8 @@ def sequential_solver( ) wake_field = model_manager.combination_model.function( - wake_field, velocity_deficit * flow_field.u_initial_sorted + wake_field, + velocity_deficit * flow_field.u_initial_sorted ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( @@ -228,9 +231,10 @@ def sequential_solver( ) # Calculate wake overlap for wake-added turbulence (WAT) - area_overlap = np.sum( - velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3) - ) / (grid.grid_resolution * grid.grid_resolution) + area_overlap = ( + np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3)) + / (grid.grid_resolution * grid.grid_resolution) + ) area_overlap = area_overlap[:, :, None, None] # Modify wake added turbulence by wake area overlap @@ -254,7 +258,8 @@ def sequential_solver( flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity flow_field.turbulence_intensity_field_sorted_avg = np.mean( - turbine_turbulence_intensity, axis=(2, 3) + turbine_turbulence_intensity, + axis=(2,3) )[:, :, None, None] @@ -262,8 +267,9 @@ def full_flow_sequential_solver( farm: Farm, flow_field: FlowField, flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, - model_manager: WakeModelManager, + model_manager: WakeModelManager ) -> None: + # Get the flow quantities and turbine performance turbine_grid_farm = copy.deepcopy(farm) turbine_grid_flow_field = copy.deepcopy(flow_field) @@ -299,9 +305,13 @@ def full_flow_sequential_solver( # Use full flow_field here to use the full grid in the wake models deflection_model_args = model_manager.deflection_model.prepare_function( - flow_field_grid, flow_field + flow_field_grid, + flow_field + ) + deficit_model_args = model_manager.velocity_model.prepare_function( + flow_field_grid, + flow_field ) - deficit_model_args = model_manager.velocity_model.prepare_function(flow_field_grid, flow_field) wake_field = np.zeros_like(flow_field.u_initial_sorted) v_wake = np.zeros_like(flow_field.v_initial_sorted) @@ -309,16 +319,17 @@ def full_flow_sequential_solver( # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(flow_field_grid.n_turbines): + # Get the current turbine quantities - x_i = np.mean(turbine_grid.x_sorted[:, i : i + 1], axis=(2, 3)) + x_i = np.mean(turbine_grid.x_sorted[:, i:i+1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(turbine_grid.y_sorted[:, i : i + 1], axis=(2, 3)) + y_i = np.mean(turbine_grid.y_sorted[:, i:i+1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(turbine_grid.z_sorted[:, i : i + 1], axis=(2, 3)) + z_i = np.mean(turbine_grid.z_sorted[:, i:i+1], axis=(2, 3)) z_i = z_i[:, :, None, None] - u_i = turbine_grid_flow_field.u_sorted[:, i : i + 1] - v_i = turbine_grid_flow_field.v_sorted[:, i : i + 1] + u_i = turbine_grid_flow_field.u_sorted[:, i:i+1] + v_i = turbine_grid_flow_field.v_sorted[:, i:i+1] ct_i = thrust_coefficient( velocities=turbine_grid_flow_field.u_sorted, @@ -348,13 +359,12 @@ def full_flow_sequential_solver( # Since we are filtering for the i'th turbine in the axial induction function, # get the first index here (0:1) axial_induction_i = axial_induction_i[:, 0:1, None, None] - turbulence_intensity_i = turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[ - :, i : i + 1 - ] - yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i : i + 1, None, None] - hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i : i + 1, None, None] - rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i : i + 1, None, None] - TSR_i = turbine_grid_farm.TSRs_sorted[:, i : i + 1, None, None] + turbulence_intensity_i = \ + turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] + yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i:i+1, None, None] + hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i:i+1, None, None] + rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i:i+1, None, None] + TSR_i = turbine_grid_farm.TSRs_sorted[:, i:i+1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i @@ -364,8 +374,8 @@ def full_flow_sequential_solver( u_i, v_i, turbine_grid_flow_field.u_initial_sorted, - turbine_grid.y_sorted[:, i : i + 1] - y_i, - turbine_grid.z_sorted[:, i : i + 1], + turbine_grid.y_sorted[:, i:i+1] - y_i, + turbine_grid.z_sorted[:, i:i+1], rotor_diameter_i, hub_height_i, ct_i, @@ -420,7 +430,8 @@ def full_flow_sequential_solver( ) wake_field = model_manager.combination_model.function( - wake_field, velocity_deficit * flow_field.u_initial_sorted + wake_field, + velocity_deficit * flow_field.u_initial_sorted ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field @@ -429,7 +440,10 @@ def full_flow_sequential_solver( def cc_solver( - farm: Farm, flow_field: FlowField, grid: TurbineGrid, model_manager: WakeModelManager + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + model_manager: WakeModelManager ) -> None: # <> deflection_model_args = model_manager.deflection_model.prepare_function(grid, flow_field) @@ -462,15 +476,16 @@ def cc_solver( # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): + # Get the current turbine quantities - x_i = np.mean(grid.x_sorted[:, i : i + 1], axis=(2, 3)) + x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(grid.y_sorted[:, i : i + 1], axis=(2, 3)) + y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(grid.z_sorted[:, i : i + 1], axis=(2, 3)) + z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3)) z_i = z_i[:, :, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[:, i : i + 1, None, None] + rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] mask2 = ( (grid.x_sorted < x_i + 0.01) @@ -479,7 +494,8 @@ def cc_solver( * (grid.y_sorted > y_i - 0.51 * rotor_diameter_i) ) turb_inflow_field = ( - turb_inflow_field * ~mask2 + (flow_field.u_initial_sorted - turb_u_wake) * mask2 + turb_inflow_field * ~mask2 + + (flow_field.u_initial_sorted - turb_u_wake) * mask2 ) turb_avg_vels = average_velocity(turb_inflow_field) @@ -493,7 +509,7 @@ def cc_solver( turbine_type_map=farm.turbine_type_map_sorted, turbine_power_thrust_tables=farm.turbine_power_thrust_tables, average_method=grid.average_method, - cubature_weights=grid.cubature_weights, + cubature_weights=grid.cubature_weights ) turb_Cts = turb_Cts[:, :, None, None] turb_aIs = axial_induction( @@ -507,12 +523,12 @@ def cc_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[i], average_method=grid.average_method, - cubature_weights=grid.cubature_weights, + cubature_weights=grid.cubature_weights ) turb_aIs = turb_aIs[:, :, None, None] - u_i = turb_inflow_field[:, i : i + 1] - v_i = flow_field.v_sorted[:, i : i + 1] + u_i = turb_inflow_field[:, i:i+1] + v_i = flow_field.v_sorted[:, i:i+1] axial_induction_i = axial_induction( velocities=flow_field.u_sorted, @@ -525,15 +541,15 @@ def cc_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[i], average_method=grid.average_method, - cubature_weights=grid.cubature_weights, + cubature_weights=grid.cubature_weights ) axial_induction_i = axial_induction_i[:, :, None, None] - turbulence_intensity_i = turbine_turbulence_intensity[:, i : i + 1] - yaw_angle_i = farm.yaw_angles_sorted[:, i : i + 1, None, None] - hub_height_i = farm.hub_heights_sorted[:, i : i + 1, None, None] - TSR_i = farm.TSRs_sorted[:, i : i + 1, None, None] + turbulence_intensity_i = turbine_turbulence_intensity[:, i:i+1] + yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] + hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] + TSR_i = farm.TSRs_sorted[:, i:i+1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i @@ -543,11 +559,11 @@ def cc_solver( u_i, v_i, flow_field.u_initial_sorted, - grid.y_sorted[:, i : i + 1] - y_i, - grid.z_sorted[:, i : i + 1], + grid.y_sorted[:, i:i+1] - y_i, + grid.z_sorted[:, i:i+1], rotor_diameter_i, hub_height_i, - turb_Cts[:, i : i + 1], + turb_Cts[:, i:i+1], TSR_i, axial_induction_i, flow_field.wind_shear, @@ -562,7 +578,7 @@ def cc_solver( y_i, effective_yaw_i, turbulence_intensity_i, - turb_Cts[:, i : i + 1], + turb_Cts[:, i:i+1], rotor_diameter_i, **deflection_model_args, ) @@ -578,7 +594,7 @@ def cc_solver( rotor_diameter_i, hub_height_i, yaw_angle_i, - turb_Cts[:, i : i + 1], + turb_Cts[:, i:i+1], TSR_i, axial_induction_i, flow_field.wind_shear, @@ -590,14 +606,12 @@ def cc_solver( u_i, turbulence_intensity_i, v_i, - flow_field.w_sorted[:, i : i + 1], - v_wake[:, i : i + 1], - w_wake[:, i : i + 1], + flow_field.w_sorted[:, i:i+1], + v_wake[:, i:i+1], + w_wake[:, i:i+1], ) gch_gain = 1.0 - turbine_turbulence_intensity[:, i : i + 1] = ( - turbulence_intensity_i + gch_gain * I_mixing - ) + turbine_turbulence_intensity[:, i:i+1] = turbulence_intensity_i + gch_gain * I_mixing turb_u_wake, Ctmp = model_manager.velocity_model.function( i, @@ -621,7 +635,8 @@ def cc_solver( # Calculate wake overlap for wake-added turbulence (WAT) area_overlap = 1 - ( - np.sum(turb_u_wake <= 0.05, axis=(2, 3)) / (grid.grid_resolution * grid.grid_resolution) + np.sum(turb_u_wake <= 0.05, axis=(2, 3)) + / (grid.grid_resolution * grid.grid_resolution) ) area_overlap = area_overlap[:, :, None, None] @@ -646,7 +661,8 @@ def cc_solver( flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity flow_field.turbulence_intensity_field_sorted_avg = np.mean( - turbine_turbulence_intensity, axis=(2, 3) + turbine_turbulence_intensity, + axis=(2,3) ) @@ -691,9 +707,13 @@ def full_flow_cc_solver( # Use full flow_field here to use the full grid in the wake models deflection_model_args = model_manager.deflection_model.prepare_function( - flow_field_grid, flow_field + flow_field_grid, + flow_field + ) + deficit_model_args = model_manager.velocity_model.prepare_function( + flow_field_grid, + flow_field ) - deficit_model_args = model_manager.velocity_model.prepare_function(flow_field_grid, flow_field) v_wake = np.zeros_like(flow_field.v_initial_sorted) w_wake = np.zeros_like(flow_field.w_initial_sorted) @@ -704,16 +724,17 @@ def full_flow_cc_solver( # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(flow_field_grid.n_turbines): + # Get the current turbine quantities - x_i = np.mean(turbine_grid.x_sorted[:, i : i + 1], axis=(2, 3)) + x_i = np.mean(turbine_grid.x_sorted[:, i:i+1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(turbine_grid.y_sorted[:, i : i + 1], axis=(2, 3)) + y_i = np.mean(turbine_grid.y_sorted[:, i:i+1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(turbine_grid.z_sorted[:, i : i + 1], axis=(2, 3)) + z_i = np.mean(turbine_grid.z_sorted[:, i:i+1], axis=(2, 3)) z_i = z_i[:, :, None, None] - u_i = turbine_grid_flow_field.u_sorted[:, i : i + 1] - v_i = turbine_grid_flow_field.v_sorted[:, i : i + 1] + u_i = turbine_grid_flow_field.u_sorted[:, i:i+1] + v_i = turbine_grid_flow_field.v_sorted[:, i:i+1] turb_avg_vels = average_velocity(turbine_grid_flow_field.u_sorted) turb_Cts = thrust_coefficient( @@ -726,7 +747,7 @@ def full_flow_cc_solver( turbine_type_map=turbine_grid_farm.turbine_type_map_sorted, turbine_power_thrust_tables=turbine_grid_farm.turbine_power_thrust_tables, average_method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights, + cubature_weights=turbine_grid.cubature_weights ) turb_Cts = turb_Cts[:, :, None, None] @@ -741,17 +762,16 @@ def full_flow_cc_solver( turbine_power_thrust_tables=turbine_grid_farm.turbine_power_thrust_tables, ix_filter=[i], average_method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights, + cubature_weights=turbine_grid.cubature_weights ) axial_induction_i = axial_induction_i[:, :, None, None] - turbulence_intensity_i = turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[ - :, i : i + 1 - ] - yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i : i + 1, None, None] - hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i : i + 1, None, None] - rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i : i + 1, None, None] - TSR_i = turbine_grid_farm.TSRs_sorted[:, i : i + 1, None, None] + turbulence_intensity_i = \ + turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] + yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i:i+1, None, None] + hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i:i+1, None, None] + rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i:i+1, None, None] + TSR_i = turbine_grid_farm.TSRs_sorted[:, i:i+1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i @@ -761,11 +781,11 @@ def full_flow_cc_solver( u_i, v_i, turbine_grid_flow_field.u_initial_sorted, - turbine_grid.y_sorted[:, i : i + 1] - y_i, - turbine_grid.z_sorted[:, i : i + 1], + turbine_grid.y_sorted[:, i:i+1] - y_i, + turbine_grid.z_sorted[:, i:i+1], rotor_diameter_i, hub_height_i, - turb_Cts[:, i : i + 1], + turb_Cts[:, i:i+1], TSR_i, axial_induction_i, flow_field.wind_shear, @@ -780,7 +800,7 @@ def full_flow_cc_solver( y_i, effective_yaw_i, turbulence_intensity_i, - turb_Cts[:, i : i + 1], + turb_Cts[:, i:i+1], rotor_diameter_i, **deflection_model_args, ) @@ -796,7 +816,7 @@ def full_flow_cc_solver( rotor_diameter_i, hub_height_i, yaw_angle_i, - turb_Cts[:, i : i + 1], + turb_Cts[:, i:i+1], TSR_i, axial_induction_i, flow_field.wind_shear, @@ -826,7 +846,10 @@ def full_flow_cc_solver( def turbopark_solver( - farm: Farm, flow_field: FlowField, grid: TurbineGrid, model_manager: WakeModelManager + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + model_manager: WakeModelManager ) -> None: # Algorithm # For each turbine, calculate its effect on every downstream turbine. @@ -861,15 +884,15 @@ def turbopark_solver( # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): # Get the current turbine quantities - x_i = np.mean(grid.x_sorted[:, i : i + 1], axis=(2, 3)) + x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(grid.y_sorted[:, i : i + 1], axis=(2, 3)) + y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(grid.z_sorted[:, i : i + 1], axis=(2, 3)) + z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3)) z_i = z_i[:, :, None, None] - u_i = flow_field.u_sorted[:, :, i : i + 1] - v_i = flow_field.v_sorted[:, :, i : i + 1] + u_i = flow_field.u_sorted[:, :, i:i+1] + v_i = flow_field.v_sorted[:, :, i:i+1] Cts = thrust_coefficient( velocities=flow_field.u_sorted, @@ -881,7 +904,7 @@ def turbopark_solver( turbine_type_map=farm.turbine_type_map_sorted, turbine_power_thrust_tables=farm.turbine_power_thrust_tables, average_method=grid.average_method, - cubature_weights=grid.cubature_weights, + cubature_weights=grid.cubature_weights ) ct_i = thrust_coefficient( @@ -895,7 +918,7 @@ def turbopark_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[i], average_method=grid.average_method, - cubature_weights=grid.cubature_weights, + cubature_weights=grid.cubature_weights ) # Since we are filtering for the i'th turbine in the thrust coefficient function, # get the first index here (0:1) @@ -911,27 +934,28 @@ def turbopark_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[i], average_method=grid.average_method, - cubature_weights=grid.cubature_weights, + cubature_weights=grid.cubature_weights ) # Since we are filtering for the i'th turbine in the axial induction function, # get the first index here (0:1) axial_induction_i = axial_induction_i[:, 0:1, None, None] - turbulence_intensity_i = turbine_turbulence_intensity[:, i : i + 1] - yaw_angle_i = farm.yaw_angles_sorted[:, i : i + 1, None, None] - hub_height_i = farm.hub_heights_sorted[:, i : i + 1, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[:, i : i + 1, None, None] - TSR_i = farm.TSRs_sorted[:, i : i + 1, None, None] + turbulence_intensity_i = turbine_turbulence_intensity[:, i:i+1] + yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] + hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] + rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] + TSR_i = farm.TSRs_sorted[:, i:i+1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i + if model_manager.enable_secondary_steering: added_yaw = wake_added_yaw( u_i, v_i, flow_field.u_initial_sorted, - grid.y_sorted[:, i : i + 1] - y_i, - grid.z_sorted[:, i : i + 1], + grid.y_sorted[:, i:i+1] - y_i, + grid.z_sorted[:, i:i+1], rotor_diameter_i, hub_height_i, ct_i, @@ -950,13 +974,13 @@ def turbopark_solver( "and perform a thorough examination of the results." ) for ii in range(i): - x_ii = np.mean(grid.x_sorted[:, ii : ii + 1], axis=(2, 3)) + x_ii = np.mean(grid.x_sorted[:, ii:ii+1], axis=(2, 3)) x_ii = x_ii[:, :, None, None] - y_ii = np.mean(grid.y_sorted[:, ii : ii + 1], axis=(2, 3)) + y_ii = np.mean(grid.y_sorted[:, ii:ii+1], axis=(2, 3)) y_ii = y_ii[:, :, None, None] - yaw_ii = farm.yaw_angles_sorted[:, ii : ii + 1, None, None] - turbulence_intensity_ii = turbine_turbulence_intensity[:, ii : ii + 1] + yaw_ii = farm.yaw_angles_sorted[:, ii:ii+1, None, None] + turbulence_intensity_ii = turbine_turbulence_intensity[:, ii:ii+1] ct_ii = thrust_coefficient( velocities=flow_field.u_sorted, yaw_angles=farm.yaw_angles_sorted, @@ -968,10 +992,10 @@ def turbopark_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[ii], average_method=grid.average_method, - cubature_weights=grid.cubature_weights, + cubature_weights=grid.cubature_weights ) ct_ii = ct_ii[:, 0:1, None, None] - rotor_diameter_ii = farm.rotor_diameters_sorted[:, ii : ii + 1, None, None] + rotor_diameter_ii = farm.rotor_diameters_sorted[:, ii:ii+1, None, None] deflection_field_ii = model_manager.deflection_model.function( x_ii, @@ -983,7 +1007,7 @@ def turbopark_solver( **deflection_model_args, ) - deflection_field[:, ii : ii + 1, :, :] = deflection_field_ii[:, i : i + 1, :, :] + deflection_field[:, ii:ii+1, :, :] = deflection_field_ii[:, i:i+1, :, :] if model_manager.enable_transverse_velocities: v_wake, w_wake = calculate_transverse_velocity( @@ -1007,14 +1031,12 @@ def turbopark_solver( u_i, turbulence_intensity_i, v_i, - flow_field.w_sorted[:, :, i : i + 1], - v_wake[:, :, i : i + 1], - w_wake[:, :, i : i + 1], + flow_field.w_sorted[:, :, i:i+1], + v_wake[:, :, i:i+1], + w_wake[:, :, i:i+1], ) gch_gain = 2 - turbine_turbulence_intensity[:, :, i : i + 1] = ( - turbulence_intensity_i + gch_gain * I_mixing - ) + turbine_turbulence_intensity[:, :, i:i+1] = turbulence_intensity_i + gch_gain * I_mixing # NOTE: exponential velocity_deficit = model_manager.velocity_model.function( @@ -1031,7 +1053,8 @@ def turbopark_solver( ) wake_field = model_manager.combination_model.function( - wake_field, velocity_deficit * flow_field.u_initial_sorted + wake_field, + velocity_deficit * flow_field.u_initial_sorted ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( @@ -1042,9 +1065,10 @@ def turbopark_solver( # compute area_overlap as the current wake deficit is solved for only upstream # turbines; could use WAT_upstream # Calculate wake overlap for wake-added turbulence (WAT) - area_overlap = np.sum( - velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3) - ) / (grid.grid_resolution * grid.grid_resolution) + area_overlap = ( + np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3)) + / (grid.grid_resolution * grid.grid_resolution) + ) area_overlap = area_overlap[:, :, None, None] # Modify wake added turbulence by wake area overlap @@ -1068,7 +1092,8 @@ def turbopark_solver( flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity flow_field.turbulence_intensity_field_sorted_avg = np.mean( - turbine_turbulence_intensity, axis=(2, 3) + turbine_turbulence_intensity, + axis=(2, 3) ) @@ -1076,13 +1101,16 @@ def full_flow_turbopark_solver( farm: Farm, flow_field: FlowField, flow_field_grid: FlowFieldGrid, - model_manager: WakeModelManager, + model_manager: WakeModelManager ) -> None: raise NotImplementedError("Plotting for the TurbOPark model is not currently implemented.") def empirical_gauss_solver( - farm: Farm, flow_field: FlowField, grid: TurbineGrid, model_manager: WakeModelManager + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + model_manager: WakeModelManager ) -> NDArrayFloat: """ Algorithm: @@ -1105,6 +1133,7 @@ def empirical_gauss_solver( NDArrayFloat: wake induced mixing field primarily for use in the full-flow EmGauss solver """ + # <> deflection_model_args = model_manager.deflection_model.prepare_function(grid, flow_field) deficit_model_args = model_manager.velocity_model.prepare_function(grid, flow_field) @@ -1114,12 +1143,11 @@ def empirical_gauss_solver( v_wake = np.zeros_like(flow_field.v_initial_sorted) w_wake = np.zeros_like(flow_field.w_initial_sorted) - x_locs = np.mean(grid.x_sorted, axis=(2, 3))[:, :, None] - downstream_distance_D = x_locs - np.transpose(x_locs, axes=(0, 2, 1)) - downstream_distance_D = downstream_distance_D / np.repeat( - farm.rotor_diameters_sorted[:, :, None], grid.n_turbines, axis=-1 - ) - downstream_distance_D = np.maximum(downstream_distance_D, 0.1) # For ease + x_locs = np.mean(grid.x_sorted, axis=(2, 3))[:,:,None] + downstream_distance_D = x_locs - np.transpose(x_locs, axes=(0,2,1)) + downstream_distance_D = downstream_distance_D / \ + np.repeat(farm.rotor_diameters_sorted[:,:,None], grid.n_turbines, axis=-1) + downstream_distance_D = np.maximum(downstream_distance_D, 0.1) # For ease # Initialize the mixing factor model using TI if specified initial_mixing_factor = model_manager.turbulence_model.atmospheric_ti_gain * np.eye( grid.n_turbines @@ -1129,16 +1157,17 @@ def empirical_gauss_solver( # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): + # Get the current turbine quantities - x_i = np.mean(grid.x_sorted[:, i : i + 1], axis=(2, 3)) + x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3)) x_i = x_i[:, :, None, None] - y_i = np.mean(grid.y_sorted[:, i : i + 1], axis=(2, 3)) + y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3)) y_i = y_i[:, :, None, None] - z_i = np.mean(grid.z_sorted[:, i : i + 1], axis=(2, 3)) + z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3)) z_i = z_i[:, :, None, None] - flow_field.u_sorted[:, i : i + 1] - flow_field.v_sorted[:, i : i + 1] + flow_field.u_sorted[:, i:i+1] + flow_field.v_sorted[:, i:i+1] ct_i = thrust_coefficient( velocities=flow_field.u_sorted, @@ -1151,7 +1180,7 @@ def empirical_gauss_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[i], average_method=grid.average_method, - cubature_weights=grid.cubature_weights, + cubature_weights=grid.cubature_weights ) # Since we are filtering for the i'th turbine in the thrust coefficient function, # get the first index here (0:1) @@ -1167,43 +1196,49 @@ def empirical_gauss_solver( turbine_power_thrust_tables=farm.turbine_power_thrust_tables, ix_filter=[i], average_method=grid.average_method, - cubature_weights=grid.cubature_weights, + cubature_weights=grid.cubature_weights ) # Since we are filtering for the i'th turbine in the axial induction function, # get the first index here (0:1) axial_induction_i = axial_induction_i[:, 0:1, None, None] - yaw_angle_i = farm.yaw_angles_sorted[:, i : i + 1, None, None] - hub_height_i = farm.hub_heights_sorted[:, i : i + 1, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[:, i : i + 1, None, None] + yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] + hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] + rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] # Secondary steering not currently implemented in EmGauss model # effective_yaw_i = np.zeros_like(yaw_angle_i) # effective_yaw_i += yaw_angle_i average_velocities = average_velocity( - flow_field.u_sorted, method=grid.average_method, cubature_weights=grid.cubature_weights + flow_field.u_sorted, + method=grid.average_method, + cubature_weights=grid.cubature_weights ) tilt_angle_i = farm.calculate_tilt_for_eff_velocities(average_velocities) - tilt_angle_i = tilt_angle_i[:, i : i + 1, None, None] + tilt_angle_i = tilt_angle_i[:, i:i+1, None, None] if model_manager.enable_secondary_steering: - raise NotImplementedError("Secondary steering not available for this model.") + raise NotImplementedError( + "Secondary steering not available for this model.") if model_manager.enable_transverse_velocities: - raise NotImplementedError("Transverse velocities not used in this model.") + raise NotImplementedError( + "Transverse velocities not used in this model.") if model_manager.enable_yaw_added_recovery: # Influence of yawing on turbine's own wake - mixing_factor[:, i : i + 1, i] += yaw_added_wake_mixing( - axial_induction_i, - yaw_angle_i, - 1, - model_manager.deflection_model.yaw_added_mixing_gain, - ) + mixing_factor[:, i:i+1, i] += \ + yaw_added_wake_mixing( + axial_induction_i, + yaw_angle_i, + 1, + model_manager.deflection_model.yaw_added_mixing_gain + ) # Extract total wake induced mixing for turbine i mixing_i = np.linalg.norm( - mixing_factor[:, i : i + 1, :, None], ord=2, axis=2, keepdims=True + mixing_factor[:, i:i+1, :, None], + ord=2, axis=2, keepdims=True ) # Model calculations @@ -1216,7 +1251,7 @@ def empirical_gauss_solver( mixing_i, ct_i, rotor_diameter_i, - **deflection_model_args, + **deflection_model_args ) # NOTE: exponential @@ -1233,28 +1268,30 @@ def empirical_gauss_solver( ct_i, hub_height_i, rotor_diameter_i, - **deficit_model_args, + **deficit_model_args ) wake_field = model_manager.combination_model.function( - wake_field, velocity_deficit * flow_field.u_initial_sorted + wake_field, + velocity_deficit * flow_field.u_initial_sorted ) # Calculate wake overlap for wake-added turbulence (WAT) - area_overlap = np.sum( - velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3) - ) / (grid.grid_resolution * grid.grid_resolution) + area_overlap = np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3))\ + / (grid.grid_resolution * grid.grid_resolution) # Compute wake induced mixing factor - mixing_factor[:, :, i] += area_overlap * model_manager.turbulence_model.function( - axial_induction_i, downstream_distance_D[:, :, i] - ) + mixing_factor[:,:,i] += \ + area_overlap * model_manager.turbulence_model.function( + axial_induction_i, downstream_distance_D[:,:,i] + ) if model_manager.enable_yaw_added_recovery: - mixing_factor[:, :, i] += area_overlap * yaw_added_wake_mixing( + mixing_factor[:,:,i] += \ + area_overlap * yaw_added_wake_mixing( axial_induction_i, yaw_angle_i, - downstream_distance_D[:, :, i], - model_manager.deflection_model.yaw_added_mixing_gain, + downstream_distance_D[:,:,i], + model_manager.deflection_model.yaw_added_mixing_gain ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field @@ -1268,8 +1305,9 @@ def full_flow_empirical_gauss_solver( farm: Farm, flow_field: FlowField, flow_field_grid: FlowFieldGrid, - model_manager: WakeModelManager, + model_manager: WakeModelManager ) -> None: + # Get the flow quantities and turbine performance turbine_grid_farm = copy.deepcopy(farm) turbine_grid_flow_field = copy.deepcopy(flow_field) @@ -1294,12 +1332,16 @@ def full_flow_empirical_gauss_solver( time_series=turbine_grid_flow_field.time_series, ) turbine_grid_farm.expand_farm_properties( - turbine_grid_flow_field.n_findex, turbine_grid.sorted_coord_indices + turbine_grid_flow_field.n_findex, + turbine_grid.sorted_coord_indices ) turbine_grid_flow_field.initialize_velocity_field(turbine_grid) turbine_grid_farm.initialize(turbine_grid.sorted_indices) wim_field = empirical_gauss_solver( - turbine_grid_farm, turbine_grid_flow_field, turbine_grid, model_manager + turbine_grid_farm, + turbine_grid_flow_field, + turbine_grid, + model_manager ) ### Referring to the quantities from above, calculate the wake in the full grid @@ -1316,16 +1358,17 @@ def full_flow_empirical_gauss_solver( # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(flow_field_grid.n_turbines): + # Get the current turbine quantities - x_i = np.mean(turbine_grid.x_sorted[:, i : i + 1], axis=(2, 3)) + x_i = np.mean(turbine_grid.x_sorted[:, i:i+1], axis=(2,3)) x_i = x_i[:, :, None, None] - y_i = np.mean(turbine_grid.y_sorted[:, i : i + 1], axis=(2, 3)) + y_i = np.mean(turbine_grid.y_sorted[:, i:i+1], axis=(2,3)) y_i = y_i[:, :, None, None] - z_i = np.mean(turbine_grid.z_sorted[:, i : i + 1], axis=(2, 3)) + z_i = np.mean(turbine_grid.z_sorted[:, i:i+1], axis=(2,3)) z_i = z_i[:, :, None, None] - turbine_grid_flow_field.u_sorted[:, i : i + 1] - turbine_grid_flow_field.v_sorted[:, i : i + 1] + turbine_grid_flow_field.u_sorted[:, i:i+1] + turbine_grid_flow_field.v_sorted[:, i:i+1] ct_i = thrust_coefficient( velocities=turbine_grid_flow_field.u_sorted, @@ -1355,26 +1398,28 @@ def full_flow_empirical_gauss_solver( # Since we are filtering for the i'th turbine in the axial induction function, # get the first index here (0:1) axial_induction_i = axial_induction_i[:, 0:1, None, None] - yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i : i + 1, None, None] - hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i : i + 1, None, None] - rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i : i + 1, None, None] - wake_induced_mixing_i = wim_field[:, i : i + 1, :, None].sum(axis=2, keepdims=1) + yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i:i+1, None, None] + hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i:i+1, None, None] + rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i:i+1, None, None] + wake_induced_mixing_i = wim_field[:, i:i+1, :, None].sum(axis=2, keepdims=1) effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i average_velocities = average_velocity( turbine_grid_flow_field.u_sorted, method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights, + cubature_weights=turbine_grid.cubature_weights ) tilt_angle_i = turbine_grid_farm.calculate_tilt_for_eff_velocities(average_velocities) - tilt_angle_i = tilt_angle_i[:, i : i + 1, None, None] + tilt_angle_i = tilt_angle_i[:, i:i+1, None, None] if model_manager.enable_secondary_steering: - raise NotImplementedError("Secondary steering not available for this model.") + raise NotImplementedError( + "Secondary steering not available for this model.") if model_manager.enable_transverse_velocities: - raise NotImplementedError("Transverse velocities not used in this model.") + raise NotImplementedError( + "Transverse velocities not used in this model.") # Model calculations # NOTE: exponential @@ -1386,7 +1431,7 @@ def full_flow_empirical_gauss_solver( wake_induced_mixing_i, ct_i, rotor_diameter_i, - **deflection_model_args, + **deflection_model_args ) # NOTE: exponential @@ -1403,11 +1448,12 @@ def full_flow_empirical_gauss_solver( ct_i, hub_height_i, rotor_diameter_i, - **deficit_model_args, + **deficit_model_args ) wake_field = model_manager.combination_model.function( - wake_field, velocity_deficit * flow_field.u_initial_sorted + wake_field, + velocity_deficit * flow_field.u_initial_sorted ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field From 557565b7c6cf6a5b19649c0b641fa6f1f3ce8cf6 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 23 Jan 2024 19:30:35 -0800 Subject: [PATCH 068/101] Remove pure format changes --- .../legacy/scipy/yaw_wind_rose.py | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/floris/tools/optimization/legacy/scipy/yaw_wind_rose.py b/floris/tools/optimization/legacy/scipy/yaw_wind_rose.py index 4341ed2a7..fd4d9ef63 100644 --- a/floris/tools/optimization/legacy/scipy/yaw_wind_rose.py +++ b/floris/tools/optimization/legacy/scipy/yaw_wind_rose.py @@ -303,7 +303,11 @@ def _get_power_for_yaw_angle_opt(self, yaw_angles_subset_norm): unc_options=self.unc_options, ) - return -1.0 * np.dot(self.turbine_weights, turbine_powers) / self.initial_farm_power + return ( + -1.0 + * np.dot(self.turbine_weights, turbine_powers) + / self.initial_farm_power + ) def _set_opt_bounds(self, minimum_yaw_angle, maximum_yaw_angle): """ @@ -340,7 +344,7 @@ def _optimize(self): self.fi.reinitialize_flow_field( wind_speed=wind_map.input_speed, wind_direction=wind_map.input_direction, - turbulence_intensities=wind_map.input_ti, + turbulence_intensity=wind_map.input_ti, ) return opt_yaw_angles @@ -365,7 +369,9 @@ def _reduce_control_variables(self): fi=self.fi, wind_direction=self.fi.floris.farm.wind_direction[0] ) downstream_turbines = np.array(downstream_turbines, dtype=int) - self.turbs_to_opt = [i for i in self.turbs_to_opt if i not in downstream_turbines] + self.turbs_to_opt = [ + i for i in self.turbs_to_opt if i not in downstream_turbines + ] # Set up a template yaw angles array with default solutions. The default # solutions are either 0.0 or the allowable yaw angle closest to 0.0 deg. @@ -375,7 +381,9 @@ def _reduce_control_variables(self): yaw_angles_template = np.zeros(self.nturbs, dtype=float) for ti in range(self.nturbs): if (self.bnds[ti][0] > 0.0) | (self.bnds[ti][1] < 0.0): - yaw_angles_template[ti] = self.bnds[ti][np.argmin(np.abs(self.bnds[ti]))] + yaw_angles_template[ti] = self.bnds[ti][ + np.argmin(np.abs(self.bnds[ti])) + ] self.yaw_angles_template = yaw_angles_template # Derive normalized initial condition and bounds @@ -385,8 +393,12 @@ def _reduce_control_variables(self): ) self.bnds_norm = [ ( - self._norm(self.bnds[i][0], self.minimum_yaw_angle, self.maximum_yaw_angle), - self._norm(self.bnds[i][1], self.minimum_yaw_angle, self.maximum_yaw_angle), + self._norm( + self.bnds[i][0], self.minimum_yaw_angle, self.maximum_yaw_angle + ), + self._norm( + self.bnds[i][1], self.minimum_yaw_angle, self.maximum_yaw_angle + ), ) for i in self.turbs_to_opt ] @@ -560,14 +572,17 @@ def reinitialize_opt_wind_rose( self.yaw_angles_baseline = yaw_angles_baseline else: self.yaw_angles_baseline = [ - turbine.yaw_angle for turbine in self.fi.floris.farm.turbine_map.turbines + turbine.yaw_angle + for turbine in self.fi.floris.farm.turbine_map.turbines ] if any(np.abs(self.yaw_angles_baseline) > 0.0): print( "INFO: Baseline yaw angles were not specified and were derived " "from the floris object." ) - print("INFO: The inherent yaw angles in the floris object are not all 0.0 degrees.") + print( + "INFO: The inherent yaw angles in the floris object are not all 0.0 degrees." + ) self.bnds = bnds if bnds is not None: @@ -584,9 +599,13 @@ def reinitialize_opt_wind_rose( if (self.bnds[ti][0] > 0.0) | (self.bnds[ti][1] < 0.0): self.x0[ti] = np.mean(self.bnds[ti]) - if any(np.array(self.yaw_angles_baseline) < np.array([b[0] for b in self.bnds])): + if any( + np.array(self.yaw_angles_baseline) < np.array([b[0] for b in self.bnds]) + ): print("INFO: yaw_angles_baseline exceed lower bound constraints.") - if any(np.array(self.yaw_angles_baseline) > np.array([b[1] for b in self.bnds])): + if any( + np.array(self.yaw_angles_baseline) > np.array([b[1] for b in self.bnds]) + ): print("INFO: yaw_angles_baseline in FLORIS exceed upper bound constraints.") if any(np.array(self.x0) < np.array([b[0] for b in self.bnds])): raise ValueError("Initial guess x0 exceeds lower bound constraints.") @@ -755,7 +774,9 @@ def calc_baseline_power(self): ) # calculate baseline power - self.fi.calculate_wake(yaw_angles=self.yaw_angles_baseline, no_wake=False) + self.fi.calculate_wake( + yaw_angles=self.yaw_angles_baseline, no_wake=False + ) power_base = self.fi.get_turbine_power( include_unc=self.include_unc, unc_pmfs=self.unc_pmfs, @@ -763,7 +784,9 @@ def calc_baseline_power(self): ) # calculate power for no wake case - self.fi.calculate_wake(yaw_angles=self.yaw_angles_baseline, no_wake=True) + self.fi.calculate_wake( + yaw_angles=self.yaw_angles_baseline, no_wake=True + ) power_no_wake = self.fi.get_turbine_power( include_unc=self.include_unc, unc_pmfs=self.unc_pmfs, From 853537cbfbcd613cd11a6c6786856ea82d80bb95 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 23 Jan 2024 19:33:13 -0800 Subject: [PATCH 069/101] Remove pure format changes --- .../legacy/scipy/yaw_wind_rose_clustered.py | 13 +++++--- .../legacy/scipy/yaw_wind_rose_parallel.py | 16 ++++++++-- .../scipy/yaw_wind_rose_parallel_clustered.py | 30 +++++++++++++++---- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_clustered.py b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_clustered.py index b72851b47..9ab247483 100644 --- a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_clustered.py +++ b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_clustered.py @@ -223,14 +223,15 @@ def __init__( ) self.clustering_wake_slope = clustering_wake_slope + def _cluster_turbines(self): wind_directions = self.fi.floris.farm.wind_direction - if np.std(wind_directions) > 0.001: + if (np.std(wind_directions) > 0.001): raise ValueError("Wind directions must be uniform for clustering algorithm.") self.clusters = cluster_turbines( fi=self.fi, wind_direction=self.fi.floris.farm.wind_direction[0], - wake_slope=self.clustering_wake_slope, + wake_slope=self.clustering_wake_slope ) def plot_clusters(self): @@ -239,9 +240,10 @@ def plot_clusters(self): fi=self.fi, wind_direction=wd, wake_slope=self.clustering_wake_slope, - plot_lines=True, + plot_lines=True ) + def optimize(self): """ This method solves for the optimum turbine yaw angles for power @@ -366,7 +368,10 @@ def optimize(self): self.x0 = x0_full self.fi = fi_full self.fi.reinitialize_flow_field( - layout_array=[np.array(fi_full.layout_x), np.array(fi_full.layout_y)] + layout_array=[ + np.array(fi_full.layout_x), + np.array(fi_full.layout_y) + ] ) if np.sum(np.abs(opt_yaw_angles)) == 0: diff --git a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel.py b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel.py index 450e19e58..1e0dd9e16 100644 --- a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel.py +++ b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel.py @@ -240,7 +240,11 @@ def _calc_baseline_power_one_case(self, ws, wd, ti=None): """ if ti is None: print( - "Computing wind speed = " + str(ws) + " m/s, wind direction = " + str(wd) + " deg." + "Computing wind speed = " + + str(ws) + + " m/s, wind direction = " + + str(wd) + + " deg." ) else: print( @@ -344,7 +348,11 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): """ if ti is None: print( - "Computing wind speed = " + str(ws) + " m/s, wind direction = " + str(wd) + " deg." + "Computing wind speed = " + + str(ws) + + " m/s, wind direction = " + + str(wd) + + " deg." ) else: print( @@ -492,6 +500,7 @@ def calc_baseline_power(self): for df_base_one in executor.map( self._calc_baseline_power_one_case, self.ws.values, self.wd.values ): + # add variables to dataframe df_base = df_base.append(df_base_one) else: @@ -501,6 +510,7 @@ def calc_baseline_power(self): self.wd.values, self.ti.values, ): + # add variables to dataframe df_base = df_base.append(df_base_one) @@ -565,6 +575,7 @@ def optimize(self): self.wd.values, self.df_base.power_baseline.values, ): + # add variables to dataframe df_opt = df_opt.append(df_opt_one) else: @@ -575,6 +586,7 @@ def optimize(self): self.df_base.power_baseline.values, self.ti.values, ): + # add variables to dataframe df_opt = df_opt.append(df_opt_one) diff --git a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel_clustered.py b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel_clustered.py index e748daba4..dad1d9cfa 100644 --- a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel_clustered.py +++ b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel_clustered.py @@ -60,7 +60,7 @@ def __init__( unc_options=None, turbine_weights=None, exclude_downstream_turbines=False, - clustering_wake_slope=0.30, + clustering_wake_slope=0.30 ): """ Instantiate YawOptimizationWindRoseParallel object with a @@ -217,7 +217,7 @@ def __init__( turbine_weights=turbine_weights, calc_init_power=False, exclude_downstream_turbines=exclude_downstream_turbines, - clustering_wake_slope=clustering_wake_slope, + clustering_wake_slope=clustering_wake_slope ) self.clustering_wake_slope = clustering_wake_slope @@ -258,7 +258,11 @@ def _calc_baseline_power_one_case(self, ws, wd, ti=None): """ if ti is None: print( - "Computing wind speed = " + str(ws) + " m/s, wind direction = " + str(wd) + " deg." + "Computing wind speed = " + + str(ws) + + " m/s, wind direction = " + + str(wd) + + " deg." ) else: print( @@ -362,7 +366,11 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): """ if ti is None: print( - "Computing wind speed = " + str(ws) + " m/s, wind direction = " + str(wd) + " deg." + "Computing wind speed = " + + str(ws) + + " m/s, wind direction = " + + str(wd) + + " deg." ) else: print( @@ -412,7 +420,10 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): self.x0 = np.array(x0_full)[cl] self.fi = copy.deepcopy(fi_full) self.fi.reinitialize_flow_field( - layout_array=[np.array(fi_full.layout_x)[cl], np.array(fi_full.layout_y)[cl]] + layout_array=[ + np.array(fi_full.layout_x)[cl], + np.array(fi_full.layout_y)[cl] + ] ) opt_yaw_angles[cl] = self._optimize() @@ -424,7 +435,10 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): self.x0 = x0_full self.fi = fi_full self.fi.reinitialize_flow_field( - layout_array=[np.array(fi_full.layout_x), np.array(fi_full.layout_y)] + layout_array=[ + np.array(fi_full.layout_x), + np.array(fi_full.layout_y) + ] ) if np.sum(np.abs(opt_yaw_angles)) == 0: @@ -549,6 +563,7 @@ def calc_baseline_power(self): for df_base_one in executor.map( self._calc_baseline_power_one_case, self.ws.values, self.wd.values ): + # add variables to dataframe df_base = df_base.append(df_base_one) else: @@ -558,6 +573,7 @@ def calc_baseline_power(self): self.wd.values, self.ti.values, ): + # add variables to dataframe df_base = df_base.append(df_base_one) @@ -622,6 +638,7 @@ def optimize(self): self.wd.values, self.df_base.power_baseline.values, ): + # add variables to dataframe df_opt = df_opt.append(df_opt_one) else: @@ -632,6 +649,7 @@ def optimize(self): self.df_base.power_baseline.values, self.ti.values, ): + # add variables to dataframe df_opt = df_opt.append(df_opt_one) From 404084d0a6ecfc44ce3b3bedce86b5dd34ad841e Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 23 Jan 2024 19:36:42 -0800 Subject: [PATCH 070/101] dont import base --- floris/tools/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/floris/tools/__init__.py b/floris/tools/__init__.py index eb73d8fc1..5859fedc5 100644 --- a/floris/tools/__init__.py +++ b/floris/tools/__init__.py @@ -48,7 +48,6 @@ ) from .wind_data import ( TimeSeries, - WindDataBase, WindRose, ) From ec3e8f12007e603e57b9e989b7b451fdbcdcdb52 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 23 Jan 2024 19:36:49 -0800 Subject: [PATCH 071/101] Remove pure format --- .../yaw_optimization/yaw_optimization_base.py | 105 ++++++++++-------- 1 file changed, 58 insertions(+), 47 deletions(-) diff --git a/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py b/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py index 2fd417f75..c8bccea37 100644 --- a/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py +++ b/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py @@ -139,7 +139,8 @@ def __init__( "were derived from the floris object." ) print( - "INFO: The inherent yaw angles in the floris object " "are not all 0.0 degrees." + "INFO: The inherent yaw angles in the floris object " + "are not all 0.0 degrees." ) # Set optimization bounds @@ -236,8 +237,8 @@ def _unpack_variable(self, variable, subset=False): ( self.fi.floris.flow_field.n_wind_directions, self.fi.floris.flow_field.n_wind_speeds, - 1, - ), + 1 + ) ) if len(np.shape(variable)) == 2: @@ -259,7 +260,7 @@ def _reduce_control_problem(self): exploit_layout_symmetry == True. """ # Initialize which turbines to optimize for - self.turbs_to_opt = self.maximum_yaw_angle - self.minimum_yaw_angle >= 0.001 + self.turbs_to_opt = (self.maximum_yaw_angle - self.minimum_yaw_angle >= 0.001) # Initialize subset variables as full set self.fi_subset = self.fi.copy() @@ -310,9 +311,9 @@ def _reduce_control_problem(self): combined_bounds = np.concatenate( ( np.expand_dims(minimum_yaw_angle_subset, axis=3), - np.expand_dims(maximum_yaw_angle_subset, axis=3), + np.expand_dims(maximum_yaw_angle_subset, axis=3) ), - axis=3, + axis=3 ) # Overwrite all values that are not allowed to be 0.0 with bound value closest to zero ids_closest = np.expand_dims(np.argmin(np.abs(combined_bounds), axis=3), axis=3) @@ -338,22 +339,20 @@ def _normalize_control_problem(self): """ lb = np.min(self._minimum_yaw_angle_subset) ub = np.max(self._maximum_yaw_angle_subset) - self._normalization_length = ub - lb + self._normalization_length = (ub - lb) self._x0_subset_norm = self._x0_subset / self._normalization_length self._minimum_yaw_angle_subset_norm = ( - self._minimum_yaw_angle_subset / self._normalization_length + self._minimum_yaw_angle_subset + / self._normalization_length ) self._maximum_yaw_angle_subset_norm = ( - self._maximum_yaw_angle_subset / self._normalization_length + self._maximum_yaw_angle_subset + / self._normalization_length ) - def _calculate_farm_power( - self, - yaw_angles=None, - wd_array=None, - turbine_weights=None, - heterogeneous_speed_multipliers=None, - ): + def _calculate_farm_power(self, yaw_angles=None, wd_array=None, turbine_weights=None, + heterogeneous_speed_multipliers=None + ): """ Calculate the wind farm power production assuming the predefined probability distribution (self.unc_options/unc_pmf), with the @@ -374,9 +373,8 @@ def _calculate_farm_power( if turbine_weights is None: turbine_weights = self._turbine_weights_subset if heterogeneous_speed_multipliers is not None: - fi_subset.floris.flow_field.heterogenous_inflow_config[ - "speed_multipliers" - ] = heterogeneous_speed_multipliers + fi_subset.floris.flow_field.\ + heterogenous_inflow_config['speed_multipliers'] = heterogeneous_speed_multipliers # Ensure format [incompatible with _subset notation] yaw_angles = self._unpack_variable(yaw_angles, subset=True) @@ -453,11 +451,9 @@ def _derive_layout_symmetry(self): wd_array = self.fi.floris.flow_field.wind_directions sym_step = df.iloc[0]["wd_range"][1] - if (0.0 not in wd_array) or (sym_step not in wd_array): - print( - "Floris wind direction array does not " - + "intersect {:.1f} and {:.1f}.".format(0.0, sym_step) - ) + if ((0.0 not in wd_array) or(sym_step not in wd_array)): + print("Floris wind direction array does not " + + "intersect {:.1f} and {:.1f}.".format(0.0, sym_step)) print("Exploitation of symmetry has been disabled.") return @@ -470,9 +466,8 @@ def _derive_layout_symmetry(self): print("Exploitation of symmetry has been disabled.") self._sym_mapping_extrap = np.array( - [np.where(np.abs(x - wd_array_min) < 0.0001)[0][0] for x in wd_array_remn], - dtype=int, - ) + [np.where(np.abs(x - wd_array_min) < 0.0001)[0][0] + for x in wd_array_remn], dtype=int) self._sym_mapping_reduce = copy.deepcopy(ids_minimal) self._sym_df = df @@ -503,7 +498,10 @@ def _unreduce_variable(self, variable): # Now process turbine mapping wd_array = self.fi.floris.flow_field.wind_directions for ii, dfrow in self._sym_df.iloc[1::].iterrows(): - ids = (wd_array >= dfrow["wd_range"][0]) & (wd_array < dfrow["wd_range"][1]) + ids = ( + (wd_array >= dfrow["wd_range"][0]) & + (wd_array < dfrow["wd_range"][1]) + ) tmap = np.argsort(dfrow["turbine_mapping"]) full_array[ids, :, :] = full_array[ids, :, :][:, :, tmap] else: @@ -520,8 +518,11 @@ def _finalize(self, farm_power_opt_subset=None, yaw_angles_opt_subset=None): # Now verify solutions for convergence, if necessary if self.verify_convergence: - yaw_angles_opt_subset, farm_power_opt_subset = self._verify_solutions_for_convergence( - farm_power_opt_subset, yaw_angles_opt_subset + yaw_angles_opt_subset, farm_power_opt_subset = ( + self._verify_solutions_for_convergence( + farm_power_opt_subset, + yaw_angles_opt_subset + ) ) # Finalization step for optimization: undo reduction step @@ -645,12 +646,13 @@ def _verify_solutions_for_convergence( farm_power = self._calculate_farm_power( yaw_angles=yaw_angles_verify, wd_array=np.tile(wd_array_nominal, n_turbs), - turbine_weights=np.tile(self._turbs_to_opt_subset, sp), + turbine_weights=np.tile(self._turbs_to_opt_subset, sp) ) # Calculate power uplift for optimal solutions uplift_o = 100 * ( - np.tile(farm_power_opt_subset, (n_turbs, 1)) / farm_power_baseline_verify - 1.0 + np.tile(farm_power_opt_subset, (n_turbs, 1)) / + farm_power_baseline_verify - 1.0 ) # Calculate power uplift for all cases we evaluated @@ -668,19 +670,29 @@ def _verify_solutions_for_convergence( ) # Overwrite yaw angles that insufficiently increased farm power with baseline values - yaw_angles_opt_subset[ids_to_simplify] = yaw_angles_baseline_subset[ids_to_simplify] + yaw_angles_opt_subset[ids_to_simplify] = ( + yaw_angles_baseline_subset[ids_to_simplify] + ) n = len(ids_to_simplify[0]) if n > 0: # Yaw angles notably changed: recalculate farm powers - farm_power_opt_subset_new = self._calculate_farm_power(yaw_angles_opt_subset) + farm_power_opt_subset_new = ( + self._calculate_farm_power(yaw_angles_opt_subset) + ) if verbose: # Calculate old uplift for all conditions - dP_old = 100.0 * (farm_power_opt_subset / farm_power_baseline_subset) - 100.0 + dP_old = 100.0 * ( + farm_power_opt_subset / + farm_power_baseline_subset + ) - 100.0 # Calculate new uplift for all conditions - dP_new = 100.0 * (farm_power_opt_subset_new / farm_power_baseline_subset) - 100.0 + dP_new = 100.0 * ( + farm_power_opt_subset_new / + farm_power_baseline_subset + ) - 100.0 # Calculate differences in power uplift diff_uplift = dP_old - dP_new @@ -688,23 +700,22 @@ def _verify_solutions_for_convergence( jj = (ids_max_loss[0][0], ids_max_loss[1][0]) ws_array_nominal = self.fi_subset.floris.flow_field.wind_speeds print( - "Nullified the optimal yaw offset for {:d}".format(n) - + " conditions and turbines." - ) + "Nullified the optimal yaw offset for {:d}".format(n) + + " conditions and turbines." + ) print( - "Simplifying the yaw angles for these conditions lead " - + "to a maximum change in wake-steering power uplift from " - + "{:.5f}% to {:.5f}% at ".format(dP_old[jj], dP_new[jj]) - + " WD = {:.1f} deg and WS = {:.1f} m/s.".format( - wd_array_nominal[jj[0]], - ws_array_nominal[jj[1]], + "Simplifying the yaw angles for these conditions lead " + + "to a maximum change in wake-steering power uplift from " + + "{:.5f}% to {:.5f}% at ".format(dP_old[jj], dP_new[jj]) + + " WD = {:.1f} deg and WS = {:.1f} m/s.".format( + wd_array_nominal[jj[0]], ws_array_nominal[jj[1]], ) ) t = timerpc() - start_time print( - "Time spent to verify the convergence of the optimal " - + "yaw angles: {:.3f} s.".format(t) + "Time spent to verify the convergence of the optimal " + + "yaw angles: {:.3f} s.".format(t) ) # Return optimal solutions to the user From e95b6e517e6242ad03138b0fc74bcf09cb13ce87 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 23 Jan 2024 19:39:29 -0800 Subject: [PATCH 072/101] Remove pure formatting changes --- floris/tools/floris_interface.py | 65 +++++++++++++++++--------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index a9df9937f..3ddbe6a50 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -77,9 +77,10 @@ def __init__(self, configuration: dict | str | Path): # Make a check on reference height and provide a helpful warning unique_heights = np.unique(np.round(self.floris.farm.hub_heights, decimals=6)) - if (len(unique_heights) == 1) and ( - np.abs(self.floris.flow_field.reference_wind_height - unique_heights[0]) > 1.0e-6 - ): + if (( + len(unique_heights) == 1) and + (np.abs(self.floris.flow_field.reference_wind_height - unique_heights[0]) > 1.0e-6 + )): err_msg = ( "The only unique hub-height is not the equal to the specified reference " "wind height. If this was unintended use -1 as the reference hub height to " @@ -99,6 +100,7 @@ def __init__(self, configuration: dict | str | Path): raise ValueError("turbine_grid_points must be less than or equal to 3.") def assign_hub_height_to_ref_height(self): + # Confirm can do this operation unique_heights = np.unique(self.floris.farm.hub_heights) if len(unique_heights) > 1: @@ -132,7 +134,7 @@ def calculate_wake( yaw_angles = np.zeros( ( self.floris.flow_field.n_findex, - self.floris.farm.n_turbines, + self.floris.farm.n_turbines ) ) self.floris.farm.yaw_angles = yaw_angles @@ -171,7 +173,7 @@ def calculate_no_wake( yaw_angles = np.zeros( ( self.floris.flow_field.n_findex, - self.floris.farm.n_turbines, + self.floris.farm.n_turbines ) ) self.floris.farm.yaw_angles = yaw_angles @@ -309,7 +311,7 @@ def get_plane_of_points( :py:class:`pandas.DataFrame`: containing values of x1, x2, x3, u, v, w """ # Get results vectors - if normal_vector == "z": + if (normal_vector == "z"): x_flat = self.floris.grid.x_sorted_inertial_frame[0].flatten() y_flat = self.floris.grid.y_sorted_inertial_frame[0].flatten() z_flat = self.floris.grid.z_sorted_inertial_frame[0].flatten() @@ -442,7 +444,7 @@ def calculate_horizontal_plane( df, self.floris.grid.grid_resolution[0], self.floris.grid.grid_resolution[1], - "z", + "z" ) # Reset the fi object back to the turbine grid configuration @@ -637,7 +639,7 @@ def get_turbine_powers(self) -> NDArrayFloat: ) # Check for negative velocities, which could indicate bad model # parameters or turbines very closely spaced. - if (self.floris.flow_field.u < 0.0).any(): + if (self.floris.flow_field.u < 0.).any(): self.logger.warning("Some velocities at the rotor are negative.") turbine_powers = power( @@ -650,7 +652,7 @@ def get_turbine_powers(self) -> NDArrayFloat: turbine_type_map=self.floris.farm.turbine_type_map, turbine_power_thrust_tables=self.floris.farm.turbine_power_thrust_tables, correct_cp_ct_for_tilt=self.floris.farm.correct_cp_ct_for_tilt, - multidim_condition=self.floris.flow_field.multidim_conditions, + multidim_condition=self.floris.flow_field.multidim_conditions ) return turbine_powers @@ -666,7 +668,7 @@ def get_turbine_thrust_coefficients(self) -> NDArrayFloat: turbine_power_thrust_tables=self.floris.farm.turbine_power_thrust_tables, average_method=self.floris.grid.average_method, cubature_weights=self.floris.grid.cubature_weights, - multidim_condition=self.floris.flow_field.multidim_conditions, + multidim_condition=self.floris.flow_field.multidim_conditions ) return turbine_thrust_coefficients @@ -691,7 +693,7 @@ def turbine_average_velocities(self) -> NDArrayFloat: return average_velocity( velocities=self.floris.flow_field.u, method=self.floris.grid.average_method, - cubature_weights=self.floris.grid.cubature_weights, + cubature_weights=self.floris.grid.cubature_weights ) def get_turbine_TIs(self) -> NDArrayFloat: @@ -749,14 +751,17 @@ def get_farm_power( turbine_weights = np.ones( ( self.floris.flow_field.n_findex, - self.floris.farm.n_turbines, + self.floris.farm.n_turbines ) ) elif len(np.shape(turbine_weights)) == 1: # Deal with situation when 1D array is provided turbine_weights = np.tile( turbine_weights, - (self.floris.flow_field.n_findex, 1), + ( + self.floris.flow_field.n_findex, + 1 + ) ) # Calculate all turbine powers and apply weights @@ -815,7 +820,6 @@ def get_farm_AEP( the flow field. This can be useful when quantifying the loss in AEP due to wakes. Defaults to *False*. - Returns: float: The Annual Energy Production (AEP) for the wind farm in @@ -832,7 +836,8 @@ def get_farm_AEP( # Check if frequency vector sums to 1.0. If not, raise a warning if np.abs(np.sum(freq) - 1.0) > 0.001: self.logger.warning( - "WARNING: The frequency array provided to get_farm_AEP() does not sum to 1.0." + "WARNING: The frequency array provided to get_farm_AEP() " + "does not sum to 1.0." ) # Copy the full wind speed array from the floris object and initialize @@ -855,14 +860,14 @@ def get_farm_AEP( yaw_angles_subset = yaw_angles[conditions_to_evaluate] self.reinitialize( wind_speeds=wind_speeds_subset, - wind_directions=wind_directions_subset, + wind_directions=wind_directions_subset ) if no_wake: self.calculate_no_wake(yaw_angles=yaw_angles_subset) else: self.calculate_wake(yaw_angles=yaw_angles_subset) - farm_power[conditions_to_evaluate] = self.get_farm_power( - turbine_weights=turbine_weights + farm_power[conditions_to_evaluate] = ( + self.get_farm_power(turbine_weights=turbine_weights) ) # Finally, calculate AEP in GWh @@ -964,17 +969,17 @@ def sample_flow_at_points(self, x: NDArrayFloat, y: NDArrayFloat, z: NDArrayFloa return self.floris.solve_for_points(x, y, z) def sample_velocity_deficit_profiles( - self, - direction: str = "cross-stream", - downstream_dists: NDArrayFloat | list = None, - profile_range: NDArrayFloat | list = None, - resolution: int = 100, - wind_direction: float = None, - homogeneous_wind_speed: float = None, - ref_rotor_diameter: float = None, - x_start: float = 0.0, - y_start: float = 0.0, - reference_height: float = None, + self, + direction: str = 'cross-stream', + downstream_dists: NDArrayFloat | list = None, + profile_range: NDArrayFloat | list = None, + resolution: int = 100, + wind_direction: float = None, + homogeneous_wind_speed: float = None, + ref_rotor_diameter: float = None, + x_start: float = 0.0, + y_start: float = 0.0, + reference_height: float = None, ) -> list[pd.DataFrame]: """ Extract velocity deficit profiles at a set of downstream distances from a starting point @@ -1008,7 +1013,7 @@ def sample_velocity_deficit_profiles( profile. """ - if direction not in ["cross-stream", "vertical"]: + if direction not in ['cross-stream', 'vertical']: raise ValueError("`direction` must be either `cross-stream` or `vertical`.") if ref_rotor_diameter is None: From 77328dafdbc69d05ed5c441f227b10ed826b6f16 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 23 Jan 2024 19:43:45 -0800 Subject: [PATCH 073/101] Remove pure formatting changes --- .../tools/floris_interface_legacy_reader.py | 33 +++++--- floris/tools/parallel_computing_interface.py | 76 ++++++++++--------- floris/tools/uncertainty_interface.py | 46 ++++++----- 3 files changed, 90 insertions(+), 65 deletions(-) diff --git a/floris/tools/floris_interface_legacy_reader.py b/floris/tools/floris_interface_legacy_reader.py index 644f33597..a12699adb 100644 --- a/floris/tools/floris_interface_legacy_reader.py +++ b/floris/tools/floris_interface_legacy_reader.py @@ -64,6 +64,7 @@ class FlorisInterfaceLegacyV2(FlorisInterface): """ def __init__(self, configuration: dict | str | Path, het_map=None): + if not isinstance(configuration, (str, Path, dict)): raise TypeError("The Floris `configuration` must of type 'dict', 'str', or 'Path'.") @@ -112,7 +113,7 @@ def _convert_v24_dictionary_to_v3(dict_legacy): dict_floris["farm"] = { "layout_x": fp["layout_x"], "layout_y": fp["layout_y"], - "turbine_type": ["nrel_5MW"], # Placeholder + "turbine_type": ["nrel_5MW"] # Placeholder } ref_height = fp["specified_wind_height"] @@ -167,9 +168,15 @@ def _convert_v24_dictionary_to_v3(dict_legacy): turbulence_subdict = copy.deepcopy(wtp) # Save parameter settings to wake dictionary - dict_floris["wake"]["wake_velocity_parameters"] = {velocity_model_str: velocity_subdict} - dict_floris["wake"]["wake_deflection_parameters"] = {deflection_model: deflection_subdict} - dict_floris["wake"]["wake_turbulence_parameters"] = {turbulence_model: turbulence_subdict} + dict_floris["wake"]["wake_velocity_parameters"] = { + velocity_model_str: velocity_subdict + } + dict_floris["wake"]["wake_deflection_parameters"] = { + deflection_model: deflection_subdict + } + dict_floris["wake"]["wake_turbulence_parameters"] = { + turbulence_model: turbulence_subdict + } # Finally add turbine information dict_turbine = { @@ -181,7 +188,7 @@ def _convert_v24_dictionary_to_v3(dict_legacy): "rotor_diameter": tp["rotor_diameter"], "TSR": tp["TSR"], "power_thrust_table": tp["power_thrust_table"], - "ref_air_density": 1.225, # This was implicit in the former input file + "ref_air_density": 1.225 # This was implicit in the former input file } return dict_floris, dict_turbine @@ -201,12 +208,16 @@ def _convert_v24_dictionary_to_v3(dict_legacy): The file format is changed from JSON to YAML and all inputs are mapped, as needed." parser = argparse.ArgumentParser(description=description) - parser.add_argument( - "-i", "--input-file", nargs=1, required=True, help="Path to the legacy input file" - ) - parser.add_argument( - "-o", "--output-file", nargs="?", default=None, help="Path to write the output file" - ) + parser.add_argument("-i", + "--input-file", + nargs=1, + required=True, + help="Path to the legacy input file") + parser.add_argument("-o", + "--output-file", + nargs="?", + default=None, + help="Path to write the output file") args = parser.parse_args() # Specify paths diff --git a/floris/tools/parallel_computing_interface.py b/floris/tools/parallel_computing_interface.py index f17093395..235cedb97 100644 --- a/floris/tools/parallel_computing_interface.py +++ b/floris/tools/parallel_computing_interface.py @@ -11,7 +11,11 @@ from floris.tools.uncertainty_interface import FlorisInterface, UncertaintyInterface -def _load_local_floris_object(fi_dict, unc_pmfs=None, fix_yaw_in_relative_frame=False): +def _load_local_floris_object( + fi_dict, + unc_pmfs=None, + fix_yaw_in_relative_frame=False +): # Load local FLORIS object if unc_pmfs is None: fi = FlorisInterface(fi_dict) @@ -72,7 +76,7 @@ def __init__( interface="multiprocessing", # Options are 'multiprocessing', 'mpi4py' or 'concurrent' use_mpi4py=None, propagate_flowfield_from_workers=False, - print_timings=False, + print_timings=False ): """A wrapper around the nominal floris_interface class that adds parallel computing to common FlorisInterface properties. @@ -112,17 +116,14 @@ def __init__( if interface == "mpi4py": import mpi4py.futures as mp - self._PoolExecutor = mp.MPIPoolExecutor elif interface == "multiprocessing": import multiprocessing as mp - self._PoolExecutor = mp.Pool if max_workers is None: max_workers = mp.cpu_count() elif interface == "concurrent": from concurrent.futures import ProcessPoolExecutor - self._PoolExecutor = ProcessPoolExecutor else: raise UserWarning( @@ -214,13 +215,11 @@ def reinitialize( def _preprocessing(self, yaw_angles=None): # Format yaw angles if yaw_angles is None: - yaw_angles = np.zeros( - ( - self.fi.floris.flow_field.n_wind_directions, - self.fi.floris.flow_field.n_wind_speeds, - self.fi.floris.farm.n_turbines, - ) - ) + yaw_angles = np.zeros(( + self.fi.floris.flow_field.n_wind_directions, + self.fi.floris.flow_field.n_wind_speeds, + self.fi.floris.farm.n_turbines + )) # Prepare settings n_wind_direction_splits = self.n_wind_direction_splits @@ -233,10 +232,12 @@ def _preprocessing(self, yaw_angles=None): # Prepare the input arguments for parallel execution fi_dict = self.fi.floris.as_dict() wind_direction_id_splits = np.array_split( - np.arange(self.fi.floris.flow_field.n_wind_directions), n_wind_direction_splits + np.arange(self.fi.floris.flow_field.n_wind_directions), + n_wind_direction_splits ) wind_speed_id_splits = np.array_split( - np.arange(self.fi.floris.flow_field.n_wind_speeds), n_wind_speed_splits + np.arange(self.fi.floris.flow_field.n_wind_speeds), + n_wind_speed_splits ) multiargs = [] for wd_id_split in wind_direction_id_splits: @@ -244,7 +245,7 @@ def _preprocessing(self, yaw_angles=None): fi_dict_split = copy.deepcopy(fi_dict) wind_directions = self.fi.floris.flow_field.wind_directions[wd_id_split] wind_speeds = self.fi.floris.flow_field.wind_speeds[ws_id_split] - yaw_angles_subset = yaw_angles[wd_id_split[0] : wd_id_split[-1] + 1, ws_id_split, :] + yaw_angles_subset = yaw_angles[wd_id_split[0]:wd_id_split[-1]+1, ws_id_split, :] fi_dict_split["flow_field"]["wind_directions"] = wind_directions fi_dict_split["flow_field"]["wind_speeds"] = wind_speeds @@ -256,7 +257,7 @@ def _preprocessing(self, yaw_angles=None): fi_dict_split, self.fi.fi.het_map, self.fi.unc_pmfs, - self.fi.fix_yaw_in_relative_frame, + self.fi.fix_yaw_in_relative_frame ) multiargs.append((fi_information, yaw_angles_subset)) @@ -270,15 +271,16 @@ def _merge_subsets(self, field, subset): [ eval("f.{:s}".format(field)) for f in subset[ - wii * self.n_wind_direction_splits : (wii + 1) + wii + * self.n_wind_direction_splits:(wii+1) * self.n_wind_direction_splits ] ], - axis=0, + axis=0 ) for wii in range(self.n_wind_speed_splits) ], - axis=1, + axis=1 ) def _postprocessing(self, output): @@ -290,14 +292,12 @@ def _postprocessing(self, output): turbine_powers = np.concatenate( [ np.concatenate( - power_subsets[ - self.n_wind_speed_splits * (ii) : self.n_wind_speed_splits * (ii + 1) - ], - axis=1, + power_subsets[self.n_wind_speed_splits*(ii):self.n_wind_speed_splits*(ii+1)], + axis=1 ) for ii in range(self.n_wind_direction_splits) ], - axis=0, + axis=0 ) # Optionally, also merge flow field dictionaries from individual floris solutions @@ -310,7 +310,8 @@ def _postprocessing(self, output): self.floris.flow_field.v = self._merge_subsets("v", flowfield_subsets) self.floris.flow_field.w = self._merge_subsets("w", flowfield_subsets) self.floris.flow_field.turbulence_intensity_field = self._merge_subsets( - "turbulence_intensity_field", flowfield_subsets + "turbulence_intensity_field", + flowfield_subsets ) return turbine_powers @@ -333,7 +334,9 @@ def get_turbine_powers(self, yaw_angles=None): out = p.starmap(_get_turbine_powers_serial, multiargs) else: out = p.map( - _get_turbine_powers_serial, [j[0] for j in multiargs], [j[1] for j in multiargs] + _get_turbine_powers_serial, + [j[0] for j in multiargs], + [j[1] for j in multiargs] ) # out = list(out) t_execution = timerpc() - t1 @@ -363,7 +366,7 @@ def get_farm_power(self, yaw_angles=None, turbine_weights=None): ( self.fi.floris.flow_field.n_wind_directions, self.fi.floris.flow_field.n_wind_speeds, - self.fi.floris.farm.n_turbines, + self.fi.floris.farm.n_turbines ) ) elif len(np.shape(turbine_weights)) == 1: @@ -373,8 +376,8 @@ def get_farm_power(self, yaw_angles=None, turbine_weights=None): ( self.fi.floris.flow_field.n_wind_directions, self.fi.floris.flow_field.n_wind_speeds, - 1, - ), + 1 + ) ) # Calculate all turbine powers and apply weights @@ -447,7 +450,7 @@ def get_farm_AEP( cut_out_wind_speed=cut_out_wind_speed, yaw_angles=yaw_angles, turbine_weights=turbine_weights, - no_wake=no_wake, + no_wake=no_wake ) # Verify dimensions of the variable "freq" @@ -484,8 +487,8 @@ def get_farm_AEP( if yaw_angles is not None: yaw_angles_subset = yaw_angles[:, conditions_to_evaluate] self.fi.reinitialize(wind_speeds=wind_speeds_subset) - farm_power[:, conditions_to_evaluate] = self.get_farm_power( - yaw_angles=yaw_angles_subset, turbine_weights=turbine_weights + farm_power[:, conditions_to_evaluate] = ( + self.get_farm_power(yaw_angles=yaw_angles_subset, turbine_weights=turbine_weights) ) # Finally, calculate AEP in GWh @@ -502,13 +505,14 @@ def optimize_yaw_angles( maximum_yaw_angle=25.0, yaw_angles_baseline=None, x0=None, - Ny_passes=[5, 4], + Ny_passes=[5,4], turbine_weights=None, exclude_downstream_turbines=True, exploit_layout_symmetry=True, verify_convergence=False, print_worker_progress=False, # Recommended disabled to avoid clutter. Useful for debugging ): + # Prepare the inputs to each core for multiprocessing module t0 = timerpc() multiargs = self._preprocessing() @@ -551,10 +555,8 @@ def optimize_yaw_angles( t2 = timerpc() # Combine all solutions from multiprocessing into single dataframe - df_opt = ( - pd.concat(df_opt_splits, axis=0) - .reset_index(drop=True) - .sort_values(by=["wind_direction", "wind_speed", "turbulence_intensity"]) + df_opt = pd.concat(df_opt_splits, axis=0).reset_index(drop=True).sort_values( + by=["wind_direction", "wind_speed", "turbulence_intensity"] ) t3 = timerpc() diff --git a/floris/tools/uncertainty_interface.py b/floris/tools/uncertainty_interface.py index 70ac24d56..aead4c887 100644 --- a/floris/tools/uncertainty_interface.py +++ b/floris/tools/uncertainty_interface.py @@ -183,9 +183,10 @@ def _expand_wind_directions_and_yaw_angles(self): # Expand wind direction and yaw angle array into the direction # of uncertainty over the ambient wind direction. - wd_array_probablistic = np.vstack( - [np.expand_dims(wd_array_nominal, axis=0) + dy for dy in unc_pmfs["wd_unc"]] - ) + wd_array_probablistic = np.vstack([ + np.expand_dims(wd_array_nominal, axis=0) + dy + for dy in unc_pmfs["wd_unc"] + ]) if self.fix_yaw_in_relative_frame: # The relative yaw angle is fixed and always has the nominal @@ -195,9 +196,10 @@ def _expand_wind_directions_and_yaw_angles(self): # wind directions. This can also be really fast, since it would # not require any additional calculations compared to the # non-uncertainty FLORIS evaluation. - yaw_angles_probablistic = np.vstack( - [np.expand_dims(yaw_angles_nominal, axis=0) for _ in unc_pmfs["wd_unc"]] - ) + yaw_angles_probablistic = np.vstack([ + np.expand_dims(yaw_angles_nominal, axis=0) + for _ in unc_pmfs["wd_unc"] + ]) else: # Fix yaw angles in the absolute (compass) reference frame, # meaning that for each probablistic wind direction evaluation, @@ -206,9 +208,10 @@ def _expand_wind_directions_and_yaw_angles(self): # direction 3 deg above the nominal value means that we evaluate # it with a relative yaw angle that is 3 deg below its nominal # value. - yaw_angles_probablistic = np.vstack( - [np.expand_dims(yaw_angles_nominal, axis=0) - dy for dy in unc_pmfs["wd_unc"]] - ) + yaw_angles_probablistic = np.vstack([ + np.expand_dims(yaw_angles_nominal, axis=0) - dy + for dy in unc_pmfs["wd_unc"] + ]) self.wd_array_probablistic = wd_array_probablistic self.yaw_angles_probablistic = yaw_angles_probablistic @@ -228,7 +231,10 @@ def copy(self): return fi_unc_copy def reinitialize_uncertainty( - self, unc_options=None, unc_pmfs=None, fix_yaw_in_relative_frame=None + self, + unc_options=None, + unc_pmfs=None, + fix_yaw_in_relative_frame=None ): """Reinitialize the wind direction and yaw angle probability distributions used in evaluating FLORIS. Must either specify @@ -411,7 +417,8 @@ def get_turbine_powers(self): # Format into conventional floris format by reshaping wd_array_probablistic = np.reshape(self.wd_array_probablistic, -1) yaw_angles_probablistic = np.reshape( - self.yaw_angles_probablistic, (-1, num_ws, num_turbines) + self.yaw_angles_probablistic, + (-1, num_ws, num_turbines) ) # Wrap wind direction array around 360 deg @@ -423,7 +430,7 @@ def get_turbine_powers(self): np.append(yaw_angles_probablistic, wd_exp, axis=2), axis=0, return_index=True, - return_inverse=True, + return_inverse=True ) wd_array_probablistic_min = wd_array_probablistic[id_unq] yaw_angles_probablistic_min = yaw_angles_probablistic[id_unq, :, :] @@ -442,7 +449,8 @@ def get_turbine_powers(self): # Reshape solutions back to full set power_probablistic = turbine_powers[id_unq_rev, :] power_probablistic = np.reshape( - power_probablistic, (num_wd_unc, num_wd, num_ws, num_turbines) + power_probablistic, + (num_wd_unc, num_wd, num_ws, num_turbines) ) # Calculate probability weighing terms @@ -487,14 +495,18 @@ def get_farm_power(self, turbine_weights=None): ( self.floris.flow_field.n_wind_directions, self.floris.flow_field.n_wind_speeds, - self.floris.farm.n_turbines, + self.floris.farm.n_turbines ) ) elif len(np.shape(turbine_weights)) == 1: # Deal with situation when 1D array is provided turbine_weights = np.tile( turbine_weights, - (self.floris.flow_field.n_wind_directions, self.floris.flow_field.n_wind_speeds, 1), + ( + self.floris.flow_field.n_wind_directions, + self.floris.flow_field.n_wind_speeds, + 1 + ) ) # Calculate all turbine powers and apply weights @@ -597,8 +609,8 @@ def get_farm_AEP( self.calculate_no_wake(yaw_angles=yaw_angles_subset) else: self.calculate_wake(yaw_angles=yaw_angles_subset) - farm_power[:, conditions_to_evaluate] = self.get_farm_power( - turbine_weights=turbine_weights + farm_power[:, conditions_to_evaluate] = ( + self.get_farm_power(turbine_weights=turbine_weights) ) # Finally, calculate AEP in GWh From 3edcfc59e92985cbca1e50eb53b98a4ecb4c2172 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 23 Jan 2024 19:48:28 -0800 Subject: [PATCH 074/101] Remove pure format changes --- examples/12_optimize_yaw_in_parallel.py | 35 +++++----- examples/19_streamlit_demo.py | 86 ++++++++++++++---------- tests/conftest.py | 89 +++++++++++++++---------- tests/floris_interface_test.py | 2 +- tests/flow_field_unit_test.py | 6 +- 5 files changed, 128 insertions(+), 90 deletions(-) diff --git a/examples/12_optimize_yaw_in_parallel.py b/examples/12_optimize_yaw_in_parallel.py index d4be8b8ec..c4233f5ef 100644 --- a/examples/12_optimize_yaw_in_parallel.py +++ b/examples/12_optimize_yaw_in_parallel.py @@ -26,10 +26,9 @@ ... """ - def load_floris(): # Load the default example floris object - fi = FlorisInterface("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 + fi = FlorisInterface("inputs/gch.yaml") # GCH model matched to the default "legacy_gauss" of V2 # fi = FlorisInterface("inputs/cc.yaml") # New CumulativeCurl model # Specify wind farm layout and update in the floris object @@ -128,6 +127,8 @@ def load_windrose(): exploit_layout_symmetry=False, ) + + # Assume linear ramp up at 5-6 m/s and ramp down at 13-14 m/s, # add to table for linear interpolant df_copy_lb = df_opt[df_opt["wind_speed"] == 6.0].copy() @@ -171,27 +172,27 @@ def load_windrose(): # Now calculate helpful variables and then plot wind rose information farm_energy_bl = np.multiply(freq_grid, farm_power_bl) farm_energy_opt = np.multiply(freq_grid, farm_power_opt) - df = pd.DataFrame( - { - "wd": wd_grid.flatten(), - "ws": ws_grid.flatten(), - "freq_val": freq_grid.flatten(), - "farm_power_baseline": farm_power_bl.flatten(), - "farm_power_opt": farm_power_opt.flatten(), - "farm_power_relative": farm_power_opt.flatten() / farm_power_bl.flatten(), - "farm_energy_baseline": farm_energy_bl.flatten(), - "farm_energy_opt": farm_energy_opt.flatten(), - "energy_uplift": (farm_energy_opt - farm_energy_bl).flatten(), - "rel_energy_uplift": farm_energy_opt.flatten() / np.sum(farm_energy_bl), - } - ) + df = pd.DataFrame({ + "wd": wd_grid.flatten(), + "ws": ws_grid.flatten(), + "freq_val": freq_grid.flatten(), + "farm_power_baseline": farm_power_bl.flatten(), + "farm_power_opt": farm_power_opt.flatten(), + "farm_power_relative": farm_power_opt.flatten() / farm_power_bl.flatten(), + "farm_energy_baseline": farm_energy_bl.flatten(), + "farm_energy_opt": farm_energy_opt.flatten(), + "energy_uplift": (farm_energy_opt - farm_energy_bl).flatten(), + "rel_energy_uplift": farm_energy_opt.flatten() / np.sum(farm_energy_bl) + }) # Plot power and AEP uplift across wind direction wd_step = np.diff(fi_aep.floris.flow_field.wind_directions)[0] # Useful variable for plotting fig, ax = plt.subplots(nrows=3, sharex=True) df_8ms = df[df["ws"] == 8.0].reset_index(drop=True) - pow_uplift = 100 * (df_8ms["farm_power_opt"] / df_8ms["farm_power_baseline"] - 1) + pow_uplift = 100 * ( + df_8ms["farm_power_opt"] / df_8ms["farm_power_baseline"] - 1 + ) ax[0].bar( x=df_8ms["wd"], height=pow_uplift, diff --git a/examples/19_streamlit_demo.py b/examples/19_streamlit_demo.py index 2e00f6161..91b4f466d 100644 --- a/examples/19_streamlit_demo.py +++ b/examples/19_streamlit_demo.py @@ -24,6 +24,7 @@ # import seaborn as sns + # """ # This example demonstrates an interactive visual comparison of FLORIS # wake models using streamlit @@ -44,16 +45,21 @@ st.set_page_config(layout="wide") # Parameters -D = 126.0 # Assume for convenience -floris_model_list = ["jensen", "gch", "cc", "turbopark"] -color_dict = {"jensen": "k", "gch": "b", "cc": "r", "turbopark": "c"} +D = 126. # Assume for convenience +floris_model_list = ['jensen','gch','cc','turbopark'] +color_dict = { + 'jensen':'k', + 'gch':'b', + 'cc':'r', + 'turbopark':'c' +} # Streamlit inputs n_turbine_per_row = st.sidebar.slider("Turbines per row", 1, 8, 2, step=1) -n_row = st.sidebar.slider("Number of rows", 1, 8, 1, step=1) -spacing = st.sidebar.slider("Turbine spacing (D)", 3.0, 10.0, 6.0, step=0.5) -wind_direction = st.sidebar.slider("Wind Direction", 240.0, 300.0, 270.0, step=1.0) -wind_speed = st.sidebar.slider("Wind Speed", 4.0, 15.0, 8.0, step=0.25) +n_row = st.sidebar.slider("Number of rows", 1, 8,1, step=1) +spacing = st.sidebar.slider("Turbine spacing (D)", 3., 10., 6., step=0.5) +wind_direction = st.sidebar.slider("Wind Direction", 240., 300., 270., step=1.) +wind_speed = st.sidebar.slider("Wind Speed", 4., 15., 8., step=0.25) turbulence_intensity = st.sidebar.slider("Turbulence Intensity", 0.01, 0.25, 0.06, step=0.01) floris_models = st.sidebar.multiselect("FLORIS Models", floris_model_list, floris_model_list) # floris_models_viz = st.sidebar.multiselect( @@ -61,8 +67,8 @@ # floris_model_list, # floris_model_list # ) -desc_yaw = st.sidebar.checkbox("Descending yaw pattern?", value=False) -front_turbine_yaw = st.sidebar.slider("Upstream yaw angle", -30.0, 30.0, 0.0, step=0.5) +desc_yaw = st.sidebar.checkbox("Descending yaw pattern?",value=False) +front_turbine_yaw = st.sidebar.slider("Upstream yaw angle", -30., 30., 0., step=0.5) # Define the layout X = [] @@ -73,18 +79,19 @@ X.append(D * spacing * x_idx) Y.append(D * spacing * y_idx) -turbine_labels = ["T%02d" % i for i in range(len(X))] +turbine_labels = ['T%02d' % i for i in range(len(X))] # Set up the yaw angle values -yaw_angles_base = np.zeros([1, 1, len(X)]) +yaw_angles_base = np.zeros([1,1,len(X)]) -yaw_angles_yaw = np.zeros([1, 1, len(X)]) +yaw_angles_yaw = np.zeros([1,1,len(X)]) if not desc_yaw: - yaw_angles_yaw[:, :, :n_row] = front_turbine_yaw + yaw_angles_yaw[:,:,:n_row] = front_turbine_yaw else: - decreasing_pattern = np.linspace(front_turbine_yaw, 0, n_turbine_per_row) + decreasing_pattern = np.linspace(front_turbine_yaw,0,n_turbine_per_row) for i in range(n_turbine_per_row): - yaw_angles_yaw[:, :, i * n_row : (i + 1) * n_row] = decreasing_pattern[i] + yaw_angles_yaw[:,:,i*n_row:(i+1)*n_row] = decreasing_pattern[i] + # Get a few quanitities @@ -96,7 +103,7 @@ num_models_to_viz = len(floris_models_viz) # Set up the visualization plot -fig_viz, axarr_viz = plt.subplots(num_models_to_viz, 2) +fig_viz, axarr_viz = plt.subplots(num_models_to_viz,2) # Set up the turbine power plot fig_turb_pow, ax_turb_pow = plt.subplots() @@ -106,8 +113,9 @@ # Now complete all these plots in a loop for fm in floris_models: + # Analyze the base case================================================== - print("Loading: ", fm) + print('Loading: ',fm) fi = FlorisInterface("inputs/%s.yaml" % fm) # Set the layout, wind direction and wind speed @@ -120,22 +128,22 @@ ) fi.calculate_wake(yaw_angles=yaw_angles_base) - turbine_powers = fi.get_turbine_powers() / 1000.0 + turbine_powers = fi.get_turbine_powers() / 1000. ax_turb_pow.plot( turbine_labels, turbine_powers.flatten(), color=color_dict[fm], - ls="-", - marker="s", - label="%s - baseline" % fm, + ls='-', + marker='s', + label='%s - baseline' % fm ) ax_turb_pow.grid(True) ax_turb_pow.legend() - ax_turb_pow.set_xlabel("Turbine") - ax_turb_pow.set_ylabel("Power (kW)") + ax_turb_pow.set_xlabel('Turbine') + ax_turb_pow.set_ylabel('Power (kW)') # Save the farm power - farm_power_results.append((fm, "base", np.sum(turbine_powers))) + farm_power_results.append((fm,'base',np.sum(turbine_powers))) # If in viz list also visualize if fm in floris_models_viz: @@ -143,12 +151,15 @@ ax = axarr_viz[ax_idx, 0] horizontal_plane_gch = fi.calculate_horizontal_plane( - x_resolution=100, y_resolution=100, yaw_angles=yaw_angles_base, height=90.0 + x_resolution=100, + y_resolution=100, + yaw_angles=yaw_angles_base, + height=90.0 ) - visualize_cut_plane(horizontal_plane_gch, ax=ax, title="%s - baseline" % fm) + visualize_cut_plane(horizontal_plane_gch, ax=ax, title='%s - baseline' % fm) # Analyze the yawed case================================================== - print("Loading: ", fm) + print('Loading: ',fm) fi = FlorisInterface("inputs/%s.yaml" % fm) # Set the layout, wind direction and wind speed @@ -161,22 +172,22 @@ ) fi.calculate_wake(yaw_angles=yaw_angles_yaw) - turbine_powers = fi.get_turbine_powers() / 1000.0 + turbine_powers = fi.get_turbine_powers() / 1000. ax_turb_pow.plot( turbine_labels, turbine_powers.flatten(), color=color_dict[fm], - ls="--", - marker="o", - label="%s - yawed" % fm, + ls='--', + marker='o', + label='%s - yawed' % fm ) ax_turb_pow.grid(True) ax_turb_pow.legend() - ax_turb_pow.set_xlabel("Turbine") - ax_turb_pow.set_ylabel("Power (kW)") + ax_turb_pow.set_xlabel('Turbine') + ax_turb_pow.set_ylabel('Power (kW)') # Save the farm power - farm_power_results.append((fm, "yawed", np.sum(turbine_powers))) + farm_power_results.append((fm,'yawed',np.sum(turbine_powers))) # If in viz list also visualize if fm in floris_models_viz: @@ -184,9 +195,12 @@ ax = axarr_viz[ax_idx, 1] horizontal_plane_gch = fi.calculate_horizontal_plane( - x_resolution=100, y_resolution=100, yaw_angles=yaw_angles_yaw, height=90.0 + x_resolution=100, + y_resolution=100, + yaw_angles=yaw_angles_yaw, + height=90.0 ) - visualize_cut_plane(horizontal_plane_gch, ax=ax, title="%s - yawed" % fm) + visualize_cut_plane(horizontal_plane_gch, ax=ax, title='%s - yawed' % fm) st.header("Visualizations") st.write(fig_viz) diff --git a/tests/conftest.py b/tests/conftest.py index cbbcd32ed..9da666d8a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,7 +56,7 @@ def print_test_values( thrusts: list, powers: list, axial_inductions: list, - max_findex_print: int | None = None, + max_findex_print: int | None =None ): n_findex, n_turb = np.shape(average_velocities) if max_findex_print is not None: @@ -66,7 +66,8 @@ def print_test_values( for j in range(n_turb): print( " [{:.7f}, {:.7f}, {:.7f}, {:.7f}],".format( - average_velocities[i, j], thrusts[i, j], powers[i, j], axial_inductions[i, j] + average_velocities[i,j], thrusts[i,j], powers[i,j], + axial_inductions[i,j] ) ) print("],") @@ -113,9 +114,21 @@ def print_test_values( # len(WIND_DIRECTIONS) or len(WIND_SPEEDS N_FINDEX = len(WIND_DIRECTIONS) -X_COORDS = [0.0, 5 * 126.0, 10 * 126.0] -Y_COORDS = [0.0, 0.0, 0.0] -Z_COORDS = [90.0, 90.0, 90.0] +X_COORDS = [ + 0.0, + 5 * 126.0, + 10 * 126.0 +] +Y_COORDS = [ + 0.0, + 0.0, + 0.0 +] +Z_COORDS = [ + 90.0, + 90.0, + 90.0 +] N_TURBINES = len(X_COORDS) ROTOR_DIAMETER = 126.0 TURBINE_GRID_RESOLUTION = 2 @@ -124,42 +137,38 @@ def print_test_values( ## Unit test fixtures - @pytest.fixture def flow_field_fixture(sample_inputs_fixture): flow_field_dict = sample_inputs_fixture.flow_field return FlowField.from_dict(flow_field_dict) - @pytest.fixture def turbine_grid_fixture(sample_inputs_fixture) -> TurbineGrid: turbine_coordinates = np.array(list(zip(X_COORDS, Y_COORDS, Z_COORDS))) - rotor_diameters = ROTOR_DIAMETER * np.ones((N_TURBINES)) + rotor_diameters = ROTOR_DIAMETER * np.ones( (N_TURBINES) ) return TurbineGrid( turbine_coordinates=turbine_coordinates, turbine_diameters=rotor_diameters, wind_directions=np.array(WIND_DIRECTIONS), grid_resolution=TURBINE_GRID_RESOLUTION, - time_series=TIME_SERIES, + time_series=TIME_SERIES ) - @pytest.fixture def flow_field_grid_fixture(sample_inputs_fixture) -> FlowFieldGrid: turbine_coordinates = np.array(list(zip(X_COORDS, Y_COORDS, Z_COORDS))) - rotor_diameters = ROTOR_DIAMETER * np.ones((N_FINDEX, N_TURBINES)) + rotor_diameters = ROTOR_DIAMETER * np.ones( (N_FINDEX, N_TURBINES) ) return FlowFieldGrid( turbine_coordinates=turbine_coordinates, turbine_diameters=rotor_diameters, wind_directions=np.array(WIND_DIRECTIONS), - grid_resolution=[3, 2, 2], + grid_resolution=[3,2,2] ) - @pytest.fixture def points_grid_fixture(sample_inputs_fixture) -> PointsGrid: turbine_coordinates = np.array(list(zip(X_COORDS, Y_COORDS, Z_COORDS))) - rotor_diameters = ROTOR_DIAMETER * np.ones((N_FINDEX, N_TURBINES)) + rotor_diameters = ROTOR_DIAMETER * np.ones( (N_FINDEX, N_TURBINES) ) points_x = [0.0, 10.0] points_y = [0.0, 0.0] points_z = [1.0, 2.0] @@ -174,13 +183,11 @@ def points_grid_fixture(sample_inputs_fixture) -> PointsGrid: points_z=points_z, ) - @pytest.fixture def floris_fixture(): sample_inputs = SampleInputs() return Floris(sample_inputs.floris) - @pytest.fixture def sample_inputs_fixture(): return SampleInputs() @@ -372,7 +379,7 @@ def __init__(self): 50.0, ], }, - "TSR": 8.0, + "TSR": 8.0 } self.turbine_floating = copy.deepcopy(self.turbine) @@ -391,13 +398,17 @@ def __init__(self): self.turbine_floating["correct_cp_ct_for_tilt"] = True self.turbine_multi_dim = copy.deepcopy(self.turbine) - del self.turbine_multi_dim["power_thrust_table"]["power"] - del self.turbine_multi_dim["power_thrust_table"]["thrust_coefficient"] - del self.turbine_multi_dim["power_thrust_table"]["wind_speed"] + del self.turbine_multi_dim['power_thrust_table']['power'] + del self.turbine_multi_dim['power_thrust_table']['thrust_coefficient'] + del self.turbine_multi_dim['power_thrust_table']['wind_speed'] self.turbine_multi_dim["multi_dimensional_cp_ct"] = True - self.turbine_multi_dim["power_thrust_table"]["power_thrust_data_file"] = "" + self.turbine_multi_dim['power_thrust_table']["power_thrust_data_file"] = "" - self.farm = {"layout_x": X_COORDS, "layout_y": Y_COORDS, "turbine_type": [self.turbine]} + self.farm = { + "layout_x": X_COORDS, + "layout_y": Y_COORDS, + "turbine_type": [self.turbine] + } self.flow_field = { "wind_speeds": WIND_SPEEDS, @@ -424,7 +435,7 @@ def __init__(self): "beta": 0.077, "dm": 1.0, "ka": 0.38, - "kb": 0.004, + "kb": 0.004 }, "jimenez": { "ad": 0.0, @@ -432,15 +443,20 @@ def __init__(self): "kd": 0.05, }, "empirical_gauss": { - "horizontal_deflection_gain_D": 3.0, - "vertical_deflection_gain_D": -1, - "deflection_rate": 30, - "mixing_gain_deflection": 0.0, - "yaw_added_mixing_gain": 0.0, + "horizontal_deflection_gain_D": 3.0, + "vertical_deflection_gain_D": -1, + "deflection_rate": 30, + "mixing_gain_deflection": 0.0, + "yaw_added_mixing_gain": 0.0 }, }, "wake_velocity_parameters": { - "gauss": {"alpha": 0.58, "beta": 0.077, "ka": 0.38, "kb": 0.004}, + "gauss": { + "alpha": 0.58, + "beta": 0.077, + "ka": 0.38, + "kb": 0.004 + }, "jensen": { "we": 0.05, }, @@ -452,15 +468,18 @@ def __init__(self): "a_f": 3.11, "b_f": -0.68, "c_f": 2.41, - "alpha_mod": 1.0, + "alpha_mod": 1.0 + }, + "turbopark": { + "A": 0.04, + "sigma_max_rel": 4.0 }, - "turbopark": {"A": 0.04, "sigma_max_rel": 4.0}, "empirical_gauss": { "wake_expansion_rates": [0.023, 0.008], "breakpoints_D": [10], "sigma_0_D": 0.28, "smoothing_length_D": 2.0, - "mixing_gain_velocity": 2.0, + "mixing_gain_velocity": 2.0 }, }, "wake_turbulence_parameters": { @@ -468,9 +487,11 @@ def __init__(self): "initial": 0.1, "constant": 0.5, "ai": 0.8, - "downstream": -0.32, + "downstream": -0.32 }, - "wake_induced_mixing": {"atmospheric_ti_gain": 0.0}, + "wake_induced_mixing": { + "atmospheric_ti_gain": 0.0 + } }, "enable_secondary_steering": False, "enable_yaw_added_recovery": False, diff --git a/tests/floris_interface_test.py b/tests/floris_interface_test.py index 56b49b07f..ace1b34c0 100644 --- a/tests/floris_interface_test.py +++ b/tests/floris_interface_test.py @@ -199,7 +199,7 @@ def test_get_farm_aep_with_conditions(): # In this case farm_aep should match farm powers np.testing.assert_allclose(farm_aep, aep) - # Confirm n_findex reset after the operation + #Confirm n_findex reset after the operation assert n_findex == fi.floris.flow_field.n_findex diff --git a/tests/flow_field_unit_test.py b/tests/flow_field_unit_test.py index 1ce53b89c..978911700 100644 --- a/tests/flow_field_unit_test.py +++ b/tests/flow_field_unit_test.py @@ -41,13 +41,15 @@ def test_initialize_velocity_field(flow_field_fixture, turbine_grid_fixture: Tur # which is the input wind speed. shape = np.shape(flow_field_fixture.u_sorted[0, 0, :, :]) n_elements = shape[0] * shape[1] - average = np.sum(flow_field_fixture.u_sorted[:, 0, :, :], axis=(-2, -1)) / np.array( - [n_elements] + average = ( + np.sum(flow_field_fixture.u_sorted[:, 0, :, :], axis=(-2, -1)) + / np.array([n_elements]) ) assert np.array_equal(average, flow_field_fixture.wind_speeds) def test_asdict(flow_field_fixture: FlowField, turbine_grid_fixture: TurbineGrid): + flow_field_fixture.initialize_velocity_field(turbine_grid_fixture) dict1 = flow_field_fixture.as_dict() From 3711c862b9d53c6ce3841b62c215c14c0410af72 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 23 Jan 2024 19:52:18 -0800 Subject: [PATCH 075/101] Import WindDataBase correctly --- tests/wind_data_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wind_data_test.py b/tests/wind_data_test.py index 00802d935..565d38ae1 100644 --- a/tests/wind_data_test.py +++ b/tests/wind_data_test.py @@ -17,9 +17,9 @@ from floris.tools import ( TimeSeries, - WindDataBase, WindRose, ) +from floris.tools.wind_data import WindDataBase class ChildClassTest(WindDataBase): From b31377771e0b3005e44c979d2a86aff890664320 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 25 Jan 2024 11:13:39 -0700 Subject: [PATCH 076/101] Add doc to examples --- examples/35_sweep_ti.py | 4 +++- examples/36_generate_ti.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/35_sweep_ti.py b/examples/35_sweep_ti.py index fcae472db..8599be550 100644 --- a/examples/35_sweep_ti.py +++ b/examples/35_sweep_ti.py @@ -24,7 +24,9 @@ """ -Demonstrate the new behavior in T4 where TI is an array rather than a float +Demonstrate the new behavior in T4 where TI is an array rather than a float. +Set up an array of two turbines and sweep TI while hold wd/ws constant. +Use the TimeSeries object to drive the FLORIS calculations. """ diff --git a/examples/36_generate_ti.py b/examples/36_generate_ti.py index fca54a348..dd5c82f3a 100644 --- a/examples/36_generate_ti.py +++ b/examples/36_generate_ti.py @@ -24,7 +24,7 @@ """ -Demonstrate usage of ti generating and plotting functionality +Demonstrate usage of ti generating and plotting functionality in the WindRose class """ @@ -32,6 +32,7 @@ wind_directions = np.array([250, 260, 270]) wind_speeds = np.array([5, 6, 7, 8, 9, 10]) +# Declare a WindRose object wind_rose = WindRose(wind_directions=wind_directions, wind_speeds=wind_speeds) From c37d7ee713960d7b57b3762372a6c67db0b51718 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 25 Jan 2024 12:56:08 -0700 Subject: [PATCH 077/101] Update IEC function to include offset and default values --- examples/36_generate_ti.py | 4 ++-- floris/tools/wind_data.py | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/examples/36_generate_ti.py b/examples/36_generate_ti.py index dd5c82f3a..689fa799d 100644 --- a/examples/36_generate_ti.py +++ b/examples/36_generate_ti.py @@ -49,7 +49,7 @@ def custom_ti_func(wind_directions, wind_speeds): # Now use the IEC Iref approach: Iref = 0.08 -wind_rose.assign_ti_using_Iref(Iref) +wind_rose.assign_ti_using_IEC_method(Iref) fig, ax = plt.subplots() wind_rose.plot_ti_over_ws(ax) ax.set_title(f"Turbulence Intensity defined by Iref = {Iref:0.2}") @@ -60,7 +60,7 @@ def custom_ti_func(wind_directions, wind_speeds): wind_directions = 270 * np.ones(N) wind_speeds = np.linspace(5, 15, N) time_series = TimeSeries(wind_directions=wind_directions, wind_speeds=wind_speeds) -time_series.assign_ti_using_Iref(Iref=Iref) +time_series.assign_ti_using_IEC_method(Iref=Iref) fig, axarr = plt.subplots(2, 1, sharex=True, figsize=(7, 8)) ax = axarr[0] diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index e45a00dd5..ff30962c9 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -345,19 +345,20 @@ def assign_ti_using_wd_ws_function(self, func): self.ti_table = func(self.wd_grid, self.ws_grid) self._build_gridded_and_flattened_version() - def assign_ti_using_Iref(self, Iref): + def assign_ti_using_IEC_method(self, Iref=0.12, offset=3.8): """ - Define TI as a function of wind speed by specifying an Iref value as in the - IEC standard appraoch + Define TI as a function of wind speed by specifying an Iref and offset + value as in theIEC standard appraoch Args: - Iref (float): Reference turbulence level, values range [0,1] + Iref (float): Reference turbulence level. Default = 0.12 + offset (float): Offset value to equation. Default = 3.8 """ if (Iref < 0) or (Iref > 1): raise ValueError("Iref must be >= 0 and <=1") def iref_func(wind_directions, wind_speeds): - sigma_1 = Iref * (0.75 * wind_speeds + 5.6) + sigma_1 = Iref * (0.75 * wind_speeds + offset) return sigma_1 / wind_speeds self.assign_ti_using_wd_ws_function(iref_func) @@ -487,19 +488,21 @@ def assign_ti_using_wd_ws_function(self, func): """ self.turbulence_intensities = func(self.wind_directions, self.wind_speeds) - def assign_ti_using_Iref(self, Iref): + def assign_ti_using_IEC_method(self, Iref=0.12, offset=3.8): """ - Define TI as a function of wind speed by specifying an Iref value as in the + Define TI as a function of wind speed by specifying an Iref and + offset value as in the IEC standard appraoch Args: - Iref (float): Reference turbulence level, values range [0,1] + Iref (float): Reference turbulence level. Default = 0.12 + offset (float): Offset value to equation. Default = 3.8 """ if (Iref < 0) or (Iref > 1): raise ValueError("Iref must be >= 0 and <=1") def iref_func(wind_directions, wind_speeds): - sigma_1 = Iref * (0.75 * wind_speeds + 5.6) + sigma_1 = Iref * (0.75 * wind_speeds + offset) return sigma_1 / wind_speeds self.assign_ti_using_wd_ws_function(iref_func) From 3d089910768303082b4d1aabf165609f574e76a5 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 25 Jan 2024 12:59:06 -0700 Subject: [PATCH 078/101] Update defaults --- floris/tools/wind_data.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index ff30962c9..8ce9c225c 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -345,13 +345,14 @@ def assign_ti_using_wd_ws_function(self, func): self.ti_table = func(self.wd_grid, self.ws_grid) self._build_gridded_and_flattened_version() - def assign_ti_using_IEC_method(self, Iref=0.12, offset=3.8): + def assign_ti_using_IEC_method(self, Iref=0.08, offset=3.8): """ Define TI as a function of wind speed by specifying an Iref and offset value as in theIEC standard appraoch Args: - Iref (float): Reference turbulence level. Default = 0.12 + Iref (float): Reference turbulence level. Default = 0.08. + Note this value is lower than IEC standard but matches observations offset (float): Offset value to equation. Default = 3.8 """ if (Iref < 0) or (Iref > 1): @@ -488,14 +489,14 @@ def assign_ti_using_wd_ws_function(self, func): """ self.turbulence_intensities = func(self.wind_directions, self.wind_speeds) - def assign_ti_using_IEC_method(self, Iref=0.12, offset=3.8): + def assign_ti_using_IEC_method(self, Iref=0.08, offset=3.8): """ - Define TI as a function of wind speed by specifying an Iref and - offset value as in the - IEC standard appraoch + Define TI as a function of wind speed by specifying an Iref and offset + value as in theIEC standard appraoch Args: - Iref (float): Reference turbulence level. Default = 0.12 + Iref (float): Reference turbulence level. Default = 0.08. + Note this value is lower than IEC standard but matches observations offset (float): Offset value to equation. Default = 3.8 """ if (Iref < 0) or (Iref > 1): From 3fef5517b740e27b5a90ffce4c9c78c80f8b5c4b Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 29 Jan 2024 13:31:11 -0700 Subject: [PATCH 079/101] Move type error with raise output --- floris/simulation/flow_field.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index 0e81fdcba..8da1f4260 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -76,10 +76,11 @@ def turbulence_intensities_validator( raise ValueError("turbulence_intensities should either be length 1 or n_findex") except TypeError as te: # Handle the TypeError here - print(f"Caught a TypeError: {te}") + raise TypeError( "turbulence_intensities must be provided as a list or array. To specify a uniform", " turbulence intensity, specify as an array of legnth 1", + f"Full TypeError Output: {te}" ) @wind_directions.validator From ab32696be89415b32ef7df3ac079e28dfc146826 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 29 Jan 2024 13:34:16 -0700 Subject: [PATCH 080/101] Back to v3 style --- tests/data/input_full_v3.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/data/input_full_v3.yaml b/tests/data/input_full_v3.yaml index 36a150bdd..1a80ac44e 100644 --- a/tests/data/input_full_v3.yaml +++ b/tests/data/input_full_v3.yaml @@ -26,8 +26,7 @@ farm: flow_field: air_density: 1.225 reference_wind_height: 90.0 - turbulence_intensities: - - 0.06 + turbulence_intensities: 0.06 wind_directions: - 270.0 wind_shear: 0.12 From 9627db16249520999362278a185d67a4f7b6ef0c Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 29 Jan 2024 13:34:30 -0700 Subject: [PATCH 081/101] Use None instead of np.newaxis --- floris/simulation/flow_field.py | 2 +- floris/simulation/solver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index 8da1f4260..f9c96d3ed 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -219,7 +219,7 @@ def initialize_velocity_field(self, grid: Grid) -> None: self.w_sorted = self.w_initial_sorted.copy() self.turbulence_intensity_field = self.turbulence_intensities[ - :, np.newaxis, np.newaxis, np.newaxis + :, None, None, None ] self.turbulence_intensity_field = np.repeat( self.turbulence_intensity_field, grid.n_turbines, axis=1 diff --git a/floris/simulation/solver.py b/floris/simulation/solver.py index b930070df..afa4d25ce 100644 --- a/floris/simulation/solver.py +++ b/floris/simulation/solver.py @@ -78,7 +78,7 @@ def sequential_solver( # Set up turbulence arrays turbine_turbulence_intensity = flow_field.turbulence_intensities[ - :, np.newaxis, np.newaxis, np.newaxis + :, None, None, None ] turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) From e8eeb20ee98091fbb37f210e72665f201601f0b6 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 29 Jan 2024 14:55:35 -0700 Subject: [PATCH 082/101] remove _v3 from input_full.yaml --- tests/data/{input_full_v3.yaml => input_full.yaml} | 0 tests/floris_interface_test.py | 2 +- tests/floris_unit_test.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename tests/data/{input_full_v3.yaml => input_full.yaml} (100%) diff --git a/tests/data/input_full_v3.yaml b/tests/data/input_full.yaml similarity index 100% rename from tests/data/input_full_v3.yaml rename to tests/data/input_full.yaml diff --git a/tests/floris_interface_test.py b/tests/floris_interface_test.py index ace1b34c0..3690fc69c 100644 --- a/tests/floris_interface_test.py +++ b/tests/floris_interface_test.py @@ -7,7 +7,7 @@ TEST_DATA = Path(__file__).resolve().parent / "data" -YAML_INPUT = TEST_DATA / "input_full_v3.yaml" +YAML_INPUT = TEST_DATA / "input_full.yaml" def test_read_yaml(): diff --git a/tests/floris_unit_test.py b/tests/floris_unit_test.py index 05c01f022..8fc75ca1f 100644 --- a/tests/floris_unit_test.py +++ b/tests/floris_unit_test.py @@ -26,7 +26,7 @@ TEST_DATA = Path(__file__).resolve().parent / "data" -YAML_INPUT = TEST_DATA / "input_full_v3.yaml" +YAML_INPUT = TEST_DATA / "input_full.yaml" DICT_INPUT = yaml.load(open(YAML_INPUT, "r"), Loader=yaml.SafeLoader) From c2ebe42b561c764fdbb98154663e1a78cd8c5b17 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 29 Jan 2024 14:56:07 -0700 Subject: [PATCH 083/101] set ti to array type input --- tests/data/input_full.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/data/input_full.yaml b/tests/data/input_full.yaml index 1a80ac44e..36a150bdd 100644 --- a/tests/data/input_full.yaml +++ b/tests/data/input_full.yaml @@ -26,7 +26,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: 90.0 - turbulence_intensities: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 From 1a142a37c32062c22ceee4931380feca3c3e4b88 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 29 Jan 2024 15:01:50 -0700 Subject: [PATCH 084/101] Add check on iter data --- floris/type_dec.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/floris/type_dec.py b/floris/type_dec.py index ebbb3178a..bd868b3ab 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -45,6 +45,14 @@ ### Custom callables for attrs objects and functions def floris_array_converter(data: Iterable) -> np.ndarray: + # Verify that `data` is iterable. + # For scalar quantities, np.array() creates a 0-dimensional array. + try: + iter(data) + except TypeError as e: + raise TypeError(e.args[0] + f". Data given: {data}") + + # Create a numpy array from the input data and cast to floris_float_type. try: a = np.array(data, dtype=floris_float_type) except TypeError as e: From 39938fff791fafce58d6f6e41a448c38583f19ca Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 29 Jan 2024 15:25:25 -0700 Subject: [PATCH 085/101] Set turbulence_intensities type hint to NDArrayFloat --- floris/simulation/flow_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index f9c96d3ed..d1d1ee1ae 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -39,7 +39,7 @@ class FlowField(BaseClass): wind_veer: float = field(converter=float) wind_shear: float = field(converter=float) air_density: float = field(converter=float) - turbulence_intensities: float = field(converter=floris_array_converter) + turbulence_intensities: NDArrayFloat = field(converter=floris_array_converter) reference_wind_height: float = field(converter=float) time_series: bool = field(default=False) heterogenous_inflow_config: dict = field(default=None) From 598aa81293b61ceb5c7c340b68a8a2d533ff4086 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 29 Jan 2024 15:28:04 -0700 Subject: [PATCH 086/101] Add a converter which can handle either scalar float or floris_array --- floris/type_dec.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/floris/type_dec.py b/floris/type_dec.py index bd868b3ab..c1b1d9c56 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -44,6 +44,21 @@ ### Custom callables for attrs objects and functions +def floris_numeric_or_float_converter(data: Any) -> Any: + # Convert numeric data to a float if not-iterable, or else + # apply the floris_array_converter + try: + iter(data) + except TypeError: + # Not iterable + try: + return float(data) + except TypeError as e: + raise TypeError(e.args[0] + f". Data given: {data}") + else: + # Iterable + return floris_array_converter(data) + def floris_array_converter(data: Iterable) -> np.ndarray: # Verify that `data` is iterable. # For scalar quantities, np.array() creates a 0-dimensional array. @@ -61,7 +76,7 @@ def floris_array_converter(data: Iterable) -> np.ndarray: def floris_numeric_dict_converter(data: dict) -> dict: try: - return {k: floris_array_converter(v) for k, v in data.items()} + return {k: floris_numeric_or_float_converter(v) for k, v in data.items()} except TypeError as e: raise TypeError(e.args[0] + f". Data given: {data}") From ab406b073a1b3345734a6e69fabdd3c48b99aa89 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 29 Jan 2024 15:30:53 -0700 Subject: [PATCH 087/101] Add test of single ti values --- tests/floris_interface_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/floris_interface_test.py b/tests/floris_interface_test.py index 3690fc69c..d682a34b6 100644 --- a/tests/floris_interface_test.py +++ b/tests/floris_interface_test.py @@ -250,3 +250,10 @@ def test_reinitailize_ti(): 280.0, ], ) + + # Test that applying a 1D array of length 1 is allowed for ti + fi.reinitialize(turbulence_intensities=[0.12]) + + # Test that applying a float however raises an error + with pytest.raises(TypeError): + fi.reinitialize(turbulence_intensities=0.12) From 5316d5f43c672c31b888f0d1967e0fcff76b4e55 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Tue, 30 Jan 2024 11:49:35 -0600 Subject: [PATCH 088/101] Replace newaxis with None for empty dimensions --- floris/simulation/solver.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/floris/simulation/solver.py b/floris/simulation/solver.py index afa4d25ce..382a4e7a8 100644 --- a/floris/simulation/solver.py +++ b/floris/simulation/solver.py @@ -77,9 +77,7 @@ def sequential_solver( w_wake = np.zeros_like(flow_field.w_initial_sorted) # Set up turbulence arrays - turbine_turbulence_intensity = flow_field.turbulence_intensities[ - :, None, None, None - ] + turbine_turbulence_intensity = flow_field.turbulence_intensities[:, None, None, None] turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity @@ -456,9 +454,7 @@ def cc_solver( turb_inflow_field = copy.deepcopy(flow_field.u_initial_sorted) # Set up turbulence arrays - turbine_turbulence_intensity = flow_field.turbulence_intensities[ - :, np.newaxis, np.newaxis, np.newaxis - ] + turbine_turbulence_intensity = flow_field.turbulence_intensities[:, None, None, None] turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensities @@ -870,9 +866,7 @@ def turbopark_solver( deflection_field = np.zeros_like(flow_field.u_initial_sorted) # Set up turbulence arrays - turbine_turbulence_intensity = flow_field.turbulence_intensities[ - :, np.newaxis, np.newaxis, np.newaxis - ] + turbine_turbulence_intensity = flow_field.turbulence_intensities[:, None, None, None] turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensities From ba9da1dbb99c7acf9d0880c95a0b5378afa4bf63 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Tue, 30 Jan 2024 11:56:26 -0600 Subject: [PATCH 089/101] Simplify syntax --- floris/simulation/flow_field.py | 8 ++++---- floris/simulation/solver.py | 33 +++++++++++++++++++++------------ floris/type_dec.py | 2 +- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index d1d1ee1ae..3d6f4fd0f 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -218,11 +218,11 @@ def initialize_velocity_field(self, grid: Grid) -> None: self.v_sorted = self.v_initial_sorted.copy() self.w_sorted = self.w_initial_sorted.copy() - self.turbulence_intensity_field = self.turbulence_intensities[ - :, None, None, None - ] + self.turbulence_intensity_field = self.turbulence_intensities[:, None, None, None] self.turbulence_intensity_field = np.repeat( - self.turbulence_intensity_field, grid.n_turbines, axis=1 + self.turbulence_intensity_field, + grid.n_turbines, + axis=1 ) self.turbulence_intensity_field_sorted = self.turbulence_intensity_field.copy() diff --git a/floris/simulation/solver.py b/floris/simulation/solver.py index 382a4e7a8..1cdf3049c 100644 --- a/floris/simulation/solver.py +++ b/floris/simulation/solver.py @@ -82,9 +82,8 @@ def sequential_solver( # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity # with extra dimension to reach 4d - ambient_turbulence_intensities = flow_field.turbulence_intensities.copy()[ - :, np.newaxis, np.newaxis, np.newaxis - ] + ambient_turbulence_intensities = flow_field.turbulence_intensities.copy() + ambient_turbulence_intensities = ambient_turbulence_intensities[:, None, None, None] # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): @@ -459,9 +458,8 @@ def cc_solver( # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensities # with extra dimension to reach 4d - ambient_turbulence_intensities = flow_field.turbulence_intensities.copy()[ - :, np.newaxis, np.newaxis, np.newaxis - ] + ambient_turbulence_intensities = flow_field.turbulence_intensities.copy() + ambient_turbulence_intensities = ambient_turbulence_intensities[:, None, None, None] shape = (farm.n_turbines,) + np.shape(flow_field.u_initial_sorted) Ctmp = np.zeros((shape)) @@ -626,7 +624,11 @@ def cc_solver( ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensities, grid.x_sorted, x_i, rotor_diameter_i, turb_aIs + ambient_turbulence_intensities, + grid.x_sorted, + x_i, + rotor_diameter_i, + turb_aIs ) # Calculate wake overlap for wake-added turbulence (WAT) @@ -871,9 +873,8 @@ def turbopark_solver( # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensities # with extra dimension to reach 4d - ambient_turbulence_intensities = flow_field.turbulence_intensities.copy()[ - :, np.newaxis, np.newaxis, np.newaxis - ] + ambient_turbulence_intensities = flow_field.turbulence_intensities.copy() + ambient_turbulence_intensities = ambient_turbulence_intensities[:, None, None, None] # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): @@ -1052,7 +1053,11 @@ def turbopark_solver( ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensities, grid.x_sorted, x_i, rotor_diameter_i, axial_induction_i + ambient_turbulence_intensities, + grid.x_sorted, + x_i, + rotor_diameter_i, + axial_induction_i ) # TODO: leaving this in for GCH quantities; will need to find another way to @@ -1146,7 +1151,11 @@ def empirical_gauss_solver( initial_mixing_factor = model_manager.turbulence_model.atmospheric_ti_gain * np.eye( grid.n_turbines ) - mixing_factor = np.repeat(initial_mixing_factor[None, :, :], flow_field.n_findex, axis=0) + mixing_factor = np.repeat( + initial_mixing_factor[None, :, :], + flow_field.n_findex, + axis=0 + ) mixing_factor = mixing_factor * flow_field.turbulence_intensities[:, None, None] # Calculate the velocity deficit sequentially from upstream to downstream turbines diff --git a/floris/type_dec.py b/floris/type_dec.py index c1b1d9c56..33b4a2fe4 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -61,7 +61,7 @@ def floris_numeric_or_float_converter(data: Any) -> Any: def floris_array_converter(data: Iterable) -> np.ndarray: # Verify that `data` is iterable. - # For scalar quantities, np.array() creates a 0-dimensional array. + # Note that for scalar quantities, np.array() creates a 0-dimensional array. try: iter(data) except TypeError as e: From c32c09761ce6ffc0efb86df62b9302eaea7773c5 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 30 Jan 2024 11:34:35 -0700 Subject: [PATCH 090/101] remove unnecsarry try/catch --- floris/simulation/flow_field.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index 3d6f4fd0f..bd26addc9 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -70,18 +70,12 @@ class FlowField(BaseClass): def turbulence_intensities_validator( self, instance: attrs.Attribute, value: NDArrayFloat ) -> None: - try: - # Check the turbulence intensity is either length 1 or n_findex - if len(value) != 1 and len(value) != self.n_findex: - raise ValueError("turbulence_intensities should either be length 1 or n_findex") - except TypeError as te: - # Handle the TypeError here - - raise TypeError( - "turbulence_intensities must be provided as a list or array. To specify a uniform", - " turbulence intensity, specify as an array of legnth 1", - f"Full TypeError Output: {te}" - ) + + # Check the turbulence intensity is either length 1 or n_findex + if len(value) != 1 and len(value) != self.n_findex: + raise ValueError("turbulence_intensities should either be length 1 or n_findex") + + @wind_directions.validator def wind_directions_validator(self, instance: attrs.Attribute, value: NDArrayFloat) -> None: From f59ab87f76287d1c1f4ba87d71259a93b40f61ef Mon Sep 17 00:00:00 2001 From: Eric Simley Date: Thu, 1 Feb 2024 14:34:53 -0700 Subject: [PATCH 091/101] minor typo fixes --- examples/35_sweep_ti.py | 4 ++-- examples/36_generate_ti.py | 3 ++- floris/tools/wind_data.py | 2 +- tests/floris_interface_test.py | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/35_sweep_ti.py b/examples/35_sweep_ti.py index 8599be550..f37cf735d 100644 --- a/examples/35_sweep_ti.py +++ b/examples/35_sweep_ti.py @@ -24,8 +24,8 @@ """ -Demonstrate the new behavior in T4 where TI is an array rather than a float. -Set up an array of two turbines and sweep TI while hold wd/ws constant. +Demonstrate the new behavior in V4 where TI is an array rather than a float. +Set up an array of two turbines and sweep TI while holding wd/ws constant. Use the TimeSeries object to drive the FLORIS calculations. """ diff --git a/examples/36_generate_ti.py b/examples/36_generate_ti.py index 689fa799d..97aa1be77 100644 --- a/examples/36_generate_ti.py +++ b/examples/36_generate_ti.py @@ -24,7 +24,8 @@ """ -Demonstrate usage of ti generating and plotting functionality in the WindRose class +Demonstrate usage of TI generating and plotting functionality in the WindRose +and TimeSeries classes """ diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 8ce9c225c..25ff0f7c9 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -335,7 +335,7 @@ def plot_wind_rose( def assign_ti_using_wd_ws_function(self, func): """ - Use the passed in function to new assign values to turbulence_intensities + Use the passed in function to assign new values to turbulence_intensities Args: func (function): Function which accepts wind_directions as its diff --git a/tests/floris_interface_test.py b/tests/floris_interface_test.py index d682a34b6..17d612a38 100644 --- a/tests/floris_interface_test.py +++ b/tests/floris_interface_test.py @@ -215,7 +215,8 @@ def test_reinitailize_ti(): ) # Now confirm can change wind speeds and directions shape without changing - # turbulence intensity since it is uniform this allowed + # turbulence intensity since this is allowed when the turbulence + # intensities are uniform # raises n_findex to 4 fi.reinitialize( wind_speeds=[8.0, 8.0, 8.0, 8.0], From f1bae5d930ff77e276ee10b4eba42c6c2f2aa4ba Mon Sep 17 00:00:00 2001 From: Eric Simley Date: Thu, 1 Feb 2024 15:35:57 -0700 Subject: [PATCH 092/101] adding more details on IEC TI method --- examples/36_generate_ti.py | 6 +++++- floris/tools/wind_data.py | 30 ++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/examples/36_generate_ti.py b/examples/36_generate_ti.py index 97aa1be77..d048a5155 100644 --- a/examples/36_generate_ti.py +++ b/examples/36_generate_ti.py @@ -48,7 +48,11 @@ def custom_ti_func(wind_directions, wind_speeds): wind_rose.plot_ti_over_ws(ax) ax.set_title("Turbulence Intensity defined by custom function") -# Now use the IEC Iref approach: +# Now use the normal turbulence model approach from the IEC 61400-1 standard, +# wherein TI is defined as a function of wind speed: +# Iref is defined as the TI value at 15 m/s. Note that Iref = 0.08 is lower +# than the values of Iref used in the IEC standard, but produces TI values more +# in line with those typically used in FLORIS (TI=9.8% at 8 m/s). Iref = 0.08 wind_rose.assign_ti_using_IEC_method(Iref) fig, ax = plt.subplots() diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 25ff0f7c9..0b54a9d34 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -348,12 +348,19 @@ def assign_ti_using_wd_ws_function(self, func): def assign_ti_using_IEC_method(self, Iref=0.08, offset=3.8): """ Define TI as a function of wind speed by specifying an Iref and offset - value as in theIEC standard appraoch + value as in the normal turbulence model in the IEC 61400-1 standard Args: - Iref (float): Reference turbulence level. Default = 0.08. - Note this value is lower than IEC standard but matches observations - offset (float): Offset value to equation. Default = 3.8 + Iref (float): Reference turbulence level, defined as the expected + value of TI at 15 m/s. Default = 0.08. Note this value is + lower than the values of Iref for turbulence classes A, B, and + C in the IEC standard (0.16, 0.14, and 0.12, respectively), but + produces TI values more in line with those typically used in + FLORIS. When the default Iref and offset are used, the TI at + 8 m/s is 9.8%. + offset (float): Offset value to equation. Default = 3.8, as defined + in the IEC standard to give the expected value of TI for + each wind speed. """ if (Iref < 0) or (Iref > 1): raise ValueError("Iref must be >= 0 and <=1") @@ -492,12 +499,19 @@ def assign_ti_using_wd_ws_function(self, func): def assign_ti_using_IEC_method(self, Iref=0.08, offset=3.8): """ Define TI as a function of wind speed by specifying an Iref and offset - value as in theIEC standard appraoch + value as in the normal turbulence model in the IEC 61400-1 standard Args: - Iref (float): Reference turbulence level. Default = 0.08. - Note this value is lower than IEC standard but matches observations - offset (float): Offset value to equation. Default = 3.8 + Iref (float): Reference turbulence level, defined as the expected + value of TI at 15 m/s. Default = 0.08. Note this value is + lower than the values of Iref for turbulence classes A, B, and + C in the IEC standard (0.16, 0.14, and 0.12, respectively), but + produces TI values more in line with those typically used in + FLORIS. When the default Iref and offset are used, the TI at + 8 m/s is 9.8%. + offset (float): Offset value to equation. Default = 3.8, as defined + in the IEC standard to give the expected value of TI for + each wind speed. """ if (Iref < 0) or (Iref > 1): raise ValueError("Iref must be >= 0 and <=1") From 9c8c007692da75899c43173df26c0f6440802874 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 2 Feb 2024 11:25:27 -0700 Subject: [PATCH 093/101] Change default to 0.07 for Iref --- floris/tools/wind_data.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 0b54a9d34..36d510e37 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -345,7 +345,7 @@ def assign_ti_using_wd_ws_function(self, func): self.ti_table = func(self.wd_grid, self.ws_grid) self._build_gridded_and_flattened_version() - def assign_ti_using_IEC_method(self, Iref=0.08, offset=3.8): + def assign_ti_using_IEC_method(self, Iref=0.07, offset=3.8): """ Define TI as a function of wind speed by specifying an Iref and offset value as in the normal turbulence model in the IEC 61400-1 standard @@ -355,9 +355,9 @@ def assign_ti_using_IEC_method(self, Iref=0.08, offset=3.8): value of TI at 15 m/s. Default = 0.08. Note this value is lower than the values of Iref for turbulence classes A, B, and C in the IEC standard (0.16, 0.14, and 0.12, respectively), but - produces TI values more in line with those typically used in + produces TI values more in line with those typically used in FLORIS. When the default Iref and offset are used, the TI at - 8 m/s is 9.8%. + 8 m/s is 9.8%. offset (float): Offset value to equation. Default = 3.8, as defined in the IEC standard to give the expected value of TI for each wind speed. @@ -496,7 +496,7 @@ def assign_ti_using_wd_ws_function(self, func): """ self.turbulence_intensities = func(self.wind_directions, self.wind_speeds) - def assign_ti_using_IEC_method(self, Iref=0.08, offset=3.8): + def assign_ti_using_IEC_method(self, Iref=0.07, offset=3.8): """ Define TI as a function of wind speed by specifying an Iref and offset value as in the normal turbulence model in the IEC 61400-1 standard @@ -506,7 +506,7 @@ def assign_ti_using_IEC_method(self, Iref=0.08, offset=3.8): value of TI at 15 m/s. Default = 0.08. Note this value is lower than the values of Iref for turbulence classes A, B, and C in the IEC standard (0.16, 0.14, and 0.12, respectively), but - produces TI values more in line with those typically used in + produces TI values more in line with those typically used in FLORIS. When the default Iref and offset are used, the TI at 8 m/s is 9.8%. offset (float): Offset value to equation. Default = 3.8, as defined From 0a4116fc3e9b360ec95806d182b520a974eedc28 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 2 Feb 2024 11:26:10 -0700 Subject: [PATCH 094/101] fix trailing whitespace --- examples/36_generate_ti.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/36_generate_ti.py b/examples/36_generate_ti.py index d048a5155..0f4e769da 100644 --- a/examples/36_generate_ti.py +++ b/examples/36_generate_ti.py @@ -49,7 +49,7 @@ def custom_ti_func(wind_directions, wind_speeds): ax.set_title("Turbulence Intensity defined by custom function") # Now use the normal turbulence model approach from the IEC 61400-1 standard, -# wherein TI is defined as a function of wind speed: +# wherein TI is defined as a function of wind speed: # Iref is defined as the TI value at 15 m/s. Note that Iref = 0.08 is lower # than the values of Iref used in the IEC standard, but produces TI values more # in line with those typically used in FLORIS (TI=9.8% at 8 m/s). From f2f4b1b2dd28422c438ebe9c3f54c1ed6af65a46 Mon Sep 17 00:00:00 2001 From: Eric Simley Date: Fri, 2 Feb 2024 12:03:46 -0700 Subject: [PATCH 095/101] updating docstrings for new default Iref --- examples/36_generate_ti.py | 6 +++--- floris/tools/wind_data.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/36_generate_ti.py b/examples/36_generate_ti.py index 0f4e769da..a42e1bf95 100644 --- a/examples/36_generate_ti.py +++ b/examples/36_generate_ti.py @@ -50,10 +50,10 @@ def custom_ti_func(wind_directions, wind_speeds): # Now use the normal turbulence model approach from the IEC 61400-1 standard, # wherein TI is defined as a function of wind speed: -# Iref is defined as the TI value at 15 m/s. Note that Iref = 0.08 is lower +# Iref is defined as the TI value at 15 m/s. Note that Iref = 0.07 is lower # than the values of Iref used in the IEC standard, but produces TI values more -# in line with those typically used in FLORIS (TI=9.8% at 8 m/s). -Iref = 0.08 +# in line with those typically used in FLORIS (TI=8.6% at 8 m/s). +Iref = 0.07 wind_rose.assign_ti_using_IEC_method(Iref) fig, ax = plt.subplots() wind_rose.plot_ti_over_ws(ax) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 36d510e37..9a384bba0 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -352,12 +352,12 @@ def assign_ti_using_IEC_method(self, Iref=0.07, offset=3.8): Args: Iref (float): Reference turbulence level, defined as the expected - value of TI at 15 m/s. Default = 0.08. Note this value is + value of TI at 15 m/s. Default = 0.07. Note this value is lower than the values of Iref for turbulence classes A, B, and C in the IEC standard (0.16, 0.14, and 0.12, respectively), but produces TI values more in line with those typically used in FLORIS. When the default Iref and offset are used, the TI at - 8 m/s is 9.8%. + 8 m/s is 8.6%. offset (float): Offset value to equation. Default = 3.8, as defined in the IEC standard to give the expected value of TI for each wind speed. @@ -503,12 +503,12 @@ def assign_ti_using_IEC_method(self, Iref=0.07, offset=3.8): Args: Iref (float): Reference turbulence level, defined as the expected - value of TI at 15 m/s. Default = 0.08. Note this value is + value of TI at 15 m/s. Default = 0.07. Note this value is lower than the values of Iref for turbulence classes A, B, and C in the IEC standard (0.16, 0.14, and 0.12, respectively), but produces TI values more in line with those typically used in FLORIS. When the default Iref and offset are used, the TI at - 8 m/s is 9.8%. + 8 m/s is 8.6%. offset (float): Offset value to equation. Default = 3.8, as defined in the IEC standard to give the expected value of TI for each wind speed. From 0bea0d5e5e5906e046149b3aa5b603fd74e60c5d Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 2 Feb 2024 12:03:53 -0700 Subject: [PATCH 096/101] Correct TI units in plot. --- floris/tools/wind_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 9a384bba0..ebf1c989c 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -396,7 +396,7 @@ def plot_ti_over_ws( if ax is None: _, ax = plt.subplots() - ax.plot(self.ws_flat, self.ti_table_flat, marker=marker, ls=ls, color=color) + ax.plot(self.ws_flat, self.ti_table_flat*100, marker=marker, ls=ls, color=color) ax.set_xlabel("Wind Speed (m/s)") ax.set_ylabel("Turbulence Intensity (%)") ax.grid(True) From 68d38648dd80de8b577c98c46aae99b87ec887f8 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 2 Feb 2024 12:15:05 -0700 Subject: [PATCH 097/101] Clean up eg 35 plot. --- examples/35_sweep_ti.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/examples/35_sweep_ti.py b/examples/35_sweep_ti.py index f37cf735d..6e235a9aa 100644 --- a/examples/35_sweep_ti.py +++ b/examples/35_sweep_ti.py @@ -47,22 +47,14 @@ fi.calculate_wake() turbine_power = fi.get_turbine_powers() -fig, axarr = plt.subplots(5, 1, sharex=True, figsize=(5, 9)) +fig, axarr = plt.subplots(2, 1, sharex=True, figsize=(6, 6)) ax = axarr[0] -ax.plot(wd_array, color="k") -ax.set_ylabel("Wind Direction") +ax.plot(ti_array*100, turbine_power[:, 0]/1000, color="k") +ax.set_ylabel("Front turbine power [kW]") ax = axarr[1] -ax.plot(ws_array, color="k") -ax.set_ylabel("Wind Speed") -ax = axarr[2] -ax.plot(ti_array, color="k") -ax.set_ylabel("Turbulence Intensity") -ax = axarr[3] -ax.plot(turbine_power[:, 0], color="k") -ax.set_ylabel("Front Turbine") -ax = axarr[4] -ax.plot(turbine_power[:, 1], color="k") -ax.set_ylabel("Rear Turbine") +ax.plot(ti_array*100, turbine_power[:, 1]/1000, color="k") +ax.set_ylabel("Rear turbine power [kW]") +ax.set_xlabel("Turbulence intensity [%]") for ax in axarr: ax.grid(True) From 077a95187fa7e824a3ceeb680baa4e627edc03d8 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 2 Feb 2024 14:15:30 -0700 Subject: [PATCH 098/101] Revert legacy code --- floris/tools/floris_interface_legacy_reader.py | 2 +- .../tools/optimization/legacy/scipy/yaw_wind_rose.py | 10 +++++----- .../legacy/scipy/yaw_wind_rose_clustered.py | 6 +++--- .../legacy/scipy/yaw_wind_rose_parallel.py | 6 +++--- .../legacy/scipy/yaw_wind_rose_parallel_clustered.py | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/floris/tools/floris_interface_legacy_reader.py b/floris/tools/floris_interface_legacy_reader.py index a12699adb..300b3566c 100644 --- a/floris/tools/floris_interface_legacy_reader.py +++ b/floris/tools/floris_interface_legacy_reader.py @@ -123,7 +123,7 @@ def _convert_v24_dictionary_to_v3(dict_legacy): dict_floris["flow_field"] = { "air_density": fp["air_density"], "reference_wind_height": ref_height, - "turbulence_intensities": fp["turbulence_intensities"][0], + "turbulence_intensity": fp["turbulence_intensity"][0], "wind_directions": [fp["wind_direction"]], "wind_shear": fp["wind_shear"], "wind_speeds": [fp["wind_speed"]], diff --git a/floris/tools/optimization/legacy/scipy/yaw_wind_rose.py b/floris/tools/optimization/legacy/scipy/yaw_wind_rose.py index fd4d9ef63..c6b2219a3 100644 --- a/floris/tools/optimization/legacy/scipy/yaw_wind_rose.py +++ b/floris/tools/optimization/legacy/scipy/yaw_wind_rose.py @@ -243,7 +243,7 @@ def _get_initial_farm_power(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensities=[self.ti[i]], + turbulence_intensity=self.ti[i], ) # initial power @@ -262,7 +262,7 @@ def _get_initial_farm_power(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensities=[self.ti[i]], + turbulence_intensity=self.ti[i], ) self.fi.calculate_wake(yaw_angles=self.yaw_angles_baseline) power_init = self.fi.get_turbine_power( @@ -770,7 +770,7 @@ def calc_baseline_power(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensities=[self.ti[i]], + turbulence_intensity=self.ti[i], ) # calculate baseline power @@ -913,7 +913,7 @@ def optimize(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensities=[self.ti[i]], + turbulence_intensity=self.ti[i], ) self.initial_farm_power = self.initial_farm_powers[i] @@ -945,7 +945,7 @@ def optimize(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensities=[self.ti[i]], + turbulence_intensity=self.ti[i], ) opt_yaw_angles = np.array(self.yaw_angles_template, copy=True) self.fi.calculate_wake(yaw_angles=opt_yaw_angles) diff --git a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_clustered.py b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_clustered.py index 9ab247483..0c5d5a8e3 100644 --- a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_clustered.py +++ b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_clustered.py @@ -319,7 +319,7 @@ def optimize(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensities=[self.ti[i]], + turbulence_intensity=self.ti[i], ) # Set initial farm power @@ -355,7 +355,7 @@ def optimize(self): self.fi.reinitialize_flow_field( layout_array=[ np.array(fi_full.layout_x)[cl], - np.array(fi_full.layout_y)[cl], + np.array(fi_full.layout_y)[cl] ] ) opt_yaw_angles[cl] = self._optimize() @@ -400,7 +400,7 @@ def optimize(self): self.fi.reinitialize_flow_field( wind_direction=[self.wd[i]], wind_speed=[self.ws[i]], - turbulence_intensities=[self.ti[i]], + turbulence_intensity=self.ti[i], ) opt_yaw_angles = np.array(self.yaw_angles_template, copy=True) self.fi.calculate_wake(yaw_angles=opt_yaw_angles) diff --git a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel.py b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel.py index 1e0dd9e16..ec46763a5 100644 --- a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel.py +++ b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel.py @@ -264,7 +264,7 @@ def _calc_baseline_power_one_case(self, ws, wd, ti=None): self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) else: self.fi.reinitialize_flow_field( - wind_direction=wd, wind_speed=ws, turbulence_intensities=ti + wind_direction=wd, wind_speed=ws, turbulence_intensity=ti ) # calculate baseline power self.fi.calculate_wake(yaw_angles=self.yaw_angles_baseline) @@ -372,7 +372,7 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) else: self.fi.reinitialize_flow_field( - wind_direction=wd, wind_speed=ws, turbulence_intensities=ti + wind_direction=wd, wind_speed=ws, turbulence_intensity=ti ) self.initial_farm_power = initial_farm_power @@ -400,7 +400,7 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) else: self.fi.reinitialize_flow_field( - wind_direction=wd, wind_speed=ws, turbulence_intensities=ti + wind_direction=wd, wind_speed=ws, turbulence_intensity=ti ) opt_yaw_angles = np.array(self.yaw_angles_template, copy=True) self.fi.calculate_wake(yaw_angles=opt_yaw_angles) diff --git a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel_clustered.py b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel_clustered.py index dad1d9cfa..caacc0429 100644 --- a/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel_clustered.py +++ b/floris/tools/optimization/legacy/scipy/yaw_wind_rose_parallel_clustered.py @@ -282,7 +282,7 @@ def _calc_baseline_power_one_case(self, ws, wd, ti=None): self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) else: self.fi.reinitialize_flow_field( - wind_direction=wd, wind_speed=ws, turbulence_intensities=ti + wind_direction=wd, wind_speed=ws, turbulence_intensity=ti ) # calculate baseline power self.fi.calculate_wake(yaw_angles=self.yaw_angles_baseline) @@ -390,7 +390,7 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) else: self.fi.reinitialize_flow_field( - wind_direction=wd, wind_speed=ws, turbulence_intensities=ti + wind_direction=wd, wind_speed=ws, turbulence_intensity=ti ) self.initial_farm_power = initial_farm_power @@ -463,7 +463,7 @@ def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) else: self.fi.reinitialize_flow_field( - wind_direction=wd, wind_speed=ws, turbulence_intensities=ti + wind_direction=wd, wind_speed=ws, turbulence_intensity=ti ) opt_yaw_angles = np.array(self.yaw_angles_template, copy=True) self.fi.calculate_wake(yaw_angles=opt_yaw_angles) From 2f84b5c874df6f72759010967d10464491196bdb Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Fri, 2 Feb 2024 16:08:45 -0600 Subject: [PATCH 099/101] Simplify gate This makes the if-statement match the comment above it. By removing a couple of levels if indentation due to the if-statements, the full condition is more clear --- floris/tools/floris_interface.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 71c26dda2..f94bd13bb 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -253,18 +253,21 @@ def reinitialize( # turbulence_intensities is None # len(turbulence intensity) != len(wind_directions) # turbulence_intensities is uniform - # in this case, automatically resize turbulence intensity - # This is the case where user is assuming same ti across all findex - if ((wind_speeds is not None) or (wind_directions is not None)) and ( - turbulence_intensities is None + # In this case, automatically resize turbulence intensity + # This is the case where user is assuming same TI across all findex + if ( + (wind_speeds is not None or wind_directions is not None) + and turbulence_intensities is None + and ( + len(flow_field_dict["turbulence_intensities"]) + != len(flow_field_dict["wind_directions"]) + ) + and len(np.unique(flow_field_dict["turbulence_intensities"])) == 1 ): - if len(flow_field_dict["turbulence_intensities"]) != len( - flow_field_dict["wind_directions"] - ): - if len(np.unique(flow_field_dict["turbulence_intensities"])) == 1: - flow_field_dict["turbulence_intensities"] = flow_field_dict[ - "turbulence_intensities" - ][0] * np.ones_like(flow_field_dict["wind_directions"]) + flow_field_dict["turbulence_intensities"] = ( + flow_field_dict["turbulence_intensities"][0] + * np.ones_like(flow_field_dict["wind_directions"]) + ) ## Farm if layout_x is not None: From 995789207775258f9aa19957dc44bafbf6705ce8 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Fri, 2 Feb 2024 19:42:14 -0600 Subject: [PATCH 100/101] Consolidate and test type conversion functions --- floris/type_dec.py | 62 +++++++++++++++++++++++-------------- tests/type_dec_unit_test.py | 48 ++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 25 deletions(-) diff --git a/floris/type_dec.py b/floris/type_dec.py index 33b4a2fe4..a346a689e 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -44,41 +44,57 @@ ### Custom callables for attrs objects and functions -def floris_numeric_or_float_converter(data: Any) -> Any: - # Convert numeric data to a float if not-iterable, or else - # apply the floris_array_converter - try: - iter(data) - except TypeError: - # Not iterable - try: - return float(data) - except TypeError as e: - raise TypeError(e.args[0] + f". Data given: {data}") - else: - # Iterable - return floris_array_converter(data) - def floris_array_converter(data: Iterable) -> np.ndarray: - # Verify that `data` is iterable. - # Note that for scalar quantities, np.array() creates a 0-dimensional array. + """ + For a given iterable, convert the data to a numpy array and cast to `floris_float_type`. + If the input is a scalar, np.array() creates a 0-dimensional array, and this is not supported + in FLORIS so this function raises an error. + + Args: + data (Iterable): The input data to be converted to a Numpy array. + + Raises: + TypeError: Raises if the input data is not iterable. + TypeError: Raises if the input data cannot be converted to a Numpy array. + + Returns: + np.ndarray: data converted to a Numpy array and cast to `floris_float_type`. + """ try: iter(data) except TypeError as e: raise TypeError(e.args[0] + f". Data given: {data}") - # Create a numpy array from the input data and cast to floris_float_type. try: a = np.array(data, dtype=floris_float_type) - except TypeError as e: + except (TypeError, ValueError) as e: raise TypeError(e.args[0] + f". Data given: {data}") return a def floris_numeric_dict_converter(data: dict) -> dict: - try: - return {k: floris_numeric_or_float_converter(v) for k, v in data.items()} - except TypeError as e: - raise TypeError(e.args[0] + f". Data given: {data}") + """ + For the given dictionary, convert all the values to a numeric type. If a value is a scalar, it + will be converted to a float. If a value is an iterable, it will be converted to a Numpy + array and cast to `floris_float_type`. If a value is not a numeric type, a TypeError will be + raised. + + Args: + data (dict): Dictionary of data to be converted to a numeric type. + + Returns: + dict: Dictionary with the same keys and all values converted to a numeric type. + """ + converted_dict = copy.deepcopy(data) # deepcopy -> data is a container and passed by reference + for k, v in data.items(): + try: + iter(v) + except TypeError: + # Not iterable so try to cast to float + converted_dict[k] = float(v) + else: + # Iterable so convert to Numpy array + converted_dict[k] = floris_array_converter(v) + return converted_dict # def array_field(**kwargs) -> Callable: # """ diff --git a/tests/type_dec_unit_test.py b/tests/type_dec_unit_test.py index 641f207dc..3c5b87ded 100644 --- a/tests/type_dec_unit_test.py +++ b/tests/type_dec_unit_test.py @@ -22,6 +22,7 @@ from floris.type_dec import ( convert_to_path, floris_array_converter, + floris_numeric_dict_converter, FromDictMixin, iter_validator, ) @@ -116,7 +117,7 @@ def test_iter_validator(): AttrsDemoClass(w=0, x=1, liststr=("a", "b")) -def test_attrs_array_converter(): +def test_array_converter(): array_input = [[1, 2, 3], [4.5, 6.3, 2.2]] test_array = np.array(array_input) @@ -124,10 +125,53 @@ def test_attrs_array_converter(): cls = AttrsDemoClass(w=0, x=1, array=array_input) np.testing.assert_allclose(test_array, cls.array) - # Test converstion on reset + # Test conversion on reset cls.array = array_input np.testing.assert_allclose(test_array, cls.array) + # Test that a non-iterable item like a scalar number fails + with pytest.raises(TypeError): + cls.array = 1 + + +def test_numeric_dict_converter(): + """ + This function converts data in a dictionary to a numeric type. + If it can't convert the data, it will raise a TypeError. + It should support scalar, list, and numpy array types + for values in the dictionary. + """ + test_dict = { + "scalar_string": "1", + "scalar_int": 1, + "scalar_float": 1.0, + "list_string": ["1", "2", "3"], + "list_int": [1, 2, 3], + "list_float": [1.0, 2.0, 3.0], + "array_string": np.array(["1", "2", "3"]), + "array_int": np.array([1, 2, 3]), + "array_float": np.array([1.0, 2.0, 3.0]), + } + numeric_dict = floris_numeric_dict_converter(test_dict) + assert numeric_dict["scalar_string"] == 1 + assert numeric_dict["scalar_int"] == 1 + assert numeric_dict["scalar_float"] == 1.0 + np.testing.assert_allclose(numeric_dict["list_string"], [1, 2, 3]) + np.testing.assert_allclose(numeric_dict["list_int"], [1, 2, 3]) + np.testing.assert_allclose(numeric_dict["list_float"], [1.0, 2.0, 3.0]) + np.testing.assert_allclose(numeric_dict["array_string"], [1, 2, 3]) + np.testing.assert_allclose(numeric_dict["array_int"], [1, 2, 3]) + np.testing.assert_allclose(numeric_dict["array_float"], [1.0, 2.0, 3.0]) + + test_dict = {"scalar_fail": "a"} + with pytest.raises(TypeError): + floris_numeric_dict_converter(test_dict) + test_dict = {"list_fail": ["a", "2", "3"]} + with pytest.raises(TypeError): + floris_numeric_dict_converter(test_dict) + test_dict = {"array_fail": np.array(["a", "2", "3"])} + with pytest.raises(TypeError): + floris_numeric_dict_converter(test_dict) def test_convert_to_path(): str_input = "../tests" From 5faeb1e25feae7c5b3ce3a27dbd4e763eeb697e2 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Fri, 2 Feb 2024 19:45:29 -0600 Subject: [PATCH 101/101] Add detail in comments --- floris/simulation/solver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/floris/simulation/solver.py b/floris/simulation/solver.py index 1cdf3049c..c80f355cc 100644 --- a/floris/simulation/solver.py +++ b/floris/simulation/solver.py @@ -76,12 +76,12 @@ def sequential_solver( v_wake = np.zeros_like(flow_field.v_initial_sorted) w_wake = np.zeros_like(flow_field.w_initial_sorted) - # Set up turbulence arrays + # Expand input turbulence intensity to 4d for (n_turbines, grid, grid) turbine_turbulence_intensity = flow_field.turbulence_intensities[:, None, None, None] turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity - # with extra dimension to reach 4d + # with dimensions expanded for (n_turbines, grid, grid) ambient_turbulence_intensities = flow_field.turbulence_intensities.copy() ambient_turbulence_intensities = ambient_turbulence_intensities[:, None, None, None]