diff --git a/docs/operation_models_user.ipynb b/docs/operation_models_user.ipynb index b5def6f76..a1e8a02f2 100644 --- a/docs/operation_models_user.ipynb +++ b/docs/operation_models_user.ipynb @@ -141,10 +141,14 @@ "The `\"cosine-loss\"` operation model describes the decrease in power and thrust produced by a \n", "wind turbine as it yaws (or tilts) away from the incoming wind. The thrust is reduced by a factor of \n", "$\\cos \\gamma$, where $\\gamma$ is the yaw misalignment angle, while the power is reduced by a factor \n", - "of $(\\cos\\gamma)^{p_P}$, where $p_P$ is the cosine loss exponent, specified by `cosine_loss_exponent_yaw`\n", - "(or `cosine_loss_exponent_tilt` for tilt angles). The power and thrust produced by the turbine\n", + "of $(\\cos\\gamma)^{p_P}$, where $p_P$ is the cosine loss exponent, specified by `cosine_loss_exponent_yaw`.\n", + "Similarly, the thrust and power are reduced by factors of $(\\cos \\theta / \\cos \\theta_{ref})$ and\n", + "$(\\cos \\theta / \\cos \\theta_{ref})^{p_T}$, respectively, where $\\theta$ is the tilt angle, \n", + "$\\theta_{ref}$ is the reference tilt angle under which the power and thrust curves are specified, and\n", + "$p_T$ is the cosine loss exponent for tilt, specified by `cosine_loss_exponent_tilt`.\n", + "The power and thrust produced by the turbine\n", "thus vary as a function of the turbine's yaw angle, set using the `yaw_angles` argument to \n", - "`FlorisModel.set()`." + "`FlorisModel.set()`, and tilt angle (if applicable, for example for floating wind turbines)." ] }, { diff --git a/floris/core/rotor_velocity.py b/floris/core/rotor_velocity.py index 43d4e3077..16007db30 100644 --- a/floris/core/rotor_velocity.py +++ b/floris/core/rotor_velocity.py @@ -47,11 +47,11 @@ def rotor_velocity_tilt_cosine_correction( tilt_angles = np.where(correct_cp_ct_for_tilt, tilt_angles, old_tilt_angle) # Compute the rotor effective velocity adjusting for tilt - relative_tilt = tilt_angles - ref_tilt rotor_effective_velocities = ( rotor_effective_velocities - * cosd(relative_tilt) ** (cosine_loss_exponent_tilt / 3.0) + * (cosd(tilt_angles) / cosd(ref_tilt)) ** (cosine_loss_exponent_tilt / 3.0) ) + return rotor_effective_velocities def simple_mean(array, axis=0): diff --git a/floris/core/turbine/operation_models.py b/floris/core/turbine/operation_models.py index 544086747..906ca0916 100644 --- a/floris/core/turbine/operation_models.py +++ b/floris/core/turbine/operation_models.py @@ -266,7 +266,8 @@ def thrust_coefficient( thrust_coefficient = ( thrust_coefficient * cosd(yaw_angles) - * cosd(tilt_angles - power_thrust_table["ref_tilt"]) + * cosd(tilt_angles) + / cosd(power_thrust_table["ref_tilt"]) ) return thrust_coefficient @@ -294,7 +295,9 @@ def axial_induction( correct_cp_ct_for_tilt=correct_cp_ct_for_tilt ) - misalignment_loss = cosd(yaw_angles) * cosd(tilt_angles - power_thrust_table["ref_tilt"]) + misalignment_loss = ( + cosd(yaw_angles) * cosd(tilt_angles) / cosd(power_thrust_table["ref_tilt"]) + ) return 0.5 / misalignment_loss * (1 - np.sqrt(1 - thrust_coefficient * misalignment_loss)) @define diff --git a/tests/reg_tests/empirical_gauss_regression_test.py b/tests/reg_tests/empirical_gauss_regression_test.py index a6bdaa991..e664881df 100644 --- a/tests/reg_tests/empirical_gauss_regression_test.py +++ b/tests/reg_tests/empirical_gauss_regression_test.py @@ -81,6 +81,36 @@ ] ) +tilted_baseline = np.array( + [ + # 8 m/s + [ + [7.9736858, 0.7824685, 1734488.7059275, 0.2658985], + [5.8875889, 0.8705526, 704778.1875928, 0.3212047], + [5.9110415, 0.8692142, 712622.7895048, 0.3202651], + ], + # 9 m/s + [ + [8.9703965, 0.7812020, 2471404.7824861, 0.2652278], + [6.6276158, 0.8354859, 1026885.3722728, 0.2980366], + [6.7091646, 0.8317630, 1063636.4762428, 0.2957326], + ], + # 10 m/s + [ + [9.9671073, 0.7792154, 3383206.6144193, 0.2641794], + [7.3711242, 0.8050505, 1396968.1317078, 0.2799135], + [7.5109456, 0.8003819, 1477742.2826442, 0.2772650], + ], + # 11 m/s + [ + [10.9638180, 0.7520150, 4470666.5322783, 0.2502624], + [8.2150645, 0.7898565, 1946589.2518193, 0.2714076], + [8.3045772, 0.7897407, 2013649.9637290, 0.2713440], + ] + ] +) + + yaw_added_recovery_baseline = np.array( [ # 8 m/s @@ -453,6 +483,103 @@ def test_regression_yaw(sample_inputs_fixture): assert_results_arrays(test_results[0:4], yawed_baseline) + +def test_regression_tilt(sample_inputs_fixture): + """ + Tandem turbines with the upstream turbine tilted + """ + sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL + sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.core["wake"]["model_strings"]["turbulence_model"] = TURBULENCE_MODEL + + floris = Core.from_dict(sample_inputs_fixture.core) + + tilt_angles = np.zeros((N_FINDEX, N_TURBINES)) + tilt_angles[:,0] = 8.0 + floris.farm.tilt_angles = tilt_angles + + floris.initialize_domain() + floris.steady_state_atmospheric_condition() + + n_turbines = floris.farm.n_turbines + n_findex = floris.flow_field.n_findex + + velocities = floris.flow_field.u + turbulence_intensities = floris.flow_field.turbulence_intensity_field + air_density = floris.flow_field.air_density + yaw_angles = floris.farm.yaw_angles + tilt_angles = floris.farm.tilt_angles + power_setpoints = floris.farm.power_setpoints + awc_modes = floris.farm.awc_modes + awc_amplitudes = floris.farm.awc_amplitudes + test_results = np.zeros((n_findex, n_turbines, 4)) + + farm_avg_velocities = average_velocity( + velocities, + ) + farm_cts = thrust_coefficient( + velocities, + turbulence_intensities, + air_density, + yaw_angles, + tilt_angles, + power_setpoints, + awc_modes, + awc_amplitudes, + floris.farm.turbine_thrust_coefficient_functions, + floris.farm.turbine_tilt_interps, + floris.farm.correct_cp_ct_for_tilt, + floris.farm.turbine_type_map, + floris.farm.turbine_power_thrust_tables, + ) + farm_powers = power( + velocities, + turbulence_intensities, + air_density, + floris.farm.turbine_power_functions, + yaw_angles, + tilt_angles, + power_setpoints, + awc_modes, + awc_amplitudes, + floris.farm.turbine_tilt_interps, + floris.farm.turbine_type_map, + floris.farm.turbine_power_thrust_tables, + ) + farm_axial_inductions = axial_induction( + velocities, + turbulence_intensities, + air_density, + yaw_angles, + tilt_angles, + power_setpoints, + awc_modes, + awc_amplitudes, + floris.farm.turbine_axial_induction_functions, + floris.farm.turbine_tilt_interps, + floris.farm.correct_cp_ct_for_tilt, + floris.farm.turbine_type_map, + floris.farm.turbine_power_thrust_tables, + ) + for i in range(n_findex): + for j in range(n_turbines): + test_results[i, j, 0] = farm_avg_velocities[i, j] + test_results[i, j, 1] = farm_cts[i, j] + test_results[i, j, 2] = farm_powers[i, j] + test_results[i, j, 3] = farm_axial_inductions[i, j] + + if DEBUG: + print_test_values( + farm_avg_velocities, + farm_cts, + farm_powers, + farm_axial_inductions, + max_findex_print=4, + ) + + assert_results_arrays(test_results[0:4], tilted_baseline) + + def test_regression_yaw_added_recovery(sample_inputs_fixture): """ Tandem turbines with the upstream turbine yawed and yaw added recovery diff --git a/tests/rotor_velocity_unit_test.py b/tests/rotor_velocity_unit_test.py index a83ed219e..ab6250e4a 100644 --- a/tests/rotor_velocity_unit_test.py +++ b/tests/rotor_velocity_unit_test.py @@ -145,6 +145,60 @@ def test_rotor_velocity_tilt_cosine_correction(): np.testing.assert_allclose(tilt_corrected_velocities, wind_speed_N_TURBINES) + ## Test angles that are not the same as the reference tilt + + # Test tilted "back" from reference tilt of 5 degrees + tilt_angle = 10.0 # Greater than the reference tilt + tilt_corrected_velocities = rotor_velocity_tilt_cosine_correction( + tilt_angles=tilt_angle*np.ones((1, N_TURBINES)), + ref_tilt=np.array([turbine_floating.power_thrust_table["ref_tilt"]] * N_TURBINES), + cosine_loss_exponent_tilt=np.array( + [turbine_floating.power_thrust_table["cosine_loss_exponent_tilt"]] * N_TURBINES + ), + tilt_interp=None, # Override wind-speed-based tilt interpolation + correct_cp_ct_for_tilt=np.array([[True] * N_TURBINES]), + rotor_effective_velocities=wind_speed_N_TURBINES, + ) + assert (tilt_corrected_velocities < wind_speed_N_TURBINES).all() + + # Test tilted "forward" from reference tilt of 5 degrees + tilt_angle = 0.0 # Less than the reference tilt + tilt_corrected_velocities = rotor_velocity_tilt_cosine_correction( + tilt_angles=tilt_angle*np.ones((1, N_TURBINES)), + ref_tilt=np.array([turbine_floating.power_thrust_table["ref_tilt"]] * N_TURBINES), + cosine_loss_exponent_tilt=np.array( + [turbine_floating.power_thrust_table["cosine_loss_exponent_tilt"]] * N_TURBINES + ), + tilt_interp=None, # Override wind-speed-based tilt interpolation + correct_cp_ct_for_tilt=np.array([[True] * N_TURBINES]), + rotor_effective_velocities=wind_speed_N_TURBINES, + ) + assert (tilt_corrected_velocities > wind_speed_N_TURBINES).all() + + # Test symmetry around zero tilt + tilt_angle = 3.0 + tilt_negative = rotor_velocity_tilt_cosine_correction( + tilt_angles=-tilt_angle*np.ones((1, N_TURBINES)), + ref_tilt=np.array([turbine_floating.power_thrust_table["ref_tilt"]] * N_TURBINES), + cosine_loss_exponent_tilt=np.array( + [turbine_floating.power_thrust_table["cosine_loss_exponent_tilt"]] * N_TURBINES + ), + tilt_interp=None, # Override wind-speed-based tilt interpolation + correct_cp_ct_for_tilt=np.array([[True] * N_TURBINES]), + rotor_effective_velocities=wind_speed_N_TURBINES, + ) + tilt_positive = rotor_velocity_tilt_cosine_correction( + tilt_angles=tilt_angle*np.ones((1, N_TURBINES)), + ref_tilt=np.array([turbine_floating.power_thrust_table["ref_tilt"]] * N_TURBINES), + cosine_loss_exponent_tilt=np.array( + [turbine_floating.power_thrust_table["cosine_loss_exponent_tilt"]] * N_TURBINES + ), + tilt_interp=None, # Override wind-speed-based tilt interpolation + correct_cp_ct_for_tilt=np.array([[True] * N_TURBINES]), + rotor_effective_velocities=wind_speed_N_TURBINES, + ) + np.testing.assert_allclose(tilt_negative, tilt_positive) + def test_compute_tilt_angles_for_floating_turbines(): N_TURBINES = 4 diff --git a/tests/turbine_multi_dim_unit_test.py b/tests/turbine_multi_dim_unit_test.py index 0c11c2564..0a1ece78d 100644 --- a/tests/turbine_multi_dim_unit_test.py +++ b/tests/turbine_multi_dim_unit_test.py @@ -97,7 +97,7 @@ def test_ct(): multidim_condition=condition ) - np.testing.assert_allclose(thrust, np.array([[0.77815736]])) + np.testing.assert_allclose(thrust, np.array([[0.77958497]])) # Multiple turbines with index filter # 4 turbines with 3 x 3 grid arrays @@ -123,27 +123,26 @@ def test_ct(): ) assert len(thrusts[0]) == len(INDEX_FILTER) - thrusts_truth = np.array([ - [0.77815736, 0.77815736], - [0.77815736, 0.77815736], - [0.77815736, 0.77815736], - [0.66626835, 0.66626835 ], - - [0.77815736, 0.77815736], - [0.77815736, 0.77815736], - [0.77815736, 0.77815736], - [0.66626835, 0.66626835 ], - - [0.77815736, 0.77815736], - [0.77815736, 0.77815736], - [0.77815736, 0.77815736], - [0.66626835, 0.66626835 ], - - [0.77815736, 0.77815736], - [0.77815736, 0.77815736], - [0.77815736, 0.77815736], - [0.66626835, 0.66626835 ], - ]) + thrusts_truth = np.array( + [ + [0.77958497, 0.77958497], + [0.77958497, 0.77958497], + [0.77958497, 0.77958497], + [0.66749069, 0.66749069], + [0.77958497, 0.77958497], + [0.77958497, 0.77958497], + [0.77958497, 0.77958497], + [0.66749069, 0.66749069], + [0.77958497, 0.77958497], + [0.77958497, 0.77958497], + [0.77958497, 0.77958497], + [0.66749069, 0.66749069], + [0.77958497, 0.77958497], + [0.77958497, 0.77958497], + [0.77958497, 0.77958497], + [0.66749069, 0.66749069] + ] + ) np.testing.assert_allclose(thrusts, thrusts_truth) def test_power(): @@ -222,7 +221,7 @@ def test_axial_induction(): turbine_type_map = turbine_type_map[None, :] condition = (2, 1) - baseline_ai = np.array([[0.26447651]]) + baseline_ai = np.array([[0.26551081]]) # Single turbine wind_speed = 10.0 diff --git a/tests/turbine_operation_models_unit_test.py b/tests/turbine_operation_models_unit_test.py index 591c3e01c..9a2c7f670 100644 --- a/tests/turbine_operation_models_unit_test.py +++ b/tests/turbine_operation_models_unit_test.py @@ -185,8 +185,11 @@ def test_CosineLossTurbine(): 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(tilt_angles_test) + / cosd(turbine_data["power_thrust_table"]["ref_tilt"]) + ) # Check that thrust coefficient works as expected @@ -200,7 +203,8 @@ def test_CosineLossTurbine(): ) baseline_misalignment_loss = ( cosd(yaw_angles_nom) - * cosd(tilt_angles_nom - turbine_data["power_thrust_table"]["ref_tilt"]) + * cosd(tilt_angles_nom) + / cosd(turbine_data["power_thrust_table"]["ref_tilt"]) ) baseline_ai = ( 1 - np.sqrt(1 - turbine_data["power_thrust_table"]["thrust_coefficient"][truth_index]) @@ -216,8 +220,13 @@ def test_CosineLossTurbine(): 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(tilt_angles_test) + / cosd(turbine_data["power_thrust_table"]["ref_tilt"]) + ) def test_SimpleDeratingTurbine():