/home/runner/work/kynema-sgf/kynema-sgf/src/wind_energy/actuator/turbine/external/turbine_external_utils.H Source File

Kynema-SGF API: /home/runner/work/kynema-sgf/kynema-sgf/src/wind_energy/actuator/turbine/external/turbine_external_utils.H Source File
Kynema-SGF API v0.1.0
CFD solver for wind plant simulations
Loading...
Searching...
No Matches
turbine_external_utils.H
Go to the documentation of this file.
1#ifndef TURBINE_EXTERNAL_UTILS_H
2#define TURBINE_EXTERNAL_UTILS_H
3
4#include <numbers>
11#include "AMReX_REAL.H"
12
13using namespace amrex::literals;
14
16
17// Note: inline is necessary to avoid duplicate symbol errors
18inline void swap_epsilon(vs::Vector& eps)
19{
20 const auto x = eps.x();
21 const auto y = eps.y();
22 eps.x() = y;
23 eps.y() = x;
24}
25
26template <typename datatype>
27void make_component_views(datatype& data)
28{
29 auto& grid = data.grid();
30 auto& tdata = data.meta();
31 const int num_blades = tdata.num_blades;
32 const int num_pts_blade = tdata.num_pts_blade;
33 const int num_vel_pts_blade = tdata.num_vel_pts_blade;
34 const int offset_vel = tdata.velocity_points_offset;
35
36 for (int ib = 0; ib < num_blades; ++ib) {
38
39 const auto start = ib * num_pts_blade + 1;
40 const auto start_vel = ib * num_vel_pts_blade + offset_vel;
41 // clang-format off
43 grid.pos, start, num_pts_blade);
45 grid.force, start, num_pts_blade);
47 grid.epsilon, start, num_pts_blade);
49 grid.orientation, start, num_pts_blade);
51 tdata.chord, start, num_pts_blade);
53 tdata.vel_rel, start, num_pts_blade);
55 grid.vel, start_vel, num_vel_pts_blade);
57 grid.vel_pos, start_vel, num_vel_pts_blade);
58 // clang-format on
59
60 tdata.blades.emplace_back(cv);
61 }
62 if (tdata.num_pts_tower > 0) {
63 const int num_pts_tower = tdata.num_pts_tower;
64 const int ntwr_start = num_blades * num_pts_blade + 1;
65 auto& cv = tdata.tower;
66
67 cv.pos =
68 ::kynema_sgf::utils::slice(grid.pos, ntwr_start, num_pts_tower);
69 cv.force =
70 ::kynema_sgf::utils::slice(grid.force, ntwr_start, num_pts_tower);
71 cv.epsilon =
72 ::kynema_sgf::utils::slice(grid.epsilon, ntwr_start, num_pts_tower);
73 cv.orientation = ::kynema_sgf::utils::slice(
74 grid.orientation, ntwr_start, num_pts_tower);
75 cv.chord =
76 ::kynema_sgf::utils::slice(tdata.chord, ntwr_start, num_pts_tower);
77 }
78 {
79 auto& cv = tdata.hub;
80 cv.pos = ::kynema_sgf::utils::slice(grid.pos, 0, 1);
81 cv.force = ::kynema_sgf::utils::slice(grid.force, 0, 1);
82 cv.epsilon = ::kynema_sgf::utils::slice(grid.epsilon, 0, 1);
83 cv.orientation = ::kynema_sgf::utils::slice(grid.orientation, 0, 1);
84 cv.chord = ::kynema_sgf::utils::slice(tdata.chord, 0, 1);
85 }
86}
87
88template <typename datatype>
89void init_epsilon(datatype& data)
90{
91 auto& tdata = data.meta();
92
93 // Swap order of epsilon based on turbine orientation
94 // Input order is (chord, span, thickness)
95 // Output order should depend on turbine type; currently ...
96 // -- this function is correct for Kynema
97 // -- this function is erroneous for OpenFAST but has been in use
98 // (Does not matter when epsilon is uniform)
99 swap_epsilon(tdata.eps_inp);
100 swap_epsilon(tdata.eps_min);
101 swap_epsilon(tdata.eps_chord);
102 swap_epsilon(tdata.eps_tower);
103
104 {
105 const auto& cd = tdata.nacelle_cd;
106 const auto& area = tdata.nacelle_area;
107 const auto eps =
108 std::sqrt(2.0_rt / std::numbers::pi_v<amrex::Real> * cd * area);
109
110 auto& nac_eps = data.grid().epsilon[0];
111 nac_eps.x() = amrex::max<amrex::Real>(eps, tdata.eps_min.x());
112 nac_eps.y() = amrex::max<amrex::Real>(eps, tdata.eps_min.y());
113 nac_eps.z() = amrex::max<amrex::Real>(eps, tdata.eps_min.z());
114 }
115
116 for (int ib = 0; ib < tdata.num_blades; ++ib) {
117 auto& cv = tdata.blades[ib];
118
119 for (int i = 0; i < tdata.num_pts_blade; ++i) {
120 const auto eps_crd = tdata.eps_chord * cv.chord[i];
121
122 for (int n = 0; n < AMREX_SPACEDIM; ++n) {
123 cv.epsilon[i][n] = amrex::max<amrex::Real>(
124 tdata.eps_min[n], tdata.eps_inp[n], eps_crd[n]);
125 }
126 }
127 }
128 {
129 auto& cv = tdata.tower;
130 for (int i = 0; i < tdata.num_pts_tower; ++i) {
131 for (int n = 0; n < AMREX_SPACEDIM; ++n) {
132 cv.epsilon[i][n] = amrex::max<amrex::Real>(
133 tdata.eps_min[n], tdata.eps_inp[n], tdata.eps_tower[n]);
134 }
135 }
136 }
137}
138
139template <typename datatype>
140void compute_nacelle_force(datatype& data)
141{
142 if (!data.info().is_root_proc) {
143 return;
144 }
145
146 const auto& cd = data.meta().nacelle_cd;
147 const auto& area = data.meta().nacelle_area;
148 const auto& cd_area = cd * area;
149 const auto& ext_tdata = data.meta().ext_data;
150 const auto& rho = data.meta().density;
151
152 const auto& eps = data.grid().epsilon[0].x();
153 // This assumes a static nacelle
154 vs::Vector vel{
155 ext_tdata.fluid_velocity(0)[0], ext_tdata.fluid_velocity(1)[0],
156 ext_tdata.fluid_velocity(2)[0]};
157 amrex::Real correction = 0.0_rt;
158 if (eps > 0.0_rt) {
159 amrex::Real fac =
160 1.0_rt -
161 (cd_area) / (2.0_rt * ::kynema_sgf::utils::two_pi() * eps * eps);
162 correction = 1.0_rt / fac;
163 }
164 amrex::Real coeff =
165 0.5_rt * rho * cd_area * vs::mag(vel) * correction * correction;
166
167 for (int dir = 0; dir < AMREX_SPACEDIM; ++dir) {
168 ext_tdata.force(dir)[0] = static_cast<float>(coeff * vel[dir]);
169 }
170}
171
172template <typename datatype>
173void ext_step(datatype& data)
174{
175 if (!data.info().is_root_proc) {
176 return;
177 }
178
179 auto& meta = data.meta();
180 auto& tf = data.meta().ext_data;
181 if (tf.is_solution0) {
182 meta.ext_ptr->init_solution(tf.tid_local);
183 } else {
184 meta.ext_ptr->advance_turbine(tf.tid_local);
185 }
186
187 meta.ext_ptr->get_hub_stats(tf.tid_local);
188
189 // Populate nacelle force into the OpenFAST data structure so that it
190 // gets broadcasted to all influenced processes in subsequent scattering
191 // of data.
193}
194
195template <typename datatype>
196void scatter_data(datatype& data)
197{
198 if (!data.info().actuator_in_proc) {
199 return;
200 }
201
202 // Create an MPI transfer buffer that packs all data in one contiguous
203 // array. 3 floats for the position vector, 3 floats for the force
204 // vector, and 9 floats for the orientation matrix = 15 floats per
205 // actuator node.
206 const auto dsize = data.grid().pos.size() * 15;
207 amrex::Vector<float> buf(dsize);
208
209 // Copy data into MPI send/recv buffer from the OpenFAST data structure.
210 // Note, other procs do not have a valid data in those pointers.
211 if (data.info().is_root_proc) {
212 BL_PROFILE(
213 "kynema-sgf::actuator::external::compute_force_op::scatter1");
214 const auto& ext_tdata = data.meta().ext_data;
215 auto it = buf.begin();
216 for (int dir = 0; dir < AMREX_SPACEDIM; ++dir) {
217 std::copy(
218 ext_tdata.force(dir),
219 ext_tdata.force(dir) + ext_tdata.length_force(dir), it);
220 std::advance(it, ext_tdata.length_force(dir));
221 }
222 for (int dir = 0; dir < AMREX_SPACEDIM; ++dir) {
223 std::copy(
224 ext_tdata.position_at_force(dir),
225 ext_tdata.position_at_force(dir) +
226 ext_tdata.length_position_at_force(dir),
227 it);
228 std::advance(it, ext_tdata.length_position_at_force(dir));
229 }
230
231 // clang-format off
232 std::copy(ext_tdata.orientation(),
233 ext_tdata.orientation() + ext_tdata.length_orientation(), it);
234 // clang-format on
235 }
236
237 // Broadcast data to all influenced procs from the root process
238 const auto& procs = data.info().procs;
239 const int tag = 1001;
240 if (data.info().is_root_proc) {
241 BL_PROFILE(
242 "kynema-sgf::actuator::external::compute_force_op::scatter2");
243 for (const int ip : procs) {
244 if (ip == data.info().root_proc) {
245 continue;
246 }
247
248 amrex::ParallelDescriptor::Send(
249 buf.data(), dsize, ip, tag, data.meta().tcomm);
250 }
251 } else {
252 BL_PROFILE(
253 "kynema-sgf::actuator::external::compute_force_op::scatter2");
254 amrex::ParallelDescriptor::Recv(
255 buf.data(), dsize, data.info().root_proc, tag, data.meta().tcomm);
256 }
257
258 // Populate the actuator grid data structures with data from the MPI
259 // send/recv buffer.
260 {
261 BL_PROFILE(
262 "kynema-sgf::actuator::external::compute_force_op::scatter3");
263 const auto& bp = data.info().base_pos;
264 auto& grid = data.grid();
265 const auto& npts = grid.pos.size();
266 const auto& rho = data.meta().density;
267 const size_t ifx = 0;
268 const size_t ify = ifx + npts;
269 const size_t ifz = ify + npts;
270 const size_t ipx = ifz + npts;
271 const size_t ipy = ipx + npts;
272 const size_t ipz = ipy + npts;
273 const size_t iori = ipz + npts;
274
275 for (int i = 0; i < npts; ++i) {
276 // Aerodynamic force vectors. Flip sign to get force on fluid.
277 // Divide by density as the source term computation will
278 // multiply by density before adding to momentum equation.
279 //
280 grid.force[i].x() = -static_cast<amrex::Real>(buf[ifx + i]) / rho;
281 grid.force[i].y() = -static_cast<amrex::Real>(buf[ify + i]) / rho;
282 grid.force[i].z() = -static_cast<amrex::Real>(buf[ifz + i]) / rho;
283
284 // Position vectors of the actuator nodes. Add shift to base
285 // locations.
286 grid.pos[i].x() = static_cast<amrex::Real>(buf[ipx + i]) + bp.x();
287 grid.pos[i].y() = static_cast<amrex::Real>(buf[ipy + i]) + bp.y();
288 grid.pos[i].z() = static_cast<amrex::Real>(buf[ipz + i]) + bp.z();
289
290 // Copy over the orientation matrix
291 //
292 // Note that we transpose the orientation matrix when copying
293 // from external to Kynema-SGF Tensor data structure. This is done
294 // so that post-multiplication of vector transforms from global
295 // to local reference frame.
296 const auto off =
297 static_cast<int>(iori) + i * AMREX_SPACEDIM * AMREX_SPACEDIM;
298 for (int j = 0; j < AMREX_SPACEDIM; ++j) {
299 for (int k = 0; k < AMREX_SPACEDIM; ++k) {
300 grid.orientation[i][j * AMREX_SPACEDIM + k] =
301 static_cast<amrex::Real>(
302 buf[off + j + k * AMREX_SPACEDIM]);
303 }
304 }
305 }
306
307 // Extract the rotor center of rotation
308 auto& meta = data.meta();
309 meta.rot_center = grid.pos[0];
310
311 // Rotor non-rotating reference frame
312 const auto xvec = grid.orientation[0].x().unit();
313 const auto yvec = vs::Vector::khat() ^ xvec;
314 const auto zvec = xvec ^ yvec;
315 meta.rotor_frame.rows(xvec, yvec.unit(), zvec.unit());
316 }
317}
318
319} // namespace kynema_sgf::actuator::external
320
321#endif /* TURBINE_EXTERNAL_UTILS_H */
Definition turbine_external_utils.H:15
void ext_step(datatype &data)
Definition turbine_external_utils.H:173
void swap_epsilon(vs::Vector &eps)
Definition turbine_external_utils.H:18
void make_component_views(datatype &data)
Definition turbine_external_utils.H:27
void compute_nacelle_force(datatype &data)
Definition turbine_external_utils.H:140
void scatter_data(datatype &data)
Definition turbine_external_utils.H:196
void init_epsilon(datatype &data)
Definition turbine_external_utils.H:89
Definition bluff_body_ops.cpp:18
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE constexpr amrex::Real two_pi()
Return .
Definition trig_ops.H:19
Slice< T > slice(std::vector< T > &vec, const size_t start, const size_t count)
Definition Slice.H:71
VectorT< amrex::Real > Vector
Definition vector.H:145
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE T mag(const TensorT< T > &t)
Definition tensorI.H:182
Definition actuator_types.H:147
TensorSlice orientation
Definition actuator_types.H:151
RealSlice chord
Definition actuator_types.H:157
VecSlice pos
Definition actuator_types.H:148
VecSlice force
Definition actuator_types.H:149
VecSlice vel_pos
Definition actuator_types.H:153
VecSlice vel_rel
Definition actuator_types.H:155
VecSlice epsilon
Definition actuator_types.H:150
VecSlice vel
Definition actuator_types.H:154
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE T & y() &
Definition vector.H:98
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE T & x() &
Definition vector.H:97
AMREX_GPU_HOST_DEVICE static AMREX_FORCE_INLINE constexpr VectorT< amrex::Real > khat(const amrex::Real &z=Traits::one())
Definition vector.H:80