Skip to content

Commit

Permalink
Improve attrs usage in simulation package (NREL#750)
Browse files Browse the repository at this point in the history
* Use attrs class for low-level classes

* Add missing attribute declarations

These were monkey patched, but slotted classes do not allow monkey patching. See the previous commit for the change that enabled slotted classes.

* Support class parameters for attrs

* Fix linting

* Swap logger inheritance for composition

This allows for the floris.simulation package to use attrs and slotted classes while the floris.tools package can remain typical Python classes.
The LoggerBase did have @define in order to maintain the slotted classes, but that is removed here.

* Add missing attribute declarations

* Initialize BaseClass with LoggingManager

* Remove unused imports

* Raise an error instead of assertion

* Use as_dict to get model parameters
  • Loading branch information
rafmudaf authored Dec 6, 2023
1 parent 599f226 commit 52b8a5f
Show file tree
Hide file tree
Showing 21 changed files with 115 additions and 83 deletions.
6 changes: 3 additions & 3 deletions floris/logging_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,10 @@ def filter(self, record):
return True


class LoggerBase:
class LoggingManager:
"""
Convenience super-class to any class requiring access to the logging
module. The virtual property here allows a simple and dynamic method
This class provide an easy access to the global logger.
The virtual property here allows a simple and dynamic method
for obtaining the correct logger for the calling class.
"""

Expand Down
57 changes: 27 additions & 30 deletions floris/simulation/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,22 @@
Defines the BaseClass parent class for all models to be based upon.
"""

from abc import ABC, abstractmethod
from abc import abstractmethod
from enum import Enum
from typing import (
Any,
Dict,
Final,
)

import attrs
from attrs import (
Attribute,
define,
field,
fields,
)

from floris.logging_manager import LoggerBase
from floris.logging_manager import LoggingManager
from floris.type_dec import FromDictMixin


Expand All @@ -37,44 +42,36 @@ class State(Enum):
USED = 2


class BaseClass(LoggerBase, FromDictMixin):
@define
class BaseClass(FromDictMixin):
"""
BaseClass object class. This class does the logging and MixIn class inheritance.
"""

state = State.UNINITIALIZED


@classmethod
def get_model_defaults(cls) -> Dict[str, Any]:
"""Produces a dictionary of the keyword arguments and their defaults.
Returns
-------
Dict[str, Any]
Dictionary of keyword argument: default.
"""
return {el.name: el.default for el in attrs.fields(cls)}
# Initialize `state` and ensure it is treated as an attribute rather than a constant parameter.
# See https://www.attrs.org/en/stable/api-attr.html#attr.ib
state = field(init=False, default=State.UNINITIALIZED)
_logging_manager: LoggingManager = field(init=False, default=LoggingManager())

def _get_model_dict(self) -> dict:
"""Convenience method that wraps the `attrs.asdict` method. Returns the object's
parameters as a dictionary.
@property
def logger(self):
"""Returns the logger manager object."""
return self._logging_manager.logger

Returns
-------
dict
The provided or default, if no input provided, model settings as a dictionary.
"""
return attrs.asdict(self)


class BaseModel(BaseClass, ABC):
@define
class BaseModel(BaseClass):
"""
BaseModel is the generic class for any wake models. It defines the API required to
create a valid model.
"""

NUM_EPS: Final[float] = 0.001 # This is a numerical epsilon to prevent divide by zeros
# This is a numerical epsilon to prevent divide by zeros
NUM_EPS: Final[float] = field(init=False, default=0.001)

@NUM_EPS.validator
def lock_num_eps(self, attribute: Attribute, value: Any) -> None:
if value != 0.001:
raise ValueError("NUM_EPS should remain a fixed value. Don't change this!")

@abstractmethod
def prepare_function() -> dict:
Expand Down
21 changes: 18 additions & 3 deletions floris/simulation/farm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@

import copy
from pathlib import Path
from typing import Any, List
from typing import (
Any,
Dict,
List,
)

import attrs
import numpy as np
from attrs import define, field
from scipy.interpolate import interp1d

from floris.simulation import (
BaseClass,
Expand Down Expand Up @@ -76,7 +81,10 @@ class Farm(BaseClass):

turbine_definitions: list = field(init=False, validator=iter_validator(list, dict))
coordinates: List[Vec3] = field(init=False)
turbine_fCts: tuple = field(init=False, default=[])

turbine_fCts: Dict[str, interp1d] | List[interp1d] = field(init=False, default=[])
turbine_fCts_sorted: NDArrayFloat = field(init=False, default=[])

turbine_fTilts: list = field(init=False, default=[])

yaw_angles: NDArrayFloat = field(init=False)
Expand All @@ -88,9 +96,14 @@ class Farm(BaseClass):
hub_heights: NDArrayFloat = field(init=False)
hub_heights_sorted: NDArrayFloat = field(init=False, default=[])

turbine_map: List[Turbine | TurbineMultiDimensional] = field(init=False, default=[])

turbine_type_map: NDArrayObject = field(init=False, default=[])
turbine_type_map_sorted: NDArrayObject = field(init=False, default=[])

turbine_power_interps: Dict[str, interp1d] | List[interp1d] = field(init=False, default=[])
turbine_power_interps_sorted: NDArrayFloat = field(init=False, default=[])

rotor_diameters: NDArrayFloat = field(init=False, default=[])
rotor_diameters_sorted: NDArrayFloat = field(init=False, default=[])

Expand All @@ -103,6 +116,9 @@ class Farm(BaseClass):
pTs: NDArrayFloat = field(init=False, default=[])
pTs_sorted: NDArrayFloat = field(init=False, default=[])

ref_density_cp_cts: NDArrayFloat = field(init=False, default=[])
ref_density_cp_cts_sorted: NDArrayFloat = field(init=False, default=[])

ref_tilt_cp_cts: NDArrayFloat = field(init=False, default=[])
ref_tilt_cp_cts_sorted: NDArrayFloat = field(init=False, default=[])

Expand Down Expand Up @@ -456,7 +472,6 @@ def finalize(self, unsorted_indices):
unsorted_indices[:,:,:,0,0],
axis=2
)
# TODO: do these need to be unsorted? Maybe we should just for completeness...
self.ref_density_cp_cts = np.take_along_axis(
self.ref_density_cp_cts_sorted,
unsorted_indices[:,:,:,0,0],
Expand Down
2 changes: 1 addition & 1 deletion floris/simulation/floris.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def steady_state_atmospheric_condition(self):

if vel_model in ["gauss", "cc", "turbopark", "jensen"] and \
self.farm.correct_cp_ct_for_tilt.any():
self.logger.warn(
self.logger.warning(
"The current model does not account for vertical wake deflection due to " +
"tilt. Corrections to Cp and Ct can be included, but no vertical wake " +
"deflection will occur."
Expand Down
2 changes: 2 additions & 0 deletions floris/simulation/turbine.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ class Turbine(BaseClass):
ref_density_cp_ct: float = field()
ref_tilt_cp_ct: float = field()
power_thrust_table: PowerThrustTable = field(default=None)
correct_cp_ct_for_tilt: bool = field(default=None)
floating_tilt_table: TiltTable = field(default=None)
floating_correct_cp_ct_for_tilt: bool = field(default=None)
power_thrust_data_file: str = field(default=None)
Expand All @@ -640,6 +641,7 @@ class Turbine(BaseClass):
fCt_interp: interp1d = field(init=False)
power_interp: interp1d = field(init=False)
tilt_interp: interp1d = field(init=False)
fTilt_interp: interp1d = field(init=False)


# For the following parameters, use default values if not user-specified
Expand Down
1 change: 1 addition & 0 deletions floris/simulation/turbine_multi_dim.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ class TurbineMultiDimensional(Turbine):
Defaults to the current file location.
"""
power_thrust_data_file: str = field(default=None)
power_thrust_data: MultiDimensionalPowerThrustTable = field(default=None)
multi_dimensional_cp_ct: bool = field(default=False)
turbine_library_path: Path = field(
default=Path(".").resolve(),
Expand Down
30 changes: 18 additions & 12 deletions floris/simulation/wake_deflection/gauss.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@

import numexpr as ne
import numpy as np
from attrs import define, field
from attrs import (
define,
field,
fields,
)
from numpy import pi

from floris.simulation import (
Expand All @@ -29,6 +33,8 @@
from floris.utilities import cosd, sind


NUM_EPS = fields(BaseModel).NUM_EPS.default

@define
class GaussVelocityDeflection(BaseModel):
"""
Expand Down Expand Up @@ -309,11 +315,11 @@ def wake_added_yaw(
### compute the spanwise and vertical velocities induced by yaw

# decay = eps ** 2 / (4 * nu * delta_x / Uinf + eps ** 2) # This is the decay downstream
yLocs = delta_y + BaseModel.NUM_EPS
yLocs = delta_y + NUM_EPS

# top vortex
# NOTE: this is the top of the grid, not the top of the rotor
zT = z_i - (HH + D / 2) + BaseModel.NUM_EPS # distance from the top of the grid
zT = z_i - (HH + D / 2) + NUM_EPS # distance from the top of the grid
rT = ne.evaluate("yLocs ** 2 + zT ** 2") # TODO: This is (-) in the paper
# This looks like spanwise decay;
# it defines the vortex profile in the spanwise directions
Expand All @@ -323,15 +329,15 @@ def wake_added_yaw(
# w_top = (-1 * Gamma_top * yLocs) / (2 * pi * rT) * core_shape * decay

# bottom vortex
zB = z_i - (HH - D / 2) + BaseModel.NUM_EPS
zB = z_i - (HH - D / 2) + NUM_EPS
rB = ne.evaluate("yLocs ** 2 + zB ** 2")
core_shape = ne.evaluate("1 - exp(-rB / (eps ** 2))")
v_bottom = ne.evaluate("(Gamma_bottom * zB) / (2 * pi * rB) * core_shape")
v_bottom = np.mean( v_bottom, axis=(3,4) )
# w_bottom = (-1 * Gamma_bottom * yLocs) / (2 * pi * rB) * core_shape * decay

# wake rotation vortex
zC = z_i - HH + BaseModel.NUM_EPS
zC = z_i - HH + NUM_EPS
rC = ne.evaluate("yLocs ** 2 + zC ** 2")
core_shape = ne.evaluate("1 - exp(-rC / (eps ** 2))")
v_core = ne.evaluate("(Gamma_wake_rotation * zC) / (2 * pi * rC) * core_shape")
Expand Down Expand Up @@ -411,10 +417,10 @@ def calculate_transverse_velocity(

# This is the decay downstream
decay = ne.evaluate("eps ** 2 / (4 * nu * delta_x / Uinf + eps ** 2)")
yLocs = delta_y + BaseModel.NUM_EPS
yLocs = delta_y + NUM_EPS

# top vortex
zT = z - (HH + D / 2) + BaseModel.NUM_EPS
zT = z - (HH + D / 2) + NUM_EPS
rT = ne.evaluate("yLocs ** 2 + zT ** 2") # TODO: This is - in the paper
# This looks like spanwise decay;
# it defines the vortex profile in the spanwise directions
Expand All @@ -423,14 +429,14 @@ def calculate_transverse_velocity(
W1 = ne.evaluate("(-1 * Gamma_top * yLocs) / (2 * pi * rT) * core_shape * decay")

# bottom vortex
zB = z - (HH - D / 2) + BaseModel.NUM_EPS
zB = z - (HH - D / 2) + NUM_EPS
rB = ne.evaluate("yLocs ** 2 + zB ** 2")
core_shape = ne.evaluate("1 - exp(-rB / (eps ** 2))")
V2 = ne.evaluate("(Gamma_bottom * zB) / (2 * pi * rB) * core_shape * decay")
W2 = ne.evaluate("(-1 * Gamma_bottom * yLocs) / (2 * pi * rB) * core_shape * decay")

# wake rotation vortex
zC = z - HH + BaseModel.NUM_EPS
zC = z - HH + NUM_EPS
rC = ne.evaluate("yLocs ** 2 + zC ** 2")
core_shape = ne.evaluate("1 - exp(-rC / (eps ** 2))")
V5 = ne.evaluate("(Gamma_wake_rotation * zC) / (2 * pi * rC) * core_shape * decay")
Expand All @@ -439,7 +445,7 @@ def calculate_transverse_velocity(
### Boundary condition - ground mirror vortex

# top vortex - ground
zTb = z + (HH + D / 2) + BaseModel.NUM_EPS
zTb = z + (HH + D / 2) + NUM_EPS
rTb = ne.evaluate("yLocs ** 2 + zTb ** 2")
# This looks like spanwise decay;
# it defines the vortex profile in the spanwise directions
Expand All @@ -448,14 +454,14 @@ def calculate_transverse_velocity(
W3 = ne.evaluate("(Gamma_top * yLocs) / (2 * pi * rTb) * core_shape * decay")

# bottom vortex - ground
zBb = z + (HH - D / 2) + BaseModel.NUM_EPS
zBb = z + (HH - D / 2) + NUM_EPS
rBb = ne.evaluate("yLocs ** 2 + zBb ** 2")
core_shape = ne.evaluate("1 - exp(-rBb / (eps ** 2))")
V4 = ne.evaluate("(-1 * Gamma_bottom * zBb) / (2 * pi * rBb) * core_shape * decay")
W4 = ne.evaluate("(Gamma_bottom * yLocs) / (2 * pi * rBb) * core_shape * decay")

# wake rotation vortex - ground effect
zCb = z + HH + BaseModel.NUM_EPS
zCb = z + HH + NUM_EPS
rCb = ne.evaluate("yLocs ** 2 + zCb ** 2")
core_shape = ne.evaluate("1 - exp(-rCb / (eps ** 2))")
V6 = ne.evaluate("(-1 * Gamma_wake_rotation * zCb) / (2 * pi * rCb) * core_shape * decay")
Expand Down
9 changes: 7 additions & 2 deletions floris/simulation/wake_velocity/jensen.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

import numexpr as ne
import numpy as np
from attrs import define, field
from attrs import (
define,
field,
fields,
)

from floris.simulation import (
BaseModel,
Expand All @@ -25,6 +29,8 @@
)


NUM_EPS = fields(BaseModel).NUM_EPS.default

@define
class JensenVelocityDeficit(BaseModel):
"""
Expand Down Expand Up @@ -107,7 +113,6 @@ def function(
dz = ne.evaluate("z - z_i")

we = self.we
NUM_EPS = JensenVelocityDeficit.NUM_EPS

# Construct a boolean mask to include all points downstream of the turbine
downstream_mask = ne.evaluate("dx > 0 + NUM_EPS")
Expand Down
7 changes: 3 additions & 4 deletions floris/tools/floris_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@

import numpy as np
import pandas as pd
from scipy.interpolate import LinearNDInterpolator, NearestNDInterpolator

from floris.logging_manager import LoggerBase
from floris.logging_manager import LoggingManager
from floris.simulation import Floris, State
from floris.simulation.turbine import (
average_velocity,
Expand All @@ -35,7 +34,7 @@
from floris.type_dec import NDArrayFloat


class FlorisInterface(LoggerBase):
class FlorisInterface(LoggingManager):
"""
FlorisInterface provides a high-level user interface to many of the
underlying methods within the FLORIS framework. It is meant to act as a
Expand All @@ -61,7 +60,7 @@ def __init__(self, configuration: dict | str | Path):
except FileNotFoundError:
# If the file cannot be found, then attempt the configuration path relative to the
# file location from which FlorisInterface was attempted to be run. If successful,
# update self.configuration to an aboslute, working file path and name.
# 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.floris = Floris.from_file(config)
Expand Down
1 change: 1 addition & 0 deletions floris/tools/interface_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def show_params(
# props = get_props(obj, fi)
props = fi.floris.wake._asdict()
# props = props["wake_velocity_parameters"][fi.floris.wake.velocity_model.model_string]
# NOTE: _get_model_dict is remove and model.as_dict() should be used instead
props = fi.floris.wake.velocity_model._get_model_dict()

if verbose:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
YawOptimizationGeometric,
)

from ....logging_manager import LoggerBase
from ....logging_manager import LoggingManager


class LayoutOptimization(LoggerBase):
class LayoutOptimization(LoggingManager):
def __init__(self, fi, boundaries, min_dist=None, freq=None, enable_geometric_yaw=False):
self.fi = fi.copy()
self.boundaries = boundaries
Expand Down
Loading

0 comments on commit 52b8a5f

Please sign in to comment.