1+ import os
2+ import sys
3+ from pathlib import Path
4+
5+ import numpy as np
6+ import pandas as pd
7+
8+ from ard .farm_aero .floris import create_FLORIS_turbine_from_windIO
9+ try :
10+ from flowfarm .flowfarm_model import (
11+ ensure_flowfarm_loaded ,
12+ resolve_turbine_inputs_for_flowfarm ,
13+ resolve_wake_model_inputs_for_flowfarm ,
14+ )
15+ except ModuleNotFoundError :
16+ # Local checkout fallback: add repository-level Ard/ to sys.path.
17+ repo_ard_dir = Path (__file__ ).resolve ().parents [2 ]
18+ if str (repo_ard_dir ) not in sys .path :
19+ sys .path .insert (0 , str (repo_ard_dir ))
20+ from flowfarm .flowfarm_model import (
21+ ensure_flowfarm_loaded ,
22+ resolve_turbine_inputs_for_flowfarm ,
23+ resolve_wake_model_inputs_for_flowfarm ,
24+ )
25+
26+ import ard .farm_aero .templates as templates
27+
28+ class FLOWFarmComponent :
29+
30+ def initialize (self ):
31+ # This mixin is invoked explicitly by derived classes; no super() chain here.
32+ return
33+
34+ def setup (self ):
35+ jl = ensure_flowfarm_loaded ()
36+ self ._jl = jl
37+ model_options = self .options ["modeling_options" ]
38+ self .N_turbines = model_options ["layout" ]["N_turbines" ]
39+ windIO = model_options ["windIO_plant" ]
40+ N_turbines = self .N_turbines
41+
42+ turbine_floris = create_FLORIS_turbine_from_windIO (windIO )
43+ ref_air_density = model_options .get ("flowfarm" , {}).get (
44+ "ref_air_density" , 1.225
45+ )
46+
47+
48+ hub_height = turbine_floris ["hub_height" ]
49+ rotor_diameter = turbine_floris ["rotor_diameter" ]
50+
51+ windIOturbine = windIO ["wind_farm" ]["turbine" ]
52+ turbine_inputs = resolve_turbine_inputs_for_flowfarm (windIOturbine )
53+ generator_efficiency = turbine_inputs ["generator_efficiency" ]
54+ rated_power = turbine_inputs ["rated_power" ]
55+ rated_wind_speed = turbine_inputs ["rated_wind_speed" ]
56+ cutin_wind_speed = turbine_inputs ["cutin_wind_speed" ]
57+ cutout_wind_speed = turbine_inputs ["cutout_wind_speed" ]
58+ ct_model = turbine_inputs ["ct_model" ]
59+ power_model = turbine_inputs ["power_model" ]
60+
61+ windrose_floris = templates .create_windresource_from_windIO (
62+ windIO ,
63+ resource_type = "probability" ,
64+ )
65+
66+ wind_directions = windrose_floris .wd_flat
67+ wind_speeds = windrose_floris .ws_flat
68+ wind_probabilities = windrose_floris .freq_table_flat
69+ turbulence_intensity = np .mean (windrose_floris .ti_table_flat )
70+ ref_height = windIO ["site" ]["energy_resource" ]["wind_resource" ].get (
71+ "reference_height" , hub_height
72+ )
73+ wind_shear = windIO ["site" ]["energy_resource" ]["wind_resource" ].get (
74+ "shear" , 0.084
75+ )
76+
77+ flowfarm_options = model_options .get ("flowfarm" , {})
78+ wake_option_keys = {
79+ "wake_deficit_model" ,
80+ "wake_deflection_model" ,
81+ "wake_combination_model" ,
82+ "local_turbulence_model" ,
83+ "tolerance" ,
84+ }
85+ wake_options_only = {
86+ key : value
87+ for key , value in flowfarm_options .items ()
88+ if key in wake_option_keys
89+ }
90+ wake_model_options = resolve_wake_model_inputs_for_flowfarm (wake_options_only )
91+
92+ # FLOWFarm expects one model object per turbine.
93+ ct_models = jl .fill (ct_model , N_turbines )
94+ power_models = jl .fill (power_model , N_turbines )
95+
96+ flowfarm_module = jl .FLOWFarm
97+ n_states = len (wind_speeds )
98+
99+ # FLOWFarm expects radians for wind direction.
100+ wind_dirs_rad = jl .Vector [jl .Float64 ](
101+ list (map (float , np .deg2rad (np .asarray (wind_directions ))))
102+ )
103+ wind_speeds_vec = jl .Vector [jl .Float64 ](
104+ list (map (float , np .asarray (wind_speeds )))
105+ )
106+ wind_probs_vec = jl .Vector [jl .Float64 ](
107+ list (map (float , np .asarray (wind_probabilities )))
108+ )
109+ ambient_tis = jl .fill (float (turbulence_intensity ), n_states )
110+ measurementheight = jl .fill (float (ref_height ), n_states )
111+
112+ wind_shear_model = flowfarm_module .PowerLawWindShear (float (wind_shear ))
113+ windresource = flowfarm_module .DiscretizedWindResource (
114+ wind_dirs_rad ,
115+ wind_speeds_vec ,
116+ wind_probs_vec ,
117+ measurementheight ,
118+ float (ref_air_density ),
119+ ambient_tis ,
120+ wind_shear_model ,
121+ )
122+
123+ wake_deficit = getattr (
124+ flowfarm_module , wake_model_options ["wake_deficit_model" ]
125+ )()
126+ wake_deflection = getattr (
127+ flowfarm_module , wake_model_options ["wake_deflection_model" ]
128+ )()
129+ wake_combine = getattr (
130+ flowfarm_module , wake_model_options ["wake_combination_model" ]
131+ )()
132+ local_ti = getattr (
133+ flowfarm_module , wake_model_options ["local_turbulence_model" ]
134+ )()
135+
136+ model_set = flowfarm_module .WindFarmModelSet (
137+ wake_deficit ,
138+ wake_deflection ,
139+ wake_combine ,
140+ local_ti ,
141+ )
142+
143+ # Temporary initialization until layout-driven vectors are wired in.
144+ x0 = jl .zeros (N_turbines * 3 )
145+ turbine_x = jl .zeros (N_turbines )
146+ turbine_y = jl .zeros (N_turbines )
147+ turbine_z = jl .zeros (N_turbines )
148+ turbine_yaw = jl .zeros (N_turbines )
149+
150+ hub_heights = jl .fill (float (hub_height ), N_turbines )
151+ rotor_diameters = jl .fill (float (rotor_diameter ), N_turbines )
152+ generator_efficiencies = jl .fill (float (generator_efficiency ), N_turbines )
153+ cut_in_speeds = jl .fill (float (cutin_wind_speed ), N_turbines )
154+ cut_out_speeds = jl .fill (float (cutout_wind_speed ), N_turbines )
155+ rated_speeds = jl .fill (float (rated_wind_speed ), N_turbines )
156+ rated_powers = jl .fill (float (rated_power ), N_turbines )
157+
158+ # Use a pure Julia callback so threaded FLOWFarm paths do not call back into Python.
159+ jl .seval (
160+ """
161+ function ard_make_flowfarm_update_fn()
162+ return function (farm, x)
163+ n = length(farm.turbine_x)
164+ @inbounds for i in 1:n
165+ farm.turbine_x[i] = x[i]
166+ farm.turbine_y[i] = x[n + i]
167+ farm.turbine_yaw[i] = x[2n + i]
168+ end
169+ return nothing
170+ end
171+ end
172+ """
173+ )
174+ update_fn = jl .ard_make_flowfarm_update_fn ()
175+ sparse_farm , sparse_struct = flowfarm_module .build_unstable_sparse_struct (
176+ x0 ,
177+ turbine_x ,
178+ turbine_y ,
179+ turbine_z ,
180+ hub_heights ,
181+ turbine_yaw ,
182+ rotor_diameters ,
183+ ct_models ,
184+ generator_efficiencies ,
185+ cut_in_speeds ,
186+ cut_out_speeds ,
187+ rated_speeds ,
188+ rated_powers ,
189+ windresource ,
190+ power_models ,
191+ model_set ,
192+ update_fn ,
193+ AEP_scale = 1 ,
194+ opt_x = True ,
195+ opt_y = True ,
196+ opt_yaw = True ,
197+ tolerance = wake_model_options .get ("tolerance" , 1e-16 ),
198+ )
199+
200+ farm = flowfarm_module .build_wind_farm_struct (
201+ x0 ,
202+ turbine_x ,
203+ turbine_y ,
204+ turbine_z ,
205+ hub_heights ,
206+ turbine_yaw ,
207+ rotor_diameters ,
208+ ct_models ,
209+ generator_efficiencies ,
210+ cut_in_speeds ,
211+ cut_out_speeds ,
212+ rated_speeds ,
213+ rated_powers ,
214+ windresource ,
215+ power_models ,
216+ model_set ,
217+ update_fn ,
218+ AEP_scale = 1 ,
219+ )
220+
221+ self .flowfarm_module = flowfarm_module
222+ self .x0 = x0
223+ self .farm = farm
224+ self .sparse_farm = sparse_farm
225+ self .sparse_struct = sparse_struct
226+
227+ def _build_design_vector (self , inputs ):
228+ x_turbines = np .asarray (inputs ["x_turbines" ], dtype = float )
229+ y_turbines = np .asarray (inputs ["y_turbines" ], dtype = float )
230+ yaw_turbines = np .asarray (inputs ["yaw_turbines" ], dtype = float )
231+ return np .concatenate ([x_turbines , y_turbines , yaw_turbines ]).ravel ()
232+
233+ def _evaluate_sparse (self , x_eval_np ):
234+ """Run sparse gradient evaluation once and cache AEP/gradient for reuse."""
235+ if hasattr (self , "_cached_sparse_x" ) and np .array_equal (self ._cached_sparse_x , x_eval_np ):
236+ return
237+
238+ jl = getattr (self , "_jl" , None )
239+ if jl is None :
240+ jl = ensure_flowfarm_loaded ()
241+ self ._jl = jl
242+ x_eval = jl .Vector [jl .Float64 ](list (map (float , x_eval_np )))
243+ calculate_grad_bang = getattr (self .flowfarm_module , "calculate_aep_gradient!" )
244+ aep_val , grad_val = calculate_grad_bang (
245+ self .sparse_farm ,
246+ x_eval ,
247+ self .sparse_struct ,
248+ )
249+
250+ self ._cached_sparse_x = x_eval_np .copy ()
251+ self ._cached_sparse_aep = float (aep_val )
252+ self ._cached_sparse_grad = np .asarray (grad_val ).ravel ().copy ()
253+
254+ def _evaluate_farm (self , x_eval_np ):
255+ """Run regular farm AEP evaluation and cache AEP."""
256+ if hasattr (self , "_cached_farm_x" ) and np .array_equal (self ._cached_farm_x , x_eval_np ):
257+ return
258+
259+ jl = getattr (self , "_jl" , None )
260+ if jl is None :
261+ jl = ensure_flowfarm_loaded ()
262+ self ._jl = jl
263+ x_eval = jl .Vector [jl .Float64 ](list (map (float , x_eval_np )))
264+ calculate_aep_bang = getattr (self .flowfarm_module , "calculate_aep!" )
265+ aep_val = calculate_aep_bang (self .farm , x_eval )
266+
267+ self ._cached_farm_x = x_eval_np .copy ()
268+ self ._cached_farm_aep = float (aep_val )
269+
270+ def _compute_aep (self , inputs , outputs ):
271+ """Compute farm AEP using regular calculate_aep!(farm, x)."""
272+ x_eval_np = self ._build_design_vector (inputs )
273+ self ._evaluate_farm (x_eval_np )
274+ outputs ["AEP_farm" ] = self ._cached_farm_aep
275+
276+ def _compute_aep_partials (self , inputs , partials ):
277+ """Compute AEP partial derivatives from sparse gradient evaluation."""
278+ x_eval_np = self ._build_design_vector (inputs )
279+ self ._evaluate_sparse (x_eval_np )
280+ grad = self ._cached_sparse_grad
281+ partials ["AEP_farm" , "x_turbines" ] = grad [: self .N_turbines ]
282+ partials ["AEP_farm" , "y_turbines" ] = grad [
283+ self .N_turbines : 2 * self .N_turbines
284+ ]
285+ partials ["AEP_farm" , "yaw_turbines" ] = grad [
286+ 2 * self .N_turbines : 3 * self .N_turbines
287+ ]
288+
289+
290+ class FLOWFarmAEP (templates .FarmAEPTemplate , FLOWFarmComponent ):
291+
292+ def initialize (self ):
293+ templates .FarmAEPTemplate .initialize (self )
294+ FLOWFarmComponent .initialize (self )
295+
296+ def setup (self ):
297+ templates .FarmAEPTemplate .setup (self )
298+ FLOWFarmComponent .setup (self )
299+
300+ def setup_partials (self ):
301+ self .declare_partials ("AEP_farm" , "x_turbines" , method = "exact" )
302+ self .declare_partials ("AEP_farm" , "y_turbines" , method = "exact" )
303+ self .declare_partials ("AEP_farm" , "yaw_turbines" , method = "exact" )
304+
305+ def compute (self , inputs , outputs ):
306+ FLOWFarmComponent ._compute_aep (self , inputs , outputs )
307+
308+ def compute_partials (self , inputs , partials ):
309+ FLOWFarmComponent ._compute_aep_partials (self , inputs , partials )
310+
311+
312+ class FLOWFarmBatchPower (templates .BatchFarmPowerTemplate , FLOWFarmComponent ):
313+
314+ def initialize (self ):
315+ templates .BatchFarmPowerTemplate .initialize (self )
316+ FLOWFarmComponent .initialize (self )
317+
318+ def setup (self ):
319+ templates .BatchFarmPowerTemplate .setup (self )
320+ FLOWFarmComponent .setup (self )
321+
322+ def setup_partials (self ):
323+ # State power sensitivities are provided via sparse_struct.state_gradients.
324+ self .declare_partials ("power_farm" , "x_turbines" , method = "exact" )
325+ self .declare_partials ("power_farm" , "y_turbines" , method = "exact" )
326+ self .declare_partials ("power_farm" , "yaw_turbines" , method = "exact" )
327+
328+ def compute (self , inputs , outputs ):
329+ x_eval_np = self ._build_design_vector (inputs )
330+ self ._evaluate_sparse (x_eval_np )
331+
332+ state_powers = np .asarray (self .sparse_struct .state_powers ).ravel ()
333+ turbine_powers = np .asarray (self .sparse_struct .turbine_powers )
334+
335+ outputs ["power_farm" ] = state_powers
336+ if (
337+ self .options ["modeling_options" ]
338+ .get ("aero" , {})
339+ .get ("return_turbine_output" )
340+ ):
341+ outputs ["power_turbines" ] = turbine_powers
342+ outputs ["thrust_turbines" ] = np .zeros (
343+ (self .N_turbines , self .N_wind_conditions )
344+ )
345+
346+ def compute_partials (self , inputs , partials ):
347+ x_eval_np = self ._build_design_vector (inputs )
348+ self ._evaluate_sparse (x_eval_np )
349+
350+ state_gradients = np .asarray (self .sparse_struct .state_gradients )
351+ partials ["power_farm" , "x_turbines" ] = state_gradients [
352+ :, : self .N_turbines
353+ ]
354+ partials ["power_farm" , "y_turbines" ] = state_gradients [
355+ :, self .N_turbines : 2 * self .N_turbines
356+ ]
357+ partials ["power_farm" , "yaw_turbines" ] = state_gradients [
358+ :, 2 * self .N_turbines : 3 * self .N_turbines
359+ ]
0 commit comments