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