Skip to content

Commit 0279c24

Browse files
committed
add FLOWFarm to aero models
1 parent 766675a commit 0279c24

File tree

11 files changed

+2234
-0
lines changed

11 files changed

+2234
-0
lines changed

ard/farm_aero/flowfarm.py

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
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

Comments
 (0)