From 1421b92de6028b5ebd086c30bd044d32e6e898a1 Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:22:17 -0800 Subject: [PATCH 01/17] add base validator mixin and array shape validators --- floris/type_dec.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/floris/type_dec.py b/floris/type_dec.py index 892739035..001667b42 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -40,6 +40,7 @@ NDArrayFilter = Union[npt.NDArray[np.int_], npt.NDArray[np.bool_]] NDArrayObject = npt.NDArray[np.object_] NDArrayBool = npt.NDArray[np.bool_] +NDArray = NDArrayFloat | NDArrayInt | NDArrayFilter | NDArrayObject | NDArrayBool ### Custom callables for attrs objects and functions @@ -144,6 +145,24 @@ def convert_to_path(fn: str | Path) -> Path: raise TypeError(f"The passed input: {fn} could not be converted to a pathlib.Path object") +def validate_3DArray_shape(instance, attribute: Attribute, value: NDArray) -> None: + if not isinstance(value, NDArray): + raise TypeError(f"{attribute.name} is not a valid NumPy array type.") + + shape = (instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines) + if value.shape != shape: + raise ValueError(f"{attribute.name} should have shape: {shape}") + + +def validate_5DArray_shape(instance, attribute: Attribute, value: NDArray) -> None: + if not isinstance(value, NDArray): + raise TypeError(f"{attribute.name} is not a valid NumPy array type.") + + grid = instance.grid_resolution + shape = (instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines, grid, grid) + if value.shape != shape: + raise ValueError(f"{attribute.name} should have shape: {shape}") + @define class FromDictMixin: """ @@ -205,6 +224,17 @@ def as_dict(self) -> dict: return attrs.asdict(self, filter=_attr_floris_filter, value_serializer=_attr_serializer) +@define +class ValidateMixin: + """ + A Mixin class to wraps the ``attrs.validate()`` method to provide ``self.validate()`` so that + all class attributes with validators can be run at once. + """ + def validate(self) -> None: + """Runs ``attrs.validate(self)``.""" + attrs.validate(self) + + # Avoids constant redefinition of the same attr.ib properties for model attributes # from functools import partial, update_wrapper From 02fa54131c06f8be43c4715963bd00c232a85a16 Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:40:16 -0800 Subject: [PATCH 02/17] add validator mixin to classes and add array validators to Grid --- floris/simulation/flow_field.py | 4 +++- floris/simulation/grid.py | 16 ++++++++------ floris/type_dec.py | 38 +++++++++++++++++++++++++++------ 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index f6baf49c4..ca35a1efd 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -29,11 +29,13 @@ from floris.type_dec import ( floris_array_converter, NDArrayFloat, + validate_3DArray_shape, + ValidateMixin, ) @define -class FlowField(BaseClass): +class FlowField(BaseClass, ValidateMixin): wind_speeds: NDArrayFloat = field(converter=floris_array_converter) wind_directions: NDArrayFloat = field(converter=floris_array_converter) wind_veer: float = field(converter=float) diff --git a/floris/simulation/grid.py b/floris/simulation/grid.py index 3786fc873..04234d622 100644 --- a/floris/simulation/grid.py +++ b/floris/simulation/grid.py @@ -28,6 +28,8 @@ floris_float_type, NDArrayFloat, NDArrayInt, + validate_5DArray_shape, + ValidateMixin, ) from floris.utilities import ( reverse_rotate_coordinates_rel_west, @@ -36,7 +38,7 @@ @define -class Grid(ABC, BaseClass): +class Grid(ABC, BaseClass, ValidateMixin): """ Grid should establish domain bounds based on given criteria, and develop three arrays to contain components of the grid @@ -77,12 +79,12 @@ class Grid(ABC, BaseClass): n_turbines: int = field(init=False) n_wind_speeds: int = field(init=False) n_wind_directions: int = field(init=False) - x_sorted: NDArrayFloat = field(init=False) - y_sorted: NDArrayFloat = field(init=False) - z_sorted: NDArrayFloat = field(init=False) - x_sorted_inertial_frame: NDArrayFloat = field(init=False) - y_sorted_inertial_frame: NDArrayFloat = field(init=False) - z_sorted_inertial_frame: NDArrayFloat = field(init=False) + x_sorted: NDArrayFloat = field(init=False, validator=validate_5DArray_shape) + y_sorted: NDArrayFloat = field(init=False, validator=validate_5DArray_shape) + z_sorted: NDArrayFloat = field(init=False, validator=validate_5DArray_shape) + x_sorted_inertial_frame: NDArrayFloat = field(init=False, validator=validate_5DArray_shape) + y_sorted_inertial_frame: NDArrayFloat = field(init=False, validator=validate_5DArray_shape) + z_sorted_inertial_frame: NDArrayFloat = field(init=False, validator=validate_5DArray_shape) cubature_weights: NDArrayFloat = field(init=False, default=None) @turbine_coordinates.validator diff --git a/floris/type_dec.py b/floris/type_dec.py index 001667b42..f818c6141 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -40,7 +40,6 @@ NDArrayFilter = Union[npt.NDArray[np.int_], npt.NDArray[np.bool_]] NDArrayObject = npt.NDArray[np.object_] NDArrayBool = npt.NDArray[np.bool_] -NDArray = NDArrayFloat | NDArrayInt | NDArrayFilter | NDArrayObject | NDArrayBool ### Custom callables for attrs objects and functions @@ -145,23 +144,48 @@ def convert_to_path(fn: str | Path) -> Path: raise TypeError(f"The passed input: {fn} could not be converted to a pathlib.Path object") -def validate_3DArray_shape(instance, attribute: Attribute, value: NDArray) -> None: - if not isinstance(value, NDArray): +def validate_3DArray_shape(instance, attribute: Attribute, value: np.ndarray) -> None: + """Validator that checks if the array's shape is N wind directions x N wind speeds x N turbines. + + Args: + instance (cls): The class instance. + attribute (Attribute): The ``attrs.Attribute`` data. + value (np.ndarray): The input or updated NumPy array. + + Raises: + TypeError: raised if :py:attr:`value` is not a NumPy array. + ValueError: raised if the shape of :py:attr:`value` is not + N wind directions x N wind speeds x N turbines. + """ + if not isinstance(value, np.ndarray): raise TypeError(f"{attribute.name} is not a valid NumPy array type.") shape = (instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines) if value.shape != shape: - raise ValueError(f"{attribute.name} should have shape: {shape}") + raise ValueError(f"{attribute.name} should have shape: {shape}; not shape: {value.shape}") -def validate_5DArray_shape(instance, attribute: Attribute, value: NDArray) -> None: - if not isinstance(value, NDArray): +def validate_5DArray_shape(instance, attribute: Attribute, value: np.ndarray) -> None: + """Validator that checks if the array's shape is + N wind directions x N wind speeds x N turbines x N grid points x N grid points. + + Args: + instance (cls): The class instance. + attribute (Attribute): The ``attrs.Attribute`` data. + value (np.ndarray): The input or updated NumPy array. + + Raises: + TypeError: raised if :py:attr:`value` is not a NumPy array. + ValueError: raised if the shape of :py:attr:`value` is not + N wind directions x N wind speeds x N turbines x N grid points x N grid points. + """ + if not isinstance(value, np.ndarray): raise TypeError(f"{attribute.name} is not a valid NumPy array type.") grid = instance.grid_resolution shape = (instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines, grid, grid) if value.shape != shape: - raise ValueError(f"{attribute.name} should have shape: {shape}") + raise ValueError(f"{attribute.name} should have shape: {shape}; not shape: {value.shape}") @define class FromDictMixin: From 7cc9335647b104d69a000b430bb6218e8ea76d6f Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:40:43 -0800 Subject: [PATCH 03/17] add test for ValidateMixin --- tests/type_dec_unit_test.py | 38 ++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/tests/type_dec_unit_test.py b/tests/type_dec_unit_test.py index 641f207dc..ef74d228b 100644 --- a/tests/type_dec_unit_test.py +++ b/tests/type_dec_unit_test.py @@ -15,6 +15,7 @@ from pathlib import Path from typing import List +import attrs import numpy as np import pytest from attrs import define, field @@ -24,16 +25,17 @@ floris_array_converter, FromDictMixin, iter_validator, + ValidateMixin, ) @define -class AttrsDemoClass(FromDictMixin): - w: int +class AttrsDemoClass(FromDictMixin, ValidateMixin): + w: int = field(validator=attrs.validators.instance_of(int)) x: int = field(converter=int) y: float = field(converter=float, default=2.1) z: str = field(converter=str, default="z") - non_initd: float = field(init=False) + non_initd: float = field(init=False, validator=attrs.validators.instance_of(float)) def __attrs_post_init__(self): self.non_initd = 1.1 @@ -98,6 +100,36 @@ def test_FromDictMixin_custom(): AttrsDemoClass.from_dict(inputs) +def test_ValidateMixin(): + inputs = { + "w": 0, + "x": 1, + "y": 2.3, + "z": "asdf", + } + demo = AttrsDemoClass.from_dict(inputs) + + # Disable the validators to set attributes with a failing value, then + # manually validate with the mixin functionality to check it's working + with attrs.validators.disabled(): + demo.w = "string" + + with pytest.raises(TypeError): + demo.validate() + + with attrs.validators.disabled(): + demo.non_initd = "2.2" + + with pytest.raises(TypeError): + demo.validate() + + with attrs.validators.disabled(): + demo.liststr = (3, 2) + + with pytest.raises(TypeError): + demo.validate() + + def test_iter_validator(): # Check the correct values work From 6c06576511f63bad37cf7c49d21a5d02b896ff75 Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:49:57 -0800 Subject: [PATCH 04/17] implement array checkers for grids and make a carveout for broadcasting --- floris/simulation/grid.py | 21 +++++++++++---------- floris/type_dec.py | 14 ++++++++++---- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/floris/simulation/grid.py b/floris/simulation/grid.py index 04234d622..e971a9581 100644 --- a/floris/simulation/grid.py +++ b/floris/simulation/grid.py @@ -28,6 +28,7 @@ floris_float_type, NDArrayFloat, NDArrayInt, + validate_3DArray_shape, validate_5DArray_shape, ValidateMixin, ) @@ -153,11 +154,11 @@ class TurbineGrid(Grid): creates a 3x3 grid within the rotor swept area. """ # TODO: describe these and the differences between `sorted_indices` and `sorted_coord_indices` - sorted_indices: NDArrayInt = field(init=False) - sorted_coord_indices: NDArrayInt = field(init=False) - unsorted_indices: NDArrayInt = field(init=False) - x_center_of_rotation: NDArrayFloat = field(init=False) - y_center_of_rotation: NDArrayFloat = field(init=False) + sorted_indices: NDArrayInt = field(init=False, validator=validate_5DArray_shape) + sorted_coord_indices: NDArrayInt = field(init=False, validator=validate_3DArray_shape) + unsorted_indices: NDArrayInt = field(init=False, validator=validate_5DArray_shape) + x_center_of_rotation: NDArrayFloat = field(init=False) # TODO: this is a numpy float + y_center_of_rotation: NDArrayFloat = field(init=False) # TODO: this is a numpy float average_method = "cubic-mean" def __attrs_post_init__(self) -> None: @@ -313,9 +314,9 @@ class TurbineCubatureGrid(Grid): include in the cubature method. This value must be in the range [1, 10], and the corresponding cubature weights are set automatically. """ - sorted_indices: NDArrayInt = field(init=False) - sorted_coord_indices: NDArrayInt = field(init=False) - unsorted_indices: NDArrayInt = field(init=False) + sorted_indices: NDArrayInt = field(init=False, validator=validate_5DArray_shape) + sorted_coord_indices: NDArrayInt = field(init=False, validator=validate_3DArray_shape) + unsorted_indices: NDArrayInt = field(init=False, validator=validate_5DArray_shape) x_center_of_rotation: NDArrayFloat = field(init=False) y_center_of_rotation: NDArrayFloat = field(init=False) average_method = "simple-cubature" @@ -557,8 +558,8 @@ class FlowFieldPlanarGrid(Grid): x2_bounds: tuple = field(default=None) x_center_of_rotation: NDArrayFloat = field(init=False) y_center_of_rotation: NDArrayFloat = field(init=False) - sorted_indices: NDArrayInt = field(init=False) - unsorted_indices: NDArrayInt = field(init=False) + sorted_indices: NDArrayInt = field(init=False, validator=validate_3DArray_shape) + unsorted_indices: NDArrayInt = field(init=False, validator=validate_3DArray_shape) def __attrs_post_init__(self) -> None: self.set_grid() diff --git a/floris/type_dec.py b/floris/type_dec.py index f818c6141..65e84e843 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -158,11 +158,16 @@ def validate_3DArray_shape(instance, attribute: Attribute, value: np.ndarray) -> N wind directions x N wind speeds x N turbines. """ if not isinstance(value, np.ndarray): - raise TypeError(f"{attribute.name} is not a valid NumPy array type.") + raise TypeError(f"`{attribute.name}` is not a valid NumPy array type.") shape = (instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines) if value.shape != shape: - raise ValueError(f"{attribute.name} should have shape: {shape}; not shape: {value.shape}") + # The grid sorted_coord_indices are broadcast along the wind speed dimension + broadcast_shape = (instance.n_wind_directions, 1, instance.n_turbines) + if value.shape != broadcast_shape: + raise ValueError( + f"`{attribute.name}` should have shape: {shape}; not shape: {value.shape}" + ) def validate_5DArray_shape(instance, attribute: Attribute, value: np.ndarray) -> None: @@ -180,12 +185,13 @@ def validate_5DArray_shape(instance, attribute: Attribute, value: np.ndarray) -> N wind directions x N wind speeds x N turbines x N grid points x N grid points. """ if not isinstance(value, np.ndarray): - raise TypeError(f"{attribute.name} is not a valid NumPy array type.") + print(type(value)) + raise TypeError(f"`{attribute.name}` is not a valid NumPy array type.") grid = instance.grid_resolution shape = (instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines, grid, grid) if value.shape != shape: - raise ValueError(f"{attribute.name} should have shape: {shape}; not shape: {value.shape}") + raise ValueError(f"`{attribute.name}` should have shape: {shape}; not shape: {value.shape}") @define class FromDictMixin: From 675e418f884618740c7ff17887f243670fa1f0b8 Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:53:37 -0800 Subject: [PATCH 05/17] fix typing on rotation and add validator to ensure it --- floris/simulation/grid.py | 40 +++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/floris/simulation/grid.py b/floris/simulation/grid.py index e971a9581..e3845ae10 100644 --- a/floris/simulation/grid.py +++ b/floris/simulation/grid.py @@ -157,8 +157,12 @@ class TurbineGrid(Grid): sorted_indices: NDArrayInt = field(init=False, validator=validate_5DArray_shape) sorted_coord_indices: NDArrayInt = field(init=False, validator=validate_3DArray_shape) unsorted_indices: NDArrayInt = field(init=False, validator=validate_5DArray_shape) - x_center_of_rotation: NDArrayFloat = field(init=False) # TODO: this is a numpy float - y_center_of_rotation: NDArrayFloat = field(init=False) # TODO: this is a numpy float + x_center_of_rotation: floris_float_type = field( + init=False, validator=attrs.validators.instance_of(floris_float_type) + ) + y_center_of_rotation: floris_float_type = field( + init=False, validator=attrs.validators.instance_of(floris_float_type) + ) average_method = "cubic-mean" def __attrs_post_init__(self) -> None: @@ -317,8 +321,12 @@ class TurbineCubatureGrid(Grid): sorted_indices: NDArrayInt = field(init=False, validator=validate_5DArray_shape) sorted_coord_indices: NDArrayInt = field(init=False, validator=validate_3DArray_shape) unsorted_indices: NDArrayInt = field(init=False, validator=validate_5DArray_shape) - x_center_of_rotation: NDArrayFloat = field(init=False) - y_center_of_rotation: NDArrayFloat = field(init=False) + x_center_of_rotation: floris_float_type = field( + init=False, validator=attrs.validators.instance_of(floris_float_type) + ) + y_center_of_rotation: floris_float_type = field( + init=False, validator=attrs.validators.instance_of(floris_float_type) + ) average_method = "simple-cubature" def __attrs_post_init__(self) -> None: @@ -478,8 +486,12 @@ class FlowFieldGrid(Grid): grid_resolution (:py:obj:`Iterable(int,)`): The number of grid points to create in each planar direction. Must be 3 components for resolution in the x, y, and z directions. """ - x_center_of_rotation: NDArrayFloat = field(init=False) - y_center_of_rotation: NDArrayFloat = field(init=False) + x_center_of_rotation: floris_float_type = field( + init=False, validator=attrs.validators.instance_of(floris_float_type) + ) + y_center_of_rotation: floris_float_type = field( + init=False, validator=attrs.validators.instance_of(floris_float_type) + ) def __attrs_post_init__(self) -> None: self.set_grid() @@ -556,8 +568,12 @@ class FlowFieldPlanarGrid(Grid): planar_coordinate: float = field() x1_bounds: tuple = field(default=None) x2_bounds: tuple = field(default=None) - x_center_of_rotation: NDArrayFloat = field(init=False) - y_center_of_rotation: NDArrayFloat = field(init=False) + x_center_of_rotation: floris_float_type = field( + init=False, validator=attrs.validators.instance_of(floris_float_type) + ) + y_center_of_rotation: floris_float_type = field( + init=False, validator=attrs.validators.instance_of(floris_float_type) + ) sorted_indices: NDArrayInt = field(init=False, validator=validate_3DArray_shape) unsorted_indices: NDArrayInt = field(init=False, validator=validate_3DArray_shape) @@ -684,8 +700,12 @@ class PointsGrid(Grid): points_x: NDArrayFloat = field(converter=floris_array_converter) points_y: NDArrayFloat = field(converter=floris_array_converter) points_z: NDArrayFloat = field(converter=floris_array_converter) - x_center_of_rotation: float | None = field(default=None) - y_center_of_rotation: float | None = field(default=None) + x_center_of_rotation: float | None = field( + default=None, validator=attrs.validators.instance_of(floris_float_type) + ) + y_center_of_rotation: float | None = field( + default=None, validator=attrs.validators.instance_of(floris_float_type) + ) def __attrs_post_init__(self) -> None: self.set_grid() From 5af909e365c746f803f217d212e44cadf79679c1 Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Tue, 12 Dec 2023 11:26:23 -0800 Subject: [PATCH 06/17] add init carveout and add validator to flow field arrays --- floris/simulation/flow_field.py | 27 ++++++++++++++++----------- floris/type_dec.py | 19 +++++++++++++++++-- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index ca35a1efd..1bfadbb72 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -27,9 +27,9 @@ Grid, ) from floris.type_dec import ( + array_5D_field, floris_array_converter, NDArrayFloat, - validate_3DArray_shape, ValidateMixin, ) @@ -49,16 +49,18 @@ class FlowField(BaseClass, ValidateMixin): n_wind_speeds: int = field(init=False) n_wind_directions: int = field(init=False) - - u_initial_sorted: NDArrayFloat = field(init=False, factory=lambda: np.array([])) - v_initial_sorted: NDArrayFloat = field(init=False, factory=lambda: np.array([])) - w_initial_sorted: NDArrayFloat = field(init=False, factory=lambda: np.array([])) - u_sorted: NDArrayFloat = field(init=False, factory=lambda: np.array([])) - v_sorted: NDArrayFloat = field(init=False, factory=lambda: np.array([])) - w_sorted: NDArrayFloat = field(init=False, factory=lambda: np.array([])) - u: NDArrayFloat = field(init=False, factory=lambda: np.array([])) - v: NDArrayFloat = field(init=False, factory=lambda: np.array([])) - w: NDArrayFloat = field(init=False, factory=lambda: np.array([])) + n_turbines: int = field(init=False) + grid_resolution: int = field(init=False) + + u_initial_sorted: NDArrayFloat = array_5D_field + v_initial_sorted: NDArrayFloat = array_5D_field + w_initial_sorted: NDArrayFloat = array_5D_field + u_sorted: NDArrayFloat = array_5D_field + v_sorted: NDArrayFloat = array_5D_field + w_sorted: NDArrayFloat = array_5D_field + u: NDArrayFloat = array_5D_field + v: NDArrayFloat = array_5D_field + w: NDArrayFloat = array_5D_field het_map: list = field(init=False, default=None) dudz_initial_sorted: NDArrayFloat = field(init=False, factory=lambda: np.array([])) @@ -133,6 +135,9 @@ def initialize_velocity_field(self, grid: Grid) -> None: # determined by this line. Since the right-most dimension on grid.z is storing the values # for height, using it here to apply the shear law makes that dimension store the vertical # wind profile. + self.n_turbines = grid.n_turbines + self.grid_resolution = grid.grid_resolution + wind_profile_plane = (grid.z_sorted / self.reference_wind_height) ** self.wind_shear dwind_profile_plane = ( self.wind_shear diff --git a/floris/type_dec.py b/floris/type_dec.py index 65e84e843..faaf9deb7 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -28,7 +28,11 @@ import attrs import numpy as np import numpy.typing as npt -from attrs import Attribute, define +from attrs import ( + Attribute, + define, + field, +) ### Define general data types used throughout @@ -160,6 +164,10 @@ def validate_3DArray_shape(instance, attribute: Attribute, value: np.ndarray) -> if not isinstance(value, np.ndarray): raise TypeError(f"`{attribute.name}` is not a valid NumPy array type.") + # Don't fail on the initialized empty array + if value.size == 0: + return + shape = (instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines) if value.shape != shape: # The grid sorted_coord_indices are broadcast along the wind speed dimension @@ -188,6 +196,10 @@ def validate_5DArray_shape(instance, attribute: Attribute, value: np.ndarray) -> print(type(value)) raise TypeError(f"`{attribute.name}` is not a valid NumPy array type.") + # Don't fail on the initialized empty array + if value.size == 0: + return + grid = instance.grid_resolution shape = (instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines, grid, grid) if value.shape != shape: @@ -265,7 +277,10 @@ def validate(self) -> None: attrs.validate(self) -# Avoids constant redefinition of the same attr.ib properties for model attributes +# Avoids constant redefinition of the same field properties for model attributes + +array_5D_field = field(init=False, factory=lambda: np.array([]), validator=validate_5DArray_shape) + # from functools import partial, update_wrapper From 8a59a731e145063101ebb7d23dd2ee6c0fe3686f Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Tue, 12 Dec 2023 11:29:18 -0800 Subject: [PATCH 07/17] fix ill-defined test for correct array shapes --- tests/floris_unit_test.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/floris_unit_test.py b/tests/floris_unit_test.py index 05c01f022..fceeb10f7 100644 --- a/tests/floris_unit_test.py +++ b/tests/floris_unit_test.py @@ -12,6 +12,7 @@ # See https://floris.readthedocs.io for documentation +from copy import deepcopy from pathlib import Path import yaml @@ -49,7 +50,11 @@ def test_init(): def test_asdict(turbine_grid_fixture: TurbineGrid): - floris = Floris.from_dict(DICT_INPUT) + grid_dict = deepcopy(DICT_INPUT) + grid_dict["flow_field"]["wind_speeds"] = turbine_grid_fixture.wind_speeds + grid_dict["flow_field"]["wind_directions"] = turbine_grid_fixture.wind_directions + + floris = Floris.from_dict(grid_dict) floris.flow_field.initialize_velocity_field(turbine_grid_fixture) dict1 = floris.as_dict() From d99f65683d70bf49c6ae0d97da8ad4fd674f7472 Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:20:28 -0800 Subject: [PATCH 08/17] add 3D array validation to Farm and add 3D array field initialization --- floris/simulation/farm.py | 53 ++++++++++++++++++++++----------------- floris/type_dec.py | 5 ++-- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/floris/simulation/farm.py b/floris/simulation/farm.py index 1bfddf695..0daf2c6ca 100644 --- a/floris/simulation/farm.py +++ b/floris/simulation/farm.py @@ -33,6 +33,7 @@ ) from floris.simulation.turbine import compute_tilt_angles_for_floating_turbines from floris.type_dec import ( + array_3D_field, convert_to_path, floris_array_converter, iter_validator, @@ -79,6 +80,9 @@ class Farm(BaseClass): default=default_turbine_library_path, converter=convert_to_path ) + n_wind_directions: int = field(init=False) + n_wind_speeds: int = field(init=False) + turbine_definitions: list = field(init=False, validator=iter_validator(list, dict)) turbine_fCts: Dict[str, interp1d] | List[interp1d] = field(init=False, factory=list) @@ -86,43 +90,43 @@ class Farm(BaseClass): turbine_fTilts: list = field(init=False, factory=list) - yaw_angles: NDArrayFloat = field(init=False) - yaw_angles_sorted: NDArrayFloat = field(init=False) + yaw_angles: NDArrayFloat = array_3D_field + yaw_angles_sorted: NDArrayFloat = array_3D_field - tilt_angles: NDArrayFloat = field(init=False) - tilt_angles_sorted: NDArrayFloat = field(init=False) + tilt_angles: NDArrayFloat = array_3D_field + tilt_angles_sorted: NDArrayFloat = array_3D_field - hub_heights: NDArrayFloat = field(init=False) - hub_heights_sorted: NDArrayFloat = field(init=False, factory=list) + hub_heights: NDArrayFloat = array_3D_field + hub_heights_sorted: NDArrayFloat = array_3D_field turbine_map: List[Turbine | TurbineMultiDimensional] = field(init=False, factory=list) - turbine_type_map: NDArrayObject = field(init=False, factory=list) - turbine_type_map_sorted: NDArrayObject = field(init=False, factory=list) + turbine_type_map: NDArrayObject = array_3D_field + turbine_type_map_sorted: NDArrayObject = array_3D_field turbine_power_interps: Dict[str, interp1d] | List[interp1d] = field(init=False, factory=list) - turbine_power_interps_sorted: NDArrayFloat = field(init=False, factory=list) + turbine_power_interps_sorted: NDArrayFloat = array_3D_field - rotor_diameters: NDArrayFloat = field(init=False, factory=list) - rotor_diameters_sorted: NDArrayFloat = field(init=False, factory=list) + rotor_diameters: NDArrayFloat = array_3D_field + rotor_diameters_sorted: NDArrayFloat = array_3D_field - TSRs: NDArrayFloat = field(init=False, factory=list) - TSRs_sorted: NDArrayFloat = field(init=False, factory=list) + TSRs: NDArrayFloat = array_3D_field + TSRs_sorted: NDArrayFloat = array_3D_field - pPs: NDArrayFloat = field(init=False, factory=list) - pPs_sorted: NDArrayFloat = field(init=False, factory=list) + pPs: NDArrayFloat = array_3D_field + pPs_sorted: NDArrayFloat = array_3D_field - pTs: NDArrayFloat = field(init=False, factory=list) - pTs_sorted: NDArrayFloat = field(init=False, factory=list) + pTs: NDArrayFloat = array_3D_field + pTs_sorted: NDArrayFloat = array_3D_field - ref_density_cp_cts: NDArrayFloat = field(init=False, factory=list) - ref_density_cp_cts_sorted: NDArrayFloat = field(init=False, factory=list) + ref_density_cp_cts: NDArrayFloat = array_3D_field + ref_density_cp_cts_sorted: NDArrayFloat = array_3D_field - ref_tilt_cp_cts: NDArrayFloat = field(init=False, factory=list) - ref_tilt_cp_cts_sorted: NDArrayFloat = field(init=False, factory=list) + ref_tilt_cp_cts: NDArrayFloat = array_3D_field + ref_tilt_cp_cts_sorted: NDArrayFloat = array_3D_field - correct_cp_ct_for_tilt: NDArrayFloat = field(init=False, factory=list) - correct_cp_ct_for_tilt_sorted: NDArrayFloat = field(init=False, factory=list) + correct_cp_ct_for_tilt: NDArrayFloat = array_3D_field + correct_cp_ct_for_tilt_sorted: NDArrayFloat = array_3D_field internal_turbine_library: Path = field(init=False, default=default_turbine_library_path) @@ -405,6 +409,9 @@ def expand_farm_properties( ) def set_yaw_angles(self, n_wind_directions: int, n_wind_speeds: int): + self.n_wind_directions = n_wind_directions + self.n_wind_speeds = n_wind_speeds + # TODO Is this just for initializing yaw angles to zero? self.yaw_angles = np.zeros((n_wind_directions, n_wind_speeds, self.n_turbines)) self.yaw_angles_sorted = np.zeros((n_wind_directions, n_wind_speeds, self.n_turbines)) diff --git a/floris/type_dec.py b/floris/type_dec.py index faaf9deb7..996e50331 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -164,8 +164,8 @@ def validate_3DArray_shape(instance, attribute: Attribute, value: np.ndarray) -> if not isinstance(value, np.ndarray): raise TypeError(f"`{attribute.name}` is not a valid NumPy array type.") - # Don't fail on the initialized empty array - if value.size == 0: + # Don't fail on the initialized empty array or initialized 1-D array + if value.size == 0 or value.ndim == 1: return shape = (instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines) @@ -279,6 +279,7 @@ def validate(self) -> None: # Avoids constant redefinition of the same field properties for model attributes +array_3D_field = field(init=False, factory=lambda: np.array([]), validator=validate_3DArray_shape) array_5D_field = field(init=False, factory=lambda: np.array([]), validator=validate_5DArray_shape) From 97b5d2f07572e98432fef5a821d91a0dd533a70d Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:21:10 -0800 Subject: [PATCH 09/17] reorder dimension to align array order and move ValidateMixin to BaseClass --- floris/simulation/base.py | 4 ++-- floris/simulation/flow_field.py | 5 ++--- floris/simulation/grid.py | 7 +++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/floris/simulation/base.py b/floris/simulation/base.py index eb26364ab..5f5ea2427 100644 --- a/floris/simulation/base.py +++ b/floris/simulation/base.py @@ -33,7 +33,7 @@ ) from floris.logging_manager import LoggingManager -from floris.type_dec import FromDictMixin +from floris.type_dec import FromDictMixin, ValidateMixin class State(Enum): @@ -43,7 +43,7 @@ class State(Enum): @define -class BaseClass(FromDictMixin): +class BaseClass(FromDictMixin, ValidateMixin): """ BaseClass object class. This class does the logging and MixIn class inheritance. """ diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index 1bfadbb72..3b6de0fd0 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -30,12 +30,11 @@ array_5D_field, floris_array_converter, NDArrayFloat, - ValidateMixin, ) @define -class FlowField(BaseClass, ValidateMixin): +class FlowField(BaseClass): wind_speeds: NDArrayFloat = field(converter=floris_array_converter) wind_directions: NDArrayFloat = field(converter=floris_array_converter) wind_veer: float = field(converter=float) @@ -47,8 +46,8 @@ class FlowField(BaseClass, ValidateMixin): heterogenous_inflow_config: dict = field(default=None) multidim_conditions: dict = field(default=None) - n_wind_speeds: int = field(init=False) n_wind_directions: int = field(init=False) + n_wind_speeds: int = field(init=False) n_turbines: int = field(init=False) grid_resolution: int = field(init=False) diff --git a/floris/simulation/grid.py b/floris/simulation/grid.py index e3845ae10..c55fe30bb 100644 --- a/floris/simulation/grid.py +++ b/floris/simulation/grid.py @@ -30,7 +30,6 @@ NDArrayInt, validate_3DArray_shape, validate_5DArray_shape, - ValidateMixin, ) from floris.utilities import ( reverse_rotate_coordinates_rel_west, @@ -39,7 +38,7 @@ @define -class Grid(ABC, BaseClass, ValidateMixin): +class Grid(ABC, BaseClass): """ Grid should establish domain bounds based on given criteria, and develop three arrays to contain components of the grid @@ -77,9 +76,9 @@ class Grid(ABC, BaseClass, ValidateMixin): time_series: bool = field() grid_resolution: int | Iterable = field() - n_turbines: int = field(init=False) - n_wind_speeds: int = field(init=False) n_wind_directions: int = field(init=False) + n_wind_speeds: int = field(init=False) + n_turbines: int = field(init=False) x_sorted: NDArrayFloat = field(init=False, validator=validate_5DArray_shape) y_sorted: NDArrayFloat = field(init=False, validator=validate_5DArray_shape) z_sorted: NDArrayFloat = field(init=False, validator=validate_5DArray_shape) From cd39eb6bcd442ce270d1797a5565fdb868a34c5e Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:15:08 -0800 Subject: [PATCH 10/17] add missing validators and a new mixed dim method --- floris/simulation/farm.py | 2 +- floris/simulation/flow_field.py | 15 ++++++--------- floris/simulation/grid.py | 4 ++-- floris/type_dec.py | 33 ++++++++++++++++++++++++++++++++- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/floris/simulation/farm.py b/floris/simulation/farm.py index 0daf2c6ca..26b307078 100644 --- a/floris/simulation/farm.py +++ b/floris/simulation/farm.py @@ -86,7 +86,7 @@ class Farm(BaseClass): turbine_definitions: list = field(init=False, validator=iter_validator(list, dict)) turbine_fCts: Dict[str, interp1d] | List[interp1d] = field(init=False, factory=list) - turbine_fCts_sorted: NDArrayFloat = field(init=False, factory=list) + turbine_fCts_sorted: NDArrayFloat = array_3D_field turbine_fTilts: list = field(init=False, factory=list) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index 3b6de0fd0..5c14fec15 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -28,6 +28,7 @@ ) from floris.type_dec import ( array_5D_field, + array_mixed_dim_field, floris_array_converter, NDArrayFloat, ) @@ -61,15 +62,11 @@ class FlowField(BaseClass): v: NDArrayFloat = array_5D_field w: NDArrayFloat = array_5D_field het_map: list = field(init=False, default=None) - dudz_initial_sorted: NDArrayFloat = field(init=False, factory=lambda: np.array([])) - - turbulence_intensity_field: NDArrayFloat = field(init=False, factory=lambda: np.array([])) - turbulence_intensity_field_sorted: NDArrayFloat = field( - init=False, factory=lambda: np.array([]) - ) - turbulence_intensity_field_sorted_avg: NDArrayFloat = field( - init=False, factory=lambda: np.array([]) - ) + dudz_initial_sorted: NDArrayFloat = array_5D_field + + turbulence_intensity_field: NDArrayFloat = array_mixed_dim_field + turbulence_intensity_field_sorted: NDArrayFloat = array_5D_field + turbulence_intensity_field_sorted_avg: NDArrayFloat = array_mixed_dim_field @wind_speeds.validator def wind_speeds_validator(self, instance: attrs.Attribute, value: NDArrayFloat) -> None: diff --git a/floris/simulation/grid.py b/floris/simulation/grid.py index c55fe30bb..321949e15 100644 --- a/floris/simulation/grid.py +++ b/floris/simulation/grid.py @@ -699,10 +699,10 @@ class PointsGrid(Grid): points_x: NDArrayFloat = field(converter=floris_array_converter) points_y: NDArrayFloat = field(converter=floris_array_converter) points_z: NDArrayFloat = field(converter=floris_array_converter) - x_center_of_rotation: float | None = field( + x_center_of_rotation: floris_float_type | None = field( default=None, validator=attrs.validators.instance_of(floris_float_type) ) - y_center_of_rotation: float | None = field( + y_center_of_rotation: floris_float_type | None = field( default=None, validator=attrs.validators.instance_of(floris_float_type) ) diff --git a/floris/type_dec.py b/floris/type_dec.py index 996e50331..0ef047bfe 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -203,7 +203,35 @@ def validate_5DArray_shape(instance, attribute: Attribute, value: np.ndarray) -> grid = instance.grid_resolution shape = (instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines, grid, grid) if value.shape != shape: - raise ValueError(f"`{attribute.name}` should have shape: {shape}; not shape: {value.shape}") + broadcast_shape = ( + instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines, 1, 1 + ) + if value.shape != broadcast_shape: + raise ValueError( + f"`{attribute.name}` should have shape: {shape}; not shape: {value.shape}" + ) + + +def validate_mixed_dim(instance, attribute: Attribute, value: np.ndarray) -> None: + """Validator that checks if the array's shape is N wind directions x N wind speeds x N turbines + or N wind directions x N wind speeds x N turbines x N grid points x N grid points. + + Args: + instance (cls): The class instance. + attribute (Attribute): The ``attrs.Attribute`` data. + value (np.ndarray): The input or updated NumPy array. + + Raises: + TypeError: raised if :py:attr:`value` is not a NumPy array. + ValueError: raised if the shape of :py:attr:`value` is not a valid 5D or 3D array. + """ + try: + validate_5DArray_shape(instance, attribute, value) + except ValueError: + try: + validate_3DArray_shape(instance, attribute, value) + except ValueError: + raise ValueError(f"`{attribute.name}` could not be validated as a 5-D or 3-D array.") @define class FromDictMixin: @@ -281,6 +309,9 @@ def validate(self) -> None: array_3D_field = field(init=False, factory=lambda: np.array([]), validator=validate_3DArray_shape) array_5D_field = field(init=False, factory=lambda: np.array([]), validator=validate_5DArray_shape) +array_mixed_dim_field = field( + init=False, factory=lambda: np.array([]), validator=validate_mixed_dim +) # from functools import partial, update_wrapper From ac634390fc0da43274535e7c6b1201bcd8ebea0f Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:59:28 -0800 Subject: [PATCH 11/17] add test for validators --- tests/type_dec_unit_test.py | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/type_dec_unit_test.py b/tests/type_dec_unit_test.py index ef74d228b..4fa66e413 100644 --- a/tests/type_dec_unit_test.py +++ b/tests/type_dec_unit_test.py @@ -21,10 +21,16 @@ from attrs import define, field from floris.type_dec import ( + array_3D_field, + array_5D_field, + array_mixed_dim_field, convert_to_path, floris_array_converter, FromDictMixin, iter_validator, + NDArrayFloat, + validate_3DArray_shape, + validate_5DArray_shape, ValidateMixin, ) @@ -51,6 +57,20 @@ def __attrs_post_init__(self): ) +@define +class ArrayValidatorDemoClass(ValidateMixin): + n_wind_directions: int = field(default=3) + n_wind_speeds: int = field(default=4) + n_turbines: int = field(default=10) + grid_resolution: int = field(default=3) + three_dim_starts_as_one: NDArrayFloat = field( + factory=lambda: np.array([]), validator=validate_3DArray_shape + ) + five_dimensions_provided: NDArrayFloat = array_5D_field + three_dimensions_provided: NDArrayFloat = array_3D_field + mixed_dimensions_provided: NDArrayFloat = array_mixed_dim_field + + def test_as_dict(): # Non-initialized attributes should not be exported cls = AttrsDemoClass(w=0, x=1, liststr=["a", "b"]) @@ -129,6 +149,36 @@ def test_ValidateMixin(): with pytest.raises(TypeError): demo.validate() +def test_array_validators(): + # NOTE: demo.validate() is called in case attrs.on_setattrs is ever disabled, but is unnecessary + + # Check initialization works + demo = ArrayValidatorDemoClass() + demo.validate() + + # Check assignment with correct shape: 3 x 4 x 10 (x 3 x 3) + demo.five_dimensions_provided = np.random.random((3, 4, 10, 3, 3)) + demo.three_dimensions_provided = np.random.random((3, 4, 10)) + demo.validate() + + # Check assignment with correct broadcatable shape: 3 x 4 x 10 x 1 x 1 or 3 x 1 x 10 + demo.five_dimensions_provided = np.random.random((3, 4, 10, 1, 1)) + demo.three_dimensions_provided = np.random.random((3, 1, 10)) + demo.validate() + + # Check for correct number of dimensions, but wrong shape + with pytest.raises(ValueError): + demo.five_dimensions_provided = np.random.random((3, 3, 3, 3, 3)) + + with pytest.raises(ValueError): + demo.three_dimensions_provided = np.random.random((3, 5, 10)) + + # Check that the 3D that starts as a 1-D shape is working + demo.three_dim_starts_as_one = np.ones(demo.n_turbines) + demo.validate() + demo.three_dim_starts_as_one = np.ones((3, 4, demo.n_turbines)) + demo.validate() + def test_iter_validator(): From 373288dc6c60ac98d88bdfec123f364f9de86177 Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Tue, 12 Dec 2023 17:02:19 -0800 Subject: [PATCH 12/17] add missing mixed dim checks --- tests/type_dec_unit_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/type_dec_unit_test.py b/tests/type_dec_unit_test.py index 4fa66e413..a4f09184f 100644 --- a/tests/type_dec_unit_test.py +++ b/tests/type_dec_unit_test.py @@ -159,17 +159,25 @@ def test_array_validators(): # Check assignment with correct shape: 3 x 4 x 10 (x 3 x 3) demo.five_dimensions_provided = np.random.random((3, 4, 10, 3, 3)) demo.three_dimensions_provided = np.random.random((3, 4, 10)) + demo.mixed_dimensions_provided = np.random.random((3, 4, 10, 3, 3)) demo.validate() # Check assignment with correct broadcatable shape: 3 x 4 x 10 x 1 x 1 or 3 x 1 x 10 demo.five_dimensions_provided = np.random.random((3, 4, 10, 1, 1)) demo.three_dimensions_provided = np.random.random((3, 1, 10)) + demo.mixed_dimensions_provided = np.random.random((3, 1, 10)) demo.validate() # Check for correct number of dimensions, but wrong shape with pytest.raises(ValueError): demo.five_dimensions_provided = np.random.random((3, 3, 3, 3, 3)) + with pytest.raises(ValueError): + demo.mixed_dimensions_provided = np.random.random((4, 3, 10, 1, 1)) + + with pytest.raises(ValueError): + demo.mixed_dimensions_provided = np.random.random((4, 3, 10)) + with pytest.raises(ValueError): demo.three_dimensions_provided = np.random.random((3, 5, 10)) From 4712b91be15ffdba46f83bd534e7d257c75e5da7 Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Wed, 13 Dec 2023 08:25:33 -0800 Subject: [PATCH 13/17] add initial 0 value for n_turbines and grid_resolution --- floris/simulation/flow_field.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index 5c14fec15..bdf186d58 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -49,8 +49,8 @@ class FlowField(BaseClass): n_wind_directions: int = field(init=False) n_wind_speeds: int = field(init=False) - n_turbines: int = field(init=False) - grid_resolution: int = field(init=False) + n_turbines: int = field(init=False, default=0) + grid_resolution: int = field(init=False, default=0) u_initial_sorted: NDArrayFloat = array_5D_field v_initial_sorted: NDArrayFloat = array_5D_field From 324b1a5fdf5db6de1474e7952295bb6ad2700e68 Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Mon, 18 Dec 2023 15:54:43 -0800 Subject: [PATCH 14/17] fix mismatched dimensions --- examples/01_opening_floris_computing_power.py | 2 +- examples/30_multi_dimensional_cp_ct.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/01_opening_floris_computing_power.py b/examples/01_opening_floris_computing_power.py index b006dfe4d..a7b8296ef 100644 --- a/examples/01_opening_floris_computing_power.py +++ b/examples/01_opening_floris_computing_power.py @@ -70,7 +70,7 @@ wind_directions = np.array([260., 270., 280.]) wind_speeds = np.array([8.0, 9.0, 10.0]) fi.reinitialize(wind_directions=wind_directions, wind_speeds=wind_speeds) -yaw_angles = np.zeros([1,3,2]) # 1 wind direction, 3 wind speeds, 2 turbines +yaw_angles = np.zeros([3,3,2]) # 1 wind direction, 3 wind speeds, 2 turbines fi.calculate_wake(yaw_angles=yaw_angles) turbine_powers = fi.get_turbine_powers()/1000. print('The turbine power matrix should be of dimensions 3 WD X 3 WS X 2 Turbines') diff --git a/examples/30_multi_dimensional_cp_ct.py b/examples/30_multi_dimensional_cp_ct.py index 2d2303018..29c5070f8 100644 --- a/examples/30_multi_dimensional_cp_ct.py +++ b/examples/30_multi_dimensional_cp_ct.py @@ -96,7 +96,7 @@ wind_directions = np.array([260., 270., 280.]) wind_speeds = np.array([8.0, 9.0, 10.0]) fi.reinitialize(wind_directions=wind_directions, wind_speeds=wind_speeds) -yaw_angles = np.zeros([1,3,2]) # 1 wind direction, 3 wind speeds, 2 turbines +yaw_angles = np.zeros([3,3,2]) # 1 wind direction, 3 wind speeds, 2 turbines fi.calculate_wake(yaw_angles=yaw_angles) turbine_powers = fi.get_turbine_powers_multidim()/1000. print('The turbine power matrix should be of dimensions 3 WD X 3 WS X 2 Turbines') From f2a0dd4cbf2bb5fabc872c281b3f28aacc0e74c7 Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Mon, 18 Dec 2023 15:58:32 -0800 Subject: [PATCH 15/17] add grid shape as an attribute defined in set_grid --- floris/simulation/flow_field.py | 18 +++++++++++++++--- floris/simulation/grid.py | 28 +++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index bdf186d58..687210551 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -47,10 +47,10 @@ class FlowField(BaseClass): heterogenous_inflow_config: dict = field(default=None) multidim_conditions: dict = field(default=None) - n_wind_directions: int = field(init=False) n_wind_speeds: int = field(init=False) + n_wind_directions: int = field(init=False) n_turbines: int = field(init=False, default=0) - grid_resolution: int = field(init=False, default=0) + grid_shape: tuple[int, int, int, int, int] = field(init=False, default=(0, 0, 0, 0, 0)) u_initial_sorted: NDArrayFloat = array_5D_field v_initial_sorted: NDArrayFloat = array_5D_field @@ -113,6 +113,18 @@ def het_map_validator(self, instance: attrs.Attribute, value: list | None) -> No "The het_map's wind direction dimension not equal to number of wind directions." ) + @grid_shape.validator + def grid_shape_validator(self, attribute: attrs.Attribute, value: tuple) -> None: + """Validates that ``grid_shape`` is length-5 tuple of integers. + + Args: + attribute (attrs.Attribute): The attrs Attribute data. + value (tuple): A length-5 tuple of integers. + """ + if len(value) != 5: + raise ValueError("`grid_shape` must be a tuple of 5 integer values.") + if not all(isinstance(v, int) for v in value): + raise TypeError("`grid_shape` must be a tuple of 5 integer values.") def __attrs_post_init__(self) -> None: if self.heterogenous_inflow_config is not None: @@ -132,7 +144,7 @@ def initialize_velocity_field(self, grid: Grid) -> None: # for height, using it here to apply the shear law makes that dimension store the vertical # wind profile. self.n_turbines = grid.n_turbines - self.grid_resolution = grid.grid_resolution + self.grid_shape = grid.grid_shape wind_profile_plane = (grid.z_sorted / self.reference_wind_height) ** self.wind_shear dwind_profile_plane = ( diff --git a/floris/simulation/grid.py b/floris/simulation/grid.py index 321949e15..4b505773d 100644 --- a/floris/simulation/grid.py +++ b/floris/simulation/grid.py @@ -79,6 +79,7 @@ class Grid(ABC, BaseClass): n_wind_directions: int = field(init=False) n_wind_speeds: int = field(init=False) n_turbines: int = field(init=False) + grid_shape: tuple[int, int, int, int, int] = field(init=False) x_sorted: NDArrayFloat = field(init=False, validator=validate_5DArray_shape) y_sorted: NDArrayFloat = field(init=False, validator=validate_5DArray_shape) z_sorted: NDArrayFloat = field(init=False, validator=validate_5DArray_shape) @@ -123,12 +124,16 @@ def grid_resolution_validator(self, instance: attrs.Attribute, value: int | Iter isinstance(self, (TurbineGrid, TurbineCubatureGrid, PointsGrid)): return elif isinstance(value, Iterable) and isinstance(self, FlowFieldPlanarGrid): - assert type(value[0]) is int - assert type(value[1]) is int + if not (len(value) == 2 and all(isinstance(v, int) for v in value)): + raise TypeError( + "`FlowFieldPlanarGrid` must have `grid_resolution` as an iterable of 2 `int`s.", + value + ) elif isinstance(value, Iterable) and isinstance(self, FlowFieldGrid): - assert type(value[0]) is int - assert type(value[1]) is int - assert type(value[2]) is int + if len(value) != 3 or all(isinstance(v, int) for v in value): + raise TypeError( + "'FlowFieldGrid` must have `grid_resolution` as an iterable of 3 `int`s.", value + ) else: raise TypeError("`grid_resolution` must be of type int or Iterable(int,)") @@ -244,6 +249,7 @@ def set_grid(self) -> None: ), dtype=floris_float_type ) + self.grid_shape = template_grid.shape # Calculate the radial distance from the center of the turbine rotor. # If a grid resolution of 1 is selected, create a disc_grid of zeros, as # np.linspace would just return the starting value of -1 * disc_area_radius @@ -365,6 +371,7 @@ def set_grid(self) -> None: ), dtype=floris_float_type ) + self.grid_shape = template_grid.shape _x = x[:, :, :, None, None] * template_grid _y = y[:, :, :, None, None] * template_grid _z = z[:, :, :, None, None] * template_grid @@ -509,6 +516,7 @@ def set_grid(self) -> None: First, sort the turbines so that we know the bounds in the correct orientation. Then, create the grid based on this wind-from-left orientation """ + self.grid_shape = (self.n_wind_directions, self.n_wind_speeds, *self.grid_resolution) # These are the rotated coordinates of the wind turbines based on the wind direction x, y, z, self.x_center_of_rotation, self.y_center_of_rotation = rotate_coordinates_rel_west( @@ -605,6 +613,9 @@ def set_grid(self) -> None: if self.x2_bounds is None: self.x2_bounds = (np.min(y) - 2 * max_diameter, np.max(y) + 2 * max_diameter) + grid_resolution = (self.grid_resolution[0], self.grid_resolution[1], 3) + self.grid_shape = (self.n_wind_directions, self.n_wind_speeds, *grid_resolution) + # TODO figure out proper z spacing for GCH, currently set to +/- 10.0 x_points, y_points, z_points = np.meshgrid( np.linspace(self.x1_bounds[0], self.x1_bounds[1], int(self.grid_resolution[0])), @@ -628,6 +639,9 @@ def set_grid(self) -> None: if self.x2_bounds is None: self.x2_bounds = (0.001, 6 * np.max(z)) + grid_resolution = (1, self.grid_resolution[0], self.grid_resolution[1]) + self.grid_shape = (self.n_wind_directions, self.n_wind_speeds, *grid_resolution) + x_points, y_points, z_points = np.meshgrid( np.array([float(self.planar_coordinate)]), np.linspace(self.x1_bounds[0], self.x1_bounds[1], int(self.grid_resolution[0])), @@ -646,6 +660,9 @@ def set_grid(self) -> None: if self.x2_bounds is None: self.x2_bounds = (0.001, 6 * np.max(z)) + grid_resolution = (self.grid_resolution[0], 1, self.grid_resolution[1]) + self.grid_shape = (self.n_wind_directions, self.n_wind_speeds, *grid_resolution) + x_points, y_points, z_points = np.meshgrid( np.linspace(self.x1_bounds[0], self.x1_bounds[1], int(self.grid_resolution[0])), np.array([float(self.planar_coordinate)]), @@ -714,6 +731,7 @@ def set_grid(self) -> None: Set points for calculation based on a series of user-supplied coordinates. """ point_coordinates = np.array(list(zip(self.points_x, self.points_y, self.points_z))) + self.grid_shape = (self.n_wind_directions, self.n_wind_speeds, self.points_x.shape[0], 1, 1) # These are the rotated coordinates of the wind turbines based on the wind direction x, y, z, _, _ = rotate_coordinates_rel_west( From 053e1f2ab93b1e0c6a401e021b15447a574be87e Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:04:10 -0800 Subject: [PATCH 16/17] update checking for grid_shape --- floris/type_dec.py | 8 +++----- tests/type_dec_unit_test.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/floris/type_dec.py b/floris/type_dec.py index 0ef047bfe..5c8d3842d 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -193,22 +193,20 @@ def validate_5DArray_shape(instance, attribute: Attribute, value: np.ndarray) -> N wind directions x N wind speeds x N turbines x N grid points x N grid points. """ if not isinstance(value, np.ndarray): - print(type(value)) raise TypeError(f"`{attribute.name}` is not a valid NumPy array type.") # Don't fail on the initialized empty array if value.size == 0: return - grid = instance.grid_resolution - shape = (instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines, grid, grid) - if value.shape != shape: + if value.shape != instance.grid_shape: broadcast_shape = ( instance.n_wind_directions, instance.n_wind_speeds, instance.n_turbines, 1, 1 ) if value.shape != broadcast_shape: raise ValueError( - f"`{attribute.name}` should have shape: {shape}; not shape: {value.shape}" + f"`{attribute.name}` should have shape: {instance.grid_shape}; not shape: " + f"{value.shape}" ) diff --git a/tests/type_dec_unit_test.py b/tests/type_dec_unit_test.py index a4f09184f..ff21b7858 100644 --- a/tests/type_dec_unit_test.py +++ b/tests/type_dec_unit_test.py @@ -62,7 +62,7 @@ class ArrayValidatorDemoClass(ValidateMixin): n_wind_directions: int = field(default=3) n_wind_speeds: int = field(default=4) n_turbines: int = field(default=10) - grid_resolution: int = field(default=3) + grid_shape: tuple[int, int, int, int, int] = field(init=False) three_dim_starts_as_one: NDArrayFloat = field( factory=lambda: np.array([]), validator=validate_3DArray_shape ) @@ -70,6 +70,15 @@ class ArrayValidatorDemoClass(ValidateMixin): three_dimensions_provided: NDArrayFloat = array_3D_field mixed_dimensions_provided: NDArrayFloat = array_mixed_dim_field + def set_grid(self, grid_resolution: int): + self.grid_shape = ( + self.n_wind_directions, + self.n_wind_speeds, + self.n_turbines, + grid_resolution, + grid_resolution + ) + def test_as_dict(): # Non-initialized attributes should not be exported @@ -154,6 +163,7 @@ def test_array_validators(): # Check initialization works demo = ArrayValidatorDemoClass() + demo.set_grid(3) demo.validate() # Check assignment with correct shape: 3 x 4 x 10 (x 3 x 3) From a4b4d1610aeceff9f3f698672b203461958de78f Mon Sep 17 00:00:00 2001 From: RHammond2 <13874373+RHammond2@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:08:08 -0800 Subject: [PATCH 17/17] add backwards compatibility for typing in test --- tests/type_dec_unit_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/type_dec_unit_test.py b/tests/type_dec_unit_test.py index ff21b7858..5b31b46c9 100644 --- a/tests/type_dec_unit_test.py +++ b/tests/type_dec_unit_test.py @@ -12,6 +12,8 @@ # See https://floris.readthedocs.io for documentation +from __future__ import annotations + from pathlib import Path from typing import List