From 8eba2421c24b7a90f55fcb110f81e29eb08061dc Mon Sep 17 00:00:00 2001 From: Maximilian Krause Date: Sun, 13 Jul 2025 17:54:17 +0100 Subject: [PATCH 1/8] fix tilt cosine law correction --- floris/core/rotor_velocity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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): From 33d9341f983c83eb35234fd244fd3d950cb64bfb Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 17 Jul 2025 14:40:44 -0600 Subject: [PATCH 2/8] Allow small tolerence on dataframe comparison --- tests/reg_tests/yaw_optimization_regression_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/reg_tests/yaw_optimization_regression_test.py b/tests/reg_tests/yaw_optimization_regression_test.py index 203856646..5ee3af827 100644 --- a/tests/reg_tests/yaw_optimization_regression_test.py +++ b/tests/reg_tests/yaw_optimization_regression_test.py @@ -183,4 +183,4 @@ def test_scipy_yaw_opt(sample_inputs_fixture): print(baseline_scipy.to_string()) print(df_opt.to_string()) - pd.testing.assert_frame_equal(df_opt, baseline_scipy) + pd.testing.assert_frame_equal(df_opt, baseline_scipy, check_exact=False) From cd8c14765293d1cae11da886bf9090dfe4a30caa Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 17 Jul 2025 16:28:41 -0600 Subject: [PATCH 3/8] Unit test for rotor velocity and reg tests for tilt response in EmGauss model --- .../empirical_gauss_regression_test.py | 129 +++++++++++++++++- tests/rotor_velocity_unit_test.py | 54 ++++++++ 2 files changed, 182 insertions(+), 1 deletion(-) diff --git a/tests/reg_tests/empirical_gauss_regression_test.py b/tests/reg_tests/empirical_gauss_regression_test.py index a6bdaa991..accaec9ec 100644 --- a/tests/reg_tests/empirical_gauss_regression_test.py +++ b/tests/reg_tests/empirical_gauss_regression_test.py @@ -17,7 +17,7 @@ ) -DEBUG = False +DEBUG = True VELOCITY_MODEL = "empirical_gauss" DEFLECTION_MODEL = "empirical_gauss" TURBULENCE_MODEL = "wake_induced_mixing" @@ -81,6 +81,36 @@ ] ) +tilted_baseline = np.array( + [ + # 8 m/s + [ + [7.9736858, 0.7860727, 1734488.7059275, 0.2685251], + [5.8772689, 0.8645242, 701326.2977965, 0.3149422], + [5.9207746, 0.8620604, 715878.3769682, 0.3132959], + ], + # 9 m/s + [ + [8.9703965, 0.7848004, 2471404.7824861, 0.2678401], + [6.6160265, 0.8296645, 1021662.4956075, 0.2928522], + [6.7190835, 0.8249955, 1068106.5905155, 0.2900683], + ], + # 10 m/s + [ + [9.9671073, 0.7828046, 3383206.6144193, 0.2667697], + [7.3582832, 0.7993753, 1389768.2236257, 0.2754008], + [7.5210618, 0.7939844, 1483719.0748672, 0.2724339], + ], + # 11 m/s + [ + [10.9638180, 0.7554790, 4470666.5322783, 0.2525779], + [8.2014817, 0.7838741, 1936413.3771887, 0.2669706], + [8.3165486, 0.7837263, 2022618.6190223, 0.2668917], + ], + ] +) + + 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 From f5999b1eb392a70af7b24abd7a998f554aa6a1c4 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 18 Jul 2025 11:44:41 -0600 Subject: [PATCH 4/8] Specify tolerences --- tests/reg_tests/yaw_optimization_regression_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/reg_tests/yaw_optimization_regression_test.py b/tests/reg_tests/yaw_optimization_regression_test.py index 5ee3af827..d87d40b39 100644 --- a/tests/reg_tests/yaw_optimization_regression_test.py +++ b/tests/reg_tests/yaw_optimization_regression_test.py @@ -183,4 +183,5 @@ def test_scipy_yaw_opt(sample_inputs_fixture): print(baseline_scipy.to_string()) print(df_opt.to_string()) - pd.testing.assert_frame_equal(df_opt, baseline_scipy, check_exact=False) + # Only require matching up until 2 decimal places + pd.testing.assert_frame_equal(df_opt, baseline_scipy, check_exact=False, rtol=1e-5, atol=1e-2) From 77b14c7f0cf7e4ccbbab90ea9128e04fe4841982 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 25 Aug 2025 13:12:56 -0600 Subject: [PATCH 5/8] Revert debug flag in test --- tests/reg_tests/empirical_gauss_regression_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/reg_tests/empirical_gauss_regression_test.py b/tests/reg_tests/empirical_gauss_regression_test.py index accaec9ec..bab409f86 100644 --- a/tests/reg_tests/empirical_gauss_regression_test.py +++ b/tests/reg_tests/empirical_gauss_regression_test.py @@ -17,7 +17,7 @@ ) -DEBUG = True +DEBUG = False VELOCITY_MODEL = "empirical_gauss" DEFLECTION_MODEL = "empirical_gauss" TURBULENCE_MODEL = "wake_induced_mixing" From caaa69faf43de3bd744b4dfa0f8e9b5900f6e46a Mon Sep 17 00:00:00 2001 From: Maximilian Krause Date: Thu, 4 Sep 2025 17:15:27 +0100 Subject: [PATCH 6/8] fix thrust tilt cosine law correction --- floris/core/turbine/operation_models.py | 7 +++++-- tests/turbine_operation_models_unit_test.py | 19 ++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) 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/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(): From 654be66d6c3655d05fdd33d01b7eb39a26e8df00 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Sun, 7 Sep 2025 18:37:44 -0600 Subject: [PATCH 7/8] Update reg tests that use non-reference tilt angles --- .../empirical_gauss_regression_test.py | 26 +++++------ tests/turbine_multi_dim_unit_test.py | 45 +++++++++---------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/tests/reg_tests/empirical_gauss_regression_test.py b/tests/reg_tests/empirical_gauss_regression_test.py index bab409f86..e664881df 100644 --- a/tests/reg_tests/empirical_gauss_regression_test.py +++ b/tests/reg_tests/empirical_gauss_regression_test.py @@ -85,28 +85,28 @@ [ # 8 m/s [ - [7.9736858, 0.7860727, 1734488.7059275, 0.2685251], - [5.8772689, 0.8645242, 701326.2977965, 0.3149422], - [5.9207746, 0.8620604, 715878.3769682, 0.3132959], + [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.7848004, 2471404.7824861, 0.2678401], - [6.6160265, 0.8296645, 1021662.4956075, 0.2928522], - [6.7190835, 0.8249955, 1068106.5905155, 0.2900683], + [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.7828046, 3383206.6144193, 0.2667697], - [7.3582832, 0.7993753, 1389768.2236257, 0.2754008], - [7.5210618, 0.7939844, 1483719.0748672, 0.2724339], + [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.7554790, 4470666.5322783, 0.2525779], - [8.2014817, 0.7838741, 1936413.3771887, 0.2669706], - [8.3165486, 0.7837263, 2022618.6190223, 0.2668917], - ], + [10.9638180, 0.7520150, 4470666.5322783, 0.2502624], + [8.2150645, 0.7898565, 1946589.2518193, 0.2714076], + [8.3045772, 0.7897407, 2013649.9637290, 0.2713440], + ] ] ) 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 From 2e1576eff957d939b1e0b70b527bebecedb631fb Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 9 Sep 2025 10:43:35 -0600 Subject: [PATCH 8/8] Update docs for CosineLoss model --- docs/operation_models_user.ipynb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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)." ] }, {