Skip to content
Draft
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
9 changes: 7 additions & 2 deletions floris/core/turbine/turbine.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
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,
ControllerDependentTurbine,
Expand Down Expand Up @@ -620,7 +620,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
14 changes: 11 additions & 3 deletions floris/floris_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,7 +1009,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 +1030,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 @@ -1578,7 +1585,7 @@ def set_operation_model(self, operation_model: str | List[str]):
Args:
operation_model (str): 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 +1604,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
111 changes: 111 additions & 0 deletions tests/turbine_operation_models_integration_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import numpy as np
import pytest
from attrs import define, field

from floris import FlorisModel
from floris.core.turbine import BaseOperationModel
from floris.type_dec import floris_float_type


# Establish a static class
@define
class UserDefinedStatic(BaseOperationModel):
def power(velocities, **_):
return 1000*np.ones(velocities.shape[:2])
def thrust_coefficient(velocities, **_):
return 0.8*np.ones(velocities.shape[:2])
def axial_induction(velocities, **_):
return 1/3*np.ones(velocities.shape[:2])

# Establish a dynamic class
@define
class UserDefinedDynamic(BaseOperationModel):
flat_power: floris_float_type = field(init=True, default=500.0)
flat_thrust_coefficient: floris_float_type = field(init=True, default=0.7)
flat_axial_induction: floris_float_type = field(init=True, default=0.3)
def power(self, velocities, **_):
return self.flat_power*np.ones(velocities.shape[:2])
def thrust_coefficient(self, velocities, **_):
return self.flat_thrust_coefficient*np.ones(velocities.shape[:2])
def axial_induction(self, velocities, **_):
return self.flat_axial_induction*np.ones(velocities.shape[:2])


def test_static_user_defined_op_model():
fmodel = FlorisModel("defaults")
fmodel.set(
layout_x=[0.0, 500.0, 1000.0],
layout_y=[0.0, 0.0, 0.0],
wind_speeds=[8.0, 9.0],
wind_directions=[270.0, 280.0],
turbulence_intensities=[0.06, 0.06]
)
fmodel.set_operation_model(UserDefinedStatic)
fmodel.run()
power = fmodel.get_turbine_powers()
thrust_coefficients = fmodel.get_turbine_thrust_coefficients()
axial_inductions = fmodel.get_turbine_axial_induction_factors()

assert np.all(power.shape == (2, 3))
assert np.all(thrust_coefficients.shape == (2, 3))
assert np.all(axial_inductions.shape == (2, 3))

assert np.allclose(power, 1000.0)
assert np.allclose(thrust_coefficients, 0.8)
assert np.allclose(axial_inductions, 1/3)

def test_dynamic_user_defined_op_model():

fmodel = FlorisModel("defaults")
fmodel.set(
layout_x=[0.0, 500.0, 1000.0],
layout_y=[0.0, 0.0, 0.0],
wind_speeds=[8.0, 9.0],
wind_directions=[270.0, 280.0],
turbulence_intensities=[0.06, 0.06]
)
# Try without instantiating (TODO: create more helpful error?)
with pytest.raises(TypeError):
fmodel.set_operation_model(UserDefinedDynamic)
fmodel.run()
# Now instantiate and try again
instantiated_operation_model = UserDefinedDynamic()
fmodel.set_operation_model(instantiated_operation_model)
fmodel.run()
power = fmodel.get_turbine_powers()
thrust_coefficients = fmodel.get_turbine_thrust_coefficients()
axial_inductions = fmodel.get_turbine_axial_induction_factors()

assert np.all(power.shape == (2, 3))
assert np.all(thrust_coefficients.shape == (2, 3))
assert np.all(axial_inductions.shape == (2, 3))

assert np.allclose(power, 500.0)
assert np.allclose(thrust_coefficients, 0.7)
assert np.allclose(axial_inductions, 0.3)

def test_set_run_ordering():
fmodel = FlorisModel("defaults")
fmodel.set_operation_model(UserDefinedStatic)
fmodel.set(
layout_x=[0.0, 500.0, 1000.0],
layout_y=[0.0, 0.0, 0.0],
wind_speeds=[8.0, 9.0],
wind_directions=[270.0, 280.0],
turbulence_intensities=[0.06, 0.06]
)
fmodel.run()

# Reset, rerun
fmodel.set(
wind_directions=[300.0, 310.0],
)
fmodel.run()

# Now, try a dynamic model
fmodel.set_operation_model(UserDefinedDynamic(flat_power=850.0))
fmodel.run()
fmodel.set(
wind_directions=[240.0, 250.0],
)
fmodel.run()