From 61e1f13e9b5956fd103451ce549359fda54d0cb9 Mon Sep 17 00:00:00 2001 From: paulf81 Date: Fri, 2 Feb 2024 22:04:54 -0700 Subject: [PATCH] Convert turbulence intensity from single value to n_findex length array (#782) * Convert turbulence_intensity to tubulence_intensities throughout the code and refactor all code to expect turbulence_intensities to be an array and not a float * Add additional tests of turbulence intensity to confirm correct behavior of new features * Complete WindRose and TimeSeries handling of turbulence intensities * Add helper functions to WindRose and TimeSeries which allow turbulence intensities to be generated, rather than provided, as a function of wind directions and wind speeds * Add additional examples of usage --------- Co-authored-by: misi9170 Co-authored-by: Rafael M Mudafort Co-authored-by: Eric Simley --- examples/12_optimize_yaw_in_parallel.py | 4 +- examples/19_streamlit_demo.py | 4 +- examples/34_wind_data.py | 2 +- examples/35_sweep_ti.py | 62 ++++++++ examples/36_generate_ti.py | 82 +++++++++++ examples/inputs/cc.yaml | 3 +- examples/inputs/emgauss.yaml | 3 +- examples/inputs/gch.yaml | 3 +- examples/inputs/gch_heterogeneous_inflow.yaml | 3 +- examples/inputs/gch_multi_dim_cp_ct.yaml | 3 +- .../inputs/gch_multiple_turbine_types.yaml | 3 +- examples/inputs/jensen.yaml | 3 +- examples/inputs/turbopark.yaml | 3 +- examples/inputs_floating/emgauss_fixed.yaml | 3 +- .../inputs_floating/emgauss_floating.yaml | 3 +- .../emgauss_floating_fixedtilt15.yaml | 3 +- .../emgauss_floating_fixedtilt5.yaml | 3 +- examples/inputs_floating/gch_fixed.yaml | 3 +- examples/inputs_floating/gch_floating.yaml | 3 +- .../gch_floating_defined_floating.yaml | 3 +- floris/simulation/flow_field.py | 30 ++-- floris/simulation/solver.py | 61 ++++---- floris/simulation/wake_velocity/turbopark.py | 4 +- floris/tools/floris_interface.py | 37 ++++- .../yaw_optimization/yaw_optimization_base.py | 28 ++-- floris/tools/parallel_computing_interface.py | 6 +- floris/tools/uncertainty_interface.py | 4 +- floris/tools/wind_data.py | 135 ++++++++++++++++-- floris/type_dec.py | 49 ++++++- tests/conftest.py | 2 +- .../{input_full_v3.yaml => input_full.yaml} | 3 +- tests/floris_interface_test.py | 60 +++++++- tests/floris_unit_test.py | 2 +- tests/flow_field_unit_test.py | 17 +++ tests/type_dec_unit_test.py | 48 ++++++- tests/wind_data_test.py | 4 +- 36 files changed, 584 insertions(+), 105 deletions(-) create mode 100644 examples/35_sweep_ti.py create mode 100644 examples/36_generate_ti.py rename tests/data/{input_full_v3.yaml => input_full.yaml} (97%) diff --git a/examples/12_optimize_yaw_in_parallel.py b/examples/12_optimize_yaw_in_parallel.py index 33c996dc1..c4233f5ef 100644 --- a/examples/12_optimize_yaw_in_parallel.py +++ b/examples/12_optimize_yaw_in_parallel.py @@ -63,7 +63,7 @@ def load_windrose(): fi_aep.reinitialize( wind_directions=wind_directions, wind_speeds=wind_speeds, - turbulence_intensity=0.08 # Assume 8% turbulence intensity + turbulence_intensities=[0.08], # Assume 8% turbulence intensity ) # Pour this into a parallel computing interface @@ -105,7 +105,7 @@ def load_windrose(): fi_opt.reinitialize( wind_directions=wind_directions, wind_speeds=wind_speeds, - turbulence_intensity=0.08 # Assume 8% turbulence intensity + turbulence_intensities=[0.08], # Assume 8% turbulence intensity ) # Pour this into a parallel computing interface diff --git a/examples/19_streamlit_demo.py b/examples/19_streamlit_demo.py index d40296c19..91b4f466d 100644 --- a/examples/19_streamlit_demo.py +++ b/examples/19_streamlit_demo.py @@ -124,7 +124,7 @@ layout_y=Y, wind_speeds=[wind_speed], wind_directions=[wind_direction], - turbulence_intensity=turbulence_intensity + turbulence_intensities=[turbulence_intensity], ) fi.calculate_wake(yaw_angles=yaw_angles_base) @@ -168,7 +168,7 @@ layout_y=Y, wind_speeds=[wind_speed], wind_directions=[wind_direction], - turbulence_intensity=turbulence_intensity + turbulence_intensities=[turbulence_intensity], ) fi.calculate_wake(yaw_angles=yaw_angles_yaw) diff --git a/examples/34_wind_data.py b/examples/34_wind_data.py index f3e87686d..5da902880 100644 --- a/examples/34_wind_data.py +++ b/examples/34_wind_data.py @@ -50,7 +50,7 @@ # Build the time series -time_series = TimeSeries(wd_array, ws_array) # , turbulence_intensity=ti_array) +time_series = TimeSeries(wd_array, ws_array, turbulence_intensities=ti_array) # Now build the wind rose wind_rose = time_series.to_wind_rose() diff --git a/examples/35_sweep_ti.py b/examples/35_sweep_ti.py new file mode 100644 index 000000000..6e235a9aa --- /dev/null +++ b/examples/35_sweep_ti.py @@ -0,0 +1,62 @@ +# Copyright 2024 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + +import matplotlib.pyplot as plt +import numpy as np + +from floris.tools import ( + FlorisInterface, + TimeSeries, + WindRose, +) +from floris.utilities import wrap_360 + + +""" +Demonstrate the new behavior in V4 where TI is an array rather than a float. +Set up an array of two turbines and sweep TI while holding wd/ws constant. +Use the TimeSeries object to drive the FLORIS calculations. +""" + + +# Generate a random time series of wind speeds, wind directions and turbulence intensities +N = 50 +wd_array = 270.0 * np.ones(N) +ws_array = 8.0 * np.ones(N) +ti_array = np.linspace(0.03, 0.2, N) + + +# Build the time series +time_series = TimeSeries(wd_array, ws_array, turbulence_intensities=ti_array) + + +# Now set up a FLORIS model and initialize it using the time +fi = FlorisInterface("inputs/gch.yaml") +fi.reinitialize(layout_x=[0, 500.0], layout_y=[0.0, 0.0], wind_data=time_series) +fi.calculate_wake() +turbine_power = fi.get_turbine_powers() + +fig, axarr = plt.subplots(2, 1, sharex=True, figsize=(6, 6)) +ax = axarr[0] +ax.plot(ti_array*100, turbine_power[:, 0]/1000, color="k") +ax.set_ylabel("Front turbine power [kW]") +ax = axarr[1] +ax.plot(ti_array*100, turbine_power[:, 1]/1000, color="k") +ax.set_ylabel("Rear turbine power [kW]") +ax.set_xlabel("Turbulence intensity [%]") + +for ax in axarr: + ax.grid(True) + +plt.show() diff --git a/examples/36_generate_ti.py b/examples/36_generate_ti.py new file mode 100644 index 000000000..a42e1bf95 --- /dev/null +++ b/examples/36_generate_ti.py @@ -0,0 +1,82 @@ +# Copyright 2024 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + +import matplotlib.pyplot as plt +import numpy as np + +from floris.tools import ( + FlorisInterface, + TimeSeries, + WindRose, +) +from floris.utilities import wrap_360 + + +""" +Demonstrate usage of TI generating and plotting functionality in the WindRose +and TimeSeries classes +""" + + +# Generate a random time series of wind speeds, wind directions and turbulence intensities +wind_directions = np.array([250, 260, 270]) +wind_speeds = np.array([5, 6, 7, 8, 9, 10]) + +# Declare a WindRose object +wind_rose = WindRose(wind_directions=wind_directions, wind_speeds=wind_speeds) + + +# Define a custom function where TI = 1 / wind_speed +def custom_ti_func(wind_directions, wind_speeds): + return 1 / wind_speeds + + +wind_rose.assign_ti_using_wd_ws_function(custom_ti_func) + +fig, ax = plt.subplots() +wind_rose.plot_ti_over_ws(ax) +ax.set_title("Turbulence Intensity defined by custom function") + +# Now use the normal turbulence model approach from the IEC 61400-1 standard, +# wherein TI is defined as a function of wind speed: +# Iref is defined as the TI value at 15 m/s. Note that Iref = 0.07 is lower +# than the values of Iref used in the IEC standard, but produces TI values more +# in line with those typically used in FLORIS (TI=8.6% at 8 m/s). +Iref = 0.07 +wind_rose.assign_ti_using_IEC_method(Iref) +fig, ax = plt.subplots() +wind_rose.plot_ti_over_ws(ax) +ax.set_title(f"Turbulence Intensity defined by Iref = {Iref:0.2}") + + +# Demonstrate equivalent usage in time series +N = 100 +wind_directions = 270 * np.ones(N) +wind_speeds = np.linspace(5, 15, N) +time_series = TimeSeries(wind_directions=wind_directions, wind_speeds=wind_speeds) +time_series.assign_ti_using_IEC_method(Iref=Iref) + +fig, axarr = plt.subplots(2, 1, sharex=True, figsize=(7, 8)) +ax = axarr[0] +ax.plot(wind_speeds) +ax.set_ylabel("Wind Speeds (m/s)") +ax.grid(True) +ax = axarr[1] +ax.plot(time_series.turbulence_intensities) +ax.set_ylabel("Turbulence Intensity (-)") +ax.grid(True) +fig.suptitle("Generating TI in TimeSeries") + + +plt.show() diff --git a/examples/inputs/cc.yaml b/examples/inputs/cc.yaml index 922fadd05..af62b0021 100644 --- a/examples/inputs/cc.yaml +++ b/examples/inputs/cc.yaml @@ -30,7 +30,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs/emgauss.yaml b/examples/inputs/emgauss.yaml index f984f421d..73344d5ea 100644 --- a/examples/inputs/emgauss.yaml +++ b/examples/inputs/emgauss.yaml @@ -30,7 +30,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs/gch.yaml b/examples/inputs/gch.yaml index 220fafeac..2cd76c7f5 100644 --- a/examples/inputs/gch.yaml +++ b/examples/inputs/gch.yaml @@ -112,7 +112,8 @@ flow_field: ### # The level of turbulence intensity level in the wind. - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 ### # The wind directions to include in the simulation. diff --git a/examples/inputs/gch_heterogeneous_inflow.yaml b/examples/inputs/gch_heterogeneous_inflow.yaml index d7cffa0d5..86507e287 100644 --- a/examples/inputs/gch_heterogeneous_inflow.yaml +++ b/examples/inputs/gch_heterogeneous_inflow.yaml @@ -44,7 +44,8 @@ flow_field: - -300. - 300. reference_wind_height: -1 - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs/gch_multi_dim_cp_ct.yaml b/examples/inputs/gch_multi_dim_cp_ct.yaml index 8709fbcc7..e14976050 100644 --- a/examples/inputs/gch_multi_dim_cp_ct.yaml +++ b/examples/inputs/gch_multi_dim_cp_ct.yaml @@ -33,7 +33,8 @@ flow_field: Hs: 3.01 air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs/gch_multiple_turbine_types.yaml b/examples/inputs/gch_multiple_turbine_types.yaml index ca2d86ea5..0ead479a1 100644 --- a/examples/inputs/gch_multiple_turbine_types.yaml +++ b/examples/inputs/gch_multiple_turbine_types.yaml @@ -29,7 +29,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: 90.0 # Since multiple defined turbines, must specify explicitly the reference wind height - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs/jensen.yaml b/examples/inputs/jensen.yaml index abb889e0a..6b4ac0dd6 100644 --- a/examples/inputs/jensen.yaml +++ b/examples/inputs/jensen.yaml @@ -30,7 +30,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs/turbopark.yaml b/examples/inputs/turbopark.yaml index 85bda5fef..682b1e801 100644 --- a/examples/inputs/turbopark.yaml +++ b/examples/inputs/turbopark.yaml @@ -30,7 +30,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: 90.0 - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/emgauss_fixed.yaml b/examples/inputs_floating/emgauss_fixed.yaml index 9d0b23960..76c3c4513 100644 --- a/examples/inputs_floating/emgauss_fixed.yaml +++ b/examples/inputs_floating/emgauss_fixed.yaml @@ -30,7 +30,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/emgauss_floating.yaml b/examples/inputs_floating/emgauss_floating.yaml index 1fd66d217..965ef7549 100644 --- a/examples/inputs_floating/emgauss_floating.yaml +++ b/examples/inputs_floating/emgauss_floating.yaml @@ -30,7 +30,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml b/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml index dfb4e3155..e8a452325 100644 --- a/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml +++ b/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml @@ -26,7 +26,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml b/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml index 67be5dfd3..7732b6213 100644 --- a/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml +++ b/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml @@ -26,7 +26,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 # -1 is code for use the hub height - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/gch_fixed.yaml b/examples/inputs_floating/gch_fixed.yaml index 497cecc95..be03460e1 100644 --- a/examples/inputs_floating/gch_fixed.yaml +++ b/examples/inputs_floating/gch_fixed.yaml @@ -26,7 +26,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/gch_floating.yaml b/examples/inputs_floating/gch_floating.yaml index 31ff7c606..09aaa5604 100644 --- a/examples/inputs_floating/gch_floating.yaml +++ b/examples/inputs_floating/gch_floating.yaml @@ -27,7 +27,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/examples/inputs_floating/gch_floating_defined_floating.yaml b/examples/inputs_floating/gch_floating_defined_floating.yaml index 3096e4c2a..d540c8d47 100644 --- a/examples/inputs_floating/gch_floating_defined_floating.yaml +++ b/examples/inputs_floating/gch_floating_defined_floating.yaml @@ -26,7 +26,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: -1 - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index a53db1fa9..bd26addc9 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -39,7 +39,7 @@ class FlowField(BaseClass): wind_veer: float = field(converter=float) wind_shear: float = field(converter=float) air_density: float = field(converter=float) - turbulence_intensity: float = field(converter=float) + turbulence_intensities: NDArrayFloat = field(converter=floris_array_converter) reference_wind_height: float = field(converter=float) time_series: bool = field(default=False) heterogenous_inflow_config: dict = field(default=None) @@ -66,6 +66,17 @@ class FlowField(BaseClass): init=False, factory=lambda: np.array([]) ) + @turbulence_intensities.validator + def turbulence_intensities_validator( + self, instance: attrs.Attribute, value: NDArrayFloat + ) -> None: + + # Check the turbulence intensity is either length 1 or n_findex + if len(value) != 1 and len(value) != self.n_findex: + raise ValueError("turbulence_intensities should either be length 1 or n_findex") + + + @wind_directions.validator def wind_directions_validator(self, instance: attrs.Attribute, value: NDArrayFloat) -> None: """Using the validator method to keep the `n_findex` attribute up to date.""" @@ -108,6 +119,10 @@ def __attrs_post_init__(self) -> None: if self.heterogenous_inflow_config is not None: self.generate_heterogeneous_wind_map() + # If turbulence_intensity is length 1, then convert it to a uniform array of + # length n_findex + if len(self.turbulence_intensities) == 1: + self.turbulence_intensities = self.turbulence_intensities[0] * np.ones(self.n_findex) def initialize_velocity_field(self, grid: Grid) -> None: @@ -197,14 +212,13 @@ def initialize_velocity_field(self, grid: Grid) -> None: self.v_sorted = self.v_initial_sorted.copy() self.w_sorted = self.w_initial_sorted.copy() - self.turbulence_intensity_field = self.turbulence_intensity * np.ones( - ( - self.n_findex, - grid.n_turbines, - 1, - 1, - ) + self.turbulence_intensity_field = self.turbulence_intensities[:, None, None, None] + self.turbulence_intensity_field = np.repeat( + self.turbulence_intensity_field, + grid.n_turbines, + axis=1 ) + self.turbulence_intensity_field_sorted = self.turbulence_intensity_field.copy() def finalize(self, unsorted_indices): diff --git a/floris/simulation/solver.py b/floris/simulation/solver.py index d32ef9d15..c80f355cc 100644 --- a/floris/simulation/solver.py +++ b/floris/simulation/solver.py @@ -76,11 +76,14 @@ def sequential_solver( v_wake = np.zeros_like(flow_field.v_initial_sorted) w_wake = np.zeros_like(flow_field.w_initial_sorted) - turbine_turbulence_intensity = ( - flow_field.turbulence_intensity - * np.ones((flow_field.n_findex, farm.n_turbines, 1, 1)) - ) - ambient_turbulence_intensity = flow_field.turbulence_intensity + # Expand input turbulence intensity to 4d for (n_turbines, grid, grid) + turbine_turbulence_intensity = flow_field.turbulence_intensities[:, None, None, None] + turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) + + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity + # with dimensions expanded for (n_turbines, grid, grid) + ambient_turbulence_intensities = flow_field.turbulence_intensities.copy() + ambient_turbulence_intensities = ambient_turbulence_intensities[:, None, None, None] # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): @@ -217,7 +220,7 @@ def sequential_solver( ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensity, + ambient_turbulence_intensities, grid.x_sorted, x_i, rotor_diameter_i, @@ -243,8 +246,7 @@ def sequential_solver( # Combine turbine TIs with WAT turbine_turbulence_intensity = np.maximum( - np.sqrt( ti_added ** 2 + ambient_turbulence_intensity ** 2 ), - turbine_turbulence_intensity + np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbine_turbulence_intensity ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field @@ -450,10 +452,14 @@ def cc_solver( turb_u_wake = np.zeros_like(flow_field.u_initial_sorted) turb_inflow_field = copy.deepcopy(flow_field.u_initial_sorted) - turbine_turbulence_intensity = ( - flow_field.turbulence_intensity * np.ones((flow_field.n_findex, farm.n_turbines, 1, 1)) - ) - ambient_turbulence_intensity = flow_field.turbulence_intensity + # Set up turbulence arrays + turbine_turbulence_intensity = flow_field.turbulence_intensities[:, None, None, None] + turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) + + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensities + # with extra dimension to reach 4d + ambient_turbulence_intensities = flow_field.turbulence_intensities.copy() + ambient_turbulence_intensities = ambient_turbulence_intensities[:, None, None, None] shape = (farm.n_turbines,) + np.shape(flow_field.u_initial_sorted) Ctmp = np.zeros((shape)) @@ -618,7 +624,7 @@ def cc_solver( ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensity, + ambient_turbulence_intensities, grid.x_sorted, x_i, rotor_diameter_i, @@ -644,8 +650,7 @@ def cc_solver( # Combine turbine TIs with WAT turbine_turbulence_intensity = np.maximum( - np.sqrt(ti_added ** 2 + ambient_turbulence_intensity ** 2), - turbine_turbulence_intensity + np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbine_turbulence_intensity ) flow_field.v_sorted += v_wake @@ -862,11 +867,14 @@ def turbopark_solver( velocity_deficit = np.zeros(shape) deflection_field = np.zeros_like(flow_field.u_initial_sorted) - turbine_turbulence_intensity = ( - flow_field.turbulence_intensity - * np.ones((flow_field.n_findex, farm.n_turbines, 1, 1)) - ) - ambient_turbulence_intensity = flow_field.turbulence_intensity + # Set up turbulence arrays + turbine_turbulence_intensity = flow_field.turbulence_intensities[:, None, None, None] + turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) + + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensities + # with extra dimension to reach 4d + ambient_turbulence_intensities = flow_field.turbulence_intensities.copy() + ambient_turbulence_intensities = ambient_turbulence_intensities[:, None, None, None] # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): @@ -1045,7 +1053,7 @@ def turbopark_solver( ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensity, + ambient_turbulence_intensities, grid.x_sorted, x_i, rotor_diameter_i, @@ -1074,8 +1082,7 @@ def turbopark_solver( # Combine turbine TIs with WAT turbine_turbulence_intensity = np.maximum( - np.sqrt( ti_added ** 2 + ambient_turbulence_intensity ** 2 ), - turbine_turbulence_intensity + np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbine_turbulence_intensity ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field @@ -1141,13 +1148,15 @@ def empirical_gauss_solver( np.repeat(farm.rotor_diameters_sorted[:,:,None], grid.n_turbines, axis=-1) downstream_distance_D = np.maximum(downstream_distance_D, 0.1) # For ease # Initialize the mixing factor model using TI if specified - initial_mixing_factor = model_manager.turbulence_model.atmospheric_ti_gain*\ - flow_field.turbulence_intensity*np.eye(grid.n_turbines) + initial_mixing_factor = model_manager.turbulence_model.atmospheric_ti_gain * np.eye( + grid.n_turbines + ) mixing_factor = np.repeat( - initial_mixing_factor[None,:,:], + initial_mixing_factor[None, :, :], flow_field.n_findex, axis=0 ) + mixing_factor = mixing_factor * flow_field.turbulence_intensities[:, None, None] # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): diff --git a/floris/simulation/wake_velocity/turbopark.py b/floris/simulation/wake_velocity/turbopark.py index 0b52c0476..637c30d34 100644 --- a/floris/simulation/wake_velocity/turbopark.py +++ b/floris/simulation/wake_velocity/turbopark.py @@ -80,7 +80,7 @@ def function( x_i: np.ndarray, y_i: np.ndarray, z_i: np.ndarray, - ambient_turbulence_intensity: np.ndarray, + ambient_turbulence_intensities: np.ndarray, Cts: np.ndarray, rotor_diameter_i: np.ndarray, rotor_diameters: np.ndarray, @@ -112,7 +112,7 @@ def function( Cts[:, i:, :, :] = 0.00001 # Characteristic wake widths from all turbines relative to turbine i - dw = characteristic_wake_width(x_dist, ambient_turbulence_intensity, Cts, self.A) + dw = characteristic_wake_width(x_dist, ambient_turbulence_intensities, Cts, self.A) epsilon = 0.25 * np.sqrt( np.min( 0.5 * (1 + np.sqrt(1 - Cts)) / np.sqrt(1 - Cts), 3, keepdims=True ) ) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 5721dfa51..f94bd13bb 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -191,7 +191,7 @@ def reinitialize( wind_shear: float | None = None, wind_veer: float | None = None, reference_wind_height: float | None = None, - turbulence_intensity: float | None = None, + turbulence_intensities: list[float] | NDArrayFloat | None = None, # turbulence_kinetic_energy=None, air_density: float | None = None, # wake: WakeModelManager = None, @@ -218,13 +218,17 @@ def reinitialize( if ( (wind_directions is not None) or (wind_speeds is not None) - or (turbulence_intensity is not None) + or (turbulence_intensities is not None) ): raise ValueError( "If wind_data is passed to reinitialize, then do not pass wind_directions, " - "wind_speeds or turbulence_intensity as this is redundant." + "wind_speeds or turbulence_intensities as this is redundant" ) - wind_directions, wind_speeds, turbulence_intensity = wind_data.unpack_for_reinitialize() + ( + wind_directions, + wind_speeds, + turbulence_intensities, + ) = wind_data.unpack_for_reinitialize() ## FlowField if wind_speeds is not None: @@ -237,13 +241,34 @@ def reinitialize( flow_field_dict["wind_veer"] = wind_veer if reference_wind_height is not None: flow_field_dict["reference_wind_height"] = reference_wind_height - if turbulence_intensity is not None: - flow_field_dict["turbulence_intensity"] = turbulence_intensity + if turbulence_intensities is not None: + flow_field_dict["turbulence_intensities"] = turbulence_intensities if air_density is not None: flow_field_dict["air_density"] = air_density if heterogenous_inflow_config is not None: flow_field_dict["heterogenous_inflow_config"] = heterogenous_inflow_config + # Handle a special case where: + # wind_speeds | wind_directions are not None + # turbulence_intensities is None + # len(turbulence intensity) != len(wind_directions) + # turbulence_intensities is uniform + # In this case, automatically resize turbulence intensity + # This is the case where user is assuming same TI across all findex + if ( + (wind_speeds is not None or wind_directions is not None) + and turbulence_intensities is None + and ( + len(flow_field_dict["turbulence_intensities"]) + != len(flow_field_dict["wind_directions"]) + ) + and len(np.unique(flow_field_dict["turbulence_intensities"])) == 1 + ): + flow_field_dict["turbulence_intensities"] = ( + flow_field_dict["turbulence_intensities"][0] + * np.ones_like(flow_field_dict["wind_directions"]) + ) + ## Farm if layout_x is not None: farm_dict["layout_x"] = layout_x diff --git a/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py b/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py index baffb9822..c8bccea37 100644 --- a/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py +++ b/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py @@ -530,20 +530,26 @@ def _finalize(self, farm_power_opt_subset=None, yaw_angles_opt_subset=None): self.yaw_angles_opt = self._unreduce_variable(yaw_angles_opt_subset) # Produce output table - ti = np.min(self.fi.floris.flow_field.turbulence_intensity) + ti = np.min(self.fi.floris.flow_field.turbulence_intensities) df_list = [] num_wind_directions = len(self.fi.floris.flow_field.wind_directions) for ii, wind_speed in enumerate(self.fi.floris.flow_field.wind_speeds): - df_list.append(pd.DataFrame({ - "wind_direction": self.fi.floris.flow_field.wind_directions, - "wind_speed": wind_speed * np.ones(num_wind_directions), - "turbulence_intensity": ti * np.ones(num_wind_directions), - "yaw_angles_opt": list(self.yaw_angles_opt[:, ii, :]), - "farm_power_opt": None if self.farm_power_opt is None \ - else self.farm_power_opt[:, ii], - "farm_power_baseline": None if self.farm_power_baseline is None \ - else self.farm_power_baseline[:, ii], - })) + df_list.append( + pd.DataFrame( + { + "wind_direction": self.fi.floris.flow_field.wind_directions, + "wind_speed": wind_speed * np.ones(num_wind_directions), + "turbulence_intensities": ti * np.ones(num_wind_directions), + "yaw_angles_opt": list(self.yaw_angles_opt[:, ii, :]), + "farm_power_opt": None + if self.farm_power_opt is None + else self.farm_power_opt[:, ii], + "farm_power_baseline": None + if self.farm_power_baseline is None + else self.farm_power_baseline[:, ii], + } + ) + ) df_opt = pd.concat(df_list, axis=0) return df_opt diff --git a/floris/tools/parallel_computing_interface.py b/floris/tools/parallel_computing_interface.py index 1192fcfdb..235cedb97 100644 --- a/floris/tools/parallel_computing_interface.py +++ b/floris/tools/parallel_computing_interface.py @@ -166,7 +166,7 @@ def reinitialize( wind_shear=None, wind_veer=None, reference_wind_height=None, - turbulence_intensity=None, + turbulence_intensities=None, air_density=None, layout=None, layout_x=None, @@ -193,7 +193,7 @@ def reinitialize( wind_shear=wind_shear, wind_veer=wind_veer, reference_wind_height=reference_wind_height, - turbulence_intensity=turbulence_intensity, + turbulence_intensities=turbulence_intensities, air_density=air_density, layout_x=layout_x, layout_y=layout_y, @@ -550,7 +550,7 @@ def optimize_yaw_angles( [j[7] for j in multiargs], [j[8] for j in multiargs], [j[9] for j in multiargs], - [j[10] for j in multiargs] + [j[10] for j in multiargs], ) t2 = timerpc() diff --git a/floris/tools/uncertainty_interface.py b/floris/tools/uncertainty_interface.py index 7f2b833ef..aead4c887 100644 --- a/floris/tools/uncertainty_interface.py +++ b/floris/tools/uncertainty_interface.py @@ -332,7 +332,7 @@ def reinitialize( wind_shear=None, wind_veer=None, reference_wind_height=None, - turbulence_intensity=None, + turbulence_intensities=None, air_density=None, layout_x=None, layout_y=None, @@ -350,7 +350,7 @@ def reinitialize( wind_shear=wind_shear, wind_veer=wind_veer, reference_wind_height=reference_wind_height, - turbulence_intensity=turbulence_intensity, + turbulence_intensities=turbulence_intensities, air_density=air_density, layout_x=layout_x, layout_y=layout_y, diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 9331ddb6b..ebf1c989c 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -333,6 +333,74 @@ def plot_wind_rose( return ax + def assign_ti_using_wd_ws_function(self, func): + """ + Use the passed in function to assign new values to turbulence_intensities + + Args: + func (function): Function which accepts wind_directions as its + first argument and wind_speeds as second argument and returns + turbulence_intensities + """ + self.ti_table = func(self.wd_grid, self.ws_grid) + self._build_gridded_and_flattened_version() + + def assign_ti_using_IEC_method(self, Iref=0.07, offset=3.8): + """ + Define TI as a function of wind speed by specifying an Iref and offset + value as in the normal turbulence model in the IEC 61400-1 standard + + Args: + Iref (float): Reference turbulence level, defined as the expected + value of TI at 15 m/s. Default = 0.07. Note this value is + lower than the values of Iref for turbulence classes A, B, and + C in the IEC standard (0.16, 0.14, and 0.12, respectively), but + produces TI values more in line with those typically used in + FLORIS. When the default Iref and offset are used, the TI at + 8 m/s is 8.6%. + offset (float): Offset value to equation. Default = 3.8, as defined + in the IEC standard to give the expected value of TI for + each wind speed. + """ + if (Iref < 0) or (Iref > 1): + raise ValueError("Iref must be >= 0 and <=1") + + def iref_func(wind_directions, wind_speeds): + sigma_1 = Iref * (0.75 * wind_speeds + offset) + return sigma_1 / wind_speeds + + self.assign_ti_using_wd_ws_function(iref_func) + + def plot_ti_over_ws( + self, + ax=None, + marker=".", + ls="None", + color="k", + ): + """ + Scatter plot the turbulence_intensities against wind_speeds + + Args: + ax (:py:class:`matplotlib.pyplot.axes`, optional): The figure axes + on which the wind rose is plotted. Defaults to None. + plot_kwargs (dict, optional): Keyword arguments to be passed to + ax.plot(). + + Returns: + :py:class:`matplotlib.pyplot.axes`: A figure axes object containing + the plotted wind rose. + """ + + # Set up figure + if ax is None: + _, ax = plt.subplots() + + ax.plot(self.ws_flat, self.ti_table_flat*100, marker=marker, ls=ls, color=color) + ax.set_xlabel("Wind Speed (m/s)") + ax.set_ylabel("Turbulence Intensity (%)") + ax.grid(True) + class TimeSeries(WindDataBase): """ @@ -343,7 +411,7 @@ class TimeSeries(WindDataBase): Args: wind_directions: NumPy array of wind directions (NDArrayFloat). wind_speeds: NumPy array of wind speeds (NDArrayFloat). - turbulence_intensity: NumPy array of wind speeds (NDArrayFloat, optional). + turbulence_intensities: NumPy array of wind speeds (NDArrayFloat, optional). Defaults to None values: NumPy array of electricity values (NDArrayFloat, optional). Defaults to None @@ -354,26 +422,28 @@ def __init__( self, wind_directions: NDArrayFloat, wind_speeds: NDArrayFloat, - turbulence_intensity: NDArrayFloat | None = None, + turbulence_intensities: NDArrayFloat | None = None, values: NDArrayFloat | None = None, ): # Wind speeds and wind directions must be the same length if len(wind_directions) != len(wind_speeds): raise ValueError("wind_directions and wind_speeds must be the same length") - # If turbulence_intensity is not None, must be same length as wind_directions - if turbulence_intensity is not None: - if len(wind_directions) != len(turbulence_intensity): - raise ValueError("wind_directions and turbulence_intensity must be the same length") + # If turbulence_intensities is not None, must be same length as wind_directions + if turbulence_intensities is not None: + if len(wind_directions) != len(turbulence_intensities): + raise ValueError( + "wind_directions and turbulence_intensities must be the same length" + ) - # If turbulence_intensity is not None, must be same length as wind_directions + # If values is not None, must be same length as wind_directions if values is not None: if len(wind_directions) != len(values): raise ValueError("wind_directions and values must be the same length") self.wind_directions = wind_directions self.wind_speeds = wind_speeds - self.turbulence_intensity = turbulence_intensity + self.turbulence_intensities = turbulence_intensities self.values = values # Record findex @@ -392,7 +462,7 @@ def unpack(self): self.wind_directions, self.wind_speeds, uniform_frequency, - self.turbulence_intensity, + self.turbulence_intensities, self.values, ) @@ -415,6 +485,43 @@ def _wrap_wind_directions_near_360(self, wind_directions, wd_step): wind_directions_wrapped[mask] = wind_directions_wrapped[mask] - 360.0 return wind_directions_wrapped + def assign_ti_using_wd_ws_function(self, func): + """ + Use the passed in function to new assign values to turbulence_intensities + + Args: + func (function): Function which accepts wind_directions as its + first argument and wind_speeds as second argument and returns + turbulence_intensities + """ + self.turbulence_intensities = func(self.wind_directions, self.wind_speeds) + + def assign_ti_using_IEC_method(self, Iref=0.07, offset=3.8): + """ + Define TI as a function of wind speed by specifying an Iref and offset + value as in the normal turbulence model in the IEC 61400-1 standard + + Args: + Iref (float): Reference turbulence level, defined as the expected + value of TI at 15 m/s. Default = 0.07. Note this value is + lower than the values of Iref for turbulence classes A, B, and + C in the IEC standard (0.16, 0.14, and 0.12, respectively), but + produces TI values more in line with those typically used in + FLORIS. When the default Iref and offset are used, the TI at + 8 m/s is 8.6%. + offset (float): Offset value to equation. Default = 3.8, as defined + in the IEC standard to give the expected value of TI for + each wind speed. + """ + if (Iref < 0) or (Iref > 1): + raise ValueError("Iref must be >= 0 and <=1") + + def iref_func(wind_directions, wind_speeds): + sigma_1 = Iref * (0.75 * wind_speeds + offset) + return sigma_1 / wind_speeds + + self.assign_ti_using_wd_ws_function(iref_func) + def to_wind_rose( self, wd_step=2.0, ws_step=1.0, wd_edges=None, ws_edges=None, bin_weights=None ): @@ -493,9 +600,9 @@ def to_wind_rose( if bin_weights is not None: df = df.assign(freq_val=df["freq_val"] * bin_weights) - # If turbulence_intensity is not none, add to dataframe - if self.turbulence_intensity is not None: - df = df.assign(turbulence_intensity=self.turbulence_intensity) + # If turbulence_intensities is not none, add to dataframe + if self.turbulence_intensities is not None: + df = df.assign(turbulence_intensities=self.turbulence_intensities) # If values is not none, add to dataframe if self.values is not None: @@ -536,8 +643,8 @@ def to_wind_rose( freq_table = freq_table.reshape((len(wd_centers), len(ws_centers))) # If turbulence intensity is not none, compute the table - if self.turbulence_intensity is not None: - ti_table = df["turbulence_intensity_mean"].values.copy() + if self.turbulence_intensities is not None: + ti_table = df["turbulence_intensities_mean"].values.copy() ti_table = ti_table.reshape((len(wd_centers), len(ws_centers))) else: ti_table = None diff --git a/floris/type_dec.py b/floris/type_dec.py index ebbb3178a..a346a689e 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -45,17 +45,56 @@ ### Custom callables for attrs objects and functions def floris_array_converter(data: Iterable) -> np.ndarray: + """ + For a given iterable, convert the data to a numpy array and cast to `floris_float_type`. + If the input is a scalar, np.array() creates a 0-dimensional array, and this is not supported + in FLORIS so this function raises an error. + + Args: + data (Iterable): The input data to be converted to a Numpy array. + + Raises: + TypeError: Raises if the input data is not iterable. + TypeError: Raises if the input data cannot be converted to a Numpy array. + + Returns: + np.ndarray: data converted to a Numpy array and cast to `floris_float_type`. + """ try: - a = np.array(data, dtype=floris_float_type) + iter(data) except TypeError as e: raise TypeError(e.args[0] + f". Data given: {data}") - return a -def floris_numeric_dict_converter(data: dict) -> dict: try: - return {k: floris_array_converter(v) for k, v in data.items()} - except TypeError as e: + a = np.array(data, dtype=floris_float_type) + except (TypeError, ValueError) as e: raise TypeError(e.args[0] + f". Data given: {data}") + return a + +def floris_numeric_dict_converter(data: dict) -> dict: + """ + For the given dictionary, convert all the values to a numeric type. If a value is a scalar, it + will be converted to a float. If a value is an iterable, it will be converted to a Numpy + array and cast to `floris_float_type`. If a value is not a numeric type, a TypeError will be + raised. + + Args: + data (dict): Dictionary of data to be converted to a numeric type. + + Returns: + dict: Dictionary with the same keys and all values converted to a numeric type. + """ + converted_dict = copy.deepcopy(data) # deepcopy -> data is a container and passed by reference + for k, v in data.items(): + try: + iter(v) + except TypeError: + # Not iterable so try to cast to float + converted_dict[k] = float(v) + else: + # Iterable so convert to Numpy array + converted_dict[k] = floris_array_converter(v) + return converted_dict # def array_field(**kwargs) -> Callable: # """ diff --git a/tests/conftest.py b/tests/conftest.py index ecd9ab9a9..124d52805 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -413,7 +413,7 @@ def __init__(self): self.flow_field = { "wind_speeds": WIND_SPEEDS, "wind_directions": WIND_DIRECTIONS, - "turbulence_intensity": 0.1, + "turbulence_intensities": [0.1], "wind_shear": 0.12, "wind_veer": 0.0, "air_density": 1.225, diff --git a/tests/data/input_full_v3.yaml b/tests/data/input_full.yaml similarity index 97% rename from tests/data/input_full_v3.yaml rename to tests/data/input_full.yaml index 5cace12df..36a150bdd 100644 --- a/tests/data/input_full_v3.yaml +++ b/tests/data/input_full.yaml @@ -26,7 +26,8 @@ farm: flow_field: air_density: 1.225 reference_wind_height: 90.0 - turbulence_intensity: 0.06 + turbulence_intensities: + - 0.06 wind_directions: - 270.0 wind_shear: 0.12 diff --git a/tests/floris_interface_test.py b/tests/floris_interface_test.py index 0196af5fc..17d612a38 100644 --- a/tests/floris_interface_test.py +++ b/tests/floris_interface_test.py @@ -1,12 +1,13 @@ from pathlib import Path import numpy as np +import pytest from floris.tools.floris_interface import FlorisInterface TEST_DATA = Path(__file__).resolve().parent / "data" -YAML_INPUT = TEST_DATA / "input_full_v3.yaml" +YAML_INPUT = TEST_DATA / "input_full.yaml" def test_read_yaml(): @@ -200,3 +201,60 @@ def test_get_farm_aep_with_conditions(): #Confirm n_findex reset after the operation assert n_findex == fi.floris.flow_field.n_findex + + +def test_reinitailize_ti(): + fi = FlorisInterface(configuration=YAML_INPUT) + + # Set wind directions and wind speeds and turbulence intensitities + # with n_findex = 3 + fi.reinitialize( + wind_speeds=[8.0, 8.0, 8.0], + wind_directions=[240.0, 250.0, 260.0], + turbulence_intensities=[0.1, 0.1, 0.1], + ) + + # Now confirm can change wind speeds and directions shape without changing + # turbulence intensity since this is allowed when the turbulence + # intensities are uniform + # raises n_findex to 4 + fi.reinitialize( + wind_speeds=[8.0, 8.0, 8.0, 8.0], + wind_directions=[ + 240.0, + 250.0, + 260.0, + 270.0, + ], + ) + + # Confirm turbulence_intensities now length 4 with single unique value + np.testing.assert_allclose(fi.floris.flow_field.turbulence_intensities, [0.1, 0.1, 0.1, 0.1]) + + # Now should be able to change turbulence intensity to changing, so long as length 4 + fi.reinitialize(turbulence_intensities=[0.08, 0.09, 0.1, 0.11]) + + # However the wrong length should raise an error + with pytest.raises(ValueError): + fi.reinitialize(turbulence_intensities=[0.08, 0.09, 0.1]) + + # Also, now that TI is not a single unique value, it can not be left default when changing + # shape of wind speeds and directions + with pytest.raises(ValueError): + fi.reinitialize( + wind_speeds=[8.0, 8.0, 8.0, 8.0, 8.0], + wind_directions=[ + 240.0, + 250.0, + 260.0, + 270.0, + 280.0, + ], + ) + + # Test that applying a 1D array of length 1 is allowed for ti + fi.reinitialize(turbulence_intensities=[0.12]) + + # Test that applying a float however raises an error + with pytest.raises(TypeError): + fi.reinitialize(turbulence_intensities=0.12) diff --git a/tests/floris_unit_test.py b/tests/floris_unit_test.py index 05c01f022..8fc75ca1f 100644 --- a/tests/floris_unit_test.py +++ b/tests/floris_unit_test.py @@ -26,7 +26,7 @@ TEST_DATA = Path(__file__).resolve().parent / "data" -YAML_INPUT = TEST_DATA / "input_full_v3.yaml" +YAML_INPUT = TEST_DATA / "input_full.yaml" DICT_INPUT = yaml.load(open(YAML_INPUT, "r"), Loader=yaml.SafeLoader) diff --git a/tests/flow_field_unit_test.py b/tests/flow_field_unit_test.py index 9b0c9a724..978911700 100644 --- a/tests/flow_field_unit_test.py +++ b/tests/flow_field_unit_test.py @@ -58,3 +58,20 @@ def test_asdict(flow_field_fixture: FlowField, turbine_grid_fixture: TurbineGrid dict2 = new_ff.as_dict() assert dict1 == dict2 + + +def test_turbulence_intensities_to_n_findex(flow_field_fixture, turbine_grid_fixture): + # Assert tubulence intensity has same length as n_findex + assert len(flow_field_fixture.turbulence_intensities) == flow_field_fixture.n_findex + + # Assert turbulence_intensity_field is the correct shape + flow_field_fixture.initialize_velocity_field(turbine_grid_fixture) + assert flow_field_fixture.turbulence_intensity_field.shape == (N_FINDEX, N_TURBINES, 1, 1) + + # Assert that turbulence_intensity_field has values matched to turbulence_intensity + for findex in range(N_FINDEX): + for t in range(N_TURBINES): + assert ( + flow_field_fixture.turbulence_intensities[findex] + == flow_field_fixture.turbulence_intensity_field[findex, t, 0, 0] + ) diff --git a/tests/type_dec_unit_test.py b/tests/type_dec_unit_test.py index 641f207dc..3c5b87ded 100644 --- a/tests/type_dec_unit_test.py +++ b/tests/type_dec_unit_test.py @@ -22,6 +22,7 @@ from floris.type_dec import ( convert_to_path, floris_array_converter, + floris_numeric_dict_converter, FromDictMixin, iter_validator, ) @@ -116,7 +117,7 @@ def test_iter_validator(): AttrsDemoClass(w=0, x=1, liststr=("a", "b")) -def test_attrs_array_converter(): +def test_array_converter(): array_input = [[1, 2, 3], [4.5, 6.3, 2.2]] test_array = np.array(array_input) @@ -124,10 +125,53 @@ def test_attrs_array_converter(): cls = AttrsDemoClass(w=0, x=1, array=array_input) np.testing.assert_allclose(test_array, cls.array) - # Test converstion on reset + # Test conversion on reset cls.array = array_input np.testing.assert_allclose(test_array, cls.array) + # Test that a non-iterable item like a scalar number fails + with pytest.raises(TypeError): + cls.array = 1 + + +def test_numeric_dict_converter(): + """ + This function converts data in a dictionary to a numeric type. + If it can't convert the data, it will raise a TypeError. + It should support scalar, list, and numpy array types + for values in the dictionary. + """ + test_dict = { + "scalar_string": "1", + "scalar_int": 1, + "scalar_float": 1.0, + "list_string": ["1", "2", "3"], + "list_int": [1, 2, 3], + "list_float": [1.0, 2.0, 3.0], + "array_string": np.array(["1", "2", "3"]), + "array_int": np.array([1, 2, 3]), + "array_float": np.array([1.0, 2.0, 3.0]), + } + numeric_dict = floris_numeric_dict_converter(test_dict) + assert numeric_dict["scalar_string"] == 1 + assert numeric_dict["scalar_int"] == 1 + assert numeric_dict["scalar_float"] == 1.0 + np.testing.assert_allclose(numeric_dict["list_string"], [1, 2, 3]) + np.testing.assert_allclose(numeric_dict["list_int"], [1, 2, 3]) + np.testing.assert_allclose(numeric_dict["list_float"], [1.0, 2.0, 3.0]) + np.testing.assert_allclose(numeric_dict["array_string"], [1, 2, 3]) + np.testing.assert_allclose(numeric_dict["array_int"], [1, 2, 3]) + np.testing.assert_allclose(numeric_dict["array_float"], [1.0, 2.0, 3.0]) + + test_dict = {"scalar_fail": "a"} + with pytest.raises(TypeError): + floris_numeric_dict_converter(test_dict) + test_dict = {"list_fail": ["a", "2", "3"]} + with pytest.raises(TypeError): + floris_numeric_dict_converter(test_dict) + test_dict = {"array_fail": np.array(["a", "2", "3"])} + with pytest.raises(TypeError): + floris_numeric_dict_converter(test_dict) def test_convert_to_path(): str_input = "../tests" diff --git a/tests/wind_data_test.py b/tests/wind_data_test.py index bc793d4fe..565d38ae1 100644 --- a/tests/wind_data_test.py +++ b/tests/wind_data_test.py @@ -244,11 +244,11 @@ def test_time_series_to_wind_rose_wrapping(): def test_time_series_to_wind_rose_with_ti(): wind_directions = np.array([259.8, 260.2, 260.3, 260.1]) wind_speeds = np.array([5.0, 5.0, 5.1, 7.2]) - turbulence_intensity = np.array([0.5, 1.0, 1.5, 2.0]) + turbulence_intensities = np.array([0.5, 1.0, 1.5, 2.0]) time_series = TimeSeries( wind_directions, wind_speeds, - turbulence_intensity=turbulence_intensity, + turbulence_intensities=turbulence_intensities, ) wind_rose = time_series.to_wind_rose(wd_step=2.0, ws_step=1.0)