Source code for waves.utilities.floris_runners

"""Provides the FLORIS-based methods for pre-processing data and post-processing results."""


from __future__ import annotations

import multiprocessing as mp

import numpy as np
import pandas as pd
from tqdm import tqdm
from floris.tools import FlorisInterface
from floris.tools.wind_rose import WindRose


# **************************************************************************************************
# Time series methods
# **************************************************************************************************


[docs] def run_chunked_time_series_floris( args: tuple, ) -> tuple[tuple[int, int], FlorisInterface, pd.DataFrame]: """Runs ``fi.calculate_wake()`` over a chunk of a larger time series analysis and returns the individual turbine powers for each corresponding time. Parameters ---------- fi : FlorisInterface A copy of the base ``FlorisInterface`` object. weather : pd.DataFrame A subset of the full weather profile, with only the datetime index and columns: "windspeed" and "wind_direction". chunk_id : tuple[int, int] A tuple of the year and month for the data being processed. reinit_kwargs : dict, optional Any additional reinitialization keyword arguments. Defaults to {}. run_kwargs : dict, optional Any additional calculate_wake keyword arguments. Defaults to {}. Returns ------- tuple[tuple[int, int], FlorisInterface, pd.DataFrame] The ``chunk_id``, a reinitialized ``fi`` using the appropriate wind parameters that can be used for further post-processing, and the resulting turbine powers. """ fi: FlorisInterface = args[0] weather: pd.DataFrame = args[1] chunk_id: tuple[int, int] = args[2] reinit_kwargs: dict = args[3] run_kwargs: dict = args[4] reinit_kwargs["wind_directions"] = weather.wind_direction.values reinit_kwargs["wind_speeds"] = weather.windspeed.values fi.reinitialize(time_series=True, **reinit_kwargs) fi.calculate_wake(**run_kwargs) power_df = pd.DataFrame(fi.get_turbine_powers()[:, 0, :], index=weather.index) return chunk_id, fi, power_df
[docs] def run_parallel_time_series_floris( args_list: list[tuple[FlorisInterface, pd.DataFrame, tuple[int, int], dict, dict]], nodes: int = -1, ) -> tuple[dict[tuple[int, int], FlorisInterface], pd.DataFrame]: """Runs the time series floris calculations in parallel. Parameters ---------- args_list : list[tuple[FlorisInterface, pd.DataFrame, tuple[int, int], dict, dict]]) A list of the chunked by month arguments that get passed to ``run_chunked_time_series_floris``. nodes : int, optional The number of nodes to parallelize over. If -1, then it will use the floor of 80% of the available CPUs on the computer. Defaults to -1. Returns ------- tuple[dict[tuple[int, int], FlorisInterface], pd.DataFrame] A dictionary of the ``chunk_id`` and ``FlorisInterface`` object, and the full turbine power dataframe (without renamed columns). """ nodes = int(mp.cpu_count() * 0.7) if nodes == -1 else nodes with mp.Pool(nodes) as pool: with tqdm(total=len(args_list), desc="Time series energy calculation") as pbar: df_list = [] fi_dict = {} for chunk_id, fi, df in pool.imap_unordered(run_chunked_time_series_floris, args_list): df_list.append(df) fi_dict[chunk_id] = fi pbar.update() fi_dict = dict(sorted(fi_dict.items())) turbine_power_df = pd.concat(df_list).sort_index() return fi_dict, turbine_power_df
# ************************************************************************************************** # Wind rose methods # **************************************************************************************************
[docs] def create_single_month_wind_rose(weather_df: pd.DataFrame, month: int) -> tuple[int, WindRose]: """Creates the FLORIS ``WindRose`` object for a given :py:attr:`month` based on the :py:attr:`weather_df`'s ``DatetimeIndex``. Parameters ---------- weather_df : pd.DataFrame The weather profile used to create long-term, month-based ``WindRose`` objects month : int The month of the year to create a ``WindRose`` object. Returns ------- tuple[int, WindRose] A tuple of the :py:attr:`month` passed and the final ``WindRose`` object. """ wd, ws = weather_df.loc[weather_df.index.month == month, ["wd", "ws"]].values.T wind_rose = WindRose() _ = wind_rose.make_wind_rose_from_user_data(wd, ws) return (month, wind_rose)
[docs] def create_monthly_wind_rose(weather_df: pd.DataFrame) -> dict[int, WindRose]: """Create a dictionary of month and a long-term ``WindRose`` object based on all the wind condition data for that month. Parameters ---------- weather_df : pd.DataFrame The weather profile used to create long-term, month-based ``WindRose`` objects month : int The month of the year to create a ``WindRose`` object. Returns ------- dict[int, WindRose] A dictionary of the integer month and the long-term ``WindRose`` object associated with all the wind conditions during that month. """ return dict(create_single_month_wind_rose(weather_df, month) for month in range(1, 13))
[docs] def check_monthly_wind_rose( project_wind_rose: WindRose, monthly_wind_rose: dict[int, WindRose] ) -> dict[int, WindRose]: """Checks the monthly wind rose parameterizations to ensure the DataFrames are the correct shape, so that when the frequency column is extracted, the compared data is the same. Parameters ---------- project_wind_rose : WindRose The ``WindRose`` created using the long term reanalysis weather profile. monthly_wind_rose : dict[int, WindRose] A dictionary of the month as an ``int`` and ``WindRose`` created from the long term project reanalysis weather profile that was filtered on weather data for the focal month. Returns ------- dict[int, WindRose] The :py:attr:`monthly_wind_rose` but with an missing wind conditions added into the ``WindRose`` with 0 frequency. """ project_df = project_wind_rose.df wr_combinations = list(map(tuple, project_df[["wd", "ws"]].values)) for month, wind_rose in monthly_wind_rose.items(): if wind_rose.df.shape == project_df.shape: continue # Find the missing combinations, add them to the wind rose DataFrame, and resort wr_df = wind_rose.df missing = set(wr_combinations).difference(list(map(tuple, wr_df[["wd", "ws"]].values))) missing_df = pd.DataFrame([], columns=wr_df.columns) missing_df[["wd", "ws"]] = list(missing) missing_df.freq_val = 0.0 wr_df = pd.concat([wr_df, missing_df]).sort_values(["wd", "ws"]) # Tidy up the WindRose object itself to ensure it can be used correctly # Note: taken from the WindRose.read_wind_rose_csv() method without renormalizing the # frequency data because we're only adding in missing values with 0 frequency wind_rose.df = wr_df # Call the resample function in order to set all the internal variables wind_rose.internal_resample_wind_speed(ws=wr_df.ws.unique()) wind_rose.internal_resample_wind_direction(wd=wr_df.wd.unique()) monthly_wind_rose[month] = wind_rose return monthly_wind_rose
[docs] def calculate_monthly_wind_rose_results( turbine_power: np.ndarray, freq_monthly: dict[int, np.ndarray], ) -> pd.DataFrame: """Calculate the turbine AEP contribution for each month of a year, in MWh. Parameters ---------- turbine_power : np.ndarray The array of turbine powers, with shape (num wd x num ws x num turbines), calculated from the possible wind conditions at the site given the turbine layout. freq_monthly : dict[int, np.ndarray] The dictionary of integer months (i.e., 1 for January) and array of frequences, with shape (num wd x num ws), created by the long term wind conditions filtered on the month. Returns ------- pd.DataFrame, pd.DataFrame A DataFrame of each month's contribution to the AEP for each turbine in the wind farm, with shape (12 x num turbines). """ month_day_map = pd.DataFrame( [ [1, 31], [2, 28], [3, 31], [4, 30], [5, 31], [6, 30], [7, 31], [8, 31], [9, 30], [10, 31], [11, 30], [12, 31], ], columns=["month", "n_days"], ).set_index("month") # Calculate the monthly turbine energy and sum over the turbines to get the farm energy turbine_energy = pd.DataFrame.from_dict( { m: np.sum( freq_monthly[m].reshape((*freq_monthly[m].shape, 1)) * turbine_power, axis=(0, 1) ) for m in freq_monthly }, orient="index", ).sort_index() turbine_energy *= month_day_map.n_days.values.reshape(12, 1) * 24 / 1e6 return turbine_energy