From d79e2c56fbffd70dc9bab5a5407c4e4eb5ec9499 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 8 Feb 2024 15:44:58 -0700 Subject: [PATCH 01/53] Add a place-holder mit loss function --- floris/simulation/rotor_velocity.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/floris/simulation/rotor_velocity.py b/floris/simulation/rotor_velocity.py index 25f94d55d..b57f11a34 100644 --- a/floris/simulation/rotor_velocity.py +++ b/floris/simulation/rotor_velocity.py @@ -42,6 +42,18 @@ def rotor_velocity_yaw_correction( return rotor_effective_velocities +def mit_rotor_velocity_yaw_correction( + pP: float, + yaw_angles: NDArrayFloat, + rotor_effective_velocities: NDArrayFloat, +) -> NDArrayFloat: + # Compute the rotor effective velocity adjusting for yaw settings + pW = pP / 3.0 # Convert from pP to w + # TODO: cosine loss hard coded + rotor_effective_velocities = rotor_effective_velocities * cosd(yaw_angles) ** pW + + return rotor_effective_velocities + def rotor_velocity_tilt_correction( tilt_angles: NDArrayFloat, ref_tilt: NDArrayFloat, From 3f1ec2996ac4b7354ab5f329e4b1e8252c66b538 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 8 Feb 2024 15:45:10 -0700 Subject: [PATCH 02/53] track new mit loss model --- floris/simulation/turbine/turbine.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/floris/simulation/turbine/turbine.py b/floris/simulation/turbine/turbine.py index f9435facb..09291d1ba 100644 --- a/floris/simulation/turbine/turbine.py +++ b/floris/simulation/turbine/turbine.py @@ -27,6 +27,7 @@ from floris.simulation import BaseClass from floris.simulation.turbine import ( CosineLossTurbine, + MITLossTurbine, MixedOperationTurbine, SimpleDeratingTurbine, SimpleTurbine, @@ -47,6 +48,7 @@ "power_thrust_model": { "simple": SimpleTurbine, "cosine-loss": CosineLossTurbine, + "mit-loss": MITLossTurbine, "simple-derating": SimpleDeratingTurbine, "mixed": MixedOperationTurbine, }, From afcb344b4d31b0d0070328bcc40bad58f2de7b9e Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 8 Feb 2024 15:45:23 -0700 Subject: [PATCH 03/53] Add mit loss model to tests --- tests/turbine_operation_models_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/turbine_operation_models_test.py b/tests/turbine_operation_models_test.py index 446695855..310ae7abf 100644 --- a/tests/turbine_operation_models_test.py +++ b/tests/turbine_operation_models_test.py @@ -3,6 +3,7 @@ from floris.simulation.turbine.operation_models import ( CosineLossTurbine, + MITLossTurbine, MixedOperationTurbine, POWER_SETPOINT_DEFAULT, rotor_velocity_air_density_correction, From c03e250ee0e7f30a285a4ae744836d6ba502de5a Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 8 Feb 2024 15:45:31 -0700 Subject: [PATCH 04/53] import mit loss model --- floris/simulation/turbine/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/floris/simulation/turbine/__init__.py b/floris/simulation/turbine/__init__.py index 355f5c2df..f38615129 100644 --- a/floris/simulation/turbine/__init__.py +++ b/floris/simulation/turbine/__init__.py @@ -14,6 +14,7 @@ from floris.simulation.turbine.operation_models import ( CosineLossTurbine, + MITLossTurbine, MixedOperationTurbine, SimpleDeratingTurbine, SimpleTurbine, From 135dc646e52829aa843a73afa458877cc1278ae2 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 8 Feb 2024 15:45:49 -0700 Subject: [PATCH 05/53] add mit loss model class --- floris/simulation/turbine/operation_models.py | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index 82c11ee70..7244bf79a 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -30,6 +30,7 @@ from floris.simulation.rotor_velocity import ( average_velocity, compute_tilt_angles_for_floating_turbines, + mit_rotor_velocity_yaw_correction, rotor_velocity_tilt_correction, rotor_velocity_yaw_correction, ) @@ -318,6 +319,151 @@ def axial_induction( misalignment_loss = cosd(yaw_angles) * cosd(tilt_angles - power_thrust_table["ref_tilt"]) return 0.5 / misalignment_loss * (1 - np.sqrt(1 - thrust_coefficient * misalignment_loss)) + + +@define +class MITLossTurbine(BaseOperationModel): + """ + Static class defining an actuator disk turbine model that may be misaligned with the flow. + Nonzero tilt and yaw angles are handled via cosine relationships, with the power lost to yawing + defined by the pP exponent. This turbine submodel is the default, and matches the turbine + model in FLORIS v3. + + As with all turbine submodules, implements only static power() and thrust_coefficient() methods, + which are called by power() and thrust_coefficient() on turbine.py, respectively. This class is + not intended to be instantiated; it simply defines a library of static methods. + + TODO: Should the turbine submodels each implement axial_induction()? + """ + + def power( + power_thrust_table: dict, + velocities: NDArrayFloat, + air_density: float, + yaw_angles: NDArrayFloat, + tilt_angles: NDArrayFloat, + tilt_interp: NDArrayObject, + average_method: str = "cubic-mean", + cubature_weights: NDArrayFloat | None = None, + correct_cp_ct_for_tilt: bool = False, + **_ # <- Allows other models to accept other keyword arguments + ): + # Construct power interpolant + power_interpolator = interp1d( + power_thrust_table["wind_speed"], + power_thrust_table["power"], + fill_value=0.0, + bounds_error=False, + ) + + # Compute the power-effective wind speed across the rotor + rotor_average_velocities = average_velocity( + velocities=velocities, + method=average_method, + cubature_weights=cubature_weights, + ) + + rotor_effective_velocities = rotor_velocity_air_density_correction( + velocities=rotor_average_velocities, + air_density=air_density, + ref_air_density=power_thrust_table["ref_air_density"] + ) + + rotor_effective_velocities = mit_rotor_velocity_yaw_correction( + pP=power_thrust_table["pP"], + yaw_angles=yaw_angles, + rotor_effective_velocities=rotor_effective_velocities, + ) + + rotor_effective_velocities = rotor_velocity_tilt_correction( + tilt_angles=tilt_angles, + ref_tilt=power_thrust_table["ref_tilt"], + pT=power_thrust_table["pT"], + tilt_interp=tilt_interp, + correct_cp_ct_for_tilt=correct_cp_ct_for_tilt, + rotor_effective_velocities=rotor_effective_velocities, + ) + + # Compute power + power = power_interpolator(rotor_effective_velocities) * 1e3 # Convert to W + + return power + + def thrust_coefficient( + power_thrust_table: dict, + velocities: NDArrayFloat, + yaw_angles: NDArrayFloat, + tilt_angles: NDArrayFloat, + tilt_interp: NDArrayObject, + average_method: str = "cubic-mean", + cubature_weights: NDArrayFloat | None = None, + correct_cp_ct_for_tilt: bool = False, + **_ # <- Allows other models to accept other keyword arguments + ): + # Construct thrust coefficient interpolant + thrust_coefficient_interpolator = interp1d( + power_thrust_table["wind_speed"], + power_thrust_table["thrust_coefficient"], + fill_value=0.0001, + bounds_error=False, + ) + + # Compute the effective wind speed across the rotor + rotor_average_velocities = average_velocity( + velocities=velocities, + method=average_method, + cubature_weights=cubature_weights, + ) + + # TODO: Do we need an air density correction here? + thrust_coefficient = thrust_coefficient_interpolator(rotor_average_velocities) + thrust_coefficient = np.clip(thrust_coefficient, 0.0001, 0.9999) + + # Apply tilt and yaw corrections + # Compute the tilt, if using floating turbines + old_tilt_angles = copy.deepcopy(tilt_angles) + tilt_angles = compute_tilt_angles_for_floating_turbines( + tilt_angles=tilt_angles, + tilt_interp=tilt_interp, + rotor_effective_velocities=rotor_average_velocities, + ) + # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) + tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) + + thrust_coefficient = ( + thrust_coefficient + * cosd(yaw_angles) + * cosd(tilt_angles - power_thrust_table["ref_tilt"]) + ) + + return thrust_coefficient + + def axial_induction( + power_thrust_table: dict, + velocities: NDArrayFloat, + yaw_angles: NDArrayFloat, + tilt_angles: NDArrayFloat, + tilt_interp: NDArrayObject, + average_method: str = "cubic-mean", + cubature_weights: NDArrayFloat | None = None, + correct_cp_ct_for_tilt: bool = False, + **_ # <- Allows other models to accept other keyword arguments + ): + + thrust_coefficient = CosineLossTurbine.thrust_coefficient( + power_thrust_table=power_thrust_table, + velocities=velocities, + yaw_angles=yaw_angles, + tilt_angles=tilt_angles, + tilt_interp=tilt_interp, + average_method=average_method, + cubature_weights=cubature_weights, + correct_cp_ct_for_tilt=correct_cp_ct_for_tilt + ) + + misalignment_loss = cosd(yaw_angles) * cosd(tilt_angles - power_thrust_table["ref_tilt"]) + return 0.5 / misalignment_loss * (1 - np.sqrt(1 - thrust_coefficient * misalignment_loss)) + @define class SimpleDeratingTurbine(BaseOperationModel): """ From 96538e760e19ced2b4235108aba82d388bfe167b Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 8 Feb 2024 15:46:00 -0700 Subject: [PATCH 06/53] Add an example to compare loss models --- examples/41_compare_yaw_loss.py | 86 +++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 examples/41_compare_yaw_loss.py diff --git a/examples/41_compare_yaw_loss.py b/examples/41_compare_yaw_loss.py new file mode 100644 index 000000000..1acf0ae7d --- /dev/null +++ b/examples/41_compare_yaw_loss.py @@ -0,0 +1,86 @@ +# 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 +import yaml + +from floris.tools import FlorisInterface + + +""" +Example to test out derating of turbines and mixed derating and yawing. Will be refined before +release. TODO: Demonstrate shutting off turbines also, once developed. +""" + +# Parameters +N = 100 # How many steps to cover yaw range in +yaw_max = 30 # Maximum yaw to test + +# Set up the yaw angle sweep +yaw_angles = np.zeros((N,1)) +yaw_angles[:,0] = np.linspace(-yaw_max, yaw_max, N) +print(yaw_angles.shape) + + + +# Now loop over the operational models to compare +op_models = ["cosine-loss", "mit-loss"] +results = {} + +for op_model in op_models: + + print(f"Evaluating model: {op_model}") + + # Grab model of FLORIS + fi = FlorisInterface("inputs/gch.yaml") + + # Initialize to a simple 1 turbine case with n_findex = N + fi.reinitialize(layout_x=[0], + layout_y=[0], + wind_directions=270 * np.ones(N), + wind_speeds=8 * np.ones(N), + ) + + with open(str( + fi.floris.as_dict()["farm"]["turbine_library_path"] / + (fi.floris.as_dict()["farm"]["turbine_type"][0] + ".yaml") + )) as t: + turbine_type = yaml.safe_load(t) + turbine_type["power_thrust_model"] = op_model + + # Change the turbine type + fi.reinitialize(turbine_type=[turbine_type]) + + # Calculate the power + fi.calculate_wake(yaw_angles=yaw_angles) + turbine_power = fi.get_turbine_powers().squeeze() + print(turbine_power.shape) + + # Save the results + results[op_model] = turbine_power + + +# Plot the results +fig, ax = plt.subplots() + +for key in results: + ax.plot(yaw_angles.squeeze(), results[key], label=key) + +ax.grid(True) +ax.legend() +ax.set_xlabel("Yaw Angle (Deg)") +ax.set_ylabel("Turbine Power (Deg)") + +plt.show() From c098819d572da2f6e4c30a0d39d0866818e35f75 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 8 Feb 2024 18:53:43 -0700 Subject: [PATCH 07/53] Update example for clarity --- examples/41_compare_yaw_loss.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/41_compare_yaw_loss.py b/examples/41_compare_yaw_loss.py index 1acf0ae7d..c30b6887d 100644 --- a/examples/41_compare_yaw_loss.py +++ b/examples/41_compare_yaw_loss.py @@ -25,7 +25,7 @@ """ # Parameters -N = 100 # How many steps to cover yaw range in +N = 101 # How many steps to cover yaw range in yaw_max = 30 # Maximum yaw to test # Set up the yaw angle sweep @@ -66,21 +66,22 @@ # Calculate the power fi.calculate_wake(yaw_angles=yaw_angles) turbine_power = fi.get_turbine_powers().squeeze() - print(turbine_power.shape) # Save the results results[op_model] = turbine_power - # Plot the results fig, ax = plt.subplots() -for key in results: - ax.plot(yaw_angles.squeeze(), results[key], label=key) +colors = ["C0", "k", "r"] +linestyles = ["solid", "dashed", "dotted"] +for key, c, ls in zip(results, colors, linestyles): + central_power = results[key][yaw_angles.squeeze() == 0] + ax.plot(yaw_angles.squeeze(), results[key]/central_power, label=key, color=c, linestyle=ls) ax.grid(True) ax.legend() -ax.set_xlabel("Yaw Angle (Deg)") -ax.set_ylabel("Turbine Power (Deg)") +ax.set_xlabel("Yaw angle [deg]") +ax.set_ylabel("Normalized turbine power [deg]") plt.show() From 80ed080ac95e0548014e0c2682ba17decf5a7404 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 8 Feb 2024 18:57:14 -0700 Subject: [PATCH 08/53] Placeholder tests. --- tests/turbine_operation_models_test.py | 106 +++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/tests/turbine_operation_models_test.py b/tests/turbine_operation_models_test.py index 310ae7abf..88f0739e0 100644 --- a/tests/turbine_operation_models_test.py +++ b/tests/turbine_operation_models_test.py @@ -499,3 +499,109 @@ def test_MixedOperationTurbine(): tilt_angles=tilt_angles_nom, tilt_interp=None ) + +def test_MITLossTurbine(): + + # NOTE: These tests should be updated to reflect actual expected behavior + # of the MITLossTurbine model. Currently, match the CosineLossTurbine model. + + n_turbines = 1 + wind_speed = 10.0 + turbine_data = SampleInputs().turbine + + yaw_angles_nom = 0 * np.ones((1, n_turbines)) + tilt_angles_nom = turbine_data["power_thrust_table"]["ref_tilt"] * np.ones((1, n_turbines)) + yaw_angles_test = 20 * np.ones((1, n_turbines)) + tilt_angles_test = 0 * np.ones((1, n_turbines)) + + + # Check that power works as expected + test_power = MITLossTurbine.power( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=turbine_data["power_thrust_table"]["ref_air_density"], # Matches ref_air_density + yaw_angles=yaw_angles_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ) + truth_index = turbine_data["power_thrust_table"]["wind_speed"].index(wind_speed) + baseline_power = turbine_data["power_thrust_table"]["power"][truth_index] * 1000 + assert np.allclose(baseline_power, test_power) + + # Check that yaw and tilt angle have an effect + test_power = MITLossTurbine.power( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=turbine_data["power_thrust_table"]["ref_air_density"], # Matches ref_air_density + yaw_angles=yaw_angles_test, + tilt_angles=tilt_angles_test, + tilt_interp=None + ) + assert test_power < baseline_power + + # Check that a lower air density decreases power appropriately + test_power = MITLossTurbine.power( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, + yaw_angles=yaw_angles_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ) + assert test_power < baseline_power + + + # Check that thrust coefficient works as expected + test_Ct = MITLossTurbine.thrust_coefficient( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, # Unused + yaw_angles=yaw_angles_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ) + baseline_Ct = turbine_data["power_thrust_table"]["thrust_coefficient"][truth_index] + assert np.allclose(baseline_Ct, test_Ct) + + # Check that yaw and tilt angle have the expected effect + test_Ct = MITLossTurbine.thrust_coefficient( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, # Unused + yaw_angles=yaw_angles_test, + tilt_angles=tilt_angles_test, + tilt_interp=None + ) + absolute_tilt = tilt_angles_test - turbine_data["power_thrust_table"]["ref_tilt"] + assert test_Ct == baseline_Ct * cosd(yaw_angles_test) * cosd(absolute_tilt) + + + # Check that thrust coefficient works as expected + test_ai = MITLossTurbine.axial_induction( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, # Unused + yaw_angles=yaw_angles_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ) + baseline_misalignment_loss = ( + cosd(yaw_angles_nom) + * cosd(tilt_angles_nom - turbine_data["power_thrust_table"]["ref_tilt"]) + ) + baseline_ai = ( + 1 - np.sqrt(1 - turbine_data["power_thrust_table"]["thrust_coefficient"][truth_index]) + ) / 2 / baseline_misalignment_loss + assert np.allclose(baseline_ai, test_ai) + + # Check that yaw and tilt angle have the expected effect + test_ai = MITLossTurbine.axial_induction( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, # Unused + yaw_angles=yaw_angles_test, + tilt_angles=tilt_angles_test, + tilt_interp=None + ) + absolute_tilt = tilt_angles_test - turbine_data["power_thrust_table"]["ref_tilt"] + assert test_Ct == baseline_Ct * cosd(yaw_angles_test) * cosd(absolute_tilt) From 56ba06ca8e7e3c52951a3e661579adae91b62be3 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 8 Feb 2024 19:01:40 -0700 Subject: [PATCH 09/53] test attributes. --- tests/turbine_operation_models_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/turbine_operation_models_test.py b/tests/turbine_operation_models_test.py index 88f0739e0..dcbf59757 100644 --- a/tests/turbine_operation_models_test.py +++ b/tests/turbine_operation_models_test.py @@ -50,6 +50,10 @@ def test_submodel_attributes(): assert hasattr(MixedOperationTurbine, "thrust_coefficient") assert hasattr(MixedOperationTurbine, "axial_induction") + assert hasattr(MITLossTurbine, "power") + assert hasattr(MITLossTurbine, "thrust_coefficient") + assert hasattr(MITLossTurbine, "axial_induction") + def test_SimpleTurbine(): n_turbines = 1 From c78107f307755edb62a8e901d0a8e470d5fed88d Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 9 Feb 2024 11:16:32 -0700 Subject: [PATCH 10/53] Update names --- examples/41_compare_yaw_loss.py | 5 ++-- floris/simulation/rotor_velocity.py | 2 +- floris/simulation/turbine/__init__.py | 2 +- floris/simulation/turbine/operation_models.py | 6 ++--- floris/simulation/turbine/turbine.py | 4 ++-- tests/turbine_operation_models_test.py | 24 +++++++++---------- 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/examples/41_compare_yaw_loss.py b/examples/41_compare_yaw_loss.py index c30b6887d..60edab17d 100644 --- a/examples/41_compare_yaw_loss.py +++ b/examples/41_compare_yaw_loss.py @@ -20,8 +20,7 @@ """ -Example to test out derating of turbines and mixed derating and yawing. Will be refined before -release. TODO: Demonstrate shutting off turbines also, once developed. +Test alternative models of loss to yawing """ # Parameters @@ -36,7 +35,7 @@ # Now loop over the operational models to compare -op_models = ["cosine-loss", "mit-loss"] +op_models = ["cosine-loss", "tum-loss"] results = {} for op_model in op_models: diff --git a/floris/simulation/rotor_velocity.py b/floris/simulation/rotor_velocity.py index b57f11a34..3376e4017 100644 --- a/floris/simulation/rotor_velocity.py +++ b/floris/simulation/rotor_velocity.py @@ -42,7 +42,7 @@ def rotor_velocity_yaw_correction( return rotor_effective_velocities -def mit_rotor_velocity_yaw_correction( +def tum_rotor_velocity_yaw_correction( pP: float, yaw_angles: NDArrayFloat, rotor_effective_velocities: NDArrayFloat, diff --git a/floris/simulation/turbine/__init__.py b/floris/simulation/turbine/__init__.py index f38615129..934cd6b21 100644 --- a/floris/simulation/turbine/__init__.py +++ b/floris/simulation/turbine/__init__.py @@ -14,8 +14,8 @@ from floris.simulation.turbine.operation_models import ( CosineLossTurbine, - MITLossTurbine, MixedOperationTurbine, SimpleDeratingTurbine, SimpleTurbine, + TUMLossTurbine, ) diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index 7244bf79a..23a0a75be 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -30,9 +30,9 @@ from floris.simulation.rotor_velocity import ( average_velocity, compute_tilt_angles_for_floating_turbines, - mit_rotor_velocity_yaw_correction, rotor_velocity_tilt_correction, rotor_velocity_yaw_correction, + tum_rotor_velocity_yaw_correction, ) from floris.type_dec import ( NDArrayFloat, @@ -322,7 +322,7 @@ def axial_induction( @define -class MITLossTurbine(BaseOperationModel): +class TUMLossTurbine(BaseOperationModel): """ Static class defining an actuator disk turbine model that may be misaligned with the flow. Nonzero tilt and yaw angles are handled via cosine relationships, with the power lost to yawing @@ -369,7 +369,7 @@ def power( ref_air_density=power_thrust_table["ref_air_density"] ) - rotor_effective_velocities = mit_rotor_velocity_yaw_correction( + rotor_effective_velocities = tum_rotor_velocity_yaw_correction( pP=power_thrust_table["pP"], yaw_angles=yaw_angles, rotor_effective_velocities=rotor_effective_velocities, diff --git a/floris/simulation/turbine/turbine.py b/floris/simulation/turbine/turbine.py index 09291d1ba..f25c6540c 100644 --- a/floris/simulation/turbine/turbine.py +++ b/floris/simulation/turbine/turbine.py @@ -27,10 +27,10 @@ from floris.simulation import BaseClass from floris.simulation.turbine import ( CosineLossTurbine, - MITLossTurbine, MixedOperationTurbine, SimpleDeratingTurbine, SimpleTurbine, + TUMLossTurbine, ) from floris.type_dec import ( convert_to_path, @@ -48,7 +48,7 @@ "power_thrust_model": { "simple": SimpleTurbine, "cosine-loss": CosineLossTurbine, - "mit-loss": MITLossTurbine, + "tum-loss": TUMLossTurbine, "simple-derating": SimpleDeratingTurbine, "mixed": MixedOperationTurbine, }, diff --git a/tests/turbine_operation_models_test.py b/tests/turbine_operation_models_test.py index dcbf59757..148331222 100644 --- a/tests/turbine_operation_models_test.py +++ b/tests/turbine_operation_models_test.py @@ -3,12 +3,12 @@ from floris.simulation.turbine.operation_models import ( CosineLossTurbine, - MITLossTurbine, MixedOperationTurbine, POWER_SETPOINT_DEFAULT, rotor_velocity_air_density_correction, SimpleDeratingTurbine, SimpleTurbine, + TUMLossTurbine, ) from floris.utilities import cosd from tests.conftest import SampleInputs, WIND_SPEEDS @@ -50,9 +50,9 @@ def test_submodel_attributes(): assert hasattr(MixedOperationTurbine, "thrust_coefficient") assert hasattr(MixedOperationTurbine, "axial_induction") - assert hasattr(MITLossTurbine, "power") - assert hasattr(MITLossTurbine, "thrust_coefficient") - assert hasattr(MITLossTurbine, "axial_induction") + assert hasattr(TUMLossTurbine, "power") + assert hasattr(TUMLossTurbine, "thrust_coefficient") + assert hasattr(TUMLossTurbine, "axial_induction") def test_SimpleTurbine(): @@ -504,7 +504,7 @@ def test_MixedOperationTurbine(): tilt_interp=None ) -def test_MITLossTurbine(): +def test_TUMLossTurbine(): # NOTE: These tests should be updated to reflect actual expected behavior # of the MITLossTurbine model. Currently, match the CosineLossTurbine model. @@ -520,7 +520,7 @@ def test_MITLossTurbine(): # Check that power works as expected - test_power = MITLossTurbine.power( + test_power = TUMLossTurbine.power( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=turbine_data["power_thrust_table"]["ref_air_density"], # Matches ref_air_density @@ -533,7 +533,7 @@ def test_MITLossTurbine(): assert np.allclose(baseline_power, test_power) # Check that yaw and tilt angle have an effect - test_power = MITLossTurbine.power( + test_power = TUMLossTurbine.power( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=turbine_data["power_thrust_table"]["ref_air_density"], # Matches ref_air_density @@ -544,7 +544,7 @@ def test_MITLossTurbine(): assert test_power < baseline_power # Check that a lower air density decreases power appropriately - test_power = MITLossTurbine.power( + test_power = TUMLossTurbine.power( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, @@ -556,7 +556,7 @@ def test_MITLossTurbine(): # Check that thrust coefficient works as expected - test_Ct = MITLossTurbine.thrust_coefficient( + test_Ct = TUMLossTurbine.thrust_coefficient( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, # Unused @@ -568,7 +568,7 @@ def test_MITLossTurbine(): assert np.allclose(baseline_Ct, test_Ct) # Check that yaw and tilt angle have the expected effect - test_Ct = MITLossTurbine.thrust_coefficient( + test_Ct = TUMLossTurbine.thrust_coefficient( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, # Unused @@ -581,7 +581,7 @@ def test_MITLossTurbine(): # Check that thrust coefficient works as expected - test_ai = MITLossTurbine.axial_induction( + test_ai = TUMLossTurbine.axial_induction( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, # Unused @@ -599,7 +599,7 @@ def test_MITLossTurbine(): assert np.allclose(baseline_ai, test_ai) # Check that yaw and tilt angle have the expected effect - test_ai = MITLossTurbine.axial_induction( + test_ai = TUMLossTurbine.axial_induction( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, # Unused From c55e3cddd00d14eb3bfc8aa61c8dbafd1f006555 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 9 Feb 2024 11:17:27 -0700 Subject: [PATCH 11/53] fix comment --- tests/turbine_operation_models_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/turbine_operation_models_test.py b/tests/turbine_operation_models_test.py index 148331222..86cb1f3a4 100644 --- a/tests/turbine_operation_models_test.py +++ b/tests/turbine_operation_models_test.py @@ -507,7 +507,7 @@ def test_MixedOperationTurbine(): def test_TUMLossTurbine(): # NOTE: These tests should be updated to reflect actual expected behavior - # of the MITLossTurbine model. Currently, match the CosineLossTurbine model. + # of the TUMLossTurbine model. Currently, match the CosineLossTurbine model. n_turbines = 1 wind_speed = 10.0 From 5aa21dc8dab6464dc22e431ca3dc8cdd2a0e9d70 Mon Sep 17 00:00:00 2001 From: sTamaroTum Date: Tue, 5 Mar 2024 14:14:32 +0100 Subject: [PATCH 12/53] New TUM misalignment model and new IEA file --- floris/simulation/turbine/operation_models.py | 634 ++++++++++++++++-- floris/turbine_library/LUT_IEA3MW.npz | Bin 0 -> 16902 bytes floris/turbine_library/iea_3MW.yaml | 230 +++++++ 3 files changed, 811 insertions(+), 53 deletions(-) create mode 100644 floris/turbine_library/LUT_IEA3MW.npz create mode 100644 floris/turbine_library/iea_3MW.yaml diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index 23a0a75be..6b5430479 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -24,7 +24,8 @@ import numpy as np from attrs import define, field -from scipy.interpolate import interp1d +from scipy.interpolate import interp1d, RegularGridInterpolator +from scipy.optimize import fsolve from floris.simulation import BaseClass from floris.simulation.rotor_velocity import ( @@ -324,24 +325,269 @@ def axial_induction( @define class TUMLossTurbine(BaseOperationModel): """ - Static class defining an actuator disk turbine model that may be misaligned with the flow. - Nonzero tilt and yaw angles are handled via cosine relationships, with the power lost to yawing - defined by the pP exponent. This turbine submodel is the default, and matches the turbine - model in FLORIS v3. - + Static class defining a wind turbine model that may be misaligned with the flow. + Nonzero tilt and yaw angles are handled via the model presented in https://doi.org/10.5194/wes-2023-133 . + + The method requires C_P, C_T look-up tables as functions of tip speed ratio and blade pitch angle, available here: + "../../LUT_IEA3MW.npz" for the IEA 3.4 MW (Bortolotti et al., 2019) As with all turbine submodules, implements only static power() and thrust_coefficient() methods, - which are called by power() and thrust_coefficient() on turbine.py, respectively. This class is - not intended to be instantiated; it simply defines a library of static methods. + which are called by power() and thrust_coefficient() on turbine.py, respectively. + There are also two new functions, i.e. compute_local_vertical_shear() and control_trajectory(). + These are called by thrust_coefficient() and power() to compute the vertical shear and predict the turbine status + in terms of tip speed ratio and pitch angle. + This class is not intended to be instantiated; it simply defines a library of static methods. TODO: Should the turbine submodels each implement axial_induction()? """ - + + def compute_local_vertical_shear(velocities,avg_velocities): + num_rows, num_cols = avg_velocities.shape + shear = np.zeros_like(avg_velocities) + for i in np.arange(num_rows): + for j in np.arange(num_cols): + mean_speed = np.mean(velocities[i,j,:,:],axis=0) + if len(mean_speed) % 2 != 0: # odd number + u_u_hh = mean_speed/mean_speed[int(np.floor(len(mean_speed)/2))] + else: + u_u_hh = mean_speed/(mean_speed[int((len(mean_speed)/2))]+mean_speed[int((len(mean_speed)/2))-1])/2 + zg_R = np.linspace(-1,1,len(mean_speed)+2) + polifit_k = np.polyfit(zg_R[1:-1],1-u_u_hh,1) + shear[i,j] = -polifit_k[0] + return shear + + def control_trajectory(rotor_average_velocities,yaw_angles,tilt_angles,air_density,R,shear,sigma,cd,cl_alfa,beta,power_setpoints,power_thrust_table): + if power_setpoints is None: + power_demanded = np.ones_like(tilt_angles)*power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] + else: + power_demanded = power_setpoints/power_thrust_table["generator_efficiency"] + + def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): + a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) + SG = np.sin(np.deg2rad(gamma)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + CD = np.cos(np.deg2rad(delta)) + k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(ct/2))/2)); + + p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 + - (tsr*cd*np.pi*(CD**2*CG**2*SD**2*k**2 + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + 8*tsr**2))/16 + - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 + + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 + + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 + + (CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2))/(4*sinMu**2) + - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 + + (CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi*(3*CG**2*SD**2 + SG**2))/(24*sinMu**2) + - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) + - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu))/(2*np.pi)) + return p + + + def get_ct(x,*data): + sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU = data + CD = np.cos(np.deg2rad(delta)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + SG = np.sin(np.deg2rad(gamma)) + a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) + k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)); + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + + 8*tsr**2))/12)/(2*np.pi) + + return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x + + + ## Define function to get tip speed ratio + def get_tsr(x,*data): + air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,pitch_in,omega_lut_pow,torque_lut_omega,cp_i,pitch_i,tsr_i = data + + omega_lut_torque = omega_lut_pow*np.pi/30; + + omega = x*u/R; + omega_rpm = omega*30/np.pi; + + pitch_in = pitch_in; + pitch_deg = pitch_in; + + torque_nm = np.interp(omega,omega_lut_torque,torque_lut_omega); + + mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) + data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),x,np.deg2rad(pitch_in)+np.deg2rad(beta),mu) + x0 = 0.1 + [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + cp = find_cp(sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),x,np.deg2rad(pitch_in)+np.deg2rad(beta),mu,ct) + + mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) + data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),x,np.deg2rad(pitch_in)+np.deg2rad(beta),mu) + x0 = 0.1 + [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + cp0 = find_cp(sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),x,np.deg2rad(pitch_in)+np.deg2rad(beta),mu,ct) + + eta_p = cp/cp0; + + interp = RegularGridInterpolator((np.squeeze((tsr_i)), + np.squeeze((pitch_i))), cp_i, + bounds_error=False, fill_value=None) + + Cp_now = interp((x,pitch_deg)); + cp_g1 = Cp_now*eta_p; + aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1; + electric_pow = torque_nm*(omega_rpm*np.pi/30); + + y = aero_pow - electric_pow + return y + + ## Define function to get pitch angle + def get_pitch(x,*data): + air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,omega_rated,omega_lut_torque,torque_lut_omega,cp_i,pitch_i,tsr_i = data + + omega_rpm = omega_rated*30/np.pi; + tsr = omega_rated*R/(u); + + pitch_in = np.deg2rad(x); + torque_nm = np.interp(omega_rpm,omega_lut_torque*30/np.pi,torque_lut_omega); + + mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) + data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),tsr,(pitch_in)+np.deg2rad(beta),mu) + x0 = 0.1 + [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + cp = find_cp(sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),tsr,(pitch_in)+np.deg2rad(beta),mu,ct) + + mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) + data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),tsr,(pitch_in)+np.deg2rad(beta),mu) + x0 = 0.1 + [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + cp0 = find_cp(sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),tsr,(pitch_in)+np.deg2rad(beta),mu,ct) + + eta_p = cp/cp0; + + interp = RegularGridInterpolator((np.squeeze((tsr_i)), + np.squeeze((pitch_i))), cp_i, + bounds_error=False, fill_value=None) + + Cp_now = interp((tsr,x)); + cp_g1 = Cp_now*eta_p; + aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1; + electric_pow = torque_nm*(omega_rpm*np.pi/30); + + y = aero_pow - electric_pow + return y + + LUT = np.load('../floris/turbine_library/LUT_IEA3MW.npz') + cp_i = LUT['cp_lut'] + pitch_i = LUT['pitch_lut'] + tsr_i = LUT['tsr_lut'] + interp_lut = RegularGridInterpolator((tsr_i,pitch_i), cp_i) + idx = np.squeeze(np.where(cp_i == np.max(cp_i))) + + tsr_opt = tsr_i[idx[0]] + pitch_opt = pitch_i[idx[1]] + max_cp = cp_i[idx[0],idx[1]] + + omega_cut_in = 1 # RPM + omega_max = 11.75 # RPM + rated_power_aero = 3.37e6/0.936 # MW + #%% Compute torque-rpm relation and check for region 2-and-a-half + Region2andAhalf = False + + omega_array = np.linspace(omega_cut_in,omega_max,21)*np.pi/30 # rad/s + Q = (0.5*air_density*omega_array**2*R**5 * np.pi * max_cp ) / tsr_opt**3 + + Paero_array = Q*omega_array + + if Paero_array[-1] < rated_power_aero: # then we have region 2and1/2 + Region2andAhalf = True + Q_extra = rated_power_aero/(omega_max*np.pi/30) + Q = np.append(Q,Q_extra) + u_r2_end = (Paero_array[-1]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3); + omega_array = np.append(omega_array,omega_array[-1]) + Paero_array = np.append(Paero_array,rated_power_aero) + else: # limit aero_power to the last Q*omega_max + rated_power_aero = Paero_array[-1] + + u_rated = (rated_power_aero/(0.5*air_density*np.pi*R**2*max_cp))**(1/3); + u_array = np.linspace(3,25,45) + idx = np.argmin(np.abs(u_array-u_rated)) + if u_rated > u_array[idx]: + u_array = np.insert(u_array,idx+1,u_rated) + else: + u_array = np.insert(u_array,idx,u_rated) + + pow_lut_omega = Paero_array; + omega_lut_pow = omega_array*30/np.pi; + torque_lut_omega = Q; + omega_lut_torque = omega_lut_pow; + + num_rows, num_cols = tilt_angles.shape + + omega_rated = np.zeros_like(rotor_average_velocities) + u_rated = np.zeros_like(rotor_average_velocities) + for i in np.arange(num_rows): + for j in np.arange(num_cols): + omega_rated[i,j] = np.interp(power_demanded[i,j],pow_lut_omega,omega_lut_pow)*np.pi/30; #rad/s + u_rated[i,j] = (power_demanded[i,j]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3); + + pitch_out = np.zeros_like(rotor_average_velocities) + tsr_out = np.zeros_like(rotor_average_velocities) + + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + k = shear[i,:] + for j in np.arange(num_cols): + u_v = rotor_average_velocities[i,j] + if u_v > u_rated[i,j]: + tsr_v = omega_rated[i,j]*R/u_v*np.cos(np.deg2rad(yaw[j]))**0.5; + else: + tsr_v = tsr_opt*np.cos(np.deg2rad(yaw[j])); + if Region2andAhalf: # fix for interpolation + omega_lut_torque[-1] = omega_lut_torque[-1]+1e-2; + omega_lut_pow[-1] = omega_lut_pow[-1]+1e-2; + + data = air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v,pitch_opt,omega_lut_pow,torque_lut_omega,cp_i,pitch_i,tsr_i + [tsr_out_soluzione,infodict,ier,mesg] = fsolve(get_tsr,tsr_v,args=data,full_output=True) + # check if solution was possible. If not, we are in region 3 + if (np.abs(infodict['fvec']) > 10 or tsr_out_soluzione < 4): + tsr_out_soluzione = 1000; + + # save solution + tsr_outO = tsr_out_soluzione; + omega = tsr_outO*u_v/R; + + # check if we are in region 2 or 3 + if omega < omega_rated[i,j]: # region 2 + # Define optimum pitch + pitch_out0 = pitch_opt; + + else: # region 3 + tsr_outO = omega_rated[i,j]*R/u_v; + data = air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v,omega_rated[i,j],omega_array,Q,cp_i,pitch_i,tsr_i + # if omega_rated[i,j]*R/u_v > 4.25: + # solve aero-electrical power balance with TSR from rated omega + [pitch_out_soluzione,infodict,ier,mesg] = fsolve(get_pitch,8,args=data,factor=0.1,full_output=True) + if pitch_out_soluzione < pitch_opt: + pitch_out_soluzione = pitch_opt + pitch_out0 = pitch_out_soluzione; + # else: + # cp_needed = power_demanded[i,j]/(0.5*air_density*np.pi*R**2*u_v**3) + # pitch_out0 = np.interp(cp_needed,np.flip(cp_i[4,20::]),np.flip(pitch_i[20::])) + #%% COMPUTE CP AND CT GIVEN THE PITCH AND TSR FOUND ABOVE + pitch_out[i,j] = pitch_out0 + tsr_out[i,j] = tsr_outO + + return pitch_out, tsr_out + def power( power_thrust_table: dict, velocities: NDArrayFloat, air_density: float, yaw_angles: NDArrayFloat, tilt_angles: NDArrayFloat, + power_setpoints: NDArrayFloat, tilt_interp: NDArrayObject, average_method: str = "cubic-mean", cubature_weights: NDArrayFloat | None = None, @@ -355,7 +601,7 @@ def power( fill_value=0.0, bounds_error=False, ) - + # Compute the power-effective wind speed across the rotor rotor_average_velocities = average_velocity( velocities=velocities, @@ -369,24 +615,130 @@ def power( ref_air_density=power_thrust_table["ref_air_density"] ) - rotor_effective_velocities = tum_rotor_velocity_yaw_correction( - pP=power_thrust_table["pP"], - yaw_angles=yaw_angles, - rotor_effective_velocities=rotor_effective_velocities, - ) - - rotor_effective_velocities = rotor_velocity_tilt_correction( - tilt_angles=tilt_angles, - ref_tilt=power_thrust_table["ref_tilt"], - pT=power_thrust_table["pT"], - tilt_interp=tilt_interp, - correct_cp_ct_for_tilt=correct_cp_ct_for_tilt, - rotor_effective_velocities=rotor_effective_velocities, - ) - # Compute power - power = power_interpolator(rotor_effective_velocities) * 1e3 # Convert to W - + def get_ct(x,*data): + sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data + CD = np.cos(np.deg2rad(delta)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + SG = np.sin(np.deg2rad(gamma)) + a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) + k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)); + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + + 8*tsr**2))/12)/(2*np.pi) + + return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x + + def computeP(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): + a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) + SG = np.sin(np.deg2rad(gamma)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + CD = np.cos(np.deg2rad(delta)) + k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(ct/2))/2)); + + p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 + - (tsr*cd*np.pi*(CD**2*CG**2*SD**2*k**2 + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + 8*tsr**2))/16 + - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 + + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 + + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 + + (CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2))/(4*sinMu**2) + - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 + + (CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi*(3*CG**2*SD**2 + SG**2))/(24*sinMu**2) + - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) + - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu))/(2*np.pi)) + return p + + num_rows, num_cols = tilt_angles.shape + u = (average_velocity(velocities)) + + shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + + beta = power_thrust_table["beta"] + cd = power_thrust_table["cd"] + cl_alfa = power_thrust_table["cl_alfa"] + + sigma = power_thrust_table["rotor_solidity"] + R = power_thrust_table["rotor_diameter"]/2 + + air_density = power_thrust_table["ref_air_density"] + + pitch_out, tsr_out = TUMLossTurbine.control_trajectory(rotor_average_velocities,yaw_angles,tilt_angles, + air_density,R,shear,sigma,cd,cl_alfa,beta,power_setpoints,power_thrust_table) + + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + tsr_array = (tsr_out); + theta_array = (np.deg2rad(pitch_out+beta)) + + x0 = 0.2 + + p = np.zeros_like(average_velocity(velocities)) + + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + k = shear[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j]) + ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) + if ier == 1: + p[i,j] = computeP(sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j],ct) + else: + p[i,j] = -1e3 + + ############################################################################ + + yaw_angles = np.zeros_like(yaw_angles) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + + p0 = np.zeros_like((average_velocity(velocities))) + + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + k = shear[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j]) + ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) + if ier == 1: + p0[i,j] = computeP(sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j],ct) + else: + p0[i,j] = -1e3 + + razio = p/p0 + + ############################################################################ + + LUT = np.load('../floris/turbine_library/LUT_IEA3MW.npz') + cp_i = LUT['cp_lut'] + pitch_i = LUT['pitch_lut'] + tsr_i = LUT['tsr_lut'] + interp_lut = RegularGridInterpolator((tsr_i,pitch_i), cp_i) + + power_coefficient = np.zeros_like(average_velocity(velocities)) + for i in np.arange(num_rows): + for j in np.arange(num_cols): + cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') + power_coefficient[i,j] = cp_interp*razio[i,j] + + print('Tip speed ratio' + str(tsr_array)) + print('Pitch out: ' + str(pitch_out)) + power = 0.5*air_density*(rotor_effective_velocities)**3*np.pi*R**2*(power_coefficient)*power_thrust_table["generator_efficiency"] return power def thrust_coefficient( @@ -394,20 +746,14 @@ def thrust_coefficient( velocities: NDArrayFloat, yaw_angles: NDArrayFloat, tilt_angles: NDArrayFloat, + power_setpoints: NDArrayFloat, tilt_interp: NDArrayObject, average_method: str = "cubic-mean", cubature_weights: NDArrayFloat | None = None, correct_cp_ct_for_tilt: bool = False, **_ # <- Allows other models to accept other keyword arguments ): - # Construct thrust coefficient interpolant - thrust_coefficient_interpolator = interp1d( - power_thrust_table["wind_speed"], - power_thrust_table["thrust_coefficient"], - fill_value=0.0001, - bounds_error=False, - ) - + # Compute the effective wind speed across the rotor rotor_average_velocities = average_velocity( velocities=velocities, @@ -415,9 +761,6 @@ def thrust_coefficient( cubature_weights=cubature_weights, ) - # TODO: Do we need an air density correction here? - thrust_coefficient = thrust_coefficient_interpolator(rotor_average_velocities) - thrust_coefficient = np.clip(thrust_coefficient, 0.0001, 0.9999) # Apply tilt and yaw corrections # Compute the tilt, if using floating turbines @@ -429,20 +772,105 @@ def thrust_coefficient( ) # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) - - thrust_coefficient = ( - thrust_coefficient - * cosd(yaw_angles) - * cosd(tilt_angles - power_thrust_table["ref_tilt"]) - ) + + def get_ct(x,*data): + sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data + CD = np.cos(np.deg2rad(delta)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + SG = np.sin(np.deg2rad(gamma)) + a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) + k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)); + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + + 8*tsr**2))/12)/(2*np.pi) + + return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x + + beta = power_thrust_table["beta"] + cd = power_thrust_table["cd"] + cl_alfa = power_thrust_table["cl_alfa"] + + sigma = power_thrust_table["rotor_solidity"] + R = power_thrust_table["rotor_diameter"]/2 + + shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + + air_density = power_thrust_table["ref_air_density"] # CHANGE + pitch_out, tsr_out = TUMLossTurbine.control_trajectory(rotor_average_velocities,yaw_angles,tilt_angles, + air_density,R,shear,sigma,cd,cl_alfa,beta,power_setpoints,power_thrust_table) + + num_rows, num_cols = tilt_angles.shape + + u = (average_velocity(velocities)) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + # u = np.squeeze(u) + theta_array = (np.deg2rad(pitch_out+beta)) + tsr_array = (tsr_out) + + x0 = 0.2 + + thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j]) + ct = fsolve(get_ct, x0,args=data) + thrust_coefficient1[i,j] = np.clip(ct, 0.0001, 0.9999) + + + yaw_angles = np.zeros_like(yaw_angles) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + + thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) + + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j]) + ct = fsolve(get_ct, x0,args=data) + thrust_coefficient0[i,j] = ct #np.clip(ct, 0.0001, 0.9999) + + ############################################################################ + + razio = thrust_coefficient1/thrust_coefficient0 + + LUT = np.load('../floris/turbine_library/LUT_IEA3MW.npz') + ct_i = LUT['ct_lut'] + pitch_i = LUT['pitch_lut'] + tsr_i = LUT['tsr_lut'] + interp_lut = RegularGridInterpolator((tsr_i,pitch_i), ct_i)#*0.9722085500886761) + + + thrust_coefficient = np.zeros_like(average_velocity(velocities)) + + for i in np.arange(num_rows): + for j in np.arange(num_cols): + ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') + thrust_coefficient[i,j] = ct_interp*razio[i,j] return thrust_coefficient - + def axial_induction( power_thrust_table: dict, velocities: NDArrayFloat, yaw_angles: NDArrayFloat, tilt_angles: NDArrayFloat, + power_setpoints: NDArrayFloat, tilt_interp: NDArrayObject, average_method: str = "cubic-mean", cubature_weights: NDArrayFloat | None = None, @@ -450,19 +878,119 @@ def axial_induction( **_ # <- Allows other models to accept other keyword arguments ): - thrust_coefficient = CosineLossTurbine.thrust_coefficient( - power_thrust_table=power_thrust_table, + # Compute the effective wind speed across the rotor + rotor_average_velocities = average_velocity( velocities=velocities, - yaw_angles=yaw_angles, + method=average_method, + cubature_weights=cubature_weights, + ) + + + # Apply tilt and yaw corrections + # Compute the tilt, if using floating turbines + old_tilt_angles = copy.deepcopy(tilt_angles) + tilt_angles = compute_tilt_angles_for_floating_turbines( tilt_angles=tilt_angles, tilt_interp=tilt_interp, - average_method=average_method, - cubature_weights=cubature_weights, - correct_cp_ct_for_tilt=correct_cp_ct_for_tilt + rotor_effective_velocities=rotor_average_velocities, ) + # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) + tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) + + def get_ct(x,*data): + sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data + CD = np.cos(np.deg2rad(delta)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + SG = np.sin(np.deg2rad(gamma)) + a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) + k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)); + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + + 8*tsr**2))/12)/(2*np.pi) + + return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x + + beta = power_thrust_table["beta"] + cd = power_thrust_table["cd"] + cl_alfa = power_thrust_table["cl_alfa"] + + sigma = power_thrust_table["rotor_solidity"] + R = power_thrust_table["rotor_diameter"]/2 + + shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + + air_density = power_thrust_table["ref_air_density"] # CHANGE + pitch_out, tsr_out = TUMLossTurbine.control_trajectory(rotor_average_velocities,yaw_angles,tilt_angles, + air_density,R,shear,sigma,cd,cl_alfa,beta,power_setpoints,power_thrust_table) + + num_rows, num_cols = tilt_angles.shape + + u = (average_velocity(velocities)) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + # u = np.squeeze(u) + theta_array = (np.deg2rad(pitch_out+beta)) + tsr_array = (tsr_out) + + x0 = 0.2 + + thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j]) + ct = fsolve(get_ct, x0,args=data) + thrust_coefficient1[i,j] = np.clip(ct, 0.0001, 0.9999) + + + yaw_angles = np.zeros_like(yaw_angles) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + + thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) + + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j]) + ct = fsolve(get_ct, x0,args=data) + thrust_coefficient0[i,j] = ct #np.clip(ct, 0.0001, 0.9999) + + ############################################################################ + + razio = thrust_coefficient1/thrust_coefficient0 + + LUT = np.load('../floris/turbine_library/LUT_IEA3MW.npz') + ct_i = LUT['ct_lut'] + pitch_i = LUT['pitch_lut'] + tsr_i = LUT['tsr_lut'] + interp_lut = RegularGridInterpolator((tsr_i,pitch_i), ct_i)#*0.9722085500886761) + + axial_induction = np.zeros_like(average_velocity(velocities)) + + for i in np.arange(num_rows): + for j in np.arange(num_cols): + ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') + ct = ct_interp*razio[i,j] + a = (1- ( (1+np.sqrt(1-ct-1/16*ct**2*sMu[j]**2))/(2*(1+1/16*ct*sMu[j]**2))) ) + axial_induction[i,j] = np.clip(a, 0.0001, 0.9999) + + return axial_induction + - misalignment_loss = cosd(yaw_angles) * cosd(tilt_angles - power_thrust_table["ref_tilt"]) - return 0.5 / misalignment_loss * (1 - np.sqrt(1 - thrust_coefficient * misalignment_loss)) @define class SimpleDeratingTurbine(BaseOperationModel): diff --git a/floris/turbine_library/LUT_IEA3MW.npz b/floris/turbine_library/LUT_IEA3MW.npz new file mode 100644 index 0000000000000000000000000000000000000000..6e43da4698201de996c4f2b2d16afd88a9e951c7 GIT binary patch literal 16902 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 literal 0 HcmV?d00001 diff --git a/floris/turbine_library/iea_3MW.yaml b/floris/turbine_library/iea_3MW.yaml new file mode 100644 index 000000000..7ee22de7f --- /dev/null +++ b/floris/turbine_library/iea_3MW.yaml @@ -0,0 +1,230 @@ +# NREL 5MW reference wind turbine. +# Data based on: +# https://github.com/NREL/turbine-models/blob/master/Offshore/NREL_5MW_126_RWT_corrected.csv +# Note: Small power variations above rated removed. Rotor diameter includes coning angle. + +### +# An ID for this type of turbine definition. +# This is not currently used, but it will be enabled in the future. This should typically +# match the root name of the file. +turbine_type: 'iea_3MW' + +### +# Setting for generator losses to power. +generator_efficiency: 0.944 + +### +# Hub height. +hub_height: 110.0 + +### +# Rotor diameter. +rotor_diameter: 130.0 + +### +# Tip speed ratio defined as linear blade tip speed normalized by the incoming wind speed. +TSR: 8.0 + +### +# Model for power and thrust curve interpretation. +#power_thrust_model: 'cosine-loss' +power_thrust_model: 'tum-loss' +### +# Cp and Ct as a function of wind speed for the turbine's full range of operating conditions. +power_thrust_table: + ### Power thrust table parameters + # The air density at which the Cp and Ct curves are defined. + ref_air_density: 1.225 + rotor_solidity: 0.0416 + generator_efficiency: 0.925 + rated_power: 3.3e6 + rotor_diameter: 130 + beta: -3.11 + cd: 0.0051 + cl_alfa: 4.75 + # The tilt angle at which the Cp and Ct curves are defined. This is used to capture + # the effects of a floating platform on a turbine's power and wake. + ref_tilt: 5.0 + # Cosine exponent for power loss due to tilt. + pT: 1.88 + # Cosine exponent for power loss due to yaw misalignment. + pP: 1.88 + ### Power thrust table data + wind_speed: + - 0.0 + - 2.9 + - 3.0 + - 4.0 + - 5.0 + - 6.0 + - 7.0 + - 7.1 + - 7.2 + - 7.3 + - 7.4 + - 7.5 + - 7.6 + - 7.7 + - 7.8 + - 7.9 + - 8.0 + - 9.0 + - 10.0 + - 10.1 + - 10.2 + - 10.3 + - 10.4 + - 10.5 + - 10.6 + - 10.7 + - 10.8 + - 10.9 + - 11.0 + - 11.1 + - 11.2 + - 11.3 + - 11.4 + - 11.5 + - 11.6 + - 11.7 + - 11.8 + - 11.9 + - 12.0 + - 13.0 + - 14.0 + - 15.0 + - 16.0 + - 17.0 + - 18.0 + - 19.0 + - 20.0 + - 21.0 + - 22.0 + - 23.0 + - 24.0 + - 25.0 + - 25.1 + - 50.0 + power: + - 0.0 + - 0.0 + - 40.518011517569214 + - 177.67162506419703 + - 403.900880943964 + - 737.5889584824021 + - 1187.1774030611875 + - 1239.245945375778 + - 1292.5184293723503 + - 1347.3213147477102 + - 1403.2573725578948 + - 1460.7011898730707 + - 1519.6419125979983 + - 1580.174365096404 + - 1642.1103166918167 + - 1705.758292831 + - 1771.1659528893977 + - 2518.553107505315 + - 3448.381605840943 + - 3552.140809000129 + - 3657.9545431794127 + - 3765.121299313842 + - 3873.928844315059 + - 3984.4800226955504 + - 4096.582833096852 + - 4210.721306623712 + - 4326.154305853405 + - 4443.395565353604 + - 4562.497934188341 + - 4683.419890251577 + - 4806.164748311019 + - 4929.931918769215 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 0.0 + - 0.0 + thrust_coefficient: + - 0.0 + - 0.0 + - 1.132034888 + - 0.999470963 + - 0.917697381 + - 0.860849503 + - 0.815371198 + - 0.811614904 + - 0.807939328 + - 0.80443352 + - 0.800993851 + - 0.79768116 + - 0.794529244 + - 0.791495834 + - 0.788560434 + - 0.787217182 + - 0.787127977 + - 0.785839257 + - 0.783812219 + - 0.783568108 + - 0.783328285 + - 0.781194418 + - 0.777292539 + - 0.773464375 + - 0.769690236 + - 0.766001924 + - 0.762348072 + - 0.758760824 + - 0.755242872 + - 0.751792927 + - 0.748434131 + - 0.745113997 + - 0.717806682 + - 0.672204789 + - 0.63831272 + - 0.610176496 + - 0.585456847 + - 0.563222111 + - 0.542912273 + - 0.399312061 + - 0.310517829 + - 0.248633226 + - 0.203543725 + - 0.169616419 + - 0.143478955 + - 0.122938861 + - 0.106515296 + - 0.093026095 + - 0.081648606 + - 0.072197368 + - 0.064388275 + - 0.057782745 + - 0.0 + - 0.0 + +### +# A boolean flag used when the user wants FLORIS to use the user-supplied multi-dimensional +# Cp/Ct information. +multi_dimensional_cp_ct: False + +### +# The path to the .csv file that contains the multi-dimensional Cp/Ct data. The format of this +# file is such that any external conditions, such as wave height or wave period, that the +# Cp/Ct data is dependent on come first, in column format. The last three columns of the .csv +# file must be ``ws``, ``Cp``, and ``Ct``, in that order. An example of fictional data is given +# in ``floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv``. +power_thrust_data_file: '../floris/turbine_library/LUT_IEA3MW.npz' From c805ee0e3a65782bdccd170a3cab0a8504307fa3 Mon Sep 17 00:00:00 2001 From: sTamaroTum Date: Wed, 6 Mar 2024 11:30:39 +0100 Subject: [PATCH 13/53] ok --- floris/turbine_library/nrel_5MW.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/floris/turbine_library/nrel_5MW.yaml b/floris/turbine_library/nrel_5MW.yaml index 9a93245eb..97b518d8d 100644 --- a/floris/turbine_library/nrel_5MW.yaml +++ b/floris/turbine_library/nrel_5MW.yaml @@ -32,6 +32,13 @@ power_thrust_table: ### Power thrust table parameters # The air density at which the Cp and Ct curves are defined. ref_air_density: 1.225 + rotor_solidity: 0.05132 + generator_efficiency: 0.944 + rated_power: 5.0e6 + rotor_diameter: 126 + beta: -0.45891 + cd: 0.0040638 + cl_alfa: 4.275049 # The tilt angle at which the Cp and Ct curves are defined. This is used to capture # the effects of a floating platform on a turbine's power and wake. ref_tilt: 5.0 From 3d40302b84a54c1f3250214f95fc6683b4ed36c5 Mon Sep 17 00:00:00 2001 From: sTamaroTum Date: Wed, 6 Mar 2024 11:51:24 +0100 Subject: [PATCH 14/53] small fix --- floris/simulation/turbine/operation_models.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index 94c9c8ba6..09cdee63b 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -317,7 +317,7 @@ class TUMLossTurbine(BaseOperationModel): Nonzero tilt and yaw angles are handled via the model presented in https://doi.org/10.5194/wes-2023-133 . The method requires C_P, C_T look-up tables as functions of tip speed ratio and blade pitch angle, available here: - "../../LUT_IEA3MW.npz" for the IEA 3.4 MW (Bortolotti et al., 2019) + "../floris/turbine_library/LUT_IEA3MW.npz" for the IEA 3.4 MW (Bortolotti et al., 2019) As with all turbine submodules, implements only static power() and thrust_coefficient() methods, which are called by power() and thrust_coefficient() on turbine.py, respectively. There are also two new functions, i.e. compute_local_vertical_shear() and control_trajectory(). @@ -348,7 +348,7 @@ def control_trajectory(rotor_average_velocities,yaw_angles,tilt_angles,air_densi power_demanded = np.ones_like(tilt_angles)*power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] else: power_demanded = power_setpoints/power_thrust_table["generator_efficiency"] - + def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) SG = np.sin(np.deg2rad(gamma)) @@ -589,6 +589,9 @@ def power( fill_value=0.0, bounds_error=False, ) + + # sign convention. in the tum model, negative tilt creates tower clearance + tilt_angles = -tilt_angles # Compute the power-effective wind speed across the rotor rotor_average_velocities = average_velocity( @@ -716,7 +719,7 @@ def computeP(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): cp_i = LUT['cp_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator((tsr_i,pitch_i), cp_i) + interp_lut = RegularGridInterpolator((tsr_i,pitch_i), cp_i,bounds_error=False, fill_value=None) power_coefficient = np.zeros_like(average_velocity(velocities)) for i in np.arange(num_rows): @@ -741,6 +744,9 @@ def thrust_coefficient( correct_cp_ct_for_tilt: bool = False, **_ # <- Allows other models to accept other keyword arguments ): + + # sign convention. in the tum model, negative tilt creates tower clearance + tilt_angles = -tilt_angles # Compute the effective wind speed across the rotor rotor_average_velocities = average_velocity( @@ -841,7 +847,7 @@ def get_ct(x,*data): ct_i = LUT['ct_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator((tsr_i,pitch_i), ct_i)#*0.9722085500886761) + interp_lut = RegularGridInterpolator((tsr_i,pitch_i), ct_i,bounds_error=False, fill_value=None)#*0.9722085500886761) thrust_coefficient = np.zeros_like(average_velocity(velocities)) @@ -866,6 +872,9 @@ def axial_induction( **_ # <- Allows other models to accept other keyword arguments ): + # sign convention. in the tum model, negative tilt creates tower clearance + tilt_angles = -tilt_angles + # Compute the effective wind speed across the rotor rotor_average_velocities = average_velocity( velocities=velocities, @@ -965,7 +974,7 @@ def get_ct(x,*data): ct_i = LUT['ct_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator((tsr_i,pitch_i), ct_i)#*0.9722085500886761) + interp_lut = RegularGridInterpolator((tsr_i,pitch_i), ct_i,bounds_error=False, fill_value=None)#*0.9722085500886761) axial_induction = np.zeros_like(average_velocity(velocities)) From 448ac07adba5fb40c03f11974a9eaa7be293a7a5 Mon Sep 17 00:00:00 2001 From: sTamaroTum Date: Wed, 6 Mar 2024 17:30:02 +0100 Subject: [PATCH 15/53] removed useless lines --- floris/simulation/turbine/operation_models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index 09cdee63b..bcc8e3dfe 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -469,7 +469,6 @@ def get_pitch(x,*data): cp_i = LUT['cp_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator((tsr_i,pitch_i), cp_i) idx = np.squeeze(np.where(cp_i == np.max(cp_i))) tsr_opt = tsr_i[idx[0]] From 1957805df94d294b1fb02842e30335c063d41765 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 6 Mar 2024 11:28:44 -0700 Subject: [PATCH 16/53] Remove assertions from tests; tests will need building out. Robust paths to data tables. --- floris/simulation/turbine/operation_models.py | 32 ++++++++++------ tests/conftest.py | 12 ++++++ ...rbine_operation_models_integration_test.py | 37 ++++++++++++------- 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index bcc8e3dfe..7c39361de 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -3,6 +3,8 @@ import copy from abc import abstractmethod +import os +from pathlib import Path from typing import ( Any, Dict, @@ -465,7 +467,9 @@ def get_pitch(x,*data): y = aero_pow - electric_pow return y - LUT = np.load('../floris/turbine_library/LUT_IEA3MW.npz') + pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] + lut_file = pkgroot / "turbine_library" / "LUT_IEA3MW.npz" + LUT = np.load(lut_file) cp_i = LUT['cp_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] @@ -581,13 +585,13 @@ def power( correct_cp_ct_for_tilt: bool = False, **_ # <- Allows other models to accept other keyword arguments ): - # Construct power interpolant - power_interpolator = interp1d( - power_thrust_table["wind_speed"], - power_thrust_table["power"], - fill_value=0.0, - bounds_error=False, - ) + # # Construct power interpolant + # power_interpolator = interp1d( + # power_thrust_table["wind_speed"], + # power_thrust_table["power"], + # fill_value=0.0, + # bounds_error=False, + # ) # sign convention. in the tum model, negative tilt creates tower clearance tilt_angles = -tilt_angles @@ -714,7 +718,9 @@ def computeP(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): ############################################################################ - LUT = np.load('../floris/turbine_library/LUT_IEA3MW.npz') + pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] + lut_file = pkgroot / "turbine_library" / "LUT_IEA3MW.npz" + LUT = np.load(lut_file) cp_i = LUT['cp_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] @@ -842,7 +848,9 @@ def get_ct(x,*data): razio = thrust_coefficient1/thrust_coefficient0 - LUT = np.load('../floris/turbine_library/LUT_IEA3MW.npz') + pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] + lut_file = pkgroot / "turbine_library" / "LUT_IEA3MW.npz" + LUT = np.load(lut_file) ct_i = LUT['ct_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] @@ -969,7 +977,9 @@ def get_ct(x,*data): razio = thrust_coefficient1/thrust_coefficient0 - LUT = np.load('../floris/turbine_library/LUT_IEA3MW.npz') + pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] + lut_file = pkgroot / "turbine_library" / "LUT_IEA3MW.npz" + LUT = np.load(lut_file) ct_i = LUT['ct_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] diff --git a/tests/conftest.py b/tests/conftest.py index 65bc4f486..118a6e302 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -368,6 +368,18 @@ def __init__(self): "TSR": 8.0 } + self.tum_loss_turbine_power_thrust_table = { + "ref_air_density": 1.225, + "rotor_solidity": 0.05132, + "generator_efficiency": 0.944, + "rated_power": 5.0e6, + "rotor_diameter": 126, + "beta": -0.45891, + "cd": 0.0040638, + "cl_alfa": 4.275049, + "ref_tilt": 5.0, + } + self.turbine_floating = copy.deepcopy(self.turbine) self.turbine_floating["floating_tilt_table"] = { "tilt": [ diff --git a/tests/turbine_operation_models_integration_test.py b/tests/turbine_operation_models_integration_test.py index 86cb1f3a4..f0421ac34 100644 --- a/tests/turbine_operation_models_integration_test.py +++ b/tests/turbine_operation_models_integration_test.py @@ -512,9 +512,11 @@ def test_TUMLossTurbine(): n_turbines = 1 wind_speed = 10.0 turbine_data = SampleInputs().turbine + turbine_data["power_thrust_table"] = SampleInputs().tum_loss_turbine_power_thrust_table yaw_angles_nom = 0 * np.ones((1, n_turbines)) tilt_angles_nom = turbine_data["power_thrust_table"]["ref_tilt"] * np.ones((1, n_turbines)) + power_setpoints_nom = POWER_SETPOINT_DEFAULT * np.ones((1, n_turbines)) yaw_angles_test = 20 * np.ones((1, n_turbines)) tilt_angles_test = 0 * np.ones((1, n_turbines)) @@ -525,12 +527,13 @@ def test_TUMLossTurbine(): velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=turbine_data["power_thrust_table"]["ref_air_density"], # Matches ref_air_density yaw_angles=yaw_angles_nom, + power_setpoints=power_setpoints_nom, tilt_angles=tilt_angles_nom, tilt_interp=None ) - truth_index = turbine_data["power_thrust_table"]["wind_speed"].index(wind_speed) - baseline_power = turbine_data["power_thrust_table"]["power"][truth_index] * 1000 - assert np.allclose(baseline_power, test_power) + # truth_index = turbine_data["power_thrust_table"]["wind_speed"].index(wind_speed) + # baseline_power = turbine_data["power_thrust_table"]["power"][truth_index] * 1000 + # assert np.allclose(baseline_power, test_power) # Check that yaw and tilt angle have an effect test_power = TUMLossTurbine.power( @@ -538,10 +541,11 @@ def test_TUMLossTurbine(): velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=turbine_data["power_thrust_table"]["ref_air_density"], # Matches ref_air_density yaw_angles=yaw_angles_test, + power_setpoints=power_setpoints_nom, tilt_angles=tilt_angles_test, tilt_interp=None ) - assert test_power < baseline_power + #assert test_power < baseline_power # Check that a lower air density decreases power appropriately test_power = TUMLossTurbine.power( @@ -549,10 +553,11 @@ def test_TUMLossTurbine(): velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, yaw_angles=yaw_angles_nom, + power_setpoints=power_setpoints_nom, tilt_angles=tilt_angles_nom, tilt_interp=None ) - assert test_power < baseline_power + #assert test_power < baseline_power # Check that thrust coefficient works as expected @@ -561,11 +566,12 @@ def test_TUMLossTurbine(): velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, # Unused yaw_angles=yaw_angles_nom, + power_setpoints=power_setpoints_nom, tilt_angles=tilt_angles_nom, tilt_interp=None ) - baseline_Ct = turbine_data["power_thrust_table"]["thrust_coefficient"][truth_index] - assert np.allclose(baseline_Ct, test_Ct) + #baseline_Ct = turbine_data["power_thrust_table"]["thrust_coefficient"][truth_index] + #assert np.allclose(baseline_Ct, test_Ct) # Check that yaw and tilt angle have the expected effect test_Ct = TUMLossTurbine.thrust_coefficient( @@ -573,11 +579,12 @@ def test_TUMLossTurbine(): velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, # Unused yaw_angles=yaw_angles_test, + power_setpoints=power_setpoints_nom, tilt_angles=tilt_angles_test, tilt_interp=None ) - absolute_tilt = tilt_angles_test - turbine_data["power_thrust_table"]["ref_tilt"] - assert test_Ct == baseline_Ct * cosd(yaw_angles_test) * cosd(absolute_tilt) + #absolute_tilt = tilt_angles_test - turbine_data["power_thrust_table"]["ref_tilt"] + #assert test_Ct == baseline_Ct * cosd(yaw_angles_test) * cosd(absolute_tilt) # Check that thrust coefficient works as expected @@ -586,6 +593,7 @@ def test_TUMLossTurbine(): velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, # Unused yaw_angles=yaw_angles_nom, + power_setpoints=power_setpoints_nom, tilt_angles=tilt_angles_nom, tilt_interp=None ) @@ -593,10 +601,10 @@ def test_TUMLossTurbine(): cosd(yaw_angles_nom) * cosd(tilt_angles_nom - turbine_data["power_thrust_table"]["ref_tilt"]) ) - baseline_ai = ( - 1 - np.sqrt(1 - turbine_data["power_thrust_table"]["thrust_coefficient"][truth_index]) - ) / 2 / baseline_misalignment_loss - assert np.allclose(baseline_ai, test_ai) + # baseline_ai = ( + # 1 - np.sqrt(1 - turbine_data["power_thrust_table"]["thrust_coefficient"][truth_index]) + # ) / 2 / baseline_misalignment_loss + # assert np.allclose(baseline_ai, test_ai) # Check that yaw and tilt angle have the expected effect test_ai = TUMLossTurbine.axial_induction( @@ -604,8 +612,9 @@ def test_TUMLossTurbine(): velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, # Unused yaw_angles=yaw_angles_test, + power_setpoints=power_setpoints_nom, tilt_angles=tilt_angles_test, tilt_interp=None ) absolute_tilt = tilt_angles_test - turbine_data["power_thrust_table"]["ref_tilt"] - assert test_Ct == baseline_Ct * cosd(yaw_angles_test) * cosd(absolute_tilt) + #assert test_Ct == baseline_Ct * cosd(yaw_angles_test) * cosd(absolute_tilt) From 4511a85bae01fa75b72435fb97ff7600c9c54c08 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 6 Mar 2024 11:28:57 -0700 Subject: [PATCH 17/53] set, run in example. --- examples/41_compare_yaw_loss.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/41_compare_yaw_loss.py b/examples/41_compare_yaw_loss.py index 60edab17d..869c411c4 100644 --- a/examples/41_compare_yaw_loss.py +++ b/examples/41_compare_yaw_loss.py @@ -46,11 +46,12 @@ fi = FlorisInterface("inputs/gch.yaml") # Initialize to a simple 1 turbine case with n_findex = N - fi.reinitialize(layout_x=[0], - layout_y=[0], - wind_directions=270 * np.ones(N), - wind_speeds=8 * np.ones(N), - ) + fi.set( + layout_x=[0], + layout_y=[0], + wind_directions=270 * np.ones(N), + wind_speeds=8 * np.ones(N), + ) with open(str( fi.floris.as_dict()["farm"]["turbine_library_path"] / @@ -60,10 +61,10 @@ turbine_type["power_thrust_model"] = op_model # Change the turbine type - fi.reinitialize(turbine_type=[turbine_type]) + fi.set(turbine_type=[turbine_type], yaw_angles=yaw_angles) # Calculate the power - fi.calculate_wake(yaw_angles=yaw_angles) + fi.run() turbine_power = fi.get_turbine_powers().squeeze() # Save the results From f68ac86fbceefd8648ae32b0203e41270dd978bf Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 6 Mar 2024 11:49:14 -0700 Subject: [PATCH 18/53] Format for ruff. --- floris/simulation/turbine/operation_models.py | 704 ++++++++++++------ 1 file changed, 458 insertions(+), 246 deletions(-) diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index 7c39361de..f9d96b6b5 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -2,8 +2,8 @@ from __future__ import annotations import copy -from abc import abstractmethod import os +from abc import abstractmethod from pathlib import Path from typing import ( Any, @@ -316,22 +316,24 @@ def axial_induction( class TUMLossTurbine(BaseOperationModel): """ Static class defining a wind turbine model that may be misaligned with the flow. - Nonzero tilt and yaw angles are handled via the model presented in https://doi.org/10.5194/wes-2023-133 . - - The method requires C_P, C_T look-up tables as functions of tip speed ratio and blade pitch angle, available here: + Nonzero tilt and yaw angles are handled via the model presented in + https://doi.org/10.5194/wes-2023-133 . + + The method requires C_P, C_T look-up tables as functions of tip speed ratio and blade pitch + angle, available here: "../floris/turbine_library/LUT_IEA3MW.npz" for the IEA 3.4 MW (Bortolotti et al., 2019) As with all turbine submodules, implements only static power() and thrust_coefficient() methods, - which are called by power() and thrust_coefficient() on turbine.py, respectively. - There are also two new functions, i.e. compute_local_vertical_shear() and control_trajectory(). - These are called by thrust_coefficient() and power() to compute the vertical shear and predict the turbine status - in terms of tip speed ratio and pitch angle. + which are called by power() and thrust_coefficient() on turbine.py, respectively. + There are also two new functions, i.e. compute_local_vertical_shear() and control_trajectory(). + These are called by thrust_coefficient() and power() to compute the vertical shear and predict + the turbine status in terms of tip speed ratio and pitch angle. This class is not intended to be instantiated; it simply defines a library of static methods. TODO: Should the turbine submodels each implement axial_induction()? """ - + def compute_local_vertical_shear(velocities,avg_velocities): - num_rows, num_cols = avg_velocities.shape + num_rows, num_cols = avg_velocities.shape shear = np.zeros_like(avg_velocities) for i in np.arange(num_rows): for j in np.arange(num_cols): @@ -339,41 +341,71 @@ def compute_local_vertical_shear(velocities,avg_velocities): if len(mean_speed) % 2 != 0: # odd number u_u_hh = mean_speed/mean_speed[int(np.floor(len(mean_speed)/2))] else: - u_u_hh = mean_speed/(mean_speed[int((len(mean_speed)/2))]+mean_speed[int((len(mean_speed)/2))-1])/2 + u_u_hh = ( + mean_speed + /(mean_speed[int((len(mean_speed)/2))] + +mean_speed[int((len(mean_speed)/2))-1] + )/2 + ) zg_R = np.linspace(-1,1,len(mean_speed)+2) polifit_k = np.polyfit(zg_R[1:-1],1-u_u_hh,1) shear[i,j] = -polifit_k[0] return shear - - def control_trajectory(rotor_average_velocities,yaw_angles,tilt_angles,air_density,R,shear,sigma,cd,cl_alfa,beta,power_setpoints,power_thrust_table): + + def control_trajectory( + rotor_average_velocities, + yaw_angles, + tilt_angles, + air_density, + R, + shear, + sigma, + cd, + cl_alfa, + beta, + power_setpoints, + power_thrust_table + ): if power_setpoints is None: - power_demanded = np.ones_like(tilt_angles)*power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] + power_demanded = ( + np.ones_like(tilt_angles)*power_thrust_table["rated_power"] + /power_thrust_table["generator_efficiency"] + ) else: - power_demanded = power_setpoints/power_thrust_table["generator_efficiency"] - + power_demanded = power_setpoints/power_thrust_table["generator_efficiency"] + def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) SG = np.sin(np.deg2rad(gamma)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - CD = np.cos(np.deg2rad(delta)) - k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(ct/2))/2)); - - p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 - - (tsr*cd*np.pi*(CD**2*CG**2*SD**2*k**2 + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + 8*tsr**2))/16 - - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 - + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 - + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 - + (CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2))/(4*sinMu**2) - - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 - + (CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi*(3*CG**2*SD**2 + SG**2))/(24*sinMu**2) - - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) - - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu))/(2*np.pi)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + CD = np.cos(np.deg2rad(delta)) + k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(ct/2))/2)) + + p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 + - (tsr*cd*np.pi*( + CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 + - 8*CD*tsr*SG*k + 8*tsr**2 + ))/16 + - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 + + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 + + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 + + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 + + ((CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2)) + /(4*sinMu**2)) + - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 + + ((CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi + *(3*CG**2*SD**2 + SG**2)) + /(24*sinMu**2)) + - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) + - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu) + )/(2*np.pi)) return p - - + + def get_ct(x,*data): sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU = data CD = np.cos(np.deg2rad(delta)) @@ -381,92 +413,153 @@ def get_ct(x,*data): SD = np.sin(np.deg2rad(delta)) SG = np.sin(np.deg2rad(gamma)) a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)); - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)) + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + 8*tsr**2))/12)/(2*np.pi) return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x - - + + ## Define function to get tip speed ratio def get_tsr(x,*data): - air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,pitch_in,omega_lut_pow,torque_lut_omega,cp_i,pitch_i,tsr_i = data - - omega_lut_torque = omega_lut_pow*np.pi/30; - - omega = x*u/R; - omega_rpm = omega*30/np.pi; - - pitch_in = pitch_in; - pitch_deg = pitch_in; - - torque_nm = np.interp(omega,omega_lut_torque,torque_lut_omega); - + (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,pitch_in,omega_lut_pow, + torque_lut_omega,cp_i,pitch_i,tsr_i) = data + + omega_lut_torque = omega_lut_pow*np.pi/30 + + omega = x*u/R + omega_rpm = omega*30/np.pi + + pitch_in = pitch_in + pitch_deg = pitch_in + + torque_nm = np.interp(omega,omega_lut_torque,torque_lut_omega) + mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),x,np.deg2rad(pitch_in)+np.deg2rad(beta),mu) + data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),x, + np.deg2rad(pitch_in)+np.deg2rad(beta),mu) x0 = 0.1 [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) - cp = find_cp(sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),x,np.deg2rad(pitch_in)+np.deg2rad(beta),mu,ct) - + cp = find_cp( + sigma, + cd, + cl_alfa, + gamma, + tilt, + shear, + np.cos(mu), + np.sin(mu), + x, + np.deg2rad(pitch_in)+np.deg2rad(beta), + mu, + ct + ) + mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),x,np.deg2rad(pitch_in)+np.deg2rad(beta),mu) + data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),x, + np.deg2rad(pitch_in)+np.deg2rad(beta),mu) x0 = 0.1 [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) - cp0 = find_cp(sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),x,np.deg2rad(pitch_in)+np.deg2rad(beta),mu,ct) - - eta_p = cp/cp0; - + cp0 = find_cp( + sigma, + cd, + cl_alfa, + 0, + tilt, + shear, + np.cos(mu), + np.sin(mu), + x, + np.deg2rad(pitch_in)+np.deg2rad(beta), + mu, + ct + ) + + eta_p = cp/cp0 + interp = RegularGridInterpolator((np.squeeze((tsr_i)), np.squeeze((pitch_i))), cp_i, bounds_error=False, fill_value=None) - - Cp_now = interp((x,pitch_deg)); - cp_g1 = Cp_now*eta_p; - aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1; - electric_pow = torque_nm*(omega_rpm*np.pi/30); - + + Cp_now = interp((x,pitch_deg)) + cp_g1 = Cp_now*eta_p + aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 + electric_pow = torque_nm*(omega_rpm*np.pi/30) + y = aero_pow - electric_pow return y ## Define function to get pitch angle def get_pitch(x,*data): - air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,omega_rated,omega_lut_torque,torque_lut_omega,cp_i,pitch_i,tsr_i = data - - omega_rpm = omega_rated*30/np.pi; - tsr = omega_rated*R/(u); - - pitch_in = np.deg2rad(x); - torque_nm = np.interp(omega_rpm,omega_lut_torque*30/np.pi,torque_lut_omega); - + (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,omega_rated,omega_lut_torque, + torque_lut_omega,cp_i,pitch_i,tsr_i) = data + + omega_rpm = omega_rated*30/np.pi + tsr = omega_rated*R/(u) + + pitch_in = np.deg2rad(x) + torque_nm = np.interp(omega_rpm,omega_lut_torque*30/np.pi,torque_lut_omega) + mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),tsr,(pitch_in)+np.deg2rad(beta),mu) + data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),tsr, + (pitch_in)+np.deg2rad(beta),mu) x0 = 0.1 [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) - cp = find_cp(sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),tsr,(pitch_in)+np.deg2rad(beta),mu,ct) - + cp = find_cp( + sigma, + cd, + cl_alfa, + gamma, + tilt, + shear, + np.cos(mu), + np.sin(mu), + tsr, + (pitch_in)+np.deg2rad(beta), + mu, + ct + ) + mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),tsr,(pitch_in)+np.deg2rad(beta),mu) + data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),tsr, + (pitch_in)+np.deg2rad(beta),mu) x0 = 0.1 [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) - cp0 = find_cp(sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),tsr,(pitch_in)+np.deg2rad(beta),mu,ct) - - eta_p = cp/cp0; - - interp = RegularGridInterpolator((np.squeeze((tsr_i)), - np.squeeze((pitch_i))), cp_i, - bounds_error=False, fill_value=None) - - Cp_now = interp((tsr,x)); - cp_g1 = Cp_now*eta_p; - aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1; - electric_pow = torque_nm*(omega_rpm*np.pi/30); - + cp0 = find_cp( + sigma, + cd, + cl_alfa, + 0, + tilt, + shear, + np.cos(mu), + np.sin(mu), + tsr, + (pitch_in)+np.deg2rad(beta), + mu, + ct + ) + + eta_p = cp/cp0 + + interp = RegularGridInterpolator( + (np.squeeze((tsr_i)), np.squeeze((pitch_i))), + cp_i, + bounds_error=False, + fill_value=None + ) + + Cp_now = interp((tsr,x)) + cp_g1 = Cp_now*eta_p + aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 + electric_pow = torque_nm*(omega_rpm*np.pi/30) + y = aero_pow - electric_pow return y - + pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] lut_file = pkgroot / "turbine_library" / "LUT_IEA3MW.npz" LUT = np.load(lut_file) @@ -486,7 +579,7 @@ def get_pitch(x,*data): Region2andAhalf = False omega_array = np.linspace(omega_cut_in,omega_max,21)*np.pi/30 # rad/s - Q = (0.5*air_density*omega_array**2*R**5 * np.pi * max_cp ) / tsr_opt**3 + Q = (0.5*air_density*omega_array**2*R**5 * np.pi * max_cp ) / tsr_opt**3 Paero_array = Q*omega_array @@ -494,84 +587,95 @@ def get_pitch(x,*data): Region2andAhalf = True Q_extra = rated_power_aero/(omega_max*np.pi/30) Q = np.append(Q,Q_extra) - u_r2_end = (Paero_array[-1]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3); + (Paero_array[-1]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) omega_array = np.append(omega_array,omega_array[-1]) Paero_array = np.append(Paero_array,rated_power_aero) else: # limit aero_power to the last Q*omega_max rated_power_aero = Paero_array[-1] - u_rated = (rated_power_aero/(0.5*air_density*np.pi*R**2*max_cp))**(1/3); + u_rated = (rated_power_aero/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) u_array = np.linspace(3,25,45) idx = np.argmin(np.abs(u_array-u_rated)) if u_rated > u_array[idx]: u_array = np.insert(u_array,idx+1,u_rated) else: u_array = np.insert(u_array,idx,u_rated) - - pow_lut_omega = Paero_array; - omega_lut_pow = omega_array*30/np.pi; - torque_lut_omega = Q; - omega_lut_torque = omega_lut_pow; - - num_rows, num_cols = tilt_angles.shape + + pow_lut_omega = Paero_array + omega_lut_pow = omega_array*30/np.pi + torque_lut_omega = Q + omega_lut_torque = omega_lut_pow + + num_rows, num_cols = tilt_angles.shape omega_rated = np.zeros_like(rotor_average_velocities) u_rated = np.zeros_like(rotor_average_velocities) for i in np.arange(num_rows): - for j in np.arange(num_cols): - omega_rated[i,j] = np.interp(power_demanded[i,j],pow_lut_omega,omega_lut_pow)*np.pi/30; #rad/s - u_rated[i,j] = (power_demanded[i,j]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3); - + for j in np.arange(num_cols): + omega_rated[i,j] = ( + np.interp(power_demanded[i,j],pow_lut_omega,omega_lut_pow) + *np.pi/30 #rad/s + ) + u_rated[i,j] = (power_demanded[i,j]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) + pitch_out = np.zeros_like(rotor_average_velocities) - tsr_out = np.zeros_like(rotor_average_velocities) - + tsr_out = np.zeros_like(rotor_average_velocities) + for i in np.arange(num_rows): yaw = yaw_angles[i,:] tilt = tilt_angles[i,:] k = shear[i,:] - for j in np.arange(num_cols): + for j in np.arange(num_cols): u_v = rotor_average_velocities[i,j] if u_v > u_rated[i,j]: - tsr_v = omega_rated[i,j]*R/u_v*np.cos(np.deg2rad(yaw[j]))**0.5; + tsr_v = omega_rated[i,j]*R/u_v*np.cos(np.deg2rad(yaw[j]))**0.5 else: - tsr_v = tsr_opt*np.cos(np.deg2rad(yaw[j])); + tsr_v = tsr_opt*np.cos(np.deg2rad(yaw[j])) if Region2andAhalf: # fix for interpolation - omega_lut_torque[-1] = omega_lut_torque[-1]+1e-2; - omega_lut_pow[-1] = omega_lut_pow[-1]+1e-2; - - data = air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v,pitch_opt,omega_lut_pow,torque_lut_omega,cp_i,pitch_i,tsr_i - [tsr_out_soluzione,infodict,ier,mesg] = fsolve(get_tsr,tsr_v,args=data,full_output=True) + omega_lut_torque[-1] = omega_lut_torque[-1]+1e-2 + omega_lut_pow[-1] = omega_lut_pow[-1]+1e-2 + + data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v,pitch_opt, + omega_lut_pow,torque_lut_omega,cp_i,pitch_i,tsr_i) + [tsr_out_soluzione,infodict,ier,mesg] = fsolve( + get_tsr,tsr_v,args=data,full_output=True + ) # check if solution was possible. If not, we are in region 3 if (np.abs(infodict['fvec']) > 10 or tsr_out_soluzione < 4): - tsr_out_soluzione = 1000; - + tsr_out_soluzione = 1000 + # save solution - tsr_outO = tsr_out_soluzione; - omega = tsr_outO*u_v/R; - - # check if we are in region 2 or 3 + tsr_outO = tsr_out_soluzione + omega = tsr_outO*u_v/R + + # check if we are in region 2 or 3 if omega < omega_rated[i,j]: # region 2 # Define optimum pitch - pitch_out0 = pitch_opt; - + pitch_out0 = pitch_opt + else: # region 3 - tsr_outO = omega_rated[i,j]*R/u_v; - data = air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v,omega_rated[i,j],omega_array,Q,cp_i,pitch_i,tsr_i + tsr_outO = omega_rated[i,j]*R/u_v + data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v, + omega_rated[i,j],omega_array,Q,cp_i,pitch_i,tsr_i) # if omega_rated[i,j]*R/u_v > 4.25: # solve aero-electrical power balance with TSR from rated omega - [pitch_out_soluzione,infodict,ier,mesg] = fsolve(get_pitch,8,args=data,factor=0.1,full_output=True) + [pitch_out_soluzione,infodict,ier,mesg] = fsolve( + get_pitch,8,args=data,factor=0.1,full_output=True + ) if pitch_out_soluzione < pitch_opt: pitch_out_soluzione = pitch_opt - pitch_out0 = pitch_out_soluzione; + pitch_out0 = pitch_out_soluzione # else: # cp_needed = power_demanded[i,j]/(0.5*air_density*np.pi*R**2*u_v**3) - # pitch_out0 = np.interp(cp_needed,np.flip(cp_i[4,20::]),np.flip(pitch_i[20::])) + # pitch_out0 = np.interp( + # cp_needed,np.flip(cp_i[4,20::]),np.flip(pitch_i[20::]) + # ) #%% COMPUTE CP AND CT GIVEN THE PITCH AND TSR FOUND ABOVE pitch_out[i,j] = pitch_out0 tsr_out[i,j] = tsr_outO - + return pitch_out, tsr_out - + def power( power_thrust_table: dict, velocities: NDArrayFloat, @@ -592,10 +696,10 @@ def power( # fill_value=0.0, # bounds_error=False, # ) - + # sign convention. in the tum model, negative tilt creates tower clearance tilt_angles = -tilt_angles - + # Compute the power-effective wind speed across the rotor rotor_average_velocities = average_velocity( velocities=velocities, @@ -617,64 +721,83 @@ def get_ct(x,*data): SD = np.sin(np.deg2rad(delta)) SG = np.sin(np.deg2rad(gamma)) a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)); - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)) + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + 8*tsr**2))/12)/(2*np.pi) return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x - + def computeP(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) SG = np.sin(np.deg2rad(gamma)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - CD = np.cos(np.deg2rad(delta)) - k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(ct/2))/2)); - - p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 - - (tsr*cd*np.pi*(CD**2*CG**2*SD**2*k**2 + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + 8*tsr**2))/16 - - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 - + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 - + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 - + (CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2))/(4*sinMu**2) - - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 - + (CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi*(3*CG**2*SD**2 + SG**2))/(24*sinMu**2) - - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) - - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu))/(2*np.pi)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + CD = np.cos(np.deg2rad(delta)) + k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(ct/2))/2)) + + p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 + - (tsr*cd*np.pi*(CD**2*CG**2*SD**2*k**2 + 3*CD**2*SG**2*k**2 + - 8*CD*tsr*SG*k + 8*tsr**2))/16 + - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 + + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 + + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 + + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 + + ((CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2)) + /(4*sinMu**2)) + - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 + + ((CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi*(3*CG**2*SD**2 + + SG**2)) + /(24*sinMu**2)) + - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) + - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu) + )/(2*np.pi) + ) return p - - num_rows, num_cols = tilt_angles.shape - u = (average_velocity(velocities)) - shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) - + num_rows, num_cols = tilt_angles.shape + (average_velocity(velocities)) + + shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + beta = power_thrust_table["beta"] cd = power_thrust_table["cd"] cl_alfa = power_thrust_table["cl_alfa"] - + sigma = power_thrust_table["rotor_solidity"] R = power_thrust_table["rotor_diameter"]/2 air_density = power_thrust_table["ref_air_density"] - pitch_out, tsr_out = TUMLossTurbine.control_trajectory(rotor_average_velocities,yaw_angles,tilt_angles, - air_density,R,shear,sigma,cd,cl_alfa,beta,power_setpoints,power_thrust_table) + pitch_out, tsr_out = TUMLossTurbine.control_trajectory( + rotor_average_velocities, + yaw_angles, + tilt_angles, + air_density, + R, + shear, + sigma, + cd, + cl_alfa, + beta, + power_setpoints, + power_thrust_table + ) MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) cosMu = (np.cos(MU)) sinMu = (np.sin(MU)) - tsr_array = (tsr_out); + tsr_array = (tsr_out) theta_array = (np.deg2rad(pitch_out+beta)) - + x0 = 0.2 - + p = np.zeros_like(average_velocity(velocities)) - + for i in np.arange(num_rows): yaw = yaw_angles[i,:] tilt = tilt_angles[i,:] @@ -683,22 +806,50 @@ def computeP(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): sMu = sinMu[i,:] Mu = MU[i,:] for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j]) - ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) - if ier == 1: - p[i,j] = computeP(sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j],ct) + # Break below command over multiple lines + data = ( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + k[j], + cMu[j], + sMu[j], + (tsr_array[i,j]), + (theta_array[i,j]), + R, + Mu[j] + ) + ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) + if ier == 1: + p[i,j] = computeP( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + k[j], + cMu[j], + sMu[j], + (tsr_array[i,j]), + (theta_array[i,j]), + R, + Mu[j], + ct + ) else: p[i,j] = -1e3 - + ############################################################################ - + yaw_angles = np.zeros_like(yaw_angles) MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) cosMu = (np.cos(MU)) sinMu = (np.sin(MU)) - + p0 = np.zeros_like((average_velocity(velocities))) - + for i in np.arange(num_rows): yaw = yaw_angles[i,:] tilt = tilt_angles[i,:] @@ -707,34 +858,57 @@ def computeP(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): sMu = sinMu[i,:] Mu = MU[i,:] for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j]) - ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) - if ier == 1: - p0[i,j] = computeP(sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j],ct) + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),R,Mu[j]) + ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) + if ier == 1: + p0[i,j] = computeP( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + k[j], + cMu[j], + sMu[j], + (tsr_array[i,j]), + (theta_array[i,j]), + R, + Mu[j], + ct + ) else: p0[i,j] = -1e3 - + razio = p/p0 - + ############################################################################ - + pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] lut_file = pkgroot / "turbine_library" / "LUT_IEA3MW.npz" LUT = np.load(lut_file) cp_i = LUT['cp_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator((tsr_i,pitch_i), cp_i,bounds_error=False, fill_value=None) - - power_coefficient = np.zeros_like(average_velocity(velocities)) + interp_lut = RegularGridInterpolator( + (tsr_i,pitch_i), + cp_i, + bounds_error=False, + fill_value=None + ) + + power_coefficient = np.zeros_like(average_velocity(velocities)) for i in np.arange(num_rows): for j in np.arange(num_cols): cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') power_coefficient[i,j] = cp_interp*razio[i,j] - + print('Tip speed ratio' + str(tsr_array)) print('Pitch out: ' + str(pitch_out)) - power = 0.5*air_density*(rotor_effective_velocities)**3*np.pi*R**2*(power_coefficient)*power_thrust_table["generator_efficiency"] + power = ( + 0.5*air_density*(rotor_effective_velocities)**3*np.pi*R**2 + *(power_coefficient)*power_thrust_table["generator_efficiency"] + ) return power def thrust_coefficient( @@ -750,9 +924,9 @@ def thrust_coefficient( **_ # <- Allows other models to accept other keyword arguments ): - # sign convention. in the tum model, negative tilt creates tower clearance + # sign convention. in the tum model, negative tilt creates tower clearance tilt_angles = -tilt_angles - + # Compute the effective wind speed across the rotor rotor_average_velocities = average_velocity( velocities=velocities, @@ -771,7 +945,7 @@ def thrust_coefficient( ) # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) - + def get_ct(x,*data): sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data CD = np.cos(np.deg2rad(delta)) @@ -779,11 +953,11 @@ def get_ct(x,*data): SD = np.sin(np.deg2rad(delta)) SG = np.sin(np.deg2rad(gamma)) a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)); - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)) + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + 8*tsr**2))/12)/(2*np.pi) return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x @@ -791,28 +965,40 @@ def get_ct(x,*data): beta = power_thrust_table["beta"] cd = power_thrust_table["cd"] cl_alfa = power_thrust_table["cl_alfa"] - + sigma = power_thrust_table["rotor_solidity"] R = power_thrust_table["rotor_diameter"]/2 - - shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + + shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) air_density = power_thrust_table["ref_air_density"] # CHANGE - pitch_out, tsr_out = TUMLossTurbine.control_trajectory(rotor_average_velocities,yaw_angles,tilt_angles, - air_density,R,shear,sigma,cd,cl_alfa,beta,power_setpoints,power_thrust_table) - - num_rows, num_cols = tilt_angles.shape + pitch_out, tsr_out = TUMLossTurbine.control_trajectory( + rotor_average_velocities, + yaw_angles, + tilt_angles, + air_density, + R, + shear, + sigma, + cd, + cl_alfa, + beta, + power_setpoints, + power_thrust_table + ) + + num_rows, num_cols = tilt_angles.shape - u = (average_velocity(velocities)) + (average_velocity(velocities)) MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) cosMu = (np.cos(MU)) sinMu = (np.sin(MU)) # u = np.squeeze(u) theta_array = (np.deg2rad(pitch_out+beta)) tsr_array = (tsr_out) - + x0 = 0.2 - + thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) for i in np.arange(num_rows): yaw = yaw_angles[i,:] @@ -821,18 +1007,19 @@ def get_ct(x,*data): sMu = sinMu[i,:] Mu = MU[i,:] for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j]) - ct = fsolve(get_ct, x0,args=data) + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),R,Mu[j]) + ct = fsolve(get_ct, x0,args=data) thrust_coefficient1[i,j] = np.clip(ct, 0.0001, 0.9999) - + yaw_angles = np.zeros_like(yaw_angles) MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) cosMu = (np.cos(MU)) sinMu = (np.sin(MU)) thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) - + for i in np.arange(num_rows): yaw = yaw_angles[i,:] tilt = tilt_angles[i,:] @@ -840,12 +1027,13 @@ def get_ct(x,*data): sMu = sinMu[i,:] Mu = MU[i,:] for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j]) - ct = fsolve(get_ct, x0,args=data) + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),R,Mu[j]) + ct = fsolve(get_ct, x0,args=data) thrust_coefficient0[i,j] = ct #np.clip(ct, 0.0001, 0.9999) - ############################################################################ - + ############################################################################ + razio = thrust_coefficient1/thrust_coefficient0 pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] @@ -854,18 +1042,23 @@ def get_ct(x,*data): ct_i = LUT['ct_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator((tsr_i,pitch_i), ct_i,bounds_error=False, fill_value=None)#*0.9722085500886761) - - + interp_lut = RegularGridInterpolator( + (tsr_i,pitch_i), + ct_i, + bounds_error=False, + fill_value=None + )#*0.9722085500886761) + + thrust_coefficient = np.zeros_like(average_velocity(velocities)) - + for i in np.arange(num_rows): for j in np.arange(num_cols): ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') thrust_coefficient[i,j] = ct_interp*razio[i,j] return thrust_coefficient - + def axial_induction( power_thrust_table: dict, velocities: NDArrayFloat, @@ -881,7 +1074,7 @@ def axial_induction( # sign convention. in the tum model, negative tilt creates tower clearance tilt_angles = -tilt_angles - + # Compute the effective wind speed across the rotor rotor_average_velocities = average_velocity( velocities=velocities, @@ -900,7 +1093,7 @@ def axial_induction( ) # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) - + def get_ct(x,*data): sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data CD = np.cos(np.deg2rad(delta)) @@ -908,11 +1101,11 @@ def get_ct(x,*data): SD = np.sin(np.deg2rad(delta)) SG = np.sin(np.deg2rad(gamma)) a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)); - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)) + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + 8*tsr**2))/12)/(2*np.pi) return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x @@ -920,28 +1113,40 @@ def get_ct(x,*data): beta = power_thrust_table["beta"] cd = power_thrust_table["cd"] cl_alfa = power_thrust_table["cl_alfa"] - + sigma = power_thrust_table["rotor_solidity"] R = power_thrust_table["rotor_diameter"]/2 - - shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + + shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) air_density = power_thrust_table["ref_air_density"] # CHANGE - pitch_out, tsr_out = TUMLossTurbine.control_trajectory(rotor_average_velocities,yaw_angles,tilt_angles, - air_density,R,shear,sigma,cd,cl_alfa,beta,power_setpoints,power_thrust_table) - - num_rows, num_cols = tilt_angles.shape + pitch_out, tsr_out = TUMLossTurbine.control_trajectory( + rotor_average_velocities, + yaw_angles, + tilt_angles, + air_density, + R, + shear, + sigma, + cd, + cl_alfa, + beta, + power_setpoints, + power_thrust_table + ) + + num_rows, num_cols = tilt_angles.shape - u = (average_velocity(velocities)) + (average_velocity(velocities)) MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) cosMu = (np.cos(MU)) sinMu = (np.sin(MU)) # u = np.squeeze(u) theta_array = (np.deg2rad(pitch_out+beta)) tsr_array = (tsr_out) - + x0 = 0.2 - + thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) for i in np.arange(num_rows): yaw = yaw_angles[i,:] @@ -950,18 +1155,19 @@ def get_ct(x,*data): sMu = sinMu[i,:] Mu = MU[i,:] for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j]) - ct = fsolve(get_ct, x0,args=data) + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),R,Mu[j]) + ct = fsolve(get_ct, x0,args=data) thrust_coefficient1[i,j] = np.clip(ct, 0.0001, 0.9999) - + yaw_angles = np.zeros_like(yaw_angles) MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) cosMu = (np.cos(MU)) sinMu = (np.sin(MU)) thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) - + for i in np.arange(num_rows): yaw = yaw_angles[i,:] tilt = tilt_angles[i,:] @@ -969,12 +1175,13 @@ def get_ct(x,*data): sMu = sinMu[i,:] Mu = MU[i,:] for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]),(theta_array[i,j]),R,Mu[j]) - ct = fsolve(get_ct, x0,args=data) + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),R,Mu[j]) + ct = fsolve(get_ct, x0,args=data) thrust_coefficient0[i,j] = ct #np.clip(ct, 0.0001, 0.9999) - ############################################################################ - + ############################################################################ + razio = thrust_coefficient1/thrust_coefficient0 pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] @@ -983,17 +1190,22 @@ def get_ct(x,*data): ct_i = LUT['ct_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator((tsr_i,pitch_i), ct_i,bounds_error=False, fill_value=None)#*0.9722085500886761) - + interp_lut = RegularGridInterpolator( + (tsr_i,pitch_i), + ct_i, + bounds_error=False, + fill_value=None + )#*0.9722085500886761) + axial_induction = np.zeros_like(average_velocity(velocities)) - + for i in np.arange(num_rows): for j in np.arange(num_cols): ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') ct = ct_interp*razio[i,j] a = (1- ( (1+np.sqrt(1-ct-1/16*ct**2*sMu[j]**2))/(2*(1+1/16*ct*sMu[j]**2))) ) axial_induction[i,j] = np.clip(a, 0.0001, 0.9999) - + return axial_induction From 3efd3fe75d9a42b0ab32fb8394a896043fa5b1c6 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 6 Mar 2024 11:49:57 -0700 Subject: [PATCH 19/53] Remove assertions on tests; tests will still need to be built. --- floris/simulation/turbine/operation_models.py | 4 ++-- ...urbine_operation_models_integration_test.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index f9d96b6b5..24cfa99e0 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -903,8 +903,8 @@ def computeP(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') power_coefficient[i,j] = cp_interp*razio[i,j] - print('Tip speed ratio' + str(tsr_array)) - print('Pitch out: ' + str(pitch_out)) + #print('Tip speed ratio' + str(tsr_array)) + #print('Pitch out: ' + str(pitch_out)) power = ( 0.5*air_density*(rotor_effective_velocities)**3*np.pi*R**2 *(power_coefficient)*power_thrust_table["generator_efficiency"] diff --git a/tests/turbine_operation_models_integration_test.py b/tests/turbine_operation_models_integration_test.py index f0421ac34..9101214a0 100644 --- a/tests/turbine_operation_models_integration_test.py +++ b/tests/turbine_operation_models_integration_test.py @@ -522,7 +522,7 @@ def test_TUMLossTurbine(): # Check that power works as expected - test_power = TUMLossTurbine.power( + TUMLossTurbine.power( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=turbine_data["power_thrust_table"]["ref_air_density"], # Matches ref_air_density @@ -536,7 +536,7 @@ def test_TUMLossTurbine(): # assert np.allclose(baseline_power, test_power) # Check that yaw and tilt angle have an effect - test_power = TUMLossTurbine.power( + TUMLossTurbine.power( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=turbine_data["power_thrust_table"]["ref_air_density"], # Matches ref_air_density @@ -548,7 +548,7 @@ def test_TUMLossTurbine(): #assert test_power < baseline_power # Check that a lower air density decreases power appropriately - test_power = TUMLossTurbine.power( + TUMLossTurbine.power( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, @@ -561,7 +561,7 @@ def test_TUMLossTurbine(): # Check that thrust coefficient works as expected - test_Ct = TUMLossTurbine.thrust_coefficient( + TUMLossTurbine.thrust_coefficient( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, # Unused @@ -574,7 +574,7 @@ def test_TUMLossTurbine(): #assert np.allclose(baseline_Ct, test_Ct) # Check that yaw and tilt angle have the expected effect - test_Ct = TUMLossTurbine.thrust_coefficient( + TUMLossTurbine.thrust_coefficient( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, # Unused @@ -588,7 +588,7 @@ def test_TUMLossTurbine(): # Check that thrust coefficient works as expected - test_ai = TUMLossTurbine.axial_induction( + TUMLossTurbine.axial_induction( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, # Unused @@ -597,7 +597,7 @@ def test_TUMLossTurbine(): tilt_angles=tilt_angles_nom, tilt_interp=None ) - baseline_misalignment_loss = ( + ( cosd(yaw_angles_nom) * cosd(tilt_angles_nom - turbine_data["power_thrust_table"]["ref_tilt"]) ) @@ -607,7 +607,7 @@ def test_TUMLossTurbine(): # assert np.allclose(baseline_ai, test_ai) # Check that yaw and tilt angle have the expected effect - test_ai = TUMLossTurbine.axial_induction( + TUMLossTurbine.axial_induction( power_thrust_table=turbine_data["power_thrust_table"], velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid air_density=1.1, # Unused @@ -616,5 +616,5 @@ def test_TUMLossTurbine(): tilt_angles=tilt_angles_test, tilt_interp=None ) - absolute_tilt = tilt_angles_test - turbine_data["power_thrust_table"]["ref_tilt"] + tilt_angles_test - turbine_data["power_thrust_table"]["ref_tilt"] #assert test_Ct == baseline_Ct * cosd(yaw_angles_test) * cosd(absolute_tilt) From 7ff142508ff8306e8cac79e198fa2ba75824d433 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 6 Mar 2024 12:56:53 -0700 Subject: [PATCH 20/53] Remove v3 generator_efficiency key (remains on power_thrust_table for use with TUMLossTurbine operation model). --- floris/turbine_library/iea_3MW.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/floris/turbine_library/iea_3MW.yaml b/floris/turbine_library/iea_3MW.yaml index 7ee22de7f..a19886181 100644 --- a/floris/turbine_library/iea_3MW.yaml +++ b/floris/turbine_library/iea_3MW.yaml @@ -9,10 +9,6 @@ # match the root name of the file. turbine_type: 'iea_3MW' -### -# Setting for generator losses to power. -generator_efficiency: 0.944 - ### # Hub height. hub_height: 110.0 From f9de335078b32b2ad0406fbe1069e03c1268d220 Mon Sep 17 00:00:00 2001 From: sTamaroTum Date: Thu, 7 Mar 2024 09:40:58 +0100 Subject: [PATCH 21/53] Added an if statement in get_ct() and find_cp() functions to add a small dummy misalignment in case both yaw and tilt are 0. This was causing a division by zero in the tests. --- floris/simulation/turbine/operation_models.py | 68 +++++++++++++------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index 24cfa99e0..10560e703 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -375,12 +375,17 @@ def control_trajectory( power_demanded = power_setpoints/power_thrust_table["generator_efficiency"] def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) SG = np.sin(np.deg2rad(gamma)) CG = np.cos(np.deg2rad(gamma)) SD = np.sin(np.deg2rad(delta)) CD = np.cos(np.deg2rad(delta)) - k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(ct/2))/2)) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(ct/2))/2)) p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 - (tsr*cd*np.pi*( @@ -408,12 +413,17 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): def get_ct(x,*data): sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU = data + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) CD = np.cos(np.deg2rad(delta)) CG = np.cos(np.deg2rad(gamma)) SD = np.sin(np.deg2rad(delta)) SG = np.sin(np.deg2rad(gamma)) a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 @@ -657,19 +667,13 @@ def get_pitch(x,*data): tsr_outO = omega_rated[i,j]*R/u_v data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v, omega_rated[i,j],omega_array,Q,cp_i,pitch_i,tsr_i) - # if omega_rated[i,j]*R/u_v > 4.25: - # solve aero-electrical power balance with TSR from rated omega + # solve aero-electrical power balance with TSR from rated omega [pitch_out_soluzione,infodict,ier,mesg] = fsolve( get_pitch,8,args=data,factor=0.1,full_output=True ) if pitch_out_soluzione < pitch_opt: pitch_out_soluzione = pitch_opt pitch_out0 = pitch_out_soluzione - # else: - # cp_needed = power_demanded[i,j]/(0.5*air_density*np.pi*R**2*u_v**3) - # pitch_out0 = np.interp( - # cp_needed,np.flip(cp_i[4,20::]),np.flip(pitch_i[20::]) - # ) #%% COMPUTE CP AND CT GIVEN THE PITCH AND TSR FOUND ABOVE pitch_out[i,j] = pitch_out0 tsr_out[i,j] = tsr_outO @@ -716,12 +720,17 @@ def power( # Compute power def get_ct(x,*data): sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) CD = np.cos(np.deg2rad(delta)) CG = np.cos(np.deg2rad(gamma)) SD = np.sin(np.deg2rad(delta)) SG = np.sin(np.deg2rad(gamma)) a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 @@ -730,13 +739,18 @@ def get_ct(x,*data): return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x - def computeP(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): + def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) SG = np.sin(np.deg2rad(gamma)) CG = np.cos(np.deg2rad(gamma)) SD = np.sin(np.deg2rad(delta)) CD = np.cos(np.deg2rad(delta)) - k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(ct/2))/2)) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(ct/2))/2)) p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 - (tsr*cd*np.pi*(CD**2*CG**2*SD**2*k**2 + 3*CD**2*SG**2*k**2 @@ -823,7 +837,7 @@ def computeP(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): ) ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) if ier == 1: - p[i,j] = computeP( + p[i,j] = find_cp( sigma, cd, cl_alfa, @@ -862,7 +876,7 @@ def computeP(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): (theta_array[i,j]),R,Mu[j]) ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) if ier == 1: - p0[i,j] = computeP( + p0[i,j] = find_cp( sigma, cd, cl_alfa, @@ -880,7 +894,7 @@ def computeP(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): else: p0[i,j] = -1e3 - razio = p/p0 + ratio = p/p0 ############################################################################ @@ -901,7 +915,7 @@ def computeP(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): for i in np.arange(num_rows): for j in np.arange(num_cols): cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - power_coefficient[i,j] = cp_interp*razio[i,j] + power_coefficient[i,j] = cp_interp*ratio[i,j] #print('Tip speed ratio' + str(tsr_array)) #print('Pitch out: ' + str(pitch_out)) @@ -948,12 +962,17 @@ def thrust_coefficient( def get_ct(x,*data): sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) CD = np.cos(np.deg2rad(delta)) CG = np.cos(np.deg2rad(gamma)) SD = np.sin(np.deg2rad(delta)) SG = np.sin(np.deg2rad(gamma)) a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 @@ -1034,7 +1053,7 @@ def get_ct(x,*data): ############################################################################ - razio = thrust_coefficient1/thrust_coefficient0 + ratio = thrust_coefficient1/thrust_coefficient0 pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] lut_file = pkgroot / "turbine_library" / "LUT_IEA3MW.npz" @@ -1055,7 +1074,7 @@ def get_ct(x,*data): for i in np.arange(num_rows): for j in np.arange(num_cols): ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - thrust_coefficient[i,j] = ct_interp*razio[i,j] + thrust_coefficient[i,j] = ct_interp*ratio[i,j] return thrust_coefficient @@ -1096,12 +1115,17 @@ def axial_induction( def get_ct(x,*data): sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) CD = np.cos(np.deg2rad(delta)) CG = np.cos(np.deg2rad(gamma)) SD = np.sin(np.deg2rad(delta)) SG = np.sin(np.deg2rad(gamma)) a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+np.sin(MU)*(x/2))/2)) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 @@ -1182,7 +1206,7 @@ def get_ct(x,*data): ############################################################################ - razio = thrust_coefficient1/thrust_coefficient0 + ratio = thrust_coefficient1/thrust_coefficient0 pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] lut_file = pkgroot / "turbine_library" / "LUT_IEA3MW.npz" @@ -1202,7 +1226,7 @@ def get_ct(x,*data): for i in np.arange(num_rows): for j in np.arange(num_cols): ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - ct = ct_interp*razio[i,j] + ct = ct_interp*ratio[i,j] a = (1- ( (1+np.sqrt(1-ct-1/16*ct**2*sMu[j]**2))/(2*(1+1/16*ct*sMu[j]**2))) ) axial_induction[i,j] = np.clip(a, 0.0001, 0.9999) From e1085222f6e0d533421164d098ae5ebdad25ccd4 Mon Sep 17 00:00:00 2001 From: sTamaroTum Date: Thu, 7 Mar 2024 16:12:41 +0100 Subject: [PATCH 22/53] Replaced rotor_average_velocities with rotor_effective_velocities when calling TUMLossTurbine.control_trajectory() so that rated power stays independent from air_density --- floris/simulation/turbine/operation_models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index 10560e703..7d7cbe9bd 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -788,7 +788,7 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): air_density = power_thrust_table["ref_air_density"] pitch_out, tsr_out = TUMLossTurbine.control_trajectory( - rotor_average_velocities, + rotor_effective_velocities, yaw_angles, tilt_angles, air_density, @@ -917,8 +917,8 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') power_coefficient[i,j] = cp_interp*ratio[i,j] - #print('Tip speed ratio' + str(tsr_array)) - #print('Pitch out: ' + str(pitch_out)) + # print('Tip speed ratio' + str(tsr_array)) + # print('Pitch out: ' + str(pitch_out)) power = ( 0.5*air_density*(rotor_effective_velocities)**3*np.pi*R**2 *(power_coefficient)*power_thrust_table["generator_efficiency"] From 84e0a5d742c7370f49d0899ec3d8a9846bc82530 Mon Sep 17 00:00:00 2001 From: sTamaroTum Date: Thu, 7 Mar 2024 16:31:38 +0100 Subject: [PATCH 23/53] Added some np.squeeze() to suppress warnings arising during pytest --- floris/simulation/turbine/operation_models.py | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index 7d7cbe9bd..17da3f25e 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -443,9 +443,6 @@ def get_tsr(x,*data): omega = x*u/R omega_rpm = omega*30/np.pi - pitch_in = pitch_in - pitch_deg = pitch_in - torque_nm = np.interp(omega,omega_lut_torque,torque_lut_omega) mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) @@ -494,7 +491,7 @@ def get_tsr(x,*data): np.squeeze((pitch_i))), cp_i, bounds_error=False, fill_value=None) - Cp_now = interp((x,pitch_deg)) + Cp_now = interp((x,pitch_in)) cp_g1 = Cp_now*eta_p aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 electric_pow = torque_nm*(omega_rpm*np.pi/30) @@ -582,7 +579,7 @@ def get_pitch(x,*data): pitch_opt = pitch_i[idx[1]] max_cp = cp_i[idx[0],idx[1]] - omega_cut_in = 1 # RPM + omega_cut_in = 0 # RPM omega_max = 11.75 # RPM rated_power_aero = 3.37e6/0.936 # MW #%% Compute torque-rpm relation and check for region 2-and-a-half @@ -675,7 +672,7 @@ def get_pitch(x,*data): pitch_out_soluzione = pitch_opt pitch_out0 = pitch_out_soluzione #%% COMPUTE CP AND CT GIVEN THE PITCH AND TSR FOUND ABOVE - pitch_out[i,j] = pitch_out0 + pitch_out[i,j] = np.squeeze(pitch_out0) tsr_out[i,j] = tsr_outO return pitch_out, tsr_out @@ -837,7 +834,7 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): ) ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) if ier == 1: - p[i,j] = find_cp( + p[i,j] = np.squeeze(find_cp( sigma, cd, cl_alfa, @@ -851,7 +848,7 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): R, Mu[j], ct - ) + )) else: p[i,j] = -1e3 @@ -876,7 +873,7 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): (theta_array[i,j]),R,Mu[j]) ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) if ier == 1: - p0[i,j] = find_cp( + p0[i,j] = np.squeeze(find_cp( sigma, cd, cl_alfa, @@ -890,7 +887,7 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): R, Mu[j], ct - ) + )) else: p0[i,j] = -1e3 @@ -915,7 +912,7 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): for i in np.arange(num_rows): for j in np.arange(num_cols): cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - power_coefficient[i,j] = cp_interp*ratio[i,j] + power_coefficient[i,j] = np.squeeze(cp_interp*ratio[i,j]) # print('Tip speed ratio' + str(tsr_array)) # print('Pitch out: ' + str(pitch_out)) @@ -1029,7 +1026,7 @@ def get_ct(x,*data): data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), (theta_array[i,j]),R,Mu[j]) ct = fsolve(get_ct, x0,args=data) - thrust_coefficient1[i,j] = np.clip(ct, 0.0001, 0.9999) + thrust_coefficient1[i,j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) yaw_angles = np.zeros_like(yaw_angles) @@ -1049,7 +1046,7 @@ def get_ct(x,*data): data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), (theta_array[i,j]),R,Mu[j]) ct = fsolve(get_ct, x0,args=data) - thrust_coefficient0[i,j] = ct #np.clip(ct, 0.0001, 0.9999) + thrust_coefficient0[i,j] = np.squeeze(ct) #np.clip(ct, 0.0001, 0.9999) ############################################################################ @@ -1074,7 +1071,7 @@ def get_ct(x,*data): for i in np.arange(num_rows): for j in np.arange(num_cols): ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - thrust_coefficient[i,j] = ct_interp*ratio[i,j] + thrust_coefficient[i,j] = np.squeeze(ct_interp*ratio[i,j]) return thrust_coefficient @@ -1182,7 +1179,7 @@ def get_ct(x,*data): data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), (theta_array[i,j]),R,Mu[j]) ct = fsolve(get_ct, x0,args=data) - thrust_coefficient1[i,j] = np.clip(ct, 0.0001, 0.9999) + thrust_coefficient1[i,j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) yaw_angles = np.zeros_like(yaw_angles) @@ -1202,7 +1199,7 @@ def get_ct(x,*data): data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), (theta_array[i,j]),R,Mu[j]) ct = fsolve(get_ct, x0,args=data) - thrust_coefficient0[i,j] = ct #np.clip(ct, 0.0001, 0.9999) + thrust_coefficient0[i,j] = np.squeeze(ct) #np.clip(ct, 0.0001, 0.9999) ############################################################################ @@ -1228,7 +1225,7 @@ def get_ct(x,*data): ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') ct = ct_interp*ratio[i,j] a = (1- ( (1+np.sqrt(1-ct-1/16*ct**2*sMu[j]**2))/(2*(1+1/16*ct*sMu[j]**2))) ) - axial_induction[i,j] = np.clip(a, 0.0001, 0.9999) + axial_induction[i,j] = np.squeeze(np.clip(a, 0.0001, 0.9999)) return axial_induction From 39b043383d0b441a4cd615ecb88e9150be27d230 Mon Sep 17 00:00:00 2001 From: sTamaroTum Date: Thu, 7 Mar 2024 18:52:48 +0100 Subject: [PATCH 24/53] Corrected incoherent interpolation method (cubic) in get_pitch() and get_tsr() --- floris/simulation/turbine/operation_models.py | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index 17da3f25e..2f406cb6b 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -491,7 +491,7 @@ def get_tsr(x,*data): np.squeeze((pitch_i))), cp_i, bounds_error=False, fill_value=None) - Cp_now = interp((x,pitch_in)) + Cp_now = interp((x,pitch_in),method='cubic') cp_g1 = Cp_now*eta_p aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 electric_pow = torque_nm*(omega_rpm*np.pi/30) @@ -559,7 +559,7 @@ def get_pitch(x,*data): fill_value=None ) - Cp_now = interp((tsr,x)) + Cp_now = interp((tsr,x),method='cubic') cp_g1 = Cp_now*eta_p aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 electric_pow = torque_nm*(omega_rpm*np.pi/30) @@ -585,7 +585,7 @@ def get_pitch(x,*data): #%% Compute torque-rpm relation and check for region 2-and-a-half Region2andAhalf = False - omega_array = np.linspace(omega_cut_in,omega_max,21)*np.pi/30 # rad/s + omega_array = np.linspace(omega_cut_in,omega_max,161)*np.pi/30 # rad/s Q = (0.5*air_density*omega_array**2*R**5 * np.pi * max_cp ) / tsr_opt**3 Paero_array = Q*omega_array @@ -666,8 +666,8 @@ def get_pitch(x,*data): omega_rated[i,j],omega_array,Q,cp_i,pitch_i,tsr_i) # solve aero-electrical power balance with TSR from rated omega [pitch_out_soluzione,infodict,ier,mesg] = fsolve( - get_pitch,8,args=data,factor=0.1,full_output=True - ) + get_pitch,u_v,args=data,factor=0.1,full_output=True, + xtol=1e-10,maxfev=2000) if pitch_out_soluzione < pitch_opt: pitch_out_soluzione = pitch_opt pitch_out0 = pitch_out_soluzione @@ -988,8 +988,15 @@ def get_ct(x,*data): shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) air_density = power_thrust_table["ref_air_density"] # CHANGE + + rotor_effective_velocities = rotor_velocity_air_density_correction( + velocities=rotor_average_velocities, + air_density=air_density, + ref_air_density=power_thrust_table["ref_air_density"] + ) + pitch_out, tsr_out = TUMLossTurbine.control_trajectory( - rotor_average_velocities, + rotor_effective_velocities, yaw_angles, tilt_angles, air_density, @@ -1141,8 +1148,15 @@ def get_ct(x,*data): shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) air_density = power_thrust_table["ref_air_density"] # CHANGE + + rotor_effective_velocities = rotor_velocity_air_density_correction( + velocities=rotor_average_velocities, + air_density=air_density, + ref_air_density=power_thrust_table["ref_air_density"] + ) + pitch_out, tsr_out = TUMLossTurbine.control_trajectory( - rotor_average_velocities, + rotor_effective_velocities, yaw_angles, tilt_angles, air_density, From f275e851a3b5b5a0c1ed5d93f9c31dd07c3b8d68 Mon Sep 17 00:00:00 2001 From: sTamaroTum Date: Fri, 8 Mar 2024 15:23:07 +0100 Subject: [PATCH 25/53] Testing the IEA 15 MW with the tum-loss model --- floris/simulation/turbine/operation_models.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index 2f406cb6b..ab9a1e17e 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -321,7 +321,7 @@ class TUMLossTurbine(BaseOperationModel): The method requires C_P, C_T look-up tables as functions of tip speed ratio and blade pitch angle, available here: - "../floris/turbine_library/LUT_IEA3MW.npz" for the IEA 3.4 MW (Bortolotti et al., 2019) + "../floris/turbine_library/LUT_iea15MW.npz" for the IEA 3.4 MW (Bortolotti et al., 2019) As with all turbine submodules, implements only static power() and thrust_coefficient() methods, which are called by power() and thrust_coefficient() on turbine.py, respectively. There are also two new functions, i.e. compute_local_vertical_shear() and control_trajectory(). @@ -568,7 +568,7 @@ def get_pitch(x,*data): return y pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_IEA3MW.npz" + lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" LUT = np.load(lut_file) cp_i = LUT['cp_lut'] pitch_i = LUT['pitch_lut'] @@ -580,8 +580,8 @@ def get_pitch(x,*data): max_cp = cp_i[idx[0],idx[1]] omega_cut_in = 0 # RPM - omega_max = 11.75 # RPM - rated_power_aero = 3.37e6/0.936 # MW + omega_max = power_thrust_table["rated_rpm"] # RPM + rated_power_aero = power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] # MW #%% Compute torque-rpm relation and check for region 2-and-a-half Region2andAhalf = False @@ -896,7 +896,7 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): ############################################################################ pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_IEA3MW.npz" + lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" LUT = np.load(lut_file) cp_i = LUT['cp_lut'] pitch_i = LUT['pitch_lut'] @@ -914,8 +914,8 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') power_coefficient[i,j] = np.squeeze(cp_interp*ratio[i,j]) - # print('Tip speed ratio' + str(tsr_array)) - # print('Pitch out: ' + str(pitch_out)) + print('Tip speed ratio' + str(tsr_array)) + print('Pitch out: ' + str(pitch_out)) power = ( 0.5*air_density*(rotor_effective_velocities)**3*np.pi*R**2 *(power_coefficient)*power_thrust_table["generator_efficiency"] @@ -1060,7 +1060,7 @@ def get_ct(x,*data): ratio = thrust_coefficient1/thrust_coefficient0 pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_IEA3MW.npz" + lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" LUT = np.load(lut_file) ct_i = LUT['ct_lut'] pitch_i = LUT['pitch_lut'] @@ -1220,7 +1220,7 @@ def get_ct(x,*data): ratio = thrust_coefficient1/thrust_coefficient0 pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_IEA3MW.npz" + lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" LUT = np.load(lut_file) ct_i = LUT['ct_lut'] pitch_i = LUT['pitch_lut'] From 91113665604742354e9aeb7d0726869ce6d36d11 Mon Sep 17 00:00:00 2001 From: sTamaroTum Date: Fri, 8 Mar 2024 15:24:02 +0100 Subject: [PATCH 26/53] Added the look-up tables of Cp and Ct for the IEAs 10MW and 15MW, and for the NREL 5MW, so that they can be run with the tum-loss model. The respective yaml files have been modified to include the necessary model parameters. --- floris/turbine_library/LUT_IEA3MW.npz | Bin 16902 -> 0 bytes floris/turbine_library/iea_10MW.yaml | 12 +++++++++++- floris/turbine_library/iea_15MW.yaml | 12 +++++++++++- floris/turbine_library/iea_3MW.yaml | 3 ++- floris/turbine_library/nrel_5MW.yaml | 1 + 5 files changed, 25 insertions(+), 3 deletions(-) delete mode 100644 floris/turbine_library/LUT_IEA3MW.npz diff --git a/floris/turbine_library/LUT_IEA3MW.npz b/floris/turbine_library/LUT_IEA3MW.npz deleted file mode 100644 index 6e43da4698201de996c4f2b2d16afd88a9e951c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16902 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 diff --git a/floris/turbine_library/iea_10MW.yaml b/floris/turbine_library/iea_10MW.yaml index 82aa899fa..10576b487 100644 --- a/floris/turbine_library/iea_10MW.yaml +++ b/floris/turbine_library/iea_10MW.yaml @@ -5,9 +5,19 @@ turbine_type: 'iea_10MW' hub_height: 119.0 rotor_diameter: 198.0 TSR: 8.0 -power_thrust_model: cosine-loss +power_thrust_model: 'tum-loss' power_thrust_table: ref_air_density: 1.225 + + rotor_solidity: 0.03500415472147307 + rated_rpm: 8.6 + generator_efficiency: 0.944 + rated_power: 10.0e6 + rotor_diameter: 198 + beta: -3.8233819218614817 + cd: 0.004612981322772105 + cl_alfa: 4.602140680380394 + ref_tilt: 6.0 cosine_loss_exponent_yaw: 1.88 cosine_loss_exponent_tilt: 1.88 diff --git a/floris/turbine_library/iea_15MW.yaml b/floris/turbine_library/iea_15MW.yaml index 456b40398..9da7607ba 100644 --- a/floris/turbine_library/iea_15MW.yaml +++ b/floris/turbine_library/iea_15MW.yaml @@ -7,9 +7,19 @@ turbine_type: 'iea_15MW' hub_height: 150.0 rotor_diameter: 242.24 TSR: 8.0 -power_thrust_model: cosine-loss +power_thrust_model: 'tum-loss' power_thrust_table: ref_air_density: 1.225 + + rotor_solidity: 0.031018237027995298 + rated_rpm: 7.55 + generator_efficiency: 0.95756 + rated_power: 15.0e6 + rotor_diameter: 242.24 + beta: -3.098605491003358 + cd: 0.004426686198054057 + cl_alfa: 4.546410770937916 + ref_tilt: 6.0 cosine_loss_exponent_yaw: 1.88 cosine_loss_exponent_tilt: 1.88 diff --git a/floris/turbine_library/iea_3MW.yaml b/floris/turbine_library/iea_3MW.yaml index a19886181..792019042 100644 --- a/floris/turbine_library/iea_3MW.yaml +++ b/floris/turbine_library/iea_3MW.yaml @@ -30,7 +30,8 @@ power_thrust_model: 'tum-loss' power_thrust_table: ### Power thrust table parameters # The air density at which the Cp and Ct curves are defined. - ref_air_density: 1.225 + ref_air_density: 1.12 + rated_rpm: 11.75 rotor_solidity: 0.0416 generator_efficiency: 0.925 rated_power: 3.3e6 diff --git a/floris/turbine_library/nrel_5MW.yaml b/floris/turbine_library/nrel_5MW.yaml index 97b518d8d..66284c3b4 100644 --- a/floris/turbine_library/nrel_5MW.yaml +++ b/floris/turbine_library/nrel_5MW.yaml @@ -32,6 +32,7 @@ power_thrust_table: ### Power thrust table parameters # The air density at which the Cp and Ct curves are defined. ref_air_density: 1.225 + rated_rpm: 12.1 rotor_solidity: 0.05132 generator_efficiency: 0.944 rated_power: 5.0e6 From 829e87f9c28ae1b318cf957d7b1c9b657e7c4752 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 08:55:57 -0700 Subject: [PATCH 27/53] Bring example up to v4. --- examples/41_compare_yaw_loss.py | 87 ------------------ .../001_compare_yaw_loss.py | 68 ++++++++++++++ floris/core/turbine/operation_models.py | 6 +- floris/turbine_library/LUT_iea15MW.npz | Bin 0 -> 16902 bytes 4 files changed, 72 insertions(+), 89 deletions(-) delete mode 100644 examples/41_compare_yaw_loss.py create mode 100644 examples/examples_operation_models/001_compare_yaw_loss.py create mode 100644 floris/turbine_library/LUT_iea15MW.npz diff --git a/examples/41_compare_yaw_loss.py b/examples/41_compare_yaw_loss.py deleted file mode 100644 index 869c411c4..000000000 --- a/examples/41_compare_yaw_loss.py +++ /dev/null @@ -1,87 +0,0 @@ -# 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 -import yaml - -from floris.tools import FlorisInterface - - -""" -Test alternative models of loss to yawing -""" - -# Parameters -N = 101 # How many steps to cover yaw range in -yaw_max = 30 # Maximum yaw to test - -# Set up the yaw angle sweep -yaw_angles = np.zeros((N,1)) -yaw_angles[:,0] = np.linspace(-yaw_max, yaw_max, N) -print(yaw_angles.shape) - - - -# Now loop over the operational models to compare -op_models = ["cosine-loss", "tum-loss"] -results = {} - -for op_model in op_models: - - print(f"Evaluating model: {op_model}") - - # Grab model of FLORIS - fi = FlorisInterface("inputs/gch.yaml") - - # Initialize to a simple 1 turbine case with n_findex = N - fi.set( - layout_x=[0], - layout_y=[0], - wind_directions=270 * np.ones(N), - wind_speeds=8 * np.ones(N), - ) - - with open(str( - fi.floris.as_dict()["farm"]["turbine_library_path"] / - (fi.floris.as_dict()["farm"]["turbine_type"][0] + ".yaml") - )) as t: - turbine_type = yaml.safe_load(t) - turbine_type["power_thrust_model"] = op_model - - # Change the turbine type - fi.set(turbine_type=[turbine_type], yaw_angles=yaw_angles) - - # Calculate the power - fi.run() - turbine_power = fi.get_turbine_powers().squeeze() - - # Save the results - results[op_model] = turbine_power - -# Plot the results -fig, ax = plt.subplots() - -colors = ["C0", "k", "r"] -linestyles = ["solid", "dashed", "dotted"] -for key, c, ls in zip(results, colors, linestyles): - central_power = results[key][yaw_angles.squeeze() == 0] - ax.plot(yaw_angles.squeeze(), results[key]/central_power, label=key, color=c, linestyle=ls) - -ax.grid(True) -ax.legend() -ax.set_xlabel("Yaw angle [deg]") -ax.set_ylabel("Normalized turbine power [deg]") - -plt.show() diff --git a/examples/examples_operation_models/001_compare_yaw_loss.py b/examples/examples_operation_models/001_compare_yaw_loss.py new file mode 100644 index 000000000..1d816fdfa --- /dev/null +++ b/examples/examples_operation_models/001_compare_yaw_loss.py @@ -0,0 +1,68 @@ +"""Example: Compare yaw loss +This example shows demonstrates how the TUM operation model alters how a turbine loses power to +yaw compared to the standard cosine loss model. +""" + +import matplotlib.pyplot as plt +import numpy as np +import yaml + +from floris import FlorisModel + + +""" +Test alternative models of loss to yawing +""" + +# Parameters +N = 101 # How many steps to cover yaw range in +yaw_max = 30 # Maximum yaw to test + +# Set up the yaw angle sweep +yaw_angles = np.zeros((N,1)) +yaw_angles[:,0] = np.linspace(-yaw_max, yaw_max, N) + +# Create the FLORIS model +fmodel = FlorisModel("../inputs/gch.yaml") + +# Initialize to a simple 1 turbine case with n_findex = N +fmodel.set( + layout_x=[0], + layout_y=[0], + wind_directions=270 * np.ones(N), + wind_speeds=8 * np.ones(N), + turbulence_intensities=0.06 * np.ones(N), + yaw_angles=yaw_angles, +) + +# Now loop over the operational models to compare +op_models = ["cosine-loss", "tum-loss"] +results = {} + +for op_model in op_models: + + print(f"Evaluating model: {op_model}") + fmodel.set_operation_model(op_model) + + # Calculate the power + fmodel.run() + turbine_power = fmodel.get_turbine_powers().squeeze() + + # Save the results + results[op_model] = turbine_power + +# Plot the results +fig, ax = plt.subplots() + +colors = ["C0", "k", "r"] +linestyles = ["solid", "dashed", "dotted"] +for key, c, ls in zip(results, colors, linestyles): + central_power = results[key][yaw_angles.squeeze() == 0] + ax.plot(yaw_angles.squeeze(), results[key]/central_power, label=key, color=c, linestyle=ls) + +ax.grid(True) +ax.legend() +ax.set_xlabel("Yaw angle [deg]") +ax.set_ylabel("Normalized turbine power [deg]") + +plt.show() diff --git a/floris/core/turbine/operation_models.py b/floris/core/turbine/operation_models.py index 96439a46e..f98faa2a0 100644 --- a/floris/core/turbine/operation_models.py +++ b/floris/core/turbine/operation_models.py @@ -905,8 +905,10 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') power_coefficient[i,j] = np.squeeze(cp_interp*ratio[i,j]) - print('Tip speed ratio' + str(tsr_array)) - print('Pitch out: ' + str(pitch_out)) + # TODO: make printout optional? + if False: + print('Tip speed ratio' + str(tsr_array)) + print('Pitch out: ' + str(pitch_out)) power = ( 0.5*air_density*(rotor_effective_velocities)**3*np.pi*R**2 *(power_coefficient)*power_thrust_table["generator_efficiency"] diff --git a/floris/turbine_library/LUT_iea15MW.npz b/floris/turbine_library/LUT_iea15MW.npz new file mode 100644 index 0000000000000000000000000000000000000000..6e43da4698201de996c4f2b2d16afd88a9e951c7 GIT binary patch literal 16902 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 literal 0 HcmV?d00001 From 0ced9d2fd0696e326892ff55477a3104159ae6e7 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 08:56:20 -0700 Subject: [PATCH 28/53] Set reference_wind_height explicitly to avoid warning. --- floris/floris_model.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index 09a5aa5d0..f87a2c8ce 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -1542,7 +1542,10 @@ def set_operation_model(self, operation_model: str | List[str]): # Set a single one here, then, and return turbine_type = self.core.farm.turbine_definitions[0] turbine_type["operation_model"] = operation_model - self.set(turbine_type=[turbine_type]) + self.set( + turbine_type=[turbine_type], + reference_wind_height=self.core.flow_field.reference_wind_height + ) return else: operation_model = [operation_model]*self.core.farm.n_turbines @@ -1561,7 +1564,10 @@ def set_operation_model(self, operation_model: str | List[str]): ) turbine_type_list[tindex]["operation_model"] = operation_model[tindex] - self.set(turbine_type=turbine_type_list) + self.set( + turbine_type=turbine_type_list, + reference_wind_height=self.core.flow_field.reference_wind_height + ) def copy(self): """Create an independent copy of the current FlorisModel object""" From 6081de79809e05e5bf8d66755a91778fb0132fcc Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 09:07:58 -0700 Subject: [PATCH 29/53] Formating updates. --- .../001_compare_yaw_loss.py | 14 ++---------- floris/core/turbine/operation_models.py | 22 ++++++++++--------- floris/turbine_library/iea_15MW.yaml | 2 +- floris/turbine_library/nrel_5MW.yaml | 2 +- tests/conftest.py | 3 ++- 5 files changed, 18 insertions(+), 25 deletions(-) diff --git a/examples/examples_operation_models/001_compare_yaw_loss.py b/examples/examples_operation_models/001_compare_yaw_loss.py index 1d816fdfa..6eac9fb56 100644 --- a/examples/examples_operation_models/001_compare_yaw_loss.py +++ b/examples/examples_operation_models/001_compare_yaw_loss.py @@ -5,15 +5,10 @@ import matplotlib.pyplot as plt import numpy as np -import yaml from floris import FlorisModel -""" -Test alternative models of loss to yawing -""" - # Parameters N = 101 # How many steps to cover yaw range in yaw_max = 30 # Maximum yaw to test @@ -35,7 +30,7 @@ yaw_angles=yaw_angles, ) -# Now loop over the operational models to compare +# Loop over the operational models to compare op_models = ["cosine-loss", "tum-loss"] results = {} @@ -44,16 +39,11 @@ print(f"Evaluating model: {op_model}") fmodel.set_operation_model(op_model) - # Calculate the power fmodel.run() - turbine_power = fmodel.get_turbine_powers().squeeze() - - # Save the results - results[op_model] = turbine_power + results[op_model] = fmodel.get_turbine_powers().squeeze() # Plot the results fig, ax = plt.subplots() - colors = ["C0", "k", "r"] linestyles = ["solid", "dashed", "dotted"] for key, c, ls in zip(results, colors, linestyles): diff --git a/floris/core/turbine/operation_models.py b/floris/core/turbine/operation_models.py index f98faa2a0..e5ec3ac6c 100644 --- a/floris/core/turbine/operation_models.py +++ b/floris/core/turbine/operation_models.py @@ -367,7 +367,7 @@ def control_trajectory( def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: + if MU == 0: MU = 1e-6 sinMu = np.sin(MU) cosMu = np.cos(MU) @@ -405,7 +405,7 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): def get_ct(x,*data): sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU = data #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: + if MU == 0: MU = 1e-6 sinMu = np.sin(MU) cosMu = np.cos(MU) @@ -571,8 +571,10 @@ def get_pitch(x,*data): max_cp = cp_i[idx[0],idx[1]] omega_cut_in = 0 # RPM - omega_max = power_thrust_table["rated_rpm"] # RPM - rated_power_aero = power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] # MW + omega_max = power_thrust_table["rated_rpm"] # RPM + rated_power_aero = ( + power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] # MW + ) #%% Compute torque-rpm relation and check for region 2-and-a-half Region2andAhalf = False @@ -709,7 +711,7 @@ def power( def get_ct(x,*data): sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: + if MU == 0: MU = 1e-6 sinMu = np.sin(MU) cosMu = np.cos(MU) @@ -729,7 +731,7 @@ def get_ct(x,*data): def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: + if MU == 0: MU = 1e-6 sinMu = np.sin(MU) cosMu = np.cos(MU) @@ -953,7 +955,7 @@ def thrust_coefficient( def get_ct(x,*data): sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: + if MU == 0: MU = 1e-6 sinMu = np.sin(MU) cosMu = np.cos(MU) @@ -1113,7 +1115,7 @@ def axial_induction( def get_ct(x,*data): sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: + if MU == 0: MU = 1e-6 sinMu = np.sin(MU) cosMu = np.cos(MU) @@ -1141,13 +1143,13 @@ def get_ct(x,*data): shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) air_density = power_thrust_table["ref_air_density"] # CHANGE - + rotor_effective_velocities = rotor_velocity_air_density_correction( velocities=rotor_average_velocities, air_density=air_density, ref_air_density=power_thrust_table["ref_air_density"] ) - + pitch_out, tsr_out = TUMLossTurbine.control_trajectory( rotor_effective_velocities, yaw_angles, diff --git a/floris/turbine_library/iea_15MW.yaml b/floris/turbine_library/iea_15MW.yaml index 18b667bf9..365280885 100644 --- a/floris/turbine_library/iea_15MW.yaml +++ b/floris/turbine_library/iea_15MW.yaml @@ -33,7 +33,7 @@ power_thrust_table: beta: -3.098605491003358 cd: 0.004426686198054057 cl_alfa: 4.546410770937916 - + # Power and thrust curves power: - 0.000000 diff --git a/floris/turbine_library/nrel_5MW.yaml b/floris/turbine_library/nrel_5MW.yaml index 0b39d8e15..a35277f6e 100644 --- a/floris/turbine_library/nrel_5MW.yaml +++ b/floris/turbine_library/nrel_5MW.yaml @@ -57,7 +57,7 @@ power_thrust_table: peak_shaving_TI_threshold: 0.1 ### Parameters for the 'tum-loss' operation model - rated_rpm: 12.1 + rated_rpm: 12.1 rotor_solidity: 0.05132 generator_efficiency: 0.944 rated_power: 5.0e6 diff --git a/tests/conftest.py b/tests/conftest.py index fffe813b2..462adcf17 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -392,6 +392,8 @@ def __init__(self): self.tum_loss_turbine_power_thrust_table = { "ref_air_density": 1.225, + "ref_tilt": 5.0, + "rated_rpm": 12.1, "rotor_solidity": 0.05132, "generator_efficiency": 0.944, "rated_power": 5.0e6, @@ -399,7 +401,6 @@ def __init__(self): "beta": -0.45891, "cd": 0.0040638, "cl_alfa": 4.275049, - "ref_tilt": 5.0, } self.turbine_floating = copy.deepcopy(self.turbine) From b66a9a642a41ca3193966f35904b61db51e611bb Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 09:54:00 -0700 Subject: [PATCH 30/53] Update 3mw model fields. --- floris/turbine_library/iea_3MW.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/floris/turbine_library/iea_3MW.yaml b/floris/turbine_library/iea_3MW.yaml index 792019042..5ec3035b4 100644 --- a/floris/turbine_library/iea_3MW.yaml +++ b/floris/turbine_library/iea_3MW.yaml @@ -24,7 +24,7 @@ TSR: 8.0 ### # Model for power and thrust curve interpretation. #power_thrust_model: 'cosine-loss' -power_thrust_model: 'tum-loss' +operation_model: 'tum-loss' ### # Cp and Ct as a function of wind speed for the turbine's full range of operating conditions. power_thrust_table: From 825a8087f63d9c6b5fd2cbbd230b58c318d58fa9 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 11:37:15 -0700 Subject: [PATCH 31/53] Separate TUM operation model to own file. --- floris/core/turbine/__init__.py | 2 +- floris/core/turbine/operation_models.py | 945 +------------------ floris/core/turbine/tum_operation_model.py | 963 ++++++++++++++++++++ tests/tum_operation_model_unit_test.py | 129 +++ tests/turbine_operation_models_unit_test.py | 118 --- 5 files changed, 1094 insertions(+), 1063 deletions(-) create mode 100644 floris/core/turbine/tum_operation_model.py create mode 100644 tests/tum_operation_model_unit_test.py diff --git a/floris/core/turbine/__init__.py b/floris/core/turbine/__init__.py index 68aa90cb8..7fc1f1884 100644 --- a/floris/core/turbine/__init__.py +++ b/floris/core/turbine/__init__.py @@ -6,5 +6,5 @@ PeakShavingTurbine, SimpleDeratingTurbine, SimpleTurbine, - TUMLossTurbine, ) +from floris.core.turbine.tum_operation_model import TUMLossTurbine diff --git a/floris/core/turbine/operation_models.py b/floris/core/turbine/operation_models.py index e5ec3ac6c..a6c1ff160 100644 --- a/floris/core/turbine/operation_models.py +++ b/floris/core/turbine/operation_models.py @@ -2,9 +2,7 @@ from __future__ import annotations import copy -import os from abc import abstractmethod -from pathlib import Path from typing import ( Any, Dict, @@ -13,8 +11,7 @@ import numpy as np from attrs import define, field -from scipy.interpolate import interp1d, RegularGridInterpolator -from scipy.optimize import fsolve +from scipy.interpolate import interp1d from floris.core import BaseClass from floris.core.rotor_velocity import ( @@ -23,7 +20,6 @@ rotor_velocity_air_density_correction, rotor_velocity_tilt_cosine_correction, rotor_velocity_yaw_cosine_correction, - tum_rotor_velocity_yaw_correction, ) from floris.type_dec import ( NDArrayFloat, @@ -301,945 +297,6 @@ def axial_induction( misalignment_loss = cosd(yaw_angles) * cosd(tilt_angles - power_thrust_table["ref_tilt"]) return 0.5 / misalignment_loss * (1 - np.sqrt(1 - thrust_coefficient * misalignment_loss)) - - -@define -class TUMLossTurbine(BaseOperationModel): - """ - Static class defining a wind turbine model that may be misaligned with the flow. - Nonzero tilt and yaw angles are handled via the model presented in - https://doi.org/10.5194/wes-2023-133 . - - The method requires C_P, C_T look-up tables as functions of tip speed ratio and blade pitch - angle, available here: - "../floris/turbine_library/LUT_iea15MW.npz" for the IEA 3.4 MW (Bortolotti et al., 2019) - As with all turbine submodules, implements only static power() and thrust_coefficient() methods, - which are called by power() and thrust_coefficient() on turbine.py, respectively. - There are also two new functions, i.e. compute_local_vertical_shear() and control_trajectory(). - These are called by thrust_coefficient() and power() to compute the vertical shear and predict - the turbine status in terms of tip speed ratio and pitch angle. - This class is not intended to be instantiated; it simply defines a library of static methods. - - TODO: Should the turbine submodels each implement axial_induction()? - """ - - def compute_local_vertical_shear(velocities,avg_velocities): - num_rows, num_cols = avg_velocities.shape - shear = np.zeros_like(avg_velocities) - for i in np.arange(num_rows): - for j in np.arange(num_cols): - mean_speed = np.mean(velocities[i,j,:,:],axis=0) - if len(mean_speed) % 2 != 0: # odd number - u_u_hh = mean_speed/mean_speed[int(np.floor(len(mean_speed)/2))] - else: - u_u_hh = ( - mean_speed - /(mean_speed[int((len(mean_speed)/2))] - +mean_speed[int((len(mean_speed)/2))-1] - )/2 - ) - zg_R = np.linspace(-1,1,len(mean_speed)+2) - polifit_k = np.polyfit(zg_R[1:-1],1-u_u_hh,1) - shear[i,j] = -polifit_k[0] - return shear - - def control_trajectory( - rotor_average_velocities, - yaw_angles, - tilt_angles, - air_density, - R, - shear, - sigma, - cd, - cl_alfa, - beta, - power_setpoints, - power_thrust_table - ): - if power_setpoints is None: - power_demanded = ( - np.ones_like(tilt_angles)*power_thrust_table["rated_power"] - /power_thrust_table["generator_efficiency"] - ) - else: - power_demanded = power_setpoints/power_thrust_table["generator_efficiency"] - - def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): - #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) - SG = np.sin(np.deg2rad(gamma)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - CD = np.cos(np.deg2rad(delta)) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(ct/2))/2)) - - p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 - - (tsr*cd*np.pi*( - CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - - 8*CD*tsr*SG*k + 8*tsr**2 - ))/16 - - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 - + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 - + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 - + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 - + ((CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2)) - /(4*sinMu**2)) - - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 - + ((CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi - *(3*CG**2*SD**2 + SG**2)) - /(24*sinMu**2)) - - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) - - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu) - )/(2*np.pi)) - return p - - - def get_ct(x,*data): - sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU = data - #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - CD = np.cos(np.deg2rad(delta)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - SG = np.sin(np.deg2rad(gamma)) - a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) - + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k - + 8*tsr**2))/12)/(2*np.pi) - - return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x - - - ## Define function to get tip speed ratio - def get_tsr(x,*data): - (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,pitch_in,omega_lut_pow, - torque_lut_omega,cp_i,pitch_i,tsr_i) = data - - omega_lut_torque = omega_lut_pow*np.pi/30 - - omega = x*u/R - omega_rpm = omega*30/np.pi - - torque_nm = np.interp(omega,omega_lut_torque,torque_lut_omega) - - mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),x, - np.deg2rad(pitch_in)+np.deg2rad(beta),mu) - x0 = 0.1 - [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) - cp = find_cp( - sigma, - cd, - cl_alfa, - gamma, - tilt, - shear, - np.cos(mu), - np.sin(mu), - x, - np.deg2rad(pitch_in)+np.deg2rad(beta), - mu, - ct - ) - - mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),x, - np.deg2rad(pitch_in)+np.deg2rad(beta),mu) - x0 = 0.1 - [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) - cp0 = find_cp( - sigma, - cd, - cl_alfa, - 0, - tilt, - shear, - np.cos(mu), - np.sin(mu), - x, - np.deg2rad(pitch_in)+np.deg2rad(beta), - mu, - ct - ) - - eta_p = cp/cp0 - - interp = RegularGridInterpolator((np.squeeze((tsr_i)), - np.squeeze((pitch_i))), cp_i, - bounds_error=False, fill_value=None) - - Cp_now = interp((x,pitch_in),method='cubic') - cp_g1 = Cp_now*eta_p - aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 - electric_pow = torque_nm*(omega_rpm*np.pi/30) - - y = aero_pow - electric_pow - return y - - ## Define function to get pitch angle - def get_pitch(x,*data): - (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,omega_rated,omega_lut_torque, - torque_lut_omega,cp_i,pitch_i,tsr_i) = data - - omega_rpm = omega_rated*30/np.pi - tsr = omega_rated*R/(u) - - pitch_in = np.deg2rad(x) - torque_nm = np.interp(omega_rpm,omega_lut_torque*30/np.pi,torque_lut_omega) - - mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),tsr, - (pitch_in)+np.deg2rad(beta),mu) - x0 = 0.1 - [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) - cp = find_cp( - sigma, - cd, - cl_alfa, - gamma, - tilt, - shear, - np.cos(mu), - np.sin(mu), - tsr, - (pitch_in)+np.deg2rad(beta), - mu, - ct - ) - - mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),tsr, - (pitch_in)+np.deg2rad(beta),mu) - x0 = 0.1 - [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) - cp0 = find_cp( - sigma, - cd, - cl_alfa, - 0, - tilt, - shear, - np.cos(mu), - np.sin(mu), - tsr, - (pitch_in)+np.deg2rad(beta), - mu, - ct - ) - - eta_p = cp/cp0 - - interp = RegularGridInterpolator( - (np.squeeze((tsr_i)), np.squeeze((pitch_i))), - cp_i, - bounds_error=False, - fill_value=None - ) - - Cp_now = interp((tsr,x),method='cubic') - cp_g1 = Cp_now*eta_p - aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 - electric_pow = torque_nm*(omega_rpm*np.pi/30) - - y = aero_pow - electric_pow - return y - - pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" - LUT = np.load(lut_file) - cp_i = LUT['cp_lut'] - pitch_i = LUT['pitch_lut'] - tsr_i = LUT['tsr_lut'] - idx = np.squeeze(np.where(cp_i == np.max(cp_i))) - - tsr_opt = tsr_i[idx[0]] - pitch_opt = pitch_i[idx[1]] - max_cp = cp_i[idx[0],idx[1]] - - omega_cut_in = 0 # RPM - omega_max = power_thrust_table["rated_rpm"] # RPM - rated_power_aero = ( - power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] # MW - ) - #%% Compute torque-rpm relation and check for region 2-and-a-half - Region2andAhalf = False - - omega_array = np.linspace(omega_cut_in,omega_max,161)*np.pi/30 # rad/s - Q = (0.5*air_density*omega_array**2*R**5 * np.pi * max_cp ) / tsr_opt**3 - - Paero_array = Q*omega_array - - if Paero_array[-1] < rated_power_aero: # then we have region 2and1/2 - Region2andAhalf = True - Q_extra = rated_power_aero/(omega_max*np.pi/30) - Q = np.append(Q,Q_extra) - (Paero_array[-1]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) - omega_array = np.append(omega_array,omega_array[-1]) - Paero_array = np.append(Paero_array,rated_power_aero) - else: # limit aero_power to the last Q*omega_max - rated_power_aero = Paero_array[-1] - - u_rated = (rated_power_aero/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) - u_array = np.linspace(3,25,45) - idx = np.argmin(np.abs(u_array-u_rated)) - if u_rated > u_array[idx]: - u_array = np.insert(u_array,idx+1,u_rated) - else: - u_array = np.insert(u_array,idx,u_rated) - - pow_lut_omega = Paero_array - omega_lut_pow = omega_array*30/np.pi - torque_lut_omega = Q - omega_lut_torque = omega_lut_pow - - num_rows, num_cols = tilt_angles.shape - - omega_rated = np.zeros_like(rotor_average_velocities) - u_rated = np.zeros_like(rotor_average_velocities) - for i in np.arange(num_rows): - for j in np.arange(num_cols): - omega_rated[i,j] = ( - np.interp(power_demanded[i,j],pow_lut_omega,omega_lut_pow) - *np.pi/30 #rad/s - ) - u_rated[i,j] = (power_demanded[i,j]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) - - pitch_out = np.zeros_like(rotor_average_velocities) - tsr_out = np.zeros_like(rotor_average_velocities) - - for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - k = shear[i,:] - for j in np.arange(num_cols): - u_v = rotor_average_velocities[i,j] - if u_v > u_rated[i,j]: - tsr_v = omega_rated[i,j]*R/u_v*np.cos(np.deg2rad(yaw[j]))**0.5 - else: - tsr_v = tsr_opt*np.cos(np.deg2rad(yaw[j])) - if Region2andAhalf: # fix for interpolation - omega_lut_torque[-1] = omega_lut_torque[-1]+1e-2 - omega_lut_pow[-1] = omega_lut_pow[-1]+1e-2 - - data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v,pitch_opt, - omega_lut_pow,torque_lut_omega,cp_i,pitch_i,tsr_i) - [tsr_out_soluzione,infodict,ier,mesg] = fsolve( - get_tsr,tsr_v,args=data,full_output=True - ) - # check if solution was possible. If not, we are in region 3 - if (np.abs(infodict['fvec']) > 10 or tsr_out_soluzione < 4): - tsr_out_soluzione = 1000 - - # save solution - tsr_outO = tsr_out_soluzione - omega = tsr_outO*u_v/R - - # check if we are in region 2 or 3 - if omega < omega_rated[i,j]: # region 2 - # Define optimum pitch - pitch_out0 = pitch_opt - - else: # region 3 - tsr_outO = omega_rated[i,j]*R/u_v - data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v, - omega_rated[i,j],omega_array,Q,cp_i,pitch_i,tsr_i) - # solve aero-electrical power balance with TSR from rated omega - [pitch_out_soluzione,infodict,ier,mesg] = fsolve( - get_pitch,u_v,args=data,factor=0.1,full_output=True, - xtol=1e-10,maxfev=2000) - if pitch_out_soluzione < pitch_opt: - pitch_out_soluzione = pitch_opt - pitch_out0 = pitch_out_soluzione - #%% COMPUTE CP AND CT GIVEN THE PITCH AND TSR FOUND ABOVE - pitch_out[i,j] = np.squeeze(pitch_out0) - tsr_out[i,j] = tsr_outO - - return pitch_out, tsr_out - - def power( - power_thrust_table: dict, - velocities: NDArrayFloat, - air_density: float, - yaw_angles: NDArrayFloat, - tilt_angles: NDArrayFloat, - power_setpoints: NDArrayFloat, - tilt_interp: NDArrayObject, - average_method: str = "cubic-mean", - cubature_weights: NDArrayFloat | None = None, - correct_cp_ct_for_tilt: bool = False, - **_ # <- Allows other models to accept other keyword arguments - ): - # # Construct power interpolant - # power_interpolator = interp1d( - # power_thrust_table["wind_speed"], - # power_thrust_table["power"], - # fill_value=0.0, - # bounds_error=False, - # ) - - # sign convention. in the tum model, negative tilt creates tower clearance - tilt_angles = -tilt_angles - - # Compute the power-effective wind speed across the rotor - rotor_average_velocities = average_velocity( - velocities=velocities, - method=average_method, - cubature_weights=cubature_weights, - ) - - rotor_effective_velocities = rotor_velocity_air_density_correction( - velocities=rotor_average_velocities, - air_density=air_density, - ref_air_density=power_thrust_table["ref_air_density"] - ) - - # Compute power - def get_ct(x,*data): - sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data - #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - CD = np.cos(np.deg2rad(delta)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - SG = np.sin(np.deg2rad(gamma)) - a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) - + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k - + 8*tsr**2))/12)/(2*np.pi) - - return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x - - def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): - #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) - SG = np.sin(np.deg2rad(gamma)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - CD = np.cos(np.deg2rad(delta)) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(ct/2))/2)) - - p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 - - (tsr*cd*np.pi*(CD**2*CG**2*SD**2*k**2 + 3*CD**2*SG**2*k**2 - - 8*CD*tsr*SG*k + 8*tsr**2))/16 - - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 - + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 - + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 - + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 - + ((CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2)) - /(4*sinMu**2)) - - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 - + ((CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi*(3*CG**2*SD**2 - + SG**2)) - /(24*sinMu**2)) - - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) - - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu) - )/(2*np.pi) - ) - return p - - num_rows, num_cols = tilt_angles.shape - (average_velocity(velocities)) - - shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) - - beta = power_thrust_table["beta"] - cd = power_thrust_table["cd"] - cl_alfa = power_thrust_table["cl_alfa"] - - sigma = power_thrust_table["rotor_solidity"] - R = power_thrust_table["rotor_diameter"]/2 - - air_density = power_thrust_table["ref_air_density"] - - pitch_out, tsr_out = TUMLossTurbine.control_trajectory( - rotor_effective_velocities, - yaw_angles, - tilt_angles, - air_density, - R, - shear, - sigma, - cd, - cl_alfa, - beta, - power_setpoints, - power_thrust_table - ) - - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) - tsr_array = (tsr_out) - theta_array = (np.deg2rad(pitch_out+beta)) - - x0 = 0.2 - - p = np.zeros_like(average_velocity(velocities)) - - for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - k = shear[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] - for j in np.arange(num_cols): - # Break below command over multiple lines - data = ( - sigma, - cd, - cl_alfa, - yaw[j], - tilt[j], - k[j], - cMu[j], - sMu[j], - (tsr_array[i,j]), - (theta_array[i,j]), - R, - Mu[j] - ) - ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) - if ier == 1: - p[i,j] = np.squeeze(find_cp( - sigma, - cd, - cl_alfa, - yaw[j], - tilt[j], - k[j], - cMu[j], - sMu[j], - (tsr_array[i,j]), - (theta_array[i,j]), - R, - Mu[j], - ct - )) - else: - p[i,j] = -1e3 - - ############################################################################ - - yaw_angles = np.zeros_like(yaw_angles) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) - - p0 = np.zeros_like((average_velocity(velocities))) - - for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - k = shear[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] - for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),R,Mu[j]) - ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) - if ier == 1: - p0[i,j] = np.squeeze(find_cp( - sigma, - cd, - cl_alfa, - yaw[j], - tilt[j], - k[j], - cMu[j], - sMu[j], - (tsr_array[i,j]), - (theta_array[i,j]), - R, - Mu[j], - ct - )) - else: - p0[i,j] = -1e3 - - ratio = p/p0 - - ############################################################################ - - pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" - LUT = np.load(lut_file) - cp_i = LUT['cp_lut'] - pitch_i = LUT['pitch_lut'] - tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator( - (tsr_i,pitch_i), - cp_i, - bounds_error=False, - fill_value=None - ) - - power_coefficient = np.zeros_like(average_velocity(velocities)) - for i in np.arange(num_rows): - for j in np.arange(num_cols): - cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - power_coefficient[i,j] = np.squeeze(cp_interp*ratio[i,j]) - - # TODO: make printout optional? - if False: - print('Tip speed ratio' + str(tsr_array)) - print('Pitch out: ' + str(pitch_out)) - power = ( - 0.5*air_density*(rotor_effective_velocities)**3*np.pi*R**2 - *(power_coefficient)*power_thrust_table["generator_efficiency"] - ) - return power - - def thrust_coefficient( - power_thrust_table: dict, - velocities: NDArrayFloat, - yaw_angles: NDArrayFloat, - tilt_angles: NDArrayFloat, - power_setpoints: NDArrayFloat, - tilt_interp: NDArrayObject, - average_method: str = "cubic-mean", - cubature_weights: NDArrayFloat | None = None, - correct_cp_ct_for_tilt: bool = False, - **_ # <- Allows other models to accept other keyword arguments - ): - - # sign convention. in the tum model, negative tilt creates tower clearance - tilt_angles = -tilt_angles - - # Compute the effective wind speed across the rotor - rotor_average_velocities = average_velocity( - velocities=velocities, - method=average_method, - cubature_weights=cubature_weights, - ) - - - # Apply tilt and yaw corrections - # Compute the tilt, if using floating turbines - old_tilt_angles = copy.deepcopy(tilt_angles) - tilt_angles = compute_tilt_angles_for_floating_turbines( - tilt_angles=tilt_angles, - tilt_interp=tilt_interp, - rotor_effective_velocities=rotor_average_velocities, - ) - # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) - tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) - - def get_ct(x,*data): - sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data - #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - CD = np.cos(np.deg2rad(delta)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - SG = np.sin(np.deg2rad(gamma)) - a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) - + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k - + 8*tsr**2))/12)/(2*np.pi) - - return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x - - beta = power_thrust_table["beta"] - cd = power_thrust_table["cd"] - cl_alfa = power_thrust_table["cl_alfa"] - - sigma = power_thrust_table["rotor_solidity"] - R = power_thrust_table["rotor_diameter"]/2 - - shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) - - air_density = power_thrust_table["ref_air_density"] # CHANGE - - rotor_effective_velocities = rotor_velocity_air_density_correction( - velocities=rotor_average_velocities, - air_density=air_density, - ref_air_density=power_thrust_table["ref_air_density"] - ) - - pitch_out, tsr_out = TUMLossTurbine.control_trajectory( - rotor_effective_velocities, - yaw_angles, - tilt_angles, - air_density, - R, - shear, - sigma, - cd, - cl_alfa, - beta, - power_setpoints, - power_thrust_table - ) - - num_rows, num_cols = tilt_angles.shape - - (average_velocity(velocities)) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) - # u = np.squeeze(u) - theta_array = (np.deg2rad(pitch_out+beta)) - tsr_array = (tsr_out) - - x0 = 0.2 - - thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) - for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] - for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),R,Mu[j]) - ct = fsolve(get_ct, x0,args=data) - thrust_coefficient1[i,j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) - - - yaw_angles = np.zeros_like(yaw_angles) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) - - thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) - - for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] - for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),R,Mu[j]) - ct = fsolve(get_ct, x0,args=data) - thrust_coefficient0[i,j] = np.squeeze(ct) #np.clip(ct, 0.0001, 0.9999) - - ############################################################################ - - ratio = thrust_coefficient1/thrust_coefficient0 - - pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" - LUT = np.load(lut_file) - ct_i = LUT['ct_lut'] - pitch_i = LUT['pitch_lut'] - tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator( - (tsr_i,pitch_i), - ct_i, - bounds_error=False, - fill_value=None - )#*0.9722085500886761) - - - thrust_coefficient = np.zeros_like(average_velocity(velocities)) - - for i in np.arange(num_rows): - for j in np.arange(num_cols): - ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - thrust_coefficient[i,j] = np.squeeze(ct_interp*ratio[i,j]) - - return thrust_coefficient - - def axial_induction( - power_thrust_table: dict, - velocities: NDArrayFloat, - yaw_angles: NDArrayFloat, - tilt_angles: NDArrayFloat, - power_setpoints: NDArrayFloat, - tilt_interp: NDArrayObject, - average_method: str = "cubic-mean", - cubature_weights: NDArrayFloat | None = None, - correct_cp_ct_for_tilt: bool = False, - **_ # <- Allows other models to accept other keyword arguments - ): - - # sign convention. in the tum model, negative tilt creates tower clearance - tilt_angles = -tilt_angles - - # Compute the effective wind speed across the rotor - rotor_average_velocities = average_velocity( - velocities=velocities, - method=average_method, - cubature_weights=cubature_weights, - ) - - - # Apply tilt and yaw corrections - # Compute the tilt, if using floating turbines - old_tilt_angles = copy.deepcopy(tilt_angles) - tilt_angles = compute_tilt_angles_for_floating_turbines( - tilt_angles=tilt_angles, - tilt_interp=tilt_interp, - rotor_effective_velocities=rotor_average_velocities, - ) - # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) - tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) - - def get_ct(x,*data): - sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data - #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - CD = np.cos(np.deg2rad(delta)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - SG = np.sin(np.deg2rad(gamma)) - a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) - + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k - + 8*tsr**2))/12)/(2*np.pi) - - return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x - - beta = power_thrust_table["beta"] - cd = power_thrust_table["cd"] - cl_alfa = power_thrust_table["cl_alfa"] - - sigma = power_thrust_table["rotor_solidity"] - R = power_thrust_table["rotor_diameter"]/2 - - shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) - - air_density = power_thrust_table["ref_air_density"] # CHANGE - - rotor_effective_velocities = rotor_velocity_air_density_correction( - velocities=rotor_average_velocities, - air_density=air_density, - ref_air_density=power_thrust_table["ref_air_density"] - ) - - pitch_out, tsr_out = TUMLossTurbine.control_trajectory( - rotor_effective_velocities, - yaw_angles, - tilt_angles, - air_density, - R, - shear, - sigma, - cd, - cl_alfa, - beta, - power_setpoints, - power_thrust_table - ) - - num_rows, num_cols = tilt_angles.shape - - (average_velocity(velocities)) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) - # u = np.squeeze(u) - theta_array = (np.deg2rad(pitch_out+beta)) - tsr_array = (tsr_out) - - x0 = 0.2 - - thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) - for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] - for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),R,Mu[j]) - ct = fsolve(get_ct, x0,args=data) - thrust_coefficient1[i,j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) - - - yaw_angles = np.zeros_like(yaw_angles) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) - - thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) - - for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] - for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),R,Mu[j]) - ct = fsolve(get_ct, x0,args=data) - thrust_coefficient0[i,j] = np.squeeze(ct) #np.clip(ct, 0.0001, 0.9999) - - ############################################################################ - - ratio = thrust_coefficient1/thrust_coefficient0 - - pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" - LUT = np.load(lut_file) - ct_i = LUT['ct_lut'] - pitch_i = LUT['pitch_lut'] - tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator( - (tsr_i,pitch_i), - ct_i, - bounds_error=False, - fill_value=None - )#*0.9722085500886761) - - axial_induction = np.zeros_like(average_velocity(velocities)) - - for i in np.arange(num_rows): - for j in np.arange(num_cols): - ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - ct = ct_interp*ratio[i,j] - a = (1- ( (1+np.sqrt(1-ct-1/16*ct**2*sMu[j]**2))/(2*(1+1/16*ct*sMu[j]**2))) ) - axial_induction[i,j] = np.squeeze(np.clip(a, 0.0001, 0.9999)) - - return axial_induction - - - @define class SimpleDeratingTurbine(BaseOperationModel): """ diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py new file mode 100644 index 000000000..bd2b1d428 --- /dev/null +++ b/floris/core/turbine/tum_operation_model.py @@ -0,0 +1,963 @@ +from __future__ import annotations + +import copy +import os +from abc import abstractmethod +from pathlib import Path +from typing import ( + Any, + Dict, + Final, +) + +import numpy as np +from attrs import define, field +from scipy.interpolate import interp1d, RegularGridInterpolator +from scipy.optimize import fsolve + +from floris.core.rotor_velocity import ( + average_velocity, + compute_tilt_angles_for_floating_turbines, + rotor_velocity_air_density_correction, +) +from floris.core.turbine.operation_models import BaseOperationModel +from floris.type_dec import ( + NDArrayFloat, + NDArrayObject, +) + + +@define +class TUMLossTurbine(BaseOperationModel): + """ + Static class defining a wind turbine model that may be misaligned with the flow. + Nonzero tilt and yaw angles are handled via the model presented in + https://doi.org/10.5194/wes-2023-133 . + + The method requires C_P, C_T look-up tables as functions of tip speed ratio and blade pitch + angle, available here: + "../floris/turbine_library/LUT_iea15MW.npz" for the IEA 3.4 MW (Bortolotti et al., 2019) + As with all turbine submodules, implements only static power() and thrust_coefficient() methods, + which are called by power() and thrust_coefficient() on turbine.py, respectively. + There are also two new functions, i.e. compute_local_vertical_shear() and control_trajectory(). + These are called by thrust_coefficient() and power() to compute the vertical shear and predict + the turbine status in terms of tip speed ratio and pitch angle. + This class is not intended to be instantiated; it simply defines a library of static methods. + + TODO: Should the turbine submodels each implement axial_induction()? + """ + + def compute_local_vertical_shear(velocities,avg_velocities): + num_rows, num_cols = avg_velocities.shape + shear = np.zeros_like(avg_velocities) + for i in np.arange(num_rows): + for j in np.arange(num_cols): + mean_speed = np.mean(velocities[i,j,:,:],axis=0) + if len(mean_speed) % 2 != 0: # odd number + u_u_hh = mean_speed/mean_speed[int(np.floor(len(mean_speed)/2))] + else: + u_u_hh = ( + mean_speed + /(mean_speed[int((len(mean_speed)/2))] + +mean_speed[int((len(mean_speed)/2))-1] + )/2 + ) + zg_R = np.linspace(-1,1,len(mean_speed)+2) + polifit_k = np.polyfit(zg_R[1:-1],1-u_u_hh,1) + shear[i,j] = -polifit_k[0] + return shear + + def control_trajectory( + rotor_average_velocities, + yaw_angles, + tilt_angles, + air_density, + R, + shear, + sigma, + cd, + cl_alfa, + beta, + power_setpoints, + power_thrust_table + ): + if power_setpoints is None: + power_demanded = ( + np.ones_like(tilt_angles)*power_thrust_table["rated_power"] + /power_thrust_table["generator_efficiency"] + ) + else: + power_demanded = power_setpoints/power_thrust_table["generator_efficiency"] + + def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) + a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) + SG = np.sin(np.deg2rad(gamma)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + CD = np.cos(np.deg2rad(delta)) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(ct/2))/2)) + + p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 + - (tsr*cd*np.pi*( + CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 + - 8*CD*tsr*SG*k + 8*tsr**2 + ))/16 + - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 + + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 + + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 + + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 + + ((CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2)) + /(4*sinMu**2)) + - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 + + ((CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi + *(3*CG**2*SD**2 + SG**2)) + /(24*sinMu**2)) + - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) + - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu) + )/(2*np.pi)) + return p + + + def get_ct(x,*data): + sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU = data + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) + CD = np.cos(np.deg2rad(delta)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + SG = np.sin(np.deg2rad(gamma)) + a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + + 8*tsr**2))/12)/(2*np.pi) + + return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x + + + ## Define function to get tip speed ratio + def get_tsr(x,*data): + (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,pitch_in,omega_lut_pow, + torque_lut_omega,cp_i,pitch_i,tsr_i) = data + + omega_lut_torque = omega_lut_pow*np.pi/30 + + omega = x*u/R + omega_rpm = omega*30/np.pi + + torque_nm = np.interp(omega,omega_lut_torque,torque_lut_omega) + + mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) + data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),x, + np.deg2rad(pitch_in)+np.deg2rad(beta),mu) + x0 = 0.1 + [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + cp = find_cp( + sigma, + cd, + cl_alfa, + gamma, + tilt, + shear, + np.cos(mu), + np.sin(mu), + x, + np.deg2rad(pitch_in)+np.deg2rad(beta), + mu, + ct + ) + + mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) + data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),x, + np.deg2rad(pitch_in)+np.deg2rad(beta),mu) + x0 = 0.1 + [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + cp0 = find_cp( + sigma, + cd, + cl_alfa, + 0, + tilt, + shear, + np.cos(mu), + np.sin(mu), + x, + np.deg2rad(pitch_in)+np.deg2rad(beta), + mu, + ct + ) + + eta_p = cp/cp0 + + interp = RegularGridInterpolator((np.squeeze((tsr_i)), + np.squeeze((pitch_i))), cp_i, + bounds_error=False, fill_value=None) + + Cp_now = interp((x,pitch_in),method='cubic') + cp_g1 = Cp_now*eta_p + aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 + electric_pow = torque_nm*(omega_rpm*np.pi/30) + + y = aero_pow - electric_pow + return y + + ## Define function to get pitch angle + def get_pitch(x,*data): + (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,omega_rated,omega_lut_torque, + torque_lut_omega,cp_i,pitch_i,tsr_i) = data + + omega_rpm = omega_rated*30/np.pi + tsr = omega_rated*R/(u) + + pitch_in = np.deg2rad(x) + torque_nm = np.interp(omega_rpm,omega_lut_torque*30/np.pi,torque_lut_omega) + + mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) + data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),tsr, + (pitch_in)+np.deg2rad(beta),mu) + x0 = 0.1 + [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + cp = find_cp( + sigma, + cd, + cl_alfa, + gamma, + tilt, + shear, + np.cos(mu), + np.sin(mu), + tsr, + (pitch_in)+np.deg2rad(beta), + mu, + ct + ) + + mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) + data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),tsr, + (pitch_in)+np.deg2rad(beta),mu) + x0 = 0.1 + [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + cp0 = find_cp( + sigma, + cd, + cl_alfa, + 0, + tilt, + shear, + np.cos(mu), + np.sin(mu), + tsr, + (pitch_in)+np.deg2rad(beta), + mu, + ct + ) + + eta_p = cp/cp0 + + interp = RegularGridInterpolator( + (np.squeeze((tsr_i)), np.squeeze((pitch_i))), + cp_i, + bounds_error=False, + fill_value=None + ) + + Cp_now = interp((tsr,x),method='cubic') + cp_g1 = Cp_now*eta_p + aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 + electric_pow = torque_nm*(omega_rpm*np.pi/30) + + y = aero_pow - electric_pow + return y + + pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] + lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" + LUT = np.load(lut_file) + cp_i = LUT['cp_lut'] + pitch_i = LUT['pitch_lut'] + tsr_i = LUT['tsr_lut'] + idx = np.squeeze(np.where(cp_i == np.max(cp_i))) + + tsr_opt = tsr_i[idx[0]] + pitch_opt = pitch_i[idx[1]] + max_cp = cp_i[idx[0],idx[1]] + + omega_cut_in = 0 # RPM + omega_max = power_thrust_table["rated_rpm"] # RPM + rated_power_aero = ( + power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] # MW + ) + #%% Compute torque-rpm relation and check for region 2-and-a-half + Region2andAhalf = False + + omega_array = np.linspace(omega_cut_in,omega_max,161)*np.pi/30 # rad/s + Q = (0.5*air_density*omega_array**2*R**5 * np.pi * max_cp ) / tsr_opt**3 + + Paero_array = Q*omega_array + + if Paero_array[-1] < rated_power_aero: # then we have region 2and1/2 + Region2andAhalf = True + Q_extra = rated_power_aero/(omega_max*np.pi/30) + Q = np.append(Q,Q_extra) + (Paero_array[-1]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) + omega_array = np.append(omega_array,omega_array[-1]) + Paero_array = np.append(Paero_array,rated_power_aero) + else: # limit aero_power to the last Q*omega_max + rated_power_aero = Paero_array[-1] + + u_rated = (rated_power_aero/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) + u_array = np.linspace(3,25,45) + idx = np.argmin(np.abs(u_array-u_rated)) + if u_rated > u_array[idx]: + u_array = np.insert(u_array,idx+1,u_rated) + else: + u_array = np.insert(u_array,idx,u_rated) + + pow_lut_omega = Paero_array + omega_lut_pow = omega_array*30/np.pi + torque_lut_omega = Q + omega_lut_torque = omega_lut_pow + + num_rows, num_cols = tilt_angles.shape + + omega_rated = np.zeros_like(rotor_average_velocities) + u_rated = np.zeros_like(rotor_average_velocities) + for i in np.arange(num_rows): + for j in np.arange(num_cols): + omega_rated[i,j] = ( + np.interp(power_demanded[i,j],pow_lut_omega,omega_lut_pow) + *np.pi/30 #rad/s + ) + u_rated[i,j] = (power_demanded[i,j]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) + + pitch_out = np.zeros_like(rotor_average_velocities) + tsr_out = np.zeros_like(rotor_average_velocities) + + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + k = shear[i,:] + for j in np.arange(num_cols): + u_v = rotor_average_velocities[i,j] + if u_v > u_rated[i,j]: + tsr_v = omega_rated[i,j]*R/u_v*np.cos(np.deg2rad(yaw[j]))**0.5 + else: + tsr_v = tsr_opt*np.cos(np.deg2rad(yaw[j])) + if Region2andAhalf: # fix for interpolation + omega_lut_torque[-1] = omega_lut_torque[-1]+1e-2 + omega_lut_pow[-1] = omega_lut_pow[-1]+1e-2 + + data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v,pitch_opt, + omega_lut_pow,torque_lut_omega,cp_i,pitch_i,tsr_i) + [tsr_out_soluzione,infodict,ier,mesg] = fsolve( + get_tsr,tsr_v,args=data,full_output=True + ) + # check if solution was possible. If not, we are in region 3 + if (np.abs(infodict['fvec']) > 10 or tsr_out_soluzione < 4): + tsr_out_soluzione = 1000 + + # save solution + tsr_outO = tsr_out_soluzione + omega = tsr_outO*u_v/R + + # check if we are in region 2 or 3 + if omega < omega_rated[i,j]: # region 2 + # Define optimum pitch + pitch_out0 = pitch_opt + + else: # region 3 + tsr_outO = omega_rated[i,j]*R/u_v + data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v, + omega_rated[i,j],omega_array,Q,cp_i,pitch_i,tsr_i) + # solve aero-electrical power balance with TSR from rated omega + [pitch_out_soluzione,infodict,ier,mesg] = fsolve( + get_pitch,u_v,args=data,factor=0.1,full_output=True, + xtol=1e-10,maxfev=2000) + if pitch_out_soluzione < pitch_opt: + pitch_out_soluzione = pitch_opt + pitch_out0 = pitch_out_soluzione + #%% COMPUTE CP AND CT GIVEN THE PITCH AND TSR FOUND ABOVE + pitch_out[i,j] = np.squeeze(pitch_out0) + tsr_out[i,j] = tsr_outO + + return pitch_out, tsr_out + + def power( + power_thrust_table: dict, + velocities: NDArrayFloat, + air_density: float, + yaw_angles: NDArrayFloat, + tilt_angles: NDArrayFloat, + power_setpoints: NDArrayFloat, + tilt_interp: NDArrayObject, + average_method: str = "cubic-mean", + cubature_weights: NDArrayFloat | None = None, + correct_cp_ct_for_tilt: bool = False, + **_ # <- Allows other models to accept other keyword arguments + ): + # # Construct power interpolant + # power_interpolator = interp1d( + # power_thrust_table["wind_speed"], + # power_thrust_table["power"], + # fill_value=0.0, + # bounds_error=False, + # ) + + # sign convention. in the tum model, negative tilt creates tower clearance + tilt_angles = -tilt_angles + + # Compute the power-effective wind speed across the rotor + rotor_average_velocities = average_velocity( + velocities=velocities, + method=average_method, + cubature_weights=cubature_weights, + ) + + rotor_effective_velocities = rotor_velocity_air_density_correction( + velocities=rotor_average_velocities, + air_density=air_density, + ref_air_density=power_thrust_table["ref_air_density"] + ) + + # Compute power + def get_ct(x,*data): + sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) + CD = np.cos(np.deg2rad(delta)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + SG = np.sin(np.deg2rad(gamma)) + a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + + 8*tsr**2))/12)/(2*np.pi) + + return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x + + def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) + a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) + SG = np.sin(np.deg2rad(gamma)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + CD = np.cos(np.deg2rad(delta)) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(ct/2))/2)) + + p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 + - (tsr*cd*np.pi*(CD**2*CG**2*SD**2*k**2 + 3*CD**2*SG**2*k**2 + - 8*CD*tsr*SG*k + 8*tsr**2))/16 + - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 + + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 + + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 + + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 + + ((CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2)) + /(4*sinMu**2)) + - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 + + ((CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi*(3*CG**2*SD**2 + + SG**2)) + /(24*sinMu**2)) + - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) + - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu) + )/(2*np.pi) + ) + return p + + num_rows, num_cols = tilt_angles.shape + (average_velocity(velocities)) + + shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + + beta = power_thrust_table["beta"] + cd = power_thrust_table["cd"] + cl_alfa = power_thrust_table["cl_alfa"] + + sigma = power_thrust_table["rotor_solidity"] + R = power_thrust_table["rotor_diameter"]/2 + + air_density = power_thrust_table["ref_air_density"] + + pitch_out, tsr_out = TUMLossTurbine.control_trajectory( + rotor_effective_velocities, + yaw_angles, + tilt_angles, + air_density, + R, + shear, + sigma, + cd, + cl_alfa, + beta, + power_setpoints, + power_thrust_table + ) + + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + tsr_array = (tsr_out) + theta_array = (np.deg2rad(pitch_out+beta)) + + x0 = 0.2 + + p = np.zeros_like(average_velocity(velocities)) + + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + k = shear[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + # Break below command over multiple lines + data = ( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + k[j], + cMu[j], + sMu[j], + (tsr_array[i,j]), + (theta_array[i,j]), + R, + Mu[j] + ) + ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) + if ier == 1: + p[i,j] = np.squeeze(find_cp( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + k[j], + cMu[j], + sMu[j], + (tsr_array[i,j]), + (theta_array[i,j]), + R, + Mu[j], + ct + )) + else: + p[i,j] = -1e3 + + ############################################################################ + + yaw_angles = np.zeros_like(yaw_angles) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + + p0 = np.zeros_like((average_velocity(velocities))) + + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + k = shear[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),R,Mu[j]) + ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) + if ier == 1: + p0[i,j] = np.squeeze(find_cp( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + k[j], + cMu[j], + sMu[j], + (tsr_array[i,j]), + (theta_array[i,j]), + R, + Mu[j], + ct + )) + else: + p0[i,j] = -1e3 + + ratio = p/p0 + + ############################################################################ + + pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] + lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" + LUT = np.load(lut_file) + cp_i = LUT['cp_lut'] + pitch_i = LUT['pitch_lut'] + tsr_i = LUT['tsr_lut'] + interp_lut = RegularGridInterpolator( + (tsr_i,pitch_i), + cp_i, + bounds_error=False, + fill_value=None + ) + + power_coefficient = np.zeros_like(average_velocity(velocities)) + for i in np.arange(num_rows): + for j in np.arange(num_cols): + cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') + power_coefficient[i,j] = np.squeeze(cp_interp*ratio[i,j]) + + # TODO: make printout optional? + if False: + print('Tip speed ratio' + str(tsr_array)) + print('Pitch out: ' + str(pitch_out)) + power = ( + 0.5*air_density*(rotor_effective_velocities)**3*np.pi*R**2 + *(power_coefficient)*power_thrust_table["generator_efficiency"] + ) + return power + + def thrust_coefficient( + power_thrust_table: dict, + velocities: NDArrayFloat, + yaw_angles: NDArrayFloat, + tilt_angles: NDArrayFloat, + power_setpoints: NDArrayFloat, + tilt_interp: NDArrayObject, + average_method: str = "cubic-mean", + cubature_weights: NDArrayFloat | None = None, + correct_cp_ct_for_tilt: bool = False, + **_ # <- Allows other models to accept other keyword arguments + ): + + # sign convention. in the tum model, negative tilt creates tower clearance + tilt_angles = -tilt_angles + + # Compute the effective wind speed across the rotor + rotor_average_velocities = average_velocity( + velocities=velocities, + method=average_method, + cubature_weights=cubature_weights, + ) + + + # Apply tilt and yaw corrections + # Compute the tilt, if using floating turbines + old_tilt_angles = copy.deepcopy(tilt_angles) + tilt_angles = compute_tilt_angles_for_floating_turbines( + tilt_angles=tilt_angles, + tilt_interp=tilt_interp, + rotor_effective_velocities=rotor_average_velocities, + ) + # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) + tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) + + def get_ct(x,*data): + sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) + CD = np.cos(np.deg2rad(delta)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + SG = np.sin(np.deg2rad(gamma)) + a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + + 8*tsr**2))/12)/(2*np.pi) + + return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x + + beta = power_thrust_table["beta"] + cd = power_thrust_table["cd"] + cl_alfa = power_thrust_table["cl_alfa"] + + sigma = power_thrust_table["rotor_solidity"] + R = power_thrust_table["rotor_diameter"]/2 + + shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + + air_density = power_thrust_table["ref_air_density"] # CHANGE + + rotor_effective_velocities = rotor_velocity_air_density_correction( + velocities=rotor_average_velocities, + air_density=air_density, + ref_air_density=power_thrust_table["ref_air_density"] + ) + + pitch_out, tsr_out = TUMLossTurbine.control_trajectory( + rotor_effective_velocities, + yaw_angles, + tilt_angles, + air_density, + R, + shear, + sigma, + cd, + cl_alfa, + beta, + power_setpoints, + power_thrust_table + ) + + num_rows, num_cols = tilt_angles.shape + + (average_velocity(velocities)) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + # u = np.squeeze(u) + theta_array = (np.deg2rad(pitch_out+beta)) + tsr_array = (tsr_out) + + x0 = 0.2 + + thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),R,Mu[j]) + ct = fsolve(get_ct, x0,args=data) + thrust_coefficient1[i,j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) + + + yaw_angles = np.zeros_like(yaw_angles) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + + thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) + + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),R,Mu[j]) + ct = fsolve(get_ct, x0,args=data) + thrust_coefficient0[i,j] = np.squeeze(ct) #np.clip(ct, 0.0001, 0.9999) + + ############################################################################ + + ratio = thrust_coefficient1/thrust_coefficient0 + + pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] + lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" + LUT = np.load(lut_file) + ct_i = LUT['ct_lut'] + pitch_i = LUT['pitch_lut'] + tsr_i = LUT['tsr_lut'] + interp_lut = RegularGridInterpolator( + (tsr_i,pitch_i), + ct_i, + bounds_error=False, + fill_value=None + )#*0.9722085500886761) + + + thrust_coefficient = np.zeros_like(average_velocity(velocities)) + + for i in np.arange(num_rows): + for j in np.arange(num_cols): + ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') + thrust_coefficient[i,j] = np.squeeze(ct_interp*ratio[i,j]) + + return thrust_coefficient + + def axial_induction( + power_thrust_table: dict, + velocities: NDArrayFloat, + yaw_angles: NDArrayFloat, + tilt_angles: NDArrayFloat, + power_setpoints: NDArrayFloat, + tilt_interp: NDArrayObject, + average_method: str = "cubic-mean", + cubature_weights: NDArrayFloat | None = None, + correct_cp_ct_for_tilt: bool = False, + **_ # <- Allows other models to accept other keyword arguments + ): + + # sign convention. in the tum model, negative tilt creates tower clearance + tilt_angles = -tilt_angles + + # Compute the effective wind speed across the rotor + rotor_average_velocities = average_velocity( + velocities=velocities, + method=average_method, + cubature_weights=cubature_weights, + ) + + + # Apply tilt and yaw corrections + # Compute the tilt, if using floating turbines + old_tilt_angles = copy.deepcopy(tilt_angles) + tilt_angles = compute_tilt_angles_for_floating_turbines( + tilt_angles=tilt_angles, + tilt_interp=tilt_interp, + rotor_effective_velocities=rotor_average_velocities, + ) + # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) + tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) + + def get_ct(x,*data): + sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) + CD = np.cos(np.deg2rad(delta)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + SG = np.sin(np.deg2rad(gamma)) + a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + + 8*tsr**2))/12)/(2*np.pi) + + return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x + + beta = power_thrust_table["beta"] + cd = power_thrust_table["cd"] + cl_alfa = power_thrust_table["cl_alfa"] + + sigma = power_thrust_table["rotor_solidity"] + R = power_thrust_table["rotor_diameter"]/2 + + shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + + air_density = power_thrust_table["ref_air_density"] # CHANGE + + rotor_effective_velocities = rotor_velocity_air_density_correction( + velocities=rotor_average_velocities, + air_density=air_density, + ref_air_density=power_thrust_table["ref_air_density"] + ) + + pitch_out, tsr_out = TUMLossTurbine.control_trajectory( + rotor_effective_velocities, + yaw_angles, + tilt_angles, + air_density, + R, + shear, + sigma, + cd, + cl_alfa, + beta, + power_setpoints, + power_thrust_table + ) + + num_rows, num_cols = tilt_angles.shape + + (average_velocity(velocities)) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + # u = np.squeeze(u) + theta_array = (np.deg2rad(pitch_out+beta)) + tsr_array = (tsr_out) + + x0 = 0.2 + + thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),R,Mu[j]) + ct = fsolve(get_ct, x0,args=data) + thrust_coefficient1[i,j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) + + + yaw_angles = np.zeros_like(yaw_angles) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + + thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) + + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),R,Mu[j]) + ct = fsolve(get_ct, x0,args=data) + thrust_coefficient0[i,j] = np.squeeze(ct) #np.clip(ct, 0.0001, 0.9999) + + ############################################################################ + + ratio = thrust_coefficient1/thrust_coefficient0 + + pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] + lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" + LUT = np.load(lut_file) + ct_i = LUT['ct_lut'] + pitch_i = LUT['pitch_lut'] + tsr_i = LUT['tsr_lut'] + interp_lut = RegularGridInterpolator( + (tsr_i,pitch_i), + ct_i, + bounds_error=False, + fill_value=None + )#*0.9722085500886761) + + axial_induction = np.zeros_like(average_velocity(velocities)) + + for i in np.arange(num_rows): + for j in np.arange(num_cols): + ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') + ct = ct_interp*ratio[i,j] + a = (1- ( (1+np.sqrt(1-ct-1/16*ct**2*sMu[j]**2))/(2*(1+1/16*ct*sMu[j]**2))) ) + axial_induction[i,j] = np.squeeze(np.clip(a, 0.0001, 0.9999)) + + return axial_induction diff --git a/tests/tum_operation_model_unit_test.py b/tests/tum_operation_model_unit_test.py new file mode 100644 index 000000000..df5f89bcc --- /dev/null +++ b/tests/tum_operation_model_unit_test.py @@ -0,0 +1,129 @@ +import numpy as np +import pytest + +from floris.core.turbine.operation_models import POWER_SETPOINT_DEFAULT +from floris.core.turbine.tum_operation_model import TUMLossTurbine +from floris.utilities import cosd +from tests.conftest import SampleInputs, WIND_SPEEDS + + +def test_submodel_attributes(): + + assert hasattr(TUMLossTurbine, "power") + assert hasattr(TUMLossTurbine, "thrust_coefficient") + assert hasattr(TUMLossTurbine, "axial_induction") + +def test_TUMLossTurbine(): + + # NOTE: These tests should be updated to reflect actual expected behavior + # of the TUMLossTurbine model. Currently, match the CosineLossTurbine model. + + n_turbines = 1 + wind_speed = 10.0 + turbine_data = SampleInputs().turbine + turbine_data["power_thrust_table"] = SampleInputs().tum_loss_turbine_power_thrust_table + + yaw_angles_nom = 0 * np.ones((1, n_turbines)) + tilt_angles_nom = turbine_data["power_thrust_table"]["ref_tilt"] * np.ones((1, n_turbines)) + power_setpoints_nom = POWER_SETPOINT_DEFAULT * np.ones((1, n_turbines)) + yaw_angles_test = 20 * np.ones((1, n_turbines)) + tilt_angles_test = 0 * np.ones((1, n_turbines)) + + + # Check that power works as expected + TUMLossTurbine.power( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=turbine_data["power_thrust_table"]["ref_air_density"], # Matches ref_air_density + yaw_angles=yaw_angles_nom, + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ) + # truth_index = turbine_data["power_thrust_table"]["wind_speed"].index(wind_speed) + # baseline_power = turbine_data["power_thrust_table"]["power"][truth_index] * 1000 + # assert np.allclose(baseline_power, test_power) + + # Check that yaw and tilt angle have an effect + TUMLossTurbine.power( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=turbine_data["power_thrust_table"]["ref_air_density"], # Matches ref_air_density + yaw_angles=yaw_angles_test, + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_test, + tilt_interp=None + ) + #assert test_power < baseline_power + + # Check that a lower air density decreases power appropriately + TUMLossTurbine.power( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, + yaw_angles=yaw_angles_nom, + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ) + #assert test_power < baseline_power + + + # Check that thrust coefficient works as expected + TUMLossTurbine.thrust_coefficient( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, # Unused + yaw_angles=yaw_angles_nom, + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ) + #baseline_Ct = turbine_data["power_thrust_table"]["thrust_coefficient"][truth_index] + #assert np.allclose(baseline_Ct, test_Ct) + + # Check that yaw and tilt angle have the expected effect + TUMLossTurbine.thrust_coefficient( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, # Unused + yaw_angles=yaw_angles_test, + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_test, + tilt_interp=None + ) + #absolute_tilt = tilt_angles_test - turbine_data["power_thrust_table"]["ref_tilt"] + #assert test_Ct == baseline_Ct * cosd(yaw_angles_test) * cosd(absolute_tilt) + + + # Check that thrust coefficient works as expected + TUMLossTurbine.axial_induction( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, # Unused + yaw_angles=yaw_angles_nom, + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ) + ( + cosd(yaw_angles_nom) + * cosd(tilt_angles_nom - turbine_data["power_thrust_table"]["ref_tilt"]) + ) + # baseline_ai = ( + # 1 - np.sqrt(1 - turbine_data["power_thrust_table"]["thrust_coefficient"][truth_index]) + # ) / 2 / baseline_misalignment_loss + # assert np.allclose(baseline_ai, test_ai) + + # Check that yaw and tilt angle have the expected effect + TUMLossTurbine.axial_induction( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, # Unused + yaw_angles=yaw_angles_test, + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_test, + tilt_interp=None + ) + tilt_angles_test - turbine_data["power_thrust_table"]["ref_tilt"] + #assert test_Ct == baseline_Ct * cosd(yaw_angles_test) * cosd(absolute_tilt) diff --git a/tests/turbine_operation_models_unit_test.py b/tests/turbine_operation_models_unit_test.py index aac1e19e4..b50aab54b 100644 --- a/tests/turbine_operation_models_unit_test.py +++ b/tests/turbine_operation_models_unit_test.py @@ -9,7 +9,6 @@ POWER_SETPOINT_DEFAULT, SimpleDeratingTurbine, SimpleTurbine, - TUMLossTurbine, ) from floris.utilities import cosd from tests.conftest import SampleInputs, WIND_SPEEDS @@ -33,9 +32,6 @@ def test_submodel_attributes(): assert hasattr(MixedOperationTurbine, "thrust_coefficient") assert hasattr(MixedOperationTurbine, "axial_induction") - assert hasattr(TUMLossTurbine, "power") - assert hasattr(TUMLossTurbine, "thrust_coefficient") - assert hasattr(TUMLossTurbine, "axial_induction") assert hasattr(AWCTurbine, "power") assert hasattr(AWCTurbine, "thrust_coefficient") assert hasattr(AWCTurbine, "axial_induction") @@ -496,120 +492,6 @@ def test_MixedOperationTurbine(): tilt_interp=None ) -def test_TUMLossTurbine(): - - # NOTE: These tests should be updated to reflect actual expected behavior - # of the TUMLossTurbine model. Currently, match the CosineLossTurbine model. - - n_turbines = 1 - wind_speed = 10.0 - turbine_data = SampleInputs().turbine - turbine_data["power_thrust_table"] = SampleInputs().tum_loss_turbine_power_thrust_table - - yaw_angles_nom = 0 * np.ones((1, n_turbines)) - tilt_angles_nom = turbine_data["power_thrust_table"]["ref_tilt"] * np.ones((1, n_turbines)) - power_setpoints_nom = POWER_SETPOINT_DEFAULT * np.ones((1, n_turbines)) - yaw_angles_test = 20 * np.ones((1, n_turbines)) - tilt_angles_test = 0 * np.ones((1, n_turbines)) - - - # Check that power works as expected - TUMLossTurbine.power( - power_thrust_table=turbine_data["power_thrust_table"], - velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid - air_density=turbine_data["power_thrust_table"]["ref_air_density"], # Matches ref_air_density - yaw_angles=yaw_angles_nom, - power_setpoints=power_setpoints_nom, - tilt_angles=tilt_angles_nom, - tilt_interp=None - ) - # truth_index = turbine_data["power_thrust_table"]["wind_speed"].index(wind_speed) - # baseline_power = turbine_data["power_thrust_table"]["power"][truth_index] * 1000 - # assert np.allclose(baseline_power, test_power) - - # Check that yaw and tilt angle have an effect - TUMLossTurbine.power( - power_thrust_table=turbine_data["power_thrust_table"], - velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid - air_density=turbine_data["power_thrust_table"]["ref_air_density"], # Matches ref_air_density - yaw_angles=yaw_angles_test, - power_setpoints=power_setpoints_nom, - tilt_angles=tilt_angles_test, - tilt_interp=None - ) - #assert test_power < baseline_power - - # Check that a lower air density decreases power appropriately - TUMLossTurbine.power( - power_thrust_table=turbine_data["power_thrust_table"], - velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid - air_density=1.1, - yaw_angles=yaw_angles_nom, - power_setpoints=power_setpoints_nom, - tilt_angles=tilt_angles_nom, - tilt_interp=None - ) - #assert test_power < baseline_power - - - # Check that thrust coefficient works as expected - TUMLossTurbine.thrust_coefficient( - power_thrust_table=turbine_data["power_thrust_table"], - velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid - air_density=1.1, # Unused - yaw_angles=yaw_angles_nom, - power_setpoints=power_setpoints_nom, - tilt_angles=tilt_angles_nom, - tilt_interp=None - ) - #baseline_Ct = turbine_data["power_thrust_table"]["thrust_coefficient"][truth_index] - #assert np.allclose(baseline_Ct, test_Ct) - - # Check that yaw and tilt angle have the expected effect - TUMLossTurbine.thrust_coefficient( - power_thrust_table=turbine_data["power_thrust_table"], - velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid - air_density=1.1, # Unused - yaw_angles=yaw_angles_test, - power_setpoints=power_setpoints_nom, - tilt_angles=tilt_angles_test, - tilt_interp=None - ) - #absolute_tilt = tilt_angles_test - turbine_data["power_thrust_table"]["ref_tilt"] - #assert test_Ct == baseline_Ct * cosd(yaw_angles_test) * cosd(absolute_tilt) - - - # Check that thrust coefficient works as expected - TUMLossTurbine.axial_induction( - power_thrust_table=turbine_data["power_thrust_table"], - velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid - air_density=1.1, # Unused - yaw_angles=yaw_angles_nom, - power_setpoints=power_setpoints_nom, - tilt_angles=tilt_angles_nom, - tilt_interp=None - ) - ( - cosd(yaw_angles_nom) - * cosd(tilt_angles_nom - turbine_data["power_thrust_table"]["ref_tilt"]) - ) - # baseline_ai = ( - # 1 - np.sqrt(1 - turbine_data["power_thrust_table"]["thrust_coefficient"][truth_index]) - # ) / 2 / baseline_misalignment_loss - # assert np.allclose(baseline_ai, test_ai) - - # Check that yaw and tilt angle have the expected effect - TUMLossTurbine.axial_induction( - power_thrust_table=turbine_data["power_thrust_table"], - velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid - air_density=1.1, # Unused - yaw_angles=yaw_angles_test, - power_setpoints=power_setpoints_nom, - tilt_angles=tilt_angles_test, - tilt_interp=None - ) - tilt_angles_test - turbine_data["power_thrust_table"]["ref_tilt"] - #assert test_Ct == baseline_Ct * cosd(yaw_angles_test) * cosd(absolute_tilt) def test_AWCTurbine(): n_turbines = 1 From 1c8b4c33ffabdf8f1c25d77e16561ad310798895 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 11:44:50 -0700 Subject: [PATCH 32/53] Remove tum_rotor_velocity_yaw_correction, since it is not used and appears identical to rotor_velocity_yaw_cosine_correction. --- floris/core/rotor_velocity.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/floris/core/rotor_velocity.py b/floris/core/rotor_velocity.py index fcb30f572..43d4e3077 100644 --- a/floris/core/rotor_velocity.py +++ b/floris/core/rotor_velocity.py @@ -54,18 +54,6 @@ def rotor_velocity_tilt_cosine_correction( ) return rotor_effective_velocities -def tum_rotor_velocity_yaw_correction( - pP: float, - yaw_angles: NDArrayFloat, - rotor_effective_velocities: NDArrayFloat, -) -> NDArrayFloat: - # Compute the rotor effective velocity adjusting for yaw settings - pW = pP / 3.0 # Convert from pP to w - # TODO: cosine loss hard coded - rotor_effective_velocities = rotor_effective_velocities * cosd(yaw_angles) ** pW - - return rotor_effective_velocities - def simple_mean(array, axis=0): return np.mean(array, axis=axis) From adf35cb4866465d30f4f3d0a86ba2fa1a14a23fc Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 12:27:25 -0700 Subject: [PATCH 33/53] Add reg test for development purposes. --- tests/tum_operation_model_unit_test.py | 125 +++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/tests/tum_operation_model_unit_test.py b/tests/tum_operation_model_unit_test.py index df5f89bcc..cc3578e22 100644 --- a/tests/tum_operation_model_unit_test.py +++ b/tests/tum_operation_model_unit_test.py @@ -127,3 +127,128 @@ def test_TUMLossTurbine(): ) tilt_angles_test - turbine_data["power_thrust_table"]["ref_tilt"] #assert test_Ct == baseline_Ct * cosd(yaw_angles_test) * cosd(absolute_tilt) + +def test_TUMLossTurbine_regression(): + """ + Adding a regression test so that we can work with the model and stay confident that results + are not changing. + """ + + n_turbines = 1 + wind_speed = 10.0 + turbine_data = SampleInputs().turbine + turbine_data["power_thrust_table"] = SampleInputs().tum_loss_turbine_power_thrust_table + + N_test = 20 + tilt_angles_nom = turbine_data["power_thrust_table"]["ref_tilt"] * np.ones((N_test, n_turbines)) + power_setpoints_nom = POWER_SETPOINT_DEFAULT * np.ones((N_test, n_turbines)) + + yaw_max = 30 # Maximum yaw to test + yaw_angles_test = np.linspace(-yaw_max, yaw_max, N_test).reshape(-1,1) + + power_base = np.array([ + 2393927.66234718, + 2540214.34341772, + 2669081.37915352, + 2781432.03897903, + 2878018.91106399, + 2958794.44370780, + 3023377.59580264, + 3071678.43043693, + 3103774.85736505, + 3119784.56415652, + 3119784.56415652, + 3103774.85736505, + 3071678.43043693, + 3023377.59580264, + 2958794.44370780, + 2878018.91106399, + 2781432.03897903, + 2669081.37915352, + 2540214.34341772, + 2393927.66234718, + ]) + + thrust_coefficient_base = np.array([ + 0.65242964, + 0.68226378, + 0.70804882, + 0.73026438, + 0.74577423, + 0.75596131, + 0.76411955, + 0.77024367, + 0.77432917, + 0.77637278, + 0.77637278, + 0.77432917, + 0.77024367, + 0.76411955, + 0.75596131, + 0.74577423, + 0.73026438, + 0.70804882, + 0.68226378, + 0.65242964, + ]) + + axial_induction_base = np.array([ + 0.20555629, + 0.21851069, + 0.23020638, + 0.24070476, + 0.24829308, + 0.25340393, + 0.25757444, + 0.26075276, + 0.26289671, + 0.26397642, + 0.26397642, + 0.26289671, + 0.26075276, + 0.25757444, + 0.25340393, + 0.24829308, + 0.24070476, + 0.23020638, + 0.21851069, + 0.20555629, + ]) + + power = TUMLossTurbine.power( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((N_test, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, + yaw_angles=yaw_angles_test, + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ).squeeze() + + thrust_coefficient = TUMLossTurbine.thrust_coefficient( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((N_test, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, + yaw_angles=yaw_angles_test, + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ).squeeze() + + axial_induction = TUMLossTurbine.axial_induction( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speed * np.ones((N_test, n_turbines, 3, 3)), # 1 findex, 1 turbine, 3x3 grid + air_density=1.1, + yaw_angles=yaw_angles_test, + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ).squeeze() + + # print(power) + # print(thrust_coefficient) + # print(axial_induction) + + assert np.allclose(power, power_base) + assert np.allclose(thrust_coefficient, thrust_coefficient_base) + assert np.allclose(axial_induction, axial_induction_base) From 97c42a5e85b872fa27fbdf521cfa543156395e0b Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 12:59:53 -0700 Subject: [PATCH 34/53] Separate find_cp and get_ct functions. --- floris/core/turbine/tum_operation_model.py | 275 +++++++-------------- 1 file changed, 91 insertions(+), 184 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index bd2b1d428..5af1994f0 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -48,6 +48,10 @@ class TUMLossTurbine(BaseOperationModel): """ def compute_local_vertical_shear(velocities,avg_velocities): + """ + Called to evaluate the vertical (linear) shear that each rotor experience, based on the + inflow velocity. This allows to make the power curve asymmetric w.r.t. yaw misalignment. + """ num_rows, num_cols = avg_velocities.shape shear = np.zeros_like(avg_velocities) for i in np.arange(num_rows): @@ -81,6 +85,12 @@ def control_trajectory( power_setpoints, power_thrust_table ): + """ + Determines the tip-speed-ratio and pitch angles that occur in operation. This routine + assumes a standard region 2 control approach (i.e. k*rpm^2) and a region 3. Also + region 2-1/2 is considered. In the future, different control strategies could be included, + even user-defined. + """ if power_setpoints is None: power_demanded = ( np.ones_like(tilt_angles)*power_thrust_table["rated_power"] @@ -89,64 +99,6 @@ def control_trajectory( else: power_demanded = power_setpoints/power_thrust_table["generator_efficiency"] - def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): - #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) - SG = np.sin(np.deg2rad(gamma)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - CD = np.cos(np.deg2rad(delta)) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(ct/2))/2)) - - p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 - - (tsr*cd*np.pi*( - CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - - 8*CD*tsr*SG*k + 8*tsr**2 - ))/16 - - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 - + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 - + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 - + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 - + ((CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2)) - /(4*sinMu**2)) - - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 - + ((CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi - *(3*CG**2*SD**2 + SG**2)) - /(24*sinMu**2)) - - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) - - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu) - )/(2*np.pi)) - return p - - - def get_ct(x,*data): - sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU = data - #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - CD = np.cos(np.deg2rad(delta)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - SG = np.sin(np.deg2rad(gamma)) - a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) - + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k - + 8*tsr**2))/12)/(2*np.pi) - - return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x - ## Define function to get tip speed ratio def get_tsr(x,*data): @@ -282,9 +234,10 @@ def get_pitch(x,*data): y = aero_pow - electric_pow return y + # TODO: generalize .npz file loading pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" - LUT = np.load(lut_file) + LUT = np.load(lut_file) cp_i = LUT['cp_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] @@ -294,12 +247,13 @@ def get_pitch(x,*data): pitch_opt = pitch_i[idx[1]] max_cp = cp_i[idx[0],idx[1]] - omega_cut_in = 0 # RPM - omega_max = power_thrust_table["rated_rpm"] # RPM - rated_power_aero = ( - power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] # MW + omega_cut_in = 0 # RPM + omega_max = power_thrust_table["rated_rpm"] # RPM + rated_power_aero = ( + power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] # MW ) - #%% Compute torque-rpm relation and check for region 2-and-a-half + + # Compute torque-rpm relation and check for region 2-and-a-half Region2andAhalf = False omega_array = np.linspace(omega_cut_in,omega_max,161)*np.pi/30 # rad/s @@ -307,10 +261,11 @@ def get_pitch(x,*data): Paero_array = Q*omega_array - if Paero_array[-1] < rated_power_aero: # then we have region 2and1/2 + if Paero_array[-1] < rated_power_aero: # then we have region 2-and-a-half Region2andAhalf = True Q_extra = rated_power_aero/(omega_max*np.pi/30) Q = np.append(Q,Q_extra) + # TODO: Expression below is not assigned to anything. Should this be removed? (Paero_array[-1]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) omega_array = np.append(omega_array,omega_array[-1]) Paero_array = np.append(Paero_array,rated_power_aero) @@ -388,7 +343,8 @@ def get_pitch(x,*data): if pitch_out_soluzione < pitch_opt: pitch_out_soluzione = pitch_opt pitch_out0 = pitch_out_soluzione - #%% COMPUTE CP AND CT GIVEN THE PITCH AND TSR FOUND ABOVE + + # COMPUTE CP AND CT GIVEN THE PITCH AND TSR FOUND ABOVE pitch_out[i,j] = np.squeeze(pitch_out0) tsr_out[i,j] = tsr_outO @@ -407,15 +363,8 @@ def power( correct_cp_ct_for_tilt: bool = False, **_ # <- Allows other models to accept other keyword arguments ): - # # Construct power interpolant - # power_interpolator = interp1d( - # power_thrust_table["wind_speed"], - # power_thrust_table["power"], - # fill_value=0.0, - # bounds_error=False, - # ) - # sign convention. in the tum model, negative tilt creates tower clearance + # Sign convention: in the tum model, negative tilt creates tower clearance tilt_angles = -tilt_angles # Compute the power-effective wind speed across the rotor @@ -432,65 +381,11 @@ def power( ) # Compute power - def get_ct(x,*data): - sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data - #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - CD = np.cos(np.deg2rad(delta)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - SG = np.sin(np.deg2rad(gamma)) - a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) - + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k - + 8*tsr**2))/12)/(2*np.pi) - - return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x - - def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): - #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) - SG = np.sin(np.deg2rad(gamma)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - CD = np.cos(np.deg2rad(delta)) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(ct/2))/2)) - - p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 - - (tsr*cd*np.pi*(CD**2*CG**2*SD**2*k**2 + 3*CD**2*SG**2*k**2 - - 8*CD*tsr*SG*k + 8*tsr**2))/16 - - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 - + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 - + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 - + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 - + ((CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2)) - /(4*sinMu**2)) - - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 - + ((CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi*(3*CG**2*SD**2 - + SG**2)) - /(24*sinMu**2)) - - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) - - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu) - )/(2*np.pi) - ) - return p num_rows, num_cols = tilt_angles.shape (average_velocity(velocities)) - shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + shear = TUMLossTurbine.compute_local_vertical_shear(velocities, average_velocity(velocities)) beta = power_thrust_table["beta"] cd = power_thrust_table["cd"] @@ -534,7 +429,7 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): sMu = sinMu[i,:] Mu = MU[i,:] for j in np.arange(num_cols): - # Break below command over multiple lines + # Create data tuple for fsolve data = ( sigma, cd, @@ -546,10 +441,9 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): sMu[j], (tsr_array[i,j]), (theta_array[i,j]), - R, Mu[j] ) - ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) + ct, info, ier, msg = fsolve(get_ct, x0, args=data, full_output=True) if ier == 1: p[i,j] = np.squeeze(find_cp( sigma, @@ -562,7 +456,6 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): sMu[j], (tsr_array[i,j]), (theta_array[i,j]), - R, Mu[j], ct )) @@ -587,7 +480,7 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): Mu = MU[i,:] for j in np.arange(num_cols): data = (sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),R,Mu[j]) + (theta_array[i,j]),Mu[j]) ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) if ier == 1: p0[i,j] = np.squeeze(find_cp( @@ -601,7 +494,6 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU,ct): sMu[j], (tsr_array[i,j]), (theta_array[i,j]), - R, Mu[j], ct )) @@ -676,27 +568,6 @@ def thrust_coefficient( # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) - def get_ct(x,*data): - sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data - #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - CD = np.cos(np.deg2rad(delta)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - SG = np.sin(np.deg2rad(gamma)) - a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) - + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k - + 8*tsr**2))/12)/(2*np.pi) - - return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x - beta = power_thrust_table["beta"] cd = power_thrust_table["cd"] cl_alfa = power_thrust_table["cl_alfa"] @@ -750,8 +621,8 @@ def get_ct(x,*data): Mu = MU[i,:] for j in np.arange(num_cols): data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),R,Mu[j]) - ct = fsolve(get_ct, x0,args=data) + (theta_array[i,j]),Mu[j]) + ct = fsolve(get_ct, x0, args=data) thrust_coefficient1[i,j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) @@ -770,8 +641,8 @@ def get_ct(x,*data): Mu = MU[i,:] for j in np.arange(num_cols): data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),R,Mu[j]) - ct = fsolve(get_ct, x0,args=data) + (theta_array[i,j]),Mu[j]) + ct = fsolve(get_ct, x0, args=data) thrust_coefficient0[i,j] = np.squeeze(ct) #np.clip(ct, 0.0001, 0.9999) ############################################################################ @@ -836,27 +707,6 @@ def axial_induction( # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) - def get_ct(x,*data): - sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,R,MU = data - #add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - CD = np.cos(np.deg2rad(delta)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - SG = np.sin(np.deg2rad(gamma)) - a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) - + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k - + 8*tsr**2))/12)/(2*np.pi) - - return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x - beta = power_thrust_table["beta"] cd = power_thrust_table["cd"] cl_alfa = power_thrust_table["cl_alfa"] @@ -910,7 +760,7 @@ def get_ct(x,*data): Mu = MU[i,:] for j in np.arange(num_cols): data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),R,Mu[j]) + (theta_array[i,j]),Mu[j]) ct = fsolve(get_ct, x0,args=data) thrust_coefficient1[i,j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) @@ -930,8 +780,8 @@ def get_ct(x,*data): Mu = MU[i,:] for j in np.arange(num_cols): data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),R,Mu[j]) - ct = fsolve(get_ct, x0,args=data) + (theta_array[i,j]),Mu[j]) + ct = fsolve(get_ct, x0, args=data) thrust_coefficient0[i,j] = np.squeeze(ct) #np.clip(ct, 0.0001, 0.9999) ############################################################################ @@ -961,3 +811,60 @@ def get_ct(x,*data): axial_induction[i,j] = np.squeeze(np.clip(a, 0.0001, 0.9999)) return axial_induction + +def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) + a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) + SG = np.sin(np.deg2rad(gamma)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + CD = np.cos(np.deg2rad(delta)) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(ct/2))/2)) + + p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 + - (tsr*cd*np.pi*( + CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 + - 8*CD*tsr*SG*k + 8*tsr**2 + ))/16 + - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 + + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 + + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 + + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 + + ((CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2)) + /(4*sinMu**2)) + - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 + + ((CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi + *(3*CG**2*SD**2 + SG**2)) + /(24*sinMu**2)) + - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) + - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu) + )/(2*np.pi)) + return p + +def get_ct(x,*data): + sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU = data + #add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) + CD = np.cos(np.deg2rad(delta)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + SG = np.sin(np.deg2rad(gamma)) + a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) + k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) + I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) + + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) + I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + + 8*tsr**2))/12)/(2*np.pi) + + return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x From 95c81b2c38616f582dadbf7e1e9b5ae643994c3d Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 13:33:01 -0700 Subject: [PATCH 35/53] Simplify axial_induction() by calling similar code in thrust_coefficient() --- floris/core/turbine/tum_operation_model.py | 132 +++------------------ 1 file changed, 17 insertions(+), 115 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index 5af1994f0..eb51c46eb 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -343,7 +343,7 @@ def get_pitch(x,*data): if pitch_out_soluzione < pitch_opt: pitch_out_soluzione = pitch_opt pitch_out0 = pitch_out_soluzione - + # COMPUTE CP AND CT GIVEN THE PITCH AND TSR FOUND ABOVE pitch_out[i,j] = np.squeeze(pitch_out0) tsr_out[i,j] = tsr_outO @@ -385,7 +385,10 @@ def power( num_rows, num_cols = tilt_angles.shape (average_velocity(velocities)) - shear = TUMLossTurbine.compute_local_vertical_shear(velocities, average_velocity(velocities)) + shear = TUMLossTurbine.compute_local_vertical_shear( + velocities, + average_velocity(velocities) + ) beta = power_thrust_table["beta"] cd = power_thrust_table["cd"] @@ -684,134 +687,33 @@ def axial_induction( correct_cp_ct_for_tilt: bool = False, **_ # <- Allows other models to accept other keyword arguments ): - - # sign convention. in the tum model, negative tilt creates tower clearance - tilt_angles = -tilt_angles - - # Compute the effective wind speed across the rotor - rotor_average_velocities = average_velocity( + num_rows, num_cols = tilt_angles.shape + thrust_coefficients = TUMLossTurbine.thrust_coefficient( + power_thrust_table=power_thrust_table, velocities=velocities, - method=average_method, - cubature_weights=cubature_weights, - ) - - - # Apply tilt and yaw corrections - # Compute the tilt, if using floating turbines - old_tilt_angles = copy.deepcopy(tilt_angles) - tilt_angles = compute_tilt_angles_for_floating_turbines( + yaw_angles=yaw_angles, tilt_angles=tilt_angles, + power_setpoints=power_setpoints, tilt_interp=tilt_interp, - rotor_effective_velocities=rotor_average_velocities, - ) - # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) - tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) - - beta = power_thrust_table["beta"] - cd = power_thrust_table["cd"] - cl_alfa = power_thrust_table["cl_alfa"] - - sigma = power_thrust_table["rotor_solidity"] - R = power_thrust_table["rotor_diameter"]/2 - - shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) - - air_density = power_thrust_table["ref_air_density"] # CHANGE - - rotor_effective_velocities = rotor_velocity_air_density_correction( - velocities=rotor_average_velocities, - air_density=air_density, - ref_air_density=power_thrust_table["ref_air_density"] - ) - - pitch_out, tsr_out = TUMLossTurbine.control_trajectory( - rotor_effective_velocities, - yaw_angles, - tilt_angles, - air_density, - R, - shear, - sigma, - cd, - cl_alfa, - beta, - power_setpoints, - power_thrust_table + average_method=average_method, + cubature_weights=cubature_weights, + correct_cp_ct_for_tilt=correct_cp_ct_for_tilt, ) - num_rows, num_cols = tilt_angles.shape - - (average_velocity(velocities)) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) - # u = np.squeeze(u) - theta_array = (np.deg2rad(pitch_out+beta)) - tsr_array = (tsr_out) - - x0 = 0.2 - - thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) - for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] - for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),Mu[j]) - ct = fsolve(get_ct, x0,args=data) - thrust_coefficient1[i,j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) - - yaw_angles = np.zeros_like(yaw_angles) MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) sinMu = (np.sin(MU)) + sMu = sinMu[-1,:] - thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) - - for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] - for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),Mu[j]) - ct = fsolve(get_ct, x0, args=data) - thrust_coefficient0[i,j] = np.squeeze(ct) #np.clip(ct, 0.0001, 0.9999) - - ############################################################################ - - ratio = thrust_coefficient1/thrust_coefficient0 - - pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" - LUT = np.load(lut_file) - ct_i = LUT['ct_lut'] - pitch_i = LUT['pitch_lut'] - tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator( - (tsr_i,pitch_i), - ct_i, - bounds_error=False, - fill_value=None - )#*0.9722085500886761) - - axial_induction = np.zeros_like(average_velocity(velocities)) - + axial_induction = np.zeros((num_rows, num_cols)) for i in np.arange(num_rows): for j in np.arange(num_cols): - ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - ct = ct_interp*ratio[i,j] + ct = thrust_coefficients[i,j] a = (1- ( (1+np.sqrt(1-ct-1/16*ct**2*sMu[j]**2))/(2*(1+1/16*ct*sMu[j]**2))) ) axial_induction[i,j] = np.squeeze(np.clip(a, 0.0001, 0.9999)) return axial_induction - + def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): #add a small misalignment in case MU = 0 to avoid division by 0 if MU == 0: From a521dcd3c76979c9140f6950ca20282e6809401f Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 14:56:58 -0700 Subject: [PATCH 36/53] Add further comments to power() and thrust_coefficient() methods. --- floris/core/turbine/tum_operation_model.py | 1089 ++++++++++---------- 1 file changed, 544 insertions(+), 545 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index eb51c46eb..d9eb413b9 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -47,32 +47,56 @@ class TUMLossTurbine(BaseOperationModel): TODO: Should the turbine submodels each implement axial_induction()? """ - def compute_local_vertical_shear(velocities,avg_velocities): - """ - Called to evaluate the vertical (linear) shear that each rotor experience, based on the - inflow velocity. This allows to make the power curve asymmetric w.r.t. yaw misalignment. - """ - num_rows, num_cols = avg_velocities.shape - shear = np.zeros_like(avg_velocities) - for i in np.arange(num_rows): - for j in np.arange(num_cols): - mean_speed = np.mean(velocities[i,j,:,:],axis=0) - if len(mean_speed) % 2 != 0: # odd number - u_u_hh = mean_speed/mean_speed[int(np.floor(len(mean_speed)/2))] - else: - u_u_hh = ( - mean_speed - /(mean_speed[int((len(mean_speed)/2))] - +mean_speed[int((len(mean_speed)/2))-1] - )/2 - ) - zg_R = np.linspace(-1,1,len(mean_speed)+2) - polifit_k = np.polyfit(zg_R[1:-1],1-u_u_hh,1) - shear[i,j] = -polifit_k[0] - return shear + def power( + power_thrust_table: dict, + velocities: NDArrayFloat, + air_density: float, + yaw_angles: NDArrayFloat, + tilt_angles: NDArrayFloat, + power_setpoints: NDArrayFloat, + tilt_interp: NDArrayObject, + average_method: str = "cubic-mean", + cubature_weights: NDArrayFloat | None = None, + correct_cp_ct_for_tilt: bool = False, + **_ # <- Allows other models to accept other keyword arguments + ): - def control_trajectory( - rotor_average_velocities, + # Sign convention: in the tum model, negative tilt creates tower clearance + tilt_angles = -tilt_angles + + # Compute the power-effective wind speed across the rotor + rotor_average_velocities = average_velocity( + velocities=velocities, + method=average_method, + cubature_weights=cubature_weights, + ) + + rotor_effective_velocities = rotor_velocity_air_density_correction( + velocities=rotor_average_velocities, + air_density=air_density, + ref_air_density=power_thrust_table["ref_air_density"] + ) + + # Compute power + + num_rows, num_cols = tilt_angles.shape + + shear = TUMLossTurbine.compute_local_vertical_shear( + velocities, + average_velocity(velocities) + ) + + beta = power_thrust_table["beta"] + cd = power_thrust_table["cd"] + cl_alfa = power_thrust_table["cl_alfa"] + + sigma = power_thrust_table["rotor_solidity"] + R = power_thrust_table["rotor_diameter"]/2 + + air_density = power_thrust_table["ref_air_density"] + + pitch_out, tsr_out = TUMLossTurbine.control_trajectory( + rotor_effective_velocities, yaw_angles, tilt_angles, air_density, @@ -84,276 +108,272 @@ def control_trajectory( beta, power_setpoints, power_thrust_table - ): - """ - Determines the tip-speed-ratio and pitch angles that occur in operation. This routine - assumes a standard region 2 control approach (i.e. k*rpm^2) and a region 3. Also - region 2-1/2 is considered. In the future, different control strategies could be included, - even user-defined. - """ - if power_setpoints is None: - power_demanded = ( - np.ones_like(tilt_angles)*power_thrust_table["rated_power"] - /power_thrust_table["generator_efficiency"] - ) - else: - power_demanded = power_setpoints/power_thrust_table["generator_efficiency"] - - - ## Define function to get tip speed ratio - def get_tsr(x,*data): - (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,pitch_in,omega_lut_pow, - torque_lut_omega,cp_i,pitch_i,tsr_i) = data - - omega_lut_torque = omega_lut_pow*np.pi/30 - - omega = x*u/R - omega_rpm = omega*30/np.pi - - torque_nm = np.interp(omega,omega_lut_torque,torque_lut_omega) - - mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),x, - np.deg2rad(pitch_in)+np.deg2rad(beta),mu) - x0 = 0.1 - [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) - cp = find_cp( - sigma, - cd, - cl_alfa, - gamma, - tilt, - shear, - np.cos(mu), - np.sin(mu), - x, - np.deg2rad(pitch_in)+np.deg2rad(beta), - mu, - ct - ) - - mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),x, - np.deg2rad(pitch_in)+np.deg2rad(beta),mu) - x0 = 0.1 - [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) - cp0 = find_cp( - sigma, - cd, - cl_alfa, - 0, - tilt, - shear, - np.cos(mu), - np.sin(mu), - x, - np.deg2rad(pitch_in)+np.deg2rad(beta), - mu, - ct - ) - - eta_p = cp/cp0 - - interp = RegularGridInterpolator((np.squeeze((tsr_i)), - np.squeeze((pitch_i))), cp_i, - bounds_error=False, fill_value=None) - - Cp_now = interp((x,pitch_in),method='cubic') - cp_g1 = Cp_now*eta_p - aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 - electric_pow = torque_nm*(omega_rpm*np.pi/30) - - y = aero_pow - electric_pow - return y - - ## Define function to get pitch angle - def get_pitch(x,*data): - (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,omega_rated,omega_lut_torque, - torque_lut_omega,cp_i,pitch_i,tsr_i) = data - - omega_rpm = omega_rated*30/np.pi - tsr = omega_rated*R/(u) - - pitch_in = np.deg2rad(x) - torque_nm = np.interp(omega_rpm,omega_lut_torque*30/np.pi,torque_lut_omega) + ) - mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),tsr, - (pitch_in)+np.deg2rad(beta),mu) - x0 = 0.1 - [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) - cp = find_cp( - sigma, - cd, - cl_alfa, - gamma, - tilt, - shear, - np.cos(mu), - np.sin(mu), - tsr, - (pitch_in)+np.deg2rad(beta), - mu, - ct - ) + tsr_array = (tsr_out) + theta_array = (np.deg2rad(pitch_out+beta)) + x0 = 0.2 - mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),tsr, - (pitch_in)+np.deg2rad(beta),mu) - x0 = 0.1 - [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) - cp0 = find_cp( - sigma, - cd, - cl_alfa, - 0, - tilt, - shear, - np.cos(mu), - np.sin(mu), - tsr, - (pitch_in)+np.deg2rad(beta), - mu, - ct - ) + # Compute power in yawed conditions + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + p = np.zeros_like(average_velocity(velocities)) + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + k = shear[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + # Create data tuple for fsolve + data = ( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + k[j], + cMu[j], + sMu[j], + (tsr_array[i,j]), + (theta_array[i,j]), + Mu[j] + ) + ct, info, ier, msg = fsolve(get_ct, x0, args=data, full_output=True) + if ier == 1: + p[i,j] = np.squeeze(find_cp( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + k[j], + cMu[j], + sMu[j], + (tsr_array[i,j]), + (theta_array[i,j]), + Mu[j], + ct + )) + else: + p[i,j] = -1e3 - eta_p = cp/cp0 + # Recompute power in non-yawed conditions + yaw_angles = np.zeros_like(yaw_angles) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) - interp = RegularGridInterpolator( - (np.squeeze((tsr_i)), np.squeeze((pitch_i))), - cp_i, - bounds_error=False, - fill_value=None - ) + p0 = np.zeros_like((average_velocity(velocities))) - Cp_now = interp((tsr,x),method='cubic') - cp_g1 = Cp_now*eta_p - aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 - electric_pow = torque_nm*(omega_rpm*np.pi/30) + for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + k = shear[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] + for j in np.arange(num_cols): + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),Mu[j]) + ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) + if ier == 1: + p0[i,j] = np.squeeze(find_cp( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + k[j], + cMu[j], + sMu[j], + (tsr_array[i,j]), + (theta_array[i,j]), + Mu[j], + ct + )) + else: + p0[i,j] = -1e3 - y = aero_pow - electric_pow - return y + # ratio of yawed to unyawed thrust coefficients + ratio = p/p0 - # TODO: generalize .npz file loading + # Load Cp surface data and construct interpolator + # TODO: remove hardcoding pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" - LUT = np.load(lut_file) + LUT = np.load(lut_file) cp_i = LUT['cp_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] - idx = np.squeeze(np.where(cp_i == np.max(cp_i))) + interp_lut = RegularGridInterpolator( + (tsr_i,pitch_i), + cp_i, + bounds_error=False, + fill_value=None + ) - tsr_opt = tsr_i[idx[0]] - pitch_opt = pitch_i[idx[1]] - max_cp = cp_i[idx[0],idx[1]] + power_coefficient = np.zeros_like(average_velocity(velocities)) + for i in np.arange(num_rows): + for j in np.arange(num_cols): + cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') + power_coefficient[i,j] = np.squeeze(cp_interp*ratio[i,j]) - omega_cut_in = 0 # RPM - omega_max = power_thrust_table["rated_rpm"] # RPM - rated_power_aero = ( - power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] # MW + # TODO: make printout optional? + if False: + print('Tip speed ratio' + str(tsr_array)) + print('Pitch out: ' + str(pitch_out)) + power = ( + 0.5*air_density*(rotor_effective_velocities)**3*np.pi*R**2 + *(power_coefficient)*power_thrust_table["generator_efficiency"] ) + return power - # Compute torque-rpm relation and check for region 2-and-a-half - Region2andAhalf = False + def thrust_coefficient( + power_thrust_table: dict, + velocities: NDArrayFloat, + yaw_angles: NDArrayFloat, + tilt_angles: NDArrayFloat, + power_setpoints: NDArrayFloat, + tilt_interp: NDArrayObject, + average_method: str = "cubic-mean", + cubature_weights: NDArrayFloat | None = None, + correct_cp_ct_for_tilt: bool = False, + **_ # <- Allows other models to accept other keyword arguments + ): - omega_array = np.linspace(omega_cut_in,omega_max,161)*np.pi/30 # rad/s - Q = (0.5*air_density*omega_array**2*R**5 * np.pi * max_cp ) / tsr_opt**3 + # sign convention. in the tum model, negative tilt creates tower clearance + tilt_angles = -tilt_angles - Paero_array = Q*omega_array + # Compute the effective wind speed across the rotor + rotor_average_velocities = average_velocity( + velocities=velocities, + method=average_method, + cubature_weights=cubature_weights, + ) - if Paero_array[-1] < rated_power_aero: # then we have region 2-and-a-half - Region2andAhalf = True - Q_extra = rated_power_aero/(omega_max*np.pi/30) - Q = np.append(Q,Q_extra) - # TODO: Expression below is not assigned to anything. Should this be removed? - (Paero_array[-1]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) - omega_array = np.append(omega_array,omega_array[-1]) - Paero_array = np.append(Paero_array,rated_power_aero) - else: # limit aero_power to the last Q*omega_max - rated_power_aero = Paero_array[-1] - u_rated = (rated_power_aero/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) - u_array = np.linspace(3,25,45) - idx = np.argmin(np.abs(u_array-u_rated)) - if u_rated > u_array[idx]: - u_array = np.insert(u_array,idx+1,u_rated) - else: - u_array = np.insert(u_array,idx,u_rated) + # Apply tilt and yaw corrections + # Compute the tilt, if using floating turbines + old_tilt_angles = copy.deepcopy(tilt_angles) + tilt_angles = compute_tilt_angles_for_floating_turbines( + tilt_angles=tilt_angles, + tilt_interp=tilt_interp, + rotor_effective_velocities=rotor_average_velocities, + ) + # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) + tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) - pow_lut_omega = Paero_array - omega_lut_pow = omega_array*30/np.pi - torque_lut_omega = Q - omega_lut_torque = omega_lut_pow + beta = power_thrust_table["beta"] + cd = power_thrust_table["cd"] + cl_alfa = power_thrust_table["cl_alfa"] + + sigma = power_thrust_table["rotor_solidity"] + R = power_thrust_table["rotor_diameter"]/2 + + shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + + air_density = power_thrust_table["ref_air_density"] # CHANGE + + rotor_effective_velocities = rotor_velocity_air_density_correction( + velocities=rotor_average_velocities, + air_density=air_density, + ref_air_density=power_thrust_table["ref_air_density"] + ) + + pitch_out, tsr_out = TUMLossTurbine.control_trajectory( + rotor_effective_velocities, + yaw_angles, + tilt_angles, + air_density, + R, + shear, + sigma, + cd, + cl_alfa, + beta, + power_setpoints, + power_thrust_table + ) num_rows, num_cols = tilt_angles.shape - omega_rated = np.zeros_like(rotor_average_velocities) - u_rated = np.zeros_like(rotor_average_velocities) + + # u = np.squeeze(u) + theta_array = (np.deg2rad(pitch_out+beta)) + tsr_array = (tsr_out) + x0 = 0.2 + + # Compute thrust coefficient in yawed conditions + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) for i in np.arange(num_rows): + yaw = yaw_angles[i,:] + tilt = tilt_angles[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] for j in np.arange(num_cols): - omega_rated[i,j] = ( - np.interp(power_demanded[i,j],pow_lut_omega,omega_lut_pow) - *np.pi/30 #rad/s - ) - u_rated[i,j] = (power_demanded[i,j]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),Mu[j]) + ct = fsolve(get_ct, x0, args=data) + thrust_coefficient1[i,j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) - pitch_out = np.zeros_like(rotor_average_velocities) - tsr_out = np.zeros_like(rotor_average_velocities) + # Recompute thrust coefficient in non-yawed conditions + yaw_angles = np.zeros_like(yaw_angles) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + cosMu = (np.cos(MU)) + sinMu = (np.sin(MU)) + + thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) for i in np.arange(num_rows): yaw = yaw_angles[i,:] tilt = tilt_angles[i,:] - k = shear[i,:] + cMu = cosMu[i,:] + sMu = sinMu[i,:] + Mu = MU[i,:] for j in np.arange(num_cols): - u_v = rotor_average_velocities[i,j] - if u_v > u_rated[i,j]: - tsr_v = omega_rated[i,j]*R/u_v*np.cos(np.deg2rad(yaw[j]))**0.5 - else: - tsr_v = tsr_opt*np.cos(np.deg2rad(yaw[j])) - if Region2andAhalf: # fix for interpolation - omega_lut_torque[-1] = omega_lut_torque[-1]+1e-2 - omega_lut_pow[-1] = omega_lut_pow[-1]+1e-2 - - data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v,pitch_opt, - omega_lut_pow,torque_lut_omega,cp_i,pitch_i,tsr_i) - [tsr_out_soluzione,infodict,ier,mesg] = fsolve( - get_tsr,tsr_v,args=data,full_output=True - ) - # check if solution was possible. If not, we are in region 3 - if (np.abs(infodict['fvec']) > 10 or tsr_out_soluzione < 4): - tsr_out_soluzione = 1000 + data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), + (theta_array[i,j]),Mu[j]) + ct = fsolve(get_ct, x0, args=data) + thrust_coefficient0[i,j] = np.squeeze(ct) #np.clip(ct, 0.0001, 0.9999) - # save solution - tsr_outO = tsr_out_soluzione - omega = tsr_outO*u_v/R + # Compute ratio of yawed to unyawed thrust coefficients + ratio = thrust_coefficient1/thrust_coefficient0 - # check if we are in region 2 or 3 - if omega < omega_rated[i,j]: # region 2 - # Define optimum pitch - pitch_out0 = pitch_opt + # Load Ct surface data and construct interpolator + # TODO: remove hardcoding + pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] + lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" + LUT = np.load(lut_file) + ct_i = LUT['ct_lut'] + pitch_i = LUT['pitch_lut'] + tsr_i = LUT['tsr_lut'] + interp_lut = RegularGridInterpolator( + (tsr_i,pitch_i), + ct_i, + bounds_error=False, + fill_value=None + )#*0.9722085500886761) - else: # region 3 - tsr_outO = omega_rated[i,j]*R/u_v - data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v, - omega_rated[i,j],omega_array,Q,cp_i,pitch_i,tsr_i) - # solve aero-electrical power balance with TSR from rated omega - [pitch_out_soluzione,infodict,ier,mesg] = fsolve( - get_pitch,u_v,args=data,factor=0.1,full_output=True, - xtol=1e-10,maxfev=2000) - if pitch_out_soluzione < pitch_opt: - pitch_out_soluzione = pitch_opt - pitch_out0 = pitch_out_soluzione - # COMPUTE CP AND CT GIVEN THE PITCH AND TSR FOUND ABOVE - pitch_out[i,j] = np.squeeze(pitch_out0) - tsr_out[i,j] = tsr_outO + # Interpolate and apply ratio to determine thrust coefficient + thrust_coefficient = np.zeros_like(average_velocity(velocities)) + for i in np.arange(num_rows): + for j in np.arange(num_cols): + ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') + thrust_coefficient[i,j] = np.squeeze(ct_interp*ratio[i,j]) - return pitch_out, tsr_out + return thrust_coefficient - def power( + def axial_induction( power_thrust_table: dict, velocities: NDArrayFloat, - air_density: float, yaw_angles: NDArrayFloat, tilt_angles: NDArrayFloat, power_setpoints: NDArrayFloat, @@ -363,44 +383,59 @@ def power( correct_cp_ct_for_tilt: bool = False, **_ # <- Allows other models to accept other keyword arguments ): - - # Sign convention: in the tum model, negative tilt creates tower clearance - tilt_angles = -tilt_angles - - # Compute the power-effective wind speed across the rotor - rotor_average_velocities = average_velocity( + num_rows, num_cols = tilt_angles.shape + thrust_coefficients = TUMLossTurbine.thrust_coefficient( + power_thrust_table=power_thrust_table, velocities=velocities, - method=average_method, + yaw_angles=yaw_angles, + tilt_angles=tilt_angles, + power_setpoints=power_setpoints, + tilt_interp=tilt_interp, + average_method=average_method, cubature_weights=cubature_weights, + correct_cp_ct_for_tilt=correct_cp_ct_for_tilt, ) - rotor_effective_velocities = rotor_velocity_air_density_correction( - velocities=rotor_average_velocities, - air_density=air_density, - ref_air_density=power_thrust_table["ref_air_density"] - ) - - # Compute power - - num_rows, num_cols = tilt_angles.shape - (average_velocity(velocities)) - - shear = TUMLossTurbine.compute_local_vertical_shear( - velocities, - average_velocity(velocities) - ) - - beta = power_thrust_table["beta"] - cd = power_thrust_table["cd"] - cl_alfa = power_thrust_table["cl_alfa"] + yaw_angles = np.zeros_like(yaw_angles) + MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) + sinMu = (np.sin(MU)) + sMu = sinMu[-1,:] - sigma = power_thrust_table["rotor_solidity"] - R = power_thrust_table["rotor_diameter"]/2 + axial_induction = np.zeros((num_rows, num_cols)) + for i in np.arange(num_rows): + for j in np.arange(num_cols): + ct = thrust_coefficients[i,j] + a = (1- ( (1+np.sqrt(1-ct-1/16*ct**2*sMu[j]**2))/(2*(1+1/16*ct*sMu[j]**2))) ) + axial_induction[i,j] = np.squeeze(np.clip(a, 0.0001, 0.9999)) - air_density = power_thrust_table["ref_air_density"] + return axial_induction + + def compute_local_vertical_shear(velocities, avg_velocities): + """ + Called to evaluate the vertical (linear) shear that each rotor experience, based on the + inflow velocity. This allows to make the power curve asymmetric w.r.t. yaw misalignment. + """ + num_rows, num_cols = avg_velocities.shape + shear = np.zeros_like(avg_velocities) + for i in np.arange(num_rows): + for j in np.arange(num_cols): + mean_speed = np.mean(velocities[i,j,:,:],axis=0) + if len(mean_speed) % 2 != 0: # odd number + u_u_hh = mean_speed/mean_speed[int(np.floor(len(mean_speed)/2))] + else: + u_u_hh = ( + mean_speed + /(mean_speed[int((len(mean_speed)/2))] + +mean_speed[int((len(mean_speed)/2))-1] + )/2 + ) + zg_R = np.linspace(-1,1,len(mean_speed)+2) + polifit_k = np.polyfit(zg_R[1:-1],1-u_u_hh,1) + shear[i,j] = -polifit_k[0] + return shear - pitch_out, tsr_out = TUMLossTurbine.control_trajectory( - rotor_effective_velocities, + def control_trajectory( + rotor_average_velocities, yaw_angles, tilt_angles, air_density, @@ -412,307 +447,271 @@ def power( beta, power_setpoints, power_thrust_table - ) + ): + """ + Determines the tip-speed-ratio and pitch angles that occur in operation. This routine + assumes a standard region 2 control approach (i.e. k*rpm^2) and a region 3. Also + region 2-1/2 is considered. In the future, different control strategies could be included, + even user-defined. + """ + if power_setpoints is None: + power_demanded = ( + np.ones_like(tilt_angles)*power_thrust_table["rated_power"] + /power_thrust_table["generator_efficiency"] + ) + else: + power_demanded = power_setpoints/power_thrust_table["generator_efficiency"] - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) - tsr_array = (tsr_out) - theta_array = (np.deg2rad(pitch_out+beta)) - x0 = 0.2 + ## Define function to get tip speed ratio + def get_tsr(x,*data): + (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,pitch_in,omega_lut_pow, + torque_lut_omega,cp_i,pitch_i,tsr_i) = data - p = np.zeros_like(average_velocity(velocities)) + omega_lut_torque = omega_lut_pow*np.pi/30 - for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - k = shear[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] - for j in np.arange(num_cols): - # Create data tuple for fsolve - data = ( - sigma, - cd, - cl_alfa, - yaw[j], - tilt[j], - k[j], - cMu[j], - sMu[j], - (tsr_array[i,j]), - (theta_array[i,j]), - Mu[j] - ) - ct, info, ier, msg = fsolve(get_ct, x0, args=data, full_output=True) - if ier == 1: - p[i,j] = np.squeeze(find_cp( - sigma, - cd, - cl_alfa, - yaw[j], - tilt[j], - k[j], - cMu[j], - sMu[j], - (tsr_array[i,j]), - (theta_array[i,j]), - Mu[j], - ct - )) - else: - p[i,j] = -1e3 + omega = x*u/R + omega_rpm = omega*30/np.pi - ############################################################################ + torque_nm = np.interp(omega,omega_lut_torque,torque_lut_omega) - yaw_angles = np.zeros_like(yaw_angles) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) + mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) + data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),x, + np.deg2rad(pitch_in)+np.deg2rad(beta),mu) + x0 = 0.1 + [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + cp = find_cp( + sigma, + cd, + cl_alfa, + gamma, + tilt, + shear, + np.cos(mu), + np.sin(mu), + x, + np.deg2rad(pitch_in)+np.deg2rad(beta), + mu, + ct + ) - p0 = np.zeros_like((average_velocity(velocities))) + mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) + data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),x, + np.deg2rad(pitch_in)+np.deg2rad(beta),mu) + x0 = 0.1 + [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + cp0 = find_cp( + sigma, + cd, + cl_alfa, + 0, + tilt, + shear, + np.cos(mu), + np.sin(mu), + x, + np.deg2rad(pitch_in)+np.deg2rad(beta), + mu, + ct + ) - for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - k = shear[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] - for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),Mu[j]) - ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) - if ier == 1: - p0[i,j] = np.squeeze(find_cp( - sigma, - cd, - cl_alfa, - yaw[j], - tilt[j], - k[j], - cMu[j], - sMu[j], - (tsr_array[i,j]), - (theta_array[i,j]), - Mu[j], - ct - )) - else: - p0[i,j] = -1e3 + eta_p = cp/cp0 - ratio = p/p0 + interp = RegularGridInterpolator((np.squeeze((tsr_i)), + np.squeeze((pitch_i))), cp_i, + bounds_error=False, fill_value=None) + + Cp_now = interp((x,pitch_in),method='cubic') + cp_g1 = Cp_now*eta_p + aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 + electric_pow = torque_nm*(omega_rpm*np.pi/30) + + y = aero_pow - electric_pow + return y + + ## Define function to get pitch angle + def get_pitch(x,*data): + (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,omega_rated,omega_lut_torque, + torque_lut_omega,cp_i,pitch_i,tsr_i) = data + + omega_rpm = omega_rated*30/np.pi + tsr = omega_rated*R/(u) + + pitch_in = np.deg2rad(x) + torque_nm = np.interp(omega_rpm,omega_lut_torque*30/np.pi,torque_lut_omega) + + mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) + data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),tsr, + (pitch_in)+np.deg2rad(beta),mu) + x0 = 0.1 + [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + cp = find_cp( + sigma, + cd, + cl_alfa, + gamma, + tilt, + shear, + np.cos(mu), + np.sin(mu), + tsr, + (pitch_in)+np.deg2rad(beta), + mu, + ct + ) + + mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) + data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),tsr, + (pitch_in)+np.deg2rad(beta),mu) + x0 = 0.1 + [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + cp0 = find_cp( + sigma, + cd, + cl_alfa, + 0, + tilt, + shear, + np.cos(mu), + np.sin(mu), + tsr, + (pitch_in)+np.deg2rad(beta), + mu, + ct + ) + + eta_p = cp/cp0 + + interp = RegularGridInterpolator( + (np.squeeze((tsr_i)), np.squeeze((pitch_i))), + cp_i, + bounds_error=False, + fill_value=None + ) - ############################################################################ + Cp_now = interp((tsr,x),method='cubic') + cp_g1 = Cp_now*eta_p + aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 + electric_pow = torque_nm*(omega_rpm*np.pi/30) + y = aero_pow - electric_pow + return y + + # TODO: generalize .npz file loading pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" - LUT = np.load(lut_file) + LUT = np.load(lut_file) cp_i = LUT['cp_lut'] pitch_i = LUT['pitch_lut'] tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator( - (tsr_i,pitch_i), - cp_i, - bounds_error=False, - fill_value=None - ) - - power_coefficient = np.zeros_like(average_velocity(velocities)) - for i in np.arange(num_rows): - for j in np.arange(num_cols): - cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - power_coefficient[i,j] = np.squeeze(cp_interp*ratio[i,j]) - - # TODO: make printout optional? - if False: - print('Tip speed ratio' + str(tsr_array)) - print('Pitch out: ' + str(pitch_out)) - power = ( - 0.5*air_density*(rotor_effective_velocities)**3*np.pi*R**2 - *(power_coefficient)*power_thrust_table["generator_efficiency"] - ) - return power - - def thrust_coefficient( - power_thrust_table: dict, - velocities: NDArrayFloat, - yaw_angles: NDArrayFloat, - tilt_angles: NDArrayFloat, - power_setpoints: NDArrayFloat, - tilt_interp: NDArrayObject, - average_method: str = "cubic-mean", - cubature_weights: NDArrayFloat | None = None, - correct_cp_ct_for_tilt: bool = False, - **_ # <- Allows other models to accept other keyword arguments - ): - - # sign convention. in the tum model, negative tilt creates tower clearance - tilt_angles = -tilt_angles - - # Compute the effective wind speed across the rotor - rotor_average_velocities = average_velocity( - velocities=velocities, - method=average_method, - cubature_weights=cubature_weights, - ) + idx = np.squeeze(np.where(cp_i == np.max(cp_i))) + tsr_opt = tsr_i[idx[0]] + pitch_opt = pitch_i[idx[1]] + max_cp = cp_i[idx[0],idx[1]] - # Apply tilt and yaw corrections - # Compute the tilt, if using floating turbines - old_tilt_angles = copy.deepcopy(tilt_angles) - tilt_angles = compute_tilt_angles_for_floating_turbines( - tilt_angles=tilt_angles, - tilt_interp=tilt_interp, - rotor_effective_velocities=rotor_average_velocities, + omega_cut_in = 0 # RPM + omega_max = power_thrust_table["rated_rpm"] # RPM + rated_power_aero = ( + power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] # MW ) - # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) - tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angles) - beta = power_thrust_table["beta"] - cd = power_thrust_table["cd"] - cl_alfa = power_thrust_table["cl_alfa"] + # Compute torque-rpm relation and check for region 2-and-a-half + Region2andAhalf = False - sigma = power_thrust_table["rotor_solidity"] - R = power_thrust_table["rotor_diameter"]/2 + omega_array = np.linspace(omega_cut_in,omega_max,161)*np.pi/30 # rad/s + Q = (0.5*air_density*omega_array**2*R**5 * np.pi * max_cp ) / tsr_opt**3 - shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + Paero_array = Q*omega_array - air_density = power_thrust_table["ref_air_density"] # CHANGE + if Paero_array[-1] < rated_power_aero: # then we have region 2-and-a-half + Region2andAhalf = True + Q_extra = rated_power_aero/(omega_max*np.pi/30) + Q = np.append(Q,Q_extra) + # TODO: Expression below is not assigned to anything. Should this be removed? + (Paero_array[-1]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) + omega_array = np.append(omega_array,omega_array[-1]) + Paero_array = np.append(Paero_array,rated_power_aero) + else: # limit aero_power to the last Q*omega_max + rated_power_aero = Paero_array[-1] - rotor_effective_velocities = rotor_velocity_air_density_correction( - velocities=rotor_average_velocities, - air_density=air_density, - ref_air_density=power_thrust_table["ref_air_density"] - ) + u_rated = (rated_power_aero/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) + u_array = np.linspace(3,25,45) + idx = np.argmin(np.abs(u_array-u_rated)) + if u_rated > u_array[idx]: + u_array = np.insert(u_array,idx+1,u_rated) + else: + u_array = np.insert(u_array,idx,u_rated) - pitch_out, tsr_out = TUMLossTurbine.control_trajectory( - rotor_effective_velocities, - yaw_angles, - tilt_angles, - air_density, - R, - shear, - sigma, - cd, - cl_alfa, - beta, - power_setpoints, - power_thrust_table - ) + pow_lut_omega = Paero_array + omega_lut_pow = omega_array*30/np.pi + torque_lut_omega = Q + omega_lut_torque = omega_lut_pow num_rows, num_cols = tilt_angles.shape - (average_velocity(velocities)) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) - # u = np.squeeze(u) - theta_array = (np.deg2rad(pitch_out+beta)) - tsr_array = (tsr_out) - - x0 = 0.2 - - thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) + omega_rated = np.zeros_like(rotor_average_velocities) + u_rated = np.zeros_like(rotor_average_velocities) for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),Mu[j]) - ct = fsolve(get_ct, x0, args=data) - thrust_coefficient1[i,j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) - - - yaw_angles = np.zeros_like(yaw_angles) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) + omega_rated[i,j] = ( + np.interp(power_demanded[i,j],pow_lut_omega,omega_lut_pow) + *np.pi/30 #rad/s + ) + u_rated[i,j] = (power_demanded[i,j]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) - thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) + pitch_out = np.zeros_like(rotor_average_velocities) + tsr_out = np.zeros_like(rotor_average_velocities) for i in np.arange(num_rows): yaw = yaw_angles[i,:] tilt = tilt_angles[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] + k = shear[i,:] for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),Mu[j]) - ct = fsolve(get_ct, x0, args=data) - thrust_coefficient0[i,j] = np.squeeze(ct) #np.clip(ct, 0.0001, 0.9999) - - ############################################################################ - - ratio = thrust_coefficient1/thrust_coefficient0 - - pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" - LUT = np.load(lut_file) - ct_i = LUT['ct_lut'] - pitch_i = LUT['pitch_lut'] - tsr_i = LUT['tsr_lut'] - interp_lut = RegularGridInterpolator( - (tsr_i,pitch_i), - ct_i, - bounds_error=False, - fill_value=None - )#*0.9722085500886761) - - - thrust_coefficient = np.zeros_like(average_velocity(velocities)) + u_v = rotor_average_velocities[i,j] + if u_v > u_rated[i,j]: + tsr_v = omega_rated[i,j]*R/u_v*np.cos(np.deg2rad(yaw[j]))**0.5 + else: + tsr_v = tsr_opt*np.cos(np.deg2rad(yaw[j])) + if Region2andAhalf: # fix for interpolation + omega_lut_torque[-1] = omega_lut_torque[-1]+1e-2 + omega_lut_pow[-1] = omega_lut_pow[-1]+1e-2 - for i in np.arange(num_rows): - for j in np.arange(num_cols): - ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - thrust_coefficient[i,j] = np.squeeze(ct_interp*ratio[i,j]) + data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v,pitch_opt, + omega_lut_pow,torque_lut_omega,cp_i,pitch_i,tsr_i) + [tsr_out_soluzione,infodict,ier,mesg] = fsolve( + get_tsr,tsr_v,args=data,full_output=True + ) + # check if solution was possible. If not, we are in region 3 + if (np.abs(infodict['fvec']) > 10 or tsr_out_soluzione < 4): + tsr_out_soluzione = 1000 - return thrust_coefficient + # save solution + tsr_outO = tsr_out_soluzione + omega = tsr_outO*u_v/R - def axial_induction( - power_thrust_table: dict, - velocities: NDArrayFloat, - yaw_angles: NDArrayFloat, - tilt_angles: NDArrayFloat, - power_setpoints: NDArrayFloat, - tilt_interp: NDArrayObject, - average_method: str = "cubic-mean", - cubature_weights: NDArrayFloat | None = None, - correct_cp_ct_for_tilt: bool = False, - **_ # <- Allows other models to accept other keyword arguments - ): - num_rows, num_cols = tilt_angles.shape - thrust_coefficients = TUMLossTurbine.thrust_coefficient( - power_thrust_table=power_thrust_table, - velocities=velocities, - yaw_angles=yaw_angles, - tilt_angles=tilt_angles, - power_setpoints=power_setpoints, - tilt_interp=tilt_interp, - average_method=average_method, - cubature_weights=cubature_weights, - correct_cp_ct_for_tilt=correct_cp_ct_for_tilt, - ) + # check if we are in region 2 or 3 + if omega < omega_rated[i,j]: # region 2 + # Define optimum pitch + pitch_out0 = pitch_opt - yaw_angles = np.zeros_like(yaw_angles) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - sinMu = (np.sin(MU)) - sMu = sinMu[-1,:] + else: # region 3 + tsr_outO = omega_rated[i,j]*R/u_v + data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v, + omega_rated[i,j],omega_array,Q,cp_i,pitch_i,tsr_i) + # solve aero-electrical power balance with TSR from rated omega + [pitch_out_soluzione,infodict,ier,mesg] = fsolve( + get_pitch,u_v,args=data,factor=0.1,full_output=True, + xtol=1e-10,maxfev=2000) + if pitch_out_soluzione < pitch_opt: + pitch_out_soluzione = pitch_opt + pitch_out0 = pitch_out_soluzione - axial_induction = np.zeros((num_rows, num_cols)) - for i in np.arange(num_rows): - for j in np.arange(num_cols): - ct = thrust_coefficients[i,j] - a = (1- ( (1+np.sqrt(1-ct-1/16*ct**2*sMu[j]**2))/(2*(1+1/16*ct*sMu[j]**2))) ) - axial_induction[i,j] = np.squeeze(np.clip(a, 0.0001, 0.9999)) + # COMPUTE CP AND CT GIVEN THE PITCH AND TSR FOUND ABOVE + pitch_out[i,j] = np.squeeze(pitch_out0) + tsr_out[i,j] = tsr_outO - return axial_induction + return pitch_out, tsr_out def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): #add a small misalignment in case MU = 0 to avoid division by 0 From e5b821b552802da374ecb8882890c8bce905d65a Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 15:05:57 -0700 Subject: [PATCH 37/53] Simplify calls to control_trajectory(). --- floris/core/turbine/tum_operation_model.py | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index d9eb413b9..fb05554c6 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -102,10 +102,6 @@ def power( air_density, R, shear, - sigma, - cd, - cl_alfa, - beta, power_setpoints, power_thrust_table ) @@ -290,10 +286,6 @@ def thrust_coefficient( air_density, R, shear, - sigma, - cd, - cl_alfa, - beta, power_setpoints, power_thrust_table ) @@ -441,10 +433,6 @@ def control_trajectory( air_density, R, shear, - sigma, - cd, - cl_alfa, - beta, power_setpoints, power_thrust_table ): @@ -454,6 +442,13 @@ def control_trajectory( region 2-1/2 is considered. In the future, different control strategies could be included, even user-defined. """ + # Unpack parameters from power_thrust_table + beta = power_thrust_table["beta"] + cd = power_thrust_table["cd"] + cl_alfa = power_thrust_table["cl_alfa"] + sigma = power_thrust_table["rotor_solidity"] + + # Compute power demanded if power_setpoints is None: power_demanded = ( np.ones_like(tilt_angles)*power_thrust_table["rated_power"] @@ -462,7 +457,6 @@ def control_trajectory( else: power_demanded = power_setpoints/power_thrust_table["generator_efficiency"] - ## Define function to get tip speed ratio def get_tsr(x,*data): (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,pitch_in,omega_lut_pow, @@ -475,6 +469,7 @@ def get_tsr(x,*data): torque_nm = np.interp(omega,omega_lut_torque,torque_lut_omega) + # Yawed case mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),x, np.deg2rad(pitch_in)+np.deg2rad(beta),mu) @@ -495,6 +490,7 @@ def get_tsr(x,*data): ct ) + # Unyawed case mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),x, np.deg2rad(pitch_in)+np.deg2rad(beta),mu) @@ -515,6 +511,7 @@ def get_tsr(x,*data): ct ) + # Ratio eta_p = cp/cp0 interp = RegularGridInterpolator((np.squeeze((tsr_i)), @@ -540,6 +537,7 @@ def get_pitch(x,*data): pitch_in = np.deg2rad(x) torque_nm = np.interp(omega_rpm,omega_lut_torque*30/np.pi,torque_lut_omega) + # Yawed case mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),tsr, (pitch_in)+np.deg2rad(beta),mu) @@ -560,6 +558,7 @@ def get_pitch(x,*data): ct ) + # Unyawed case mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),tsr, (pitch_in)+np.deg2rad(beta),mu) @@ -580,9 +579,10 @@ def get_pitch(x,*data): ct ) + # Ratio yawed / unyawed eta_p = cp/cp0 - interp = RegularGridInterpolator( + interp = RegularGridInterpolator( (np.squeeze((tsr_i)), np.squeeze((pitch_i))), cp_i, bounds_error=False, @@ -590,7 +590,7 @@ def get_pitch(x,*data): ) Cp_now = interp((tsr,x),method='cubic') - cp_g1 = Cp_now*eta_p + cp_g1 = Cp_now*eta_p aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 electric_pow = torque_nm*(omega_rpm*np.pi/30) From 97f2a174c6e7a8f4e5b92e17fae1161e52ba6194 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 15:35:34 -0700 Subject: [PATCH 38/53] Various formatting changes throughout for improved alignment with rest of code. --- floris/core/turbine/tum_operation_model.py | 217 +++++++++++---------- 1 file changed, 116 insertions(+), 101 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index fb05554c6..302c53109 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -47,6 +47,7 @@ class TUMLossTurbine(BaseOperationModel): TODO: Should the turbine submodels each implement axial_induction()? """ + @staticmethod def power( power_thrust_table: dict, velocities: NDArrayFloat, @@ -54,10 +55,8 @@ def power( yaw_angles: NDArrayFloat, tilt_angles: NDArrayFloat, power_setpoints: NDArrayFloat, - tilt_interp: NDArrayObject, average_method: str = "cubic-mean", cubature_weights: NDArrayFloat | None = None, - correct_cp_ct_for_tilt: bool = False, **_ # <- Allows other models to accept other keyword arguments ): @@ -78,13 +77,9 @@ def power( ) # Compute power - num_rows, num_cols = tilt_angles.shape - shear = TUMLossTurbine.compute_local_vertical_shear( - velocities, - average_velocity(velocities) - ) + shear = TUMLossTurbine.compute_local_vertical_shear(velocities) beta = power_thrust_table["beta"] cd = power_thrust_table["cd"] @@ -227,6 +222,7 @@ def power( ) return power + @staticmethod def thrust_coefficient( power_thrust_table: dict, velocities: NDArrayFloat, @@ -269,7 +265,7 @@ def thrust_coefficient( sigma = power_thrust_table["rotor_solidity"] R = power_thrust_table["rotor_diameter"]/2 - shear = TUMLossTurbine.compute_local_vertical_shear(velocities,average_velocity(velocities)) + shear = TUMLossTurbine.compute_local_vertical_shear(velocities) air_density = power_thrust_table["ref_air_density"] # CHANGE @@ -402,30 +398,33 @@ def axial_induction( return axial_induction - def compute_local_vertical_shear(velocities, avg_velocities): + @staticmethod + def compute_local_vertical_shear(velocities): """ Called to evaluate the vertical (linear) shear that each rotor experience, based on the inflow velocity. This allows to make the power curve asymmetric w.r.t. yaw misalignment. """ - num_rows, num_cols = avg_velocities.shape - shear = np.zeros_like(avg_velocities) + num_rows, num_cols = velocities.shape[:2] + shear = np.zeros((num_rows,num_cols)) for i in np.arange(num_rows): for j in np.arange(num_cols): mean_speed = np.mean(velocities[i,j,:,:],axis=0) if len(mean_speed) % 2 != 0: # odd number - u_u_hh = mean_speed/mean_speed[int(np.floor(len(mean_speed)/2))] + u_u_hh = mean_speed/mean_speed[int(np.floor(len(mean_speed)/2))] else: - u_u_hh = ( + u_u_hh = ( mean_speed - /(mean_speed[int((len(mean_speed)/2))] - +mean_speed[int((len(mean_speed)/2))-1] - )/2 + /( + mean_speed[int((len(mean_speed)/2))] + + mean_speed[int((len(mean_speed)/2))-1] + )/2 ) - zg_R = np.linspace(-1,1,len(mean_speed)+2) - polifit_k = np.polyfit(zg_R[1:-1],1-u_u_hh,1) + zg_R = np.linspace(-1, 1, len(mean_speed)+2) + polifit_k = np.polyfit(zg_R[1:-1], 1-u_u_hh, 1) shear[i,j] = -polifit_k[0] return shear + @staticmethod def control_trajectory( rotor_average_velocities, yaw_angles, @@ -451,7 +450,7 @@ def control_trajectory( # Compute power demanded if power_setpoints is None: power_demanded = ( - np.ones_like(tilt_angles)*power_thrust_table["rated_power"] + np.ones_like(tilt_angles)*power_thrust_table["rated_power"]*1000 /power_thrust_table["generator_efficiency"] ) else: @@ -459,21 +458,21 @@ def control_trajectory( ## Define function to get tip speed ratio def get_tsr(x,*data): - (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,pitch_in,omega_lut_pow, - torque_lut_omega,cp_i,pitch_i,tsr_i) = data + (air_density, R, sigma, shear, cd, cl_alfa, beta, gamma, tilt, u, pitch_in, + omega_lut_pow, torque_lut_omega, cp_i,pitch_i, tsr_i) = data omega_lut_torque = omega_lut_pow*np.pi/30 - omega = x*u/R + omega = x*u/R omega_rpm = omega*30/np.pi - torque_nm = np.interp(omega,omega_lut_torque,torque_lut_omega) + torque_nm = np.interp(omega, omega_lut_torque, torque_lut_omega) # Yawed case - mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),x, - np.deg2rad(pitch_in)+np.deg2rad(beta),mu) - x0 = 0.1 + mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) + data = (sigma, cd, cl_alfa, gamma, tilt, shear, np.cos(mu), np.sin(mu), x, + np.deg2rad(pitch_in) + np.deg2rad(beta), mu) + x0 = 0.1 [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) cp = find_cp( sigma, @@ -491,11 +490,11 @@ def get_tsr(x,*data): ) # Unyawed case - mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),x, - np.deg2rad(pitch_in)+np.deg2rad(beta),mu) - x0 = 0.1 - [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) + data = (sigma, cd, cl_alfa, 0, tilt, shear, np.cos(mu), np.sin(mu), x, + np.deg2rad(pitch_in) + np.deg2rad(beta), mu) + x0 = 0.1 + [ct, infodict, ier, mesg] = fsolve(get_ct, x0, args=data, full_output=True, factor=0.1) cp0 = find_cp( sigma, cd, @@ -514,12 +513,15 @@ def get_tsr(x,*data): # Ratio eta_p = cp/cp0 - interp = RegularGridInterpolator((np.squeeze((tsr_i)), - np.squeeze((pitch_i))), cp_i, - bounds_error=False, fill_value=None) + interp = RegularGridInterpolator( + (np.squeeze((tsr_i)), np.squeeze((pitch_i))), + cp_i, + bounds_error=False, + fill_value=None + ) Cp_now = interp((x,pitch_in),method='cubic') - cp_g1 = Cp_now*eta_p + cp_g1 = Cp_now*eta_p aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 electric_pow = torque_nm*(omega_rpm*np.pi/30) @@ -528,21 +530,21 @@ def get_tsr(x,*data): ## Define function to get pitch angle def get_pitch(x,*data): - (air_density,R,sigma,shear,cd,cl_alfa,beta,gamma,tilt,u,omega_rated,omega_lut_torque, - torque_lut_omega,cp_i,pitch_i,tsr_i) = data + (air_density, R, sigma, shear, cd, cl_alfa, beta, gamma, tilt, u, omega_rated, omega_lut_torque, + torque_lut_omega, cp_i, pitch_i, tsr_i) = data - omega_rpm = omega_rated*30/np.pi - tsr = omega_rated*R/(u) + omega_rpm = omega_rated*30/np.pi + tsr = omega_rated*R/(u) pitch_in = np.deg2rad(x) - torque_nm = np.interp(omega_rpm,omega_lut_torque*30/np.pi,torque_lut_omega) + torque_nm = np.interp(omega_rpm, omega_lut_torque*30/np.pi, torque_lut_omega) # Yawed case mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,gamma,tilt,shear,np.cos(mu),np.sin(mu),tsr, - (pitch_in)+np.deg2rad(beta),mu) + data = (sigma, cd, cl_alfa, gamma, tilt, shear, np.cos(mu), np.sin(mu), tsr, + (pitch_in) + np.deg2rad(beta), mu) x0 = 0.1 - [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + [ct, infodict, ier, mesg] = fsolve(get_ct, x0,args=data, full_output=True, factor=0.1) cp = find_cp( sigma, cd, @@ -560,10 +562,10 @@ def get_pitch(x,*data): # Unyawed case mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) - data = (sigma,cd,cl_alfa,0,tilt,shear,np.cos(mu),np.sin(mu),tsr, - (pitch_in)+np.deg2rad(beta),mu) + data = (sigma, cd, cl_alfa, 0, tilt, shear, np.cos(mu), np.sin(mu), tsr, + (pitch_in) + np.deg2rad(beta), mu) x0 = 0.1 - [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + [ct, infodict, ier, mesg] = fsolve(get_ct, x0, args=data, full_output=True, factor=0.1) cp0 = find_cp( sigma, cd, @@ -606,20 +608,20 @@ def get_pitch(x,*data): tsr_i = LUT['tsr_lut'] idx = np.squeeze(np.where(cp_i == np.max(cp_i))) - tsr_opt = tsr_i[idx[0]] + tsr_opt = tsr_i[idx[0]] pitch_opt = pitch_i[idx[1]] - max_cp = cp_i[idx[0],idx[1]] + max_cp = cp_i[idx[0],idx[1]] omega_cut_in = 0 # RPM omega_max = power_thrust_table["rated_rpm"] # RPM rated_power_aero = ( - power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] # MW - ) + power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] # kW + ) * 1000 # Compute torque-rpm relation and check for region 2-and-a-half Region2andAhalf = False - omega_array = np.linspace(omega_cut_in,omega_max,161)*np.pi/30 # rad/s + omega_array = np.linspace(omega_cut_in, omega_max, 161)*np.pi/30 # rad/s Q = (0.5*air_density*omega_array**2*R**5 * np.pi * max_cp ) / tsr_opt**3 Paero_array = Q*omega_array @@ -636,8 +638,8 @@ def get_pitch(x,*data): rated_power_aero = Paero_array[-1] u_rated = (rated_power_aero/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) - u_array = np.linspace(3,25,45) - idx = np.argmin(np.abs(u_array-u_rated)) + u_array = np.linspace(3, 25, 45) + idx = np.argmin(np.abs(u_array - u_rated)) if u_rated > u_array[idx]: u_array = np.insert(u_array,idx+1,u_rated) else: @@ -651,12 +653,11 @@ def get_pitch(x,*data): num_rows, num_cols = tilt_angles.shape omega_rated = np.zeros_like(rotor_average_velocities) - u_rated = np.zeros_like(rotor_average_velocities) + u_rated = np.zeros_like(rotor_average_velocities) for i in np.arange(num_rows): for j in np.arange(num_cols): omega_rated[i,j] = ( - np.interp(power_demanded[i,j],pow_lut_omega,omega_lut_pow) - *np.pi/30 #rad/s + np.interp(power_demanded[i,j], pow_lut_omega, omega_lut_pow)*np.pi/30 #rad/s ) u_rated[i,j] = (power_demanded[i,j]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) @@ -674,13 +675,13 @@ def get_pitch(x,*data): else: tsr_v = tsr_opt*np.cos(np.deg2rad(yaw[j])) if Region2andAhalf: # fix for interpolation - omega_lut_torque[-1] = omega_lut_torque[-1]+1e-2 - omega_lut_pow[-1] = omega_lut_pow[-1]+1e-2 + omega_lut_torque[-1] = omega_lut_torque[-1] + 1e-2 + omega_lut_pow[-1] = omega_lut_pow[-1] + 1e-2 - data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v,pitch_opt, - omega_lut_pow,torque_lut_omega,cp_i,pitch_i,tsr_i) - [tsr_out_soluzione,infodict,ier,mesg] = fsolve( - get_tsr,tsr_v,args=data,full_output=True + data = (air_density, R, sigma, k[j], cd, cl_alfa, beta, yaw[j], tilt[j], u_v, + pitch_opt, omega_lut_pow, torque_lut_omega, cp_i, pitch_i, tsr_i) + [tsr_out_soluzione, infodict, ier, mesg] = fsolve( + get_tsr, tsr_v, args=data, full_output=True ) # check if solution was possible. If not, we are in region 3 if (np.abs(infodict['fvec']) > 10 or tsr_out_soluzione < 4): @@ -688,7 +689,7 @@ def get_pitch(x,*data): # save solution tsr_outO = tsr_out_soluzione - omega = tsr_outO*u_v/R + omega = tsr_outO*u_v/R # check if we are in region 2 or 3 if omega < omega_rated[i,j]: # region 2 @@ -697,19 +698,25 @@ def get_pitch(x,*data): else: # region 3 tsr_outO = omega_rated[i,j]*R/u_v - data = (air_density,R,sigma,k[j],cd,cl_alfa,beta,yaw[j],tilt[j],u_v, - omega_rated[i,j],omega_array,Q,cp_i,pitch_i,tsr_i) + data = (air_density, R, sigma, k[j], cd, cl_alfa, beta, yaw[j], tilt[j], u_v, + omega_rated[i,j], omega_array, Q, cp_i, pitch_i, tsr_i) # solve aero-electrical power balance with TSR from rated omega - [pitch_out_soluzione,infodict,ier,mesg] = fsolve( - get_pitch,u_v,args=data,factor=0.1,full_output=True, - xtol=1e-10,maxfev=2000) + [pitch_out_soluzione, infodict, ier, mesg] = fsolve( + get_pitch, + u_v, + args=data, + factor=0.1, + full_output=True, + xtol=1e-10, + maxfev=2000 + ) if pitch_out_soluzione < pitch_opt: pitch_out_soluzione = pitch_opt pitch_out0 = pitch_out_soluzione - # COMPUTE CP AND CT GIVEN THE PITCH AND TSR FOUND ABOVE - pitch_out[i,j] = np.squeeze(pitch_out0) - tsr_out[i,j] = tsr_outO + # pitch and tsr will be used to compute Cp and Ct + pitch_out[i,j] = np.squeeze(pitch_out0) + tsr_out[i,j] = tsr_outO return pitch_out, tsr_out @@ -719,7 +726,7 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): MU = 1e-6 sinMu = np.sin(MU) cosMu = np.cos(MU) - a = 1-((1+np.sqrt(1-ct-1/16*sinMu**2*ct**2))/(2*(1+1/16*ct*sinMu**2))) + a = 1 - ((1 + np.sqrt(1 - ct - 1/16*sinMu**2*ct**2))/(2*(1 + 1/16*ct*sinMu**2))) SG = np.sin(np.deg2rad(gamma)) CG = np.cos(np.deg2rad(gamma)) SD = np.sin(np.deg2rad(delta)) @@ -727,31 +734,35 @@ def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(ct/2))/2)) p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 - - (tsr*cd*np.pi*( - CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - - 8*CD*tsr*SG*k + 8*tsr**2 - ))/16 - - (np.pi*tsr*sinMu**2*cd)/2 - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 - + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 - + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 - + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 - + ((CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2)) - /(4*sinMu**2)) - - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 - + ((CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi - *(3*CG**2*SD**2 + SG**2)) - /(24*sinMu**2)) - - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) - - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu) - )/(2*np.pi)) + - (tsr*cd*np.pi*(CD**2*CG**2*SD**2*k**2 + + 3*CD**2*SG**2*k**2 + - 8*CD*tsr*SG*k + 8*tsr**2 + ) + )/16 + - (np.pi*tsr*sinMu**2*cd)/2 + - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 + + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 + + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 + + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 + + ((CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2)) + /(4*sinMu**2) + ) + - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 + + ((CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi + *(3*CG**2*SD**2 + SG**2) + )/(24*sinMu**2) + ) + - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu + + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) + - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu) + )/(2*np.pi) + ) return p def get_ct(x,*data): - sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU = data - #add a small misalignment in case MU = 0 to avoid division by 0 + sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU = data + # Add a small misalignment in case MU = 0 to avoid division by 0 if MU == 0: MU = 1e-6 sinMu = np.sin(MU) @@ -760,12 +771,16 @@ def get_ct(x,*data): CG = np.cos(np.deg2rad(gamma)) SD = np.sin(np.deg2rad(delta)) SG = np.sin(np.deg2rad(gamma)) - a = (1- ( (1+np.sqrt(1-x-1/16*x**2*sinMu**2))/(2*(1+1/16*x*sinMu**2))) ) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(x/2))/2)) + a = (1 - ((1 + np.sqrt(1 - x - 1/16*x**2*sinMu**2))/(2*(1 + 1/16*x*sinMu**2)))) + k_1s = -1*(15*np.pi/32*np.tan((MU + sinMu*(x/2))/2)) I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) - + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu))/(2*np.pi) + + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu) + )/(2*np.pi) I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k - + 8*tsr**2))/12)/(2*np.pi) + + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k + + 8*tsr**2 + ) + )/12 + )/(2*np.pi) - return (sigma*(cd+cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x + return (sigma*(cd + cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x From ea684acbe1232990baa2b123b5a106f3121c75b4 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 15:36:21 -0700 Subject: [PATCH 39/53] Move to kW as base power unit for consistency with other turbine specifications. --- floris/turbine_library/iea_10MW.yaml | 2 +- floris/turbine_library/iea_15MW.yaml | 2 +- floris/turbine_library/iea_3MW.yaml | 2 +- floris/turbine_library/nrel_5MW.yaml | 2 +- tests/conftest.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/floris/turbine_library/iea_10MW.yaml b/floris/turbine_library/iea_10MW.yaml index edda83707..6515c4b66 100644 --- a/floris/turbine_library/iea_10MW.yaml +++ b/floris/turbine_library/iea_10MW.yaml @@ -26,7 +26,7 @@ power_thrust_table: rotor_solidity: 0.03500415472147307 rated_rpm: 8.6 generator_efficiency: 0.944 - rated_power: 10.0e6 + rated_power: 10000.0 rotor_diameter: 198 beta: -3.8233819218614817 cd: 0.004612981322772105 diff --git a/floris/turbine_library/iea_15MW.yaml b/floris/turbine_library/iea_15MW.yaml index 365280885..8321f460b 100644 --- a/floris/turbine_library/iea_15MW.yaml +++ b/floris/turbine_library/iea_15MW.yaml @@ -28,7 +28,7 @@ power_thrust_table: rotor_solidity: 0.031018237027995298 rated_rpm: 7.55 generator_efficiency: 0.95756 - rated_power: 15.0e6 + rated_power: 15000.00 rotor_diameter: 242.24 beta: -3.098605491003358 cd: 0.004426686198054057 diff --git a/floris/turbine_library/iea_3MW.yaml b/floris/turbine_library/iea_3MW.yaml index 5ec3035b4..1867bc900 100644 --- a/floris/turbine_library/iea_3MW.yaml +++ b/floris/turbine_library/iea_3MW.yaml @@ -34,7 +34,7 @@ power_thrust_table: rated_rpm: 11.75 rotor_solidity: 0.0416 generator_efficiency: 0.925 - rated_power: 3.3e6 + rated_power: 3300.0 rotor_diameter: 130 beta: -3.11 cd: 0.0051 diff --git a/floris/turbine_library/nrel_5MW.yaml b/floris/turbine_library/nrel_5MW.yaml index a35277f6e..90dd56d17 100644 --- a/floris/turbine_library/nrel_5MW.yaml +++ b/floris/turbine_library/nrel_5MW.yaml @@ -60,7 +60,7 @@ power_thrust_table: rated_rpm: 12.1 rotor_solidity: 0.05132 generator_efficiency: 0.944 - rated_power: 5.0e6 + rated_power: 5000.0 rotor_diameter: 126 beta: -0.45891 cd: 0.0040638 diff --git a/tests/conftest.py b/tests/conftest.py index 462adcf17..0b03ac597 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -396,7 +396,7 @@ def __init__(self): "rated_rpm": 12.1, "rotor_solidity": 0.05132, "generator_efficiency": 0.944, - "rated_power": 5.0e6, + "rated_power": 5000.0, "rotor_diameter": 126, "beta": -0.45891, "cd": 0.0040638, From c6445a05c9e721199676eb035da15f068543e11e Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 15:41:43 -0700 Subject: [PATCH 40/53] Format using ruff format. --- floris/core/turbine/tum_operation_model.py | 854 +++++++++++++-------- 1 file changed, 555 insertions(+), 299 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index 302c53109..ca7f6a0ab 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -2,17 +2,11 @@ import copy import os -from abc import abstractmethod from pathlib import Path -from typing import ( - Any, - Dict, - Final, -) import numpy as np -from attrs import define, field -from scipy.interpolate import interp1d, RegularGridInterpolator +from attrs import define +from scipy.interpolate import RegularGridInterpolator from scipy.optimize import fsolve from floris.core.rotor_velocity import ( @@ -57,9 +51,8 @@ def power( power_setpoints: NDArrayFloat, average_method: str = "cubic-mean", cubature_weights: NDArrayFloat | None = None, - **_ # <- Allows other models to accept other keyword arguments + **_, # <- Allows other models to accept other keyword arguments ): - # Sign convention: in the tum model, negative tilt creates tower clearance tilt_angles = -tilt_angles @@ -73,7 +66,7 @@ def power( rotor_effective_velocities = rotor_velocity_air_density_correction( velocities=rotor_average_velocities, air_density=air_density, - ref_air_density=power_thrust_table["ref_air_density"] + ref_air_density=power_thrust_table["ref_air_density"], ) # Compute power @@ -86,7 +79,7 @@ def power( cl_alfa = power_thrust_table["cl_alfa"] sigma = power_thrust_table["rotor_solidity"] - R = power_thrust_table["rotor_diameter"]/2 + R = power_thrust_table["rotor_diameter"] / 2 air_density = power_thrust_table["ref_air_density"] @@ -98,25 +91,27 @@ def power( R, shear, power_setpoints, - power_thrust_table + power_thrust_table, ) - tsr_array = (tsr_out) - theta_array = (np.deg2rad(pitch_out+beta)) + tsr_array = tsr_out + theta_array = np.deg2rad(pitch_out + beta) x0 = 0.2 # Compute power in yawed conditions - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) + MU = np.arccos( + np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) + ) + cosMu = np.cos(MU) + sinMu = np.sin(MU) p = np.zeros_like(average_velocity(velocities)) for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - k = shear[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] + yaw = yaw_angles[i, :] + tilt = tilt_angles[i, :] + k = shear[i, :] + cMu = cosMu[i, :] + sMu = sinMu[i, :] + Mu = MU[i, :] for j in np.arange(num_cols): # Create data tuple for fsolve data = ( @@ -128,97 +123,118 @@ def power( k[j], cMu[j], sMu[j], - (tsr_array[i,j]), - (theta_array[i,j]), - Mu[j] + (tsr_array[i, j]), + (theta_array[i, j]), + Mu[j], ) ct, info, ier, msg = fsolve(get_ct, x0, args=data, full_output=True) if ier == 1: - p[i,j] = np.squeeze(find_cp( - sigma, - cd, - cl_alfa, - yaw[j], - tilt[j], - k[j], - cMu[j], - sMu[j], - (tsr_array[i,j]), - (theta_array[i,j]), - Mu[j], - ct - )) + p[i, j] = np.squeeze( + find_cp( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + k[j], + cMu[j], + sMu[j], + (tsr_array[i, j]), + (theta_array[i, j]), + Mu[j], + ct, + ) + ) else: - p[i,j] = -1e3 + p[i, j] = -1e3 # Recompute power in non-yawed conditions yaw_angles = np.zeros_like(yaw_angles) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) + MU = np.arccos( + np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) + ) + cosMu = np.cos(MU) + sinMu = np.sin(MU) p0 = np.zeros_like((average_velocity(velocities))) for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - k = shear[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] + yaw = yaw_angles[i, :] + tilt = tilt_angles[i, :] + k = shear[i, :] + cMu = cosMu[i, :] + sMu = sinMu[i, :] + Mu = MU[i, :] for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],k[j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),Mu[j]) - ct, info, ier, msg = fsolve(get_ct, x0,args=data,full_output=True) + data = ( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + k[j], + cMu[j], + sMu[j], + (tsr_array[i, j]), + (theta_array[i, j]), + Mu[j], + ) + ct, info, ier, msg = fsolve(get_ct, x0, args=data, full_output=True) if ier == 1: - p0[i,j] = np.squeeze(find_cp( - sigma, - cd, - cl_alfa, - yaw[j], - tilt[j], - k[j], - cMu[j], - sMu[j], - (tsr_array[i,j]), - (theta_array[i,j]), - Mu[j], - ct - )) + p0[i, j] = np.squeeze( + find_cp( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + k[j], + cMu[j], + sMu[j], + (tsr_array[i, j]), + (theta_array[i, j]), + Mu[j], + ct, + ) + ) else: - p0[i,j] = -1e3 + p0[i, j] = -1e3 # ratio of yawed to unyawed thrust coefficients - ratio = p/p0 + ratio = p / p0 # Load Cp surface data and construct interpolator # TODO: remove hardcoding pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" - LUT = np.load(lut_file) - cp_i = LUT['cp_lut'] - pitch_i = LUT['pitch_lut'] - tsr_i = LUT['tsr_lut'] + LUT = np.load(lut_file) + cp_i = LUT["cp_lut"] + pitch_i = LUT["pitch_lut"] + tsr_i = LUT["tsr_lut"] interp_lut = RegularGridInterpolator( - (tsr_i,pitch_i), - cp_i, - bounds_error=False, - fill_value=None + (tsr_i, pitch_i), cp_i, bounds_error=False, fill_value=None ) power_coefficient = np.zeros_like(average_velocity(velocities)) for i in np.arange(num_rows): for j in np.arange(num_cols): - cp_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - power_coefficient[i,j] = np.squeeze(cp_interp*ratio[i,j]) + cp_interp = interp_lut( + np.array([(tsr_array[i, j]), (pitch_out[i, j])]), method="cubic" + ) + power_coefficient[i, j] = np.squeeze(cp_interp * ratio[i, j]) # TODO: make printout optional? if False: - print('Tip speed ratio' + str(tsr_array)) - print('Pitch out: ' + str(pitch_out)) + print("Tip speed ratio" + str(tsr_array)) + print("Pitch out: " + str(pitch_out)) power = ( - 0.5*air_density*(rotor_effective_velocities)**3*np.pi*R**2 - *(power_coefficient)*power_thrust_table["generator_efficiency"] + 0.5 + * air_density + * (rotor_effective_velocities)**3 + * np.pi + * R**2 + * (power_coefficient) + * power_thrust_table["generator_efficiency"] ) return power @@ -233,9 +249,8 @@ def thrust_coefficient( average_method: str = "cubic-mean", cubature_weights: NDArrayFloat | None = None, correct_cp_ct_for_tilt: bool = False, - **_ # <- Allows other models to accept other keyword arguments + **_, # <- Allows other models to accept other keyword arguments ): - # sign convention. in the tum model, negative tilt creates tower clearance tilt_angles = -tilt_angles @@ -246,7 +261,6 @@ def thrust_coefficient( cubature_weights=cubature_weights, ) - # Apply tilt and yaw corrections # Compute the tilt, if using floating turbines old_tilt_angles = copy.deepcopy(tilt_angles) @@ -263,16 +277,16 @@ def thrust_coefficient( cl_alfa = power_thrust_table["cl_alfa"] sigma = power_thrust_table["rotor_solidity"] - R = power_thrust_table["rotor_diameter"]/2 + R = power_thrust_table["rotor_diameter"] / 2 shear = TUMLossTurbine.compute_local_vertical_shear(velocities) - air_density = power_thrust_table["ref_air_density"] # CHANGE + air_density = power_thrust_table["ref_air_density"] # CHANGE rotor_effective_velocities = rotor_velocity_air_density_correction( velocities=rotor_average_velocities, air_density=air_density, - ref_air_density=power_thrust_table["ref_air_density"] + ref_air_density=power_thrust_table["ref_air_density"], ) pitch_out, tsr_out = TUMLossTurbine.control_trajectory( @@ -283,79 +297,102 @@ def thrust_coefficient( R, shear, power_setpoints, - power_thrust_table + power_thrust_table, ) num_rows, num_cols = tilt_angles.shape - # u = np.squeeze(u) - theta_array = (np.deg2rad(pitch_out+beta)) - tsr_array = (tsr_out) + theta_array = np.deg2rad(pitch_out + beta) + tsr_array = tsr_out x0 = 0.2 # Compute thrust coefficient in yawed conditions - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) + MU = np.arccos( + np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) + ) + cosMu = np.cos(MU) + sinMu = np.sin(MU) thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] + yaw = yaw_angles[i, :] + tilt = tilt_angles[i, :] + cMu = cosMu[i, :] + sMu = sinMu[i, :] + Mu = MU[i, :] for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),Mu[j]) + data = ( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + shear[i, j], + cMu[j], + sMu[j], + (tsr_array[i, j]), + (theta_array[i, j]), + Mu[j], + ) ct = fsolve(get_ct, x0, args=data) - thrust_coefficient1[i,j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) + thrust_coefficient1[i, j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) # Recompute thrust coefficient in non-yawed conditions yaw_angles = np.zeros_like(yaw_angles) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - cosMu = (np.cos(MU)) - sinMu = (np.sin(MU)) + MU = np.arccos( + np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) + ) + cosMu = np.cos(MU) + sinMu = np.sin(MU) thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - cMu = cosMu[i,:] - sMu = sinMu[i,:] - Mu = MU[i,:] + yaw = yaw_angles[i, :] + tilt = tilt_angles[i, :] + cMu = cosMu[i, :] + sMu = sinMu[i, :] + Mu = MU[i, :] for j in np.arange(num_cols): - data = (sigma,cd,cl_alfa,yaw[j],tilt[j],shear[i,j],cMu[j],sMu[j],(tsr_array[i,j]), - (theta_array[i,j]),Mu[j]) + data = ( + sigma, + cd, + cl_alfa, + yaw[j], + tilt[j], + shear[i, j], + cMu[j], + sMu[j], + (tsr_array[i, j]), + (theta_array[i, j]), + Mu[j], + ) ct = fsolve(get_ct, x0, args=data) - thrust_coefficient0[i,j] = np.squeeze(ct) #np.clip(ct, 0.0001, 0.9999) + thrust_coefficient0[i, j] = np.squeeze(ct) # np.clip(ct, 0.0001, 0.9999) # Compute ratio of yawed to unyawed thrust coefficients - ratio = thrust_coefficient1/thrust_coefficient0 + ratio = thrust_coefficient1 / thrust_coefficient0 # Load Ct surface data and construct interpolator # TODO: remove hardcoding pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" LUT = np.load(lut_file) - ct_i = LUT['ct_lut'] - pitch_i = LUT['pitch_lut'] - tsr_i = LUT['tsr_lut'] + ct_i = LUT["ct_lut"] + pitch_i = LUT["pitch_lut"] + tsr_i = LUT["tsr_lut"] interp_lut = RegularGridInterpolator( - (tsr_i,pitch_i), - ct_i, - bounds_error=False, - fill_value=None - )#*0.9722085500886761) - + (tsr_i, pitch_i), ct_i, bounds_error=False, fill_value=None + ) # *0.9722085500886761) # Interpolate and apply ratio to determine thrust coefficient thrust_coefficient = np.zeros_like(average_velocity(velocities)) for i in np.arange(num_rows): for j in np.arange(num_cols): - ct_interp = interp_lut(np.array([(tsr_array[i,j]),(pitch_out[i,j])]),method='cubic') - thrust_coefficient[i,j] = np.squeeze(ct_interp*ratio[i,j]) + ct_interp = interp_lut( + np.array([(tsr_array[i, j]), (pitch_out[i, j])]), method="cubic" + ) + thrust_coefficient[i, j] = np.squeeze(ct_interp * ratio[i, j]) return thrust_coefficient @@ -369,7 +406,7 @@ def axial_induction( average_method: str = "cubic-mean", cubature_weights: NDArrayFloat | None = None, correct_cp_ct_for_tilt: bool = False, - **_ # <- Allows other models to accept other keyword arguments + **_, # <- Allows other models to accept other keyword arguments ): num_rows, num_cols = tilt_angles.shape thrust_coefficients = TUMLossTurbine.thrust_coefficient( @@ -385,19 +422,24 @@ def axial_induction( ) yaw_angles = np.zeros_like(yaw_angles) - MU = np.arccos(np.cos(np.deg2rad((yaw_angles)))*np.cos(np.deg2rad((tilt_angles)))) - sinMu = (np.sin(MU)) - sMu = sinMu[-1,:] + MU = np.arccos( + np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) + ) + sinMu = np.sin(MU) + sMu = sinMu[-1, :] axial_induction = np.zeros((num_rows, num_cols)) for i in np.arange(num_rows): for j in np.arange(num_cols): - ct = thrust_coefficients[i,j] - a = (1- ( (1+np.sqrt(1-ct-1/16*ct**2*sMu[j]**2))/(2*(1+1/16*ct*sMu[j]**2))) ) - axial_induction[i,j] = np.squeeze(np.clip(a, 0.0001, 0.9999)) + ct = thrust_coefficients[i, j] + a = 1 - ( + (1 + np.sqrt(1 - ct - 1 / 16 * ct**2 * sMu[j] ** 2)) + / (2 * (1 + 1 / 16 * ct * sMu[j] ** 2)) + ) + axial_induction[i, j] = np.squeeze(np.clip(a, 0.0001, 0.9999)) return axial_induction - + @staticmethod def compute_local_vertical_shear(velocities): """ @@ -405,36 +447,37 @@ def compute_local_vertical_shear(velocities): inflow velocity. This allows to make the power curve asymmetric w.r.t. yaw misalignment. """ num_rows, num_cols = velocities.shape[:2] - shear = np.zeros((num_rows,num_cols)) + shear = np.zeros((num_rows, num_cols)) for i in np.arange(num_rows): for j in np.arange(num_cols): - mean_speed = np.mean(velocities[i,j,:,:],axis=0) - if len(mean_speed) % 2 != 0: # odd number - u_u_hh = mean_speed/mean_speed[int(np.floor(len(mean_speed)/2))] + mean_speed = np.mean(velocities[i, j, :, :], axis=0) + if len(mean_speed) % 2 != 0: # odd number + u_u_hh = mean_speed / mean_speed[int(np.floor(len(mean_speed) / 2))] else: u_u_hh = ( mean_speed - /( - mean_speed[int((len(mean_speed)/2))] - + mean_speed[int((len(mean_speed)/2))-1] - )/2 + / ( + mean_speed[int((len(mean_speed) / 2))] + + mean_speed[int((len(mean_speed) / 2)) - 1] + ) + / 2 ) - zg_R = np.linspace(-1, 1, len(mean_speed)+2) - polifit_k = np.polyfit(zg_R[1:-1], 1-u_u_hh, 1) - shear[i,j] = -polifit_k[0] + zg_R = np.linspace(-1, 1, len(mean_speed) + 2) + polifit_k = np.polyfit(zg_R[1:-1], 1 - u_u_hh, 1) + shear[i, j] = -polifit_k[0] return shear @staticmethod def control_trajectory( - rotor_average_velocities, - yaw_angles, - tilt_angles, - air_density, - R, - shear, - power_setpoints, - power_thrust_table - ): + rotor_average_velocities, + yaw_angles, + tilt_angles, + air_density, + R, + shear, + power_setpoints, + power_thrust_table, + ): """ Determines the tip-speed-ratio and pitch angles that occur in operation. This routine assumes a standard region 2 control approach (i.e. k*rpm^2) and a region 3. Also @@ -446,34 +489,67 @@ def control_trajectory( cd = power_thrust_table["cd"] cl_alfa = power_thrust_table["cl_alfa"] sigma = power_thrust_table["rotor_solidity"] - + # Compute power demanded if power_setpoints is None: power_demanded = ( - np.ones_like(tilt_angles)*power_thrust_table["rated_power"]*1000 - /power_thrust_table["generator_efficiency"] + np.ones_like(tilt_angles) + * power_thrust_table["rated_power"] + * 1000 + / power_thrust_table["generator_efficiency"] ) else: - power_demanded = power_setpoints/power_thrust_table["generator_efficiency"] + power_demanded = ( + power_setpoints / power_thrust_table["generator_efficiency"] + ) ## Define function to get tip speed ratio - def get_tsr(x,*data): - (air_density, R, sigma, shear, cd, cl_alfa, beta, gamma, tilt, u, pitch_in, - omega_lut_pow, torque_lut_omega, cp_i,pitch_i, tsr_i) = data + def get_tsr(x, *data): + ( + air_density, + R, + sigma, + shear, + cd, + cl_alfa, + beta, + gamma, + tilt, + u, + pitch_in, + omega_lut_pow, + torque_lut_omega, + cp_i, + pitch_i, + tsr_i, + ) = data - omega_lut_torque = omega_lut_pow*np.pi/30 + omega_lut_torque = omega_lut_pow * np.pi / 30 - omega = x*u/R - omega_rpm = omega*30/np.pi + omega = x * u / R + omega_rpm = omega * 30 / np.pi torque_nm = np.interp(omega, omega_lut_torque, torque_lut_omega) # Yawed case - mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) - data = (sigma, cd, cl_alfa, gamma, tilt, shear, np.cos(mu), np.sin(mu), x, - np.deg2rad(pitch_in) + np.deg2rad(beta), mu) + mu = np.arccos(np.cos(np.deg2rad(gamma)) * np.cos(np.deg2rad(tilt))) + data = ( + sigma, + cd, + cl_alfa, + gamma, + tilt, + shear, + np.cos(mu), + np.sin(mu), + x, + np.deg2rad(pitch_in) + np.deg2rad(beta), + mu, + ) x0 = 0.1 - [ct,infodict,ier,mesg] = fsolve(get_ct, x0,args=data,full_output=True,factor=0.1) + [ct, infodict, ier, mesg] = fsolve( + get_ct, x0, args=data, full_output=True, factor=0.1 + ) cp = find_cp( sigma, cd, @@ -484,17 +560,30 @@ def get_tsr(x,*data): np.cos(mu), np.sin(mu), x, - np.deg2rad(pitch_in)+np.deg2rad(beta), + np.deg2rad(pitch_in) + np.deg2rad(beta), mu, - ct + ct, ) # Unyawed case - mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) - data = (sigma, cd, cl_alfa, 0, tilt, shear, np.cos(mu), np.sin(mu), x, - np.deg2rad(pitch_in) + np.deg2rad(beta), mu) + mu = np.arccos(np.cos(np.deg2rad(0)) * np.cos(np.deg2rad(tilt))) + data = ( + sigma, + cd, + cl_alfa, + 0, + tilt, + shear, + np.cos(mu), + np.sin(mu), + x, + np.deg2rad(pitch_in) + np.deg2rad(beta), + mu, + ) x0 = 0.1 - [ct, infodict, ier, mesg] = fsolve(get_ct, x0, args=data, full_output=True, factor=0.1) + [ct, infodict, ier, mesg] = fsolve( + get_ct, x0, args=data, full_output=True, factor=0.1 + ) cp0 = find_cp( sigma, cd, @@ -505,46 +594,77 @@ def get_tsr(x,*data): np.cos(mu), np.sin(mu), x, - np.deg2rad(pitch_in)+np.deg2rad(beta), + np.deg2rad(pitch_in) + np.deg2rad(beta), mu, - ct + ct, ) # Ratio - eta_p = cp/cp0 + eta_p = cp / cp0 - interp = RegularGridInterpolator( + interp = RegularGridInterpolator( (np.squeeze((tsr_i)), np.squeeze((pitch_i))), cp_i, bounds_error=False, - fill_value=None + fill_value=None, ) - Cp_now = interp((x,pitch_in),method='cubic') - cp_g1 = Cp_now*eta_p - aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 - electric_pow = torque_nm*(omega_rpm*np.pi/30) + Cp_now = interp((x, pitch_in), method="cubic") + cp_g1 = Cp_now * eta_p + aero_pow = 0.5 * air_density * (np.pi * R**2) * (u)**3 * cp_g1 + electric_pow = torque_nm * (omega_rpm * np.pi / 30) y = aero_pow - electric_pow return y ## Define function to get pitch angle - def get_pitch(x,*data): - (air_density, R, sigma, shear, cd, cl_alfa, beta, gamma, tilt, u, omega_rated, omega_lut_torque, - torque_lut_omega, cp_i, pitch_i, tsr_i) = data + def get_pitch(x, *data): + ( + air_density, + R, + sigma, + shear, + cd, + cl_alfa, + beta, + gamma, + tilt, + u, + omega_rated, + omega_lut_torque, + torque_lut_omega, + cp_i, + pitch_i, + tsr_i, + ) = data - omega_rpm = omega_rated*30/np.pi - tsr = omega_rated*R/(u) + omega_rpm = omega_rated * 30 / np.pi + tsr = omega_rated * R / (u) pitch_in = np.deg2rad(x) - torque_nm = np.interp(omega_rpm, omega_lut_torque*30/np.pi, torque_lut_omega) + torque_nm = np.interp( + omega_rpm, omega_lut_torque * 30 / np.pi, torque_lut_omega + ) # Yawed case - mu = np.arccos(np.cos(np.deg2rad(gamma))*np.cos(np.deg2rad(tilt))) - data = (sigma, cd, cl_alfa, gamma, tilt, shear, np.cos(mu), np.sin(mu), tsr, - (pitch_in) + np.deg2rad(beta), mu) - x0 = 0.1 - [ct, infodict, ier, mesg] = fsolve(get_ct, x0,args=data, full_output=True, factor=0.1) + mu = np.arccos(np.cos(np.deg2rad(gamma)) * np.cos(np.deg2rad(tilt))) + data = ( + sigma, + cd, + cl_alfa, + gamma, + tilt, + shear, + np.cos(mu), + np.sin(mu), + tsr, + (pitch_in) + np.deg2rad(beta), + mu, + ) + x0 = 0.1 + [ct, infodict, ier, mesg] = fsolve( + get_ct, x0, args=data, full_output=True, factor=0.1 + ) cp = find_cp( sigma, cd, @@ -555,17 +675,30 @@ def get_pitch(x,*data): np.cos(mu), np.sin(mu), tsr, - (pitch_in)+np.deg2rad(beta), + (pitch_in) + np.deg2rad(beta), mu, - ct + ct, ) # Unyawed case - mu = np.arccos(np.cos(np.deg2rad(0))*np.cos(np.deg2rad(tilt))) - data = (sigma, cd, cl_alfa, 0, tilt, shear, np.cos(mu), np.sin(mu), tsr, - (pitch_in) + np.deg2rad(beta), mu) - x0 = 0.1 - [ct, infodict, ier, mesg] = fsolve(get_ct, x0, args=data, full_output=True, factor=0.1) + mu = np.arccos(np.cos(np.deg2rad(0)) * np.cos(np.deg2rad(tilt))) + data = ( + sigma, + cd, + cl_alfa, + 0, + tilt, + shear, + np.cos(mu), + np.sin(mu), + tsr, + (pitch_in) + np.deg2rad(beta), + mu, + ) + x0 = 0.1 + [ct, infodict, ier, mesg] = fsolve( + get_ct, x0, args=data, full_output=True, factor=0.1 + ) cp0 = find_cp( sigma, cd, @@ -576,25 +709,25 @@ def get_pitch(x,*data): np.cos(mu), np.sin(mu), tsr, - (pitch_in)+np.deg2rad(beta), + (pitch_in) + np.deg2rad(beta), mu, - ct + ct, ) # Ratio yawed / unyawed - eta_p = cp/cp0 + eta_p = cp / cp0 interp = RegularGridInterpolator( (np.squeeze((tsr_i)), np.squeeze((pitch_i))), cp_i, bounds_error=False, - fill_value=None + fill_value=None, ) - Cp_now = interp((tsr,x),method='cubic') - cp_g1 = Cp_now*eta_p - aero_pow = 0.5*air_density*(np.pi*R**2)*(u)**3*cp_g1 - electric_pow = torque_nm*(omega_rpm*np.pi/30) + Cp_now = interp((tsr, x), method="cubic") + cp_g1 = Cp_now * eta_p + aero_pow = 0.5 * air_density * (np.pi * R**2) * (u)**3 * cp_g1 + electric_pow = torque_nm * (omega_rpm * np.pi / 30) y = aero_pow - electric_pow return y @@ -603,50 +736,53 @@ def get_pitch(x,*data): pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" LUT = np.load(lut_file) - cp_i = LUT['cp_lut'] - pitch_i = LUT['pitch_lut'] - tsr_i = LUT['tsr_lut'] + cp_i = LUT["cp_lut"] + pitch_i = LUT["pitch_lut"] + tsr_i = LUT["tsr_lut"] idx = np.squeeze(np.where(cp_i == np.max(cp_i))) tsr_opt = tsr_i[idx[0]] pitch_opt = pitch_i[idx[1]] - max_cp = cp_i[idx[0],idx[1]] + max_cp = cp_i[idx[0], idx[1]] - omega_cut_in = 0 # RPM - omega_max = power_thrust_table["rated_rpm"] # RPM + omega_cut_in = 0 # RPM + omega_max = power_thrust_table["rated_rpm"] # RPM rated_power_aero = ( - power_thrust_table["rated_power"]/power_thrust_table["generator_efficiency"] # kW + power_thrust_table["rated_power"] + / power_thrust_table["generator_efficiency"] # kW ) * 1000 # Compute torque-rpm relation and check for region 2-and-a-half Region2andAhalf = False - omega_array = np.linspace(omega_cut_in, omega_max, 161)*np.pi/30 # rad/s - Q = (0.5*air_density*omega_array**2*R**5 * np.pi * max_cp ) / tsr_opt**3 + omega_array = np.linspace(omega_cut_in, omega_max, 161) * np.pi / 30 # rad/s + Q = (0.5 * air_density * omega_array**2 * R**5 * np.pi * max_cp) / tsr_opt**3 - Paero_array = Q*omega_array + Paero_array = Q * omega_array - if Paero_array[-1] < rated_power_aero: # then we have region 2-and-a-half + if Paero_array[-1] < rated_power_aero: # then we have region 2-and-a-half Region2andAhalf = True - Q_extra = rated_power_aero/(omega_max*np.pi/30) - Q = np.append(Q,Q_extra) + Q_extra = rated_power_aero / (omega_max * np.pi / 30) + Q = np.append(Q, Q_extra) # TODO: Expression below is not assigned to anything. Should this be removed? - (Paero_array[-1]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) - omega_array = np.append(omega_array,omega_array[-1]) - Paero_array = np.append(Paero_array,rated_power_aero) - else: # limit aero_power to the last Q*omega_max + (Paero_array[-1] / (0.5 * air_density * np.pi * R**2 * max_cp)) ** (1 / 3) + omega_array = np.append(omega_array, omega_array[-1]) + Paero_array = np.append(Paero_array, rated_power_aero) + else: # limit aero_power to the last Q*omega_max rated_power_aero = Paero_array[-1] - u_rated = (rated_power_aero/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) + u_rated = (rated_power_aero / (0.5 * air_density * np.pi * R**2 * max_cp)) ** ( + 1 / 3 + ) u_array = np.linspace(3, 25, 45) idx = np.argmin(np.abs(u_array - u_rated)) if u_rated > u_array[idx]: - u_array = np.insert(u_array,idx+1,u_rated) + u_array = np.insert(u_array, idx + 1, u_rated) else: - u_array = np.insert(u_array,idx,u_rated) + u_array = np.insert(u_array, idx, u_rated) pow_lut_omega = Paero_array - omega_lut_pow = omega_array*30/np.pi + omega_lut_pow = omega_array * 30 / np.pi torque_lut_omega = Q omega_lut_torque = omega_lut_pow @@ -656,50 +792,89 @@ def get_pitch(x,*data): u_rated = np.zeros_like(rotor_average_velocities) for i in np.arange(num_rows): for j in np.arange(num_cols): - omega_rated[i,j] = ( - np.interp(power_demanded[i,j], pow_lut_omega, omega_lut_pow)*np.pi/30 #rad/s + omega_rated[i, j] = ( + np.interp(power_demanded[i, j], pow_lut_omega, omega_lut_pow) + * np.pi + / 30 # rad/s + ) + u_rated[i, j] = ( + (power_demanded[i, j] / (0.5 * air_density * np.pi * R**2 * max_cp)) + ** (1 / 3) ) - u_rated[i,j] = (power_demanded[i,j]/(0.5*air_density*np.pi*R**2*max_cp))**(1/3) pitch_out = np.zeros_like(rotor_average_velocities) tsr_out = np.zeros_like(rotor_average_velocities) for i in np.arange(num_rows): - yaw = yaw_angles[i,:] - tilt = tilt_angles[i,:] - k = shear[i,:] + yaw = yaw_angles[i, :] + tilt = tilt_angles[i, :] + k = shear[i, :] for j in np.arange(num_cols): - u_v = rotor_average_velocities[i,j] - if u_v > u_rated[i,j]: - tsr_v = omega_rated[i,j]*R/u_v*np.cos(np.deg2rad(yaw[j]))**0.5 + u_v = rotor_average_velocities[i, j] + if u_v > u_rated[i, j]: + tsr_v = ( + omega_rated[i, j] * R / u_v * np.cos(np.deg2rad(yaw[j])) ** 0.5 + ) else: - tsr_v = tsr_opt*np.cos(np.deg2rad(yaw[j])) - if Region2andAhalf: # fix for interpolation + tsr_v = tsr_opt * np.cos(np.deg2rad(yaw[j])) + if Region2andAhalf: # fix for interpolation omega_lut_torque[-1] = omega_lut_torque[-1] + 1e-2 omega_lut_pow[-1] = omega_lut_pow[-1] + 1e-2 - data = (air_density, R, sigma, k[j], cd, cl_alfa, beta, yaw[j], tilt[j], u_v, - pitch_opt, omega_lut_pow, torque_lut_omega, cp_i, pitch_i, tsr_i) + data = ( + air_density, + R, + sigma, + k[j], + cd, + cl_alfa, + beta, + yaw[j], + tilt[j], + u_v, + pitch_opt, + omega_lut_pow, + torque_lut_omega, + cp_i, + pitch_i, + tsr_i, + ) [tsr_out_soluzione, infodict, ier, mesg] = fsolve( get_tsr, tsr_v, args=data, full_output=True ) # check if solution was possible. If not, we are in region 3 - if (np.abs(infodict['fvec']) > 10 or tsr_out_soluzione < 4): + if np.abs(infodict["fvec"]) > 10 or tsr_out_soluzione < 4: tsr_out_soluzione = 1000 # save solution tsr_outO = tsr_out_soluzione - omega = tsr_outO*u_v/R + omega = tsr_outO * u_v / R # check if we are in region 2 or 3 - if omega < omega_rated[i,j]: # region 2 + if omega < omega_rated[i, j]: # region 2 # Define optimum pitch pitch_out0 = pitch_opt - else: # region 3 - tsr_outO = omega_rated[i,j]*R/u_v - data = (air_density, R, sigma, k[j], cd, cl_alfa, beta, yaw[j], tilt[j], u_v, - omega_rated[i,j], omega_array, Q, cp_i, pitch_i, tsr_i) + else: # region 3 + tsr_outO = omega_rated[i, j] * R / u_v + data = ( + air_density, + R, + sigma, + k[j], + cd, + cl_alfa, + beta, + yaw[j], + tilt[j], + u_v, + omega_rated[i, j], + omega_array, + Q, + cp_i, + pitch_i, + tsr_i, + ) # solve aero-electrical power balance with TSR from rated omega [pitch_out_soluzione, infodict, ier, mesg] = fsolve( get_pitch, @@ -708,59 +883,120 @@ def get_pitch(x,*data): factor=0.1, full_output=True, xtol=1e-10, - maxfev=2000 + maxfev=2000, ) if pitch_out_soluzione < pitch_opt: pitch_out_soluzione = pitch_opt pitch_out0 = pitch_out_soluzione # pitch and tsr will be used to compute Cp and Ct - pitch_out[i,j] = np.squeeze(pitch_out0) - tsr_out[i,j] = tsr_outO + pitch_out[i, j] = np.squeeze(pitch_out0) + tsr_out[i, j] = tsr_outO return pitch_out, tsr_out -def find_cp(sigma,cd,cl_alfa,gamma,delta,k,cosMu,sinMu,tsr,theta,MU,ct): - #add a small misalignment in case MU = 0 to avoid division by 0 + +def find_cp(sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU, ct): + # add a small misalignment in case MU = 0 to avoid division by 0 if MU == 0: MU = 1e-6 sinMu = np.sin(MU) cosMu = np.cos(MU) - a = 1 - ((1 + np.sqrt(1 - ct - 1/16*sinMu**2*ct**2))/(2*(1 + 1/16*ct*sinMu**2))) + a = 1 - ( + (1 + np.sqrt(1 - ct - 1 / 16 * sinMu**2 * ct**2)) + / (2 * (1 + 1 / 16 * ct * sinMu**2)) + ) SG = np.sin(np.deg2rad(gamma)) CG = np.cos(np.deg2rad(gamma)) SD = np.sin(np.deg2rad(delta)) CD = np.cos(np.deg2rad(delta)) - k_1s = -1*(15*np.pi/32*np.tan((MU+sinMu*(ct/2))/2)) - - p = sigma*((np.pi*cosMu**2*tsr*cl_alfa*(a - 1)**2 - - (tsr*cd*np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - - 8*CD*tsr*SG*k + 8*tsr**2 - ) - )/16 - - (np.pi*tsr*sinMu**2*cd)/2 - - (2*np.pi*cosMu*tsr**2*cl_alfa*theta)/3 - + (np.pi*cosMu**2*k_1s**2*tsr*a**2*cl_alfa)/4 - + (2*np.pi*cosMu*tsr**2*a*cl_alfa*theta)/3 - + (2*np.pi*CD*cosMu*tsr*SG*cl_alfa*k*theta)/3 - + ((CD**2*cosMu**2*tsr*cl_alfa*k**2*np.pi*(a - 1)**2*(CG**2*SD**2 + SG**2)) - /(4*sinMu**2) - ) - - (2*np.pi*CD*cosMu*tsr*SG*a*cl_alfa*k*theta)/3 - + ((CD**2*cosMu**2*k_1s**2*tsr*a**2*cl_alfa*k**2*np.pi - *(3*CG**2*SD**2 + SG**2) - )/(24*sinMu**2) - ) - - (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu**2*k_1s*tsr*SD*a**2*cl_alfa*k)/sinMu - + (np.pi*CD*CG*cosMu*k_1s*tsr**2*SD*a*cl_alfa*k*theta)/(5*sinMu) - - (np.pi*CD**2*CG*cosMu*k_1s*tsr*SD*SG*a*cl_alfa*k**2*theta)/(10*sinMu) - )/(2*np.pi) - ) + k_1s = -1 * (15 * np.pi / 32 * np.tan((MU + sinMu * (ct / 2)) / 2)) + + p = sigma * ( + ( + np.pi + * cosMu**2 + * tsr + * cl_alfa + * (a - 1) ** 2 + - ( + tsr + * cd + * np.pi + * ( + CD + **2 + * CG**2 + * SD**2 + * k**2 + + 3 * CD**2 * SG**2 * k**2 + - 8 * CD * tsr * SG * k + + 8 * tsr**2 + ) + ) + / 16 + - (np.pi * tsr * sinMu**2 * cd) / 2 + - (2 * np.pi * cosMu * tsr**2 * cl_alfa * theta) / 3 + + (np.pi * cosMu**2 * k_1s**2 * tsr * a**2 * cl_alfa) / 4 + + (2 * np.pi * cosMu * tsr**2 * a * cl_alfa * theta) / 3 + + (2 * np.pi * CD * cosMu * tsr * SG * cl_alfa * k * theta) / 3 + + ( + ( + CD + **2 + * cosMu**2 + * tsr + * cl_alfa + * k**2 + * np.pi + * (a - 1) ** 2 + * (CG**2 * SD**2 + SG**2) + ) + / (4 * sinMu**2) + ) + - (2 * np.pi * CD * cosMu * tsr * SG * a * cl_alfa * k * theta) / 3 + + ( + ( + CD + **2 + * cosMu**2 + * k_1s**2 + * tsr + * a**2 + * cl_alfa + * k**2 + * np.pi + * (3 * CG**2 * SD**2 + SG**2) + ) + / (24 * sinMu**2) + ) + - (np.pi * CD * CG * cosMu**2 * k_1s * tsr * SD * a * cl_alfa * k) / sinMu + + (np.pi * CD * CG * cosMu**2 * k_1s * tsr * SD * a**2 * cl_alfa * k) + / sinMu + + (np.pi * CD * CG * cosMu * k_1s * tsr**2 * SD * a * cl_alfa * k * theta) + / (5 * sinMu) + - ( + np.pi + * CD**2 + * CG + * cosMu + * k_1s + * tsr + * SD + * SG + * a + * cl_alfa + * k**2 + * theta + ) + / (10 * sinMu) + ) + / (2 * np.pi) + ) return p -def get_ct(x,*data): + +def get_ct(x, *data): sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU = data # Add a small misalignment in case MU = 0 to avoid division by 0 if MU == 0: @@ -771,16 +1007,36 @@ def get_ct(x,*data): CG = np.cos(np.deg2rad(gamma)) SD = np.sin(np.deg2rad(delta)) SG = np.sin(np.deg2rad(gamma)) - a = (1 - ((1 + np.sqrt(1 - x - 1/16*x**2*sinMu**2))/(2*(1 + 1/16*x*sinMu**2)))) - k_1s = -1*(15*np.pi/32*np.tan((MU + sinMu*(x/2))/2)) - I1 = -(np.pi*cosMu*(tsr - CD*SG*k)*(a - 1) - + (CD*CG*cosMu*k_1s*SD*a*k*np.pi*(2*tsr - CD*SG*k))/(8*sinMu) - )/(2*np.pi) - I2 = (np.pi*sinMu**2 + (np.pi*(CD**2*CG**2*SD**2*k**2 - + 3*CD**2*SG**2*k**2 - 8*CD*tsr*SG*k - + 8*tsr**2 - ) - )/12 - )/(2*np.pi) - - return (sigma*(cd + cl_alfa)*(I1) - sigma*cl_alfa*theta*(I2)) - x + a = 1 - ( + (1 + np.sqrt(1 - x - 1 / 16 * x**2 * sinMu**2)) + / (2 * (1 + 1 / 16 * x * sinMu**2)) + ) + k_1s = -1 * (15 * np.pi / 32 * np.tan((MU + sinMu * (x / 2)) / 2)) + I1 = -( + np.pi + * cosMu + * (tsr - CD * SG * k) + * (a - 1) + + (CD * CG * cosMu * k_1s * SD * a * k * np.pi * (2 * tsr - CD * SG * k)) + / (8 * sinMu) + ) / (2 * np.pi) + I2 = ( + np.pi + * sinMu**2 + + ( + np.pi + * ( + CD + **2 + * CG**2 + * SD**2 + * k**2 + + 3 * CD**2 * SG**2 * k**2 + - 8 * CD * tsr * SG * k + + 8 * tsr**2 + ) + ) + / 12 + ) / (2 * np.pi) + + return (sigma * (cd + cl_alfa) * (I1) - sigma * cl_alfa * theta * (I2)) - x From 92d1720855c268990bdb7a3d4f95536aace754ed Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 Nov 2024 16:25:37 -0700 Subject: [PATCH 41/53] Specify data file on turbine yaml; update to type_dec to allow strings --- floris/core/turbine/tum_operation_model.py | 7 +++---- floris/core/turbine/turbine.py | 5 ++++- floris/turbine_library/iea_10MW.yaml | 1 + floris/turbine_library/iea_15MW.yaml | 1 + floris/turbine_library/iea_3MW.yaml | 1 + floris/turbine_library/nrel_5MW.yaml | 1 + floris/type_dec.py | 10 ++++++---- tests/conftest.py | 1 + 8 files changed, 18 insertions(+), 9 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index ca7f6a0ab..2a79aaaf4 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -204,9 +204,8 @@ def power( ratio = p / p0 # Load Cp surface data and construct interpolator - # TODO: remove hardcoding pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" + lut_file = pkgroot / "turbine_library" / power_thrust_table["cp_ct_data_file"] LUT = np.load(lut_file) cp_i = LUT["cp_lut"] pitch_i = LUT["pitch_lut"] @@ -376,7 +375,7 @@ def thrust_coefficient( # Load Ct surface data and construct interpolator # TODO: remove hardcoding pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" + lut_file = pkgroot / "turbine_library" / power_thrust_table["cp_ct_data_file"] LUT = np.load(lut_file) ct_i = LUT["ct_lut"] pitch_i = LUT["pitch_lut"] @@ -734,7 +733,7 @@ def get_pitch(x, *data): # TODO: generalize .npz file loading pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / "LUT_iea15MW.npz" + lut_file = pkgroot / "turbine_library" / power_thrust_table["cp_ct_data_file"] LUT = np.load(lut_file) cp_i = LUT["cp_lut"] pitch_i = LUT["pitch_lut"] diff --git a/floris/core/turbine/turbine.py b/floris/core/turbine/turbine.py index a400eaf12..df164e930 100644 --- a/floris/core/turbine/turbine.py +++ b/floris/core/turbine/turbine.py @@ -510,7 +510,10 @@ def __post_init__(self) -> None: if self.multi_dimensional_cp_ct: self._initialize_multidim_power_thrust_table() else: - self.power_thrust_table = floris_numeric_dict_converter(self.power_thrust_table) + self.power_thrust_table = floris_numeric_dict_converter( + self.power_thrust_table, + allow_strings=True + ) def _initialize_power_thrust_functions(self) -> None: turbine_function_model = TURBINE_MODEL_MAP["operation_model"][self.operation_model] diff --git a/floris/turbine_library/iea_10MW.yaml b/floris/turbine_library/iea_10MW.yaml index 6515c4b66..01740dad7 100644 --- a/floris/turbine_library/iea_10MW.yaml +++ b/floris/turbine_library/iea_10MW.yaml @@ -31,6 +31,7 @@ power_thrust_table: beta: -3.8233819218614817 cd: 0.004612981322772105 cl_alfa: 4.602140680380394 + cp_ct_data_file: "LUT_iea15MW.npz" # Power and thrust curves power: diff --git a/floris/turbine_library/iea_15MW.yaml b/floris/turbine_library/iea_15MW.yaml index 8321f460b..082825e38 100644 --- a/floris/turbine_library/iea_15MW.yaml +++ b/floris/turbine_library/iea_15MW.yaml @@ -33,6 +33,7 @@ power_thrust_table: beta: -3.098605491003358 cd: 0.004426686198054057 cl_alfa: 4.546410770937916 + cp_ct_data_file: "LUT_iea15MW.npz" # Power and thrust curves power: diff --git a/floris/turbine_library/iea_3MW.yaml b/floris/turbine_library/iea_3MW.yaml index 1867bc900..510a83eb2 100644 --- a/floris/turbine_library/iea_3MW.yaml +++ b/floris/turbine_library/iea_3MW.yaml @@ -36,6 +36,7 @@ power_thrust_table: generator_efficiency: 0.925 rated_power: 3300.0 rotor_diameter: 130 + cp_ct_data_file: "LUT_iea15MW.npz" beta: -3.11 cd: 0.0051 cl_alfa: 4.75 diff --git a/floris/turbine_library/nrel_5MW.yaml b/floris/turbine_library/nrel_5MW.yaml index 90dd56d17..e830230b3 100644 --- a/floris/turbine_library/nrel_5MW.yaml +++ b/floris/turbine_library/nrel_5MW.yaml @@ -65,6 +65,7 @@ power_thrust_table: beta: -0.45891 cd: 0.0040638 cl_alfa: 4.275049 + cp_ct_data_file: "LUT_iea15MW.npz" ### Power thrust table data # wind speeds for look-up tables of power and thrust_coefficient diff --git a/floris/type_dec.py b/floris/type_dec.py index 319a09917..f41a416d2 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -59,7 +59,7 @@ def floris_array_converter(data: Iterable) -> np.ndarray: raise TypeError(e.args[0] + f". Data given: {data}") return a -def floris_numeric_dict_converter(data: dict) -> dict: +def floris_numeric_dict_converter(data: dict, allow_strings=False) -> 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 @@ -79,9 +79,11 @@ def floris_numeric_dict_converter(data: dict) -> dict: 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) + else: # Iterable so convert to Numpy array + if allow_strings and isinstance(v, str): + converted_dict[k] = v + else: + 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 0b03ac597..532fb6e7d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -401,6 +401,7 @@ def __init__(self): "beta": -0.45891, "cd": 0.0040638, "cl_alfa": 4.275049, + "cp_ct_data_file": "LUT_iea15MW.npz" } self.turbine_floating = copy.deepcopy(self.turbine) From 77ec24df5003793b7d74ba264f5c7b01a8390881 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 6 Nov 2024 13:42:58 -0700 Subject: [PATCH 42/53] Revert "Set reference_wind_height explicitly to avoid warning." This reverts commit 0ced9d2fd0696e326892ff55477a3104159ae6e7. --- floris/floris_model.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index f87a2c8ce..09a5aa5d0 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -1542,10 +1542,7 @@ def set_operation_model(self, operation_model: str | List[str]): # Set a single one here, then, and return turbine_type = self.core.farm.turbine_definitions[0] turbine_type["operation_model"] = operation_model - self.set( - turbine_type=[turbine_type], - reference_wind_height=self.core.flow_field.reference_wind_height - ) + self.set(turbine_type=[turbine_type]) return else: operation_model = [operation_model]*self.core.farm.n_turbines @@ -1564,10 +1561,7 @@ def set_operation_model(self, operation_model: str | List[str]): ) turbine_type_list[tindex]["operation_model"] = operation_model[tindex] - self.set( - turbine_type=turbine_type_list, - reference_wind_height=self.core.flow_field.reference_wind_height - ) + self.set(turbine_type=turbine_type_list) def copy(self): """Create an independent copy of the current FlorisModel object""" From 1f63aecb75aa151357e63f7011c725dc1b4e9fe6 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 6 Nov 2024 15:03:08 -0700 Subject: [PATCH 43/53] Raise error if velocities provided are insufficient to get shear information; add tests for range of wind speeds and grid points. --- floris/core/turbine/tum_operation_model.py | 10 +- tests/tum_operation_model_unit_test.py | 106 +++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index 2a79aaaf4..9c2e42e18 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -445,6 +445,14 @@ def compute_local_vertical_shear(velocities): Called to evaluate the vertical (linear) shear that each rotor experience, based on the inflow velocity. This allows to make the power curve asymmetric w.r.t. yaw misalignment. """ + # Check that there is a vertical profile to compute a shear profile for. If not, + # raise an error. + if velocities.shape[3] == 1: + raise ValueError(( + "The TUMLossModel compute a local shear based on inflow wind speeds across the " + "rotor. The provided velocities does not contain a vertical profile." + " This can occur if n_grid is set to 1 in the FLORIS input yaml." + )) num_rows, num_cols = velocities.shape[:2] shear = np.zeros((num_rows, num_cols)) for i in np.arange(num_rows): @@ -890,7 +898,7 @@ def get_pitch(x, *data): # pitch and tsr will be used to compute Cp and Ct pitch_out[i, j] = np.squeeze(pitch_out0) - tsr_out[i, j] = tsr_outO + tsr_out[i, j] = np.squeeze(tsr_outO) return pitch_out, tsr_out diff --git a/tests/tum_operation_model_unit_test.py b/tests/tum_operation_model_unit_test.py index cc3578e22..cdc842df7 100644 --- a/tests/tum_operation_model_unit_test.py +++ b/tests/tum_operation_model_unit_test.py @@ -252,3 +252,109 @@ def test_TUMLossTurbine_regression(): assert np.allclose(power, power_base) assert np.allclose(thrust_coefficient, thrust_coefficient_base) assert np.allclose(axial_induction, axial_induction_base) + +def test_TUMLossTurbine_integration(): + """ + Test the TUMLossTurbine model with a range of wind speeds, and then + whether it works regardless of number of grid points. + """ + + n_turbines = 1 + turbine_data = SampleInputs().turbine + turbine_data["power_thrust_table"] = SampleInputs().tum_loss_turbine_power_thrust_table + + N_test = 20 + tilt_angles_nom = turbine_data["power_thrust_table"]["ref_tilt"] * np.ones((N_test, n_turbines)) + power_setpoints_nom = POWER_SETPOINT_DEFAULT * np.ones((N_test, n_turbines)) + + # Check runs over a range of wind speeds + wind_speeds = np.linspace(1, 30, N_test) + wind_speeds = np.tile(wind_speeds[:,None,None,None], (1, 1, 3, 3)) + + power0 = TUMLossTurbine.power( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speeds, + air_density=1.1, + yaw_angles=0 * np.ones((N_test, n_turbines)), + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ).squeeze() + + power20 = TUMLossTurbine.power( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speeds, + air_density=1.1, + yaw_angles=20 * np.ones((N_test, n_turbines)), + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ).squeeze() + + assert (power0 - power20 >= -1e6).all() + + # Won't compare; just checking runs as expected + TUMLossTurbine.thrust_coefficient( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speeds, + air_density=1.1, + yaw_angles=0 * np.ones((N_test, n_turbines)), + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ).squeeze() + + TUMLossTurbine.thrust_coefficient( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speeds, + air_density=1.1, + yaw_angles=20 * np.ones((N_test, n_turbines)), + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ).squeeze() + + # Try a set of wind speeds for 5 grid points; then 2; then a single grid point + # without any shear + N_test = 1 + n_turbines = 1 + tilt_angles_nom = turbine_data["power_thrust_table"]["ref_tilt"] * np.ones((N_test, n_turbines)) + power_setpoints_nom = POWER_SETPOINT_DEFAULT * np.ones((N_test, n_turbines)) + + + wind_speeds = 10.0 * np.ones((N_test, n_turbines, 5, 5)) + power5gp = TUMLossTurbine.power( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speeds, + air_density=1.1, + yaw_angles=0 * np.ones((N_test, n_turbines)), + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ).squeeze() + + wind_speeds = 10.0 * np.ones((N_test, n_turbines, 2, 2)) + power2gp = TUMLossTurbine.power( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speeds, + air_density=1.1, + yaw_angles=0 * np.ones((N_test, n_turbines)), + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ).squeeze() + + assert np.allclose(power5gp, power2gp) + + # No shear information for the TUM model to use + wind_speeds = 10.0 * np.ones((N_test, n_turbines, 1, 1)) + with pytest.raises(ValueError): + TUMLossTurbine.power( + power_thrust_table=turbine_data["power_thrust_table"], + velocities=wind_speeds, + air_density=1.1, + yaw_angles=0 * np.ones((N_test, n_turbines)), + power_setpoints=power_setpoints_nom, + tilt_angles=tilt_angles_nom, + tilt_interp=None + ) From 655cf0c9fdfa092ec5e0c47d2e95e37d10bee510 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 6 Nov 2024 15:22:49 -0700 Subject: [PATCH 44/53] Add description in documentation (still to build out). --- docs/operation_models_user.ipynb | 81 ++++++++++++++++++- docs/references.bib | 12 +++ .../001_compare_yaw_loss.py | 4 +- 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/docs/operation_models_user.ipynb b/docs/operation_models_user.ipynb index 6ad796d68..567f2820a 100644 --- a/docs/operation_models_user.ipynb +++ b/docs/operation_models_user.ipynb @@ -460,10 +460,87 @@ "ax[1].set_ylabel(\"Power [kW]\")" ] }, + { + "cell_type": "markdown", + "id": "92912bf7", + "metadata": {}, + "source": [ + "### TUM loss model\n", + "\n", + "User-level name: `\"tum-loss\"`\n", + "\n", + "Underlying class: `TUMLossTurbine`\n", + "\n", + "Required data on `power_thrust_table`:\n", + "- `rotor_solidity`: (scalar)\n", + "- `rated_rpm`: (scalar)\n", + "- `generator_efficiency`: (scalar)\n", + "- `rated_power`: (scalar)\n", + "- `rotor_diameter`: (scalar)\n", + "- `beta`: (scalar)\n", + "- `cd`: (scalar)\n", + "- `cl_alfa`: (scalar)\n", + "- `cp_ct_data_file`: (string)\n", + "\n", + "The `\"tum-loss\"` operation model is an advanced operation model that uses the turbine's Cp/Ct\n", + "surface to optimize performance under yaw misalignment.\n", + "\n", + "TODO: add description of the model at a high level, and also what each of the parameters listed above (`rotor_solidity`, `rated_rpm`, etc) is used for in the model.\n", + "\n", + "Details can be found in {cite:t}`tamaro2024beyondcosine`." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "92912bf7", + "id": "6e4c5ba2", + "metadata": {}, + "outputs": [], + "source": [ + "N = 101 # How many steps to cover yaw range in\n", + "yaw_max = 30 # Maximum yaw to test\n", + "\n", + "# Set up the yaw angle sweep\n", + "yaw_angles = np.linspace(-yaw_max, yaw_max, N).reshape(-1,1)\n", + "\n", + "# We can use the same FlorisModel, but we'll set it up for this test\n", + "fmodel.set(\n", + " wind_data=TimeSeries(\n", + " wind_speeds=np.ones(N) * 8.0,\n", + " wind_directions=np.ones(N) * 270.0,\n", + " turbulence_intensities=0.06\n", + " ),\n", + " yaw_angles=yaw_angles,\n", + ")\n", + "\n", + "# We'll compare the \"tum-loss\" model to the standard \"cosine-loss\" model\n", + "op_models = [\"cosine-loss\", \"tum-loss\"]\n", + "results = {}\n", + "\n", + "for op_model in op_models:\n", + " print(f\"Evaluating model: {op_model}\")\n", + " fmodel.set_operation_model(op_model)\n", + "\n", + " fmodel.run()\n", + " results[op_model] = fmodel.get_turbine_powers().squeeze()\n", + "\n", + "fig, ax = plt.subplots()\n", + "colors = [\"C0\", \"k\"]\n", + "linestyles = [\"solid\", \"dashed\"]\n", + "for key, c, ls in zip(results, colors, linestyles):\n", + " central_power = results[key][yaw_angles.squeeze() == 0]\n", + " ax.plot(yaw_angles.squeeze(), results[key]/central_power, label=key, color=c, linestyle=ls)\n", + "\n", + "ax.grid(True)\n", + "ax.legend()\n", + "ax.set_xlabel(\"Yaw angle [deg]\")\n", + "ax.set_ylabel(\"Normalized turbine power [deg]\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90d5c155", "metadata": {}, "outputs": [], "source": [] @@ -485,7 +562,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.10.6" } }, "nbformat": 4, diff --git a/docs/references.bib b/docs/references.bib index da217dd4f..f8cc8491e 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -299,3 +299,15 @@ @article{SinnerFleming2024grs title = {Robust wind farm layout optimization}, journal = {Journal of Physics: Conference Series}, } + +@Article{tamaro2024beyondcosine, +AUTHOR = {Tamaro, S. and Campagnolo, F. and Bottasso, C. L.}, +TITLE = {On the power and control of a misaligned rotor -- beyond the cosine law}, +JOURNAL = {Wind Energy Science}, +VOLUME = {9}, +YEAR = {2024}, +NUMBER = {7}, +PAGES = {1547--1575}, +URL = {https://wes.copernicus.org/articles/9/1547/2024/}, +DOI = {10.5194/wes-9-1547-2024} +} diff --git a/examples/examples_operation_models/001_compare_yaw_loss.py b/examples/examples_operation_models/001_compare_yaw_loss.py index 6eac9fb56..0c185c9cd 100644 --- a/examples/examples_operation_models/001_compare_yaw_loss.py +++ b/examples/examples_operation_models/001_compare_yaw_loss.py @@ -44,8 +44,8 @@ # Plot the results fig, ax = plt.subplots() -colors = ["C0", "k", "r"] -linestyles = ["solid", "dashed", "dotted"] +colors = ["C0", "k"] +linestyles = ["solid", "dashed"] for key, c, ls in zip(results, colors, linestyles): central_power = results[key][yaw_angles.squeeze() == 0] ax.plot(yaw_angles.squeeze(), results[key]/central_power, label=key, color=c, linestyle=ls) From 47170f8a77fcdf550429fd701435ec048ef7e86c Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 7 Nov 2024 11:42:52 -0700 Subject: [PATCH 45/53] Simplifying formatting; adding references to equations in paper where appropriate. --- floris/core/turbine/tum_operation_model.py | 256 +++++++++------------ 1 file changed, 109 insertions(+), 147 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index 9c2e42e18..cfce1e0b2 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -70,7 +70,7 @@ def power( ) # Compute power - num_rows, num_cols = tilt_angles.shape + n_findex, n_turbines = tilt_angles.shape shear = TUMLossTurbine.compute_local_vertical_shear(velocities) @@ -98,34 +98,30 @@ def power( theta_array = np.deg2rad(pitch_out + beta) x0 = 0.2 - # Compute power in yawed conditions + ### Solve for the power in yawed conditions + # Compute overall misalignment (eq. (1) in Tamaro et al.) MU = np.arccos( np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) ) cosMu = np.cos(MU) sinMu = np.sin(MU) p = np.zeros_like(average_velocity(velocities)) - for i in np.arange(num_rows): - yaw = yaw_angles[i, :] - tilt = tilt_angles[i, :] - k = shear[i, :] - cMu = cosMu[i, :] - sMu = sinMu[i, :] - Mu = MU[i, :] - for j in np.arange(num_cols): + # Need to loop over findices to use fsolve + for i in np.arange(n_findex): + for j in np.arange(n_turbines): # Create data tuple for fsolve data = ( sigma, cd, cl_alfa, - yaw[j], - tilt[j], - k[j], - cMu[j], - sMu[j], + yaw_angles[i, j], + tilt_angles[i, j], + shear[i, j], + cosMu[i, j], + sinMu[i, j], (tsr_array[i, j]), (theta_array[i, j]), - Mu[j], + MU[i, j], ) ct, info, ier, msg = fsolve(get_ct, x0, args=data, full_output=True) if ier == 1: @@ -134,22 +130,23 @@ def power( sigma, cd, cl_alfa, - yaw[j], - tilt[j], - k[j], - cMu[j], - sMu[j], + yaw_angles[i, j], + tilt_angles[i, j], + shear[i, j], + cosMu[i, j], + sinMu[i, j], (tsr_array[i, j]), (theta_array[i, j]), - Mu[j], + MU[i, j], ct, ) ) else: p[i, j] = -1e3 - # Recompute power in non-yawed conditions + ### Solve for the power in non-yawed conditions yaw_angles = np.zeros_like(yaw_angles) + # Compute overall misalignment (eq. (1) in Tamaro et al.) MU = np.arccos( np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) ) @@ -158,26 +155,20 @@ def power( p0 = np.zeros_like((average_velocity(velocities))) - for i in np.arange(num_rows): - yaw = yaw_angles[i, :] - tilt = tilt_angles[i, :] - k = shear[i, :] - cMu = cosMu[i, :] - sMu = sinMu[i, :] - Mu = MU[i, :] - for j in np.arange(num_cols): + for i in np.arange(n_findex): + for j in np.arange(n_turbines): data = ( sigma, cd, cl_alfa, - yaw[j], - tilt[j], - k[j], - cMu[j], - sMu[j], + yaw_angles[i, j], + tilt_angles[i, j], + shear[i, j], + cosMu[i, j], + sinMu[i, j], (tsr_array[i, j]), (theta_array[i, j]), - Mu[j], + MU[i, j], ) ct, info, ier, msg = fsolve(get_ct, x0, args=data, full_output=True) if ier == 1: @@ -186,14 +177,14 @@ def power( sigma, cd, cl_alfa, - yaw[j], - tilt[j], - k[j], - cMu[j], - sMu[j], + yaw_angles[i, j], + tilt_angles[i, j], + shear[i, j], + cosMu[i, j], + sinMu[i, j], (tsr_array[i, j]), (theta_array[i, j]), - Mu[j], + MU[i, j], ct, ) ) @@ -215,8 +206,9 @@ def power( ) power_coefficient = np.zeros_like(average_velocity(velocities)) - for i in np.arange(num_rows): - for j in np.arange(num_cols): + # TODO: see if this can be vectorized + for i in np.arange(n_findex): + for j in np.arange(n_turbines): cp_interp = interp_lut( np.array([(tsr_array[i, j]), (pitch_out[i, j])]), method="cubic" ) @@ -288,6 +280,7 @@ def thrust_coefficient( ref_air_density=power_thrust_table["ref_air_density"], ) + # Apply standard control trajectory to determine pitch and TSR pitch_out, tsr_out = TUMLossTurbine.control_trajectory( rotor_effective_velocities, yaw_angles, @@ -299,44 +292,41 @@ def thrust_coefficient( power_thrust_table, ) - num_rows, num_cols = tilt_angles.shape + n_findex, n_turbines = tilt_angles.shape # u = np.squeeze(u) theta_array = np.deg2rad(pitch_out + beta) tsr_array = tsr_out - x0 = 0.2 + x0 = 0.2 # Initial guess for the thrust coefficient solve - # Compute thrust coefficient in yawed conditions + ### Solve for the thrust coefficient in yawed conditions + # Compute overall misalignment (eq. (1) in Tamaro et al.) MU = np.arccos( np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) ) cosMu = np.cos(MU) sinMu = np.sin(MU) thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) - for i in np.arange(num_rows): - yaw = yaw_angles[i, :] - tilt = tilt_angles[i, :] - cMu = cosMu[i, :] - sMu = sinMu[i, :] - Mu = MU[i, :] - for j in np.arange(num_cols): + # Need to loop over n_findex and n_turbines here to use fsolve + for i in np.arange(n_findex): + for j in np.arange(n_turbines): data = ( sigma, cd, cl_alfa, - yaw[j], - tilt[j], + yaw_angles[i, j], + tilt_angles[i, j], shear[i, j], - cMu[j], - sMu[j], + cosMu[i, j], + sinMu[i, j], (tsr_array[i, j]), (theta_array[i, j]), - Mu[j], + MU[i, j], ) - ct = fsolve(get_ct, x0, args=data) + ct = fsolve(get_ct, x0, args=data) # Solves eq. (25) from Tamaro et al. thrust_coefficient1[i, j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) - # Recompute thrust coefficient in non-yawed conditions + ### Resolve thrust coefficient in non-yawed conditions yaw_angles = np.zeros_like(yaw_angles) MU = np.arccos( np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) @@ -345,35 +335,29 @@ def thrust_coefficient( sinMu = np.sin(MU) thrust_coefficient0 = np.zeros_like(average_velocity(velocities)) - - for i in np.arange(num_rows): - yaw = yaw_angles[i, :] - tilt = tilt_angles[i, :] - cMu = cosMu[i, :] - sMu = sinMu[i, :] - Mu = MU[i, :] - for j in np.arange(num_cols): + # Need to loop over n_findex and n_turbines here to use fsolve + for i in np.arange(n_findex): + for j in np.arange(n_turbines): data = ( sigma, cd, cl_alfa, - yaw[j], - tilt[j], + yaw_angles[i, j], + tilt_angles[i, j], shear[i, j], - cMu[j], - sMu[j], + cosMu[i, j], + sinMu[i, j], (tsr_array[i, j]), (theta_array[i, j]), - Mu[j], + MU[i, j], ) - ct = fsolve(get_ct, x0, args=data) + ct = fsolve(get_ct, x0, args=data) # Solves eq. (25) from Tamaro et al. thrust_coefficient0[i, j] = np.squeeze(ct) # np.clip(ct, 0.0001, 0.9999) # Compute ratio of yawed to unyawed thrust coefficients - ratio = thrust_coefficient1 / thrust_coefficient0 + ratio = thrust_coefficient1 / thrust_coefficient0 # See above eq. (29) # Load Ct surface data and construct interpolator - # TODO: remove hardcoding pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] lut_file = pkgroot / "turbine_library" / power_thrust_table["cp_ct_data_file"] LUT = np.load(lut_file) @@ -385,9 +369,10 @@ def thrust_coefficient( ) # *0.9722085500886761) # Interpolate and apply ratio to determine thrust coefficient + # TODO: see if this can be vectorized thrust_coefficient = np.zeros_like(average_velocity(velocities)) - for i in np.arange(num_rows): - for j in np.arange(num_cols): + for i in np.arange(n_findex): + for j in np.arange(n_turbines): ct_interp = interp_lut( np.array([(tsr_array[i, j]), (pitch_out[i, j])]), method="cubic" ) @@ -407,7 +392,7 @@ def axial_induction( correct_cp_ct_for_tilt: bool = False, **_, # <- Allows other models to accept other keyword arguments ): - num_rows, num_cols = tilt_angles.shape + n_findex, n_turbines = tilt_angles.shape thrust_coefficients = TUMLossTurbine.thrust_coefficient( power_thrust_table=power_thrust_table, velocities=velocities, @@ -420,17 +405,21 @@ def axial_induction( correct_cp_ct_for_tilt=correct_cp_ct_for_tilt, ) + # TODO: should the axial induction calculation be based on MU for zero yaw (as it is + # currently) or should this be the actual yaw angle? yaw_angles = np.zeros_like(yaw_angles) MU = np.arccos( np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) ) sinMu = np.sin(MU) - sMu = sinMu[-1, :] + sMu = sinMu[-1, :] # all the same in this case anyway (since yaw zero) - axial_induction = np.zeros((num_rows, num_cols)) - for i in np.arange(num_rows): - for j in np.arange(num_cols): + # TODO: see if this can be vectorized + axial_induction = np.zeros((n_findex, n_turbines)) + for i in np.arange(n_findex): + for j in np.arange(n_turbines): ct = thrust_coefficients[i, j] + # Eq. (25a) from Tamaro et al. a = 1 - ( (1 + np.sqrt(1 - ct - 1 / 16 * ct**2 * sMu[j] ** 2)) / (2 * (1 + 1 / 16 * ct * sMu[j] ** 2)) @@ -453,10 +442,10 @@ def compute_local_vertical_shear(velocities): "rotor. The provided velocities does not contain a vertical profile." " This can occur if n_grid is set to 1 in the FLORIS input yaml." )) - num_rows, num_cols = velocities.shape[:2] - shear = np.zeros((num_rows, num_cols)) - for i in np.arange(num_rows): - for j in np.arange(num_cols): + n_findex, n_turbines = velocities.shape[:2] + shear = np.zeros((n_findex, n_turbines)) + for i in np.arange(n_findex): + for j in np.arange(n_turbines): mean_speed = np.mean(velocities[i, j, :, :], axis=0) if len(mean_speed) % 2 != 0: # odd number u_u_hh = mean_speed / mean_speed[int(np.floor(len(mean_speed) / 2))] @@ -739,7 +728,7 @@ def get_pitch(x, *data): y = aero_pow - electric_pow return y - # TODO: generalize .npz file loading + # Load Cp/Ct data pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] lut_file = pkgroot / "turbine_library" / power_thrust_table["cp_ct_data_file"] LUT = np.load(lut_file) @@ -793,12 +782,13 @@ def get_pitch(x, *data): torque_lut_omega = Q omega_lut_torque = omega_lut_pow - num_rows, num_cols = tilt_angles.shape + n_findex, n_turbines = tilt_angles.shape + # TODO: can this be vectorized? omega_rated = np.zeros_like(rotor_average_velocities) u_rated = np.zeros_like(rotor_average_velocities) - for i in np.arange(num_rows): - for j in np.arange(num_cols): + for i in np.arange(n_findex): + for j in np.arange(n_turbines): omega_rated[i, j] = ( np.interp(power_demanded[i, j], pow_lut_omega, omega_lut_pow) * np.pi @@ -812,18 +802,16 @@ def get_pitch(x, *data): pitch_out = np.zeros_like(rotor_average_velocities) tsr_out = np.zeros_like(rotor_average_velocities) - for i in np.arange(num_rows): - yaw = yaw_angles[i, :] - tilt = tilt_angles[i, :] - k = shear[i, :] - for j in np.arange(num_cols): + # Must loop to use fsolve + for i in np.arange(n_findex): + for j in np.arange(n_turbines): u_v = rotor_average_velocities[i, j] if u_v > u_rated[i, j]: tsr_v = ( - omega_rated[i, j] * R / u_v * np.cos(np.deg2rad(yaw[j])) ** 0.5 + omega_rated[i, j] * R / u_v * np.cos(np.deg2rad(yaw_angles[i, j])) ** 0.5 ) else: - tsr_v = tsr_opt * np.cos(np.deg2rad(yaw[j])) + tsr_v = tsr_opt * np.cos(np.deg2rad(yaw_angles[i, j])) if Region2andAhalf: # fix for interpolation omega_lut_torque[-1] = omega_lut_torque[-1] + 1e-2 omega_lut_pow[-1] = omega_lut_pow[-1] + 1e-2 @@ -832,12 +820,12 @@ def get_pitch(x, *data): air_density, R, sigma, - k[j], + shear[i, j], cd, cl_alfa, beta, - yaw[j], - tilt[j], + yaw_angles[i, j], + tilt_angles[i, j], u_v, pitch_opt, omega_lut_pow, @@ -868,12 +856,12 @@ def get_pitch(x, *data): air_density, R, sigma, - k[j], + shear[i, j], cd, cl_alfa, beta, - yaw[j], - tilt[j], + yaw_angles[i, j], + tilt_angles[i, j], u_v, omega_rated[i, j], omega_array, @@ -931,11 +919,7 @@ def find_cp(sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU, c * cd * np.pi * ( - CD - **2 - * CG**2 - * SD**2 - * k**2 + CD**2 * CG**2 * SD**2 * k**2 + 3 * CD**2 * SG**2 * k**2 - 8 * CD * tsr * SG * k + 8 * tsr**2 @@ -949,14 +933,7 @@ def find_cp(sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU, c + (2 * np.pi * CD * cosMu * tsr * SG * cl_alfa * k * theta) / 3 + ( ( - CD - **2 - * cosMu**2 - * tsr - * cl_alfa - * k**2 - * np.pi - * (a - 1) ** 2 + CD**2 * cosMu**2 * tsr * cl_alfa * k**2 * np.pi * (a - 1)**2 * (CG**2 * SD**2 + SG**2) ) / (4 * sinMu**2) @@ -964,15 +941,7 @@ def find_cp(sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU, c - (2 * np.pi * CD * cosMu * tsr * SG * a * cl_alfa * k * theta) / 3 + ( ( - CD - **2 - * cosMu**2 - * k_1s**2 - * tsr - * a**2 - * cl_alfa - * k**2 - * np.pi + CD**2 * cosMu**2 * k_1s**2 * tsr * a**2 * cl_alfa * k**2 * np.pi * (3 * CG**2 * SD**2 + SG**2) ) / (24 * sinMu**2) @@ -982,20 +951,7 @@ def find_cp(sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU, c / sinMu + (np.pi * CD * CG * cosMu * k_1s * tsr**2 * SD * a * cl_alfa * k * theta) / (5 * sinMu) - - ( - np.pi - * CD**2 - * CG - * cosMu - * k_1s - * tsr - * SD - * SG - * a - * cl_alfa - * k**2 - * theta - ) + - (np.pi * CD**2 * CG * cosMu * k_1s * tsr * SD * SG * a * cl_alfa * k**2 * theta) / (10 * sinMu) ) / (2 * np.pi) @@ -1004,6 +960,11 @@ def find_cp(sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU, c def get_ct(x, *data): + """ + System of equations for Ct, as represented in Eq. (25) of Tamaro et al. + x is a stand-in variable for Ct, which a numerical solver will solve for. + data is a tuple of input parameters to the system of equations to solve. + """ sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU = data # Add a small misalignment in case MU = 0 to avoid division by 0 if MU == 0: @@ -1014,11 +975,15 @@ def get_ct(x, *data): CG = np.cos(np.deg2rad(gamma)) SD = np.sin(np.deg2rad(delta)) SG = np.sin(np.deg2rad(gamma)) + + # Axial induction a = 1 - ( (1 + np.sqrt(1 - x - 1 / 16 * x**2 * sinMu**2)) / (2 * (1 + 1 / 16 * x * sinMu**2)) ) + k_1s = -1 * (15 * np.pi / 32 * np.tan((MU + sinMu * (x / 2)) / 2)) + I1 = -( np.pi * cosMu @@ -1027,17 +992,14 @@ def get_ct(x, *data): + (CD * CG * cosMu * k_1s * SD * a * k * np.pi * (2 * tsr - CD * SG * k)) / (8 * sinMu) ) / (2 * np.pi) + I2 = ( np.pi * sinMu**2 + ( np.pi * ( - CD - **2 - * CG**2 - * SD**2 - * k**2 + CD**2 * CG**2 * SD**2 * k**2 + 3 * CD**2 * SG**2 * k**2 - 8 * CD * tsr * SG * k + 8 * tsr**2 From b072291390b82ce17938cd38a18addd23538a129 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 7 Nov 2024 13:17:51 -0700 Subject: [PATCH 46/53] Vectorize interpolation operations. --- floris/core/turbine/tum_operation_model.py | 67 +++++++--------------- 1 file changed, 21 insertions(+), 46 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index cfce1e0b2..b8603f073 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -206,13 +206,10 @@ def power( ) power_coefficient = np.zeros_like(average_velocity(velocities)) - # TODO: see if this can be vectorized - for i in np.arange(n_findex): - for j in np.arange(n_turbines): - cp_interp = interp_lut( - np.array([(tsr_array[i, j]), (pitch_out[i, j])]), method="cubic" - ) - power_coefficient[i, j] = np.squeeze(cp_interp * ratio[i, j]) + cp_interp = interp_lut( + np.concatenate((tsr_array[:,:,None], pitch_out[:,:,None]), axis=2), method="cubic" + ) + power_coefficient = cp_interp * ratio # TODO: make printout optional? if False: @@ -224,7 +221,7 @@ def power( * (rotor_effective_velocities)**3 * np.pi * R**2 - * (power_coefficient) + * power_coefficient * power_thrust_table["generator_efficiency"] ) return power @@ -369,14 +366,10 @@ def thrust_coefficient( ) # *0.9722085500886761) # Interpolate and apply ratio to determine thrust coefficient - # TODO: see if this can be vectorized - thrust_coefficient = np.zeros_like(average_velocity(velocities)) - for i in np.arange(n_findex): - for j in np.arange(n_turbines): - ct_interp = interp_lut( - np.array([(tsr_array[i, j]), (pitch_out[i, j])]), method="cubic" - ) - thrust_coefficient[i, j] = np.squeeze(ct_interp * ratio[i, j]) + ct_interp = interp_lut( + np.concatenate((tsr_array[:,:,None], pitch_out[:,:,None]), axis=2), method="cubic" + ) + thrust_coefficient = ct_interp * ratio return thrust_coefficient @@ -411,20 +404,14 @@ def axial_induction( MU = np.arccos( np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) ) - sinMu = np.sin(MU) - sMu = sinMu[-1, :] # all the same in this case anyway (since yaw zero) + sinMu = np.sin(MU) # all the same in this case anyway (since yaw zero) - # TODO: see if this can be vectorized - axial_induction = np.zeros((n_findex, n_turbines)) - for i in np.arange(n_findex): - for j in np.arange(n_turbines): - ct = thrust_coefficients[i, j] - # Eq. (25a) from Tamaro et al. - a = 1 - ( - (1 + np.sqrt(1 - ct - 1 / 16 * ct**2 * sMu[j] ** 2)) - / (2 * (1 + 1 / 16 * ct * sMu[j] ** 2)) - ) - axial_induction[i, j] = np.squeeze(np.clip(a, 0.0001, 0.9999)) + # Eq. (25a) from Tamaro et al. + a = 1 - ( + (1 + np.sqrt(1 - thrust_coefficients - 1 / 16 * thrust_coefficients**2 * sinMu ** 2)) + / (2 * (1 + 1 / 16 * thrust_coefficients * sinMu ** 2)) + ) + axial_induction = np.clip(a, 0.0001, 0.9999) return axial_induction @@ -784,20 +771,8 @@ def get_pitch(x, *data): n_findex, n_turbines = tilt_angles.shape - # TODO: can this be vectorized? - omega_rated = np.zeros_like(rotor_average_velocities) - u_rated = np.zeros_like(rotor_average_velocities) - for i in np.arange(n_findex): - for j in np.arange(n_turbines): - omega_rated[i, j] = ( - np.interp(power_demanded[i, j], pow_lut_omega, omega_lut_pow) - * np.pi - / 30 # rad/s - ) - u_rated[i, j] = ( - (power_demanded[i, j] / (0.5 * air_density * np.pi * R**2 * max_cp)) - ** (1 / 3) - ) + omega_rated = np.interp(power_demanded, pow_lut_omega, omega_lut_pow) * np.pi / 30 # rad/s + u_rated = (power_demanded / (0.5 * air_density * np.pi * R**2 * max_cp)) ** (1 / 3) pitch_out = np.zeros_like(rotor_average_velocities) tsr_out = np.zeros_like(rotor_average_velocities) @@ -981,9 +956,9 @@ def get_ct(x, *data): (1 + np.sqrt(1 - x - 1 / 16 * x**2 * sinMu**2)) / (2 * (1 + 1 / 16 * x * sinMu**2)) ) - + k_1s = -1 * (15 * np.pi / 32 * np.tan((MU + sinMu * (x / 2)) / 2)) - + I1 = -( np.pi * cosMu @@ -992,7 +967,7 @@ def get_ct(x, *data): + (CD * CG * cosMu * k_1s * SD * a * k * np.pi * (2 * tsr - CD * SG * k)) / (8 * sinMu) ) / (2 * np.pi) - + I2 = ( np.pi * sinMu**2 From f00a46937c4874b1c8747c8fc5786d7e23f3996f Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 8 Nov 2024 17:03:04 -0700 Subject: [PATCH 47/53] Make all functions staticmethods on TUMLossModel --- floris/core/turbine/tum_operation_model.py | 246 ++++++++++----------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index b8603f073..ded6129d8 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -123,10 +123,10 @@ def power( (theta_array[i, j]), MU[i, j], ) - ct, info, ier, msg = fsolve(get_ct, x0, args=data, full_output=True) + ct, info, ier, msg = fsolve(TUMLossTurbine.get_ct, x0, args=data, full_output=True) if ier == 1: p[i, j] = np.squeeze( - find_cp( + TUMLossTurbine.find_cp( sigma, cd, cl_alfa, @@ -170,10 +170,10 @@ def power( (theta_array[i, j]), MU[i, j], ) - ct, info, ier, msg = fsolve(get_ct, x0, args=data, full_output=True) + ct, info, ier, msg = fsolve(TUMLossTurbine.get_ct, x0, args=data, full_output=True) if ier == 1: p0[i, j] = np.squeeze( - find_cp( + TUMLossTurbine.find_cp( sigma, cd, cl_alfa, @@ -320,7 +320,7 @@ def thrust_coefficient( (theta_array[i, j]), MU[i, j], ) - ct = fsolve(get_ct, x0, args=data) # Solves eq. (25) from Tamaro et al. + ct = fsolve(TUMLossTurbine.get_ct, x0, args=data) # Solves eq. (25) thrust_coefficient1[i, j] = np.squeeze(np.clip(ct, 0.0001, 0.9999)) ### Resolve thrust coefficient in non-yawed conditions @@ -348,7 +348,7 @@ def thrust_coefficient( (theta_array[i, j]), MU[i, j], ) - ct = fsolve(get_ct, x0, args=data) # Solves eq. (25) from Tamaro et al. + ct = fsolve(TUMLossTurbine.get_ct, x0, args=data) # Solves eq. (25) thrust_coefficient0[i, j] = np.squeeze(ct) # np.clip(ct, 0.0001, 0.9999) # Compute ratio of yawed to unyawed thrust coefficients @@ -373,6 +373,7 @@ def thrust_coefficient( return thrust_coefficient + @staticmethod def axial_induction( power_thrust_table: dict, velocities: NDArrayFloat, @@ -385,7 +386,6 @@ def axial_induction( correct_cp_ct_for_tilt: bool = False, **_, # <- Allows other models to accept other keyword arguments ): - n_findex, n_turbines = tilt_angles.shape thrust_coefficients = TUMLossTurbine.thrust_coefficient( power_thrust_table=power_thrust_table, velocities=velocities, @@ -531,9 +531,9 @@ def get_tsr(x, *data): ) x0 = 0.1 [ct, infodict, ier, mesg] = fsolve( - get_ct, x0, args=data, full_output=True, factor=0.1 + TUMLossTurbine.get_ct, x0, args=data, full_output=True, factor=0.1 ) - cp = find_cp( + cp = TUMLossTurbine.find_cp( sigma, cd, cl_alfa, @@ -565,9 +565,9 @@ def get_tsr(x, *data): ) x0 = 0.1 [ct, infodict, ier, mesg] = fsolve( - get_ct, x0, args=data, full_output=True, factor=0.1 + TUMLossTurbine.get_ct, x0, args=data, full_output=True, factor=0.1 ) - cp0 = find_cp( + cp0 = TUMLossTurbine.find_cp( sigma, cd, cl_alfa, @@ -646,9 +646,9 @@ def get_pitch(x, *data): ) x0 = 0.1 [ct, infodict, ier, mesg] = fsolve( - get_ct, x0, args=data, full_output=True, factor=0.1 + TUMLossTurbine.get_ct, x0, args=data, full_output=True, factor=0.1 ) - cp = find_cp( + cp = TUMLossTurbine.find_cp( sigma, cd, cl_alfa, @@ -680,9 +680,9 @@ def get_pitch(x, *data): ) x0 = 0.1 [ct, infodict, ier, mesg] = fsolve( - get_ct, x0, args=data, full_output=True, factor=0.1 + TUMLossTurbine.get_ct, x0, args=data, full_output=True, factor=0.1 ) - cp0 = find_cp( + cp0 = TUMLossTurbine.find_cp( sigma, cd, cl_alfa, @@ -865,122 +865,122 @@ def get_pitch(x, *data): return pitch_out, tsr_out + @staticmethod + def find_cp(sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU, ct): + # add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) + a = 1 - ( + (1 + np.sqrt(1 - ct - 1 / 16 * sinMu**2 * ct**2)) + / (2 * (1 + 1 / 16 * ct * sinMu**2)) + ) + SG = np.sin(np.deg2rad(gamma)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + CD = np.cos(np.deg2rad(delta)) + k_1s = -1 * (15 * np.pi / 32 * np.tan((MU + sinMu * (ct / 2)) / 2)) -def find_cp(sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU, ct): - # add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - a = 1 - ( - (1 + np.sqrt(1 - ct - 1 / 16 * sinMu**2 * ct**2)) - / (2 * (1 + 1 / 16 * ct * sinMu**2)) - ) - SG = np.sin(np.deg2rad(gamma)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - CD = np.cos(np.deg2rad(delta)) - k_1s = -1 * (15 * np.pi / 32 * np.tan((MU + sinMu * (ct / 2)) / 2)) - - p = sigma * ( - ( - np.pi - * cosMu**2 - * tsr - * cl_alfa - * (a - 1) ** 2 - - ( - tsr - * cd - * np.pi - * ( - CD**2 * CG**2 * SD**2 * k**2 - + 3 * CD**2 * SG**2 * k**2 - - 8 * CD * tsr * SG * k - + 8 * tsr**2 + p = sigma * ( + ( + np.pi + * cosMu**2 + * tsr + * cl_alfa + * (a - 1) ** 2 + - ( + tsr + * cd + * np.pi + * ( + CD**2 * CG**2 * SD**2 * k**2 + + 3 * CD**2 * SG**2 * k**2 + - 8 * CD * tsr * SG * k + + 8 * tsr**2 + ) ) - ) - / 16 - - (np.pi * tsr * sinMu**2 * cd) / 2 - - (2 * np.pi * cosMu * tsr**2 * cl_alfa * theta) / 3 - + (np.pi * cosMu**2 * k_1s**2 * tsr * a**2 * cl_alfa) / 4 - + (2 * np.pi * cosMu * tsr**2 * a * cl_alfa * theta) / 3 - + (2 * np.pi * CD * cosMu * tsr * SG * cl_alfa * k * theta) / 3 - + ( - ( - CD**2 * cosMu**2 * tsr * cl_alfa * k**2 * np.pi * (a - 1)**2 - * (CG**2 * SD**2 + SG**2) + / 16 + - (np.pi * tsr * sinMu**2 * cd) / 2 + - (2 * np.pi * cosMu * tsr**2 * cl_alfa * theta) / 3 + + (np.pi * cosMu**2 * k_1s**2 * tsr * a**2 * cl_alfa) / 4 + + (2 * np.pi * cosMu * tsr**2 * a * cl_alfa * theta) / 3 + + (2 * np.pi * CD * cosMu * tsr * SG * cl_alfa * k * theta) / 3 + + ( + ( + CD**2 * cosMu**2 * tsr * cl_alfa * k**2 * np.pi * (a - 1)**2 + * (CG**2 * SD**2 + SG**2) + ) + / (4 * sinMu**2) ) - / (4 * sinMu**2) - ) - - (2 * np.pi * CD * cosMu * tsr * SG * a * cl_alfa * k * theta) / 3 - + ( - ( - CD**2 * cosMu**2 * k_1s**2 * tsr * a**2 * cl_alfa * k**2 * np.pi - * (3 * CG**2 * SD**2 + SG**2) + - (2 * np.pi * CD * cosMu * tsr * SG * a * cl_alfa * k * theta) / 3 + + ( + ( + CD**2 * cosMu**2 * k_1s**2 * tsr * a**2 * cl_alfa * k**2 * np.pi + * (3 * CG**2 * SD**2 + SG**2) + ) + / (24 * sinMu**2) ) - / (24 * sinMu**2) + - (np.pi * CD * CG * cosMu**2 * k_1s * tsr * SD * a * cl_alfa * k) / sinMu + + (np.pi * CD * CG * cosMu**2 * k_1s * tsr * SD * a**2 * cl_alfa * k) + / sinMu + + (np.pi * CD * CG * cosMu * k_1s * tsr**2 * SD * a * cl_alfa * k * theta) + / (5 * sinMu) + - (np.pi * CD**2 * CG * cosMu * k_1s * tsr * SD * SG * a * cl_alfa * k**2 * theta) + / (10 * sinMu) ) - - (np.pi * CD * CG * cosMu**2 * k_1s * tsr * SD * a * cl_alfa * k) / sinMu - + (np.pi * CD * CG * cosMu**2 * k_1s * tsr * SD * a**2 * cl_alfa * k) - / sinMu - + (np.pi * CD * CG * cosMu * k_1s * tsr**2 * SD * a * cl_alfa * k * theta) - / (5 * sinMu) - - (np.pi * CD**2 * CG * cosMu * k_1s * tsr * SD * SG * a * cl_alfa * k**2 * theta) - / (10 * sinMu) + / (2 * np.pi) ) - / (2 * np.pi) - ) - return p + return p + @staticmethod + def get_ct(x, *data): + """ + System of equations for Ct, as represented in Eq. (25) of Tamaro et al. + x is a stand-in variable for Ct, which a numerical solver will solve for. + data is a tuple of input parameters to the system of equations to solve. + """ + sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU = data + # Add a small misalignment in case MU = 0 to avoid division by 0 + if MU == 0: + MU = 1e-6 + sinMu = np.sin(MU) + cosMu = np.cos(MU) + CD = np.cos(np.deg2rad(delta)) + CG = np.cos(np.deg2rad(gamma)) + SD = np.sin(np.deg2rad(delta)) + SG = np.sin(np.deg2rad(gamma)) + + # Axial induction + a = 1 - ( + (1 + np.sqrt(1 - x - 1 / 16 * x**2 * sinMu**2)) + / (2 * (1 + 1 / 16 * x * sinMu**2)) + ) -def get_ct(x, *data): - """ - System of equations for Ct, as represented in Eq. (25) of Tamaro et al. - x is a stand-in variable for Ct, which a numerical solver will solve for. - data is a tuple of input parameters to the system of equations to solve. - """ - sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU = data - # Add a small misalignment in case MU = 0 to avoid division by 0 - if MU == 0: - MU = 1e-6 - sinMu = np.sin(MU) - cosMu = np.cos(MU) - CD = np.cos(np.deg2rad(delta)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - SG = np.sin(np.deg2rad(gamma)) - - # Axial induction - a = 1 - ( - (1 + np.sqrt(1 - x - 1 / 16 * x**2 * sinMu**2)) - / (2 * (1 + 1 / 16 * x * sinMu**2)) - ) - - k_1s = -1 * (15 * np.pi / 32 * np.tan((MU + sinMu * (x / 2)) / 2)) - - I1 = -( - np.pi - * cosMu - * (tsr - CD * SG * k) - * (a - 1) - + (CD * CG * cosMu * k_1s * SD * a * k * np.pi * (2 * tsr - CD * SG * k)) - / (8 * sinMu) - ) / (2 * np.pi) - - I2 = ( - np.pi - * sinMu**2 - + ( + k_1s = -1 * (15 * np.pi / 32 * np.tan((MU + sinMu * (x / 2)) / 2)) + + I1 = -( + np.pi + * cosMu + * (tsr - CD * SG * k) + * (a - 1) + + (CD * CG * cosMu * k_1s * SD * a * k * np.pi * (2 * tsr - CD * SG * k)) + / (8 * sinMu) + ) / (2 * np.pi) + + I2 = ( np.pi - * ( - CD**2 * CG**2 * SD**2 * k**2 - + 3 * CD**2 * SG**2 * k**2 - - 8 * CD * tsr * SG * k - + 8 * tsr**2 + * sinMu**2 + + ( + np.pi + * ( + CD**2 * CG**2 * SD**2 * k**2 + + 3 * CD**2 * SG**2 * k**2 + - 8 * CD * tsr * SG * k + + 8 * tsr**2 + ) ) - ) - / 12 - ) / (2 * np.pi) + / 12 + ) / (2 * np.pi) - return (sigma * (cd + cl_alfa) * (I1) - sigma * cl_alfa * theta * (I2)) - x + return (sigma * (cd + cl_alfa) * (I1) - sigma * cl_alfa * theta * (I2)) - x From 6950d28d331840b127daa45721049c7c4e63bdd3 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 8 Nov 2024 17:13:45 -0700 Subject: [PATCH 48/53] Switch to cosd, sind from floris.utilities. --- floris/core/turbine/tum_operation_model.py | 49 +++++++++------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index ded6129d8..0db363464 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -19,6 +19,7 @@ NDArrayFloat, NDArrayObject, ) +from floris.utilities import cosd, sind @define @@ -100,9 +101,7 @@ def power( ### Solve for the power in yawed conditions # Compute overall misalignment (eq. (1) in Tamaro et al.) - MU = np.arccos( - np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) - ) + MU = np.arccos(cosd(yaw_angles) * cosd(tilt_angles)) cosMu = np.cos(MU) sinMu = np.sin(MU) p = np.zeros_like(average_velocity(velocities)) @@ -147,9 +146,7 @@ def power( ### Solve for the power in non-yawed conditions yaw_angles = np.zeros_like(yaw_angles) # Compute overall misalignment (eq. (1) in Tamaro et al.) - MU = np.arccos( - np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) - ) + MU = np.arccos(cosd(yaw_angles) * cosd(tilt_angles)) cosMu = np.cos(MU) sinMu = np.sin(MU) @@ -298,9 +295,7 @@ def thrust_coefficient( ### Solve for the thrust coefficient in yawed conditions # Compute overall misalignment (eq. (1) in Tamaro et al.) - MU = np.arccos( - np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) - ) + MU = np.arccos(cosd(yaw_angles) * cosd(tilt_angles)) cosMu = np.cos(MU) sinMu = np.sin(MU) thrust_coefficient1 = np.zeros_like(average_velocity(velocities)) @@ -325,9 +320,7 @@ def thrust_coefficient( ### Resolve thrust coefficient in non-yawed conditions yaw_angles = np.zeros_like(yaw_angles) - MU = np.arccos( - np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) - ) + MU = np.arccos(cosd(yaw_angles) * cosd(tilt_angles)) cosMu = np.cos(MU) sinMu = np.sin(MU) @@ -401,9 +394,7 @@ def axial_induction( # TODO: should the axial induction calculation be based on MU for zero yaw (as it is # currently) or should this be the actual yaw angle? yaw_angles = np.zeros_like(yaw_angles) - MU = np.arccos( - np.cos(np.deg2rad((yaw_angles))) * np.cos(np.deg2rad((tilt_angles))) - ) + MU = np.arccos(cosd(yaw_angles) * cosd(tilt_angles)) sinMu = np.sin(MU) # all the same in this case anyway (since yaw zero) # Eq. (25a) from Tamaro et al. @@ -515,7 +506,7 @@ def get_tsr(x, *data): torque_nm = np.interp(omega, omega_lut_torque, torque_lut_omega) # Yawed case - mu = np.arccos(np.cos(np.deg2rad(gamma)) * np.cos(np.deg2rad(tilt))) + mu = np.arccos(cosd(gamma) * cosd(tilt)) data = ( sigma, cd, @@ -549,7 +540,7 @@ def get_tsr(x, *data): ) # Unyawed case - mu = np.arccos(np.cos(np.deg2rad(0)) * np.cos(np.deg2rad(tilt))) + mu = np.arccos(cosd(0) * cosd(tilt)) data = ( sigma, cd, @@ -630,7 +621,7 @@ def get_pitch(x, *data): ) # Yawed case - mu = np.arccos(np.cos(np.deg2rad(gamma)) * np.cos(np.deg2rad(tilt))) + mu = np.arccos(cosd(gamma) * cosd(tilt)) data = ( sigma, cd, @@ -664,7 +655,7 @@ def get_pitch(x, *data): ) # Unyawed case - mu = np.arccos(np.cos(np.deg2rad(0)) * np.cos(np.deg2rad(tilt))) + mu = np.arccos(cosd(0) * cosd(tilt)) data = ( sigma, cd, @@ -783,10 +774,10 @@ def get_pitch(x, *data): u_v = rotor_average_velocities[i, j] if u_v > u_rated[i, j]: tsr_v = ( - omega_rated[i, j] * R / u_v * np.cos(np.deg2rad(yaw_angles[i, j])) ** 0.5 + omega_rated[i, j] * R / u_v * cosd(yaw_angles[i, j]) ** 0.5 ) else: - tsr_v = tsr_opt * np.cos(np.deg2rad(yaw_angles[i, j])) + tsr_v = tsr_opt * cosd(yaw_angles[i, j]) if Region2andAhalf: # fix for interpolation omega_lut_torque[-1] = omega_lut_torque[-1] + 1e-2 omega_lut_pow[-1] = omega_lut_pow[-1] + 1e-2 @@ -876,10 +867,10 @@ def find_cp(sigma, cd, cl_alfa, gamma, delta, k, cosMu, sinMu, tsr, theta, MU, c (1 + np.sqrt(1 - ct - 1 / 16 * sinMu**2 * ct**2)) / (2 * (1 + 1 / 16 * ct * sinMu**2)) ) - SG = np.sin(np.deg2rad(gamma)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - CD = np.cos(np.deg2rad(delta)) + SG = sind(gamma) + CG = cosd(gamma) + SD = sind(delta) + CD = cosd(delta) k_1s = -1 * (15 * np.pi / 32 * np.tan((MU + sinMu * (ct / 2)) / 2)) p = sigma * ( @@ -946,10 +937,10 @@ def get_ct(x, *data): MU = 1e-6 sinMu = np.sin(MU) cosMu = np.cos(MU) - CD = np.cos(np.deg2rad(delta)) - CG = np.cos(np.deg2rad(gamma)) - SD = np.sin(np.deg2rad(delta)) - SG = np.sin(np.deg2rad(gamma)) + CD = cosd(delta) + CG = cosd(gamma) + SD = sind(delta) + SG = sind(gamma) # Axial induction a = 1 - ( From 94ec71380c913dfcdbf23ca26010aa043ee48a75 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 18 Nov 2024 14:26:21 -0700 Subject: [PATCH 49/53] Add correct 15MW data and remove 3MW model. Reg tests fail due to updated 15MW data. --- floris/turbine_library/LUT_iea15MW.npz | Bin 16902 -> 18230 bytes floris/turbine_library/iea_3MW.yaml | 228 ------------------------- 2 files changed, 228 deletions(-) delete mode 100644 floris/turbine_library/iea_3MW.yaml diff --git a/floris/turbine_library/LUT_iea15MW.npz b/floris/turbine_library/LUT_iea15MW.npz index 6e43da4698201de996c4f2b2d16afd88a9e951c7..8c80117e8a1073df572178abe8f05608bbaa3f7d 100644 GIT binary patch literal 18230 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 literal 16902 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 diff --git a/floris/turbine_library/iea_3MW.yaml b/floris/turbine_library/iea_3MW.yaml deleted file mode 100644 index 510a83eb2..000000000 --- a/floris/turbine_library/iea_3MW.yaml +++ /dev/null @@ -1,228 +0,0 @@ -# NREL 5MW reference wind turbine. -# Data based on: -# https://github.com/NREL/turbine-models/blob/master/Offshore/NREL_5MW_126_RWT_corrected.csv -# Note: Small power variations above rated removed. Rotor diameter includes coning angle. - -### -# An ID for this type of turbine definition. -# This is not currently used, but it will be enabled in the future. This should typically -# match the root name of the file. -turbine_type: 'iea_3MW' - -### -# Hub height. -hub_height: 110.0 - -### -# Rotor diameter. -rotor_diameter: 130.0 - -### -# Tip speed ratio defined as linear blade tip speed normalized by the incoming wind speed. -TSR: 8.0 - -### -# Model for power and thrust curve interpretation. -#power_thrust_model: 'cosine-loss' -operation_model: 'tum-loss' -### -# Cp and Ct as a function of wind speed for the turbine's full range of operating conditions. -power_thrust_table: - ### Power thrust table parameters - # The air density at which the Cp and Ct curves are defined. - ref_air_density: 1.12 - rated_rpm: 11.75 - rotor_solidity: 0.0416 - generator_efficiency: 0.925 - rated_power: 3300.0 - rotor_diameter: 130 - cp_ct_data_file: "LUT_iea15MW.npz" - beta: -3.11 - cd: 0.0051 - cl_alfa: 4.75 - # The tilt angle at which the Cp and Ct curves are defined. This is used to capture - # the effects of a floating platform on a turbine's power and wake. - ref_tilt: 5.0 - # Cosine exponent for power loss due to tilt. - pT: 1.88 - # Cosine exponent for power loss due to yaw misalignment. - pP: 1.88 - ### Power thrust table data - wind_speed: - - 0.0 - - 2.9 - - 3.0 - - 4.0 - - 5.0 - - 6.0 - - 7.0 - - 7.1 - - 7.2 - - 7.3 - - 7.4 - - 7.5 - - 7.6 - - 7.7 - - 7.8 - - 7.9 - - 8.0 - - 9.0 - - 10.0 - - 10.1 - - 10.2 - - 10.3 - - 10.4 - - 10.5 - - 10.6 - - 10.7 - - 10.8 - - 10.9 - - 11.0 - - 11.1 - - 11.2 - - 11.3 - - 11.4 - - 11.5 - - 11.6 - - 11.7 - - 11.8 - - 11.9 - - 12.0 - - 13.0 - - 14.0 - - 15.0 - - 16.0 - - 17.0 - - 18.0 - - 19.0 - - 20.0 - - 21.0 - - 22.0 - - 23.0 - - 24.0 - - 25.0 - - 25.1 - - 50.0 - power: - - 0.0 - - 0.0 - - 40.518011517569214 - - 177.67162506419703 - - 403.900880943964 - - 737.5889584824021 - - 1187.1774030611875 - - 1239.245945375778 - - 1292.5184293723503 - - 1347.3213147477102 - - 1403.2573725578948 - - 1460.7011898730707 - - 1519.6419125979983 - - 1580.174365096404 - - 1642.1103166918167 - - 1705.758292831 - - 1771.1659528893977 - - 2518.553107505315 - - 3448.381605840943 - - 3552.140809000129 - - 3657.9545431794127 - - 3765.121299313842 - - 3873.928844315059 - - 3984.4800226955504 - - 4096.582833096852 - - 4210.721306623712 - - 4326.154305853405 - - 4443.395565353604 - - 4562.497934188341 - - 4683.419890251577 - - 4806.164748311019 - - 4929.931918769215 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 5000.00 - - 0.0 - - 0.0 - thrust_coefficient: - - 0.0 - - 0.0 - - 1.132034888 - - 0.999470963 - - 0.917697381 - - 0.860849503 - - 0.815371198 - - 0.811614904 - - 0.807939328 - - 0.80443352 - - 0.800993851 - - 0.79768116 - - 0.794529244 - - 0.791495834 - - 0.788560434 - - 0.787217182 - - 0.787127977 - - 0.785839257 - - 0.783812219 - - 0.783568108 - - 0.783328285 - - 0.781194418 - - 0.777292539 - - 0.773464375 - - 0.769690236 - - 0.766001924 - - 0.762348072 - - 0.758760824 - - 0.755242872 - - 0.751792927 - - 0.748434131 - - 0.745113997 - - 0.717806682 - - 0.672204789 - - 0.63831272 - - 0.610176496 - - 0.585456847 - - 0.563222111 - - 0.542912273 - - 0.399312061 - - 0.310517829 - - 0.248633226 - - 0.203543725 - - 0.169616419 - - 0.143478955 - - 0.122938861 - - 0.106515296 - - 0.093026095 - - 0.081648606 - - 0.072197368 - - 0.064388275 - - 0.057782745 - - 0.0 - - 0.0 - -### -# A boolean flag used when the user wants FLORIS to use the user-supplied multi-dimensional -# Cp/Ct information. -multi_dimensional_cp_ct: False - -### -# The path to the .csv file that contains the multi-dimensional Cp/Ct data. The format of this -# file is such that any external conditions, such as wave height or wave period, that the -# Cp/Ct data is dependent on come first, in column format. The last three columns of the .csv -# file must be ``ws``, ``Cp``, and ``Ct``, in that order. An example of fictional data is given -# in ``floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv``. -power_thrust_data_file: '../floris/turbine_library/LUT_IEA3MW.npz' From b18819b3859a67fe800cef42cbc93ff596c12b16 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 18 Nov 2024 14:31:43 -0700 Subject: [PATCH 50/53] Update base values in reg test. --- tests/tum_operation_model_unit_test.py | 120 ++++++++++++------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/tests/tum_operation_model_unit_test.py b/tests/tum_operation_model_unit_test.py index cdc842df7..ae6b28413 100644 --- a/tests/tum_operation_model_unit_test.py +++ b/tests/tum_operation_model_unit_test.py @@ -147,72 +147,72 @@ def test_TUMLossTurbine_regression(): yaw_angles_test = np.linspace(-yaw_max, yaw_max, N_test).reshape(-1,1) power_base = np.array([ - 2393927.66234718, - 2540214.34341772, - 2669081.37915352, - 2781432.03897903, - 2878018.91106399, - 2958794.44370780, - 3023377.59580264, - 3071678.43043693, - 3103774.85736505, - 3119784.56415652, - 3119784.56415652, - 3103774.85736505, - 3071678.43043693, - 3023377.59580264, - 2958794.44370780, - 2878018.91106399, - 2781432.03897903, - 2669081.37915352, - 2540214.34341772, - 2393927.66234718, + 2480803.17307080, + 2604861.74554374, + 2717127.82421417, + 2816883.67907730, + 2903511.88325795, + 2976489.92724703, + 3035386.26405968, + 3079857.64410033, + 3109647.49833632, + 3124585.07963994, + 3124585.07963994, + 3109647.49833632, + 3079857.64410033, + 3035386.26405968, + 2976489.92724703, + 2903511.88325795, + 2816883.67907730, + 2717127.82421417, + 2604861.74554374, + 2480803.17307080, ]) thrust_coefficient_base = np.array([ - 0.65242964, - 0.68226378, - 0.70804882, - 0.73026438, - 0.74577423, - 0.75596131, - 0.76411955, - 0.77024367, - 0.77432917, - 0.77637278, - 0.77637278, - 0.77432917, - 0.77024367, - 0.76411955, - 0.75596131, - 0.74577423, - 0.73026438, - 0.70804882, - 0.68226378, - 0.65242964, + 0.64290577, + 0.65767615, + 0.67081063, + 0.68231005, + 0.69217361, + 0.70039934, + 0.70698463, + 0.71192670, + 0.71522301, + 0.71687168, + 0.71687168, + 0.71522301, + 0.71192670, + 0.70698463, + 0.70039934, + 0.69217361, + 0.68231005, + 0.67081063, + 0.65767615, + 0.64290577, ]) axial_induction_base = np.array([ - 0.20555629, - 0.21851069, - 0.23020638, - 0.24070476, - 0.24829308, - 0.25340393, - 0.25757444, - 0.26075276, - 0.26289671, - 0.26397642, - 0.26397642, - 0.26289671, - 0.26075276, - 0.25757444, - 0.25340393, - 0.24829308, - 0.24070476, - 0.23020638, - 0.21851069, - 0.20555629, + 0.20153901, + 0.20779287, + 0.21346846, + 0.21853124, + 0.22294734, + 0.22668459, + 0.22971369, + 0.23200938, + 0.23355155, + 0.23432623, + 0.23432623, + 0.23355155, + 0.23200938, + 0.22971369, + 0.22668459, + 0.22294734, + 0.21853124, + 0.21346846, + 0.20779287, + 0.20153901, ]) power = TUMLossTurbine.power( From 437afe882265c4431aafcddad1d9209750a39d81 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 18 Nov 2024 14:32:39 -0700 Subject: [PATCH 51/53] Remove unneeded print statements. --- floris/core/turbine/tum_operation_model.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index 0db363464..1463487c9 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -208,10 +208,6 @@ def power( ) power_coefficient = cp_interp * ratio - # TODO: make printout optional? - if False: - print("Tip speed ratio" + str(tsr_array)) - print("Pitch out: " + str(pitch_out)) power = ( 0.5 * air_density From 71a22514b5efcb380e8be318792cd3df640e4ff8 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 18 Nov 2024 15:12:37 -0700 Subject: [PATCH 52/53] Load cp_ct data on turbine construction rather than in operation model calls. Revert type_dec changes. --- floris/core/turbine/tum_operation_model.py | 35 ++++++++-------------- floris/core/turbine/turbine.py | 23 ++++++++------ floris/type_dec.py | 10 +++---- tests/tum_operation_model_unit_test.py | 15 ++++++++++ 4 files changed, 45 insertions(+), 38 deletions(-) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index 1463487c9..80cb60ece 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -1,8 +1,6 @@ from __future__ import annotations import copy -import os -from pathlib import Path import numpy as np from attrs import define @@ -191,13 +189,10 @@ def power( # ratio of yawed to unyawed thrust coefficients ratio = p / p0 - # Load Cp surface data and construct interpolator - pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / power_thrust_table["cp_ct_data_file"] - LUT = np.load(lut_file) - cp_i = LUT["cp_lut"] - pitch_i = LUT["pitch_lut"] - tsr_i = LUT["tsr_lut"] + # Extract data from lookup table and construct interpolator + cp_i = power_thrust_table["cp_ct_data"]["cp_lut"] + pitch_i = power_thrust_table["cp_ct_data"]["pitch_lut"] + tsr_i = power_thrust_table["cp_ct_data"]["tsr_lut"] interp_lut = RegularGridInterpolator( (tsr_i, pitch_i), cp_i, bounds_error=False, fill_value=None ) @@ -343,13 +338,10 @@ def thrust_coefficient( # Compute ratio of yawed to unyawed thrust coefficients ratio = thrust_coefficient1 / thrust_coefficient0 # See above eq. (29) - # Load Ct surface data and construct interpolator - pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / power_thrust_table["cp_ct_data_file"] - LUT = np.load(lut_file) - ct_i = LUT["ct_lut"] - pitch_i = LUT["pitch_lut"] - tsr_i = LUT["tsr_lut"] + # Extract data from lookup table and construct interpolator + ct_i = power_thrust_table["cp_ct_data"]["ct_lut"] + pitch_i = power_thrust_table["cp_ct_data"]["pitch_lut"] + tsr_i = power_thrust_table["cp_ct_data"]["tsr_lut"] interp_lut = RegularGridInterpolator( (tsr_i, pitch_i), ct_i, bounds_error=False, fill_value=None ) # *0.9722085500886761) @@ -702,13 +694,10 @@ def get_pitch(x, *data): y = aero_pow - electric_pow return y - # Load Cp/Ct data - pkgroot = Path(os.path.dirname(os.path.abspath(__file__))).resolve().parents[1] - lut_file = pkgroot / "turbine_library" / power_thrust_table["cp_ct_data_file"] - LUT = np.load(lut_file) - cp_i = LUT["cp_lut"] - pitch_i = LUT["pitch_lut"] - tsr_i = LUT["tsr_lut"] + # Extract data from lookup table + cp_i = power_thrust_table["cp_ct_data"]["cp_lut"] + pitch_i = power_thrust_table["cp_ct_data"]["pitch_lut"] + tsr_i = power_thrust_table["cp_ct_data"]["tsr_lut"] idx = np.squeeze(np.where(cp_i == np.max(cp_i))) tsr_opt = tsr_i[idx[0]] diff --git a/floris/core/turbine/turbine.py b/floris/core/turbine/turbine.py index df164e930..3fe117266 100644 --- a/floris/core/turbine/turbine.py +++ b/floris/core/turbine/turbine.py @@ -2,6 +2,7 @@ from __future__ import annotations import copy +import os from collections.abc import Callable, Iterable from pathlib import Path @@ -476,10 +477,6 @@ class Turbine(BaseClass): correct_cp_ct_for_tilt: bool = field(default=False) floating_tilt_table: dict[str, NDArrayFloat] | None = field(default=None) - # Even though this Turbine class does not support the multidimensional features as they - # are implemented in TurbineMultiDim, providing the following two attributes here allows - # the turbine data inputs to keep the multidimensional Cp and Ct curve but switch them off - # with multi_dimensional_cp_ct = False multi_dimensional_cp_ct: bool = field(default=False) # Initialized in the post_init function @@ -507,13 +504,21 @@ def __attrs_post_init__(self) -> None: def __post_init__(self) -> None: self._initialize_tilt_interpolation() + + bypass_numeric_converter = False if self.multi_dimensional_cp_ct: self._initialize_multidim_power_thrust_table() - else: - self.power_thrust_table = floris_numeric_dict_converter( - self.power_thrust_table, - allow_strings=True - ) + bypass_numeric_converter = True + + # Check for whether a cp_ct_data_file is specified, and load it if so. + if "cp_ct_data_file" in self.power_thrust_table: + floris_root = Path(__file__).resolve().parents[2] + file_path = floris_root / "turbine_library" / self.power_thrust_table["cp_ct_data_file"] + self.power_thrust_table["cp_ct_data"] = np.load(file_path) + bypass_numeric_converter = True + + if not bypass_numeric_converter: + self.power_thrust_table = floris_numeric_dict_converter(self.power_thrust_table) def _initialize_power_thrust_functions(self) -> None: turbine_function_model = TURBINE_MODEL_MAP["operation_model"][self.operation_model] diff --git a/floris/type_dec.py b/floris/type_dec.py index f41a416d2..319a09917 100644 --- a/floris/type_dec.py +++ b/floris/type_dec.py @@ -59,7 +59,7 @@ def floris_array_converter(data: Iterable) -> np.ndarray: raise TypeError(e.args[0] + f". Data given: {data}") return a -def floris_numeric_dict_converter(data: dict, allow_strings=False) -> dict: +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 @@ -79,11 +79,9 @@ def floris_numeric_dict_converter(data: dict, allow_strings=False) -> dict: except TypeError: # Not iterable so try to cast to float converted_dict[k] = float(v) - else: # Iterable so convert to Numpy array - if allow_strings and isinstance(v, str): - converted_dict[k] = v - else: - converted_dict[k] = floris_array_converter(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/tum_operation_model_unit_test.py b/tests/tum_operation_model_unit_test.py index ae6b28413..8c02edb99 100644 --- a/tests/tum_operation_model_unit_test.py +++ b/tests/tum_operation_model_unit_test.py @@ -1,3 +1,6 @@ +import os +from pathlib import Path + import numpy as np import pytest @@ -22,6 +25,10 @@ def test_TUMLossTurbine(): wind_speed = 10.0 turbine_data = SampleInputs().turbine turbine_data["power_thrust_table"] = SampleInputs().tum_loss_turbine_power_thrust_table + data_file_path = Path(__file__).resolve().parents[1] / "floris" / "turbine_library" + turbine_data["power_thrust_table"]["cp_ct_data"] = np.load( + data_file_path / turbine_data["power_thrust_table"]["cp_ct_data_file"] + ) yaw_angles_nom = 0 * np.ones((1, n_turbines)) tilt_angles_nom = turbine_data["power_thrust_table"]["ref_tilt"] * np.ones((1, n_turbines)) @@ -138,6 +145,10 @@ def test_TUMLossTurbine_regression(): wind_speed = 10.0 turbine_data = SampleInputs().turbine turbine_data["power_thrust_table"] = SampleInputs().tum_loss_turbine_power_thrust_table + data_file_path = Path(__file__).resolve().parents[1] / "floris" / "turbine_library" + turbine_data["power_thrust_table"]["cp_ct_data"] = np.load( + data_file_path / turbine_data["power_thrust_table"]["cp_ct_data_file"] + ) N_test = 20 tilt_angles_nom = turbine_data["power_thrust_table"]["ref_tilt"] * np.ones((N_test, n_turbines)) @@ -262,6 +273,10 @@ def test_TUMLossTurbine_integration(): n_turbines = 1 turbine_data = SampleInputs().turbine turbine_data["power_thrust_table"] = SampleInputs().tum_loss_turbine_power_thrust_table + data_file_path = Path(__file__).resolve().parents[1] / "floris" / "turbine_library" + turbine_data["power_thrust_table"]["cp_ct_data"] = np.load( + data_file_path / turbine_data["power_thrust_table"]["cp_ct_data_file"] + ) N_test = 20 tilt_angles_nom = turbine_data["power_thrust_table"]["ref_tilt"] * np.ones((N_test, n_turbines)) From 0f8887b28690cb13b872a1e82d664f5a9d977c43 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 18 Nov 2024 15:24:40 -0700 Subject: [PATCH 53/53] Rename cp_ct data file to be clearer. Update fields in all turine models; may remove from iea10mw, nrel5mw subsequently. --- floris/core/turbine/tum_operation_model.py | 2 +- floris/turbine_library/iea_10MW.yaml | 2 +- floris/turbine_library/iea_15MW.yaml | 2 +- .../{LUT_iea15MW.npz => iea_15MW_cp_ct_surface.npz} | Bin floris/turbine_library/nrel_5MW.yaml | 2 +- tests/conftest.py | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename floris/turbine_library/{LUT_iea15MW.npz => iea_15MW_cp_ct_surface.npz} (100%) diff --git a/floris/core/turbine/tum_operation_model.py b/floris/core/turbine/tum_operation_model.py index 80cb60ece..e72350607 100644 --- a/floris/core/turbine/tum_operation_model.py +++ b/floris/core/turbine/tum_operation_model.py @@ -29,7 +29,7 @@ class TUMLossTurbine(BaseOperationModel): The method requires C_P, C_T look-up tables as functions of tip speed ratio and blade pitch angle, available here: - "../floris/turbine_library/LUT_iea15MW.npz" for the IEA 3.4 MW (Bortolotti et al., 2019) + "floris/turbine_library/iea_15MW_cp_ct_surface.npz" for the IEA 15MW reference turbine. As with all turbine submodules, implements only static power() and thrust_coefficient() methods, which are called by power() and thrust_coefficient() on turbine.py, respectively. There are also two new functions, i.e. compute_local_vertical_shear() and control_trajectory(). diff --git a/floris/turbine_library/iea_10MW.yaml b/floris/turbine_library/iea_10MW.yaml index 01740dad7..db641417f 100644 --- a/floris/turbine_library/iea_10MW.yaml +++ b/floris/turbine_library/iea_10MW.yaml @@ -31,7 +31,7 @@ power_thrust_table: beta: -3.8233819218614817 cd: 0.004612981322772105 cl_alfa: 4.602140680380394 - cp_ct_data_file: "LUT_iea15MW.npz" + cp_ct_data_file: "iea_15MW_cp_ct_surface.npz" # Power and thrust curves power: diff --git a/floris/turbine_library/iea_15MW.yaml b/floris/turbine_library/iea_15MW.yaml index 082825e38..a479eb052 100644 --- a/floris/turbine_library/iea_15MW.yaml +++ b/floris/turbine_library/iea_15MW.yaml @@ -33,7 +33,7 @@ power_thrust_table: beta: -3.098605491003358 cd: 0.004426686198054057 cl_alfa: 4.546410770937916 - cp_ct_data_file: "LUT_iea15MW.npz" + cp_ct_data_file: "iea_15MW_cp_ct_surface.npz" # Power and thrust curves power: diff --git a/floris/turbine_library/LUT_iea15MW.npz b/floris/turbine_library/iea_15MW_cp_ct_surface.npz similarity index 100% rename from floris/turbine_library/LUT_iea15MW.npz rename to floris/turbine_library/iea_15MW_cp_ct_surface.npz diff --git a/floris/turbine_library/nrel_5MW.yaml b/floris/turbine_library/nrel_5MW.yaml index e830230b3..c2965039a 100644 --- a/floris/turbine_library/nrel_5MW.yaml +++ b/floris/turbine_library/nrel_5MW.yaml @@ -65,7 +65,7 @@ power_thrust_table: beta: -0.45891 cd: 0.0040638 cl_alfa: 4.275049 - cp_ct_data_file: "LUT_iea15MW.npz" + cp_ct_data_file: "iea_15MW_cp_ct_surface.npz" ### Power thrust table data # wind speeds for look-up tables of power and thrust_coefficient diff --git a/tests/conftest.py b/tests/conftest.py index 532fb6e7d..f223ab871 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -401,7 +401,7 @@ def __init__(self): "beta": -0.45891, "cd": 0.0040638, "cl_alfa": 4.275049, - "cp_ct_data_file": "LUT_iea15MW.npz" + "cp_ct_data_file": "iea_15MW_cp_ct_surface.npz" } self.turbine_floating = copy.deepcopy(self.turbine)