From 8ea3558005544f03f85174597a6ca2cc709e7753 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 17 Oct 2025 20:27:01 -0600 Subject: [PATCH 01/19] Change main solve name for clarity --- floris/core/core.py | 2 +- floris/floris_model.py | 2 +- profiling/profiling.py | 4 ++-- profiling/quality_metrics.py | 6 +++--- .../reg_tests/cumulative_curl_regression_test.py | 2 +- .../reg_tests/empirical_gauss_regression_test.py | 16 ++++++++-------- tests/reg_tests/gauss_regression_test.py | 16 ++++++++-------- .../reg_tests/jensen_jimenez_regression_test.py | 8 ++++---- tests/reg_tests/none_regression_test.py | 8 ++++---- tests/reg_tests/turbopark_regression_test.py | 8 ++++---- .../reg_tests/turboparkgauss_regression_test.py | 8 ++++---- .../turbulence_models_regression_test.py | 2 +- 12 files changed, 41 insertions(+), 41 deletions(-) diff --git a/floris/core/core.py b/floris/core/core.py index 857b90fa45..b5f11338d6 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -148,7 +148,7 @@ def initialize_domain(self): self.state.INITIALIZED - def steady_state_atmospheric_condition(self): + def solve_for_turbines(self): """Perform the steady-state wind farm wake calculations. Note that initialize_domain() is required to be called before this function.""" diff --git a/floris/floris_model.py b/floris/floris_model.py index aaa3840298..545e0c465d 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -501,7 +501,7 @@ def run(self) -> None: self.core.initialize_domain() # Perform the wake calculations - self.core.steady_state_atmospheric_condition() + self.core.solve_for_turbines() def run_no_wake(self) -> None: """ diff --git a/profiling/profiling.py b/profiling/profiling.py index a4fcc769d2..d13910f4f3 100644 --- a/profiling/profiling.py +++ b/profiling/profiling.py @@ -24,7 +24,7 @@ def run_floris(): # floris.farm.flow_field.calculate_wake() # start = time.time() - # cProfile.run('re.compile("floris.steady_state_atmospheric_condition()")') + # cProfile.run('re.compile("floris.solve_for_turbines()")') # end = time.time() # print(start, end, end - start) @@ -51,4 +51,4 @@ def run_floris(): for i in range(N): core = Core.from_dict(copy.deepcopy(sample_inputs.core)) core.initialize_domain() - core.steady_state_atmospheric_condition() + core.solve_for_turbines() diff --git a/profiling/quality_metrics.py b/profiling/quality_metrics.py index 142480550d..b9c0305b7c 100644 --- a/profiling/quality_metrics.py +++ b/profiling/quality_metrics.py @@ -35,7 +35,7 @@ def run_floris(input_dict): start = time.perf_counter() core = Core.from_dict(copy.deepcopy(input_dict.core)) core.initialize_domain() - core.steady_state_atmospheric_condition() + core.solve_for_turbines() end = time.perf_counter() return end - start except KeyError: @@ -87,13 +87,13 @@ def memory_profile(input_dict): # Run once to initialize Python and memory core = Core.from_dict(copy.deepcopy(input_dict.core)) core.initialize_domain() - core.steady_state_atmospheric_condition() + core.solve_for_turbines() with perf(): for i in range(N_ITERATIONS): core = Core.from_dict(copy.deepcopy(input_dict.core)) core.initialize_domain() - core.steady_state_atmospheric_condition() + core.solve_for_turbines() print( "Size of one data array: " diff --git a/tests/reg_tests/cumulative_curl_regression_test.py b/tests/reg_tests/cumulative_curl_regression_test.py index c9428b2619..e846d24223 100644 --- a/tests/reg_tests/cumulative_curl_regression_test.py +++ b/tests/reg_tests/cumulative_curl_regression_test.py @@ -194,7 +194,7 @@ def test_regression_tandem(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex diff --git a/tests/reg_tests/empirical_gauss_regression_test.py b/tests/reg_tests/empirical_gauss_regression_test.py index e664881dff..e02c94c501 100644 --- a/tests/reg_tests/empirical_gauss_regression_test.py +++ b/tests/reg_tests/empirical_gauss_regression_test.py @@ -227,7 +227,7 @@ def test_regression_tandem(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -368,7 +368,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -403,7 +403,7 @@ def test_regression_yaw(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -499,7 +499,7 @@ def test_regression_tilt(sample_inputs_fixture): floris.farm.tilt_angles = tilt_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -601,7 +601,7 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -685,7 +685,7 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -781,7 +781,7 @@ def test_regression_helix(sample_inputs_fixture): floris.farm.awc_amplitudes = awc_amplitudes floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -898,7 +898,7 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u diff --git a/tests/reg_tests/gauss_regression_test.py b/tests/reg_tests/gauss_regression_test.py index 3c97ee0a16..276e5a93cd 100644 --- a/tests/reg_tests/gauss_regression_test.py +++ b/tests/reg_tests/gauss_regression_test.py @@ -286,7 +286,7 @@ def test_regression_tandem(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -427,7 +427,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -461,7 +461,7 @@ def test_regression_yaw(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -559,7 +559,7 @@ def test_regression_gch(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -652,7 +652,7 @@ def test_regression_gch(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -753,7 +753,7 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -853,7 +853,7 @@ def test_regression_secondary_steering(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -969,7 +969,7 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u diff --git a/tests/reg_tests/jensen_jimenez_regression_test.py b/tests/reg_tests/jensen_jimenez_regression_test.py index 026bfc0c9a..c5ac8cfafd 100644 --- a/tests/reg_tests/jensen_jimenez_regression_test.py +++ b/tests/reg_tests/jensen_jimenez_regression_test.py @@ -136,7 +136,7 @@ def test_regression_tandem(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -276,7 +276,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -310,7 +310,7 @@ def test_regression_yaw(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -426,7 +426,7 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u diff --git a/tests/reg_tests/none_regression_test.py b/tests/reg_tests/none_regression_test.py index 5f50920cb4..92b2afeb32 100644 --- a/tests/reg_tests/none_regression_test.py +++ b/tests/reg_tests/none_regression_test.py @@ -137,7 +137,7 @@ def test_regression_tandem(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -277,7 +277,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -312,7 +312,7 @@ def test_regression_yaw(sample_inputs_fixture): floris.initialize_domain() with pytest.raises(ValueError): - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() def test_regression_small_grid_rotation(sample_inputs_fixture): @@ -350,7 +350,7 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u diff --git a/tests/reg_tests/turbopark_regression_test.py b/tests/reg_tests/turbopark_regression_test.py index f4be3f3845..a155b2ce04 100644 --- a/tests/reg_tests/turbopark_regression_test.py +++ b/tests/reg_tests/turbopark_regression_test.py @@ -96,7 +96,7 @@ def test_regression_tandem(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -237,7 +237,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -271,7 +271,7 @@ def test_regression_yaw(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -382,7 +382,7 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u diff --git a/tests/reg_tests/turboparkgauss_regression_test.py b/tests/reg_tests/turboparkgauss_regression_test.py index 1548b71440..ca70ee76a2 100644 --- a/tests/reg_tests/turboparkgauss_regression_test.py +++ b/tests/reg_tests/turboparkgauss_regression_test.py @@ -138,7 +138,7 @@ def test_regression_tandem(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -279,7 +279,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -313,7 +313,7 @@ def test_regression_yaw(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -424,7 +424,7 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u diff --git a/tests/reg_tests/turbulence_models_regression_test.py b/tests/reg_tests/turbulence_models_regression_test.py index 42e63be411..6801331ad9 100644 --- a/tests/reg_tests/turbulence_models_regression_test.py +++ b/tests/reg_tests/turbulence_models_regression_test.py @@ -20,7 +20,7 @@ def test_NoneWakeTurbulence(sample_inputs_fixture): core = Core.from_dict(sample_inputs_fixture.core) core.initialize_domain() - core.steady_state_atmospheric_condition() + core.solve_for_turbines() assert ( core.flow_field.turbulence_intensity_field_sorted[0,:] == turbulence_intensities[0] From 56642845575b02bd659a6a8bf2795639ca9d0423 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 17 Oct 2025 20:27:34 -0600 Subject: [PATCH 02/19] First rough pass for main Jensen solve --- floris/core/core.py | 8 + floris/core/jensen.py | 358 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 floris/core/jensen.py diff --git a/floris/core/core.py b/floris/core/core.py index b5f11338d6..95d1c2fc9c 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -30,6 +30,7 @@ turbopark_solver, WakeModelManager, ) +from floris.core.jensen import JensenJimenez from floris.type_dec import NDArrayFloat from floris.utilities import ( load_yaml, @@ -198,6 +199,13 @@ def solve_for_turbines(self): self.grid, self.wake ) + elif vel_model=="jensen": + model = JensenJimenez() # TODO: what to pass here? + model.turbine_solve( + self.farm, + self.flow_field, + self.grid, + ) else: sequential_solver( self.farm, diff --git a/floris/core/jensen.py b/floris/core/jensen.py new file mode 100644 index 0000000000..3fb8b876e7 --- /dev/null +++ b/floris/core/jensen.py @@ -0,0 +1,358 @@ +import numexpr as ne +import numpy as np + +from floris.core import ( + axial_induction, + BaseModel, + Farm, + FlowField, + FlowFieldGrid, + FlowFieldPlanarGrid, + PointsGrid, + thrust_coefficient, + TurbineGrid, +) + +from attrs import define, field, fields + +from floris.utilities import cosd, sind + +NUM_EPS = fields(BaseModel).NUM_EPS.default + +# TODO: I'll likely want an abstract base class; can work on that later + +@define +class JensenJimenez(): + + # Jensen deficit model parameters + we: float = field(default=0.05) + + # Jimenez deflection model parameters + kd: float = field(default=0.05) # TODO: is this the same as we? + ad: float = field(default=0.0) + bd: float = field(default=0.0) + + # Crespo-Hernandez turbulence model parameters + initial: float = field(converter=float, default=0.1) + constant: float = field(converter=float, default=0.9) + ai: float = field(converter=float, default=0.8) + downstream: float = field(converter=float, default=-0.32) + + # Storage + x_i: np.ndarray = field(init=False) + y_i: np.ndarray = field(init=False) + z_i: np.ndarray = field(init=False) + + yaw_angle_i: np.ndarray = field(init=False) + hub_height_i: np.ndarray = field(init=False) + rotor_diameter_i: np.ndarray = field(init=False) + + ambient_turbulence_intensities: np.ndarray = field(init=False) + + def velocity_deficit( + self, + axial_induction_i: np.ndarray, + deflection_field_i: np.ndarray, + turbulence_intensity_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + y: np.ndarray, + z: np.ndarray, + ) -> np.ndarray: + + # u is 4-dimensional (n wind speeds, n turbines, grid res 1, grid res 2) + # velocities is 3-dimensional (n turbines, grid res 1, grid res 2) + + x_i = self.x_i + y_i = self.y_i + z_i = self.z_i + yaw_angle_i = self.yaw_angle_i + hub_height_i = self.hub_height_i + rotor_diameter_i = self.rotor_diameter_i # Check if numexpr can use these without unpacking? + + rotor_radius = rotor_diameter_i / 2.0 + + dx = ne.evaluate("x - x_i") + dy = ne.evaluate("y - y_i - deflection_field_i") + dz = ne.evaluate("z - z_i") + + we = self.we + + # Construct a boolean mask to include all points downstream of the turbine + downstream_mask = ne.evaluate("dx > 0 + NUM_EPS") + + # Construct a boolean mask to include all points within the wake boundary + # as defined by the Jensen model. This is a linear wake expansion that makes + # a shape like a cone and starts at the turbine disc. + # The left side of the inequality below evaluates the distance from the wake centerline + # for all points including positive and negative values. The inequality compares distance + # from the centerline and it must be below the line defined by the wake + # expansion parameter, "we". + boundary_mask = ne.evaluate("sqrt(dy ** 2 + dz ** 2) < we * dx + rotor_radius") + + # Calculate C for points within the mask and fill points outside with 0 + c = np.where( + np.logical_and(downstream_mask, boundary_mask), + ne.evaluate("(rotor_radius / (rotor_radius + we * dx + NUM_EPS)) ** 2"), # This is "C" + 0.0, + ) + + velocity_deficit = ne.evaluate("2 * axial_induction_i * c") + + return velocity_deficit + + def deflection( + self, + turbulence_intensity_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + ) -> np.ndarray: + # TODO: Does it make more sense for x to simply be on the class? Seems to. + # What should be passed in vs live on the class? + """ + Calculates the deflection field of the wake in relation to the yaw of + the turbine. This is coded as defined in [1]. + + Args: + x_locations (np.array): streamwise locations in wake + y_locations (np.array): spanwise locations in wake + z_locations (np.array): vertical locations in wake + (not used in Jiménez) + turbine (:py:class:`floris.core.turbine.Turbine`): + Turbine object + coord + (:py:meth:`floris.core.turbine_map.TurbineMap.coords`): + Spatial coordinates of wind turbine. + flow_field + (:py:class:`floris.core.flow_field.FlowField`): + Flow field object. + + Returns: + deflection (np.array): Deflected wake centerline. + + + This function calculates the deflection of the entire flow field + given the yaw angle and Ct of the current turbine + """ + + # Unpack for numexpr + kd = self.kd + ad = self.ad + bd = self.bd + + x_i = self.x_i + y_i = self.y_i + yaw_i = self.yaw_angle_i + rotor_diameter_i = self.rotor_diameter_i + + # angle of deflection + xi_init = cosd(yaw_i) * sind(yaw_i) * ct_i / 2.0 + + delta_x = ne.evaluate("x - x_i") + A = ne.evaluate("15 * (2 * kd * delta_x / rotor_diameter_i + 1) ** 4.0 + xi_init ** 2.0") + B = ne.evaluate("(30 * kd / rotor_diameter_i)") + B = ne.evaluate("B * ( 2 * kd * delta_x / rotor_diameter_i + 1 ) ** 5.0") + C = ne.evaluate("xi_init * rotor_diameter_i * (15 + xi_init ** 2.0)") + D = ne.evaluate("30 * kd") + + yYaw_init = ne.evaluate("(xi_init * A / B) - (C / D)") + deflection = ne.evaluate("yYaw_init + ad + bd * delta_x") + + return deflection + + def combination(self, wake_field: np.ndarray, velocity_field: np.ndarray): + """ + Combines the base flow field with the velocity deficits + using sum of squares. + + Args: + u_field (np.array): The base flow field. + u_wake (np.array): The wake to apply to the base flow field. + + Returns: + np.array: The resulting flow field after applying the wake to the + base. + """ + return np.hypot(wake_field, velocity_field) + + def wake_turbulence( + self, + x: np.ndarray, + axial_induction: np.ndarray, + ) -> np.ndarray: + # Replace zeros and negatives with 1 to prevent nans/infs + x_i = self.x_i + rotor_diameter_i = self.rotor_diameter_i + delta_x = x - x_i + ambient_TI = self.ambient_turbulence_intensities + + # TODO: ensure that these fudge factors are needed for different rotations + upstream_mask = delta_x <= 0.1 + downstream_mask = delta_x > -0.1 + + # Keep downstream components Set upstream to 1.0 + delta_x = delta_x * downstream_mask + np.ones_like(delta_x) * upstream_mask + + # turbulence intensity calculation based on Crespo et. al. + constant = self.constant + ai = self.ai + initial = self.initial + downstream = self.downstream + ti = ne.evaluate( + "constant" + " * axial_induction ** ai" + " * ambient_TI ** initial" + " * (delta_x / rotor_diameter_i) ** downstream" + ) + # Mask the 1 values from above with zeros + return ti * downstream_mask + + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + v_wake = np.zeros_like(flow_field.v_initial_sorted) + w_wake = np.zeros_like(flow_field.w_initial_sorted) + + # Expand input turbulence intensity to 4d for (n_turbines, grid, grid) + turbine_turbulence_intensity = np.repeat( + flow_field.turbulence_intensities[:, None, None, None], + farm.n_turbines, + axis=1 + ) + + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity + # with dimensions expanded for (n_turbines, grid, grid) + self.ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + + # Calculate the velocity deficit sequentially from upstream to downstream turbines + for i in range(grid.n_turbines): + + # Turbine quantities + self.set_turbine_i(grid, farm, i) + thrust_coefficient_i = self.turbine_thrust_coefficient(grid, farm, flow_field, i) + axial_induction_i = self.turbine_axial_induction(grid, farm, flow_field, i) + + # Model calculations + deflection_field = self.deflection( + turbine_turbulence_intensity[:, i:i+1], + thrust_coefficient_i, + grid.x_sorted, + ) + + velocity_deficit = self.velocity_deficit( + axial_induction_i, + deflection_field, + turbine_turbulence_intensity[:, i:i+1], + thrust_coefficient_i, + grid.x_sorted, + grid.y_sorted, + grid.z_sorted + ) + + wake_field = self.combination( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + wake_added_turbulence_intensity = self.wake_turbulence( + grid.x_sorted, + axial_induction_i, + ) + + # TODO: all of this looks like it should actually be part of the turbulence model? + + # Calculate wake overlap for wake-added turbulence (WAT) + area_overlap = ( + np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3)) + / (grid.grid_resolution * grid.grid_resolution) + ) + area_overlap = area_overlap[:, :, None, None] + + # Modify wake added turbulence by wake area overlap + downstream_influence_length = 15 * self.rotor_diameter_i + ti_added = ( + area_overlap + * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) + * (grid.x_sorted > self.x_i) + * (np.abs(self.y_i - grid.y_sorted) < 2 * self.rotor_diameter_i) + * (grid.x_sorted <= downstream_influence_length + self.x_i) + ) + + # Combine turbine TIs with WAT + turbine_turbulence_intensity = np.maximum( + np.sqrt(ti_added**2 + self.ambient_turbulence_intensities**2), + turbine_turbulence_intensity + ) + # END TODO + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + flow_field.v_sorted += v_wake + flow_field.w_sorted += w_wake + + def set_turbine_i(self, grid, farm, i): + # TODO: Candidate for method on base model + + # Get the current turbine quantities + self.x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3), keepdims=True) + self.y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3), keepdims=True) + self.z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3), keepdims=True) + + self.yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] + self.hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] + self.rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] + + @staticmethod + def turbine_thrust_coefficient(grid, farm, flow_field, i): + # TODO: put on base model + ct_i = thrust_coefficient( + velocities=flow_field.u_sorted, + turbulence_intensities=flow_field.turbulence_intensity_field_sorted, + air_density=flow_field.air_density, + yaw_angles=farm.yaw_angles_sorted, + tilt_angles=farm.tilt_angles_sorted, + power_setpoints=farm.power_setpoints_sorted, + awc_modes=farm.awc_modes_sorted, + awc_amplitudes=farm.awc_amplitudes_sorted, + thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, + tilt_interps=farm.turbine_tilt_interps, + correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + turbine_power_thrust_tables=farm.turbine_power_thrust_tables, + ix_filter=[i], + average_method=grid.average_method, + cubature_weights=grid.cubature_weights, + multidim_condition=flow_field.multidim_conditions + ) + # Since we are filtering for the i'th turbine in the thrust coefficient function, + # get the first index here (0:1) + return ct_i[:, 0:1, None, None] + + @staticmethod + def turbine_axial_induction(grid, farm, flow_field, i): + # TODO: put on base model + axial_induction_i = axial_induction( + velocities=flow_field.u_sorted, + turbulence_intensities=flow_field.turbulence_intensity_field_sorted, + air_density=flow_field.air_density, + yaw_angles=farm.yaw_angles_sorted, + tilt_angles=farm.tilt_angles_sorted, + power_setpoints=farm.power_setpoints_sorted, + awc_modes=farm.awc_modes_sorted, + awc_amplitudes=farm.awc_amplitudes_sorted, + axial_induction_functions=farm.turbine_axial_induction_functions, + tilt_interps=farm.turbine_tilt_interps, + correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + turbine_power_thrust_tables=farm.turbine_power_thrust_tables, + ix_filter=[i], + average_method=grid.average_method, + cubature_weights=grid.cubature_weights, + multidim_condition=flow_field.multidim_conditions + ) + + return axial_induction_i[:, 0:1, None, None] From f7fdeacf5bf106042c96a73eefcca251c964de61 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 20 Oct 2025 07:49:00 -0600 Subject: [PATCH 03/19] Move to an abstract base model --- floris/core/jensen.py | 151 ++++++++++++++++++++++++------------------ 1 file changed, 87 insertions(+), 64 deletions(-) diff --git a/floris/core/jensen.py b/floris/core/jensen.py index 3fb8b876e7..0cfea7c652 100644 --- a/floris/core/jensen.py +++ b/floris/core/jensen.py @@ -1,3 +1,5 @@ +from abc import abstractmethod + import numexpr as ne import numpy as np @@ -22,7 +24,91 @@ # TODO: I'll likely want an abstract base class; can work on that later @define -class JensenJimenez(): +class BaseWakeModel(): + def set_turbine_i(self, grid, farm, i): + + # Get the current turbine quantities + self.x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3), keepdims=True) + self.y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3), keepdims=True) + self.z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3), keepdims=True) + + self.yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] + self.hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] + self.rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] + + @staticmethod + def turbine_thrust_coefficient(grid, farm, flow_field, i): + ct_i = thrust_coefficient( + velocities=flow_field.u_sorted, + turbulence_intensities=flow_field.turbulence_intensity_field_sorted, + air_density=flow_field.air_density, + yaw_angles=farm.yaw_angles_sorted, + tilt_angles=farm.tilt_angles_sorted, + power_setpoints=farm.power_setpoints_sorted, + awc_modes=farm.awc_modes_sorted, + awc_amplitudes=farm.awc_amplitudes_sorted, + thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, + tilt_interps=farm.turbine_tilt_interps, + correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + turbine_power_thrust_tables=farm.turbine_power_thrust_tables, + ix_filter=[i], + average_method=grid.average_method, + cubature_weights=grid.cubature_weights, + multidim_condition=flow_field.multidim_conditions + ) + # Since we are filtering for the i'th turbine in the thrust coefficient function, + # get the first index here (0:1) + return ct_i[:, 0:1, None, None] + + @abstractmethod + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + raise NotImplementedError( + "The turbine_solve method has not yet been implemented for "+self.__class__.__name__ + ) + + @abstractmethod + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + ): + raise NotImplementedError( + "points_solve is not implemented for "+self.__class__.__name__ + ) + + @staticmethod + def turbine_axial_induction(grid, farm, flow_field, i): + axial_induction_i = axial_induction( + velocities=flow_field.u_sorted, + turbulence_intensities=flow_field.turbulence_intensity_field_sorted, + air_density=flow_field.air_density, + yaw_angles=farm.yaw_angles_sorted, + tilt_angles=farm.tilt_angles_sorted, + power_setpoints=farm.power_setpoints_sorted, + awc_modes=farm.awc_modes_sorted, + awc_amplitudes=farm.awc_amplitudes_sorted, + axial_induction_functions=farm.turbine_axial_induction_functions, + tilt_interps=farm.turbine_tilt_interps, + correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + turbine_power_thrust_tables=farm.turbine_power_thrust_tables, + ix_filter=[i], + average_method=grid.average_method, + cubature_weights=grid.cubature_weights, + multidim_condition=flow_field.multidim_conditions + ) + + return axial_induction_i[:, 0:1, None, None] + +@define +class JensenJimenez(BaseWakeModel): # Jensen deficit model parameters we: float = field(default=0.05) @@ -293,66 +379,3 @@ def turbine_solve( flow_field.u_sorted = flow_field.u_initial_sorted - wake_field flow_field.v_sorted += v_wake flow_field.w_sorted += w_wake - - def set_turbine_i(self, grid, farm, i): - # TODO: Candidate for method on base model - - # Get the current turbine quantities - self.x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - self.y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - self.z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - - self.yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] - self.hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] - self.rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] - - @staticmethod - def turbine_thrust_coefficient(grid, farm, flow_field, i): - # TODO: put on base model - ct_i = thrust_coefficient( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions - ) - # Since we are filtering for the i'th turbine in the thrust coefficient function, - # get the first index here (0:1) - return ct_i[:, 0:1, None, None] - - @staticmethod - def turbine_axial_induction(grid, farm, flow_field, i): - # TODO: put on base model - axial_induction_i = axial_induction( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - axial_induction_functions=farm.turbine_axial_induction_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions - ) - - return axial_induction_i[:, 0:1, None, None] From 5f42b4cc3549b3c50001a414e19c867e3f1c30b2 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 18 Nov 2025 08:37:45 -0700 Subject: [PATCH 04/19] wip --- floris/core/jensen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/floris/core/jensen.py b/floris/core/jensen.py index 0cfea7c652..53f8c89340 100644 --- a/floris/core/jensen.py +++ b/floris/core/jensen.py @@ -21,8 +21,6 @@ NUM_EPS = fields(BaseModel).NUM_EPS.default -# TODO: I'll likely want an abstract base class; can work on that later - @define class BaseWakeModel(): def set_turbine_i(self, grid, farm, i): @@ -149,12 +147,14 @@ def velocity_deficit( # u is 4-dimensional (n wind speeds, n turbines, grid res 1, grid res 2) # velocities is 3-dimensional (n turbines, grid res 1, grid res 2) + # TODO: How much faster is numexpr? Is it worth it still worth it? + x_i = self.x_i y_i = self.y_i z_i = self.z_i yaw_angle_i = self.yaw_angle_i hub_height_i = self.hub_height_i - rotor_diameter_i = self.rotor_diameter_i # Check if numexpr can use these without unpacking? + rotor_diameter_i = self.rotor_diameter_i # Must be unpacked for numexpr? rotor_radius = rotor_diameter_i / 2.0 From d4a48a844babb588e25b8f80da5ab8749c61c90e Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 12 Mar 2026 12:50:39 -0600 Subject: [PATCH 05/19] Full flow solve added --- floris/core/core.py | 7 +++ floris/core/jensen.py | 134 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 139 insertions(+), 2 deletions(-) diff --git a/floris/core/core.py b/floris/core/core.py index b69643629a..5ea0b38440 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -232,6 +232,13 @@ def solve_for_viz(self): full_flow_turbopark_solver(self.farm, self.flow_field, self.grid, self.wake) elif vel_model=="empirical_gauss": full_flow_empirical_gauss_solver(self.farm, self.flow_field, self.grid, self.wake) + elif vel_model=="jensen": + model = JensenJimenez() # TODO: what to pass here? + model.point_solve( + self.farm, + self.flow_field, + self.grid, + ) else: full_flow_sequential_solver(self.farm, self.flow_field, self.grid, self.wake) diff --git a/floris/core/jensen.py b/floris/core/jensen.py index 53f8c89340..fed6f0c293 100644 --- a/floris/core/jensen.py +++ b/floris/core/jensen.py @@ -1,5 +1,6 @@ from abc import abstractmethod +import copy import numexpr as ne import numpy as np @@ -377,5 +378,134 @@ def turbine_solve( # END TODO flow_field.u_sorted = flow_field.u_initial_sorted - wake_field - flow_field.v_sorted += v_wake - flow_field.w_sorted += w_wake + + # Add the final turbine turbulence intensity field to the flow field object + flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity + flow_field.turbulence_intensity_field_sorted_avg = np.mean( + turbine_turbulence_intensity, + axis=(2,3), + keepdims=True + ) + + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + ) -> None: + + # ** TODO: Can this block be moved to a general method, perhaps? + # Get the flow quantities and turbine performance + turbine_grid_farm = copy.deepcopy(farm) + turbine_grid_flow_field = copy.deepcopy(flow_field) + + turbine_grid_farm.construct_turbine_map() + turbine_grid_farm.construct_turbine_thrust_coefficient_functions() + turbine_grid_farm.construct_turbine_axial_induction_functions() + turbine_grid_farm.construct_turbine_power_functions() + turbine_grid_farm.construct_hub_heights() + turbine_grid_farm.construct_rotor_diameters() + turbine_grid_farm.construct_turbine_TSRs() + turbine_grid_farm.construct_turbine_ref_tilts() + turbine_grid_farm.construct_turbine_tilt_interps() + turbine_grid_farm.construct_turbine_correct_cp_ct_for_tilt() + turbine_grid_farm.set_tilt_to_ref_tilt(flow_field.n_findex) + + turbine_grid = TurbineGrid( + turbine_coordinates=turbine_grid_farm.coordinates, + turbine_diameters=turbine_grid_farm.rotor_diameters, + wind_directions=turbine_grid_flow_field.wind_directions, + grid_resolution=3, + ) + turbine_grid_farm.expand_farm_properties( + turbine_grid_flow_field.n_findex, + turbine_grid.sorted_coord_indices, + ) + turbine_grid_flow_field.initialize_velocity_field(turbine_grid) + turbine_grid_farm.initialize(turbine_grid.sorted_indices) + # ** END TODO + + self.turbine_solve(turbine_grid_farm, turbine_grid_flow_field, turbine_grid) + + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + v_wake = np.zeros_like(flow_field.v_initial_sorted) + w_wake = np.zeros_like(flow_field.w_initial_sorted) + + # Initialize the turbulence intensity field over the entire flow field grid + n_points = flow_field_grid.x_sorted.shape[1] + ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) + turbulence_intensity_field = ambient_turbulence_intensities.copy() + + # Calculate the velocity deficit in the full grid sequentially from upstream to + # downstream turbines + for i in range(flow_field_grid.n_turbines): + + # Get the current turbine quantities + self.set_turbine_i(turbine_grid, turbine_grid_farm, i) + thrust_coefficient_i = self.turbine_thrust_coefficient( + turbine_grid, + turbine_grid_farm, + turbine_grid_flow_field, + i + ) + axial_induction_i = self.turbine_axial_induction( + turbine_grid, + turbine_grid_farm, + turbine_grid_flow_field, + i + ) + turbulence_intensity_i = \ + turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] + + # Model calculations + deflection_field = self.deflection( + turbulence_intensity_i, + thrust_coefficient_i, + flow_field_grid.x_sorted, + ) + + velocity_deficit = self.velocity_deficit( + axial_induction_i, + deflection_field, + turbulence_intensity_i, + thrust_coefficient_i, + flow_field_grid.x_sorted, + flow_field_grid.y_sorted, + flow_field_grid.z_sorted + ) + + wake_field = self.combination( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + wake_added_turbulence_intensity = self.wake_turbulence( + flow_field_grid.x_sorted, + axial_induction_i, + ) + + # TODO: all of this looks like it should actually be part of the turbulence model? + + # Calculate locations where wake-added turbulence (WAT) applies + area_overlap = np.where(velocity_deficit * flow_field.u_initial_sorted > 0.05, 1, 0) + + # Modify wake added turbulence by wake area overlap + downstream_influence_length = 15 * self.rotor_diameter_i + ti_added = ( + area_overlap + * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) + * (flow_field_grid.x_sorted > self.x_i) + * (np.abs(self.y_i - flow_field_grid.y_sorted) < 2 * self.rotor_diameter_i) + * (flow_field_grid.x_sorted <= downstream_influence_length + self.x_i) + ) + # Combine turbine TIs with WAT + turbulence_intensity_field = np.maximum( + np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbulence_intensity_field + ) + # END TODO + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + + flow_field.turbulence_intensity_field_sorted = turbulence_intensity_field From 6a501f48c23302b4c208d090a561078bb0071dac Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 12 Mar 2026 13:42:09 -0600 Subject: [PATCH 06/19] Pass in model parameters (needs to be fully built out) --- floris/core/core.py | 6 ++++-- floris/core/jensen.py | 4 ---- floris/core/wake.py | 1 + 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/floris/core/core.py b/floris/core/core.py index 5ea0b38440..fc1fdc6be0 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -153,6 +153,7 @@ def solve_for_turbines(self): initialize_domain() is required to be called before this function.""" vel_model = self.wake.model_strings["velocity_model"] + model_parameters = self.wake.wake_velocity_parameters[vel_model] if vel_model not in ["empirical_gauss"] and \ self.farm.correct_cp_ct_for_tilt.any(): @@ -199,7 +200,7 @@ def solve_for_turbines(self): self.wake ) elif vel_model=="jensen": - model = JensenJimenez() # TODO: what to pass here? + model = JensenJimenez(**model_parameters) model.turbine_solve( self.farm, self.flow_field, @@ -225,6 +226,7 @@ def solve_for_viz(self): self.flow_field.initialize_velocity_field(self.grid) vel_model = self.wake.model_strings["velocity_model"] + model_parameters = self.wake.wake_velocity_parameters[vel_model] if vel_model=="cc": full_flow_cc_solver(self.farm, self.flow_field, self.grid, self.wake) @@ -233,7 +235,7 @@ def solve_for_viz(self): elif vel_model=="empirical_gauss": full_flow_empirical_gauss_solver(self.farm, self.flow_field, self.grid, self.wake) elif vel_model=="jensen": - model = JensenJimenez() # TODO: what to pass here? + model = JensenJimenez(**model_parameters) model.point_solve( self.farm, self.flow_field, diff --git a/floris/core/jensen.py b/floris/core/jensen.py index fed6f0c293..febf2a298b 100644 --- a/floris/core/jensen.py +++ b/floris/core/jensen.py @@ -302,8 +302,6 @@ def turbine_solve( ) -> None: wake_field = np.zeros_like(flow_field.u_initial_sorted) - v_wake = np.zeros_like(flow_field.v_initial_sorted) - w_wake = np.zeros_like(flow_field.w_initial_sorted) # Expand input turbulence intensity to 4d for (n_turbines, grid, grid) turbine_turbulence_intensity = np.repeat( @@ -429,8 +427,6 @@ def point_solve( wake_field = np.zeros_like(flow_field.u_initial_sorted) - v_wake = np.zeros_like(flow_field.v_initial_sorted) - w_wake = np.zeros_like(flow_field.w_initial_sorted) # Initialize the turbulence intensity field over the entire flow field grid n_points = flow_field_grid.x_sorted.shape[1] diff --git a/floris/core/wake.py b/floris/core/wake.py index e58a85cb46..4016c0c18d 100644 --- a/floris/core/wake.py +++ b/floris/core/wake.py @@ -61,6 +61,7 @@ @define class WakeModelManager(BaseClass): + # TODO: Will likely want to reconfigure this, eventually """ WakeModelManager is a container class for the wake velocity, deflection, turbulence, and combination models. From 4de8b17e34514f8c66811cfd2118fded9c57eba5 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 12 Mar 2026 17:05:35 -0600 Subject: [PATCH 07/19] Streamline TI calc --- floris/core/jensen.py | 76 ++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 45 deletions(-) diff --git a/floris/core/jensen.py b/floris/core/jensen.py index febf2a298b..9ea62c6558 100644 --- a/floris/core/jensen.py +++ b/floris/core/jensen.py @@ -262,10 +262,13 @@ def combination(self, wake_field: np.ndarray, velocity_field: np.ndarray): """ return np.hypot(wake_field, velocity_field) - def wake_turbulence( + def turbulence( self, + turbulence_intensity: np.ndarray, x: np.ndarray, + y: np.ndarray, axial_induction: np.ndarray, + area_overlap: np.ndarray, ) -> np.ndarray: # Replace zeros and negatives with 1 to prevent nans/infs x_i = self.x_i @@ -292,7 +295,23 @@ def wake_turbulence( " * (delta_x / rotor_diameter_i) ** downstream" ) # Mask the 1 values from above with zeros - return ti * downstream_mask + wake_added_turbulence_intensity = ti * downstream_mask + + # Modify wake added turbulence by wake area overlap + downstream_influence_length = 15 * self.rotor_diameter_i + ti_added = ( + area_overlap + * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) + * (x > self.x_i) + * (np.abs(self.y_i - y) < 2 * self.rotor_diameter_i) + * (x <= downstream_influence_length + self.x_i) + ) + # Combine turbine TIs with WAT + turbulence_intensity = np.maximum( + np.sqrt(ti_added**2 + ambient_TI**2), turbulence_intensity + ) + + return turbulence_intensity def turbine_solve( self, @@ -344,13 +363,6 @@ def turbine_solve( velocity_deficit * flow_field.u_initial_sorted ) - wake_added_turbulence_intensity = self.wake_turbulence( - grid.x_sorted, - axial_induction_i, - ) - - # TODO: all of this looks like it should actually be part of the turbulence model? - # Calculate wake overlap for wake-added turbulence (WAT) area_overlap = ( np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3)) @@ -358,22 +370,13 @@ def turbine_solve( ) area_overlap = area_overlap[:, :, None, None] - # Modify wake added turbulence by wake area overlap - downstream_influence_length = 15 * self.rotor_diameter_i - ti_added = ( - area_overlap - * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) - * (grid.x_sorted > self.x_i) - * (np.abs(self.y_i - grid.y_sorted) < 2 * self.rotor_diameter_i) - * (grid.x_sorted <= downstream_influence_length + self.x_i) - ) - - # Combine turbine TIs with WAT - turbine_turbulence_intensity = np.maximum( - np.sqrt(ti_added**2 + self.ambient_turbulence_intensities**2), - turbine_turbulence_intensity + turbine_turbulence_intensity = self.turbulence( + turbine_turbulence_intensity, + grid.x_sorted, + grid.y_sorted, + axial_induction_i, + area_overlap, ) - # END TODO flow_field.u_sorted = flow_field.u_initial_sorted - wake_field @@ -477,31 +480,14 @@ def point_solve( velocity_deficit * flow_field.u_initial_sorted ) - wake_added_turbulence_intensity = self.wake_turbulence( + turbulence_intensity_field = self.turbulence( + turbulence_intensity_field, flow_field_grid.x_sorted, + flow_field_grid.y_sorted, axial_induction_i, + np.where(velocity_deficit * flow_field.u_initial_sorted > 0.05, 1, 0), ) - # TODO: all of this looks like it should actually be part of the turbulence model? - - # Calculate locations where wake-added turbulence (WAT) applies - area_overlap = np.where(velocity_deficit * flow_field.u_initial_sorted > 0.05, 1, 0) - - # Modify wake added turbulence by wake area overlap - downstream_influence_length = 15 * self.rotor_diameter_i - ti_added = ( - area_overlap - * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) - * (flow_field_grid.x_sorted > self.x_i) - * (np.abs(self.y_i - flow_field_grid.y_sorted) < 2 * self.rotor_diameter_i) - * (flow_field_grid.x_sorted <= downstream_influence_length + self.x_i) - ) - # Combine turbine TIs with WAT - turbulence_intensity_field = np.maximum( - np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbulence_intensity_field - ) - # END TODO - flow_field.u_sorted = flow_field.u_initial_sorted - wake_field flow_field.turbulence_intensity_field_sorted = turbulence_intensity_field From 7bc9af708801f7a5e5a09682c69381c91aedc02b Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 12 Mar 2026 17:16:01 -0600 Subject: [PATCH 08/19] Construct grids separately --- floris/core/jensen.py | 83 ++++++++++++++++++++++++++----------------- pyproject.toml | 1 + 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/floris/core/jensen.py b/floris/core/jensen.py index 9ea62c6558..40d28be3d9 100644 --- a/floris/core/jensen.py +++ b/floris/core/jensen.py @@ -1,8 +1,13 @@ +import copy from abc import abstractmethod -import copy import numexpr as ne import numpy as np +from attrs import ( + define, + field, + fields, +) from floris.core import ( axial_induction, @@ -15,11 +20,9 @@ thrust_coefficient, TurbineGrid, ) - -from attrs import define, field, fields - from floris.utilities import cosd, sind + NUM_EPS = fields(BaseModel).NUM_EPS.default @define @@ -106,6 +109,44 @@ def turbine_axial_induction(grid, farm, flow_field, i): return axial_induction_i[:, 0:1, None, None] + @staticmethod + def generate_turbine_grid_objects( + farm: Farm, + flow_field: FlowField, + ): + """Generate turbine grid objects from points grid objects. + Intermediate step of point_solve. + """ + turbine_grid_farm = copy.deepcopy(farm) + turbine_grid_flow_field = copy.deepcopy(flow_field) + + turbine_grid_farm.construct_turbine_map() + turbine_grid_farm.construct_turbine_thrust_coefficient_functions() + turbine_grid_farm.construct_turbine_axial_induction_functions() + turbine_grid_farm.construct_turbine_power_functions() + turbine_grid_farm.construct_hub_heights() + turbine_grid_farm.construct_rotor_diameters() + turbine_grid_farm.construct_turbine_TSRs() + turbine_grid_farm.construct_turbine_ref_tilts() + turbine_grid_farm.construct_turbine_tilt_interps() + turbine_grid_farm.construct_turbine_correct_cp_ct_for_tilt() + turbine_grid_farm.set_tilt_to_ref_tilt(flow_field.n_findex) + + turbine_grid = TurbineGrid( + turbine_coordinates=turbine_grid_farm.coordinates, + turbine_diameters=turbine_grid_farm.rotor_diameters, + wind_directions=turbine_grid_flow_field.wind_directions, + grid_resolution=3, + ) + turbine_grid_farm.expand_farm_properties( + turbine_grid_flow_field.n_findex, + turbine_grid.sorted_coord_indices, + ) + turbine_grid_flow_field.initialize_velocity_field(turbine_grid) + turbine_grid_farm.initialize(turbine_grid.sorted_indices) + + return turbine_grid_farm, turbine_grid_flow_field, turbine_grid + @define class JensenJimenez(BaseWakeModel): @@ -395,36 +436,12 @@ def point_solve( flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, ) -> None: - # ** TODO: Can this block be moved to a general method, perhaps? # Get the flow quantities and turbine performance - turbine_grid_farm = copy.deepcopy(farm) - turbine_grid_flow_field = copy.deepcopy(flow_field) - - turbine_grid_farm.construct_turbine_map() - turbine_grid_farm.construct_turbine_thrust_coefficient_functions() - turbine_grid_farm.construct_turbine_axial_induction_functions() - turbine_grid_farm.construct_turbine_power_functions() - turbine_grid_farm.construct_hub_heights() - turbine_grid_farm.construct_rotor_diameters() - turbine_grid_farm.construct_turbine_TSRs() - turbine_grid_farm.construct_turbine_ref_tilts() - turbine_grid_farm.construct_turbine_tilt_interps() - turbine_grid_farm.construct_turbine_correct_cp_ct_for_tilt() - turbine_grid_farm.set_tilt_to_ref_tilt(flow_field.n_findex) - - turbine_grid = TurbineGrid( - turbine_coordinates=turbine_grid_farm.coordinates, - turbine_diameters=turbine_grid_farm.rotor_diameters, - wind_directions=turbine_grid_flow_field.wind_directions, - grid_resolution=3, - ) - turbine_grid_farm.expand_farm_properties( - turbine_grid_flow_field.n_findex, - turbine_grid.sorted_coord_indices, - ) - turbine_grid_flow_field.initialize_velocity_field(turbine_grid) - turbine_grid_farm.initialize(turbine_grid.sorted_indices) - # ** END TODO + ( + turbine_grid_farm, + turbine_grid_flow_field, + turbine_grid + ) = self.generate_turbine_grid_objects(farm, flow_field) self.turbine_solve(turbine_grid_farm, turbine_grid_flow_field, turbine_grid) diff --git a/pyproject.toml b/pyproject.toml index 5111ba8643..13564ef7bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -196,6 +196,7 @@ lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" "floris/core/wake_velocity/jensen.py" = ["F841"] "floris/core/wake_velocity/gauss.py" = ["F841"] "floris/core/wake_velocity/empirical_gauss.py" = ["F841"] +"floris/core/jensen.py" = ["F841"] # Ignore `F401` (import violations) in all `__init__.py` files, and in `path/to/file.py`. "__init__.py" = ["F401"] From 0552f22d7cf846eb1de9d29255e18fe967b58508 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 12 Mar 2026 17:33:26 -0600 Subject: [PATCH 09/19] Reorganize to separate module and base class --- floris/core/core.py | 2 +- floris/core/{ => wake_model}/jensen.py | 128 +----------------- pyproject.toml | 2 +- .../cumulative_curl_regression_test.py | 10 +- 4 files changed, 8 insertions(+), 134 deletions(-) rename floris/core/{ => wake_model}/jensen.py (71%) diff --git a/floris/core/core.py b/floris/core/core.py index fc1fdc6be0..9b34a3870e 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -29,7 +29,7 @@ turbopark_solver, WakeModelManager, ) -from floris.core.jensen import JensenJimenez +from floris.core.wake_model import JensenJimenez from floris.type_dec import NDArrayFloat from floris.utilities import ( load_yaml, diff --git a/floris/core/jensen.py b/floris/core/wake_model/jensen.py similarity index 71% rename from floris/core/jensen.py rename to floris/core/wake_model/jensen.py index 40d28be3d9..7951ea5cf3 100644 --- a/floris/core/jensen.py +++ b/floris/core/wake_model/jensen.py @@ -1,6 +1,3 @@ -import copy -from abc import abstractmethod - import numexpr as ne import numpy as np from attrs import ( @@ -10,143 +7,20 @@ ) from floris.core import ( - axial_induction, BaseModel, Farm, FlowField, FlowFieldGrid, FlowFieldPlanarGrid, PointsGrid, - thrust_coefficient, TurbineGrid, ) +from floris.core.wake_model import BaseWakeModel from floris.utilities import cosd, sind NUM_EPS = fields(BaseModel).NUM_EPS.default -@define -class BaseWakeModel(): - def set_turbine_i(self, grid, farm, i): - - # Get the current turbine quantities - self.x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - self.y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - self.z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - - self.yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] - self.hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] - self.rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] - - @staticmethod - def turbine_thrust_coefficient(grid, farm, flow_field, i): - ct_i = thrust_coefficient( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions - ) - # Since we are filtering for the i'th turbine in the thrust coefficient function, - # get the first index here (0:1) - return ct_i[:, 0:1, None, None] - - @abstractmethod - def turbine_solve( - self, - farm: Farm, - flow_field: FlowField, - grid: TurbineGrid, - ) -> None: - raise NotImplementedError( - "The turbine_solve method has not yet been implemented for "+self.__class__.__name__ - ) - - @abstractmethod - def point_solve( - self, - farm: Farm, - flow_field: FlowField, - flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, - ): - raise NotImplementedError( - "points_solve is not implemented for "+self.__class__.__name__ - ) - - @staticmethod - def turbine_axial_induction(grid, farm, flow_field, i): - axial_induction_i = axial_induction( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - axial_induction_functions=farm.turbine_axial_induction_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions - ) - - return axial_induction_i[:, 0:1, None, None] - - @staticmethod - def generate_turbine_grid_objects( - farm: Farm, - flow_field: FlowField, - ): - """Generate turbine grid objects from points grid objects. - Intermediate step of point_solve. - """ - turbine_grid_farm = copy.deepcopy(farm) - turbine_grid_flow_field = copy.deepcopy(flow_field) - - turbine_grid_farm.construct_turbine_map() - turbine_grid_farm.construct_turbine_thrust_coefficient_functions() - turbine_grid_farm.construct_turbine_axial_induction_functions() - turbine_grid_farm.construct_turbine_power_functions() - turbine_grid_farm.construct_hub_heights() - turbine_grid_farm.construct_rotor_diameters() - turbine_grid_farm.construct_turbine_TSRs() - turbine_grid_farm.construct_turbine_ref_tilts() - turbine_grid_farm.construct_turbine_tilt_interps() - turbine_grid_farm.construct_turbine_correct_cp_ct_for_tilt() - turbine_grid_farm.set_tilt_to_ref_tilt(flow_field.n_findex) - - turbine_grid = TurbineGrid( - turbine_coordinates=turbine_grid_farm.coordinates, - turbine_diameters=turbine_grid_farm.rotor_diameters, - wind_directions=turbine_grid_flow_field.wind_directions, - grid_resolution=3, - ) - turbine_grid_farm.expand_farm_properties( - turbine_grid_flow_field.n_findex, - turbine_grid.sorted_coord_indices, - ) - turbine_grid_flow_field.initialize_velocity_field(turbine_grid) - turbine_grid_farm.initialize(turbine_grid.sorted_indices) - - return turbine_grid_farm, turbine_grid_flow_field, turbine_grid - @define class JensenJimenez(BaseWakeModel): diff --git a/pyproject.toml b/pyproject.toml index 13564ef7bc..9315e8c925 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -196,7 +196,7 @@ lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" "floris/core/wake_velocity/jensen.py" = ["F841"] "floris/core/wake_velocity/gauss.py" = ["F841"] "floris/core/wake_velocity/empirical_gauss.py" = ["F841"] -"floris/core/jensen.py" = ["F841"] +"floris/core/wake_model/*.py" = ["F841"] # Ignore `F401` (import violations) in all `__init__.py` files, and in `path/to/file.py`. "__init__.py" = ["F401"] diff --git a/tests/reg_tests/cumulative_curl_regression_test.py b/tests/reg_tests/cumulative_curl_regression_test.py index e846d24223..bd8d59bc07 100644 --- a/tests/reg_tests/cumulative_curl_regression_test.py +++ b/tests/reg_tests/cumulative_curl_regression_test.py @@ -334,7 +334,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -368,7 +368,7 @@ def test_regression_yaw(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -469,7 +469,7 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -569,7 +569,7 @@ def test_regression_secondary_steering(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -685,7 +685,7 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u From 38a798edd235ab9d2520a0e1c5149169e6bbd4b7 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 12 Mar 2026 17:48:11 -0600 Subject: [PATCH 10/19] Add files missing from last commit; create None wake model --- floris/core/core.py | 19 ++- floris/core/wake_model/__init__.py | 3 + floris/core/wake_model/base_wake_model.py | 139 ++++++++++++++++++++++ floris/core/wake_model/none_model.py | 55 +++++++++ tests/conftest.py | 1 + tests/reg_tests/none_regression_test.py | 18 --- 6 files changed, 216 insertions(+), 19 deletions(-) create mode 100644 floris/core/wake_model/__init__.py create mode 100644 floris/core/wake_model/base_wake_model.py create mode 100644 floris/core/wake_model/none_model.py diff --git a/floris/core/core.py b/floris/core/core.py index 9b34a3870e..02d90bbb3f 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -29,7 +29,10 @@ turbopark_solver, WakeModelManager, ) -from floris.core.wake_model import JensenJimenez +from floris.core.wake_model import ( + JensenJimenez, + NoneWake, +) from floris.type_dec import NDArrayFloat from floris.utilities import ( load_yaml, @@ -206,6 +209,13 @@ def solve_for_turbines(self): self.flow_field, self.grid, ) + elif vel_model=="none": + model = NoneWake(**model_parameters) + model.turbine_solve( + self.farm, + self.flow_field, + self.grid, + ) else: sequential_solver( self.farm, @@ -241,6 +251,13 @@ def solve_for_viz(self): self.flow_field, self.grid, ) + elif vel_model=="none": + model = NoneWake(**model_parameters) + model.point_solve( + self.farm, + self.flow_field, + self.grid, + ) else: full_flow_sequential_solver(self.farm, self.flow_field, self.grid, self.wake) diff --git a/floris/core/wake_model/__init__.py b/floris/core/wake_model/__init__.py new file mode 100644 index 0000000000..a39ee7a5d9 --- /dev/null +++ b/floris/core/wake_model/__init__.py @@ -0,0 +1,3 @@ +from floris.core.wake_model.base_wake_model import BaseWakeModel +from floris.core.wake_model.jensen import JensenJimenez +from floris.core.wake_model.none_model import NoneWake diff --git a/floris/core/wake_model/base_wake_model.py b/floris/core/wake_model/base_wake_model.py new file mode 100644 index 0000000000..bbcc3b2c1b --- /dev/null +++ b/floris/core/wake_model/base_wake_model.py @@ -0,0 +1,139 @@ +import copy +from abc import abstractmethod + +import numpy as np +from attrs import define + +from floris.core import ( + BaseModel, + axial_induction, + Farm, + FlowField, + FlowFieldGrid, + FlowFieldPlanarGrid, + PointsGrid, + thrust_coefficient, + TurbineGrid, +) + +@define +class BaseWakeModel(BaseModel): + def set_turbine_i(self, grid, farm, i): + + # Get the current turbine quantities + self.x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3), keepdims=True) + self.y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3), keepdims=True) + self.z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3), keepdims=True) + + self.yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] + self.hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] + self.rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] + + @staticmethod + def turbine_thrust_coefficient(grid, farm, flow_field, i): + ct_i = thrust_coefficient( + velocities=flow_field.u_sorted, + turbulence_intensities=flow_field.turbulence_intensity_field_sorted, + air_density=flow_field.air_density, + yaw_angles=farm.yaw_angles_sorted, + tilt_angles=farm.tilt_angles_sorted, + power_setpoints=farm.power_setpoints_sorted, + awc_modes=farm.awc_modes_sorted, + awc_amplitudes=farm.awc_amplitudes_sorted, + thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, + tilt_interps=farm.turbine_tilt_interps, + correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + turbine_power_thrust_tables=farm.turbine_power_thrust_tables, + ix_filter=[i], + average_method=grid.average_method, + cubature_weights=grid.cubature_weights, + multidim_condition=flow_field.multidim_conditions + ) + # Since we are filtering for the i'th turbine in the thrust coefficient function, + # get the first index here (0:1) + return ct_i[:, 0:1, None, None] + + @abstractmethod + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + raise NotImplementedError( + "The turbine_solve method has not yet been implemented for "+self.__class__.__name__ + ) + + @abstractmethod + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + ): + raise NotImplementedError( + "points_solve is not implemented for "+self.__class__.__name__ + ) + + @staticmethod + def turbine_axial_induction(grid, farm, flow_field, i): + axial_induction_i = axial_induction( + velocities=flow_field.u_sorted, + turbulence_intensities=flow_field.turbulence_intensity_field_sorted, + air_density=flow_field.air_density, + yaw_angles=farm.yaw_angles_sorted, + tilt_angles=farm.tilt_angles_sorted, + power_setpoints=farm.power_setpoints_sorted, + awc_modes=farm.awc_modes_sorted, + awc_amplitudes=farm.awc_amplitudes_sorted, + axial_induction_functions=farm.turbine_axial_induction_functions, + tilt_interps=farm.turbine_tilt_interps, + correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + turbine_power_thrust_tables=farm.turbine_power_thrust_tables, + ix_filter=[i], + average_method=grid.average_method, + cubature_weights=grid.cubature_weights, + multidim_condition=flow_field.multidim_conditions + ) + + return axial_induction_i[:, 0:1, None, None] + + @staticmethod + def generate_turbine_grid_objects( + farm: Farm, + flow_field: FlowField, + ): + """Generate turbine grid objects from points grid objects. + Intermediate step of point_solve. + """ + turbine_grid_farm = copy.deepcopy(farm) + turbine_grid_flow_field = copy.deepcopy(flow_field) + + turbine_grid_farm.construct_turbine_map() + turbine_grid_farm.construct_turbine_thrust_coefficient_functions() + turbine_grid_farm.construct_turbine_axial_induction_functions() + turbine_grid_farm.construct_turbine_power_functions() + turbine_grid_farm.construct_hub_heights() + turbine_grid_farm.construct_rotor_diameters() + turbine_grid_farm.construct_turbine_TSRs() + turbine_grid_farm.construct_turbine_ref_tilts() + turbine_grid_farm.construct_turbine_tilt_interps() + turbine_grid_farm.construct_turbine_correct_cp_ct_for_tilt() + turbine_grid_farm.set_tilt_to_ref_tilt(flow_field.n_findex) + + turbine_grid = TurbineGrid( + turbine_coordinates=turbine_grid_farm.coordinates, + turbine_diameters=turbine_grid_farm.rotor_diameters, + wind_directions=turbine_grid_flow_field.wind_directions, + grid_resolution=3, + ) + turbine_grid_farm.expand_farm_properties( + turbine_grid_flow_field.n_findex, + turbine_grid.sorted_coord_indices, + ) + turbine_grid_flow_field.initialize_velocity_field(turbine_grid) + turbine_grid_farm.initialize(turbine_grid.sorted_indices) + + return turbine_grid_farm, turbine_grid_flow_field, turbine_grid diff --git a/floris/core/wake_model/none_model.py b/floris/core/wake_model/none_model.py new file mode 100644 index 0000000000..7c99d99759 --- /dev/null +++ b/floris/core/wake_model/none_model.py @@ -0,0 +1,55 @@ +import numexpr as ne +import numpy as np +from attrs import ( + define, + field, + fields, +) + +from floris.core import ( + BaseModel, + Farm, + FlowField, + FlowFieldGrid, + FlowFieldPlanarGrid, + PointsGrid, + TurbineGrid, +) +from floris.core.wake_model import BaseWakeModel +from floris.utilities import cosd, sind + + +NUM_EPS = fields(BaseModel).NUM_EPS.default + +@define +class NoneWake(BaseWakeModel): + + def __attrs_post_init__(self): + self.logger.warning("The wake model is set to 'none'. Wake modeling disabled.") + + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + + # None wake model does not calculate any velocity deficits, so simply set the flow field + flow_field.u_sorted = flow_field.u_initial_sorted.copy() + + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + ) -> None: + + + # Initialize the turbulence intensity field over the entire flow field grid + n_points = flow_field_grid.x_sorted.shape[1] + ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) + + # None wake model; set to final values. + flow_field.u_sorted = flow_field.u_initial_sorted + flow_field.turbulence_intensity_field_sorted = ambient_turbulence_intensities.copy() diff --git a/tests/conftest.py b/tests/conftest.py index 2be43a4c77..f3b9875172 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -520,6 +520,7 @@ def __init__(self): "awc_wake_exp": 1.2, "awc_wake_denominator": 400 }, + "none": {} }, "wake_turbulence_parameters": { "crespo_hernandez": { diff --git a/tests/reg_tests/none_regression_test.py b/tests/reg_tests/none_regression_test.py index 92b2afeb32..df0447aa56 100644 --- a/tests/reg_tests/none_regression_test.py +++ b/tests/reg_tests/none_regression_test.py @@ -297,24 +297,6 @@ def test_regression_rotation(sample_inputs_fixture): assert np.allclose(t3_270, t2_360) -def test_regression_yaw(sample_inputs_fixture): - """ - Tandem turbines with the upstream turbine yawed - """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - - floris = Core.from_dict(sample_inputs_fixture.core) - - yaw_angles = np.zeros((N_FINDEX, N_TURBINES)) - yaw_angles[:,0] = 5.0 - floris.farm.yaw_angles = yaw_angles - - floris.initialize_domain() - with pytest.raises(ValueError): - floris.solve_for_turbines() - - def test_regression_small_grid_rotation(sample_inputs_fixture): """ This utilizes a 5x5 wind farm with the layout in a regular grid oriented along the cardinal From 695eef3e961b6742860766a0b12826b98be85db9 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 12 Mar 2026 18:39:37 -0600 Subject: [PATCH 11/19] WIP gauss model implementation. Not yet passing reg tests --- floris/core/core.py | 15 + floris/core/wake_model/__init__.py | 1 + floris/core/wake_model/base_wake_model.py | 18 +- floris/core/wake_model/gauss.py | 620 ++++++++++++++++++++++ floris/core/wake_model/jensen.py | 10 +- tests/reg_tests/gauss_regression_test.py | 1 + 6 files changed, 655 insertions(+), 10 deletions(-) create mode 100644 floris/core/wake_model/gauss.py diff --git a/floris/core/core.py b/floris/core/core.py index 02d90bbb3f..257b0b4c1d 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -30,6 +30,7 @@ WakeModelManager, ) from floris.core.wake_model import ( + Gauss, JensenJimenez, NoneWake, ) @@ -209,6 +210,13 @@ def solve_for_turbines(self): self.flow_field, self.grid, ) + elif vel_model=="gauss": + model = Gauss(**model_parameters) + model.turbine_solve( + self.farm, + self.flow_field, + self.grid, + ) elif vel_model=="none": model = NoneWake(**model_parameters) model.turbine_solve( @@ -251,6 +259,13 @@ def solve_for_viz(self): self.flow_field, self.grid, ) + elif vel_model=="gauss": + model = Gauss(**model_parameters) + model.point_solve( + self.farm, + self.flow_field, + self.grid, + ) elif vel_model=="none": model = NoneWake(**model_parameters) model.point_solve( diff --git a/floris/core/wake_model/__init__.py b/floris/core/wake_model/__init__.py index a39ee7a5d9..a48f88a237 100644 --- a/floris/core/wake_model/__init__.py +++ b/floris/core/wake_model/__init__.py @@ -1,3 +1,4 @@ from floris.core.wake_model.base_wake_model import BaseWakeModel +from floris.core.wake_model.gauss import Gauss from floris.core.wake_model.jensen import JensenJimenez from floris.core.wake_model.none_model import NoneWake diff --git a/floris/core/wake_model/base_wake_model.py b/floris/core/wake_model/base_wake_model.py index bbcc3b2c1b..717430e685 100644 --- a/floris/core/wake_model/base_wake_model.py +++ b/floris/core/wake_model/base_wake_model.py @@ -2,7 +2,11 @@ from abc import abstractmethod import numpy as np -from attrs import define +from attrs import ( + define, + field, + fields, +) from floris.core import ( BaseModel, @@ -18,6 +22,17 @@ @define class BaseWakeModel(BaseModel): + + # Storage + x_i: np.ndarray = field(init=False) + y_i: np.ndarray = field(init=False) + z_i: np.ndarray = field(init=False) + + yaw_angle_i: np.ndarray = field(init=False) + hub_height_i: np.ndarray = field(init=False) + rotor_diameter_i: np.ndarray = field(init=False) + TSR_i: np.ndarray = field(init=False) + def set_turbine_i(self, grid, farm, i): # Get the current turbine quantities @@ -28,6 +43,7 @@ def set_turbine_i(self, grid, farm, i): self.yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] self.hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] self.rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] + self.TSR_i = farm.TSRs_sorted[:, i:i+1, None, None] @staticmethod def turbine_thrust_coefficient(grid, farm, flow_field, i): diff --git a/floris/core/wake_model/gauss.py b/floris/core/wake_model/gauss.py new file mode 100644 index 0000000000..6ff6f0db95 --- /dev/null +++ b/floris/core/wake_model/gauss.py @@ -0,0 +1,620 @@ +import numexpr as ne +import numpy as np +from attrs import ( + define, + field, + fields, +) + +from floris.core import ( + BaseModel, + Farm, + FlowField, + FlowFieldGrid, + FlowFieldPlanarGrid, + PointsGrid, + TurbineGrid, +) +from floris.core.wake_model import BaseWakeModel +from floris.utilities import cosd, sind +from floris.core.wake_deflection.gauss import ( + calculate_transverse_velocity, + wake_added_yaw, + yaw_added_turbulence_mixing, +) + + +NUM_EPS = fields(BaseModel).NUM_EPS.default + +@define +class Gauss(BaseWakeModel): + + # Gauss deficit model parameters + alpha: float = field(default=0.58) + beta: float = field(default=0.077) + ka: float = field(default=0.38) + kb: float = field(default=0.004) + + # Gauss deflection model parameters + ad: float = field(converter=float, default=0.0) + bd: float = field(converter=float, default=0.0) + dm: float = field(converter=float, default=1.0) + eps_gain: float = field(converter=float, default=0.2) + use_secondary_steering: bool = field(converter=bool, default=True) + + # Crespo-Hernandez turbulence model parameters + initial: float = field(converter=float, default=0.1) + constant: float = field(converter=float, default=0.9) + ai: float = field(converter=float, default=0.8) + downstream: float = field(converter=float, default=-0.32) + + effective_yaw_i: np.ndarray = field(init=False) + + ambient_turbulence_intensities: np.ndarray = field(init=False) + wind_veer: float = field(init=False) + freestream_velocity: np.ndarray = field(init=False) + + def velocity_deficit( + self, + axial_induction_i: np.ndarray, + deflection_field_i: np.ndarray, + turbulence_intensity_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + y: np.ndarray, + z: np.ndarray, + ) -> np.ndarray: + + # yaw_angle is all turbine yaw angles for each wind speed + # Extract and broadcast only the current turbine yaw setting + # for all wind speeds + + # Opposite sign convention in this model + yaw_angle = -1 * self.yaw_angle_i + + # Initialize the velocity deficit + uR = self.freestream_velocity * ct_i / (2.0 * (1 - np.sqrt(1 - ct_i))) + u0 = self.freestream_velocity * np.sqrt(1 - ct_i) + + # Initial lateral bounds + sigma_z0 = self.rotor_diameter_i * 0.5 * np.sqrt(uR / (self.freestream_velocity + u0)) + sigma_y0 = sigma_z0 * cosd(yaw_angle) * cosd(self.wind_veer) + + # Compute the bounds of the near and far wake regions and a mask + + # Start of the near wake + xR = self.x_i + + # Start of the far wake + x0 = np.ones_like(self.freestream_velocity) + x0 *= self.rotor_diameter_i * cosd(yaw_angle) * (1 + np.sqrt(1 - ct_i) ) + x0 /= np.sqrt(2) * ( + 4 * self.alpha * turbulence_intensity_i + 2 * self.beta * (1 - np.sqrt(1 - ct_i) ) + ) + x0 += self.x_i + + # Initialize the velocity deficit array + velocity_deficit = np.zeros_like(self.freestream_velocity) + + # Masks + # When we have only an inequality, the current turbine may be applied its own + # wake in cases where numerical precision cause in incorrect comparison. We've + # applied a small bump to avoid this. "0.1" is arbitrary but it is a small, non + # zero value. + + # This mask defines the near wake; keeps the areas downstream of xR and upstream of x0 + near_wake_mask = (x > xR + 0.1) * (x < x0) + far_wake_mask = (x >= x0) + + # Compute the velocity deficit in the NEAR WAKE region + # ONLY If there are points within the near wake boundary + # TODO: for the TurbineGrid, do we need to do this near wake calculation at all? + # same question for any grid with a resolution larger than the near wake region + if np.sum(near_wake_mask): + + # Calculate the wake expansion + + # This is a linear ramp from 0 to 1 from the start of the near wake to the start + # of the far wake. + near_wake_ramp_up = (x - xR) / (x0 - xR) + # Another linear ramp, but positive upstream of the far wake and negative in the + # far wake; 0 at the start of the far wake + near_wake_ramp_down = (x0 - x) / (x0 - xR) + # near_wake_ramp_down = -1 * (near_wake_ramp_up - 1) # : this is equivalent, right? + + sigma_y = near_wake_ramp_down * 0.501 * self.rotor_diameter_i * np.sqrt(ct_i / 2.0) + sigma_y += near_wake_ramp_up * sigma_y0 + sigma_y *= (x >= xR) + sigma_y += np.ones_like(sigma_y) * (x < xR) * 0.5 * self.rotor_diameter_i + + sigma_z = near_wake_ramp_down * 0.501 * self.rotor_diameter_i * np.sqrt(ct_i / 2.0) + sigma_z += near_wake_ramp_up * sigma_z0 + sigma_z *= (x >= xR) + sigma_z += np.ones_like(sigma_z) * (x < xR) * 0.5 * self.rotor_diameter_i + + r_squared, C = rC( + self.wind_veer, + sigma_y, + sigma_z, + y, + self.y_i, + deflection_field_i, + z, + self.hub_height_i, + ct_i, + yaw_angle, + self.rotor_diameter_i, + ) + + near_wake_deficit = gaussian_function(C, r_squared, 1, np.sqrt(0.5)) + near_wake_deficit *= near_wake_mask + + velocity_deficit += near_wake_deficit + + # Compute the velocity deficit in the FAR WAKE region + if np.sum(far_wake_mask): + + # Wake expansion in the lateral (y) and the vertical (z) + ky = self.ka * turbulence_intensity_i + self.kb # wake expansion parameters + kz = self.ka * turbulence_intensity_i + self.kb # wake expansion parameters + sigma_y = (ky * (x - x0) + sigma_y0) * far_wake_mask + sigma_y0 * (x < x0) + sigma_z = (kz * (x - x0) + sigma_z0) * far_wake_mask + sigma_z0 * (x < x0) + + r_squared, C = rC( + self.wind_veer, + sigma_y, + sigma_z, + y, + self.y_i, + deflection_field_i, + z, + self.hub_height_i, + ct_i, + yaw_angle, + self.rotor_diameter_i, + ) + + far_wake_deficit = gaussian_function(C, r_squared, 1, np.sqrt(0.5)) + far_wake_deficit *= far_wake_mask + + velocity_deficit += far_wake_deficit + + return velocity_deficit + + def deflection( + self, + turbulence_intensity_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + ) -> np.ndarray: + """ + Calculates the deflection field of the wake. See + :cite:`gdm-bastankhah2016experimental` and :cite:`gdm-King2019Controls` + for details on the methods used. + + Args: + x_i (np.array): x-coordinates of turbine i. + y_i (np.array): y-coordinates of turbine i. + yaw_i (np.array): Yaw angle of turbine i. + turbulence_intensity_i (np.array): Turbulence intensity at turbine i. + ct_i (np.array): Thrust coefficient of turbine i. + rotor_diameter_i (float): Rotor diameter of turbine i. + + Returns: + np.array: Deflection field for the wake. + """ + # ============================================================== + + # Opposite sign convention in this model + yaw_i = -1 * self.effective_yaw_i + + # TODO: connect support for tilt + tilt = 0.0 # turbine.tilt_angle + + # initial velocity deficits + uR = ( + self.freestream_velocity + * ct_i + * cosd(tilt) + * cosd(yaw_i) + / (2.0 * (1 - np.sqrt(1 - (ct_i * cosd(tilt) * cosd(yaw_i))))) + ) + u0 = self.freestream_velocity * np.sqrt(1 - ct_i) + + # length of near wake + x0 = ( + self.rotor_diameter_i + * (cosd(yaw_i) * (1 + np.sqrt(1 - ct_i * cosd(yaw_i)))) + / (np.sqrt(2) * ( + 4 * self.alpha * turbulence_intensity_i + 2 * self.beta * (1 - np.sqrt(1 - ct_i)) + )) + self.x_i + ) + + # wake expansion parameters + ky = self.ka * turbulence_intensity_i + self.kb + kz = self.ka * turbulence_intensity_i + self.kb + + C0 = 1 - u0 / self.freestream_velocity + M0 = C0 * (2 - C0) + E0 = ne.evaluate("C0 ** 2 - 3 * exp(1.0 / 12.0) * C0 + 3 * exp(1.0 / 3.0)") + + # initial Gaussian wake expansion + freestream_velocity = self.freestream_velocity # Extract for numexpr + rotor_diameter_i = self.rotor_diameter_i # Extract for numexpr + sigma_z0 = ne.evaluate("rotor_diameter_i * 0.5 * sqrt(uR / (freestream_velocity + u0))") + sigma_y0 = sigma_z0 * cosd(yaw_i) * cosd(self.wind_veer) + + # yR = y - y_i + xR = self.x_i # yR * tand(yaw) + x_i + + # yaw parameters (skew angle and distance from centerline) + # skew angle in radians + theta_c0 = self.dm * (0.3 * np.radians(yaw_i) / cosd(yaw_i)) + theta_c0 *= (1 - np.sqrt(1 - ct_i * cosd(yaw_i))) + delta0 = np.tan(theta_c0) * (x0 - self.x_i) # initial wake deflection; + # NOTE: use np.tan here since theta_c0 is radians + + # deflection in the near wake + delta_near_wake = ((x - xR) / (x0 - xR)) * delta0 + (self.ad + self.bd * (x - self.x_i)) + delta_near_wake *= (x >= xR) & (x <= x0) + + # deflection in the far wake + sigma_y = ky * (x - x0) + sigma_y0 + sigma_z = kz * (x - x0) + sigma_z0 + sigma_y = sigma_y * (x >= x0) + sigma_y0 * (x < x0) + sigma_z = sigma_z * (x >= x0) + sigma_z0 * (x < x0) + + M0_sqrt = np.sqrt(M0) + middle_term = np.sqrt(sigma_y * sigma_z / (sigma_y0 * sigma_z0)) + ln_deltaNum = (1.6 + M0_sqrt) * (1.6 * middle_term - M0_sqrt) + ln_deltaDen = (1.6 - M0_sqrt) * (1.6 * middle_term + M0_sqrt) + + middle_term = ne.evaluate( + "theta_c0" + " * E0" + " / 5.2" + " * sqrt(sigma_y0 * sigma_z0 / (ky * kz * M0))" + " * log(ln_deltaNum / ln_deltaDen)" + ) + delta_far_wake = delta0 + middle_term + (self.ad + self.bd * (x - self.x_i)) + + delta_far_wake = delta_far_wake * (x > x0) + deflection = delta_near_wake + delta_far_wake + + return deflection + + def combination(self, wake_field: np.ndarray, velocity_field: np.ndarray): + """ + Combines the base flow field with the velocity deficits + using sum of squares. + + Args: + u_field (np.array): The base flow field. + u_wake (np.array): The wake to apply to the base flow field. + + Returns: + np.array: The resulting flow field after applying the wake to the + base. + """ + return np.hypot(wake_field, velocity_field) + + def turbulence( + self, + turbulence_intensity: np.ndarray, + x: np.ndarray, + y: np.ndarray, + axial_induction: np.ndarray, + area_overlap: np.ndarray, + ) -> np.ndarray: + # Replace zeros and negatives with 1 to prevent nans/infs + x_i = self.x_i + rotor_diameter_i = self.rotor_diameter_i + delta_x = x - x_i + ambient_TI = self.ambient_turbulence_intensities + + # TODO: ensure that these fudge factors are needed for different rotations + upstream_mask = delta_x <= 0.1 + downstream_mask = delta_x > -0.1 + + # Keep downstream components Set upstream to 1.0 + delta_x = delta_x * downstream_mask + np.ones_like(delta_x) * upstream_mask + + # turbulence intensity calculation based on Crespo et. al. + constant = self.constant + ai = self.ai + initial = self.initial + downstream = self.downstream + ti = ne.evaluate( + "constant" + " * axial_induction ** ai" + " * ambient_TI ** initial" + " * (delta_x / rotor_diameter_i) ** downstream" + ) + # Mask the 1 values from above with zeros + wake_added_turbulence_intensity = ti * downstream_mask + + # Modify wake added turbulence by wake area overlap + downstream_influence_length = 15 * self.rotor_diameter_i + ti_added = ( + area_overlap + * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) + * (x > self.x_i) + * (np.abs(self.y_i - y) < 2 * self.rotor_diameter_i) + * (x <= downstream_influence_length + self.x_i) + ) + # Combine turbine TIs with WAT + turbulence_intensity = np.maximum( + np.sqrt(ti_added**2 + ambient_TI**2), turbulence_intensity + ) + + return turbulence_intensity + + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + + # Expand input turbulence intensity to 4d for (n_turbines, grid, grid) + turbine_turbulence_intensity = np.repeat( + flow_field.turbulence_intensities[:, None, None, None], + farm.n_turbines, + axis=1 + ) + + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity + # with dimensions expanded for (n_turbines, grid, grid) + self.ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + + # Copy uniform flow field parameters + self.freestream_velocity = flow_field.u_initial_sorted + self.wind_veer = flow_field.wind_veer + + # Secondary effect options (TODO: these should be options set by user) + enable_secondary_steering = False + enable_transverse_velocities = False + enable_yaw_added_recovery = False + + # Calculate the velocity deficit sequentially from upstream to downstream turbines + for i in range(grid.n_turbines): + + # Turbine quantities + self.set_turbine_i(grid, farm, i) + thrust_coefficient_i = self.turbine_thrust_coefficient(grid, farm, flow_field, i) + axial_induction_i = self.turbine_axial_induction(grid, farm, flow_field, i) + u_i = flow_field.u_sorted[:, i:i+1] + v_i = flow_field.v_sorted[:, i:i+1] + turbulence_intensity_i = turbine_turbulence_intensity[:, i:i+1] + + # Initialize the effective yaw angle + self.effective_yaw_i = self.yaw_angle_i.copy() + + # Model calculations + if enable_secondary_steering: + added_yaw = wake_added_yaw( + u_i, + v_i, + flow_field.u_initial_sorted, + grid.y_sorted[:, i:i+1] - self.y_i, + grid.z_sorted[:, i:i+1], + self.rotor_diameter_i, + self.hub_height_i, + thrust_coefficient_i, + self.TSR_i, + axial_induction_i, + flow_field.wind_shear, + ) + self.effective_yaw_i += added_yaw + + deflection_field = self.deflection( + turbine_turbulence_intensity[:, i:i+1], + thrust_coefficient_i, + grid.x_sorted, + ) + + if enable_transverse_velocities: + v_wake, w_wake = calculate_transverse_velocity( + u_i, + flow_field.u_initial_sorted, + flow_field.dudz_initial_sorted, + grid.x_sorted - self.x_i, + grid.y_sorted - self.y_i, + grid.z_sorted, + self.rotor_diameter_i, + self.hub_height_i, + self.yaw_angle_i, + thrust_coefficient_i, + self.TSR_i, + axial_induction_i, + flow_field.wind_shear, + ) + + if enable_yaw_added_recovery: + I_mixing = yaw_added_turbulence_mixing( + u_i, + turbulence_intensity_i, + v_i, + flow_field.w_sorted[:, i:i+1], + v_wake[:, i:i+1], + w_wake[:, i:i+1], + ) + gch_gain = 2 + turbine_turbulence_intensity[:, i:i+1] = turbulence_intensity_i + gch_gain * I_mixing + + velocity_deficit = self.velocity_deficit( + axial_induction_i, + deflection_field, + turbine_turbulence_intensity[:, i:i+1], + thrust_coefficient_i, + grid.x_sorted, + grid.y_sorted, + grid.z_sorted + ) + + wake_field = self.combination( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + # Calculate wake overlap for wake-added turbulence (WAT) + area_overlap = ( + np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3)) + / (grid.grid_resolution * grid.grid_resolution) + ) + area_overlap = area_overlap[:, :, None, None] + + turbine_turbulence_intensity = self.turbulence( + turbine_turbulence_intensity, + grid.x_sorted, + grid.y_sorted, + axial_induction_i, + area_overlap, + ) + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + + # Add the final turbine turbulence intensity field to the flow field object + flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity + flow_field.turbulence_intensity_field_sorted_avg = np.mean( + turbine_turbulence_intensity, + axis=(2,3), + keepdims=True + ) + + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + ) -> None: + + # Get the flow quantities and turbine performance + ( + turbine_grid_farm, + turbine_grid_flow_field, + turbine_grid + ) = self.generate_turbine_grid_objects(farm, flow_field) + + self.turbine_solve(turbine_grid_farm, turbine_grid_flow_field, turbine_grid) + + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + + # Initialize the turbulence intensity field over the entire flow field grid + n_points = flow_field_grid.x_sorted.shape[1] + ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) + turbulence_intensity_field = ambient_turbulence_intensities.copy() + + # Calculate the velocity deficit in the full grid sequentially from upstream to + # downstream turbines + for i in range(flow_field_grid.n_turbines): + + # Get the current turbine quantities + self.set_turbine_i(turbine_grid, turbine_grid_farm, i) + thrust_coefficient_i = self.turbine_thrust_coefficient( + turbine_grid, + turbine_grid_farm, + turbine_grid_flow_field, + i + ) + axial_induction_i = self.turbine_axial_induction( + turbine_grid, + turbine_grid_farm, + turbine_grid_flow_field, + i + ) + turbulence_intensity_i = \ + turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] + + # Model calculations + deflection_field = self.deflection( + turbulence_intensity_i, + thrust_coefficient_i, + flow_field_grid.x_sorted, + ) + + velocity_deficit = self.velocity_deficit( + axial_induction_i, + deflection_field, + turbulence_intensity_i, + thrust_coefficient_i, + flow_field_grid.x_sorted, + flow_field_grid.y_sorted, + flow_field_grid.z_sorted + ) + + wake_field = self.combination( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + turbulence_intensity_field = self.turbulence( + turbulence_intensity_field, + flow_field_grid.x_sorted, + flow_field_grid.y_sorted, + axial_induction_i, + np.where(velocity_deficit * flow_field.u_initial_sorted > 0.05, 1, 0), + ) + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + + flow_field.turbulence_intensity_field_sorted = turbulence_intensity_field + + +# @profile +def rC(wind_veer, sigma_y, sigma_z, y, y_i, delta, z, HH, Ct, yaw, D): + + ## original + # a = cosd(wind_veer) ** 2 / (2 * sigma_y ** 2) + sind(wind_veer) ** 2 / (2 * sigma_z ** 2) + # b = -sind(2 * wind_veer) / (4 * sigma_y ** 2) + sind(2 * wind_veer) / (4 * sigma_z ** 2) + # c = sind(wind_veer) ** 2 / (2 * sigma_y ** 2) + cosd(wind_veer) ** 2 / (2 * sigma_z ** 2) + # r_squared = ( + # a * (y - y_i - delta) ** 2 + # - 2 * b * (y - y_i - delta) * (z - HH) + # + c * (z - HH) ** 2 + # ) + # C = 1 - np.sqrt(np.clip(1 - (Ct * cosd(yaw) / (8.0 * sigma_y * sigma_z / D ** 2)), 0.0, 1.0)) + + ## Precalculate some parts + # twox_sigmay_2 = 2 * sigma_y ** 2 + # twox_sigmaz_2 = 2 * sigma_z ** 2 + # a = cosd(wind_veer) ** 2 / (twox_sigmay_2) + sind(wind_veer) ** 2 / (twox_sigmaz_2) + # b = -sind(2 * wind_veer) / (2 * twox_sigmay_2) + sind(2 * wind_veer) / (2 * twox_sigmaz_2) + # c = sind(wind_veer) ** 2 / (twox_sigmay_2) + cosd(wind_veer) ** 2 / (twox_sigmaz_2) + # delta_y = y - y_i - delta + # delta_z = z - HH + # r_squared = (a * (delta_y ** 2) - 2 * b * (delta_y) * (delta_z) + c * (delta_z ** 2)) + # C = 1 - np.sqrt(np.clip(1 - (Ct * cosd(yaw) / (8.0 * sigma_y * sigma_z / (D * D))), 0.0, 1.0)) + + ## Numexpr + wind_veer = np.deg2rad(wind_veer) + a = ne.evaluate( + "cos(wind_veer) ** 2 / (2 * sigma_y ** 2) + sin(wind_veer) ** 2 / (2 * sigma_z ** 2)" + ) + b = ne.evaluate( + "-sin(2 * wind_veer) / (4 * sigma_y ** 2) + sin(2 * wind_veer) / (4 * sigma_z ** 2)" + ) + c = ne.evaluate( + "sin(wind_veer) ** 2 / (2 * sigma_y ** 2) + cos(wind_veer) ** 2 / (2 * sigma_z ** 2)" + ) + r_squared = ne.evaluate( + "a * ((y - y_i - delta) ** 2) - 2 * b * (y - y_i - delta) * (z - HH) + c * ((z - HH) ** 2)" + ) + d = np.clip(1 - (Ct * cosd(yaw) / ( 8.0 * sigma_y * sigma_z / (D * D) )), 0.0, 1.0) + C = ne.evaluate("1 - sqrt(d)") + return r_squared, C + + +def mask_upstream_wake(mesh_y_rotated, x_coord_rotated, y_coord_rotated, turbine_yaw): + yR = mesh_y_rotated - y_coord_rotated + xR = yR * tand(turbine_yaw) + x_coord_rotated + return xR, yR + + +def gaussian_function(C, r_squared, n, sigma): + result = ne.evaluate("C * exp(-1 * r_squared ** n / (2 * sigma ** 2))") + return result diff --git a/floris/core/wake_model/jensen.py b/floris/core/wake_model/jensen.py index 7951ea5cf3..6ea4fdc6e9 100644 --- a/floris/core/wake_model/jensen.py +++ b/floris/core/wake_model/jensen.py @@ -38,15 +38,7 @@ class JensenJimenez(BaseWakeModel): ai: float = field(converter=float, default=0.8) downstream: float = field(converter=float, default=-0.32) - # Storage - x_i: np.ndarray = field(init=False) - y_i: np.ndarray = field(init=False) - z_i: np.ndarray = field(init=False) - - yaw_angle_i: np.ndarray = field(init=False) - hub_height_i: np.ndarray = field(init=False) - rotor_diameter_i: np.ndarray = field(init=False) - + # Uninitialized attributes set in turbine_solve ambient_turbulence_intensities: np.ndarray = field(init=False) def velocity_deficit( diff --git a/tests/reg_tests/gauss_regression_test.py b/tests/reg_tests/gauss_regression_test.py index 276e5a93cd..a76a012b3f 100644 --- a/tests/reg_tests/gauss_regression_test.py +++ b/tests/reg_tests/gauss_regression_test.py @@ -364,6 +364,7 @@ def test_regression_tandem(sample_inputs_fixture): max_findex_print=4, ) + import ipdb; ipdb.set_trace() assert_results_arrays(test_results[0:4], baseline) From 9d75bb2909182189438fddce074584a26b8551a5 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 13 Mar 2026 16:33:30 -0600 Subject: [PATCH 12/19] Guass model passing all tests; pass in parameters --- floris/core/core.py | 29 ++++++- floris/core/wake_model/base_wake_model.py | 3 +- floris/core/wake_model/gauss.py | 77 +++++++++++++++---- tests/conftest.py | 1 - tests/reg_tests/gauss_regression_test.py | 1 - .../turbulence_models_regression_test.py | 49 ++++++------ 6 files changed, 116 insertions(+), 44 deletions(-) diff --git a/floris/core/core.py b/floris/core/core.py index 257b0b4c1d..34cf1b1ef1 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -157,7 +157,7 @@ def solve_for_turbines(self): initialize_domain() is required to be called before this function.""" vel_model = self.wake.model_strings["velocity_model"] - model_parameters = self.wake.wake_velocity_parameters[vel_model] + model_parameters = _temp_create_single_wake_model_dict(self.wake, vel_model) if vel_model not in ["empirical_gauss"] and \ self.farm.correct_cp_ct_for_tilt.any(): @@ -244,7 +244,7 @@ def solve_for_viz(self): self.flow_field.initialize_velocity_field(self.grid) vel_model = self.wake.model_strings["velocity_model"] - model_parameters = self.wake.wake_velocity_parameters[vel_model] + model_parameters = _temp_create_single_wake_model_dict(self.wake, vel_model) if vel_model=="cc": full_flow_cc_solver(self.farm, self.flow_field, self.grid, self.wake) @@ -460,3 +460,28 @@ def check_input_file_for_v3_keys(input_dict) -> None: + "velocity_model to gauss. " + v3_deprecation_msg ) + +def _temp_create_single_wake_model_dict(wake, vel_model): + """ + This is a temporary function until wake model parametrization is unified on the + input dictionary. However, we may use it going forward for back compatibility with + v4. In that case, checks should be made to ensure compatible deficit/deflection/turbulence + models are being used together. + """ + if vel_model == "gauss": + model_parameters = wake.wake_velocity_parameters["gauss"] | \ + wake.wake_deflection_parameters["gauss"] | \ + wake.wake_turbulence_parameters["crespo_hernandez"] + model_parameters["enable_transverse_velocities"] = wake.enable_transverse_velocities + model_parameters["enable_yaw_added_recovery"] = wake.enable_yaw_added_recovery + model_parameters["enable_secondary_steering"] = wake.enable_secondary_steering + elif vel_model == "jensen": + model_parameters = wake.wake_velocity_parameters["jensen"] | \ + wake.wake_deflection_parameters["jimenez"] | \ + wake.wake_turbulence_parameters["crespo_hernandez"] + elif vel_model == "none": + model_parameters = {} + else: + model_parameters = None + + return model_parameters diff --git a/floris/core/wake_model/base_wake_model.py b/floris/core/wake_model/base_wake_model.py index 717430e685..f2df30a898 100644 --- a/floris/core/wake_model/base_wake_model.py +++ b/floris/core/wake_model/base_wake_model.py @@ -9,8 +9,8 @@ ) from floris.core import ( - BaseModel, axial_induction, + BaseModel, Farm, FlowField, FlowFieldGrid, @@ -20,6 +20,7 @@ TurbineGrid, ) + @define class BaseWakeModel(BaseModel): diff --git a/floris/core/wake_model/gauss.py b/floris/core/wake_model/gauss.py index 6ff6f0db95..b0f5196884 100644 --- a/floris/core/wake_model/gauss.py +++ b/floris/core/wake_model/gauss.py @@ -15,13 +15,13 @@ PointsGrid, TurbineGrid, ) -from floris.core.wake_model import BaseWakeModel -from floris.utilities import cosd, sind from floris.core.wake_deflection.gauss import ( calculate_transverse_velocity, wake_added_yaw, yaw_added_turbulence_mixing, ) +from floris.core.wake_model import BaseWakeModel +from floris.utilities import cosd NUM_EPS = fields(BaseModel).NUM_EPS.default @@ -48,6 +48,11 @@ class Gauss(BaseWakeModel): ai: float = field(converter=float, default=0.8) downstream: float = field(converter=float, default=-0.32) + # Secondary effects parameters (GCH) + enable_transverse_velocities: bool = field(converter=bool, default=True) + enable_yaw_added_recovery: bool = field(converter=bool, default=True) + enable_secondary_steering: bool = field(converter=bool, default=True) + effective_yaw_i: np.ndarray = field(init=False) ambient_turbulence_intensities: np.ndarray = field(init=False) @@ -373,10 +378,6 @@ def turbine_solve( self.freestream_velocity = flow_field.u_initial_sorted self.wind_veer = flow_field.wind_veer - # Secondary effect options (TODO: these should be options set by user) - enable_secondary_steering = False - enable_transverse_velocities = False - enable_yaw_added_recovery = False # Calculate the velocity deficit sequentially from upstream to downstream turbines for i in range(grid.n_turbines): @@ -393,7 +394,7 @@ def turbine_solve( self.effective_yaw_i = self.yaw_angle_i.copy() # Model calculations - if enable_secondary_steering: + if self.enable_secondary_steering: added_yaw = wake_added_yaw( u_i, v_i, @@ -415,7 +416,7 @@ def turbine_solve( grid.x_sorted, ) - if enable_transverse_velocities: + if self.enable_transverse_velocities: v_wake, w_wake = calculate_transverse_velocity( u_i, flow_field.u_initial_sorted, @@ -431,8 +432,11 @@ def turbine_solve( axial_induction_i, flow_field.wind_shear, ) + else: + v_wake = np.zeros_like(flow_field.v_initial_sorted) + w_wake = np.zeros_like(flow_field.w_initial_sorted) - if enable_yaw_added_recovery: + if self.enable_yaw_added_recovery: I_mixing = yaw_added_turbulence_mixing( u_i, turbulence_intensity_i, @@ -442,7 +446,9 @@ def turbine_solve( w_wake[:, i:i+1], ) gch_gain = 2 - turbine_turbulence_intensity[:, i:i+1] = turbulence_intensity_i + gch_gain * I_mixing + turbine_turbulence_intensity[:, i:i+1] = ( + turbulence_intensity_i + gch_gain * I_mixing + ) velocity_deficit = self.velocity_deficit( axial_induction_i, @@ -475,6 +481,8 @@ def turbine_solve( ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + flow_field.v_sorted += v_wake + flow_field.w_sorted += w_wake # Add the final turbine turbulence intensity field to the flow field object flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity @@ -509,6 +517,9 @@ def point_solve( ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) turbulence_intensity_field = ambient_turbulence_intensities.copy() + # Extract freestream velocity for deficit, deflection calculations + self.freestream_velocity = flow_field.u_initial_sorted + # Calculate the velocity deficit in the full grid sequentially from upstream to # downstream turbines for i in range(flow_field_grid.n_turbines): @@ -527,16 +538,54 @@ def point_solve( turbine_grid_flow_field, i ) + u_i = turbine_grid_flow_field.u_sorted[:, i:i+1] + v_i = turbine_grid_flow_field.v_sorted[:, i:i+1] turbulence_intensity_i = \ turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] # Model calculations + if self.enable_secondary_steering: + added_yaw = wake_added_yaw( + u_i, + v_i, + turbine_grid_flow_field.u_initial_sorted, + turbine_grid.y_sorted[:, i:i+1] - self.y_i, + turbine_grid.z_sorted[:, i:i+1], + self.rotor_diameter_i, + self.hub_height_i, + thrust_coefficient_i, + self.TSR_i, + axial_induction_i, + flow_field.wind_shear, + ) + self.effective_yaw_i += added_yaw + deflection_field = self.deflection( turbulence_intensity_i, thrust_coefficient_i, flow_field_grid.x_sorted, ) + if self.enable_transverse_velocities: + v_wake, w_wake = calculate_transverse_velocity( + u_i, + flow_field.u_initial_sorted, + flow_field.dudz_initial_sorted, + flow_field_grid.x_sorted - self.x_i, + flow_field_grid.y_sorted - self.y_i, + flow_field_grid.z_sorted, + self.rotor_diameter_i, + self.hub_height_i, + self.yaw_angle_i, + thrust_coefficient_i, + self.TSR_i, + axial_induction_i, + flow_field.wind_shear, + ) + else: + v_wake = np.zeros_like(flow_field.v_initial_sorted) + w_wake = np.zeros_like(flow_field.w_initial_sorted) + velocity_deficit = self.velocity_deficit( axial_induction_i, deflection_field, @@ -561,6 +610,8 @@ def point_solve( ) flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + flow_field.v_sorted += v_wake + flow_field.w_sorted += w_wake flow_field.turbulence_intensity_field_sorted = turbulence_intensity_field @@ -609,12 +660,6 @@ def rC(wind_veer, sigma_y, sigma_z, y, y_i, delta, z, HH, Ct, yaw, D): return r_squared, C -def mask_upstream_wake(mesh_y_rotated, x_coord_rotated, y_coord_rotated, turbine_yaw): - yR = mesh_y_rotated - y_coord_rotated - xR = yR * tand(turbine_yaw) + x_coord_rotated - return xR, yR - - def gaussian_function(C, r_squared, n, sigma): result = ne.evaluate("C * exp(-1 * r_squared ** n / (2 * sigma ** 2))") return result diff --git a/tests/conftest.py b/tests/conftest.py index f3b9875172..2be43a4c77 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -520,7 +520,6 @@ def __init__(self): "awc_wake_exp": 1.2, "awc_wake_denominator": 400 }, - "none": {} }, "wake_turbulence_parameters": { "crespo_hernandez": { diff --git a/tests/reg_tests/gauss_regression_test.py b/tests/reg_tests/gauss_regression_test.py index a76a012b3f..276e5a93cd 100644 --- a/tests/reg_tests/gauss_regression_test.py +++ b/tests/reg_tests/gauss_regression_test.py @@ -364,7 +364,6 @@ def test_regression_tandem(sample_inputs_fixture): max_findex_print=4, ) - import ipdb; ipdb.set_trace() assert_results_arrays(test_results[0:4], baseline) diff --git a/tests/reg_tests/turbulence_models_regression_test.py b/tests/reg_tests/turbulence_models_regression_test.py index 6801331ad9..30d4dcc6cb 100644 --- a/tests/reg_tests/turbulence_models_regression_test.py +++ b/tests/reg_tests/turbulence_models_regression_test.py @@ -1,30 +1,33 @@ -from floris.core import Core -from floris.core.wake_turbulence import NoneWakeTurbulence +# NOTE: This test is no longer valid, as the "none" turbulence model at this +# stage cannot be used with the gauss deficit model. +# from floris.core import Core +# from floris.core.wake_turbulence import NoneWakeTurbulence -VELOCITY_MODEL = "gauss" -DEFLECTION_MODEL = "gauss" -def test_NoneWakeTurbulence(sample_inputs_fixture): +# VELOCITY_MODEL = "gauss" +# DEFLECTION_MODEL = "gauss" - turbulence_intensities = [0.1, 0.05] +# def test_NoneWakeTurbulence(sample_inputs_fixture): - 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"] = "none" - sample_inputs_fixture.core["farm"]["layout_x"] = [0.0, 0.0, 600.0, 600.0] - sample_inputs_fixture.core["farm"]["layout_y"] = [0.0, 600.0, 0.0, 600.0] - sample_inputs_fixture.core["flow_field"]["wind_directions"] = [270.0, 360.0] - sample_inputs_fixture.core["flow_field"]["wind_speeds"] = [8.0, 8.0] - sample_inputs_fixture.core["flow_field"]["turbulence_intensities"] = turbulence_intensities +# turbulence_intensities = [0.1, 0.05] - core = Core.from_dict(sample_inputs_fixture.core) - core.initialize_domain() - core.solve_for_turbines() +# 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"] = "none" +# sample_inputs_fixture.core["farm"]["layout_x"] = [0.0, 0.0, 600.0, 600.0] +# sample_inputs_fixture.core["farm"]["layout_y"] = [0.0, 600.0, 0.0, 600.0] +# sample_inputs_fixture.core["flow_field"]["wind_directions"] = [270.0, 360.0] +# sample_inputs_fixture.core["flow_field"]["wind_speeds"] = [8.0, 8.0] +# sample_inputs_fixture.core["flow_field"]["turbulence_intensities"] = turbulence_intensities - assert ( - core.flow_field.turbulence_intensity_field_sorted[0,:] == turbulence_intensities[0] - ).all() - assert ( - core.flow_field.turbulence_intensity_field_sorted[1,:] == turbulence_intensities[1] - ).all() +# core = Core.from_dict(sample_inputs_fixture.core) +# core.initialize_domain() +# core.solve_for_turbines() + +# assert ( +# core.flow_field.turbulence_intensity_field_sorted[0,:] == turbulence_intensities[0] +# ).all() +# assert ( +# core.flow_field.turbulence_intensity_field_sorted[1,:] == turbulence_intensities[1] +# ).all() From 67d0e0dd03c5db0129a5bf416ecb8f30a5cf4d34 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 16 Mar 2026 09:49:40 -0600 Subject: [PATCH 13/19] Fix impropoer initialization of effective yaw angle --- floris/core/core.py | 36 ++++++--------------------------- floris/core/wake_model/gauss.py | 3 +++ 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/floris/core/core.py b/floris/core/core.py index 34cf1b1ef1..3260b269b0 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -205,25 +205,13 @@ def solve_for_turbines(self): ) elif vel_model=="jensen": model = JensenJimenez(**model_parameters) - model.turbine_solve( - self.farm, - self.flow_field, - self.grid, - ) + model.turbine_solve(self.farm, self.flow_field, self.grid) elif vel_model=="gauss": model = Gauss(**model_parameters) - model.turbine_solve( - self.farm, - self.flow_field, - self.grid, - ) + model.turbine_solve(self.farm, self.flow_field, self.grid) elif vel_model=="none": model = NoneWake(**model_parameters) - model.turbine_solve( - self.farm, - self.flow_field, - self.grid, - ) + model.turbine_solve(self.farm, self.flow_field, self.grid) else: sequential_solver( self.farm, @@ -254,25 +242,13 @@ def solve_for_viz(self): full_flow_empirical_gauss_solver(self.farm, self.flow_field, self.grid, self.wake) elif vel_model=="jensen": model = JensenJimenez(**model_parameters) - model.point_solve( - self.farm, - self.flow_field, - self.grid, - ) + model.point_solve(self.farm, self.flow_field, self.grid) elif vel_model=="gauss": model = Gauss(**model_parameters) - model.point_solve( - self.farm, - self.flow_field, - self.grid, - ) + model.point_solve(self.farm, self.flow_field, self.grid) elif vel_model=="none": model = NoneWake(**model_parameters) - model.point_solve( - self.farm, - self.flow_field, - self.grid, - ) + model.point_solve(self.farm, self.flow_field, self.grid) else: full_flow_sequential_solver(self.farm, self.flow_field, self.grid, self.wake) diff --git a/floris/core/wake_model/gauss.py b/floris/core/wake_model/gauss.py index b0f5196884..0575ca63ca 100644 --- a/floris/core/wake_model/gauss.py +++ b/floris/core/wake_model/gauss.py @@ -543,6 +543,9 @@ def point_solve( turbulence_intensity_i = \ turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] + # Initialize the effective yaw angle + self.effective_yaw_i = self.yaw_angle_i.copy() + # Model calculations if self.enable_secondary_steering: added_yaw = wake_added_yaw( From 6149bfbcd38204541da2ab91be07a7aef243f808 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 9 Apr 2026 08:43:13 -0600 Subject: [PATCH 14/19] Use generic grid name for point_solve --- floris/core/wake_model/base_wake_model.py | 2 +- floris/core/wake_model/gauss.py | 24 +++++++++++------------ floris/core/wake_model/jensen.py | 18 ++++++++--------- floris/core/wake_model/none_model.py | 4 ++-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/floris/core/wake_model/base_wake_model.py b/floris/core/wake_model/base_wake_model.py index f2df30a898..7a1202e2af 100644 --- a/floris/core/wake_model/base_wake_model.py +++ b/floris/core/wake_model/base_wake_model.py @@ -87,7 +87,7 @@ def point_solve( self, farm: Farm, flow_field: FlowField, - flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, ): raise NotImplementedError( "points_solve is not implemented for "+self.__class__.__name__ diff --git a/floris/core/wake_model/gauss.py b/floris/core/wake_model/gauss.py index 0575ca63ca..c6c92b1122 100644 --- a/floris/core/wake_model/gauss.py +++ b/floris/core/wake_model/gauss.py @@ -496,7 +496,7 @@ def point_solve( self, farm: Farm, flow_field: FlowField, - flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, ) -> None: # Get the flow quantities and turbine performance @@ -512,7 +512,7 @@ def point_solve( wake_field = np.zeros_like(flow_field.u_initial_sorted) # Initialize the turbulence intensity field over the entire flow field grid - n_points = flow_field_grid.x_sorted.shape[1] + n_points = grid.x_sorted.shape[1] ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) turbulence_intensity_field = ambient_turbulence_intensities.copy() @@ -522,7 +522,7 @@ def point_solve( # Calculate the velocity deficit in the full grid sequentially from upstream to # downstream turbines - for i in range(flow_field_grid.n_turbines): + for i in range(grid.n_turbines): # Get the current turbine quantities self.set_turbine_i(turbine_grid, turbine_grid_farm, i) @@ -566,7 +566,7 @@ def point_solve( deflection_field = self.deflection( turbulence_intensity_i, thrust_coefficient_i, - flow_field_grid.x_sorted, + grid.x_sorted, ) if self.enable_transverse_velocities: @@ -574,9 +574,9 @@ def point_solve( u_i, flow_field.u_initial_sorted, flow_field.dudz_initial_sorted, - flow_field_grid.x_sorted - self.x_i, - flow_field_grid.y_sorted - self.y_i, - flow_field_grid.z_sorted, + grid.x_sorted - self.x_i, + grid.y_sorted - self.y_i, + grid.z_sorted, self.rotor_diameter_i, self.hub_height_i, self.yaw_angle_i, @@ -594,9 +594,9 @@ def point_solve( deflection_field, turbulence_intensity_i, thrust_coefficient_i, - flow_field_grid.x_sorted, - flow_field_grid.y_sorted, - flow_field_grid.z_sorted + grid.x_sorted, + grid.y_sorted, + grid.z_sorted ) wake_field = self.combination( @@ -606,8 +606,8 @@ def point_solve( turbulence_intensity_field = self.turbulence( turbulence_intensity_field, - flow_field_grid.x_sorted, - flow_field_grid.y_sorted, + grid.x_sorted, + grid.y_sorted, axial_induction_i, np.where(velocity_deficit * flow_field.u_initial_sorted > 0.05, 1, 0), ) diff --git a/floris/core/wake_model/jensen.py b/floris/core/wake_model/jensen.py index 6ea4fdc6e9..396eca4b8f 100644 --- a/floris/core/wake_model/jensen.py +++ b/floris/core/wake_model/jensen.py @@ -299,7 +299,7 @@ def point_solve( self, farm: Farm, flow_field: FlowField, - flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, ) -> None: # Get the flow quantities and turbine performance @@ -315,14 +315,14 @@ def point_solve( wake_field = np.zeros_like(flow_field.u_initial_sorted) # Initialize the turbulence intensity field over the entire flow field grid - n_points = flow_field_grid.x_sorted.shape[1] + n_points = grid.x_sorted.shape[1] ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) turbulence_intensity_field = ambient_turbulence_intensities.copy() # Calculate the velocity deficit in the full grid sequentially from upstream to # downstream turbines - for i in range(flow_field_grid.n_turbines): + for i in range(grid.n_turbines): # Get the current turbine quantities self.set_turbine_i(turbine_grid, turbine_grid_farm, i) @@ -345,7 +345,7 @@ def point_solve( deflection_field = self.deflection( turbulence_intensity_i, thrust_coefficient_i, - flow_field_grid.x_sorted, + grid.x_sorted, ) velocity_deficit = self.velocity_deficit( @@ -353,9 +353,9 @@ def point_solve( deflection_field, turbulence_intensity_i, thrust_coefficient_i, - flow_field_grid.x_sorted, - flow_field_grid.y_sorted, - flow_field_grid.z_sorted + grid.x_sorted, + grid.y_sorted, + grid.z_sorted ) wake_field = self.combination( @@ -365,8 +365,8 @@ def point_solve( turbulence_intensity_field = self.turbulence( turbulence_intensity_field, - flow_field_grid.x_sorted, - flow_field_grid.y_sorted, + grid.x_sorted, + grid.y_sorted, axial_induction_i, np.where(velocity_deficit * flow_field.u_initial_sorted > 0.05, 1, 0), ) diff --git a/floris/core/wake_model/none_model.py b/floris/core/wake_model/none_model.py index 7c99d99759..ef2b26fbdc 100644 --- a/floris/core/wake_model/none_model.py +++ b/floris/core/wake_model/none_model.py @@ -41,12 +41,12 @@ def point_solve( self, farm: Farm, flow_field: FlowField, - flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, ) -> None: # Initialize the turbulence intensity field over the entire flow field grid - n_points = flow_field_grid.x_sorted.shape[1] + n_points = grid.x_sorted.shape[1] ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) From 4eb581062c7372c62e03a5c6e45302fd5c9d0739 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 10 Apr 2026 14:07:03 -0600 Subject: [PATCH 15/19] Remove FlowFieldGrid type hint, as never used. --- floris/core/wake_model/base_wake_model.py | 3 +-- floris/core/wake_model/gauss.py | 3 +-- floris/core/wake_model/jensen.py | 3 +-- floris/core/wake_model/none_model.py | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/floris/core/wake_model/base_wake_model.py b/floris/core/wake_model/base_wake_model.py index 7a1202e2af..b3e171a558 100644 --- a/floris/core/wake_model/base_wake_model.py +++ b/floris/core/wake_model/base_wake_model.py @@ -13,7 +13,6 @@ BaseModel, Farm, FlowField, - FlowFieldGrid, FlowFieldPlanarGrid, PointsGrid, thrust_coefficient, @@ -87,7 +86,7 @@ def point_solve( self, farm: Farm, flow_field: FlowField, - grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + grid: FlowFieldPlanarGrid | PointsGrid, ): raise NotImplementedError( "points_solve is not implemented for "+self.__class__.__name__ diff --git a/floris/core/wake_model/gauss.py b/floris/core/wake_model/gauss.py index c6c92b1122..fa97c54d2d 100644 --- a/floris/core/wake_model/gauss.py +++ b/floris/core/wake_model/gauss.py @@ -10,7 +10,6 @@ BaseModel, Farm, FlowField, - FlowFieldGrid, FlowFieldPlanarGrid, PointsGrid, TurbineGrid, @@ -496,7 +495,7 @@ def point_solve( self, farm: Farm, flow_field: FlowField, - grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + grid: FlowFieldPlanarGrid | PointsGrid, ) -> None: # Get the flow quantities and turbine performance diff --git a/floris/core/wake_model/jensen.py b/floris/core/wake_model/jensen.py index 396eca4b8f..8611ad1568 100644 --- a/floris/core/wake_model/jensen.py +++ b/floris/core/wake_model/jensen.py @@ -10,7 +10,6 @@ BaseModel, Farm, FlowField, - FlowFieldGrid, FlowFieldPlanarGrid, PointsGrid, TurbineGrid, @@ -299,7 +298,7 @@ def point_solve( self, farm: Farm, flow_field: FlowField, - grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + grid: FlowFieldPlanarGrid | PointsGrid, ) -> None: # Get the flow quantities and turbine performance diff --git a/floris/core/wake_model/none_model.py b/floris/core/wake_model/none_model.py index ef2b26fbdc..a40d5ccc4a 100644 --- a/floris/core/wake_model/none_model.py +++ b/floris/core/wake_model/none_model.py @@ -10,7 +10,6 @@ BaseModel, Farm, FlowField, - FlowFieldGrid, FlowFieldPlanarGrid, PointsGrid, TurbineGrid, @@ -41,7 +40,7 @@ def point_solve( self, farm: Farm, flow_field: FlowField, - grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, + grid: FlowFieldPlanarGrid | PointsGrid, ) -> None: From f9b8dffda14a69c31d38516c5f71483ba0f80fab Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 10 Apr 2026 15:42:52 -0600 Subject: [PATCH 16/19] Add functionality for user-defined solvers and document. Some rough edges yet. --- docs/user_defined_wake_models.ipynb | 356 ++++++++++++++++++++++ floris/core/__init__.py | 2 +- floris/core/base.py | 31 ++ floris/core/core.py | 18 +- floris/core/wake.py | 7 + floris/core/wake_model/base_wake_model.py | 20 +- floris/core/wake_model/gauss.py | 8 +- floris/core/wake_model/jensen.py | 2 +- floris/floris_model.py | 10 + 9 files changed, 435 insertions(+), 19 deletions(-) create mode 100644 docs/user_defined_wake_models.ipynb diff --git a/docs/user_defined_wake_models.ipynb b/docs/user_defined_wake_models.ipynb new file mode 100644 index 0000000000..706e6d110a --- /dev/null +++ b/docs/user_defined_wake_models.ipynb @@ -0,0 +1,356 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ba9ae6ce", + "metadata": {}, + "source": [ + "# User-defined Wake Models\n", + "\n", + "Beginning in v5, FLORIS supports user-defined wake models that can be passed directly into FLORIS using `fmodel.set_wake_model`. A user-defined wake model may be a dynamic or static class, but will usually be dynamic to allow model parameters to be set as attributes. It must conform to the `attrs` package for declaring attributes (in particular, wake model parameters). Additionally all user-defined operation models should inherit from the abstract parent class `BaseWakeModel`, available in FLORIS.\n", + "\n", + "All operation models must implement the following \"fundamental\" methods:\n", + "- `turbine_solve`: computes the flow solution at all turbine locations, part of the main FLORIS `run` procedure.\n", + "- `point_solve`: computes the flow solution at arbitrary, user-provided points in the flow, or for cut planes for visualization purposes.\n", + "\n", + "Wake models may then implement additional methods as needed.\n", + "\n", + "The following arguments are passed to either `turbine_solve` or `point_solve` at runtime:\n", + "\n", + "| Argument | Data type | Description |\n", + "|----------|-----------|----------|\n", + "| `farm` | `floris.core.Farm` | text |\n", + "| `flow_field` | `floris.core.FlowField` | The flow field object, which contains the flow solution and other flow-related quantities. |\n", + "| `grid` | `floris.core.TurbineGrid` or `floris.core.FlowFieldPlanarGrid` or `floris.core.PointsGrid` | The grid object corresponding to the type of solve being performed. For `turbine_solve`, this will be a `TurbineGrid`. For `point_solve`, this will be either a `FlowFieldPlanarGrid` or `PointsGrid`, depending on the type of points being solved for (visualization-type solves or individual point solves, respectively). |\n", + "\n", + "The `turbine_solve` and `point_solve` methods do not return any values, but instead update the `flow_field` argument in-place." + ] + }, + { + "cell_type": "markdown", + "id": "aefe4f59", + "metadata": {}, + "source": [ + "### Static example\n", + "\n", + "We begin with a very simple example that will produce a \"straight\" wake behind each turbine, whose velocity deficit (as a fraction of the free stream velocity) is constant and user-definable. This is not a good wake model (and doesn't adhere to momentum conservation)! We're just using it as a basic example to demonstrate the functionality." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d751aa3c", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from attrs import define, field\n", + "from floris.type_dec import floris_float_type, NDArrayFloat\n", + "from floris.core.wake_model import BaseWakeModel\n", + "from floris.flow_visualization import visualize_cut_plane\n", + "\n", + "from floris.core import (\n", + " BaseModel,\n", + " Farm,\n", + " FlowField,\n", + " FlowFieldPlanarGrid,\n", + " PointsGrid,\n", + " TurbineGrid,\n", + ")\n", + "\n", + "@define\n", + "class StraightWake(BaseWakeModel):\n", + " \"\"\"\n", + " A simple wake model that produces a straight wake behind each turbine.\n", + " \"\"\"\n", + "\n", + " # Using attrs, we can define model parameters as class attributes.\n", + " velocity_deficit: float = field(default=0.2)\n", + " wake_width: float = field(default=100.0)\n", + "\n", + " # Define a method for determining whether a test point is within the wake of at least one\n", + " # turbine\n", + " def _is_in_wake(self, grid, turbine_i_x, turbine_i_y, turbine_i_z):\n", + "\n", + " # Declare all True to start\n", + " in_wake_i = np.full(grid.x_sorted.shape, True)\n", + "\n", + " # Check if downstream of any turbine\n", + " in_wake_i &= (grid.x_sorted > turbine_i_x.mean(axis=(2,3), keepdims=True))\n", + "\n", + " # Check if within wake width of any turbine\n", + " in_wake_i &= (\n", + " np.abs(grid.y_sorted - turbine_i_y.mean(axis=(2,3), keepdims=True))\n", + " < self.wake_width / 2\n", + " )\n", + " in_wake_i &= (\n", + " np.abs(grid.z_sorted - turbine_i_z.mean(axis=(2,3), keepdims=True))\n", + " < self.wake_width / 2\n", + " )\n", + "\n", + " # Return resulting boolean array\n", + " return in_wake_i\n", + "\n", + " # Define the main turbine_solve method for solving at turbine locations\n", + " def turbine_solve(\n", + " self,\n", + " farm: Farm,\n", + " flow_field: FlowField,\n", + " grid: TurbineGrid,\n", + " ) -> None:\n", + "\n", + " # Initialize an array to keep track of whether each point is in the wake of any turbine\n", + " in_wake = np.full(grid.x_sorted.shape, False)\n", + "\n", + " for i in range(grid.n_turbines):\n", + "\n", + " # Check if the points are in the wake of turbine i\n", + " in_wake_i = self._is_in_wake(\n", + " grid,\n", + " grid.x_sorted[:, i:i+1, :, :],\n", + " grid.y_sorted[:, i:i+1, :, :],\n", + " grid.z_sorted[:, i:i+1, :, :]\n", + " )\n", + "\n", + " # Update the overall in_wake array to include the wake of turbine i\n", + " in_wake |= in_wake_i\n", + "\n", + " # Apply velocity deficits\n", + " flow_field.u_sorted = flow_field.u_initial_sorted * (1 - self.velocity_deficit * in_wake)\n", + "\n", + " return None\n", + "\n", + " # Define the secondary point_solve method for solving at arbitrary points in the flow\n", + " def point_solve(\n", + " self,\n", + " farm: Farm,\n", + " flow_field: FlowField,\n", + " grid: FlowFieldPlanarGrid | PointsGrid,\n", + " ) -> None:\n", + " # Use parent class method to access the turbine grid\n", + " turbine_grid = self.generate_turbine_grid_objects(farm, flow_field)[2]\n", + "\n", + " # Initialize an array to keep track of whether each point is in the wake of any turbine\n", + " in_wake = np.full(grid.x_sorted.shape, False)\n", + "\n", + " for i in range(turbine_grid.n_turbines):\n", + "\n", + " # Check if the turbine is in the wake of any other turbine\n", + " in_wake_i = self._is_in_wake(\n", + " grid,\n", + " turbine_grid.x_sorted[:, i:i+1, :, :],\n", + " turbine_grid.y_sorted[:, i:i+1, :, :],\n", + " turbine_grid.z_sorted[:, i:i+1, :, :]\n", + " )\n", + "\n", + " # Update the overall in_wake array to include the wake of turbine i\n", + " in_wake |= in_wake_i\n", + "\n", + " # Apply velocity deficits\n", + " flow_field.u_sorted = flow_field.u_initial_sorted * (1 - self.velocity_deficit * in_wake)\n", + "\n", + " return None" + ] + }, + { + "cell_type": "markdown", + "id": "29ea6830", + "metadata": {}, + "source": [ + "Let's now use this straight wake model in FLORIS." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "b74802c2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Powers [W]:\n", + " [[1753954.45917917 907959.89293936]\n", + " [3417797.00509157 1753954.45917917]\n", + " [5000000. 5000000. ]] \n", + "\n", + "Thrust coefficients [-]:\n", + " [[0.78715145 0.84361556]\n", + " [0.78387889 0.78715145]\n", + " [0.55092883 0.55092883]] \n", + "\n" + ] + } + ], + "source": [ + "from floris import FlorisModel, TimeSeries\n", + "\n", + "fmodel = FlorisModel(\"defaults\")\n", + "time_series = TimeSeries(\n", + " wind_directions=np.array([270.0, 270.0, 280.0]),\n", + " wind_speeds=np.array([8.0, 10.0, 12.0]),\n", + " turbulence_intensities=np.array([0.06, 0.06, 0.06]),\n", + ")\n", + "fmodel.set(\n", + " layout_x = [0.0, 500.0],\n", + " layout_y = [0.0, 0.0],\n", + " wind_data=time_series,\n", + ")\n", + "fmodel.set_wake_model(StraightWake(velocity_deficit=0.2, wake_width=100.0))\n", + "\n", + "fmodel.run()\n", + "\n", + "print(\"Powers [W]:\\n\", fmodel.get_turbine_powers(), \"\\n\")\n", + "print(\"Thrust coefficients [-]:\\n\", fmodel.get_turbine_thrust_coefficients(), \"\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "aa2b73b0", + "metadata": {}, + "source": [ + "## Visualization example\n", + "\n", + "Now, we will perform a flow visualization, which uses the `point_solve` method." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "93f1e99f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAj0AAAEgCAYAAABfMcLZAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAATY1JREFUeJztnQecXFXd/p/Z6bO9l2R3swlpJKEFCAEJKrwEjSWKiohSpQkIBhEiCEZ9DRCKigryvlLeF5Gigv+XppDQYkIVSA8kJJts353tu9N25/4/vzNzh5nZ2ZotU57v5zPZzJ07d86555bn/p7zO8egaZoGQgghhJAkJ22qC0AIIYQQMhlQ9BBCCCEkJaDoIYQQQkhKQNFDCCGEkJSAoocQQgghKQFFDyGEEEJSAooeQgghhKQEFD2EEEIISQkoegghhBCSElD0kENmxowZOP/885HovPLKKzAYDOpvqu8XKbeUf6TrZmRkIJ759Kc/jYULFw673v79+9Ux8NBDD01KucjE8tOf/lS153ge79HI9q+88kqMFzwGJxaKniRCThI5Wd55551DuvAnKr/85S/x9NNPT9p+jvW64YYbkIz09vaqG8h4CcLh9mP4a6w3o0S9Qesvs9ms6v79738f7e3tY9rmc889p7ZLCAlgCv4lZMzs3r0baWlpcSF6vva1r2HlypWT8ns/+9nPUFVVFbEsWUTlf/3Xf8Hv90eInjVr1oTE83iwbNky/O///m/Esu9+97s4/vjjcckll4SWTWYUqbKyEi6XSwmOqeLee+9Vde7p6cH69etxzz334N///jc2btw4JtHzu9/9jsKHkCAUPWRMyDy1brcbdrsdVqsVqcjnPvc5HHvssUhGJuOmP3PmTPUK57LLLlPLvv3tb4/Lb4hwSE9PH/H6EmGx2WyYSkS4FxQUqP9feuml+OY3v4nHH38cb731lhKEU42IYa/XO+X7iZCxMPWP52RK6evrw89//nPMmjVLiRcJp//4xz+Gx+OJWE+Wf+ELX8A//vEPdaMXsfOHP/whZt+VoawK8at1NmzYgJNPPlndlHJycvDlL38ZO3fujBny37Nnj/oNWS87OxsXXHCBij6E/6bc4B5++OHQb+llqq6uxve+9z3MnTtXlTs/Px9f//rXI8oyWXz88cfqt/Py8uBwOHDCCSfg2WefjRCTcsNbtWpVxE1G6m00GiNsjttuuw0mkwnd3d0xf0vWle/85je/CS1raWlRUTnZB/JbOpdffjlKSkpi9nGQ/VRYWKj+L9Eeff9GRw9qa2tVlE2iFLL+D3/4Q/T3909IP6tY/R70vkV79+7F5z//eWRmZuKcc86J+N67776LE088UR0HEqW77777RrzdkdRP2upXv/oVFixYoERBcXGxEi5tbW1j3gdyjghSr3CefPJJLF68WNVFjhkRilLG8HJLlEcIPwd15Hy59tprUV5ers59OT/uuOOOiOMivM/Kn/70J1UvWfeFF15Qnz322GOqDLKvs7KysGjRIvz6178etk7yO9IOchxK+WUbf/nLXwasp/+22NYSRZXfljLovx+ORMKOO+44td/leqZfn8bKSMuoI/tH9qH8vqz72muvDVhH2ufCCy9Ux4VelwceeGDYsjQ0NKhr3vTp09X3SktL1fVyKq5hiQ4jPUlIR0eHurlF4/P5BiwTO0GEgjxdygXwzTffxNq1a5X4eOqppwbYWGeffba6iF988cXqBI9FtGUh3HTTTWhqagpZFS+99JKKlMhTvdw8xVKQMP5JJ52kQvnR/Ti+8Y1vqJuUlE0+/+///m8UFRWpG7/+m9HWiFz4hLfffhubNm1ST8xy0ZALhVgIYtPs2LFDiY/x2s/6E3osGhsb1UVUxJr005CLqez7L33pS+pi+pWvfEVd5GUfhF8wt2zZon5LxMq//vUvrFixQi1//fXXcfTRRw9q/4hQkhuFbEt+T78xyG+0traqustFV9+WfnONRm7wsr9EGEkZv/rVr6rlRxxxRGgdufkvX74cS5YsUTcLad8777xTtYF8bzJFvJTjU5/6lCpHeNuK8BAxJMeSHMdPPPGEKpvFYlE3oqEYaf3k3BDBJDco2ef79u3Db3/7W7z33nuq7cYSQdNvbLm5uaFl+m/ITV7OCTm2RGzIb8hvSdtLWerq6vDiiy8OOCdF2Mhx9/LLL+Oiiy7CUUcdpR5orrvuOnVjvvvuuyPWlwcU2V8iQOQYl/NTtiv78dRTTw2dh3LdkDJcffXVQ9ZJyiq/L6JUokYinuRh4Jlnngkd3zpyzP7tb39TDy4irkTEn3nmmThw4IA6h4StW7fi9NNPV8eqXE/kOLjllluUuBgroynjq6++qqJx0uYiSn7/+9/jjDPOUNE53fKWNpKHHF3ISVmff/55tf87OztxzTXXDFoWqe/27dtx1VVXqX0v11LZ/7IPUqXP27ihkaThwQcflEe0IV8LFiwIrf/++++rZd/97ncjtvPDH/5QLd+wYUNoWWVlpVr2wgsvDPhd+ey8884btFy33367+u7//M//hJYdddRRWlFRkeZ0OkPLPvjgAy0tLU0799xzQ8tuueUW9d0LL7wwYptf+cpXtPz8/Ihl6enpMcvR29s7YNnmzZsHlOnll19Wy+TvWPfzUPvlmmuuUeu8/vrroWVdXV1aVVWVNmPGDK2/v18tW7dunWY0GrXOzk71/je/+Y3a1vHHH69df/31apmsm5OTo/3gBz8YsqxXXHGFVlxcHHq/atUqbdmyZWrf33vvvWqZtIHBYNB+/etfh9aTcstv6jQ3N6uyS3tEI+vKZz/72c8ilh999NHa4sWLtdEQ3YaDtcm+ffvUcmmL6HLccMMNA7Z7yimnqM/uvPPO0DKPxxM6Dr1e77DbHa5+0q6y3p/+9KeI9eScibU8Gv1Y3717t9rf+/fv1x544AHNbrdrhYWFWk9Pj1pPyiplXrhwoeZyuULff+aZZ9T3b7755oj2j3WZf/rpp9XyX/ziFxHLv/a1r6ljYc+ePaFlsp6cl9u3b49Y9+qrr9aysrK0vr4+bbREn5NSJ6nPZz/72Yjl8tsWiyWiPHKdkOX33HNPaNnKlSs1m82mVVdXh5bt2LFDnUcjuc1FH++jLaO83nnnndAyKYeUR65TOhdddJFWWlqqtbS0RHz/m9/8ppadnR36vehjsK2tTb2X6wI5dGhvJSES0pangOhX+JO53slRCLdSBIn4COG2iyCRFnnaHQ3yJLl69Wr1hPKd73xHLauvr8f777+vwu9i8+hI+f7jP/4jVK7ovh7hSFTC6XSqJ6ThkNB0eLRLvnfYYYepp2GJGo3nfh4KqZdEoiQKoSNRGolMydO8RF70uklkQaJT4VEYecn/hW3btin7arDojI58Lk+YEqXTtyUdiMO3JU/Scu0eblvDEauNxM6bbAaLLIkVKNEPHYnwyHt5ahbb61DrJ3aTWK9yDEsEUH+J1SHtLOfCSJAIqkQB5AleIlByrEpEQI9aSXamlFkiH+H9aiT6MG/evAHn7WDHolifegQw/NyXY0F+L5xTTjkFhx9+eMQyOX/EIhvuuB/unJQInEQyZX/GOh9PO+20UNRWv06IlabvezlXJEol1mNFRUVovfnz54/6ejXWMi5dulS1s46UQ+wnKZeUT/bpX//6V3zxi19U/w8/PqSMsu3BrkVSDjlWxeI9FJuUBKC9lYTIjTVWB1sJj4fbMdLXRSwTuaiGI3075IImn4cTnak0HDU1NTjrrLOUXXPXXXdF/K4Qyx6TC5VcKKI7oIZfzPS6CHIRkAvgUIh1JhbAgw8+qEL34X0W5GIz3vt5MKTeYo/EqrP+uYTCjznmGHWDE1EiF0T5K31ppF3EApQO5LpgCRdQsdCFjKwv1p5YH7/4xS/UTVVsGv0z2YdHHnkkxorcfPV+P+FtNNkXaRE2Us9YlJWVDejUPGfOHPVXRKdYD4dSv48++kgdT2K7xkKEykiQm6O0R3Nzs7JyxCILvwEPdf6I6BlJlpdsQ/aH2EWDHYvDnfsiusTyEpt62rRpyl4S61BsneEQi0iOQ3n4Ce8/GGtMnehzP3rfy36Sc3z27NkD1pN9FOshaiSMpoyxfluOLbGypXxynZWHlPvvv1+9RnN8iF0m9qEIUrHr5DiV/pXnnntuRD88MjIoesiIBu8Swi+8wyEeuPQTkhNWLoxyMzoU5Kk0FtGdLmMhUSYRPOKZyxOZPI1LnaWPT3hadrwg/T5EHElfHOnALZ0YRbzIBU8iVdLvSoSK3OCib8TRyI1NbliyLYkcyP6SfSDfk34XcnOTbUlfo0MZdmCw9pmoY3OwDtJyvE3E8AkjqZ8cSyJ4pENrLIZrKx2JxOl9wyQyIJ2DpV+JRKOmamiIWOe+1FUEgTykSGRIXnKeyc1Y+qoNhhxv0ldG6il9X6RTrhzz8t1HH310XM/9sTLaMg6Hfp2RzubnnXdezHWiI/HhyLVLjgXp0C37+yc/+Yl6kJO+VtKvj4wcip4URsYkkZNRnlD1JzxB7BB5KpHPx4qEzeWCKDfb6M6E+nZ1yyWcXbt2qQv+aNKMh7tBSidhudBIx1MdiZaMdcC3sSL1HqzO+uc6InLk6U46zMr+EIEj9ZOOx3JBlpc87Y0E2Za0g4gf6bAqT/cS1RHxJ1kwElbXx+A5VGE83ugRvei2io5EjATp1BsdQfzwww/V3/HoDCoWjLSXRDZH84AwFGKLSYdc6bQsDw8i1MPPn89+9rMR68uy8ONosHaTdaSsXV1dEdGeWMfiUIjtIjdjecm1RKI/kjUlN+XoCHJ4JEsiZ3LzDh/uQgTFWBAxKftbrmPRxDrfRsJoyxjrt+XYkoitLnZlP4tYF7turMeXRHvkJb8n57Jc0x555JExbS9VYZ+eFEYyWQRJsQ1Ht6KiMxRGilwY5MInfV5ijSsiT01ywsrTYPjNTPqp/POf/wyVa7TIzSyWkJEnxeinQrGJDjWderRIvSSbY/PmzaFlchOWcLfcdMP7TIhQkZC6tI1YWPrNS5ZLJo7cwEfaB0fWE/tGskv070jEQKI70tYSPRpuW3p/kqkQitJ+0em/8vQ9WiSjJzyNWaKR8l5uSuH9McaKWDtyTMkQELF+e6z7TqI8YtnpGVJiqUqURdLtw20XibRI9lT4easLvOjflmNRyiqZZeFI1pYca2JZDYf0jQtHjik9WhE95EU40p7yG+HnnxyfYx1NXbYnNrB8X7KZdGRfiGgZ6zZHU0Y5p8P75Bw8eBB///vfleUn25KXZGCJmJLrXDRigQ2GWGTykBYtgEREDbWfSWwY6Ulh5GlfIiBy05WLonRWlJuyiBHpFPiZz3xm1NuUPkPytCc3cHlCin4KkZRnuRCvW7dOXVjFapGUTT1lXaIPYx09Vm5c8vQqN3Ld1hGbSCIiIhRk21IuuUDJenq662QhU1T8+c9/VvWWSJh04pZ9LX025GIYbl3IfhFLUJ5Uw0cnlnC7pI8LoxE9gmxLRq0O35bcKKWdJPV5KORJWvadCCfpqyBll/5HEz0CtbSZpAnLsSE3IbnYS1+LkfaPCUeOCREOcvOSOkhdJBopx/94DMYo5490jBbbQbYrNzzZrjyVSydnSYEWy3e0yDbEipR0conMSZ8ZqYdEf+Q3JW1cT1kX8fyDH/wg9F1dzMnxJsJAbr4SLZLIjJzfN954o9ofci2QBw65UYuVEt5xeDBkiAgZ+kCiTSLKJPom7SQPNOGR42hElMk5KvX41re+pdpSHpAkMiTDM4wFiVTKvpFjXa4/IjKlLBIZHcs2R1tGOQ9k/4anrOvl0rn11ltVZ3a5JsmQH3I+yf4TsSTXI/l/LCRiJMMCiKiW78h1QYYTkTaXtiSjZBwywEicoKdSv/322zE/l7Td8JR1wefzaWvWrFFp02azWSsvL9dWr16tud3uiPUknXPFihUxtxuemq2nWw72ks91XnrpJe2kk05SKbmS+vrFL35RpZnGSuOVFN5YdQ3f3q5du1Q6tmxPPtPLJCmfF1xwgVZQUKBlZGRoy5cvV+tGp5SPNmV9sP0ca7/o7N27V6UFS7q5pLRKGrqkGsfiuOOOU7/z5ptvhpbV1NSoZdJOo0FSnOV7jY2NoWUbN25Uy04++eQRpfBu2rRJpWhLCnF4+rqsK6nm0ehtNxpiDTsgbX/mmWdqDodDy83N1S699FJt27ZtMVPLY5Uj/NiXtOKlS5eqfS/1++1vfxux3mAp66Op3/3336/2kxyHmZmZ2qJFi7Qf/ehHWl1d3ZB1H+xYFzo6OlRas9RD5/HHH1dp81arVcvLy9POOeccdXyEI+nkV111lUp5l1T08PLKcAky5EFZWZk692fPnq3Sov1+f8Q25DuS+h7NX/7yF+30009Xx5YcExUVFapt6uvrteH44x//qH5Pyj5v3jy1v2Ptz8F+O9a59eqrr4aOz5kzZ2r33XffiI/BWMf7aMv4yCOPhNaXdol1HZHzT9aV81f2eUlJiXbqqaeqY2awY1BS3OU7UgY5DuU4WLJkifbEE08MWy8yEIP8M1qhRAghhBCSaLBPDyGEEEJSAooeQgghhKQEFD2EEEIISQkoegghhBCSElD0EEIIISQloOghhBBCSEqQEoMTyvDoMoKtjGA5VcPpE0IIIWR0yKg6Ml2KDC46HnPPpYToEcFTXl4+1cUghBBCyBiQqT1k5O9DJSVEjz6hnuy0rKyscd/+P/KOGdX6y1s/maOFEEIIIbHp7OxUQYvwiXEPhZQQPbqlJYJnIkSPw2Ac1fqv5w+c52iFb2yzARNCCCHJjmGcuqakhOhJBJ41z425nGKIEEIIGR8oeuIciiFCCCFkfKDoSSIxRCFECCGEDA5FzwRFYqYCRoUIIYSQwaHoSQEohgghhBCKnpSGFhkhhJBUgqKHRMCoECGEkGSFooeMCIohQgghiQ5FDzkkaJERQghJFCh6kiRzK55gVIgQQkg8QtFDJg2KIUIIIVMJRQ+ZcmiREUIImQwoekhcwqgQIYSQ8YaihyQUFEOEEELGCkXPGGEn5viCYogQQshwUPSQpIb9hQghhOhQ9JCUg1EhQghJTSh6CAlCMUQIIckNRQ8hw0CLjBBCkgOKHkLGAKNChBCSeFD0jAFmbpHBoBgihJD4haKHkEmAFhkhhEw9FD2ETBGMChFCyORC0UNInEExRAghEwNFDyEJAi0yQgg5NCh6CElgGBUihJCRQ9EzSpi5RRIBiiFCCBkIRQ8hKQQtMkJIKkPRQ0iKw6gQISRVoOghhMSEYogQkmxQ9BBCRgUtMkJIokLRMwrYiZmQ2DAqRAhJBCh6CCETBsUQISSeoOghhEw6tMgIIVMBRQ8hJC5gVIgQMtGkTeTG165di+OOOw6ZmZkoKirCypUrsXt35AXM7XbjiiuuQH5+PjIyMnDmmWeisbExYp0DBw5gxYoVcDgcajvXXXcd+vr6JrLohJA4EkOxXoQQElei59VXX1WC5o033sCLL74In8+H008/HT09PaF1fvCDH+D//u//8OSTT6r16+rq8NWvfjX0eX9/vxI8Xq8XmzZtwsMPP4yHHnoIN99880QWnRAS51AIEUJGi0HTNA2TRHNzs4rUiLhZtmwZOjo6UFhYiEcffRRf+9rX1Dq7du3C/PnzsXnzZpxwwgl4/vnn8YUvfEGJoeLiYrXOfffdh+uvv15tz2KxDPu7nZ2dyM7OVr+XlZU1prLzgkpI4kKLjJDEZDzu35MW6YlGCi3k5eWpv++++66K/px22mmhdebNm4eKigolegT5u2jRopDgEZYvX652xPbt2yez+ISQBIUWGSFkUjsy+/1+XHPNNTjppJOwcOFCtayhoUFFanJyciLWFYEjn+nrhAse/XP9s1h4PB710hGBRAgh0TCLjJDUYtJEj/Tt2bZtGzZu3DjhvyUdqNesWTPhv0MIST6YRUZI8jIpoufKK6/EM888g9deew3Tp08PLS8pKVEdlNvb2yOiPZK9JZ/p67z11lsR29Ozu/R1olm9ejVWrVoVEekpLy8f93oRQlIHiiFCEp8J7dMjfaRF8Dz11FPYsGEDqqqqIj5fvHgxzGYz1q9fH1omKe2Sor506VL1Xv5u3boVTU1NoXUkE0w6NB1++OExf9dqtarPw1+EEDIRsK8QIYmDaaItLcnM+vvf/67G6tH74EhPbLvdrv5edNFFKiojnZtFnFx11VVK6EjmliAp7iJuvvOd7+D2229X27jpppvUtkXcTAa8iBFCRgOjQoSkYMq6wWCIufzBBx/E+eefHxqc8Nprr8Wf//xn1flYMrN+//vfR1hX1dXVuPzyy/HKK68gPT0d5513Hm699VaYTKZJSXmj6CGETCQUQ4RMTsr6pI7TM1VQ9BBCEg0KIUIw7qKHc28RQkgcQouMkPGHoocQQhIIiiFCxg5FzzDQ2iKEJAIcaJGQ4aHoIYSQJIVRIUIioeghhJAUg2KIpCoUPYQQQhS0yEiyQ9FDCCFkUBgVIskERQ8hhJBRQzFEEhGKniFg5hYhhIwOWmQknqHoIYQQMqEwKkTiBYoeQgghUwLFEJlsKHoIIYTEFbTIyERB0UMIISTuYVSIjAcUPYQQQhIWiiEyGih6BoGZW4QQkrjQIiOxoOghhBCSEjAqRCh6CCGEpDQUQ6kDRQ8hhBASA1pkyQdFDyGEEDJCGBVKbCh6CCGEkEOEYigxoOiJATO3CCGEjAe0yOILih5CCCFkEmFUaOqg6CGEEELiAIqhiYeihxBCCIljaJGNHxQ9hBBCSILBqNDYoOiJgp2YCSGEJCoUQ0ND0UMIIYQkObTIAlD0EJKE9GkmdGo5yDB0wmLwTnVxCCFxyLMpGBWi6CHo9megH0ZkGLpgNPinujhkHKjRZuBx/+XoQhZy0YQyHMA07EeZ4QAKDI3INHTAaOif6mISQuKQZ5NYDFH0ELzkX4l/YxmM8KEENSjDfnWDLDHUINfQApvBBYNhqktJRkOLVgLrrOnodVShtcuN2i4vXnW6kab1w6q5kY1WFKIe07AP07EPxYYa5BmaYTf0Is2gTXXxCSFxyLNJYJFR9KQ4Ps2MJpQjc1EV2vtzsK93EXZ1+uBu7YVJ88GhdSEPTUoMyQ1ymqEahYZ6ZBvaYDb4prr4ZBCaUYp+zYDp86w40LgbZp8JC2cvQJrXAlerBz1Nbhxs82Jvtxe9Tpdqa7vWg1z1zQMoQzXKDNUoNtSqtrYaPFNdJUJIHPJsgkWFKHpSPHPLhXR0IxOmrAyUzrShoXoX5iw4EhaDBe4WN9oP9KKjpRJb2o7C+zDC4+yGRfMgA+0oQIOKCEmkoMhQo2wTWmRTj6YBzVoZDJk5gMMKe6YLDgPQ1fpG4HMzgGIjTNlmzF10FFobDUjzmdHr9KK3zYWdncdhS7cH7tYeWDQvHOhUbS1iaHrIImtQ/YVMhr6pri4hJA55Nk7FEEVPitOu5cENO7LMfhSUauhqc6HxYODmKPjzDDBmmGAttqJi/kIYfWVwOz3oanChqWUuqruWwuV0KdvEprmUbRJtkeUYnMo2oUU2eUK2E7kwGPxw9/sG7Hd5bzb3q1fD/k/a2pAJmK0m+HMsmH34ETD1T4OrxY2Oejfamg9DbacXr4VZZFloQxHqAhFA1dYHlR3qMPTQIiOExKVFRtGT4ji1Ivhggc+WiR1vvwd7euTnaWkarDaferU1BCMFGuAvTIM504z+XiuqzlgAY58ZvS1e9LS4sK9joEWWixaUqEjBPmWbFNEimzC6tQz0IAPWzExk5Wvobh/Z9wxpgMXWp14tdWHCN1+ErxlmlzVkkbnbPOhudONg6wLs6fEq4WtUbR2wyEpwUFlk0wz7UWyoQ7ahlRYZIWTKo0IUPSmO9ODInFkIR7of3drIrAqJFBiNfhjTPbCle9DTsVkt12xAmrJNLJiz8EhYELDIOmp60d5Uge3uJfjA5YXb2Q1rlEVWimqUBjOLaJEdGm0ohBdWZFmAD9/bgoysQ9temlGDzeFVr5BFZgIMxWmqrcUia2s2wOAJWGQuscg6fNjao1tkPjjQgQI0Bi2yfaqtC2mREUKGEUO92vhmmVL0pDASsWnRSmHIyobfaoPRfWgHl7JNLP0wW1xoPBAWKcgxwOgwqUjBLN0ia/Wgu96FJudcHOhcqjrThltkxarjdLWyTmiRjQ6nVoKMmcXwp6fD0jsxkTRpB5PZD5PZHdMi68+xYE7QIhPh29ngRlvTYajtOg6vtXxikWWiHcWoVe0slmgpLTJCyARC0ZPCeGBDGwpggIaCIqC9c2J+J5ZFJvSLRZZlRn+PFYeHLDIPelvc2N9+BHZ3eVWkwKT1BS0yJ0pQHbTIDqBI2Sa0yKJp0krh1wCfyQaTeXKjKGKRmW196hVhkeUZYEoPWmSfD1hknjYPuhrdqGmbj73dvgFZZEWoVW0tFpm0dQ4tMkJIqoie3/3ud1i3bh0aGhpw5JFH4p577sHxxx8/bttPxcytHi1T9f2wZGSiyzf5o/Yqi8zhgc0RaZEZdIts0ZGwaIFIQUdNT9AiOx4f9Oq2iQeZyjaR8Wb2K0EUyCxqUrZJKlpk/ZpR9Z5Ky85CbgHgqouPaIlYZFaHV72iLTJjtgVpvTbMOW2+sshcYpG1u7C73YftPR64WnthDWaR5aNBDbQoWWSlavgEWmSEkCQTPY8//jhWrVqF++67D0uWLMGvfvUrLF++HLt370ZRUdFUFy9haUc+vLAh3axh7wdbkZEz1SWKssiqY1lkFsyavwimvmlwOQMWWXPrHBzoCFhkRs0Pq9YbtMjENpEsMkmprwsNvpfMFllgCIJsGA0G9PTF9/QT4RZZeqYbnp6A8EVYFtmc+WKRWeAR4dvgRnvTLNQPYpGVBodPEDtU2poWGSGJz/LWfwPZ2akleu666y5cfPHFuOCCC9R7ET/PPvssHnjgAdxwww1TXbyExakVI31WMbT0TJhd8X2DHMoiM2WZYe6xYsHnDkeazxJpkXVLx+lAFpld61ZZZNJpWokhlUVWhyxDe9LMT9WpZSvhk5GVgew8oKMJCcdwFpkpaJGJGHK1DG6R5aBF9Q0LWGTS1rW0yAhJceJe9Hi9Xrz77rtYvXp1aFlaWhpOO+00bN4cfDKMwuPxqJdOZ+cEdVZJcJowDX5/GtIzNbT3J+Y8TOEWWXf7GwMtsiOOhMUfiBS0i0XWNhc7uiItsgx0qCkZJKVedagNWWSSRdafgEMQmNUQBLvffX/AEASJzHAWmTHcImsNZJGJRbZNZZGFW2RNoXGkZPgEfS4yWmSEJD9xL3paWlrQ39+P4uLiiOXyfteuXTG/s3btWqxZs2aSSpiY+DUDmrVA3w9NMreMydP/JcIi2z/QIjO5zZg57wiY+6epqFB3gwvNTrHIAlMypGl+2JRF1hbqTCs3SZmSId4tshaUIXNmEewOP3pGOARBIjOoRZYRwyJzBgZa7GiehfrOxWEWmfQNC7S1LoYki4wWGSHJR9yLnrEgUSHpAxQe6SkvLx90/VTsxOyGA13IURd0tz85rJ0xW2QFaTBlmmDutQ2wyKrbF+FDlUWmW2S9atbygEUWGHxPn4ssHiwyydxCVjb6zDYYTckveoa0yKx96hVtkclAi6ZeKxauWABTX8Aik4EWa9vm42NlkUlb9ymLLDDCuD79RkD40iIjJHGJe9FTUFAAo9GIxsbGiOXyvqSkJOZ3rFarepHB6dRy0It02DMykZkPdLQiZQlYZIHB9wZaZGbMOeIoWLXgXGQHe9DePjtgkQUH3zOHZZF9Mur0wbCBFifHIvNqFjUwoQxBkF8MdPVOys8mnkVm96pXl/MTiwzRFpk3aJG1uvBh+zHY3uuByxmwyOxqLjKxyALjSOkDLdIiIyT+iXvRY7FYsHjxYqxfvx4rV65Uy/x+v3p/5ZVXTnXxEn7UXrMtEx/++wM4Mqe6RPFqkfUPapHNmhcYfE+iQj2NLrS0zMHBDi9eHdQik1nLaybMIuvRMtTkscaMLPRMwRAEyWmRGeHPsWL2/CNg9gdHGK93oaN5ZkyLrDBiLrIa5BuaaJERMkZkGorx7pMb96JHEKvqvPPOw7HHHqvG5pGU9Z6enlA2Fxk90p9Hpp+wSd8PDu43JousNdwiy0+DKSMw6vSCWTLQogUuGXW6MWiRBbPIjMo26UaemgDkQDCLbHwssg7kwwMHckx+7N26DVm541RppLrw7Y1pkZl7rTjiCwuUHeqWLLImN+pa52NfmEUmwjdHDaoZaZFJW9sM7imtHyGpSEKInrPOOgvNzc24+eab1eCERx11FF544YUBnZvJyGnWStGvpcFrkr4fiZWhlDAWmXkIi6xjDnZ06hZZN8yaV1lk+Wp+qsB4M6WGGhQqi0wGWhy+jVpkCILDiuCzZ8FiZKRnMiyyjpYwi6woMBdZX0/AIkvzmtGrW2QdxwQGWnT2qoxBB7qDc5EFRhiXGepF+GbRIiNkQkkI0SOIlUU7a3zo00zq2dOYnYm8QqC3hqH3ybbITA4j+nMtIYtMRYXqXXC2zEFNxxK86nSH5iLLQltwLrL9yiIrCZufKtwik9hRvz8NOZl+dHfyxjlVFpkjY3CLzKJZ4GrWLbKqCIvMpiyydhQG5yKjRUZICoueiSIVM7d6Eej7YXBkwxXno/Ymq0VmsfWp18gtsoUxLbISHAx2nK5GnVYJQ0YO/HYb0nqSZwiCZLbILL1WHKlbZM7AQIt1rfMiLDKr5lKDapaE2aH6XGS0yAgZHSkvelKRdi1Ppaxnmf3IygOcDVNdIjKkRVYSsMjmBi0yV4sbDbVp6Gl3YWeHD1uCWWR9MKPSY0S62Ru34wiRISwyY2yLzNXmRa/KIjsa23u8QYtMBlrsCllkgbGFJIuMFhlJnk7MEwFFTwrSqhWGRu3d8fZ7STVqb1JGCsz96tUQZpEhywCzTWwTCw4LWmT2DGDHtjfhrN9L0ZNEFpmWLtEiI/zZVsw+/Ag1wrhYZJ0xLLJAFll7cKDFT8QQLTJCAlD0pCAywkjWzAI4Mvzo7uYTYaJbZE7dImsGHBSwyW2R1Y7FIhMxJINqylxkATtUzxjMNThhhZsimaQMFD0phqZJlk+JGrW338LMLUKS2SKb9x+BgRb1LLKP2o/GjiiLTDIGJSqkXoZqJYYyDZ0wcygLkoRQ9KQYHtjQjgJ09VrRvr0T+eVLYM+3ot/oxYHd22Cxe2C29KlIAiEksS0yV/cnFpnJYoRZLLKFR8DSJ1EhNzpqXehsrkLDIBaZjCUVGD7hgBpUM93QTYuMJDQpLXpSMXPLiH7MxQfo/9Cknu8+3pIDLyzwIw3p+YUwZpuRUWRBToUD1jwbfAYvPt7+ASw2L0zmfobBCUl0i+zg4BaZTMza2xwYaLHeORf7uvtUVCiQUh9pkUlUSGWRwamyyHhtIImAQdPE8EhuZBjr7OxsdHR0ICsrK6VFTzg+zazm4GrSylCrVaIeFahDJVpRhF5kqmwgW54DlkwL7DkWOPLtsOdboFl8qP1oB6zpblhsPqSl+XnBIyRJkDtCf18avB4LPD02lM36ZC4yySJzt3vh0y0yfGKRhQ+fQIuMjFfm1mD377GS0pGeVEcuSPmGZuSjGfPxQeiC59bsaNMK0KhNQ23rDNS2VqGpugyNyIcLDvhhhD0/H8ZMCyz5VmSU2GDPs6LP6MXBD2mREZLUFpkMtLjgCFj6LfA43WivEYtsBho6FmNjcFBNWmQkXqHoIQMueHaDC3YcRBkO4mgEQuEyZUW3lolWrQgNWjlqnVWodVageX8ZWpAND6zQkAZHuEVW7oA1X7fItsBi89AiIySJLbKj5i6AMYZFJllkRq0fdjUX2ScWmQghmYuMFhmZLGhvkXGzyOpFDGHGAIvMmueANWiR2fPtcERZZGaLD0YTLTJCks8iMyuLbNphhwMes7LGeloiLTIzvEhHF/LQFIoK0SJLbVZMoL2VsqKHgmfiiLTIylCLKvWSS5pkjrlhRz9MSgDZMi3IyLcis8QGW4EV/Wm0yAhJ5mtDn88Ij9uKGQuOgNUfnIS3xoXuZg96OvrQLRYZ+mGFPhdZnUqn1ydmlYEWaZElNyvYp4cktkX2Zsgi69EylEVWr1Wg1lmJWucMZZE1h1lk6flFMGablEWWNd0Be0HAItu7fQustMgISUqLLC3TBEuPDUfNC8w7p1tkDc652B9lkWWrucjk+nIAZYbAxKw5kLnIXLw2kCFhpIcklEVmy7bAURCwyPwmH+r27oDF4YbFSouMkGS1yLy9kkV2OAweM1xBi8zT7oV3GIuswNCILEM7LbIEnnOrk5EeklpZZHloxHSVQVbfWo4GlKMpwiLLR1qGBdYCK9IliyzXin6TFwdloEUHLTJCEj+LzANHhge9XcEsMjtgKjGiP5hFpibhbXajvdaFrqYZaOw4BhudLqRpfpVFlqEssvrADPWq83QN8g2Nai4yo8E/1dUkkwxFD4lzi0wmTqyNsMh6tXQ4teJAVMg5I2CRVUdmkekWWXqRFdnT7LAX2uAxeLGPFhkhSWORNQ/IIou0yFzBucgaW+Zif48PrhYXjFofbJpLZZGJRVaKg5hm2Idi1CDXQIss2aHoIQmFPJllGrqQiS7MwJ4YFlmpSqmvcVah3lmBlo+LcQAZ8MGiBlq0ZlbCmC2RITscuRb4LbTICEkGJKJrtfnUq705OBeZAdAK0mDMlJR6G2acfjjSfGb0OgMW2Z6Oo7G9ywt3q1hkPji0gEWmd5wuNRxUWWS0yJKHlOzTw/48qcEAi0yrQj0CFlkHCuDSLbICO+zpZtjzbcgstsEhWWQmLw7s2h5MqadFRkiyZpHNlIEWgxaZmousyYPejj70OF0wwK+yyDLQobLIAhZZtRpfiBZZYvbpoeghKUekRTZdpdPXYQaaUYou5CiLTEadzsi3IV1lkVmRE7TI3AYv9tMiIyQp8fsN8HlN8LqsKJ+zUFlk7lYPOhvc6G2R0al9cDldag5DGwIDLcqo0yKEpL+QDLSYa5CBFmmRjYfgEdiRmZBxt8heibDIWrRi1GmVIYus9eMiHERmyCKzhFlk9tzgQIt7ZKBFDyxWLy0yQpLJIkOkRVZ1xuFqLjKxyHqdLnzc7sXOLi88rb0waZEWmR4VkolZaZHFBxQ9hMTIIpuLbREWWTvy0KBNR31rhZqPrBHlaFYWmQP9ai6yApgyTEC+DY4iGxyFtMgIScYssp6OT7LIjMVGmLOtmLvgCJg1C9xONzoO9qKjqRLvdRyNf4lFFsoiC7fI9qv+QnnBgRZpkU0eFD2EjDCLrDRmFllgLjKJCtU5K1UW2f4Ii6wQpmyTEkE50+2w5dvgMdIiIySZssgaY2SR2VxWzJ6zEKb+YBZZgwuNrXOxv1PGFpKUen0uMieKlBjap8RQwCJroUU2QbBPDyHjiFhkXVo2mrUSZZHJQIv1qFSXNRlo0QuZf8wBc7oFthwLHHl22PMCFlnd3p0qKkSLjJAkHGixPw0+fS6y2Z9YZCKAZMBFj26RSRYZPrHIAqNOV6eERbZiEvr0pJzoeT3/uKkuDkkxAhaZDe3IR6M2TU3BUYMqZZG1By2yPpiQLnORZciAizZkBi2yPhloUSwyNdCiD2nGpD9dCUm5LDKv24KZC45UFpmn1Y32A2KRedHb4UOP0z0gi0yEkD5Lfa6hGRmGrqSwyFZQ9IwPFD0kHvFrhuBcZIUhIVSPCjRhWkQWWWaBDfYsMzILLcois+bZ4DXRIiMkVbLITH4LXC0edDe40C3Roa7A9Btp8MMOmYusFcUqi2wfxGCXucgSLYtsRQzBI1D0jAGKHpJ4FlkWmrXSMIusAk4Ux7TI7Hl2OIIWWe3enbDRIiMk6ZA7td+fBq870iJzt3rR3eyCp8MDT1cf3K09AywymY9sWpxbZCsoesYPih6STBaZiKFabYYaX0gGWmxTc5E5VFRIxJA1wwxHnlUNtGiXLDKjzEVGi4yQlLDIYIHHGbTIWrzobfOpa4PH2QULvMG5yD6xyIoMNWpi1qm2yCh6xhGKHpIqFpnMTi+RIbHIupEDt55FVmCDI2iRZZUGBlpUFtm2LbDaaZERkswWWcW8wECLrgEWmQtp6FcWWRbaUIKaoEVWHbTIJi+LjKJnHNF32hPGWXAYjFNdHEImnD7NhE6VRVaqxJCeRdYSsshk2o10ZZFZg1lkYpH1m32o/5gWGSFJb5H1WjHtsAUw9JnhbglaZJ0eeDp9ai6ycItMJmOVqJBYZDIXWbahbdwtMoqecYSih5AAyiLT8tCklQUtshloQIWyyDywqywyR5RFZsu3wm/24sDu7bDRIiMk6S0yi8ECd4tukXnQ29YXtMi6xTwLWmQNqtN0YPqNQ7fIKHrGEYoeQoa2yPSBFsMtsmaUoQu5yiLTYER6LIsszYv9O2iREZL0Ftn8hTD6AnORddUHLDJ3Z2AqDrHIbHCpLDKxyAJTcOxTFlmOwQm7oXfIa8Nggkeg6BkDFD2EjLdFlgEvrKEsMmu2BQ4ZZyg4FxktMkKS2yLzecxw9wQsMmOfGb0tXvQ0u+COYZHlohklOKgssrLgXGThFtlkih5OQ0EIiYnJ0Ic8gxN5cIbmItMtsg4tVw20WOucoV5ikTmRr7LIAhZZPvoyzLDkWeEIs8iqd2+HnRYZIQmLwQAYjX4YHR5ld4fmIrMBaSVGmHIsmLPoSFi0oEVWIwMtVmC7+wR84PLC7ewOzUVWgHp8Fk9hxSSWn6KHEDIqbAY3bIZ6FKMeR+CdARaZTMxa66xSYkjmIqsOWmR+mJBRUIj+LLNKpc8ulZR6G7wGL/bvlIEWvTCZ+2BIm+oaEkLGPheZC43VYXOR5RhgdJhgdlkwa/4imPrKAnOR1bvQ5JyDoi9mTW45aW8RQibaImvRSoIWWZXqMzSURWbPtcAftMisDrcSQ7TICEku+oNzkZksXrzx/LJB16O9RQhJSItsDraHlns0q8oiE4tMZqeXWeoDFlke3EgPWWT9GWYgzwp7kQ32AlpkhCQLxqBFtvH/TpnU36XoIYRMOlaDB8WDWGSBgRbLlT1W55yBpuppOIDc0Fxk6VEWma3QBh8tMkLIVIme/fv34+c//zk2bNiAhoYGlJWV4dvf/jZuvPFGWCyW0HpbtmzBFVdcgbfffhuFhYW46qqr8KMf/ShiW08++SR+8pOfqG3Onj0bt912Gz7/+c9PRLEJIVNImkFDhqEbGehGBfYBeC1kkXWpLLKgRdYyA3UtM9DycTHqkAlPcC4yk2MG0nLMMOUFs8istMgIIZMgenbt2gW/348//OEPOOyww7Bt2zZcfPHF6OnpwR133BHy6U4//XScdtppuO+++7B161ZceOGFyMnJwSWXXKLW2bRpE84++2ysXbsWX/jCF/Doo49i5cqV+Pe//42FCxdORNEJIXFokcmM0bkjsMgaD8pcZPmoi7LItDwb7EVWZZH1m7w48CEtMkJSkUnryLxu3Trce++9+Pjjj9V7+b9EfiQSpEd/brjhBjz99NNKNAlnnXWWEkrPPPNMaDsnnHACjjrqKCWURgo7MhOSGgywyDADdcG5yLqQozpO98OkBlq0Z1qQWWRRFpk134Y+Iy0yQiab4fr0JGxHZilwXl5e6P3mzZuxbNmyCLtr+fLlyr5qa2tDbm6uWmfVqlUR25F1RBgNhcfjUa/wnUYISX6Gs8hatGLUa5WoCVpkzn0y33S2sshseZJFFrDIjDl22PMDFlmdDLRIi4yQpGBSRM+ePXtwzz33hKwtQSI8VVVVEesVFxeHPhPRI3/1ZeHryPKhEDtszZo141oHQkhyWGSzsWOARabmImudgdpWscimoQ2FqFcWmfkTiyzXBnuxpNhb0Wfy4uBH22G3e2C20iIjZCxMdubWqEWP2E8SiRmKnTt3Yt68eaH3tbW1OOOMM/D1r39d9euZDFavXh0RIZJIT3l5+aT8NiEkMbPIFuHdARaZDLQoGWQy2GKjyiLLgQc2lUXmKCiEOdMCe5HMRSajTgcssn07tsAmFpmFFhkh8caoRM+1116L888/f8h1Zs6cGfp/XV0dPvOZz+DEE0/E/fffH7FeSUkJGhsbI5bp7+WzodbRPx8Mq9WqXtEsb/13yBN81jx3yG0QQlKTgRbZ6zEssgrUtFShvqUCLfskiywbXphhy0uHKX0GjLTICEl80SNp5fIaCRLhEcGzePFiPPjgg0hLi3zkWbp0qerI7PP5YDab1bIXX3wRc+fOVdaWvs769etxzTXXhL4n68jyQ2WwCc4ohgghh2KR1bXOQMPB6QMtsnTJIrPCLnOR5VrRbwlYZDa7BxZaZIQkbvaWCJ5Pf/rTqKysxMMPPwyj8ZOMKT1KIx2bReBI2vr111+v0tolZf3uu++OSFk/5ZRTcOutt2LFihV47LHH8Mtf/nLUKevj0fubYogQMlIGWGSYoabgaEIZOpVFZg9YZPk2OLIsyCgMWGT2AlpkJHXYOII+PeOdvTUhouehhx7CBRdcEPOz8J8LH5ywoKBADU4oAih6cMKbbropNDjh7bffPurBCcd7p+lQCBFCRsMAiwwzUB+ai0wsskAWmSndClu2GbZcOxz5gbnI8oo07N36Piw2H0ymflpkJOHZmCyiJ96YKNEzGBRDhJDRIBZZh5arBlqs1WRsoUo0QAZaLIQrZJHZYUm3wJFnQbrMRZZnhZ8WGUnyzK3ORB2nJ5VgfyFCyGizyIoMDShCw4AssjatAI3adDXitGSRNR8oQ42aiyyYRZZfCHOWBcZwi8zgxb5dgYEWzbTICAlB0TPFYohCiBAyXBZZOfbjWGxUy/s1Y8giq9MqUeusRJ1TsshKUYcs+JRFlg5zxgykZZlhyg1kkRVPAzwGL/ZueR8WOy0ykppQ9EwxjAoRQkaD0dCPHEMrctCKw7AzpkXW0FqOmtYq1KMCbShAPTKwW7fIMqpgybXAGrTIJIushhYZSREoeuIUiiFCyERYZDI5a1P1NNQgb3CLLN8GX5oX+2mRkSSDHZmTAAohQshoGGCRoVyl1begFL3IVBOzKoss3QpLthn2XDtseRaUTqdFRiZ3+gl2ZCYDYFSIEDIeFplXs6iBFiMssoMBi8yFDHwEE+z56bBGW2RmL2r2BCwys7UPRqN/SutHyGBQ9CQxFEOEkNFgMXhHZJHVOyvVXGQDLDKJChVYAxZZng0+Iy0yEl9Q9KQgzCIjhIxnFlm9Vo4alUU2A86PJYssE76QRTYDadmBLDJrrgWl5YDb4MXHtMjIFEDRQxSMChFCxtMia9ZKUddaqSyyhoPlaFUDLeoWmUNZZOYcC6zFQYvM5EXNXlpkZGKh6CFDQjFECBmrRbYA74UsMpfmUBZZg1aOWucMlUXWWD0dtciBG46QRWbKtsBeYEFmqXSetqI/zCIzWfqRlpb0uTdkAmH2Fhk3KIQIIaNBLLJuLQvNyiKbjhpUqSwyJ0rQrQZa/CSLzCpzkWXbUTLdD1uBTWWR0SJL7swtgdlbJG5hVIgQMlqLLNvQhmy04TDsGmCRtWglqG2dEWGRyUCLfTEsMluuFX7JIqNFRoaAoodMOBRDhJCxWmSH4321TDwJschatUJlkYk9JjaZDLRYi9ygRZYGR34BTNlWZZFllNpVf6E+oxfVu7aoGeoli4wWWepC0UOmDGaREUJGithXDkMvHKjGdFSLURJhkbVoRahTWWSBlHrnxyWoQ3ZEFpkx24zMrD7klKdHWmQ2H0xmWmSpAEUPiSsYFSKEjNUim4XdERaZzEUmWWRikdW2VqL+YCUaUYh9SEc/zLANZpHt2QGbQywyHy2yJIOihyQEFEOEkNFaZIUGkTiNMS0yGWhRLDKJDEVbZHZlkVlgyw9aZPkBi+wALbKEh9lbJOmgECKEjIZPLLJC1GkVKousAZVoUVlk2RFzkVkzzMjJ7UP29IBF5k2jRTZRmVsCs7cIGQZGhQghY7fIPoywyDq1HDRpZahrrQjMRQaxyAqwX2WRRVpk5gIbHAVikflQ89F22NJpkcUbFD0kZaAYIoSM1iIrMDShAE0xLTIRQ7XOqqBFVoZ6NReZHf1qoMWgRZYXtMgKghbZzq2w2ANzkdEim3woekjKwywyQshYs8iOweYwiywzmEVWgXpnFeqcFWj+uBQN4RaZoxJGmw+ZRZYoi+wDWGTUaVpkEwpFDyExYFSIEDJ6i6wd2WgfkUXWtEe3yExKDFkzZwQtMjscBRb0m32opUU27rAjMyHjAMUQIWSk6BaZzEXWqEnmWBVqMQONmIaOCIvMDluWBRn5gbnIVBaZKTEtso1j6MQssCMzIXEILTJCyOgtsgOYhgNhFlla0CIrVhZZQ3DU6eZ9pWhCNjxikeVnwGyPYZHJQItbaZENB0UPIRMELTJCyGgwGvzINnQgGx0RFplPM6uBFpu00sDYQiqlviJokWUGssjyHMoiM8WwyKwOj0qpN9Iio+ghZLKhGCKEjAazwReWRfZBhEXWruWjQZuO2tYq1LVWoqF6usoic8MOfzCLzJj1yUCLtjwr+s1ikW2DRSZmTSCLbDyg6CEkTqBFRggZvUXWizIcjGmRNWrlyh6TV8u+EjQhJ2SRpaEQxlwTMgqsyKlwwJpvgw9efLw9uS0yih5C4hhGhQgh42mRNctcZNJXSCwyZzma9xSi+o0wiyyrEsYsK+yFdhUd0sQi27M9MBeZzYe0NH9CiyGKHkISEIohQshYLbL5YRaZW7OrLDKxyMQeE5usAdPRgFy41FxkRjUXmTHTAmuBBRnFgSyyfhlocffILLKxZm5NBBQ9hCQRtMgIISPFYADsBhfsODgCi6wSLfsliywHXlhgzc8MWmRmZBRYAhZZng0+Q3xbZBQ9hCQ5jAoRQibCIqtHZWDU6T1FkRZZZiWM2QGLbOHJFsQTHJyQEBIBxRAhZKQMsMgQ7C+EaehEHj6Nv+M+7WcYKxyckBAyodAiI4SMl0VmgwvA2EXPeEPRQwgZFlpkhJCxWGTxBkUPIWTMUAwRQsZyjZgqKHoIIeMOLTJCSDxC0UMImRQYFSKETDUUPYSQKYViiBAyWaRN9A94PB4cddRRMBgMeP/99yM+27JlC04++WTYbDaUl5fj9ttvH/D9J598EvPmzVPrLFq0CM8999xEF5kQEidiKPpFCCFxLXp+9KMfoaysLGbu/emnn47Kykq8++67WLduHX7605/i/vvvD62zadMmnH322bjooovw3nvvYeXKleq1bdu2iS42ISRBhBDFECHxyYo4PDcndHDC559/HqtWrcJf//pXLFiwQAkXifoI9957L2688UY0NDTAYgmM2HjDDTfg6aefxq5du9T7s846Cz09PXjmmWdC2zzhhBPUNu67774Rl4ODExKSmtAiIySxRU9nogxO2NjYiIsvvliJGIfDMeDzzZs3Y9myZSHBIyxfvhy33XYb2trakJubq9YR0RSOrCPbHM5Sk1f4TiOEpB7MIiOETLjokeDR+eefj8suuwzHHnss9u/fP2AdifBUVVVFLCsuLg59JqJH/urLwteR5UOxdu1arFmzZlzqQghJLthxmpDUZVSiR+wnicQMxc6dO/HPf/4TXV1dWL16NaYC+d3wCJFEeqSjNCGEDAbFECHJz6hEz7XXXqsiOEMxc+ZMbNiwQVlTVqs14jOJ+pxzzjl4+OGHUVJSoiywcPT38pn+N9Y6+ueDIb8b/duEEDIWaJERkqKip7CwUL2G4ze/+Q1+8YtfhN7X1dWpvjiPP/44lixZopYtXbpUdWT2+Xwwm81q2Ysvvoi5c+cqa0tfZ/369bjmmmtC25J1ZDkhhEwVjAoRkniZWxPWp6eioiLifUZGhvo7a9YsTJ8+Xf3/W9/6lup3I+no119/vUpD//Wvf42777479L2rr74ap5xyCu68806sWLECjz32GN55552ItHZCCIkXKIYIiW+mbERmSUGTvj9XXHEFFi9ejIKCAtx888245JJLQuuceOKJePTRR3HTTTfhxz/+MWbPnq0ytxYuXDhVxSaEkFFDi4yQFBinJ17gOD2EkESBYogkAyvGyd5KmHF6CCGEjB5aZIRMHBQ9hBCSANAiI+TQoeghhJAEhVEhEo+siNPMLYGihxBCkgyKIUJiQ9FDCCEpAi0ykupQ9BBCSArDqBBJJSh6CCGEDIBiiCQjFD2EEEJGDC0ykqidmAWKHkIIIYcEo0IkUaDoIYQQMiFQDJF4g6KHEELIpEKLjEwVFD2EEEKmHEaFyGRA0UMIISRuoRgi4wlFDyGEkISDFln8sSLOM7cEih5CCCFJAaNCZDgoegghhCQ1FENEh6KHEEJISkIxlHpQ9BBCCCFhsL9Q8kLRQwghhAwDo0LJAUUPIYQQMkYohhInc0ug6CGEEELGGVpk8QlFDyGEEDIJMCo09VD0EEIIIVMIxdDkQdFDCCGExCG0yMYfih5CCCEkQYjHqNCKBOnELFD0EEIIIQlOPIqheISihxBCCElSaJFFQtFDCCGEpBArUjgqlBKiR9M09bezs3Oqi0IIIYTEJSc73465/B95xwz5vYm8t+rb1u/jh0pKiJ6uri71t7y8fKqLQgghhCQX2dmTch/PHoffMWjjJZ/iGL/fj7q6OmRmZsJgMAypKEUYHTx4EFlZWUhmUqmuAuub3KRSfVOprgLrm9r11TRNCZ6ysjKkpaUd8u+lRKRHdtT06dNHvL7s+FQ42FKtrgLrm9ykUn1Tqa4C65u69c0ex0jSocsmQgghhJAEgKKHEEIIISkBRU8YVqsVt9xyi/qb7KRSXQXWN7lJpfqmUl0F1je5sU5yfVOiIzMhhBBCCCM9hBBCCEkJKHoIIYQQkhJQ9BBCCCEkJaDoIYQQQkhKkHKiZ//+/bjoootQVVUFu92OWbNmqZ7jXq83Yh0ZuTn69cYbb0Rs68knn8S8efNgs9mwaNEiPPfcc0gUfve732HGjBmq7EuWLMFbb72FRGPt2rU47rjj1EjbRUVFWLlyJXbvjpxI79Of/vSAdrzssssi1jlw4ABWrFgBh8OhtnPdddehr68P8cZPf/rTAXWR40/H7XbjiiuuQH5+PjIyMnDmmWeisbExIesqyPEZ6zyUOiZ627722mv44he/qEaZlXI//fTTEZ9LfsnNN9+M0tJSdZ067bTT8NFHH0Ws09rainPOOUcN6JaTk6Oua93d3RHrbNmyBSeffLI6z2XU29tvvx3xVl+fz4frr79eXUPT09PVOueee64aRX+44+HWW29NuPoK559//oC6nHHGGUnZvkKs81he69atw6S3r5ZiPP/889r555+v/eMf/9D27t2r/f3vf9eKioq0a6+9NrTOvn37JKNNe+mll7T6+vrQy+v1htb517/+pRmNRu3222/XduzYod10002a2WzWtm7dqsU7jz32mGaxWLQHHnhA2759u3bxxRdrOTk5WmNjo5ZILF++XHvwwQe1bdu2ae+//772+c9/XquoqNC6u7tD65xyyimqfuHt2NHREfq8r69PW7hwoXbaaadp7733nvbcc89pBQUF2urVq7V445ZbbtEWLFgQUZfm5ubQ55dddplWXl6urV+/XnvnnXe0E044QTvxxBMTsq5CU1NTRF1ffPFFdV6+/PLLCd+2UpYbb7xR+9vf/qbq9NRTT0V8fuutt2rZ2dna008/rX3wwQfal770Ja2qqkpzuVyhdc444wztyCOP1N544w3t9ddf1w477DDt7LPPDn0u+6K4uFg755xz1Dny5z//WbPb7dof/vAHLZ7q297ertro8ccf13bt2qVt3rxZO/7447XFixdHbKOyslL72c9+FtHe4ed6otRXOO+881T7hdeltbU1Yp1kaV8hvJ7yknuPwWBQ9+DJbt+UEz2xEOEiF5Ro0SMXysH4xje+oa1YsSJi2ZIlS7RLL71Ui3fkgnLFFVeE3vf392tlZWXa2rVrtURGbpLSbq+++mpomdwYr7766iFP1rS0NK2hoSG07N5779WysrI0j8ejxZvokYtgLOTGIaL7ySefDC3buXOn2h9yE0m0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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fmodel = FlorisModel(\"defaults\")\n", + "fmodel.set(wind_speeds=[8.0], wind_directions=[280.0], turbulence_intensities=[0.06])\n", + "fmodel.set(\n", + " layout_x = [0.0, 500.0],\n", + " layout_y = [0.0, 0.0],\n", + ")\n", + "# TEMPORARY: reset the wake model (TODO: Work out how to avoid doing this)\n", + "fmodel.set_wake_model(StraightWake(velocity_deficit=0.2, wake_width=100.0))\n", + "horizontal_plane = fmodel.calculate_horizontal_plane(\n", + " x_resolution=200,\n", + " y_resolution=100,\n", + " height=90.0,\n", + ")\n", + "\n", + "fig, ax = plt.subplots()\n", + "visualize_cut_plane(\n", + " horizontal_plane,\n", + " ax=ax,\n", + " label_contours=False,\n", + " title=\"Horizontal Flow with Turbine Rotors and labels\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0f3409fc", + "metadata": {}, + "source": [ + "## Prepackaged wake models\n", + "\n", + "Naturally, prepackaged wake models can also be used in this way. Let's take a look at using the `Gauss` wake model from FLORIS, either as one of the preset defaults or by passing the class in directly." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1dd9ed49", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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XrcF+7t69Wq56KKL9NjjXorjj2OQLzFvI4ZBcoY777wTj2h9TgcccEBi+XfeeUfnffOb30xaz/e//32dv3LlysS86dOn67zHH3+8x+/iuzPPPLPX7bruuuv0f//nf/4nMW/BggVGTU2NUV9fn5j373//2ygoKDDOOOOMxLyf/exn+r9nn3120jo/97nPGZWVlUnzioqK0m5HR0dHj3mvvPJKj2165plndB5eh3qc+zoul1xyiS7zwgsvJOa1trYaM2fONGbMmGFEo1Gdd/311xuFhYVGS0uLfr7lllt0XYcffrhx+eWX6zwsW1ZWZnzve9/rc1svuOACY8KECYnPl156qbFkyRI99rfddpvOQxs4HA7j5ptvTiyH7cZvWuzZs0e3He2RCpbFd7/4xS+S5h9yyCHGokWLjMGQ2oa9tcnGjRt1PtoidTuuuOKKHus97rjj9Lsbb7wxMS8UCiXOw3A43O96+9s/tCuW+/Of/5y0HK6ZdPNTsc71devW6fHetGmT8cc//tHw+XxGdXW10d7ersthW7HN8+fPNzo7OxP///DDD+v///SnP01q/3S3+QcffFDn//KXv0yaf+qpp+q5sH79+sQ8LIfrcvXq1UnLXnzxxUZJSYnR1dVlDJbUaxL7hP35+Mc/njQfv+12u5O2B/cJzL/11lsT80455RTD6/UamzdvTsx7//339ToaSDeXer4Pdhsxvfnmm4l52A5sD+5TFuecc44xadIkY+/evUn//5WvfMUoLS1N/F7qOdjY2KifcV8gw4furRwEJm08BaRO9idzK8gR2F0pABYfYHe7AFha8LQ7GPAkeeWVV+oTyte//nWdt3PnTnnnnXfU/A43jwW27z/+4z8S25Ua62EHVon6+np9QuoPmKbt1i783z777KNPw7AajeRx7gvsFyxRsEJYwEoDyxSe5mF5sfYNlgVYp+xWGEx4D1atWqXuq96sMxb4Hk+YsNJZ60IAsX1deJLGvbu/dfVHujaCO2+s6c2yBFcgrB8WsPDgM56a4fYa7v7B3QTXK85hWACtCa4OtDOuhYEACyqsAHiChwUK5yosApbVCtmZ2GZYPuxxNbA+zJs3r8d129u5CNenZQG0X/s4F/B7do477jjZf//9k+bh+oGLrL/zvr9rEhY4WDJxPNNdjyeeeGLCamvdJ+BKs449rhVYqeB6nDZtWmK5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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from floris.core.wake_model import Gauss\n", + "\n", + "fmodel.set_wake_model(Gauss()) # Use Gauss defaults\n", + "horizontal_plane = fmodel.calculate_horizontal_plane(\n", + " x_resolution=200,\n", + " y_resolution=100,\n", + " height=90.0,\n", + ")\n", + "\n", + "fig, ax = plt.subplots()\n", + "visualize_cut_plane(\n", + " horizontal_plane,\n", + " ax=ax,\n", + " label_contours=False,\n", + " title=\"Horizontal Flow with Turbine Rotors and labels\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ef42e63", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "floris", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/floris/core/__init__.py b/floris/core/__init__.py index e37f9c113d..18e0ad5244 100644 --- a/floris/core/__init__.py +++ b/floris/core/__init__.py @@ -22,7 +22,7 @@ import floris.logging_manager -from .base import BaseClass, BaseModel, State +from .base import BaseClass, BaseLibrary, BaseModel, State from .turbine.turbine import ( axial_induction, power, diff --git a/floris/core/base.py b/floris/core/base.py index 76c131597f..aa666477d4 100644 --- a/floris/core/base.py +++ b/floris/core/base.py @@ -1,4 +1,5 @@ +import importlib from abc import abstractmethod from enum import Enum from typing import ( @@ -63,3 +64,33 @@ def prepare_function() -> dict: @abstractmethod def function() -> None: raise NotImplementedError("BaseModel.function") + +@define +class BaseLibrary(BaseClass): + """ + Base class that writes the name and module of the class into the attrs dictionary. + """ + __classinfo__: dict = {"module": "", "name": ""} + def __attrs_post_init__(self) -> None: + #import ipdb; ipdb.set_trace() + self.__classinfo__ = { + "module": type(self).__module__, + "name": type(self).__name__ + } + + @staticmethod + def from_dict(data_dict): + """Recreate instance from dictionary with class information""" + if "__classinfo__" not in data_dict: + raise ValueError( + "Dictionary does not contain class information. ", + "Insure inheritance from BaseLibrary." + ) + data_noinfo = data_dict.copy() + class_info = data_noinfo.pop("__classinfo__") + + # Import the module and get the class + module = importlib.import_module(class_info["module"]) + cls = getattr(module, class_info["name"]) + + return cls(**data_noinfo) diff --git a/floris/core/core.py b/floris/core/core.py index 3260b269b0..602283e625 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -10,6 +10,7 @@ from floris import logging_manager from floris.core import ( BaseClass, + BaseLibrary, cc_solver, empirical_gauss_solver, Farm, @@ -61,7 +62,9 @@ class Core(BaseClass): description: str = field(converter=str) floris_version: str = field(converter=str) - grid: Grid = field(init=False) + grid: Grid | TurbineGrid | TurbineCubatureGrid | FlowFieldPlanarGrid | PointsGrid = field( + init=False + ) def __attrs_post_init__(self) -> None: @@ -139,6 +142,11 @@ def __attrs_post_init__(self) -> None: self.grid.sorted_coord_indices ) + if isinstance(self.wake.user_defined_wake_model, dict): + self.wake.user_defined_wake_model = BaseLibrary.from_dict( + self.wake.user_defined_wake_model + ) + def initialize_domain(self): """Initialize solution space prior to wake calculations""" @@ -178,7 +186,9 @@ def solve_for_turbines(self): "be included, but no enhanced wake recovery will occur." ) - if vel_model=="cc": + if self.wake.user_defined_wake_model is not None: + self.wake.user_defined_wake_model.turbine_solve(self.farm, self.flow_field, self.grid) + elif vel_model=="cc": cc_solver( self.farm, self.flow_field, @@ -234,7 +244,9 @@ def solve_for_viz(self): vel_model = self.wake.model_strings["velocity_model"] model_parameters = _temp_create_single_wake_model_dict(self.wake, vel_model) - if vel_model=="cc": + if self.wake.user_defined_wake_model is not None: + self.wake.user_defined_wake_model.point_solve(self.farm, self.flow_field, self.grid) + elif vel_model=="cc": full_flow_cc_solver(self.farm, self.flow_field, self.grid, self.wake) elif vel_model=="turbopark": full_flow_turbopark_solver(self.farm, self.flow_field, self.grid, self.wake) diff --git a/floris/core/wake.py b/floris/core/wake.py index 4016c0c18d..d688719afc 100644 --- a/floris/core/wake.py +++ b/floris/core/wake.py @@ -14,6 +14,7 @@ JimenezVelocityDeflection, NoneVelocityDeflection, ) +from floris.core.wake_model import BaseWakeModel from floris.core.wake_turbulence import ( CrespoHernandez, NoneWakeTurbulence, @@ -87,8 +88,11 @@ class WakeModelManager(BaseClass): deflection_model: BaseModel = field(init=False) turbulence_model: BaseModel = field(init=False) velocity_model: BaseModel = field(init=False) + user_defined_wake_model: BaseWakeModel | None = field(default=None) def __attrs_post_init__(self) -> None: + # TODO: May want to replace this with something that simply instantiates the correct model + # class. velocity_model_string = self.model_strings["velocity_model"].lower() model: BaseModel = MODEL_MAP["velocity_model"][velocity_model_string] if velocity_model_string == "none": @@ -127,6 +131,9 @@ def __attrs_post_init__(self) -> None: model: BaseModel = MODEL_MAP["combination_model"][combination_model_string] self.combination_model = model() + def assign_user_defined_wake_model(self, wake_model: BaseWakeModel): + self.user_defined_wake_model = wake_model + @model_strings.validator def validate_model_strings(self, instance: attrs.Attribute, value: dict) -> None: required_strings = [ diff --git a/floris/core/wake_model/base_wake_model.py b/floris/core/wake_model/base_wake_model.py index b3e171a558..253e4702e3 100644 --- a/floris/core/wake_model/base_wake_model.py +++ b/floris/core/wake_model/base_wake_model.py @@ -10,7 +10,7 @@ from floris.core import ( axial_induction, - BaseModel, + BaseLibrary, Farm, FlowField, FlowFieldPlanarGrid, @@ -21,17 +21,17 @@ @define -class BaseWakeModel(BaseModel): +class BaseWakeModel(BaseLibrary): # Inherit instead from BaseLibrary # Storage - x_i: np.ndarray = field(init=False) - y_i: np.ndarray = field(init=False) - z_i: np.ndarray = field(init=False) - - yaw_angle_i: np.ndarray = field(init=False) - hub_height_i: np.ndarray = field(init=False) - rotor_diameter_i: np.ndarray = field(init=False) - TSR_i: np.ndarray = field(init=False) + x_i: np.ndarray = field(init=False, default=None) + y_i: np.ndarray = field(init=False, default=None) + z_i: np.ndarray = field(init=False, default=None) + + yaw_angle_i: np.ndarray = field(init=False, default=None) + hub_height_i: np.ndarray = field(init=False, default=None) + rotor_diameter_i: np.ndarray = field(init=False, default=None) + TSR_i: np.ndarray = field(init=False, default=None) def set_turbine_i(self, grid, farm, i): diff --git a/floris/core/wake_model/gauss.py b/floris/core/wake_model/gauss.py index fa97c54d2d..beea94fa75 100644 --- a/floris/core/wake_model/gauss.py +++ b/floris/core/wake_model/gauss.py @@ -52,11 +52,11 @@ class Gauss(BaseWakeModel): enable_yaw_added_recovery: bool = field(converter=bool, default=True) enable_secondary_steering: bool = field(converter=bool, default=True) - effective_yaw_i: np.ndarray = field(init=False) + effective_yaw_i: np.ndarray = field(init=False, default=None) - ambient_turbulence_intensities: np.ndarray = field(init=False) - wind_veer: float = field(init=False) - freestream_velocity: np.ndarray = field(init=False) + ambient_turbulence_intensities: np.ndarray = field(init=False, default=None) + wind_veer: float = field(init=False, default=None) + freestream_velocity: np.ndarray = field(init=False, default=None) def velocity_deficit( self, diff --git a/floris/core/wake_model/jensen.py b/floris/core/wake_model/jensen.py index 8611ad1568..b1df25736d 100644 --- a/floris/core/wake_model/jensen.py +++ b/floris/core/wake_model/jensen.py @@ -38,7 +38,7 @@ class JensenJimenez(BaseWakeModel): downstream: float = field(converter=float, default=-0.32) # Uninitialized attributes set in turbine_solve - ambient_turbulence_intensities: np.ndarray = field(init=False) + ambient_turbulence_intensities: np.ndarray = field(init=False, default=None) def velocity_deficit( self, diff --git a/floris/floris_model.py b/floris/floris_model.py index 37ad9c3feb..96304071b7 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -21,6 +21,7 @@ power, thrust_coefficient, ) +from floris.core.wake_model import BaseWakeModel from floris.cut_plane import CutPlane from floris.logging_manager import LoggingManager from floris.type_dec import ( @@ -1615,6 +1616,15 @@ def set_operation_model(self, operation_model: str | List[str]): reference_wind_height=self.reference_wind_height ) + def set_wake_model(self, wake_model: BaseWakeModel): + """Set the wake model. + + Args: + wake_model (BaseWakeModel): The wake model to set. + """ + self.core.wake.assign_user_defined_wake_model(wake_model) + # TODO: Won't be kept through a new .set() operation; will need to handle that. + def copy(self): """Create an independent copy of the current FlorisModel object From 1bab40ff943fe8e209a3dc8ec4fed942e0146964 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 10 Apr 2026 16:13:23 -0600 Subject: [PATCH 17/19] Clean notebook --- docs/user_defined_wake_models.ipynb | 73 +++-------------------------- 1 file changed, 7 insertions(+), 66 deletions(-) diff --git a/docs/user_defined_wake_models.ipynb b/docs/user_defined_wake_models.ipynb index 706e6d110a..9c63398e11 100644 --- a/docs/user_defined_wake_models.ipynb +++ b/docs/user_defined_wake_models.ipynb @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "d751aa3c", "metadata": {}, "outputs": [], @@ -163,27 +163,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "b74802c2", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Powers [W]:\n", - " [[1753954.45917917 907959.89293936]\n", - " [3417797.00509157 1753954.45917917]\n", - " [5000000. 5000000. ]] \n", - "\n", - "Thrust coefficients [-]:\n", - " [[0.78715145 0.84361556]\n", - " [0.78387889 0.78715145]\n", - " [0.55092883 0.55092883]] \n", - "\n" - ] - } - ], + "outputs": [], "source": [ "from floris import FlorisModel, TimeSeries\n", "\n", @@ -218,31 +201,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "93f1e99f", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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IYQQkhJQ9BBCCCEkJaDoIYQQQkhKkHKiZ//+/bjoootQVVUFu92OWbNmqZ7jXq83Yh0ZuTn69cYbb0Rs68knn8S8efNgs9mwaNEiPPfcc0gUfve732HGjBmq7EuWLMFbb72FRGPt2rU47rjj1EjbRUVFWLlyJXbvjpxI79Of/vSAdrzssssi1jlw4ABWrFgBh8OhtnPdddehr68P8cZPf/rTAXWR40/H7XbjiiuuQH5+PjIyMnDmmWeisbExIesqyPEZ6zyUOiZ627722mv44he/qEaZlXI//fTTEZ9LfsnNN9+M0tJSdZ067bTT8NFHH0Ws09rainPOOUcN6JaTk6Oua93d3RHrbNmyBSeffLI6z2XU29tvvx3xVl+fz4frr79eXUPT09PVOueee64aRX+44+HWW29NuPoK559//oC6nHHGGUnZvkKs81he69atw6S3r5ZiPP/889r555+v/eMf/9D27t2r/f3vf9eKioq0a6+9NrTOvn37JKNNe+mll7T6+vrQy+v1htb517/+pRmNRu3222/XduzYod10002a2WzWtm7dqsU7jz32mGaxWLQHHnhA2759u3bxxRdrOTk5WmNjo5ZILF++XHvwwQe1bdu2ae+//772+c9/XquoqNC6u7tD65xyyimqfuHt2NHREfq8r69PW7hwoXbaaadp7733nvbcc89pBQUF2urVq7V445ZbbtEWLFgQUZfm5ubQ55dddplWXl6urV+/XnvnnXe0E044QTvxxBMTsq5CU1NTRF1ffPFFdV6+/PLLCd+2UpYbb7xR+9vf/qbq9NRTT0V8fuutt2rZ2dna008/rX3wwQfal770Ja2qqkpzuVyhdc444wztyCOP1N544w3t9ddf1w477DDt7LPPDn0u+6K4uFg755xz1Dny5z//WbPb7dof/vAHLZ7q297ertro8ccf13bt2qVt3rxZO/7447XFixdHbKOyslL72c9+FtHe4ed6otRXOO+881T7hdeltbU1Yp1kaV8hvJ7yknuPwWBQ9+DJbt+UEz2xEOEiF5Ro0SMXysH4xje+oa1YsSJi2ZIlS7RLL71Ui3fkgnLFFVeE3vf392tlZWXa2rVrtURGbpLSbq+++mpomdwYr7766iFP1rS0NK2hoSG07N5779WysrI0j8ejxZvokYtgLOTGIaL7ySefDC3buXOn2h9yE0m0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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "fmodel = FlorisModel(\"defaults\")\n", "fmodel.set(wind_speeds=[8.0], wind_directions=[280.0], turbulence_intensities=[0.06])\n", @@ -279,31 +241,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "1dd9ed49", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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XrcF+7t69Wq56KKL9NjjXorjj2OQLzFvI4ZBcoY777wTj2h9TgcccEBi+XfeeUfnffOb30xaz/e//32dv3LlysS86dOn67zHH3+8x+/iuzPPPLPX7bruuuv0f//nf/4nMW/BggVGTU2NUV9fn5j373//2ygoKDDOOOOMxLyf/exn+r9nn3120jo/97nPGZWVlUnzioqK0m5HR0dHj3mvvPJKj2165plndB5eh3qc+zoul1xyiS7zwgsvJOa1trYaM2fONGbMmGFEo1Gdd/311xuFhYVGS0uLfr7lllt0XYcffrhx+eWX6zwsW1ZWZnzve9/rc1svuOACY8KECYnPl156qbFkyRI99rfddpvOQxs4HA7j5ptvTiyH7cZvWuzZs0e3He2RCpbFd7/4xS+S5h9yyCHGokWLjMGQ2oa9tcnGjRt1PtoidTuuuOKKHus97rjj9Lsbb7wxMS8UCiXOw3A43O96+9s/tCuW+/Of/5y0HK6ZdPNTsc71devW6fHetGmT8cc//tHw+XxGdXW10d7ersthW7HN8+fPNzo7OxP///DDD+v///SnP01q/3S3+QcffFDn//KXv0yaf+qpp+q5sH79+sQ8LIfrcvXq1UnLXnzxxUZJSYnR1dVlDJbUaxL7hP35+Mc/njQfv+12u5O2B/cJzL/11lsT80455RTD6/UamzdvTsx7//339ToaSDeXer4Pdhsxvfnmm4l52A5sD+5TFuecc44xadIkY+/evUn//5WvfMUoLS1N/F7qOdjY2KifcV8gw4furRwEJm08BaRO9idzK8gR2F0pABYfYHe7AFha8LQ7GPAkeeWVV+oTyte//nWdt3PnTnnnnXfU/A43jwW27z/+4z8S25Ua62EHVon6+np9QuoPmKbt1i783z777KNPw7AajeRx7gvsFyxRsEJYwEoDyxSe5mF5sfYNlgVYp+xWGEx4D1atWqXuq96sMxb4Hk+YsNJZ60IAsX1deJLGvbu/dfVHujaCO2+s6c2yBFcgrB8WsPDgM56a4fYa7v7B3QTXK85hWACtCa4OtDOuhYEACyqsAHiChwUK5yosApbVCtmZ2GZYPuxxNbA+zJs3r8d129u5CNenZQG0X/s4F/B7do477jjZf//9k+bh+oGLrL/zvr9rEhY4WDJxPNNdjyeeeGLCamvdJ+BKs449rhVYqeB6nDZtWmK5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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from floris.core.wake_model import Gauss\n", "\n", From 1e6397e23a5ad6ec6dec8c89227e2835641c209e Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 May 2026 14:51:30 -0600 Subject: [PATCH 18/19] Add class version of EmGauss model --- floris/core/core.py | 18 +- floris/core/wake_model/__init__.py | 1 + floris/core/wake_model/empirical_gauss.py | 530 ++++++++++++++++++++++ 3 files changed, 542 insertions(+), 7 deletions(-) create mode 100644 floris/core/wake_model/empirical_gauss.py diff --git a/floris/core/core.py b/floris/core/core.py index 602283e625..8004508060 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -31,6 +31,7 @@ WakeModelManager, ) from floris.core.wake_model import ( + EmpiricalGauss, Gauss, JensenJimenez, NoneWake, @@ -207,12 +208,8 @@ def solve_for_turbines(self): self.wake ) elif vel_model=="empirical_gauss": - empirical_gauss_solver( - self.farm, - self.flow_field, - self.grid, - self.wake - ) + model = EmpiricalGauss(**model_parameters) + model.turbine_solve(self.farm, self.flow_field, self.grid) elif vel_model=="jensen": model = JensenJimenez(**model_parameters) model.turbine_solve(self.farm, self.flow_field, self.grid) @@ -251,7 +248,8 @@ def solve_for_viz(self): elif vel_model=="turbopark": full_flow_turbopark_solver(self.farm, self.flow_field, self.grid, self.wake) elif vel_model=="empirical_gauss": - full_flow_empirical_gauss_solver(self.farm, self.flow_field, self.grid, self.wake) + model = EmpiricalGauss(**model_parameters) + model.point_solve(self.farm, self.flow_field, self.grid) elif vel_model=="jensen": model = JensenJimenez(**model_parameters) model.point_solve(self.farm, self.flow_field, self.grid) @@ -467,6 +465,12 @@ def _temp_create_single_wake_model_dict(wake, vel_model): model_parameters = wake.wake_velocity_parameters["jensen"] | \ wake.wake_deflection_parameters["jimenez"] | \ wake.wake_turbulence_parameters["crespo_hernandez"] + elif vel_model == "empirical_gauss": + model_parameters = wake.wake_velocity_parameters["empirical_gauss"] | \ + wake.wake_deflection_parameters["empirical_gauss"] | \ + wake.wake_turbulence_parameters["wake_induced_mixing"] + model_parameters["enable_yaw_added_recovery"] = wake.enable_yaw_added_recovery + model_parameters["enable_active_wake_mixing"] = wake.enable_active_wake_mixing elif vel_model == "none": model_parameters = {} else: diff --git a/floris/core/wake_model/__init__.py b/floris/core/wake_model/__init__.py index a48f88a237..0ad8539ac7 100644 --- a/floris/core/wake_model/__init__.py +++ b/floris/core/wake_model/__init__.py @@ -1,4 +1,5 @@ from floris.core.wake_model.base_wake_model import BaseWakeModel +from floris.core.wake_model.empirical_gauss import EmpiricalGauss from floris.core.wake_model.gauss import Gauss from floris.core.wake_model.jensen import JensenJimenez from floris.core.wake_model.none_model import NoneWake diff --git a/floris/core/wake_model/empirical_gauss.py b/floris/core/wake_model/empirical_gauss.py new file mode 100644 index 0000000000..1673f6e146 --- /dev/null +++ b/floris/core/wake_model/empirical_gauss.py @@ -0,0 +1,530 @@ +import numexpr as ne +import numpy as np +from attrs import ( + define, + field, + fields, +) + +from floris.type_dec import floris_float_type + +from floris.core import ( + BaseModel, + Farm, + FlowField, + FlowFieldPlanarGrid, + PointsGrid, + TurbineGrid, +) +from floris.core.rotor_velocity import average_velocity +from floris.core.wake_model.gauss import gaussian_function +from floris.core.wake_model import BaseWakeModel +from floris.utilities import cosd + + +NUM_EPS = fields(BaseModel).NUM_EPS.default + +@define +class EmpiricalGauss(BaseWakeModel): + + # Deficit model parameters + wake_expansion_rates: list = field(factory=lambda: [0.023, 0.008]) + breakpoints_D: list = field(factory=lambda: [10]) + sigma_0_D: float = field(default=0.28) + smoothing_length_D: float = field(default=2.0) + mixing_gain_velocity: float = field(default=2.0) + awc_mode: str = field(default="baseline") + awc_wake_exp: float = field(default=1.2) + awc_wake_denominator: float = field(default=400) + include_mirror_wake: bool = field(default=True) + + # Deflection model parameters + horizontal_deflection_gain_D: float = field(default=3.0) + vertical_deflection_gain_D: float = field(default=-1) + deflection_rate: float = field(default=22) + mixing_gain_deflection: float = field(default=0.0) + yaw_added_mixing_gain: float = field(default=0.0) + + # Mixing model parameters + atmospheric_ti_gain: float = field(converter=float, default=0.0) + enable_yaw_added_recovery: bool = field(default=True) + enable_active_wake_mixing: bool = field(default=True) + + tilt_angle_i: np.ndarray = field(init=False, default=None) + + ambient_turbulence_intensities: np.ndarray = field(init=False, default=None) + wind_veer: float = field(init=False, default=None) + freestream_velocity: np.ndarray = field(init=False, default=None) + mixing_factor: np.ndarray = field(init=False, default=None) + + def velocity_deficit( + self, + deflection_field_y_i: np.ndarray, + deflection_field_z_i: np.ndarray, + mixing_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + y: np.ndarray, + z: np.ndarray, + ) -> np.ndarray: + + # Only symmetric terms using yaw, but keep for consistency + yaw_angle = -1 * self.yaw_angle_i + + # Initial wake widths + sigma_y0 = self.sigma_0_D * self.rotor_diameter_i * cosd(yaw_angle) + sigma_z0 = self.sigma_0_D * self.rotor_diameter_i * cosd(self.tilt_angle_i) + + # No specific near, far wakes in this model + downstream_mask = (x > self.x_i + 0.1) + upstream_mask = (x < self.x_i - 0.1) + + # Wake expansion in the lateral (y) and the vertical (z) + # TODO: could compute shared components in sigma_z, sigma_y + # with one function call. + sigma_y = empirical_gauss_model_wake_width( + x - self.x_i, + self.wake_expansion_rates, + [b * self.rotor_diameter_i for b in self.breakpoints_D], # .flatten()[0] + sigma_y0, + self.smoothing_length_D * self.rotor_diameter_i, + self.mixing_gain_velocity * mixing_i, + ) + sigma_y[upstream_mask] = \ + np.tile(sigma_y0, np.shape(sigma_y)[1:])[upstream_mask] + + sigma_z = empirical_gauss_model_wake_width( + x - self.x_i, + self.wake_expansion_rates, + [b * self.rotor_diameter_i for b in self.breakpoints_D], # .flatten()[0] + sigma_z0, + self.smoothing_length_D * self.rotor_diameter_i, + self.mixing_gain_velocity * mixing_i, + ) + sigma_z[upstream_mask] = \ + np.tile(sigma_z0, np.shape(sigma_z)[1:])[upstream_mask] + + # 'Standard' wake component + r, C = rCalt( + self.wind_veer, + sigma_y, + sigma_z, + y, + self.y_i, + deflection_field_y_i, + deflection_field_z_i, + z, + self.hub_height_i, + ct_i, + yaw_angle, + self.tilt_angle_i, + self.rotor_diameter_i, + sigma_y0, + sigma_z0 + ) + # Normalize to match end of actuator disk model tube + C = C / (8 * self.sigma_0_D**2 ) + + wake_deficit = gaussian_function(C, r, 1, np.sqrt(0.5)) + + if self.include_mirror_wake: + # TODO: speed up this option by calculating various elements in + # rCalt only once. + # Mirror component + r_mirr, C_mirr = rCalt( + self.wind_veer, # TODO: Is veer OK with mirror wakes? + sigma_y, + sigma_z, + y, + self.y_i, + deflection_field_y_i, + deflection_field_z_i, + z, + -self.hub_height_i, # Turbine at negative hub height location + ct_i, + yaw_angle, + self.tilt_angle_i, + self.rotor_diameter_i, + sigma_y0, + sigma_z0 + ) + # Normalize to match end of actuator disk model tube + C_mirr = C_mirr / (8 * self.sigma_0_D**2) + + # ASSUME sum-of-squares superposition for the real and mirror wakes + wake_deficit = np.sqrt( + wake_deficit**2 + + gaussian_function(C_mirr, r_mirr, 1, np.sqrt(0.5))**2 + ) + + velocity_deficit = wake_deficit * downstream_mask + + return velocity_deficit + + def deflection( + self, + mixing_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + ) -> tuple[np.ndarray, np.ndarray]: + + deflection_gain_y = self.horizontal_deflection_gain_D * self.rotor_diameter_i + if self.vertical_deflection_gain_D == -1: + deflection_gain_z = deflection_gain_y + else: + deflection_gain_z = self.vertical_deflection_gain_D * self.rotor_diameter_i + + # Convert to radians, CW yaw for consistency with other models + yaw_r = np.pi/180 * -self.yaw_angle_i + tilt_r = np.pi/180 * self.tilt_angle_i + + A_y = (deflection_gain_y * ct_i * yaw_r) / (1 + self.mixing_gain_deflection * mixing_i) + A_z = (deflection_gain_z * ct_i * tilt_r) / (1 + self.mixing_gain_deflection * mixing_i) + + # Apply downstream mask in the process + x_normalized = (x - self.x_i) * (x > self.x_i + 0.1) / self.rotor_diameter_i + + log_term = np.log( + (x_normalized - self.deflection_rate) / (x_normalized + self.deflection_rate) + + 2 + ) + + deflection_y = A_y * log_term + deflection_z = A_z * log_term + + return deflection_y, deflection_z + + def combination(self, wake_field: np.ndarray, velocity_field: np.ndarray): + """ + Combines the base flow field with the velocity deficits + using sum of squares. + + Args: + u_field (np.array): The base flow field. + u_wake (np.array): The wake to apply to the base flow field. + + Returns: + np.array: The resulting flow field after applying the wake to the + base. + """ + return np.hypot(wake_field, velocity_field) + + def mixing( + self, + axial_induction_i: np.ndarray, + downstream_distance_D_i: np.ndarray, + ) -> np.ndarray: + """ + Calculates the contribution of turbine i to all other turbines' + mixing terms. + + Args: + axial_induction_i (np.array): Axial induction factor of + the ith turbine (-). + downstream_distance_D_i (np.array): The distance downstream + from turbine i to all other turbines (specified in terms + of multiples of turbine i's rotor diameter) (D). + + Returns: + np.array: Components of the wake-induced mixing term due to + the ith turbine. + """ + + wake_induced_mixing = axial_induction_i[:,:,0,0] / downstream_distance_D_i**2 + + return wake_induced_mixing + + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + + # Initialize mixing factor information + x_locs = np.mean(grid.x_sorted, axis=(2, 3))[:,:,None] + downstream_distance_D = x_locs - np.transpose(x_locs, axes=(0,2,1)) + downstream_distance_D = downstream_distance_D / \ + np.repeat(farm.rotor_diameters_sorted[:,:,None], grid.n_turbines, axis=-1) + downstream_distance_D = np.maximum(downstream_distance_D, 0.1) # For ease + # Initialize the mixing factor model using TI if specified + initial_mixing_factor = self.atmospheric_ti_gain * np.eye(grid.n_turbines) + mixing_factor = np.repeat( + initial_mixing_factor[None, :, :], + flow_field.n_findex, + axis=0 + ) + mixing_factor = mixing_factor * flow_field.turbulence_intensities[:, None, None] + + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity + # with dimensions expanded for (n_turbines, grid, grid) + self.ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + + # Copy uniform flow field parameters + self.freestream_velocity = flow_field.u_initial_sorted + self.wind_veer = flow_field.wind_veer + + + # Calculate the velocity deficit sequentially from upstream to downstream turbines + for i in range(grid.n_turbines): + + # Turbine quantities + self.set_turbine_i(grid, farm, i) + thrust_coefficient_i = self.turbine_thrust_coefficient(grid, farm, flow_field, i) + axial_induction_i = self.turbine_axial_induction(grid, farm, flow_field, i) + + # Compute the tilt angle of the ith turbine + average_velocities = average_velocity( + flow_field.u_sorted, + method=grid.average_method, + cubature_weights=grid.cubature_weights + ) + self.tilt_angle_i = farm.calculate_tilt_for_eff_velocities( + average_velocities + )[:, i:i+1, None, None] + + if self.enable_yaw_added_recovery: + # Influence of yawing on turbine's own wake + mixing_factor[:, i:i+1, i] += \ + yaw_added_wake_mixing( + axial_induction_i, self.yaw_angle_i, 1, self.yaw_added_mixing_gain + ) + if self.enable_active_wake_mixing: + # Influence of awc on turbine's own wake + mixing_factor[:, i:i+1, i] += \ + awc_added_wake_mixing( + farm.awc_modes_sorted[:, i:i+1, None, None], + farm.awc_amplitudes_sorted[:, i:i+1, None, None], + farm.awc_frequencies_sorted[:, i:i+1, None, None], + self.awc_wake_exp, + self.awc_wake_denominator + ) + + # Extract total wake induced mixing for turbine i + mixing_i = np.linalg.norm( + mixing_factor[:, i:i+1, :, None], + ord=2, axis=2, keepdims=True + ) + + # Primary model calculations + deflection_field_y, deflection_field_z = self.deflection( + mixing_i, + thrust_coefficient_i, + grid.x_sorted, + ) + + velocity_deficit = self.velocity_deficit( + deflection_field_y, + deflection_field_z, + mixing_i, + thrust_coefficient_i, + grid.x_sorted, + grid.y_sorted, + grid.z_sorted + ) + + wake_field = self.combination( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + # Calculate wake overlap for wake-added turbulence (WAT) + area_overlap = np.sum( + velocity_deficit * flow_field.u_initial_sorted > 0.05, + axis=(2, 3) + ) / (grid.grid_resolution * grid.grid_resolution) + + # Compute wake induced mixing factor + mixing_factor[:,:,i] += area_overlap * self.mixing( + axial_induction_i, downstream_distance_D[:,:,i] + ) + + if self.enable_yaw_added_recovery: + mixing_factor[:,:,i] += \ + area_overlap * yaw_added_wake_mixing( + axial_induction_i, + self.yaw_angle_i, + downstream_distance_D[:,:,i], + self.yaw_added_mixing_gain + ) + + # Remove wakes from flow field + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + + # Store for use in point_solve + self.mixing_factor = mixing_factor + + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: FlowFieldPlanarGrid | PointsGrid, + ) -> None: + + # Get the flow quantities and turbine performance + ( + turbine_grid_farm, + turbine_grid_flow_field, + turbine_grid + ) = self.generate_turbine_grid_objects(farm, flow_field) + + self.turbine_solve(turbine_grid_farm, turbine_grid_flow_field, turbine_grid) + + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + + # Initialize the turbulence intensity field over the entire flow field grid + n_points = grid.x_sorted.shape[1] + ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) + turbulence_intensity_field = ambient_turbulence_intensities.copy() + + # Extract freestream velocity for deficit, deflection calculations + self.freestream_velocity = flow_field.u_initial_sorted + + # Calculate the velocity deficit in the full grid sequentially from upstream to + # downstream turbines + for i in range(grid.n_turbines): + + # Get the current turbine quantities + self.set_turbine_i(turbine_grid, turbine_grid_farm, i) + thrust_coefficient_i = self.turbine_thrust_coefficient( + turbine_grid, + turbine_grid_farm, + turbine_grid_flow_field, + i + ) + axial_induction_i = self.turbine_axial_induction( + turbine_grid, + turbine_grid_farm, + turbine_grid_flow_field, + i + ) + + # Get mixing_i based on turbine_solve results + mixing_i = self.mixing_factor[:, i:i+1, :, None].sum(axis=2, keepdims=1) + + average_velocities = average_velocity( + turbine_grid_flow_field.u_sorted, + method=turbine_grid.average_method, + cubature_weights=turbine_grid.cubature_weights + ) + tilt_angle_i = turbine_grid_farm.calculate_tilt_for_eff_velocities(average_velocities) + tilt_angle_i = tilt_angle_i[:, i:i+1, None, None] + + # Model calculations + deflection_field_y, deflection_field_z = self.deflection( + mixing_i, + thrust_coefficient_i, + grid.x_sorted, + ) + + velocity_deficit = self.velocity_deficit( + deflection_field_y, + deflection_field_z, + mixing_i, + thrust_coefficient_i, + grid.x_sorted, + grid.y_sorted, + grid.z_sorted + ) + + wake_field = self.combination( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + + flow_field.turbulence_intensity_field_sorted = turbulence_intensity_field + + +# @profile +def rCalt(wind_veer, sigma_y, sigma_z, y, y_i, delta_y, delta_z, z, HH, Ct, + yaw, tilt, D, sigma_y0, sigma_z0): + + ## Numexpr + wind_veer = np.deg2rad(wind_veer) + a = ne.evaluate( + "cos(wind_veer) ** 2 / (2 * sigma_y ** 2) + sin(wind_veer) ** 2 / (2 * sigma_z ** 2)" + ) + b = ne.evaluate( + "-sin(2 * wind_veer) / (4 * sigma_y ** 2) + sin(2 * wind_veer) / (4 * sigma_z ** 2)" + ) + c = ne.evaluate( + "sin(wind_veer) ** 2 / (2 * sigma_y ** 2) + cos(wind_veer) ** 2 / (2 * sigma_z ** 2)" + ) + r = ne.evaluate( + "a * ( (y - y_i - delta_y) ** 2) - "+\ + "2 * b * (y - y_i - delta_y) * (z - HH - delta_z) + "+\ + "c * ((z - HH - delta_z) ** 2)" + ) + d = 1 - Ct * (sigma_y0 * sigma_z0)/(sigma_y * sigma_z) * cosd(yaw) * cosd(tilt) + C = ne.evaluate("1 - sqrt(d)") + return r, C + +def sigmoid_integral(x, center=0, width=1): + y = np.zeros_like(x) + # TODO: Can this be made faster? + above_smoothing_zone = (x-center) > width/2 + y[above_smoothing_zone] = (x-center)[above_smoothing_zone] + in_smoothing_zone = ((x-center) >= -width/2) & ((x-center) <= width/2) + z = ((x-center)/width + 0.5)[in_smoothing_zone] + if width.shape[0] > 1: # multiple turbine sizes + width = np.broadcast_to(width, x.shape)[in_smoothing_zone] + y[in_smoothing_zone] = (width*(z**6 - 3*z**5 + 5/2*z**4)).flatten() + return y + +def empirical_gauss_model_wake_width( + x, + wake_expansion_rates, + breakpoints, + sigma_0, + smoothing_length, + mixing_final, + ): + assert len(wake_expansion_rates) == len(breakpoints) + 1, \ + "Invalid combination of wake_expansion_rates and breakpoints." + + sigma = (wake_expansion_rates[0] + mixing_final) * x + sigma_0 + for ib, b in enumerate(breakpoints): + sigma += (wake_expansion_rates[ib+1] - wake_expansion_rates[ib]) * \ + sigmoid_integral(x, center=b, width=smoothing_length) + + return sigma + +def awc_added_wake_mixing( + awc_mode_i, + awc_amplitude_i, + awc_frequency_i, + awc_wake_exp, + awc_wake_denominator +): + # Drop surplus (grid) dimensions + awc_amplitude_i = awc_amplitude_i[:,:,0,0] + awc_mode_i = awc_mode_i[:,:,0,0] + + # TODO: Add TI in the mix, finetune amplitude/freq effect + awc_mixing_factor = np.zeros_like(awc_amplitude_i, dtype=floris_float_type) + helix_mask = awc_mode_i == 'helix' + + awc_mixing_factor[helix_mask] = ( + awc_amplitude_i[helix_mask]**awc_wake_exp/awc_wake_denominator + ) + + return awc_mixing_factor + +def yaw_added_wake_mixing( + axial_induction_i, + yaw_angle_i, + downstream_distance_D_i, + yaw_added_mixing_gain +): + return ( + axial_induction_i[:,:,0,0] + * yaw_added_mixing_gain + * (1 - cosd(yaw_angle_i[:,:,0,0])) + / downstream_distance_D_i**2 + ) From 70958b68b70afa28ffa9c3640948b009aab15613 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 5 May 2026 15:57:53 -0600 Subject: [PATCH 19/19] point sampling using point_solve method --- floris/core/core.py | 28 +++++++++++++++-------- floris/core/wake_model/empirical_gauss.py | 5 ++-- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/floris/core/core.py b/floris/core/core.py index 8004508060..b2e22f6f6e 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -12,13 +12,11 @@ BaseClass, BaseLibrary, cc_solver, - empirical_gauss_solver, Farm, FlowField, FlowFieldGrid, FlowFieldPlanarGrid, full_flow_cc_solver, - full_flow_empirical_gauss_solver, full_flow_sequential_solver, full_flow_turbopark_solver, Grid, @@ -285,16 +283,26 @@ def solve_for_points(self, x, y, z): self.flow_field.initialize_velocity_field(field_grid) vel_model = self.wake.model_strings["velocity_model"] + model_parameters = _temp_create_single_wake_model_dict(self.wake, vel_model) - if vel_model == "turbopark": - raise NotImplementedError( - "solve_for_points is not available for the legacy \'turbopark\' model. " - "However, it is available for \'turboparkgauss\'." - ) - elif vel_model == "empirical_gauss": - full_flow_empirical_gauss_solver(self.farm, self.flow_field, field_grid, self.wake) - elif vel_model == "cc": + if self.wake.user_defined_wake_model is not None: + self.wake.user_defined_wake_model.point_solve(self.farm, self.flow_field, field_grid) + elif vel_model=="cc": full_flow_cc_solver(self.farm, self.flow_field, field_grid, self.wake) + elif vel_model=="turbopark": + full_flow_turbopark_solver(self.farm, self.flow_field, field_grid, self.wake) + elif vel_model=="empirical_gauss": + model = EmpiricalGauss(**model_parameters) + model.point_solve(self.farm, self.flow_field, field_grid) + elif vel_model=="jensen": + model = JensenJimenez(**model_parameters) + model.point_solve(self.farm, self.flow_field, field_grid) + elif vel_model=="gauss": + model = Gauss(**model_parameters) + model.point_solve(self.farm, self.flow_field, field_grid) + elif vel_model=="none": + model = NoneWake(**model_parameters) + model.point_solve(self.farm, self.flow_field, field_grid) else: full_flow_sequential_solver(self.farm, self.flow_field, field_grid, self.wake) diff --git a/floris/core/wake_model/empirical_gauss.py b/floris/core/wake_model/empirical_gauss.py index 1673f6e146..ca80625302 100644 --- a/floris/core/wake_model/empirical_gauss.py +++ b/floris/core/wake_model/empirical_gauss.py @@ -6,8 +6,6 @@ fields, ) -from floris.type_dec import floris_float_type - from floris.core import ( BaseModel, Farm, @@ -17,8 +15,9 @@ TurbineGrid, ) from floris.core.rotor_velocity import average_velocity -from floris.core.wake_model.gauss import gaussian_function from floris.core.wake_model import BaseWakeModel +from floris.core.wake_model.gauss import gaussian_function +from floris.type_dec import floris_float_type from floris.utilities import cosd