Source code for floris.floris_model


from __future__ import annotations

import copy
import inspect
from pathlib import Path
from typing import (
    Any,
    List,
    Optional,
)

import numpy as np
import pandas as pd

from floris.core import Core, State
from floris.core.rotor_velocity import average_velocity
from floris.core.turbine.operation_models import (
    POWER_SETPOINT_DEFAULT,
    POWER_SETPOINT_DISABLED,
)
from floris.core.turbine.turbine import (
    axial_induction,
    power,
    thrust_coefficient,
)
from floris.cut_plane import CutPlane
from floris.logging_manager import LoggingManager
from floris.type_dec import (
    floris_array_converter,
    NDArrayBool,
    NDArrayFloat,
    NDArrayStr,
)
from floris.utilities import (
    nested_get,
    nested_set,
    print_nested_dict,
)
from floris.wind_data import (
    TimeSeries,
    WindDataBase,
    WindRose,
    WindRoseWRG,
    WindTIRose,
)


[docs] class FlorisModel(LoggingManager): """ FlorisModel provides a high-level user interface to many of the underlying methods within the FLORIS framework. It is meant to act as a single entry-point for the majority of users, simplifying the calls to methods on objects within FLORIS. Args: configuration (:py:obj:`dict`): The Floris configuration dictionary or YAML file. The configuration should have the following inputs specified. - **flow_field**: See `floris.simulation.flow_field.FlowField` for more details. - **farm**: See `floris.simulation.farm.Farm` for more details. - **turbine**: See `floris.simulation.turbine.Turbine` for more details. - **wake**: See `floris.simulation.wake.WakeManager` for more details. - **logging**: See `floris.simulation.core.Core` for more details. """ def __init__(self, configuration: dict | str | Path): self.configuration = configuration if isinstance(self.configuration, (str, Path)): try: self.core = Core.from_file(self.configuration) except FileNotFoundError: # If the file cannot be found, then attempt the configuration path relative to the # file location from which FlorisModel was attempted to be run. If successful, # update self.configuration to an absolute, working file path and name. base_fn = Path(inspect.stack()[-1].filename).resolve().parent config = (base_fn / self.configuration).resolve() self.core = Core.from_file(config) self.configuration = config elif isinstance(self.configuration, dict): self.core = Core.from_dict(self.configuration) else: raise TypeError("The Floris `configuration` must be of type 'dict', 'str', or 'Path'.") # If ref height is -1, assign the hub height if np.abs(self.core.flow_field.reference_wind_height + 1.0) < 1.0e-6: self.assign_hub_height_to_ref_height() # Make a check on reference height and provide a helpful warning unique_heights = np.unique(np.round(self.core.farm.hub_heights, decimals=6)) if (( len(unique_heights) == 1) and (np.abs(self.core.flow_field.reference_wind_height - unique_heights[0]) > 1.0e-6 )): err_msg = ( "The only unique hub-height is not equal to the specified reference " "wind height. If this was unintended use -1 as the reference hub height to " "indicate use of hub-height as reference wind height." ) self.logger.warning(err_msg, stack_info=True) # Check the turbine_grid_points is reasonable if self.core.solver["type"] == "turbine_grid": if self.core.solver["turbine_grid_points"] > 3: self.logger.error( f"turbine_grid_points value is {self.core.solver['turbine_grid_points']} " "which is larger than the recommended value of less than or equal to 3. " "High amounts of turbine grid points reduce the computational performance " "but have a small change on accuracy." ) raise ValueError("turbine_grid_points must be less than or equal to 3.") # Initialize stored wind_data object to None self._wind_data = None ### Methods for setting and running the FlorisModel def _reinitialize( self, wind_speeds: list[float] | NDArrayFloat | None = None, wind_directions: list[float] | NDArrayFloat | None = None, wind_shear: float | None = None, wind_veer: float | None = None, reference_wind_height: float | None = None, turbulence_intensities: list[float] | NDArrayFloat | None = None, air_density: float | None = None, layout_x: list[float] | NDArrayFloat | None = None, layout_y: list[float] | NDArrayFloat | None = None, turbine_type: list | None = None, turbine_library_path: str | Path | None = None, solver_settings: dict | None = None, heterogeneous_inflow_config=None, wind_data: type[WindDataBase] | None = None, ): """ Instantiate a new Floris object with updated conditions set by arguments. Any parameters in Floris that aren't changed by arguments to this function retain their values. Note that, although it's name is similar to the reinitialize() method from Floris v3, this function is not meant to be called directly by the user---users should instead call the set() method. Args: wind_speeds (NDArrayFloat | list[float] | None, optional): Wind speeds at each findex. Defaults to None. wind_directions (NDArrayFloat | list[float] | None, optional): Wind directions at each findex. Defaults to None. wind_shear (float | None, optional): Wind shear exponent. Defaults to None. wind_veer (float | None, optional): Wind veer. Defaults to None. reference_wind_height (float | None, optional): Reference wind height. Defaults to None. turbulence_intensities (NDArrayFloat | list[float] | None, optional): Turbulence intensities at each findex. Defaults to None. air_density (float | None, optional): Air density. Defaults to None. layout_x (NDArrayFloat | list[float] | None, optional): X-coordinates of the turbines. Defaults to None. layout_y (NDArrayFloat | list[float] | None, optional): Y-coordinates of the turbines. Defaults to None. turbine_type (list | None, optional): Turbine type. Defaults to None. turbine_library_path (str | Path | None, optional): Path to the turbine library. Defaults to None. solver_settings (dict | None, optional): Solver settings. Defaults to None. heterogeneous_inflow_config (None, optional): heterogeneous inflow configuration. Defaults to None. wind_data (type[WindDataBase] | None, optional): Wind data. Defaults to None. """ # Export the floris object recursively as a dictionary floris_dict = self.core.as_dict() flow_field_dict = floris_dict["flow_field"] farm_dict = floris_dict["farm"] ## Farm if layout_x is not None: farm_dict["layout_x"] = layout_x if layout_y is not None: farm_dict["layout_y"] = layout_y if turbine_type is not None: farm_dict["turbine_type"] = turbine_type if turbine_library_path is not None: farm_dict["turbine_library_path"] = turbine_library_path ## If layout is changed and self._wind_data is not None, update the layout in wind_data if (layout_x is not None) or (layout_y is not None): if self._wind_data is not None: self._wind_data.set_layout(farm_dict["layout_x"], farm_dict["layout_y"]) # Wind data if ( (wind_directions is not None) or (wind_speeds is not None) or (turbulence_intensities is not None) or (heterogeneous_inflow_config is not None) ): if wind_data is not None: raise ValueError( "If wind_data is passed to reinitialize, then do not pass wind_directions, " "wind_speeds, turbulence_intensities or " "heterogeneous_inflow_config as this is redundant" ) elif self.wind_data is not None: self.logger.warning("Deleting stored wind_data information.") self._wind_data = None if wind_data is not None: # Set the wind data to the current layout wind_data.set_layout(farm_dict["layout_x"], farm_dict["layout_y"]) # Unpack wind data for reinitialization and save wind_data for use in output ( wind_directions, wind_speeds, turbulence_intensities, heterogeneous_inflow_config, ) = wind_data.unpack_for_reinitialize() self._wind_data = wind_data ## FlowField if wind_speeds is not None: flow_field_dict["wind_speeds"] = wind_speeds if wind_directions is not None: flow_field_dict["wind_directions"] = wind_directions if wind_shear is not None: flow_field_dict["wind_shear"] = wind_shear if wind_veer is not None: 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_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 heterogeneous_inflow_config is not None: if ( "z" in heterogeneous_inflow_config and flow_field_dict["wind_shear"] != 0.0 and heterogeneous_inflow_config['z'] is not None ): raise ValueError( "Heterogeneous inflow configuration contains a z term, and " "flow_field_dict['wind_shear'] is not 0.0. Combining both options " "is not currently allowed in FLORIS. If using a z term in the " " heterogeneous inflow configuration, set flow_field_dict['wind_shear'] " "to 0.0." ) flow_field_dict["heterogeneous_inflow_config"] = heterogeneous_inflow_config if solver_settings is not None: floris_dict["solver"] = solver_settings floris_dict["flow_field"] = flow_field_dict floris_dict["farm"] = farm_dict # Create a new instance of floris and attach to self self.core = Core.from_dict(floris_dict)
[docs] def set_operation( self, yaw_angles: NDArrayFloat | list[float] | None = None, power_setpoints: NDArrayFloat | list[float] | list[float, None] | None = None, awc_modes: NDArrayStr | list[str] | list[str, None] | None = None, awc_amplitudes: NDArrayFloat | list[float] | list[float, None] | None = None, awc_frequencies: NDArrayFloat | list[float] | list[float, None] | None = None, disable_turbines: NDArrayBool | list[bool] | None = None, ): """ Apply operating setpoints to the floris object. This function is not meant to be called directly by most users---users should instead call the set() method. Args: yaw_angles (NDArrayFloat | list[float] | None, optional): Turbine yaw angles. Defaults to None. power_setpoints (NDArrayFloat | list[float] | list[float, None] | None, optional): Turbine power setpoints. Defaults to None. disable_turbines (NDArrayBool | list[bool] | None, optional): Boolean array on whether to disable turbines. Defaults to None. """ # Add operating conditions to the floris object if yaw_angles is not None: if np.array(yaw_angles).shape[1] != self.core.farm.n_turbines: raise ValueError( f"yaw_angles has a size of {np.array(yaw_angles).shape[1]} in the 1st " f"dimension, must be equal to n_turbines={self.core.farm.n_turbines}" ) self.core.farm.set_yaw_angles(yaw_angles) if power_setpoints is not None: if np.array(power_setpoints).shape[1] != self.core.farm.n_turbines: raise ValueError( f"power_setpoints has a size of {np.array(power_setpoints).shape[1]} in the 1st" f" dimension, must be equal to n_turbines={self.core.farm.n_turbines}" ) power_setpoints = np.array(power_setpoints) # Convert any None values to the default power setpoint power_setpoints[ power_setpoints == np.full(power_setpoints.shape, None) ] = POWER_SETPOINT_DEFAULT power_setpoints = floris_array_converter(power_setpoints) self.core.farm.set_power_setpoints(power_setpoints) if awc_modes is None: awc_modes = np.array( [["baseline"] *self.core.farm.n_turbines] *self.core.flow_field.n_findex ) self.core.farm.awc_modes = awc_modes if awc_amplitudes is None: awc_amplitudes = np.zeros( ( self.core.flow_field.n_findex, self.core.farm.n_turbines, ) ) self.core.farm.awc_amplitudes = awc_amplitudes if awc_frequencies is None: awc_frequencies = np.zeros( ( self.core.flow_field.n_findex, self.core.farm.n_turbines, ) ) self.core.farm.awc_frequencies = awc_frequencies # Check for turbines to disable if disable_turbines is not None: # Force to numpy array disable_turbines = np.array(disable_turbines) # Must have first dimension = n_findex if disable_turbines.shape[0] != self.core.flow_field.n_findex: raise ValueError( f"disable_turbines has a size of {disable_turbines.shape[0]} " f"in the 0th dimension, must be equal to " f"n_findex={self.core.flow_field.n_findex}" ) # Must have first dimension = n_turbines if disable_turbines.shape[1] != self.core.farm.n_turbines: raise ValueError( f"disable_turbines has a size of {disable_turbines.shape[1]} " f"in the 1th dimension, must be equal to " f"n_turbines={self.core.farm.n_turbines}" ) # Set power setpoints to small value (non zero to avoid numerical issues) and # yaw_angles to 0 in all locations where disable_turbines is True self.core.farm.yaw_angles[disable_turbines] = 0.0 self.core.farm.power_setpoints[disable_turbines] = POWER_SETPOINT_DISABLED if any([yaw_angles is not None, power_setpoints is not None, disable_turbines is not None]): self.core.state = State.UNINITIALIZED
[docs] def set( self, wind_speeds: list[float] | NDArrayFloat | None = None, wind_directions: list[float] | NDArrayFloat | None = None, wind_shear: float | None = None, wind_veer: float | None = None, reference_wind_height: float | None = None, turbulence_intensities: list[float] | NDArrayFloat | None = None, air_density: float | None = None, layout_x: list[float] | NDArrayFloat | None = None, layout_y: list[float] | NDArrayFloat | None = None, turbine_type: list | None = None, turbine_library_path: str | Path | None = None, solver_settings: dict | None = None, heterogeneous_inflow_config=None, wind_data: type[WindDataBase] | None = None, yaw_angles: NDArrayFloat | list[float] | None = None, power_setpoints: NDArrayFloat | list[float] | list[float, None] | None = None, awc_modes: NDArrayStr | list[str] | list[str, None] | None = None, awc_amplitudes: NDArrayFloat | list[float] | list[float, None] | None = None, awc_frequencies: NDArrayFloat | list[float] | list[float, None] | None = None, disable_turbines: NDArrayBool | list[bool] | None = None, ): """ Set the wind conditions and operation setpoints for the wind farm. Args: wind_speeds (NDArrayFloat | list[float] | None, optional): Wind speeds at each findex. Defaults to None. wind_directions (NDArrayFloat | list[float] | None, optional): Wind directions at each findex. Defaults to None. wind_shear (float | None, optional): Wind shear exponent. Defaults to None. wind_veer (float | None, optional): Wind veer. Defaults to None. reference_wind_height (float | None, optional): Reference wind height. Defaults to None. turbulence_intensities (NDArrayFloat | list[float] | None, optional): Turbulence intensities at each findex. Defaults to None. air_density (float | None, optional): Air density. Defaults to None. layout_x (NDArrayFloat | list[float] | None, optional): X-coordinates of the turbines. Defaults to None. layout_y (NDArrayFloat | list[float] | None, optional): Y-coordinates of the turbines. Defaults to None. turbine_type (list | None, optional): Turbine type. Defaults to None. turbine_library_path (str | Path | None, optional): Path to the turbine library. Defaults to None. solver_settings (dict | None, optional): Solver settings. Defaults to None. heterogeneous_inflow_config (None, optional): heterogeneous inflow configuration. Defaults to None. wind_data (type[WindDataBase] | None, optional): Wind data. Defaults to None. yaw_angles (NDArrayFloat | list[float] | None, optional): Turbine yaw angles. Defaults to None. power_setpoints (NDArrayFloat | list[float] | list[float, None] | None, optional): Turbine power setpoints. disable_turbines (NDArrayBool | list[bool] | None, optional): NDArray with dimensions n_findex x n_turbines. True values indicate the turbine is disabled at that findex and the power setpoint at that position is set to 0. Defaults to None. """ # Initialize a new Floris object after saving the setpoints _yaw_angles = self.core.farm.yaw_angles _power_setpoints = self.core.farm.power_setpoints _awc_modes = self.core.farm.awc_modes _awc_amplitudes = self.core.farm.awc_amplitudes _awc_frequencies = self.core.farm.awc_frequencies self._reinitialize( wind_speeds=wind_speeds, wind_directions=wind_directions, wind_shear=wind_shear, wind_veer=wind_veer, reference_wind_height=reference_wind_height, turbulence_intensities=turbulence_intensities, air_density=air_density, layout_x=layout_x, layout_y=layout_y, turbine_type=turbine_type, turbine_library_path=turbine_library_path, solver_settings=solver_settings, heterogeneous_inflow_config=heterogeneous_inflow_config, wind_data=wind_data, ) # If the yaw angles or power setpoints are not the default, set them back to the # previous setting if not (_yaw_angles == 0).all(): self.core.farm.set_yaw_angles(_yaw_angles) if not ( (_power_setpoints == POWER_SETPOINT_DEFAULT) | (_power_setpoints == POWER_SETPOINT_DISABLED) ).all(): self.core.farm.set_power_setpoints(_power_setpoints) if _awc_modes is not None: self.core.farm.set_awc_modes(_awc_modes) if not (_awc_amplitudes == 0).all(): self.core.farm.set_awc_amplitudes(_awc_amplitudes) if not (_awc_frequencies == 0).all(): self.core.farm.set_awc_frequencies(_awc_frequencies) # Set the operation self.set_operation( yaw_angles=yaw_angles, power_setpoints=power_setpoints, awc_modes=awc_modes, awc_amplitudes=awc_amplitudes, awc_frequencies=awc_frequencies, disable_turbines=disable_turbines, )
[docs] def reset_operation(self): """ Instantiate a new Floris object to set all operation setpoints to their default values. """ self._reinitialize()
[docs] def run(self) -> None: """ Run the FLORIS solve to compute the velocity field and wake effects. """ # Initialize solution space self.core.initialize_domain() # Perform the wake calculations self.core.steady_state_atmospheric_condition()
[docs] def run_no_wake(self) -> None: """ This function is similar to `run()` except that it does not apply a wake model. That is, the wind farm is modeled as if there is no wake in the flow. Operation settings may reduce the power and thrust of the turbine to where they're applied. """ # Initialize solution space self.core.initialize_domain() # Finalize values to user-supplied order self.core.finalize()
### Methods for extracting turbine performance after running def _get_turbine_powers(self) -> NDArrayFloat: """Calculates the power at each turbine in the wind farm. Returns: NDArrayFloat: Powers at each turbine. """ # Confirm calculate wake has been run if self.core.state is not State.USED: raise RuntimeError( "Can't compute turbine powers without first running `FlorisModel.run()`." ) # Check for negative velocities, which could indicate bad model # parameters or turbines very closely spaced. if (self.core.flow_field.u < 0.0).any(): self.logger.warning("Some velocities at the rotor are negative.") turbine_powers = power( velocities=self.core.flow_field.u, turbulence_intensities=self.core.flow_field.turbulence_intensity_field[:,:,None,None], air_density=self.core.flow_field.air_density, power_functions=self.core.farm.turbine_power_functions, yaw_angles=self.core.farm.yaw_angles, tilt_angles=self.core.farm.tilt_angles, power_setpoints=self.core.farm.power_setpoints, awc_modes = self.core.farm.awc_modes, awc_amplitudes=self.core.farm.awc_amplitudes, tilt_interps=self.core.farm.turbine_tilt_interps, turbine_type_map=self.core.farm.turbine_type_map, turbine_power_thrust_tables=self.core.farm.turbine_power_thrust_tables, correct_cp_ct_for_tilt=self.core.farm.correct_cp_ct_for_tilt, multidim_condition=self.core.flow_field.multidim_conditions, ) return turbine_powers
[docs] def get_turbine_powers(self): """ Calculates the power at each turbine in the wind farm. Returns: NDArrayFloat: Powers at each turbine. """ turbine_powers = self._get_turbine_powers() if self.wind_data is not None: if isinstance(self.wind_data, (WindRose, WindRoseWRG)): turbine_powers_rose = np.full( (len(self.wind_data.wd_flat), self.core.farm.n_turbines), np.nan ) turbine_powers_rose[self.wind_data.non_zero_freq_mask, :] = turbine_powers turbine_powers = turbine_powers_rose.reshape( len(self.wind_data.wind_directions), len(self.wind_data.wind_speeds), self.core.farm.n_turbines ) elif type(self.wind_data) is WindTIRose: turbine_powers_rose = np.full( (len(self.wind_data.wd_flat), self.core.farm.n_turbines), np.nan ) turbine_powers_rose[self.wind_data.non_zero_freq_mask, :] = turbine_powers turbine_powers = turbine_powers_rose.reshape( len(self.wind_data.wind_directions), len(self.wind_data.wind_speeds), len(self.wind_data.turbulence_intensities), self.core.farm.n_turbines ) return turbine_powers
[docs] def get_expected_turbine_powers(self, freq=None): """ Compute the expected (mean) power of each turbine. Args: freq (NDArrayFloat): NumPy array with shape with the frequencies of each wind direction and wind speed combination. freq is either a 1D array, in which case the same frequencies are used for all turbines, or a 2D array with shape equal to (n_findex, n_turbines), in which case each turbine has a unique set of frequencies (this is the case for example using WindRoseByTurbine). These frequencies should typically sum across rows up to 1.0 and are used to weigh the wind farm power for every condition in calculating the wind farm's AEP. Defaults to None. If None and a WindData object was supplied, the WindData object's frequencies will be used. Otherwise, uniform frequencies are assumed (i.e., a simple mean over the findices is computed). """ turbine_powers = self._get_turbine_powers() if freq is None: if self.wind_data is None: freq = np.array([1.0/self.core.flow_field.n_findex]) else: freq = self.wind_data.unpack_freq() # If freq is 2d, then use the per turbine frequencies if len(np.shape(freq)) == 2: return np.nansum(np.multiply(freq, turbine_powers), axis=0) else: return np.nansum(np.multiply(freq.reshape(-1, 1), turbine_powers), axis=0)
def _get_weighted_turbine_powers( self, turbine_weights=None, use_turbulence_correction=False, ): if use_turbulence_correction: raise NotImplementedError( "Turbulence correction is not yet implemented in the power calculation." ) # Confirm run() has been run if self.core.state is not State.USED: raise RuntimeError( f"Can't run function `{self.__class__.__name__}.get_farm_power` without " f"first running `{self.__class__.__name__}.run`." ) if turbine_weights is None: # Default to equal weighing of all turbines when turbine_weights is None turbine_weights = np.ones( ( self.core.flow_field.n_findex, self.core.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.core.flow_field.n_findex, 1), ) # Calculate all turbine powers and apply weights turbine_powers = self._get_turbine_powers() turbine_powers = np.multiply(turbine_weights, turbine_powers) return turbine_powers def _get_farm_power( self, turbine_weights=None, use_turbulence_correction=False, ): """ Report wind plant power from instance of floris. Optionally includes uncertainty in wind direction and yaw position when determining power. Uncertainty is included by computing the mean wind farm power for a distribution of wind direction and yaw position deviations from the original wind direction and yaw angles. Args: 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. use_turbulence_correction: (bool, optional): When True uses a turbulence parameter to adjust power output calculations. Defaults to False. Not currently implemented. Returns: float: Sum of wind turbine powers in W. """ turbine_powers = self._get_weighted_turbine_powers( turbine_weights=turbine_weights, use_turbulence_correction=use_turbulence_correction ) return np.sum(turbine_powers, axis=1)
[docs] def get_farm_power( self, turbine_weights=None, use_turbulence_correction=False, ): """ Report wind plant power from instance of floris. Optionally includes uncertainty in wind direction and yaw position when determining power. Uncertainty is included by computing the mean wind farm power for a distribution of wind direction and yaw position deviations from the original wind direction and yaw angles. Args: 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. use_turbulence_correction: (bool, optional): When True uses a turbulence parameter to adjust power output calculations. Defaults to False. Not currently implemented. Returns: float: Sum of wind turbine powers in W. """ farm_power = self._get_farm_power(turbine_weights, use_turbulence_correction) if self.wind_data is not None: if isinstance(self.wind_data, (WindRose, WindRoseWRG)): farm_power_rose = np.full(len(self.wind_data.wd_flat), np.nan) farm_power_rose[self.wind_data.non_zero_freq_mask] = farm_power farm_power = farm_power_rose.reshape( len(self.wind_data.wind_directions), len(self.wind_data.wind_speeds) ) elif type(self.wind_data) is WindTIRose: farm_power_rose = np.full(len(self.wind_data.wd_flat), np.nan) farm_power_rose[self.wind_data.non_zero_freq_mask] = farm_power farm_power = farm_power_rose.reshape( len(self.wind_data.wind_directions), len(self.wind_data.wind_speeds), len(self.wind_data.turbulence_intensities) ) return farm_power
[docs] def get_expected_farm_power( self, freq=None, turbine_weights=None, ) -> float: """ Compute the expected (mean) power of the wind farm. 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. Defaults to None. If None and a WindData object was supplied, the WindData object's frequencies will be used. Otherwise, uniform frequencies are assumed (i.e., a simple mean over the findices is computed). 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. """ if freq is None: if self.wind_data is None: freq = np.array([1.0/self.core.flow_field.n_findex]) else: freq = self.wind_data.unpack_freq() # If freq is 1d if len(np.shape(freq)) == 1: farm_power = self._get_farm_power(turbine_weights=turbine_weights) return np.nansum(np.multiply(freq, farm_power)) else: weighted_turbine_powers = self._get_weighted_turbine_powers( turbine_weights=turbine_weights, ) return np.nansum(np.multiply(freq, weighted_turbine_powers))
[docs] def get_farm_AEP( self, freq=None, turbine_weights=None, hours_per_year=8760, ) -> 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. Defaults to None. If None and a WindData object was supplied, the WindData object's frequencies will be used. Otherwise, uniform frequencies are assumed. 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. hours_per_year (float, optional): Number of hours in a year. Defaults to 365 * 24. Returns: float: The Annual Energy Production (AEP) for the wind farm in watt-hours. """ if freq is None and not isinstance(self.wind_data, (WindRose, WindRoseWRG, WindTIRose)): self.logger.warning( "Computing AEP with uniform frequencies. Results results may not reflect annual " "operation." ) return self.get_expected_farm_power( freq=freq, turbine_weights=turbine_weights ) * hours_per_year
[docs] def get_expected_farm_value( self, freq=None, values=None, turbine_weights=None, ) -> float: """ Compute the expected (mean) value produced by the wind farm. This is computed by multiplying the wind farm power for each wind condition by the corresponding value of the power generated (e.g., electricity market price per unit of energy), then weighting by frequency and summing over all conditions. Args: freq (NDArrayFloat): NumPy array with shape (n_findex) with the frequencies of each wind condition combination. These frequencies should typically sum up to 1.0 and are used to weigh the wind farm value for every condition in calculating the wind farm's expected value. Defaults to None. If None and a WindData object is supplied, the WindData object's frequencies will be used. Otherwise, uniform frequencies are assumed (i.e., a simple mean over the findices is computed). values (NDArrayFloat): NumPy array with shape (n_findex) with the values corresponding to the power generated for each wind condition combination. The wind farm power is multiplied by the value for every condition in calculating the wind farm's expected value. Defaults to None. If None and a WindData object is supplied, the WindData object's values will be used. Otherwise, a value of 1 for all conditions is assumed (i.e., the expected farm value will be equivalent to the expected farm power). 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 value 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 expected value. If None, this is an array with all values 1.0 and with shape equal to (n_findex, n_turbines). Defaults to None. Returns: float: The expected value produced by the wind farm in units of value. """ if freq is None: if self.wind_data is None: freq = np.array([1.0/self.core.flow_field.n_findex]) else: freq = self.wind_data.unpack_freq() # If freq is 1d if len(np.shape(freq)) == 1: farm_power = self._get_farm_power(turbine_weights=turbine_weights) farm_power = np.multiply(freq, farm_power) else: weighted_turbine_powers = self._get_weighted_turbine_powers( turbine_weights=turbine_weights ) farm_power = np.nansum(np.multiply(freq, weighted_turbine_powers), axis=1) if values is None: if self.wind_data is None: values = np.array([1.0]) else: values = self.wind_data.unpack_value() return np.nansum(np.multiply(values, farm_power))
[docs] def get_farm_AVP( self, freq=None, values=None, turbine_weights=None, hours_per_year=8760, ) -> float: """ Estimate annual value production (AVP) for distribution of wind conditions, frequencies of occurrence, and corresponding values of power generated (e.g., electricity price per unit of energy). Args: freq (NDArrayFloat): NumPy array with shape (n_findex) with the frequencies of each wind condition combination. These frequencies should typically sum up to 1.0 and are used to weigh the wind farm value for every condition in calculating the wind farm's AVP. Defaults to None. If None and a WindData object is supplied, the WindData object's frequencies will be used. Otherwise, uniform frequencies are assumed (i.e., a simple mean over the findices is computed). values (NDArrayFloat): NumPy array with shape (n_findex) with the values corresponding to the power generated for each wind condition combination. The wind farm power is multiplied by the value for every condition in calculating the wind farm's AVP. Defaults to None. If None and a WindData object is supplied, the WindData object's values will be used. Otherwise, a value of 1 for all conditions is assumed (i.e., the AVP will be equivalent to the AEP). 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 value 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 AVP. If None, this is an array with all values 1.0 and with shape equal to (n_findex, n_turbines). Defaults to None. hours_per_year (float, optional): Number of hours in a year. Defaults to 365 * 24. Returns: float: The Annual Value Production (AVP) for the wind farm in units of value. """ if ( freq is None and not isinstance(self.wind_data, WindRose) and not isinstance(self.wind_data, WindRoseWRG) and not isinstance(self.wind_data, WindTIRose) ): self.logger.warning( "Computing AVP with uniform frequencies. Results results may not reflect annual " "operation." ) if values is None and self.wind_data is None: self.logger.warning( "Computing AVP with uniform value equal to 1. Results will be equivalent to " "annual energy production." ) return self.get_expected_farm_value( freq=freq, values=values, turbine_weights=turbine_weights ) * hours_per_year
[docs] def get_turbine_ais(self) -> NDArrayFloat: turbine_ais = axial_induction( velocities=self.core.flow_field.u, turbulence_intensities=self.core.flow_field.turbulence_intensity_field[:,:,None,None], air_density=self.core.flow_field.air_density, yaw_angles=self.core.farm.yaw_angles, tilt_angles=self.core.farm.tilt_angles, power_setpoints=self.core.farm.power_setpoints, awc_modes = self.core.farm.awc_modes, awc_amplitudes=self.core.farm.awc_amplitudes, axial_induction_functions=self.core.farm.turbine_axial_induction_functions, tilt_interps=self.core.farm.turbine_tilt_interps, correct_cp_ct_for_tilt=self.core.farm.correct_cp_ct_for_tilt, turbine_type_map=self.core.farm.turbine_type_map, turbine_power_thrust_tables=self.core.farm.turbine_power_thrust_tables, average_method=self.core.grid.average_method, cubature_weights=self.core.grid.cubature_weights, multidim_condition=self.core.flow_field.multidim_conditions, ) return turbine_ais
[docs] def get_turbine_thrust_coefficients(self) -> NDArrayFloat: turbine_thrust_coefficients = thrust_coefficient( velocities=self.core.flow_field.u, turbulence_intensities=self.core.flow_field.turbulence_intensity_field[:,:,None,None], air_density=self.core.flow_field.air_density, yaw_angles=self.core.farm.yaw_angles, tilt_angles=self.core.farm.tilt_angles, power_setpoints=self.core.farm.power_setpoints, awc_modes = self.core.farm.awc_modes, awc_amplitudes=self.core.farm.awc_amplitudes, thrust_coefficient_functions=self.core.farm.turbine_thrust_coefficient_functions, tilt_interps=self.core.farm.turbine_tilt_interps, correct_cp_ct_for_tilt=self.core.farm.correct_cp_ct_for_tilt, turbine_type_map=self.core.farm.turbine_type_map, turbine_power_thrust_tables=self.core.farm.turbine_power_thrust_tables, average_method=self.core.grid.average_method, cubature_weights=self.core.grid.cubature_weights, multidim_condition=self.core.flow_field.multidim_conditions, ) return turbine_thrust_coefficients
[docs] def get_turbine_TIs(self) -> NDArrayFloat: return self.core.flow_field.turbulence_intensity_field
### Methods for sampling and visualization
[docs] def set_for_viz(self, findex: int, solver_settings: dict) -> None: """ Set the floris object to a single findex for visualization. Args: findex (int): The findex to set the floris object to. solver_settings (dict): The solver settings to use for visualization. """ # If not None, set the heterogeneous inflow configuration if self.core.flow_field.heterogeneous_inflow_config is not None: heterogeneous_inflow_config = { 'x': self.core.flow_field.heterogeneous_inflow_config['x'], 'y': self.core.flow_field.heterogeneous_inflow_config['y'], 'speed_multipliers': self.core.flow_field.heterogeneous_inflow_config['speed_multipliers'][findex:findex+1], } if 'z' in self.core.flow_field.heterogeneous_inflow_config: heterogeneous_inflow_config['z'] = ( self.core.flow_field.heterogeneous_inflow_config['z'] ) else: heterogeneous_inflow_config = None self.set( wind_speeds=self.wind_speeds[findex:findex+1], wind_directions=self.wind_directions[findex:findex+1], turbulence_intensities=self.turbulence_intensities[findex:findex+1], yaw_angles=self.core.farm.yaw_angles[findex:findex+1,:], power_setpoints=self.core.farm.power_setpoints[findex:findex+1,:], awc_modes=self.core.farm.awc_modes[findex:findex+1,:], awc_amplitudes=self.core.farm.awc_amplitudes[findex:findex+1,:], heterogeneous_inflow_config = heterogeneous_inflow_config, solver_settings=solver_settings, )
[docs] def calculate_cross_plane( self, downstream_dist, y_resolution=200, z_resolution=200, y_bounds=None, z_bounds=None, findex_for_viz=None, ): """ Shortcut method to instantiate a :py:class:`~.tools.cut_plane.CutPlane` object containing the velocity field in a horizontal plane cut through the simulation domain at a specific height. Args: downstream_dist (float): Distance downstream of turbines to compute. y_resolution (float, optional): Output array resolution. Defaults to 200 points. z_resolution (float, optional): Output array resolution. Defaults to 200 points. y_bounds (tuple, optional): Limits of output array (in m). Defaults to None. z_bounds (tuple, optional): Limits of output array (in m). Defaults to None. finder_for_viz (int, optional): Index of the condition to visualize. Returns: :py:class:`~.tools.cut_plane.CutPlane`: containing values of x, y, u, v, w """ if self.n_findex > 1 and findex_for_viz is None: self.logger.warning( "Multiple findices detected. Using first findex for visualization." ) if findex_for_viz is None: findex_for_viz = 0 # Store the current state for reinitialization fmodel_viz = copy.deepcopy(self) # Set the solver to a flow field planar grid solver_settings = { "type": "flow_field_planar_grid", "normal_vector": "x", "planar_coordinate": downstream_dist, "flow_field_grid_points": [y_resolution, z_resolution], "flow_field_bounds": [y_bounds, z_bounds], } fmodel_viz.set_for_viz(findex_for_viz, solver_settings) # Calculate wake fmodel_viz.core.solve_for_viz() # Get the points of data in a dataframe # TODO this just seems to be flattening and storing the data in a df; is this necessary? # It seems the biggest dependency is on CutPlane and the subsequent visualization tools. df = fmodel_viz.get_plane_of_points( normal_vector="x", planar_coordinate=downstream_dist, ) # Compute the cutplane cross_plane = CutPlane(df, y_resolution, z_resolution, "x") return cross_plane
[docs] def calculate_horizontal_plane( self, height, x_resolution=200, y_resolution=200, x_bounds=None, y_bounds=None, findex_for_viz=None, ): """ Shortcut method to instantiate a :py:class:`~.tools.cut_plane.CutPlane` object containing the velocity field in a horizontal plane cut through the simulation domain at a specific height. Args: height (float): Height of cut plane. Defaults to Hub-height. x_resolution (float, optional): Output array resolution. Defaults to 200 points. y_resolution (float, optional): Output array resolution. Defaults to 200 points. x_bounds (tuple, optional): Limits of output array (in m). Defaults to None. y_bounds (tuple, optional): Limits of output array (in m). Defaults to None. finder_for_viz (int, optional): Index of the condition to visualize. Returns: :py:class:`~.tools.cut_plane.CutPlane`: containing values of x, y, u, v, w """ if self.n_findex > 1 and findex_for_viz is None: self.logger.warning( "Multiple findices detected. Using first findex for visualization." ) if findex_for_viz is None: findex_for_viz = 0 # Store the current state for reinitialization fmodel_viz = copy.deepcopy(self) # Set the solver to a flow field planar grid solver_settings = { "type": "flow_field_planar_grid", "normal_vector": "z", "planar_coordinate": height, "flow_field_grid_points": [x_resolution, y_resolution], "flow_field_bounds": [x_bounds, y_bounds], } fmodel_viz.set_for_viz(findex_for_viz, solver_settings) # Calculate wake fmodel_viz.core.solve_for_viz() # Get the points of data in a dataframe # TODO this just seems to be flattening and storing the data in a df; is this necessary? # It seems the biggest depenedcy is on CutPlane and the subsequent visualization tools. df = fmodel_viz.get_plane_of_points( normal_vector="z", planar_coordinate=height, ) # Compute the cutplane horizontal_plane = CutPlane( df, fmodel_viz.core.grid.grid_resolution[0], fmodel_viz.core.grid.grid_resolution[1], "z", ) return horizontal_plane
[docs] def calculate_y_plane( self, crossstream_dist, x_resolution=200, z_resolution=200, x_bounds=None, z_bounds=None, findex_for_viz=None, ): """ Shortcut method to instantiate a :py:class:`~.tools.cut_plane.CutPlane` object containing the velocity field in a horizontal plane cut through the simulation domain at a specific height. Args: height (float): Height of cut plane. Defaults to Hub-height. x_resolution (float, optional): Output array resolution. Defaults to 200 points. z_resolution (float, optional): Output array resolution. Defaults to 200 points. x_bounds (tuple, optional): Limits of output array (in m). Defaults to None. z_bounds (tuple, optional): Limits of output array (in m). Defaults to None. findex_for_viz (int, optional): Index of the condition to visualize. Defaults to 0. Returns: :py:class:`~.tools.cut_plane.CutPlane`: containing values of x, y, u, v, w """ if self.n_findex > 1 and findex_for_viz is None: self.logger.warning( "Multiple findices detected. Using first findex for visualization." ) if findex_for_viz is None: findex_for_viz = 0 # Store the current state for reinitialization fmodel_viz = copy.deepcopy(self) # Set the solver to a flow field planar grid solver_settings = { "type": "flow_field_planar_grid", "normal_vector": "y", "planar_coordinate": crossstream_dist, "flow_field_grid_points": [x_resolution, z_resolution], "flow_field_bounds": [x_bounds, z_bounds], } fmodel_viz.set_for_viz(findex_for_viz, solver_settings) # Calculate wake fmodel_viz.core.solve_for_viz() # Get the points of data in a dataframe # TODO this just seems to be flattening and storing the data in a df; is this necessary? # It seems the biggest depenedcy is on CutPlane and the subsequent visualization tools. df = fmodel_viz.get_plane_of_points( normal_vector="y", planar_coordinate=crossstream_dist, ) # Compute the cutplane y_plane = CutPlane(df, x_resolution, z_resolution, "y") return y_plane
[docs] def get_plane_of_points( self, normal_vector="z", planar_coordinate=None, ): """ Calculates velocity values through the :py:meth:`FlorisModel.calculate_wake` method at points in plane specified by inputs. Args: normal_vector (string, optional): Vector normal to plane. Defaults to z. planar_coordinate (float, optional): Value of normal vector to slice through. Defaults to None. Returns: :py:class:`pandas.DataFrame`: containing values of x1, x2, x3, u, v, w """ # Get results vectors if normal_vector == "z": x_flat = self.core.grid.x_sorted_inertial_frame[0].flatten() y_flat = self.core.grid.y_sorted_inertial_frame[0].flatten() z_flat = self.core.grid.z_sorted_inertial_frame[0].flatten() else: x_flat = self.core.grid.x_sorted[0].flatten() y_flat = self.core.grid.y_sorted[0].flatten() z_flat = self.core.grid.z_sorted[0].flatten() u_flat = self.core.flow_field.u_sorted[0].flatten() v_flat = self.core.flow_field.v_sorted[0].flatten() w_flat = self.core.flow_field.w_sorted[0].flatten() # Create a df of these if normal_vector == "z": df = pd.DataFrame( { "x1": x_flat, "x2": y_flat, "x3": z_flat, "u": u_flat, "v": v_flat, "w": w_flat, } ) if normal_vector == "x": df = pd.DataFrame( { "x1": y_flat, "x2": z_flat, "x3": x_flat, "u": u_flat, "v": v_flat, "w": w_flat, } ) if normal_vector == "y": df = pd.DataFrame( { "x1": x_flat, "x2": z_flat, "x3": y_flat, "u": u_flat, "v": v_flat, "w": w_flat, } ) # Subset to plane # TODO: Seems sloppy as need more than one plane in the z-direction for GCH if planar_coordinate is not None: df = df[np.isclose(df.x3, planar_coordinate)] # , atol=0.1, rtol=0.0)] # Drop duplicates # TODO is this still needed now that we setup a grid for just this plane? df = df.drop_duplicates() # Sort values of df to make sure plotting is acceptable df = df.sort_values(["x2", "x1"]).reset_index(drop=True) return df
[docs] def sample_flow_at_points(self, x: NDArrayFloat, y: NDArrayFloat, z: NDArrayFloat): """ Extract the wind speed at points in the flow. Args: x (1DArrayFloat | list): x-locations of points where flow is desired. y (1DArrayFloat | list): y-locations of points where flow is desired. z (1DArrayFloat | list): z-locations of points where flow is desired. Returns: 3DArrayFloat containing wind speed with dimensions (# of findex, # of sample points) """ # Check that x, y, z are all the same length if not len(x) == len(y) == len(z): raise ValueError("x, y, and z must be the same size") return self.core.solve_for_points(x, y, z)
[docs] 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, ) -> list[pd.DataFrame]: """ Extract velocity deficit profiles at a set of downstream distances from a starting point (usually a turbine location). For each downstream distance, a profile is sampled along a line in either the cross-stream direction (x2) or the vertical direction (x3). Velocity deficit is here defined as (homogeneous_wind_speed - u)/homogeneous_wind_speed, where u is the wake velocity obtained when wind_shear = 0.0. Args: direction: At each downstream location, this is the direction in which to sample the profile. Either `cross-stream` or `vertical`. downstream_dists: A list/array of streamwise locations for where to sample the profiles. Default starting point is (0.0, 0.0, reference_height). profile_range: Determines the extent of the line along which the profiles are sampled. The range is defined about a point which lies some distance directly downstream of the starting point. resolution: Number of sample points in each profile. wind_direction: A single wind direction. homogeneous_wind_speed: A single wind speed. It is called homogeneous since 'wind_shear' is temporarily set to 0.0 in this method. ref_rotor_diameter: A reference rotor diameter which is used to normalize the coordinates. x_start: x-coordinate of starting point. y_start: y-coordinate of starting point. reference_height: If `direction` is cross-stream, then `reference_height` defines the height of the horizontal plane in which the velocity profiles are sampled. If `direction` is vertical, then the velocity is sampled along the vertical direction with the `profile_range` being relative to the `reference_height`. Returns: A list of pandas DataFrame objects where each DataFrame represents one velocity deficit profile. """ if direction not in ["cross-stream", "vertical"]: raise ValueError("`direction` must be either `cross-stream` or `vertical`.") if ref_rotor_diameter is None: unique_rotor_diameters = np.unique(self.core.farm.rotor_diameters) if len(unique_rotor_diameters) == 1: ref_rotor_diameter = unique_rotor_diameters[0] else: raise ValueError( "Please provide a `ref_rotor_diameter`. This is needed to normalize the " "coordinates. Could not select a value automatically since the number of " "unique rotor diameters in the turbine layout is not 1. " f"Found the following rotor diameters: {unique_rotor_diameters}." ) if downstream_dists is None: downstream_dists = ref_rotor_diameter * np.array([3, 5, 7, 9]) if profile_range is None: profile_range = ref_rotor_diameter * np.array([-2, 2]) wind_directions_copy = np.array(self.core.flow_field.wind_directions, copy=True) wind_speeds_copy = np.array(self.core.flow_field.wind_speeds, copy=True) wind_shear_copy = self.core.flow_field.wind_shear if wind_direction is None: if len(wind_directions_copy) == 1: wind_direction = wind_directions_copy[0] else: raise ValueError( "Could not determine a wind direction for which to sample the velocity " "profiles. Either provide a single `wind_direction` as an argument to this " "method, or initialize the Floris object with a single wind direction." ) if homogeneous_wind_speed is None: if len(wind_speeds_copy) == 1: homogeneous_wind_speed = wind_speeds_copy[0] self.logger.warning( "`homogeneous_wind_speed` not provided. Setting it to the following wind speed " f"found in the current flow field: {wind_speeds_copy[0]} m/s. Note that the " "inflow is always homogeneous when calculating the velocity deficit profiles. " "This is done by temporarily setting `wind_shear` to 0.0" ) else: raise ValueError( "Could not determine a wind speed for which to sample the velocity " "profiles. Provide a single `homogeneous_wind_speed` to this method." ) if reference_height is None: reference_height = self.core.flow_field.reference_wind_height self.set( wind_directions=[wind_direction], wind_speeds=[homogeneous_wind_speed], wind_shear=0.0, ) velocity_deficit_profiles = self.core.solve_for_velocity_deficit_profiles( direction, downstream_dists, profile_range, resolution, homogeneous_wind_speed, ref_rotor_diameter, x_start, y_start, reference_height, ) self.set( wind_directions=wind_directions_copy, wind_speeds=wind_speeds_copy, wind_shear=wind_shear_copy, ) return velocity_deficit_profiles
### Utility methods
[docs] def assign_hub_height_to_ref_height(self): # Confirm can do this operation unique_heights = np.unique(self.core.farm.hub_heights) if len(unique_heights) > 1: raise ValueError( "To assign hub heights to reference height, can not have more than one " "specified height. " f"Current length is {unique_heights}." ) self.core.flow_field.reference_wind_height = unique_heights[0]
[docs] def get_operation_model(self) -> str: """Get the operation model of a FlorisModel. Returns: str: The operation_model. """ operation_models = [ self.core.farm.turbine_definitions[tindex]["operation_model"] for tindex in range(self.core.farm.n_turbines) ] if len(set(operation_models)) == 1: return operation_models[0] else: return operation_models
[docs] def set_operation_model(self, operation_model: str | List[str]): """Set the turbine operation model(s). Args: operation_model (str): The operation model to set. """ if isinstance(operation_model, str): if len(self.core.farm.turbine_type) == 1: # Set a single one here, then, and return turbine_type = self.core.farm.turbine_definitions[0] turbine_type["operation_model"] = operation_model self.set(turbine_type=[turbine_type]) return else: operation_model = [operation_model]*self.core.farm.n_turbines if len(operation_model) != self.core.farm.n_turbines: raise ValueError( "The length of the operation_model list must be " "equal to the number of turbines." ) turbine_type_list = self.core.farm.turbine_definitions for tindex in range(self.core.farm.n_turbines): turbine_type_list[tindex]["turbine_type"] = ( turbine_type_list[tindex]["turbine_type"]+"_"+operation_model[tindex] ) turbine_type_list[tindex]["operation_model"] = operation_model[tindex] self.set(turbine_type=turbine_type_list)
[docs] def copy(self): """Create an independent copy of the current FlorisModel object""" return FlorisModel(self.core.as_dict())
[docs] def get_param( self, param: List[str], param_idx: Optional[int] = None ) -> Any: """Get a parameter from a FlorisModel object. Args: param (List[str]): A list of keys to traverse the FlorisModel dictionary. param_idx (Optional[int], optional): The index to get the value at. Defaults to None. If None, the entire parameter is returned. Returns: Any: The value of the parameter. """ fm_dict = self.core.as_dict() if param_idx is None: return nested_get(fm_dict, param) else: return nested_get(fm_dict, param)[param_idx]
[docs] def set_param( self, param: List[str], value: Any, param_idx: Optional[int] = None ): """Set a parameter in a FlorisModel object. Args: param (List[str]): A list of keys to traverse the FlorisModel dictionary. value (Any): The value to set. param_idx (Optional[int], optional): The index to set the value at. Defaults to None. """ fm_dict_mod = self.core.as_dict() nested_set(fm_dict_mod, param, value, param_idx) self.__init__(fm_dict_mod)
[docs] def get_turbine_layout(self, z=False): """ Get turbine layout Args: z (bool): When *True*, return lists of x, y, and z coords, otherwise, return x and y only. Defaults to *False*. Returns: np.array: lists of x, y, and (optionally) z coordinates of each turbine """ xcoords, ycoords, zcoords = self.core.farm.coordinates.T if z: return xcoords, ycoords, zcoords else: return xcoords, ycoords
[docs] def print_dict(self) -> None: """Print the FlorisModel dictionary. """ print_nested_dict(self.core.as_dict())
### Properties @property def layout_x(self): """ Wind turbine coordinate information. Returns: np.array: Wind turbine x-coordinate. """ return self.core.farm.layout_x @property def layout_y(self): """ Wind turbine coordinate information. Returns: np.array: Wind turbine y-coordinate. """ return self.core.farm.layout_y @property def wind_directions(self): """ Wind direction information. Returns: np.array: Wind direction. """ return self.core.flow_field.wind_directions @property def wind_speeds(self): """ Wind speed information. Returns: np.array: Wind speed. """ return self.core.flow_field.wind_speeds @property def turbulence_intensities(self): """ Turbulence intensity information. Returns: np.array: Turbulence intensity. """ return self.core.flow_field.turbulence_intensities @property def n_findex(self): """ Number of floris indices (findex). Returns: int: Number of flow indices. """ return self.core.flow_field.n_findex @property def n_turbines(self): """ Number of turbines. Returns: int: Number of turbines. """ return self.core.farm.n_turbines @property def turbine_average_velocities(self) -> NDArrayFloat: return average_velocity( velocities=self.core.flow_field.u, method=self.core.grid.average_method, cubature_weights=self.core.grid.cubature_weights, ) @property def wind_data(self): return self._wind_data ### v3 functions that are removed - raise an error if used
[docs] def calculate_wake(self, **_): raise NotImplementedError( "The calculate_wake method has been removed. Please use the run method. " "See https://nrel.github.io/floris/v3_to_v4.html for more information." )
[docs] def reinitialize(self, **_): raise NotImplementedError( "The reinitialize method has been removed. Please use the set method. " "See https://nrel.github.io/floris/v3_to_v4.html for more information." )
[docs] @staticmethod def merge_floris_models(fmodel_list, reference_wind_height=None): """Merge a list of FlorisModel objects into a single FlorisModel object. Note that it uses the very first object specified in fmodel_list to build upon, so it uses those wake model parameters, air density, and so on. Args: fmodel_list (list): Array-like of FlorisModel objects. reference_wind_height (float, optional): Height in meters at which the reference wind speed is assigned. If None, will assume this value is equal to the reference wind height specified in the FlorisModel objects. This only works if all objects have the same value for their reference_wind_height. Returns: fmodel_merged (FlorisModel): The merged FlorisModel object, merged in the same order as fmodel_list. The objects are merged on the turbine locations and turbine types, but not on the wake parameters or general solver settings. """ if not isinstance(fmodel_list[0], FlorisModel): raise ValueError( "Incompatible input specified. fmodel_list must be a list of FlorisModel objects." ) # Get the turbine locations and specifications for each subset and save as a list x_list = [] y_list = [] turbine_type_list = [] reference_wind_heights = [] for fmodel in fmodel_list: # Remove any control setpoints that might be specified for the turbines on one fmodel fmodel.reset_operation() x_list.extend(fmodel.layout_x) y_list.extend(fmodel.layout_y) fmodel_turbine_type = fmodel.core.farm.turbine_type if len(fmodel_turbine_type) == 1: fmodel_turbine_type = fmodel_turbine_type * len(fmodel.layout_x) elif not len(fmodel_turbine_type) == len(fmodel.layout_x): raise ValueError("Incompatible format of turbine_type in fmodel.") turbine_type_list.extend(fmodel_turbine_type) reference_wind_heights.append(fmodel.core.flow_field.reference_wind_height) # Derive reference wind height, if unspecified by the user if reference_wind_height is None: reference_wind_height = np.mean(reference_wind_heights) if np.any(np.abs(np.array(reference_wind_heights) - reference_wind_height) > 1.0e-3): raise ValueError( "Cannot automatically derive a fitting reference_wind_height since they " "substantially differ between FlorisModel objects. " "Please specify 'reference_wind_height' manually." ) # Construct the merged FLORIS model based on the first entry in fmodel_list fmodel_merged = fmodel_list[0].copy() fmodel_merged.set( layout_x=x_list, layout_y=y_list, turbine_type=turbine_type_list, reference_wind_height=reference_wind_height, ) return fmodel_merged