Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
394 changes: 394 additions & 0 deletions docs/user_defined_operation_models.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion floris/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

import floris.logging_manager

from .base import BaseClass, BaseModel, State
from .base import BaseClass, BaseLibrary, BaseModel, State
from .turbine.turbine import (
axial_induction,
power,
Expand Down
31 changes: 31 additions & 0 deletions floris/core/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

import importlib
from abc import abstractmethod
from enum import Enum
from typing import (
Expand Down Expand Up @@ -63,3 +64,33 @@ def prepare_function() -> dict:
@abstractmethod
def function() -> None:
raise NotImplementedError("BaseModel.function")

@define
class BaseLibrary(BaseClass):
"""
Base class that writes the name and module of the class into the attrs dictionary.
"""
__classinfo__: dict = {"module": "", "name": ""}
def __attrs_post_init__(self) -> None:
#import ipdb; ipdb.set_trace()
self.__classinfo__ = {
"module": type(self).__module__,
"name": type(self).__name__
}

@staticmethod
def from_dict(data_dict):
"""Recreate instance from dictionary with class information"""
if "__classinfo__" not in data_dict:
raise ValueError(
"Dictionary does not contain class information. ",
"Insure inheritance from BaseLibrary."
)
data_noinfo = data_dict.copy()
class_info = data_noinfo.pop("__classinfo__")

# Import the module and get the class
module = importlib.import_module(class_info["module"])
cls = getattr(module, class_info["name"])

return cls(**data_noinfo)
4 changes: 2 additions & 2 deletions floris/core/turbine/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

from floris.core.turbine.controller_dependent_operation_model import ControllerDependentTurbine
from floris.core.turbine.operation_model_base import BaseOperationModel
from floris.core.turbine.operation_models import (
AWCTurbine,
CosineLossTurbine,
Expand All @@ -8,4 +7,5 @@
SimpleDeratingTurbine,
SimpleTurbine,
)
from floris.core.turbine.controller_dependent_operation_model import ControllerDependentTurbine
from floris.core.turbine.unified_momentum_model import UnifiedMomentumModelTurbine
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
compute_tilt_angles_for_floating_turbines,
rotor_velocity_air_density_correction,
)
from floris.core.turbine.operation_models import BaseOperationModel
from floris.core.turbine import BaseOperationModel
from floris.type_dec import (
NDArrayFloat,
NDArrayObject,
Expand Down
38 changes: 38 additions & 0 deletions floris/core/turbine/operation_model_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from abc import abstractmethod

from attrs import define

from floris.core import BaseLibrary


@define
class BaseOperationModel(BaseLibrary):
"""
Base class for turbine operation models. All turbine operation models must implement static
power(), thrust_coefficient(), and axial_induction() methods, which are called by power() and
thrust_coefficient() through the interface in the turbine.py module.

Args:
BaseClass (_type_): _description_

Raises:
NotImplementedError: _description_
NotImplementedError: _description_
"""
@staticmethod
@abstractmethod
def power() -> None:
raise NotImplementedError("BaseOperationModel.power")

@staticmethod
@abstractmethod
def thrust_coefficient() -> None:
raise NotImplementedError("BaseOperationModel.thrust_coefficient")

@staticmethod
@abstractmethod
def axial_induction() -> None:
# TODO: Consider whether we can make a generic axial_induction method
# based purely on thrust_coefficient so that we don't need to implement
# axial_induction() in individual operation models.
raise NotImplementedError("BaseOperationModel.axial_induction")
34 changes: 1 addition & 33 deletions floris/core/turbine/operation_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
rotor_velocity_tilt_cosine_correction,
rotor_velocity_yaw_cosine_correction,
)
from floris.core.turbine import BaseOperationModel
from floris.type_dec import (
NDArrayFloat,
NDArrayObject,
Expand All @@ -28,39 +29,6 @@
POWER_SETPOINT_DEFAULT = 1e12
POWER_SETPOINT_DISABLED = 0.001


@define
class BaseOperationModel(BaseClass):
"""
Base class for turbine operation models. All turbine operation models must implement static
power(), thrust_coefficient(), and axial_induction() methods, which are called by power() and
thrust_coefficient() through the interface in the turbine.py module.

Args:
BaseClass (_type_): _description_

Raises:
NotImplementedError: _description_
NotImplementedError: _description_
"""
@staticmethod
@abstractmethod
def power() -> None:
raise NotImplementedError("BaseOperationModel.power")

@staticmethod
@abstractmethod
def thrust_coefficient() -> None:
raise NotImplementedError("BaseOperationModel.thrust_coefficient")

@staticmethod
@abstractmethod
def axial_induction() -> None:
# TODO: Consider whether we can make a generic axial_induction method
# based purely on thrust_coefficient so that we don't need to implement
# axial_induciton() in individual operation models.
raise NotImplementedError("BaseOperationModel.axial_induction")

@define
class SimpleTurbine(BaseOperationModel):
"""
Expand Down
15 changes: 12 additions & 3 deletions floris/core/turbine/turbine.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
from attrs import define, field
from scipy.interpolate import interp1d

from floris.core import BaseClass
from floris.core import BaseClass, BaseLibrary
from floris.core.turbine import (
AWCTurbine,
BaseOperationModel,
ControllerDependentTurbine,
CosineLossTurbine,
MixedOperationTurbine,
Expand Down Expand Up @@ -534,6 +535,9 @@ class Turbine(BaseClass):
defined.
ref_tilt (float): The implicit tilt of the turbine for which the Cp and Ct
curves are defined. This is typically the nacelle tilt.
operation_model (str | BaseOperationModel): The turbine operation model to use for this
turbine. This can be given as a string corresponding to one of the provided operation
models, or a custom operation model defined as a subclass of BaseOperationModel.
correct_cp_ct_for_tilt (bool): A flag to indicate whether to correct Cp and Ct for tilt
usually for a floating turbine.
Optional, defaults to False.
Expand All @@ -552,7 +556,7 @@ class Turbine(BaseClass):
hub_height: float = field()
TSR: float = field()
power_thrust_table: dict = field(default={}) # conversion to numpy in __post_init__
operation_model: str = field(default="cosine-loss")
operation_model: str | BaseOperationModel = field(default="cosine-loss")

correct_cp_ct_for_tilt: bool = field(default=False)
floating_tilt_table: dict[str, NDArrayFloat] | None = field(default=None)
Expand Down Expand Up @@ -620,7 +624,12 @@ def __post_init__(self) -> None:
self.power_thrust_table = floris_numeric_dict_converter(self.power_thrust_table)

def _initialize_power_thrust_functions(self) -> None:
turbine_function_model = TURBINE_MODEL_MAP["operation_model"][self.operation_model]
if isinstance(self.operation_model, str):
turbine_function_model = TURBINE_MODEL_MAP["operation_model"][self.operation_model]
elif isinstance(self.operation_model, dict):
turbine_function_model = BaseLibrary.from_dict(self.operation_model)
else:
turbine_function_model = self.operation_model
self.thrust_coefficient_function = turbine_function_model.thrust_coefficient
self.axial_induction_function = turbine_function_model.axial_induction
self.power_function = turbine_function_model.power
Expand Down
2 changes: 1 addition & 1 deletion floris/core/turbine/unified_momentum_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
average_velocity,
rotor_velocity_air_density_correction,
)
from floris.core.turbine.operation_models import BaseOperationModel
from floris.core.turbine import BaseOperationModel
from floris.type_dec import NDArrayFloat


Expand Down
27 changes: 22 additions & 5 deletions floris/floris_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from floris.core import Core, State
from floris.core.rotor_velocity import average_velocity
from floris.core.turbine import BaseOperationModel
from floris.core.turbine.operation_models import (
POWER_SETPOINT_DEFAULT,
POWER_SETPOINT_DISABLED,
Expand Down Expand Up @@ -1009,7 +1010,7 @@ def get_farm_AVP(
turbine_weights=turbine_weights
) * hours_per_year

def get_turbine_ais(self) -> NDArrayFloat:
def get_turbine_axial_induction_factors(self) -> NDArrayFloat:
turbine_ais = axial_induction(
velocities=self.core.flow_field.u,
turbulence_intensities=self.core.flow_field.turbulence_intensity_field[:,:,None,None],
Expand All @@ -1030,6 +1031,13 @@ def get_turbine_ais(self) -> NDArrayFloat:
)
return turbine_ais

def get_turbine_ais(self) -> NDArrayFloat:
self.logger.warning(
"Computing axial inductions with get_turbine_ais is now deprecated. Please use"
" the more explicit get_turbine_axial_induction_factors method instead."
)
return self.get_turbine_axial_induction_factors()

def get_turbine_thrust_coefficients(self) -> NDArrayFloat:
turbine_thrust_coefficients = thrust_coefficient(
velocities=self.core.flow_field.u,
Expand Down Expand Up @@ -1572,13 +1580,21 @@ def get_operation_model(self) -> str:
else:
return operation_models

def set_operation_model(self, operation_model: str | List[str]):
def set_operation_model(
self,
operation_model: str | List[str] | BaseOperationModel | List[BaseOperationModel]
):
"""Set the turbine operation model(s).

Can be provided either as a string representing one of the built-in operation
models, or as a custom operation model object that inherits from
:py:class:`~.turbine_operation.BaseOperationModel`. Also, a list of operation
models can be provided to set different operation models for each turbine.

Args:
operation_model (str): The operation model to set.
operation_model (str, BaseOperationModel, list): The operation model to set.
"""
if isinstance(operation_model, str):
if (not isinstance(operation_model, (list, np.ndarray))):
if len(self.core.farm.turbine_type) == 1:
# Set a single one here, then, and return
turbine_type = self.core.farm.turbine_definitions[0]
Expand All @@ -1597,11 +1613,12 @@ def set_operation_model(self, operation_model: str | List[str]):
"equal to the number of turbines."
)

# Proceed to update turbine definitions
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]["turbine_type"]+"_"+str(operation_model[tindex])
)
turbine_type_list[tindex]["operation_model"] = operation_model[tindex]

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ lines_after_imports = 2
line_length = 100
order_by_type = false
split_on_trailing_comma = true
skip = ["floris/core/turbine/__init__.py"]

# length_sort = true
# case_sensitive: False
Expand Down
Loading