PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
les.c
Go to the documentation of this file.
1/**
2 * @file les.c
3 * @brief Implements the Large Eddy Simulation (LES) subgrid-scale closure.
4 *
5 * The closure is built from small kernels that each do one thing, so the physics is
6 * readable at the call sites and testable without a running solve:
7 *
8 * - symmetric-tensor algebra (`SymTensor*`), which knows nothing about turbulence;
9 * - strain rate and filter width, shared by every eddy-viscosity model;
10 * - the Germano identity pieces, `LeonardStress()` and `GermanoModelTensor()`;
11 * - the coefficient averaging, which is `PicurvSpatialRatioAverage()` from the shared
12 * spatial-target module: it turns the local dynamic model into the plane- or
13 * volume-averaged one without a separate code path per mode;
14 * - limiting and eddy-viscosity assembly.
15 *
16 * Two routines drive them. `ComputeSmagorinskyConstant()` runs the Germano-Lilly
17 * procedure for the dynamic model, and `ComputeEddyViscosityLES()` turns whichever
18 * coefficient is in force into the eddy viscosity the momentum equations add to the
19 * molecular one. `LogLESDiagnostics()` reports the coefficient's time history.
20 *
21 * The coefficient stored in `UserCtx::CS` is `C`, the factor multiplying
22 * `Delta^2 |S|`, which is `Cs^2` in the classical notation and is signed when
23 * backscatter is admitted. Conversion to the familiar `Cs` happens only where a
24 * human reads the number.
25 */
26
27#include "les.h"
28#include "statistics_target.h"
29
30// A small constant to prevent division by zero in sensitive calculations.
31static const double LES_EPSILON = 1.0e-12;
32
33/*================================================================================*
34 * INTERNAL (STATIC) FUNCTIONS *
35 *================================================================================*/
36
37/**
38 * @brief Synchronizes the completed Smagorinsky coefficient field.
39 */
40static PetscErrorCode FinalizeSmagorinskyConstantField(UserCtx *user)
41{
42 const FieldId fields[] = {FIELD_ID_CS};
43
44 PetscFunctionBeginUser;
45 PetscCall(SynchronizePeriodicCellFields(user, 1, fields));
46 PetscCall(UpdateLocalGhosts(user, FIELD_ID_CS));
47 PetscFunctionReturn(0);
48}
49
50/**
51 * @brief Resolves the iteration domain the closure computes over.
52 *
53 * Delegates to the shared spatial target so the closure, the statistics pipeline, and
54 * anything else looping over cell-centred data agree on which indices are physical.
55 * The domain excludes PETSc halo storage and the solver's boundary, dummy, and
56 * duplicate-periodic indices; the last of those matters here, because counting a
57 * duplicate plane in a spatial average would weight a real cell twice.
58 *
59 * The plan is resolved for `CS` as a representative cell-centred field. The bounds
60 * depend on the layout and the block's periodicity, not on which field is being
61 * iterated, so the scratch vectors this module allocates share them.
62 */
63static PetscErrorCode LESResolveDomain(UserCtx *user, SpatialTargetPlan *plan)
64{
65 PetscFunctionBeginUser;
67 PetscFunctionReturn(0);
68}
69
70/**
71 * @brief Marks the near-wall cells a wall model overwrites, so the procedure skips them.
72 *
73 * A wall model replaces the first interior cell's velocity with an analytic profile.
74 * The Germano identity evaluated there measures that imposed profile rather than
75 * resolved turbulence, so the cell is excluded from both contraction sums. This is not
76 * a modelling preference: the data at those cells is not a measurement, and averaging it
77 * in biases whatever set it belongs to. Under global averaging that set is the whole
78 * domain; under homogeneous averaging retaining the wall-normal direction it is the wall
79 * plane, whose value would otherwise be entirely imposed.
80 *
81 * A cell left with no data receives a zero coefficient, which is the right answer where
82 * a wall model is already supplying the stress.
83 *
84 * Only meaningful while a wall model is active; with none, nothing is imposed and every
85 * cell carries a real measurement.
86 */
87static PetscErrorCode BuildWallModelExclusionMask(UserCtx *user, Vec inclusion)
88{
89 const DMDALocalInfo info = user->info;
90 const BCFace negative[3] = {BC_FACE_NEG_X, BC_FACE_NEG_Y, BC_FACE_NEG_Z};
91 const BCFace positive[3] = {BC_FACE_POS_X, BC_FACE_POS_Y, BC_FACE_POS_Z};
92 const PetscInt extent[3] = {info.mx, info.my, info.mz};
93 PetscReal ***mask = NULL;
95
96 PetscFunctionBeginUser;
97 PetscCall(VecSet(inclusion, 1.0));
98 PetscCall(LESResolveDomain(user, &plan));
99 PetscCall(DMDAVecGetArray(user->da, inclusion, &mask));
100 for (PetscInt k = plan.start[2]; k < plan.end[2]; k++)
101 for (PetscInt j = plan.start[1]; j < plan.end[1]; j++)
102 for (PetscInt i = plan.start[0]; i < plan.end[0]; i++) {
103 const PetscInt coord[3] = {i, j, k};
104
105 for (PetscInt axis = 0; axis < 3; axis++) {
106 /* Index 1 and extent-2 are the first interior cells the wall model writes. */
107 if ((user->boundary_faces[negative[axis]].mathematical_type == WALL &&
108 coord[axis] == 1) ||
109 (user->boundary_faces[positive[axis]].mathematical_type == WALL &&
110 coord[axis] == extent[axis] - 2)) {
111 mask[k][j][i] = 0.0;
112 break;
113 }
114 }
115 }
116 PetscCall(DMDAVecRestoreArray(user->da, inclusion, &mask));
117 PetscFunctionReturn(0);
118}
119
120/**
121 * @brief Reports the block's per-axis periodicity in `(xi, eta, zeta)` order.
122 *
123 * Reads the resolved global flags rather than the boundary face configuration. The two
124 * cannot disagree: the flags are derived from the boundary files, and the loader
125 * rejects a case whose opposite faces or whose blocks do not agree on periodicity. The
126 * flags are also what the DMDA itself was built from, and what the shared spatial
127 * target consults, so taking them here keeps one source of truth.
128 */
129static void LESPeriodicAxes(const SimCtx *simCtx, PetscBool periodic[3])
130{
131 periodic[0] = (PetscBool)(simCtx->i_periodic != 0);
132 periodic[1] = (PetscBool)(simCtx->j_periodic != 0);
133 periodic[2] = (PetscBool)(simCtx->k_periodic != 0);
134}
135
136#undef __FUNCT__
137#define __FUNCT__ "SymTensorSelfOuter"
138/**
139 * @brief Implementation of \ref SymTensorSelfOuter().
140 * @details Full API contract is documented with the header declaration in
141 * `include/les.h`.
142 * @see SymTensorSelfOuter()
143 */
145{
146 SymTensor t;
147
148 t.xx = v.x * v.x;
149 t.xy = v.x * v.y;
150 t.xz = v.x * v.z;
151 t.yy = v.y * v.y;
152 t.yz = v.y * v.z;
153 t.zz = v.z * v.z;
154 return t;
155}
156
157#undef __FUNCT__
158#define __FUNCT__ "SymTensorCombine"
159/**
160 * @brief Implementation of \ref SymTensorCombine().
161 * @details Full API contract is documented with the header declaration in
162 * `include/les.h`.
163 * @see SymTensorCombine()
164 */
165SymTensor SymTensorCombine(PetscReal a, SymTensor x, PetscReal b, SymTensor y)
166{
167 SymTensor t;
168
169 t.xx = a * x.xx + b * y.xx;
170 t.xy = a * x.xy + b * y.xy;
171 t.xz = a * x.xz + b * y.xz;
172 t.yy = a * x.yy + b * y.yy;
173 t.yz = a * x.yz + b * y.yz;
174 t.zz = a * x.zz + b * y.zz;
175 return t;
176}
177
178#undef __FUNCT__
179#define __FUNCT__ "SymTensorTrace"
180/**
181 * @brief Implementation of \ref SymTensorTrace().
182 * @details Full API contract is documented with the header declaration in
183 * `include/les.h`.
184 * @see SymTensorTrace()
185 */
187{
188 return t.xx + t.yy + t.zz;
189}
190
191#undef __FUNCT__
192#define __FUNCT__ "SymTensorDeviator"
193/**
194 * @brief Implementation of \ref SymTensorDeviator().
195 * @details Full API contract is documented with the header declaration in
196 * `include/les.h`.
197 * @see SymTensorDeviator()
198 */
200{
201 const PetscReal isotropic = SymTensorTrace(t) / 3.0;
202
203 t.xx -= isotropic;
204 t.yy -= isotropic;
205 t.zz -= isotropic;
206 return t;
207}
208
209#undef __FUNCT__
210#define __FUNCT__ "SymTensorContract"
211/**
212 * @brief Implementation of \ref SymTensorContract().
213 * @details Full API contract is documented with the header declaration in
214 * `include/les.h`.
215 * @see SymTensorContract()
216 */
218{
219 // The three off-diagonal components each stand for two entries of the full tensor.
220 return a.xx * b.xx + a.yy * b.yy + a.zz * b.zz +
221 2.0 * (a.xy * b.xy + a.xz * b.xz + a.yz * b.yz);
222}
223
224#undef __FUNCT__
225#define __FUNCT__ "SymTensorTimesVector"
226/**
227 * @brief Implementation of \ref SymTensorTimesVector().
228 * @details Full API contract is documented with the header declaration in
229 * `include/les.h`.
230 * @see SymTensorTimesVector()
231 */
233{
234 Cmpnts result;
235
236 result.x = t.xx * v.x + t.xy * v.y + t.xz * v.z;
237 result.y = t.xy * v.x + t.yy * v.y + t.yz * v.z;
238 result.z = t.xz * v.x + t.yz * v.y + t.zz * v.z;
239 return result;
240}
241
242#undef __FUNCT__
243#define __FUNCT__ "SymTensorNormSq"
244/**
245 * @brief Implementation of \ref SymTensorNormSq().
246 * @details Full API contract is documented with the header declaration in
247 * `include/les.h`.
248 * @see SymTensorNormSq()
249 */
251{
252 return SymTensorContract(t, t);
253}
254
255/*================================================================================*
256 * STRAIN RATE AND FILTER WIDTH *
257 *================================================================================*/
258
259#undef __FUNCT__
260#define __FUNCT__ "StrainRateFromGradients"
261/**
262 * @brief Implementation of \ref StrainRateFromGradients().
263 * @details Full API contract is documented with the header declaration in
264 * `include/les.h`.
265 * @see StrainRateFromGradients()
266 */
267PetscErrorCode StrainRateFromGradients(Cmpnts dudx, Cmpnts dvdx, Cmpnts dwdx,
268 SymTensor *strain, PetscReal *magnitude)
269{
270 SymTensor s;
271
272 PetscFunctionBeginUser;
273
274 s.xx = dudx.x;
275 s.xy = 0.5 * (dudx.y + dvdx.x);
276 s.xz = 0.5 * (dudx.z + dwdx.x);
277 s.yy = dvdx.y;
278 s.yz = 0.5 * (dvdx.z + dwdx.y);
279 s.zz = dwdx.z;
280
281 if (strain) *strain = s;
282 // |S| = sqrt(2 S_ij S_ij), with the off-diagonal doubling handled by the contraction.
283 if (magnitude) *magnitude = PetscSqrtReal(2.0 * SymTensorNormSq(s));
284
285 PetscFunctionReturn(0);
286}
287
288#undef __FUNCT__
289#define __FUNCT__ "ComputeCellFilterWidth"
290/**
291 * @brief Implementation of \ref ComputeCellFilterWidth().
292 * @details Full API contract is documented with the header declaration in
293 * `include/les.h`.
294 * @see ComputeCellFilterWidth()
295 */
296PetscErrorCode ComputeCellFilterWidth(LESFilterWidthModel model, PetscReal aj,
297 Cmpnts csi, Cmpnts eta, Cmpnts zet, PetscReal *delta)
298{
299 double dx = 0.0, dy = 0.0, dz = 0.0;
300
301 PetscFunctionBeginUser;
302 PetscCheck(delta != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL,
303 "Filter width destination cannot be NULL.");
304 PetscCheck(aj > 0.0, PETSC_COMM_SELF, PETSC_ERR_ARG_OUTOFRANGE,
305 "Cell Jacobian must be positive to define a filter width; received %g.", (double)aj);
306
308 // Cell volume alone: exact for a cube, and increasingly optimistic as the
309 // cell is stretched, because it averages the three extents geometrically.
310 *delta = PetscPowReal(1.0 / aj, 1.0 / 3.0);
311 PetscFunctionReturn(0);
312 }
313
314 // The cell's own extents, not the Cartesian components of its diagonal: a filter
315 // width that changed when the same cell was rotated would hand a bend nine times the
316 // eddy viscosity of an identical cell in a straight section.
317 PetscCall(ComputeCellDirectionalExtents(aj, csi, eta, zet, &dx, &dy, &dz));
318
319 switch (model) {
321 // The largest unresolved scale the cell can carry; conservative on stretched grids.
322 *delta = PetscMax(dx, PetscMax(dy, dz));
323 break;
325 // Scotti, Meneveau & Lilly (1993): the cube root of the extents, multiplied by
326 // f(a1, a2), where a1 and a2 are the two shorter extents over the longest. f is 1
327 // for a cube and grows with the cell's distortion. It assumes the cut-off lies in
328 // the inertial range in every direction, which a very long cell violates.
329 const double longest = PetscMax(dx, PetscMax(dy, dz));
330 const double shortest = PetscMin(dx, PetscMin(dy, dz));
331 const double middle = dx + dy + dz - longest - shortest;
332 const double ln_a1 = log(shortest / longest), ln_a2 = log(middle / longest);
333 const double shape = cosh(sqrt((4.0 / 27.0) *
334 (ln_a1 * ln_a1 - ln_a1 * ln_a2 + ln_a2 * ln_a2)));
335 *delta = PetscPowReal(PetscMax(dx * dy * dz, LES_EPSILON), 1.0 / 3.0) * shape;
336 break;
337 }
339 default:
340 *delta = PetscPowReal(PetscMax(dx * dy * dz, LES_EPSILON), 1.0 / 3.0);
341 break;
342 }
343
344 PetscFunctionReturn(0);
345}
346
347/*================================================================================*
348 * GERMANO IDENTITY KERNELS *
349 *================================================================================*/
350
351#undef __FUNCT__
352#define __FUNCT__ "LeonardStress"
353/**
354 * @brief Implementation of \ref LeonardStress().
355 * @details Full API contract is documented with the header declaration in
356 * `include/les.h`.
357 * @see LeonardStress()
358 */
359SymTensor LeonardStress(Cmpnts velocity_filtered, SymTensor velocity_product_filtered)
360{
361 // L_ij = (u_i u_j)^ - u^_i u^_j : the stress carried between the two filter widths.
362 return SymTensorCombine(1.0, velocity_product_filtered,
363 -1.0, SymTensorSelfOuter(velocity_filtered));
364}
365
366#undef __FUNCT__
367#define __FUNCT__ "GermanoModelTensor"
368/**
369 * @brief Implementation of \ref GermanoModelTensor().
370 * @details Full API contract is documented with the header declaration in
371 * `include/les.h`.
372 * @see GermanoModelTensor()
373 */
374SymTensor GermanoModelTensor(PetscReal delta, PetscReal alpha,
375 PetscReal strain_magnitude_filtered,
376 SymTensor strain_filtered,
377 SymTensor strain_product_filtered)
378{
379 // Term one: the model evaluated on the test-filtered strain. Both factors were
380 // filtered separately, then multiplied.
381 const SymTensor test_scale = SymTensorCombine(alpha * strain_magnitude_filtered,
382 strain_filtered, 0.0, strain_filtered);
383
384 // Term two: the grid-scale model formed pointwise and then test-filtered. This is
385 // the filter of a product, and it is not equal to the product of the filtered
386 // factors above. That inequality carries the whole information content of the
387 // dynamic procedure.
388 const SymTensor grid_scale = strain_product_filtered;
389
390 const SymTensor model = SymTensorCombine(-2.0 * delta * delta, test_scale,
391 2.0 * delta * delta, grid_scale);
392
393 // Trace-free, so the Leonard stress's trace cannot enter the contraction through
394 // whatever discrete divergence the velocity field carries.
395 return SymTensorDeviator(model);
396}
397
398/*================================================================================*
399 * COEFFICIENT AVERAGING AND LIMITING *
400 *================================================================================*/
401
402#undef __FUNCT__
403#define __FUNCT__ "ResolveLESAveragingDirections"
404/**
405 * @brief Implementation of \ref ResolveLESAveragingDirections().
406 * @details Full API contract is documented with the header declaration in
407 * `include/les.h`.
408 * @see ResolveLESAveragingDirections()
409 */
410PetscErrorCode ResolveLESAveragingDirections(UserCtx *user, PetscBool direction[3])
411{
412 const LESConfig *config = &user->simCtx->les_config;
413 PetscInt selected = 0;
414 PetscBool periodic[3];
415
416 PetscFunctionBeginUser;
417 PetscCheck(direction != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL,
418 "Averaging direction destination cannot be NULL.");
419 LESPeriodicAxes(user->simCtx, periodic);
420
421 for (PetscInt axis = 0; axis < 3; axis++) direction[axis] = PETSC_FALSE;
422
423 switch (config->averaging_mode) {
425 // No averaging: the coefficient is whatever the single cell's contractions give.
426 PetscFunctionReturn(0);
427
429 for (PetscInt axis = 0; axis < 3; axis++) direction[axis] = PETSC_TRUE;
430 PetscFunctionReturn(0);
431
433 default:
434 for (PetscInt axis = 0; axis < 3; axis++) {
435 if (config->averaging_direction[axis]) {
436 direction[axis] = PETSC_TRUE;
437 selected++;
438 }
439 }
440 // Naming no direction asks for the periodic axes, which is where the flow is
441 // homogeneous in every case the solver can currently express.
442 if (selected == 0) {
443 for (PetscInt axis = 0; axis < 3; axis++) {
444 if (periodic[axis]) {
445 direction[axis] = PETSC_TRUE;
446 selected++;
447 }
448 }
449 }
450 PetscCheck(selected > 0, PETSC_COMM_SELF, PETSC_ERR_ARG_WRONGSTATE,
451 "Homogeneous LES averaging was requested, but this block declares no "
452 "periodic axis and the configuration names no direction. Name the "
453 "homogeneous directions explicitly or select local averaging.");
454 PetscFunctionReturn(0);
455 }
456}
457
458#undef __FUNCT__
459#define __FUNCT__ "ClipModelCoefficient"
460/**
461 * @brief Implementation of \ref ClipModelCoefficient().
462 * @details Full API contract is documented with the header declaration in
463 * `include/les.h`.
464 * @see ClipModelCoefficient()
465 */
466PetscReal ClipModelCoefficient(PetscReal coefficient, const LESConfig *config, PetscBool *limited)
467{
468 PetscReal result = coefficient;
469
470 switch (config->clip_mode) {
471 case LES_CLIP_NONE:
472 // Signed coefficient retained: backscatter survives, and stability is left to
473 // the total-viscosity floor rather than to the coefficient's sign.
474 break;
476 if (result < 0.0) result = 0.0;
477 break;
478 case LES_CLIP_CLAMP:
479 default:
480 if (result < 0.0) result = 0.0;
481 if (result > config->max_cs * config->max_cs) result = config->max_cs * config->max_cs;
482 break;
483 }
484
485 if (limited) *limited = (PetscBool)(result != coefficient);
486 return result;
487}
488
489#undef __FUNCT__
490#define __FUNCT__ "EddyViscosityFromCoefficient"
491/**
492 * @brief Implementation of \ref EddyViscosityFromCoefficient().
493 * @details Full API contract is documented with the header declaration in
494 * `include/les.h`.
495 * @see EddyViscosityFromCoefficient()
496 */
497PetscReal EddyViscosityFromCoefficient(PetscReal coefficient, PetscReal delta,
498 PetscReal strain_magnitude,
499 PetscReal molecular_viscosity,
500 PetscReal min_viscosity_ratio)
501{
502 PetscReal nu_t = coefficient * delta * delta * strain_magnitude;
503
504 // A negative eddy viscosity is physical up to the point where it would drive the
505 // total viscosity non-positive and make the momentum operator ill posed.
506 const PetscReal floor = (min_viscosity_ratio - 1.0) * molecular_viscosity;
507
508 if (nu_t < floor) nu_t = floor;
509 return nu_t;
510}
511
512#undef __FUNCT__
513#define __FUNCT__ "WALEEddyViscosity"
514/**
515 * @brief Implementation of \ref WALEEddyViscosity().
516 * @details Full API contract is documented with the header declaration in
517 * `include/les.h`.
518 * @see WALEEddyViscosity()
519 */
520PetscReal WALEEddyViscosity(Cmpnts dudx, Cmpnts dvdx, Cmpnts dwdx,
521 PetscReal delta, PetscReal coefficient)
522{
523 const PetscReal g[3][3] = {{dudx.x, dudx.y, dudx.z},
524 {dvdx.x, dvdx.y, dvdx.z},
525 {dwdx.x, dwdx.y, dwdx.z}};
526 PetscReal g2[3][3], sd[3][3], trace = 0.0, ss = 0.0, sdsd = 0.0, denominator;
527
528 for (PetscInt a = 0; a < 3; ++a)
529 for (PetscInt b = 0; b < 3; ++b) {
530 g2[a][b] = 0.0;
531 for (PetscInt c = 0; c < 3; ++c) g2[a][b] += g[a][c] * g[c][b];
532 }
533 for (PetscInt a = 0; a < 3; ++a) trace += g2[a][a];
534 for (PetscInt a = 0; a < 3; ++a)
535 for (PetscInt b = 0; b < 3; ++b) {
536 const PetscReal s = 0.5 * (g[a][b] + g[b][a]);
537 // Traceless symmetric part of the squared gradient: it vanishes in pure shear,
538 // which is what lets the model switch itself off in a laminar shear layer.
539 sd[a][b] = 0.5 * (g2[a][b] + g2[b][a]) - (a == b ? trace / 3.0 : 0.0);
540 ss += s * s;
541 sdsd += sd[a][b] * sd[a][b];
542 }
543
544 denominator = PetscPowReal(ss, 2.5) + PetscPowReal(sdsd, 1.25);
545 if (denominator <= LES_EPSILON * LES_EPSILON) return 0.0;
546 return coefficient * coefficient * delta * delta * PetscPowReal(sdsd, 1.5) / denominator;
547}
548
549#undef __FUNCT__
550#define __FUNCT__ "VremanEddyViscosity"
551/**
552 * @brief Implementation of \ref VremanEddyViscosity().
553 * @details Full API contract is documented with the header declaration in
554 * `include/les.h`.
555 * @see VremanEddyViscosity()
556 */
557PetscReal VremanEddyViscosity(Cmpnts dudx, Cmpnts dvdx, Cmpnts dwdx,
558 const Cmpnts edges[3], PetscReal coefficient)
559{
560 const Cmpnts gradient[3] = {dudx, dvdx, dwdx};
561 PetscReal beta[3][3] = {{0.0}}, alpha_sq = 0.0, b_beta;
562
563 // Vreman's beta_ij = sum_m Delta_m^2 alpha_mi alpha_mj on a Cartesian grid, where
564 // Delta_m alpha_mi is the change in u_i across the cell in direction m. On a
565 // curvilinear grid that change is edge_m . grad(u_i), so the same sum over the
566 // cell's own directions reproduces the Cartesian form on an aligned grid and does
567 // not depend on the grid's orientation elsewhere.
568 for (PetscInt m = 0; m < 3; ++m) {
569 PetscReal change[3];
570 for (PetscInt a = 0; a < 3; ++a)
571 change[a] = edges[m].x * gradient[a].x + edges[m].y * gradient[a].y + edges[m].z * gradient[a].z;
572 for (PetscInt a = 0; a < 3; ++a)
573 for (PetscInt b = 0; b < 3; ++b) beta[a][b] += change[a] * change[b];
574 }
575 for (PetscInt a = 0; a < 3; ++a)
576 alpha_sq += gradient[a].x * gradient[a].x + gradient[a].y * gradient[a].y + gradient[a].z * gradient[a].z;
577
578 b_beta = beta[0][0] * beta[1][1] - beta[0][1] * beta[0][1]
579 + beta[0][0] * beta[2][2] - beta[0][2] * beta[0][2]
580 + beta[1][1] * beta[2][2] - beta[1][2] * beta[1][2];
581
582 // B_beta is non-negative in exact arithmetic; rounding can leave it a hair below.
583 if (alpha_sq <= LES_EPSILON * LES_EPSILON || b_beta <= 0.0) return 0.0;
584 return coefficient * PetscSqrtReal(b_beta / alpha_sq);
585}
586
587#undef __FUNCT__
588#define __FUNCT__ "SubgridKineticEnergy"
589/**
590 * @brief Implementation of \ref SubgridKineticEnergy().
591 * @details Full API contract is documented with the header declaration in
592 * `include/les.h`.
593 * @see SubgridKineticEnergy()
594 */
595PetscReal SubgridKineticEnergy(PetscReal yoshizawa_ci, PetscReal delta, PetscReal strain_magnitude)
596{
597 return 2.0 * yoshizawa_ci * delta * delta * strain_magnitude * strain_magnitude;
598}
599
600/*================================================================================*
601 * DYNAMIC PROCEDURE INTERNALS *
602 *================================================================================*/
603
604/**
605 * @brief Maps a cell index to the index whose strain rate represents it.
606 *
607 * The strain rate must be known one cell beyond the computed range, because the
608 * test-filter stencil reaches there. At an interior index the answer is the index
609 * itself. At the two layout-boundary planes it is not, and for opposite reasons.
610 *
611 * On a periodic axis, index 0 and index `m-1` are duplicates of the last and first
612 * interior cells. A central difference evaluated at index 0 straddles the wrap and
613 * returns nonsense, so the strain is evaluated instead at ghost index `-2`, which
614 * addresses the same physical cell from the interior side. Index `m+1` plays the
615 * same role at the other end. These are the ghost indices the periodic cell
616 * synchronization itself copies from, and the DMDA carries the three ghost layers
617 * they need whenever any axis is periodic.
618 *
619 * On a non-periodic axis the two planes are the solver's boundary layer, where a
620 * central difference would reach outside the domain. The nearest interior cell's
621 * strain is used instead, which is a zeroth-order extrapolation into a layer that
622 * only ever contributes to a filter average.
623 */
624static PetscInt LESStrainSampleIndex(PetscInt index, PetscInt extent, PetscBool periodic)
625{
626 if (index == 0) return periodic ? -2 : 1;
627 if (index == extent - 1) return periodic ? extent + 1 : extent - 2;
628 return index;
629}
630
631/**
632 * @brief Fills the strain-rate scratch fields across the computed range and its halo.
633 */
634static PetscErrorCode ComputeStrainRateField(UserCtx *user, Cmpnts ***ucat,
635 Vec strain_diagonal, Vec strain_offdiagonal,
636 Vec strain_magnitude)
637{
638 DM da = user->da, fda = user->fda;
639 DMDALocalInfo info;
641 PetscBool periodic[3];
642 Cmpnts ***diagonal = NULL, ***offdiagonal = NULL;
643 PetscReal ***magnitude = NULL;
644
645 PetscFunctionBeginUser;
646
647 PetscCall(DMDAGetLocalInfo(da, &info));
648 PetscCall(LESResolveDomain(user, &plan));
649 LESPeriodicAxes(user->simCtx, periodic);
650
651 PetscCall(DMDAVecGetArray(fda, strain_diagonal, &diagonal));
652 PetscCall(DMDAVecGetArray(fda, strain_offdiagonal, &offdiagonal));
653 PetscCall(DMDAVecGetArray(da, strain_magnitude, &magnitude));
654
655 // One cell beyond the computed range in every direction, because that is how far
656 // the 3x3x3 test-filter stencil reaches from the outermost computed cell.
657 for (PetscInt k = plan.start[2] - 1; k <= plan.end[2]; k++)
658 for (PetscInt j = plan.start[1] - 1; j <= plan.end[1]; j++)
659 for (PetscInt i = plan.start[0] - 1; i <= plan.end[0]; i++) {
660 const PetscInt si = LESStrainSampleIndex(i, info.mx, periodic[0]);
661 const PetscInt sj = LESStrainSampleIndex(j, info.my, periodic[1]);
662 const PetscInt sk = LESStrainSampleIndex(k, info.mz, periodic[2]);
663
664 Cmpnts dudx, dvdx, dwdx;
665 SymTensor strain;
666 PetscReal strain_abs;
667
668 PetscCall(ComputeVectorFieldDerivatives(user, si, sj, sk, ucat, &dudx, &dvdx, &dwdx));
669 PetscCall(StrainRateFromGradients(dudx, dvdx, dwdx, &strain, &strain_abs));
670
671 diagonal[k][j][i].x = strain.xx;
672 diagonal[k][j][i].y = strain.yy;
673 diagonal[k][j][i].z = strain.zz;
674 offdiagonal[k][j][i].x = strain.xy;
675 offdiagonal[k][j][i].y = strain.xz;
676 offdiagonal[k][j][i].z = strain.yz;
677 magnitude[k][j][i] = strain_abs;
678 }
679
680 PetscCall(DMDAVecRestoreArray(fda, strain_diagonal, &diagonal));
681 PetscCall(DMDAVecRestoreArray(fda, strain_offdiagonal, &offdiagonal));
682 PetscCall(DMDAVecRestoreArray(da, strain_magnitude, &magnitude));
683
684 PetscFunctionReturn(0);
685}
686
687/**
688 * @brief Rebuilds a symmetric strain tensor from its two scratch storage vectors.
689 */
690static inline SymTensor LESStrainAt(Cmpnts diagonal, Cmpnts offdiagonal)
691{
692 SymTensor s;
693
694 s.xx = diagonal.x;
695 s.yy = diagonal.y;
696 s.zz = diagonal.z;
697 s.xy = offdiagonal.x;
698 s.xz = offdiagonal.y;
699 s.yz = offdiagonal.z;
700 return s;
701}
702
703/*================================================================================*
704 * RUNTIME ENTRY POINTS *
705 *================================================================================*/
706
707#undef __FUNCT__
708#define __FUNCT__ "ComputeSmagorinskyConstant"
709/**
710 * @brief Implementation of \ref ComputeSmagorinskyConstant().
711 * @details Full API contract (arguments, ownership, side effects) is documented with
712 * the header declaration in `include/les.h`.
713 * @see ComputeSmagorinskyConstant()
714 */
716{
717 SimCtx *simCtx = user->simCtx;
718 const LESConfig *config = &simCtx->les_config;
719 DM da = user->da, fda = user->fda;
721 PetscBool average_direction[3];
722 PetscReal alpha;
723
724 Vec lStrainDiag, lStrainOff, lStrainAbs, lLM, lMM, lCsq;
725 Vec lWallMask = NULL;
726
727 Cmpnts ***ucat = NULL, ***strain_diag = NULL, ***strain_off = NULL;
728 Cmpnts ***csi = NULL, ***eta = NULL, ***zet = NULL;
729 PetscReal ***strain_abs = NULL, ***nvert = NULL, ***aj = NULL;
730 PetscReal ***lm = NULL, ***mm = NULL, ***csq = NULL, ***cs_out = NULL;
731
732 LESDiagnosticsState diagnostics = {0.0, 0.0, 0.0, 0.0, 0.0, PETSC_TRUE};
733
734 PetscFunctionBeginUser;
736
737 PetscCheck(simCtx->les == DYNAMIC_SMAGORINSKY, PETSC_COMM_SELF, PETSC_ERR_ARG_WRONGSTATE,
738 "The dynamic procedure runs only for the dynamic Smagorinsky model. The "
739 "constant model prescribes its coefficient from configuration and holds "
740 "no coefficient field.");
741
742 // The dynamic procedure needs a developed field to sample. Starting from rest it
743 // has nothing to measure, so the first steps run with no subgrid dissipation.
744 if (simCtx->step < 2 && simCtx->StartStep == 0) {
745 PetscCall(VecSet(user->CS, 0.0));
746 PetscCall(FinalizeSmagorinskyConstantField(user));
747 user->les_diagnostics = (LESDiagnosticsState){0.0, 0.0, 0.0, 0.0, 0.0, PETSC_FALSE};
749 "Holding the dynamic coefficient at zero for the opening steps (step=%d).\n",
750 simCtx->step);
752 PetscFunctionReturn(0);
753 }
754
755 PetscCall(LESResolveDomain(user, &plan));
756 PetscCall(ResolveLESAveragingDirections(user, average_direction));
757
758 // alpha is the squared ratio of the two filter widths and sets the relative weight
759 // of the test-scale and grid-scale terms in the model tensor. The widths are the
760 // cube roots of the three directional widths, and the configured ratio applies to
761 // each direction the kernel filters. The box filters all three, so alpha is the
762 // ratio squared. Simpson filters only i and k and leaves j at the grid width, so
763 // its widths differ by ratio^(2/3) and alpha is ratio^(4/3); squaring the ratio
764 // there halved the dynamic coefficient on HOM02 (0.091 against the box's 0.194).
766 alpha = PetscPowReal(config->test_filter_width_ratio, 4.0 / 3.0);
767 } else {
768 alpha = config->test_filter_width_ratio * config->test_filter_width_ratio;
769 }
770
771 PetscCall(VecDuplicate(user->lUcat, &lStrainDiag));
772 PetscCall(VecDuplicate(user->lUcat, &lStrainOff));
773 PetscCall(VecDuplicate(user->lNvert, &lStrainAbs));
774 PetscCall(VecDuplicate(user->lNvert, &lLM));
775 PetscCall(VecDuplicate(user->lNvert, &lMM));
776 PetscCall(VecDuplicate(user->lNvert, &lCsq));
777 PetscCall(VecSet(lStrainDiag, 0.0));
778 PetscCall(VecSet(lStrainOff, 0.0));
779 PetscCall(VecSet(lStrainAbs, 0.0));
780 PetscCall(VecSet(lLM, 0.0));
781 PetscCall(VecSet(lMM, 0.0));
782 PetscCall(VecSet(lCsq, 0.0));
783 /* A wall model overwrites the first interior cell, so those cells carry an imposed
784 profile rather than a measurement and are kept out of the contraction sums. */
785 if (simCtx->wallfunction) {
786 PetscCall(VecDuplicate(user->lNvert, &lWallMask));
787 PetscCall(BuildWallModelExclusionMask(user, lWallMask));
788 }
789
790 PetscCall(DMDAVecGetArray(fda, user->lUcat, &ucat));
791
792 // --- 1. Strain rate everywhere the filter stencil will look -----------------
793 PetscCall(ComputeStrainRateField(user, ucat, lStrainDiag, lStrainOff, lStrainAbs));
794
795 PetscCall(DMDAVecGetArrayRead(fda, lStrainDiag, &strain_diag));
796 PetscCall(DMDAVecGetArrayRead(fda, lStrainOff, &strain_off));
797 PetscCall(DMDAVecGetArrayRead(da, lStrainAbs, &strain_abs));
798 PetscCall(DMDAVecGetArrayRead(fda, user->lCsi, &csi));
799 PetscCall(DMDAVecGetArrayRead(fda, user->lEta, &eta));
800 PetscCall(DMDAVecGetArrayRead(fda, user->lZet, &zet));
801 PetscCall(DMDAVecGetArrayRead(da, user->lNvert, &nvert));
802 PetscCall(DMDAVecGetArrayRead(da, user->lAj, &aj));
803 PetscCall(DMDAVecGetArray(da, lLM, &lm));
804 PetscCall(DMDAVecGetArray(da, lMM, &mm));
805
806 // --- 2. Germano contractions at every fluid cell ----------------------------
807 for (PetscInt k = plan.start[2]; k < plan.end[2]; k++)
808 for (PetscInt j = plan.start[1]; j < plan.end[1]; j++)
809 for (PetscInt i = plan.start[0]; i < plan.end[0]; i++) {
810 double velocity_x[3][3][3], velocity_y[3][3][3], velocity_z[3][3][3];
811 double strain_abs_stencil[3][3][3], weights[3][3][3];
812 SymTensor strain_stencil[3][3][3], product_stencil[3][3][3], velocity_product[3][3][3];
813 Cmpnts velocity_filtered;
814 SymTensor strain_filtered, product_filtered, velocity_product_filtered;
815 SymTensor leonard, model;
816 PetscReal strain_abs_filtered, delta, volume;
817
818 if (!SpatialTargetPlanMaskAllows(&plan, nvert[k][j][i])) {
819 lm[k][j][i] = 0.0;
820 mm[k][j][i] = 0.0;
821 continue;
822 }
823
824 // --- 2a. Gather the 3x3x3 stencil -------------------------------------
825 for (PetscInt r = -1; r <= 1; r++)
826 for (PetscInt q = -1; q <= 1; q++)
827 for (PetscInt p = -1; p <= 1; p++) {
828 const PetscInt R = r + 1, Q = q + 1, P = p + 1;
829 const PetscInt KK = k + r, JJ = j + q, II = i + p;
830 const SymTensor strain = LESStrainAt(strain_diag[KK][JJ][II], strain_off[KK][JJ][II]);
831 const PetscReal strain_abs_here = strain_abs[KK][JJ][II];
832
833 velocity_x[R][Q][P] = ucat[KK][JJ][II].x;
834 velocity_y[R][Q][P] = ucat[KK][JJ][II].y;
835 velocity_z[R][Q][P] = ucat[KK][JJ][II].z;
836
837 strain_stencil[R][Q][P] = strain;
838 strain_abs_stencil[R][Q][P] = strain_abs_here;
839
840 // The grid-scale model formed pointwise, before any test filtering. Test
841 // filtering this product is the term the model tensor cannot do without,
842 // and it is not the same as multiplying the two filtered factors.
843 product_stencil[R][Q][P] = SymTensorCombine(strain_abs_here, strain, 0.0, strain);
844
845 velocity_product[R][Q][P] = SymTensorSelfOuter(ucat[KK][JJ][II]);
846 weights[R][Q][P] = aj[KK][JJ][II];
847 }
848
849 // --- 2b. Test-filter every quantity the identity needs ----------------
850 velocity_filtered.x = ApplyLESTestFilter(config->test_filter_kernel, velocity_x, weights);
851 velocity_filtered.y = ApplyLESTestFilter(config->test_filter_kernel, velocity_y, weights);
852 velocity_filtered.z = ApplyLESTestFilter(config->test_filter_kernel, velocity_z, weights);
853 strain_abs_filtered = ApplyLESTestFilter(config->test_filter_kernel, strain_abs_stencil, weights);
854 PetscCall(ApplyLESTestFilterSymTensor(config->test_filter_kernel, strain_stencil, weights, &strain_filtered));
855 PetscCall(ApplyLESTestFilterSymTensor(config->test_filter_kernel, product_stencil, weights, &product_filtered));
856 PetscCall(ApplyLESTestFilterSymTensor(config->test_filter_kernel, velocity_product, weights, &velocity_product_filtered));
857
858 // --- 2c. Leonard stress and model tensor -------------------------------
859 leonard = LeonardStress(velocity_filtered, velocity_product_filtered);
860
861 PetscCall(ComputeCellFilterWidth(config->filter_width_model, aj[k][j][i],
862 csi[k][j][i], eta[k][j][i], zet[k][j][i], &delta));
863
864 model = GermanoModelTensor(delta, alpha, strain_abs_filtered, strain_filtered, product_filtered);
865
866 // --- 2d. Lilly's least-squares contraction -----------------------------
867 // Stored pre-multiplied by cell volume. The average that follows is over
868 // volume, not over cell count, so that a refined region does not dominate it
869 // merely by holding more cells; carrying the weight here keeps the shared
870 // averaging kernel free of any weighting policy of its own.
871 volume = 1.0 / aj[k][j][i];
872 lm[k][j][i] = volume * SymTensorContract(leonard, model);
873 mm[k][j][i] = volume * SymTensorNormSq(model);
874 }
875
876 PetscCall(DMDAVecRestoreArrayRead(fda, lStrainDiag, &strain_diag));
877 PetscCall(DMDAVecRestoreArrayRead(fda, lStrainOff, &strain_off));
878 PetscCall(DMDAVecRestoreArray(da, lLM, &lm));
879 PetscCall(DMDAVecRestoreArray(da, lMM, &mm));
880 PetscCall(DMDAVecRestoreArray(fda, user->lUcat, &ucat));
881
882 // --- 3. Average the two contractions, then divide ---------------------------
883 // Both sums are formed before the division. Averaging the pointwise quotients
884 // instead would not be the least-squares coefficient Lilly's closure defines.
885 PetscCall(PicurvSpatialRatioAverage(user, &plan, lLM, lMM, lWallMask, average_direction,
886 PetscObjectComm((PetscObject)da), lCsq, NULL));
887
888 // --- 4. Limit the coefficient and record what limiting cost -----------------
889 PetscCall(DMDAVecGetArrayRead(da, lLM, &lm));
890 PetscCall(DMDAVecGetArrayRead(da, lMM, &mm));
891 PetscCall(DMDAVecGetArrayRead(da, lCsq, &csq));
892 PetscCall(DMDAVecGetArray(da, user->CS, &cs_out));
893
894 for (PetscInt k = plan.start[2]; k < plan.end[2]; k++)
895 for (PetscInt j = plan.start[1]; j < plan.end[1]; j++)
896 for (PetscInt i = plan.start[0]; i < plan.end[0]; i++) {
897 PetscBool limited = PETSC_FALSE;
898 PetscReal weight, raw;
899
900 if (!SpatialTargetPlanMaskAllows(&plan, nvert[k][j][i])) {
901 cs_out[k][j][i] = 0.0;
902 continue;
903 }
904
905 weight = 1.0 / aj[k][j][i];
906 raw = csq[k][j][i];
907
908 cs_out[k][j][i] = ClipModelCoefficient(raw, config, &limited);
909
910 // The two contraction fields already carry the cell volume from the loop
911 // above, so they are summed as they stand rather than weighted twice.
912 diagnostics.contraction_lm += lm[k][j][i];
913 diagnostics.contraction_mm += mm[k][j][i];
914 diagnostics.fluid_volume += weight;
915 if (raw < 0.0) diagnostics.backscatter_volume += weight;
916 if (limited) diagnostics.limited_volume += weight;
917 }
918
919 PetscCall(DMDAVecRestoreArrayRead(da, lLM, &lm));
920 PetscCall(DMDAVecRestoreArrayRead(da, lMM, &mm));
921 PetscCall(DMDAVecRestoreArrayRead(da, lCsq, &csq));
922 PetscCall(DMDAVecRestoreArray(da, user->CS, &cs_out));
923 PetscCall(DMDAVecRestoreArrayRead(fda, user->lCsi, &csi));
924 PetscCall(DMDAVecRestoreArrayRead(fda, user->lEta, &eta));
925 PetscCall(DMDAVecRestoreArrayRead(fda, user->lZet, &zet));
926 PetscCall(DMDAVecRestoreArrayRead(da, user->lNvert, &nvert));
927 PetscCall(DMDAVecRestoreArrayRead(da, user->lAj, &aj));
928
929 PetscCall(VecDestroy(&lStrainDiag));
930 PetscCall(VecDestroy(&lStrainOff));
931 PetscCall(VecDestroy(&lStrainAbs));
932 PetscCall(VecDestroy(&lLM));
933 PetscCall(VecDestroy(&lMM));
934 PetscCall(VecDestroy(&lCsq));
935 if (lWallMask) PetscCall(VecDestroy(&lWallMask));
936
937 user->les_diagnostics = diagnostics;
938 PetscCall(FinalizeSmagorinskyConstantField(user));
939
941 PetscFunctionReturn(0);
942}
943
944#undef __FUNCT__
945#define __FUNCT__ "ComputeEddyViscosityLES"
946/**
947 * @brief Implementation of \ref ComputeEddyViscosityLES().
948 * @details Full API contract (arguments, ownership, side effects) is documented with
949 * the header declaration in `include/les.h`.
950 * @see ComputeEddyViscosityLES()
951 */
952PetscErrorCode ComputeEddyViscosityLES(UserCtx *user)
953{
954 SimCtx *simCtx = user->simCtx;
955 const LESConfig *config = &simCtx->les_config;
956 DM da = user->da, fda = user->fda;
958 const PetscReal molecular_viscosity = 1.0 / simCtx->ren;
959
960 // The constant model's coefficient is a number from the configuration, so it needs
961 // no field, no ghost exchange, and no periodic synchronization.
962 const PetscBool coefficient_is_field = (PetscBool)(simCtx->les == DYNAMIC_SMAGORINSKY);
963 const PetscReal prescribed_coefficient = config->constant_cs * config->constant_cs;
964
965 Cmpnts ***ucat = NULL, ***csi = NULL, ***eta = NULL, ***zet = NULL;
966 PetscReal ***coefficient = NULL, ***nu_t = NULL, ***nvert = NULL, ***aj = NULL;
967
968 PetscFunctionBeginUser;
970
971 PetscCall(LESResolveDomain(user, &plan));
972
973 PetscCall(DMDAVecGetArrayRead(fda, user->lUcat, &ucat));
974 PetscCall(DMDAVecGetArrayRead(fda, user->lCsi, &csi));
975 PetscCall(DMDAVecGetArrayRead(fda, user->lEta, &eta));
976 PetscCall(DMDAVecGetArrayRead(fda, user->lZet, &zet));
977 PetscCall(DMDAVecGetArray(da, user->Nu_t, &nu_t));
978 PetscCall(DMDAVecGetArrayRead(da, user->lNvert, &nvert));
979 PetscCall(DMDAVecGetArrayRead(da, user->lAj, &aj));
980 if (coefficient_is_field) PetscCall(DMDAVecGetArrayRead(da, user->lCs, &coefficient));
981
982 for (PetscInt k = plan.start[2]; k < plan.end[2]; k++)
983 for (PetscInt j = plan.start[1]; j < plan.end[1]; j++)
984 for (PetscInt i = plan.start[0]; i < plan.end[0]; i++) {
985 Cmpnts dudx, dvdx, dwdx;
986 PetscReal strain_magnitude, delta, model_coefficient;
987
988 if (!SpatialTargetPlanMaskAllows(&plan, nvert[k][j][i])) {
989 nu_t[k][j][i] = 0.0;
990 continue;
991 }
992
993 PetscCall(ComputeVectorFieldDerivatives(user, i, j, k, (Cmpnts ***)ucat, &dudx, &dvdx, &dwdx));
994
995 if (simCtx->les == VREMAN) {
996 // Resolved per direction from the cell's own edges; no scalar width enters.
997 Cmpnts edges[3];
998 PetscCall(ComputeCellEdgeVectors(aj[k][j][i], csi[k][j][i], eta[k][j][i], zet[k][j][i], edges));
999 nu_t[k][j][i] = VremanEddyViscosity(dudx, dvdx, dwdx, edges, config->vreman_coefficient);
1000 continue;
1001 }
1002
1003 PetscCall(ComputeCellFilterWidth(config->filter_width_model, aj[k][j][i],
1004 csi[k][j][i], eta[k][j][i], zet[k][j][i], &delta));
1005 if (simCtx->les == WALE) {
1006 nu_t[k][j][i] = WALEEddyViscosity(dudx, dvdx, dwdx, delta, config->wale_coefficient);
1007 continue;
1008 }
1009
1010 PetscCall(StrainRateFromGradients(dudx, dvdx, dwdx, NULL, &strain_magnitude));
1011 model_coefficient = coefficient_is_field ? coefficient[k][j][i] : prescribed_coefficient;
1012
1013 nu_t[k][j][i] = EddyViscosityFromCoefficient(model_coefficient, delta, strain_magnitude,
1014 molecular_viscosity, config->min_viscosity_ratio);
1015 }
1016
1017 if (coefficient_is_field) PetscCall(DMDAVecRestoreArrayRead(da, user->lCs, &coefficient));
1018 PetscCall(DMDAVecRestoreArrayRead(fda, user->lUcat, &ucat));
1019 PetscCall(DMDAVecRestoreArrayRead(fda, user->lCsi, &csi));
1020 PetscCall(DMDAVecRestoreArrayRead(fda, user->lEta, &eta));
1021 PetscCall(DMDAVecRestoreArrayRead(fda, user->lZet, &zet));
1022 PetscCall(DMDAVecRestoreArray(da, user->Nu_t, &nu_t));
1023 PetscCall(DMDAVecRestoreArrayRead(da, user->lNvert, &nvert));
1024 PetscCall(DMDAVecRestoreArrayRead(da, user->lAj, &aj));
1025
1026 {
1027 const FieldId periodic_fields[] = {FIELD_ID_NU_T};
1028
1029 PetscCall(SynchronizePeriodicCellFields(user, 1, periodic_fields));
1030 PetscCall(UpdateLocalGhosts(user, FIELD_ID_NU_T));
1031 }
1032
1034 PetscFunctionReturn(0);
1035}
1036
1037#undef __FUNCT__
1038#define __FUNCT__ "LogLESDiagnostics"
1039/**
1040 * @brief Implementation of \ref LogLESDiagnostics().
1041 * @details Full API contract (arguments, ownership, side effects) is documented with
1042 * the header declaration in `include/les.h`.
1043 * @see LogLESDiagnostics()
1044 */
1045PetscErrorCode LogLESDiagnostics(UserCtx *user)
1046{
1047 SimCtx *simCtx = user->simCtx;
1048 const LESConfig *config = &simCtx->les_config;
1049 DM da = user->da, fda = user->fda;
1050 SpatialTargetPlan plan;
1051 MPI_Comm comm;
1052
1053 const PetscBool dynamic = (PetscBool)(simCtx->les == DYNAMIC_SMAGORINSKY);
1054 // Vreman and WALE carry no Smagorinsky coefficient, so the coefficient columns are
1055 // written as nan rather than as a number that would read like a measured Cs.
1056 const PetscBool has_coefficient =
1057 (PetscBool)(simCtx->les == DYNAMIC_SMAGORINSKY || simCtx->les == CONSTANT_SMAGORINSKY);
1058
1059 // Index 0..4: fluid volume, coefficient first and second moments, nu_t volume sum,
1060 // subgrid-energy volume sum. Index 5..6: the pre-clip fractions. Reduced together
1061 // so the diagnostic costs one collective rather than seven.
1062 PetscReal local_sum[7] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
1063 PetscReal global_sum[7];
1064 PetscReal local_max[2] = {0.0, 0.0}, global_max[2];
1065 PetscReal local_min_coefficient = PETSC_MAX_REAL, global_min_coefficient;
1066 PetscReal contraction[2], global_contraction[2];
1067
1068 Cmpnts ***ucat = NULL, ***csi = NULL, ***eta = NULL, ***zet = NULL;
1069 PetscReal ***coefficient = NULL, ***nu_t = NULL, ***nvert = NULL, ***aj = NULL;
1070
1071 PetscFunctionBeginUser;
1072
1073 if (!config->diagnostics_enabled) PetscFunctionReturn(0);
1074 if (config->diagnostics_cadence <= 0) PetscFunctionReturn(0);
1075 if (simCtx->step % config->diagnostics_cadence != 0) PetscFunctionReturn(0);
1076
1077 PetscCall(LESResolveDomain(user, &plan));
1078 PetscCall(PetscObjectGetComm((PetscObject)da, &comm));
1079
1080 PetscCall(DMDAVecGetArrayRead(fda, user->lUcat, &ucat));
1081 PetscCall(DMDAVecGetArrayRead(fda, user->lCsi, &csi));
1082 PetscCall(DMDAVecGetArrayRead(fda, user->lEta, &eta));
1083 PetscCall(DMDAVecGetArrayRead(fda, user->lZet, &zet));
1084 PetscCall(DMDAVecGetArrayRead(da, user->lNu_t, &nu_t));
1085 PetscCall(DMDAVecGetArrayRead(da, user->lNvert, &nvert));
1086 PetscCall(DMDAVecGetArrayRead(da, user->lAj, &aj));
1087 if (dynamic) PetscCall(DMDAVecGetArrayRead(da, user->lCs, &coefficient));
1088
1089 for (PetscInt k = plan.start[2]; k < plan.end[2]; k++)
1090 for (PetscInt j = plan.start[1]; j < plan.end[1]; j++)
1091 for (PetscInt i = plan.start[0]; i < plan.end[0]; i++) {
1092 Cmpnts dudx, dvdx, dwdx;
1093 PetscReal strain_magnitude, delta, weight, value;
1094
1095 if (!SpatialTargetPlanMaskAllows(&plan, nvert[k][j][i])) continue;
1096
1097 weight = 1.0 / aj[k][j][i];
1098 value = dynamic ? coefficient[k][j][i]
1099 : config->constant_cs * config->constant_cs;
1100
1101 PetscCall(ComputeVectorFieldDerivatives(user, i, j, k, (Cmpnts ***)ucat, &dudx, &dvdx, &dwdx));
1102 PetscCall(StrainRateFromGradients(dudx, dvdx, dwdx, NULL, &strain_magnitude));
1103 PetscCall(ComputeCellFilterWidth(config->filter_width_model, aj[k][j][i],
1104 csi[k][j][i], eta[k][j][i], zet[k][j][i], &delta));
1105
1106 local_sum[0] += weight;
1107 local_sum[3] += weight * nu_t[k][j][i];
1108 local_sum[4] += weight * SubgridKineticEnergy(config->yoshizawa_ci, delta, strain_magnitude);
1109 local_max[1] = PetscMax(local_max[1], nu_t[k][j][i]);
1110 if (has_coefficient) {
1111 local_sum[1] += weight * value;
1112 local_sum[2] += weight * value * value;
1113 local_max[0] = PetscMax(local_max[0], value);
1114 local_min_coefficient = PetscMin(local_min_coefficient, value);
1115 }
1116 }
1117
1118 if (dynamic) PetscCall(DMDAVecRestoreArrayRead(da, user->lCs, &coefficient));
1119 PetscCall(DMDAVecRestoreArrayRead(fda, user->lUcat, &ucat));
1120 PetscCall(DMDAVecRestoreArrayRead(fda, user->lCsi, &csi));
1121 PetscCall(DMDAVecRestoreArrayRead(fda, user->lEta, &eta));
1122 PetscCall(DMDAVecRestoreArrayRead(fda, user->lZet, &zet));
1123 PetscCall(DMDAVecRestoreArrayRead(da, user->lNu_t, &nu_t));
1124 PetscCall(DMDAVecRestoreArrayRead(da, user->lNvert, &nvert));
1125 PetscCall(DMDAVecRestoreArrayRead(da, user->lAj, &aj));
1126
1127 local_sum[5] = user->les_diagnostics.valid ? user->les_diagnostics.backscatter_volume : 0.0;
1128 local_sum[6] = user->les_diagnostics.valid ? user->les_diagnostics.limited_volume : 0.0;
1129 contraction[0] = user->les_diagnostics.valid ? user->les_diagnostics.contraction_lm : 0.0;
1130 contraction[1] = user->les_diagnostics.valid ? user->les_diagnostics.contraction_mm : 0.0;
1131
1132 PetscCallMPI(MPI_Allreduce(local_sum, global_sum, 7, MPIU_REAL, MPI_SUM, comm));
1133 PetscCallMPI(MPI_Allreduce(local_max, global_max, 2, MPIU_REAL, MPI_MAX, comm));
1134 PetscCallMPI(MPI_Allreduce(&local_min_coefficient, &global_min_coefficient, 1, MPIU_REAL, MPI_MIN, comm));
1135 PetscCallMPI(MPI_Allreduce(contraction, global_contraction, 2, MPIU_REAL, MPI_SUM, comm));
1136
1137 if (simCtx->rank == 0) {
1138 const PetscReal volume = (global_sum[0] > 0.0) ? global_sum[0] : 1.0;
1139 const PetscReal mean = global_sum[1] / volume;
1140 const PetscReal mean_square = global_sum[2] / volume;
1141 const PetscReal variance = PetscMax(mean_square - mean * mean, 0.0);
1142 const PetscReal molecular = 1.0 / simCtx->ren;
1143 const PetscReal effective = (global_contraction[1] > LES_EPSILON)
1144 ? (global_contraction[0] / global_contraction[1]) : 0.0;
1145 FILE *file = NULL;
1146
1147 // Reported as Cs rather than as the stored coefficient, because that is the
1148 // number with a recognised value: near 0.16 for decaying isotropic turbulence.
1149 // A negative average means the box is backscattering on balance, and is written
1150 // as a negative Cs rather than hidden behind a square root.
1151 const PetscReal cs_effective = (effective >= 0.0) ? PetscSqrtReal(effective)
1152 : -PetscSqrtReal(-effective);
1153 const PetscReal cs_mean = (mean >= 0.0) ? PetscSqrtReal(mean) : -PetscSqrtReal(-mean);
1154
1155 PetscCall(PicurvOpenDiagnosticsCsv(simCtx, "les_coefficient.csv",
1156 "step,time,cs_effective,cs_mean,coefficient_mean,"
1157 "coefficient_rms,coefficient_min,coefficient_max,"
1158 "nu_t_mean,nu_t_max,nu_t_over_nu_mean,k_sgs_mean,"
1159 "backscatter_fraction,limited_fraction,physical_time", &file));
1160 PetscReal physical_time = 0.0;
1161 PetscCall(PicurvPhysicalTime(simCtx, simCtx->ti, &physical_time));
1162 fprintf(file,
1163 "%d,%.6e,%.6e,%.6e,%.6e,%.6e,%.6e,%.6e,%.6e,%.6e,%.6e,%.6e,%.6e,%.6e,%.6e\n",
1164 (int)simCtx->step, (double)simCtx->ti,
1165 has_coefficient ? (double)cs_effective : (double)NAN,
1166 has_coefficient ? (double)cs_mean : (double)NAN,
1167 has_coefficient ? (double)mean : (double)NAN,
1168 has_coefficient ? (double)PetscSqrtReal(variance) : (double)NAN,
1169 has_coefficient ? (double)global_min_coefficient : (double)NAN,
1170 has_coefficient ? (double)global_max[0] : (double)NAN,
1171 (double)(global_sum[3] / volume), (double)global_max[1],
1172 (double)(global_sum[3] / volume / molecular),
1173 (double)(global_sum[4] / volume),
1174 (double)(global_sum[5] / volume), (double)(global_sum[6] / volume),
1175 (double)physical_time);
1176 PetscCheck(fclose(file) == 0, PETSC_COMM_SELF, PETSC_ERR_FILE_WRITE,
1177 "Unable to close the LES diagnostics file.");
1178
1180 " LES coefficient: Cs_effective=%.4f, nu_t/nu (mean)=%.3e, backscatter=%.1f%%\n",
1181 (double)cs_effective, (double)(global_sum[3] / volume / molecular),
1182 (double)(100.0 * global_sum[5] / volume));
1183 }
1184
1185 PetscFunctionReturn(0);
1186}
PetscErrorCode SynchronizePeriodicCellFields(UserCtx *user, PetscInt num_fields, const FieldId field_ids[])
Synchronizes periodic endpoint cells for a list of cell-centered fields.
double ApplyLESTestFilter(LESTestFilterKernel kernel, double values[3][3][3], double weights[3][3][3])
Applies a numerical "test filter" to a 3x3x3 stencil of data points.
Definition Filter.c:123
PetscErrorCode ApplyLESTestFilterSymTensor(LESTestFilterKernel kernel, SymTensor values[3][3][3], double weights[3][3][3], SymTensor *filtered)
Applies the test filter to all six components of a symmetric tensor.
Definition Filter.c:147
PetscErrorCode ComputeCellEdgeVectors(PetscReal ajc, Cmpnts csi, Cmpnts eta, Cmpnts zet, Cmpnts edges[3])
Computes a cell's edge vectors along its three grid directions.
Definition Metric.c:360
PetscErrorCode ComputeCellDirectionalExtents(PetscReal ajc, Cmpnts csi, Cmpnts eta, Cmpnts zet, double *l_xi, double *l_eta, double *l_zeta)
Computes a cell's extent along each of its own grid directions.
Definition Metric.c:328
FieldId
Compile-time identity for a catalogued Eulerian field.
@ FIELD_ID_NU_T
@ FIELD_ID_CS
PetscErrorCode PicurvPhysicalTime(const SimCtx *simCtx, PetscReal solver_time, PetscReal *physical)
Convert a solver time to physical seconds, t * L_ref / U_ref.
Definition io.c:3177
Cmpnts SymTensorTimesVector(SymTensor t, Cmpnts v)
Implementation of SymTensorTimesVector().
Definition les.c:232
PetscReal SymTensorNormSq(SymTensor t)
Implementation of SymTensorNormSq().
Definition les.c:250
PetscErrorCode ComputeEddyViscosityLES(UserCtx *user)
Implementation of ComputeEddyViscosityLES().
Definition les.c:952
SymTensor SymTensorCombine(PetscReal a, SymTensor x, PetscReal b, SymTensor y)
Implementation of SymTensorCombine().
Definition les.c:165
SymTensor SymTensorSelfOuter(Cmpnts v)
Implementation of SymTensorSelfOuter().
Definition les.c:144
SymTensor SymTensorDeviator(SymTensor t)
Implementation of SymTensorDeviator().
Definition les.c:199
PetscReal SubgridKineticEnergy(PetscReal yoshizawa_ci, PetscReal delta, PetscReal strain_magnitude)
Implementation of SubgridKineticEnergy().
Definition les.c:595
static void LESPeriodicAxes(const SimCtx *simCtx, PetscBool periodic[3])
Reports the block's per-axis periodicity in (xi, eta, zeta) order.
Definition les.c:129
PetscReal SymTensorTrace(SymTensor t)
Implementation of SymTensorTrace().
Definition les.c:186
PetscReal EddyViscosityFromCoefficient(PetscReal coefficient, PetscReal delta, PetscReal strain_magnitude, PetscReal molecular_viscosity, PetscReal min_viscosity_ratio)
Implementation of EddyViscosityFromCoefficient().
Definition les.c:497
SymTensor GermanoModelTensor(PetscReal delta, PetscReal alpha, PetscReal strain_magnitude_filtered, SymTensor strain_filtered, SymTensor strain_product_filtered)
Implementation of GermanoModelTensor().
Definition les.c:374
PetscReal ClipModelCoefficient(PetscReal coefficient, const LESConfig *config, PetscBool *limited)
Implementation of ClipModelCoefficient().
Definition les.c:466
PetscErrorCode ComputeCellFilterWidth(LESFilterWidthModel model, PetscReal aj, Cmpnts csi, Cmpnts eta, Cmpnts zet, PetscReal *delta)
Implementation of ComputeCellFilterWidth().
Definition les.c:296
PetscErrorCode StrainRateFromGradients(Cmpnts dudx, Cmpnts dvdx, Cmpnts dwdx, SymTensor *strain, PetscReal *magnitude)
Implementation of StrainRateFromGradients().
Definition les.c:267
PetscErrorCode ResolveLESAveragingDirections(UserCtx *user, PetscBool direction[3])
Implementation of ResolveLESAveragingDirections().
Definition les.c:410
SymTensor LeonardStress(Cmpnts velocity_filtered, SymTensor velocity_product_filtered)
Implementation of LeonardStress().
Definition les.c:359
PetscReal SymTensorContract(SymTensor a, SymTensor b)
Implementation of SymTensorContract().
Definition les.c:217
static SymTensor LESStrainAt(Cmpnts diagonal, Cmpnts offdiagonal)
Rebuilds a symmetric strain tensor from its two scratch storage vectors.
Definition les.c:690
static PetscErrorCode FinalizeSmagorinskyConstantField(UserCtx *user)
Synchronizes the completed Smagorinsky coefficient field.
Definition les.c:40
PetscErrorCode LogLESDiagnostics(UserCtx *user)
Implementation of LogLESDiagnostics().
Definition les.c:1045
static PetscErrorCode ComputeStrainRateField(UserCtx *user, Cmpnts ***ucat, Vec strain_diagonal, Vec strain_offdiagonal, Vec strain_magnitude)
Fills the strain-rate scratch fields across the computed range and its halo.
Definition les.c:634
static PetscInt LESStrainSampleIndex(PetscInt index, PetscInt extent, PetscBool periodic)
Maps a cell index to the index whose strain rate represents it.
Definition les.c:624
PetscErrorCode ComputeSmagorinskyConstant(UserCtx *user)
Implementation of ComputeSmagorinskyConstant().
Definition les.c:715
static const double LES_EPSILON
Definition les.c:31
static PetscErrorCode LESResolveDomain(UserCtx *user, SpatialTargetPlan *plan)
Resolves the iteration domain the closure computes over.
Definition les.c:63
static PetscErrorCode BuildWallModelExclusionMask(UserCtx *user, Vec inclusion)
Marks the near-wall cells a wall model overwrites, so the procedure skips them.
Definition les.c:87
PetscReal WALEEddyViscosity(Cmpnts dudx, Cmpnts dvdx, Cmpnts dwdx, PetscReal delta, PetscReal coefficient)
Implementation of WALEEddyViscosity().
Definition les.c:520
PetscReal VremanEddyViscosity(Cmpnts dudx, Cmpnts dvdx, Cmpnts dwdx, const Cmpnts edges[3], PetscReal coefficient)
Implementation of VremanEddyViscosity().
Definition les.c:557
#define GLOBAL
Scope for global logging across all processes.
Definition logging.h:46
#define LOG_ALLOW(scope, level, fmt,...)
Logging macro that checks both the log level and whether the calling function is in the allowed-funct...
Definition logging.h:200
#define PROFILE_FUNCTION_END
Marks the end of a profiled code block.
Definition logging.h:894
@ LOG_INFO
Informational messages about program execution.
Definition logging.h:31
@ LOG_DEBUG
Detailed debugging information.
Definition logging.h:32
#define PROFILE_FUNCTION_BEGIN
Marks the beginning of a profiled code block (typically a function).
Definition logging.h:885
PetscErrorCode PicurvOpenDiagnosticsCsv(const SimCtx *simCtx, const char *filename, const char *header, FILE **file)
Opens a per-run diagnostics CSV in the run's analysis directory for appending.
Definition logging.c:3503
PetscErrorCode ComputeVectorFieldDerivatives(UserCtx *user, PetscInt i, PetscInt j, PetscInt k, Cmpnts ***field_data, Cmpnts *dudx, Cmpnts *dvdx, Cmpnts *dwdx)
Computes the derivatives of a cell-centered vector field at a specific grid point.
Definition setup.c:4074
PetscErrorCode UpdateLocalGhosts(UserCtx *user, FieldId field_id)
Updates the local vector (including ghost points) from its corresponding global vector.
Definition setup.c:2489
Spatial target resolution for the field-statistics pipeline.
PetscBool SpatialTargetPlanMaskAllows(const SpatialTargetPlan *plan, PetscReal nvert_value)
Reports whether a point passes the plan's mask.
@ PICURV_STATISTICS_MASK_FLUID
PetscInt end[3]
Exclusive end per dimension (i, j, k).
PetscInt start[3]
Inclusive start per dimension (i, j, k).
PetscErrorCode SpatialTargetPlanCreate(UserCtx *user, FieldId field_id, PicurvStatisticsMask mask, SpatialTargetPlan *plan)
Resolves the iteration domain for one field on one block.
PetscErrorCode PicurvSpatialRatioAverage(UserCtx *user, const SpatialTargetPlan *plan, Vec numerator, Vec denominator, Vec inclusion, const PetscBool average_direction[3], MPI_Comm comm, Vec ratio, PetscReal *scalar)
Averages two fields over a target domain and divides the results.
Resolved iteration domain for one field on one block.
@ DYNAMIC_SMAGORINSKY
Definition variables.h:551
@ VREMAN
Definition variables.h:552
@ WALE
Definition variables.h:553
@ CONSTANT_SMAGORINSKY
Definition variables.h:550
@ WALL
Definition variables.h:312
PetscReal yoshizawa_ci
Yoshizawa constant for the reported SGS kinetic energy.
Definition variables.h:642
PetscMPIInt rank
Definition variables.h:862
BoundaryFaceConfig boundary_faces[6]
Definition variables.h:1099
PetscReal yy
Definition variables.h:139
LESFilterWidthModel filter_width_model
How the grid filter width Delta is derived per cell.
Definition variables.h:634
PetscReal contraction_mm
Volume-weighted sum of M_ij M_ij over fluid cells.
Definition variables.h:656
LESTestFilterKernel test_filter_kernel
Discrete test-filter stencil.
Definition variables.h:635
Vec lNvert
Definition variables.h:1113
LESConfig les_config
Parameters of the LES closure selected by les.
Definition variables.h:987
SimCtx * simCtx
Back-pointer to the master simulation context.
Definition variables.h:1077
PetscBool valid
Set once a dynamic update has populated this state.
Definition variables.h:660
PetscReal ren
Definition variables.h:906
@ LES_CLIP_CLIP_NEGATIVE
Definition variables.h:619
@ LES_CLIP_CLAMP
Definition variables.h:618
@ LES_CLIP_NONE
Definition variables.h:620
PetscInt k_periodic
Definition variables.h:953
@ LES_TEST_FILTER_SIMPSON_IK
Definition variables.h:593
PetscReal max_cs
Ceiling on Cs under LES_CLIP_CLAMP.
Definition variables.h:640
LESClipMode clip_mode
Admissible range for the coefficient.
Definition variables.h:639
LESDiagnosticsState les_diagnostics
Pre-clipping statistics from the last dynamic update.
Definition variables.h:1157
PetscInt StartStep
Definition variables.h:869
PetscScalar x
Definition variables.h:122
PetscBool averaging_direction[3]
Averaged-over logical directions (xi, eta, zeta).
Definition variables.h:638
PetscReal wale_coefficient
Model constant C_w for WALE (Nicoud & Ducros 1999).
Definition variables.h:633
PetscReal limited_volume
Fluid volume whose coefficient the clip modified.
Definition variables.h:658
PetscReal fluid_volume
Total fluid volume sampled.
Definition variables.h:659
Vec lNu_t
Definition variables.h:1156
PetscReal contraction_lm
Volume-weighted sum of L_ij M_ij over fluid cells.
Definition variables.h:655
PetscScalar z
Definition variables.h:122
PetscInt diagnostics_cadence
Steps between diagnostic rows.
Definition variables.h:644
PetscReal min_viscosity_ratio
Enforce nu + nu_t >= ratio * nu.
Definition variables.h:641
PetscInt wallfunction
Enable wall functions on WALL faces.
Definition variables.h:985
@ LES_AVERAGING_LOCAL
Definition variables.h:605
@ LES_AVERAGING_GLOBAL
Definition variables.h:607
@ LES_AVERAGING_HOMOGENEOUS
Definition variables.h:606
PetscBool diagnostics_enabled
Append per-step coefficient statistics to the run log directory.
Definition variables.h:643
PetscReal constant_cs
Fixed Cs for CONSTANT_SMAGORINSKY; unused by the dynamic model.
Definition variables.h:631
PetscReal test_filter_width_ratio
Test-to-grid ratio per filtered direction; alpha is its square for the box, ratio^(4/3) for Simpson.
Definition variables.h:636
PetscReal xx
Definition variables.h:139
LESAveragingMode averaging_mode
Averaging set for the Germano contractions.
Definition variables.h:637
PetscInt i_periodic
Definition variables.h:953
PetscInt step
Definition variables.h:867
PetscReal vreman_coefficient
Model constant c for VREMAN (Vreman 2004: 2.5 Cs^2).
Definition variables.h:632
DMDALocalInfo info
Definition variables.h:1086
Vec lUcat
Definition variables.h:1113
PetscScalar y
Definition variables.h:122
PetscInt les
Active LES closure; an LESModelType value.
Definition variables.h:984
BCType mathematical_type
Definition variables.h:392
PetscReal yz
Definition variables.h:139
PetscReal zz
Definition variables.h:139
PetscReal backscatter_volume
Fluid volume whose raw coefficient was negative.
Definition variables.h:657
PetscReal ti
Definition variables.h:868
LESFilterWidthModel
Selects how a cell's grid filter width is derived from its metrics.
Definition variables.h:579
@ LES_FILTER_WIDTH_SCOTTI
Definition variables.h:583
@ LES_FILTER_WIDTH_GEOMETRIC_MEAN
Definition variables.h:581
@ LES_FILTER_WIDTH_CUBE_ROOT_VOLUME
Definition variables.h:580
@ LES_FILTER_WIDTH_MAX_EDGE
Definition variables.h:582
PetscReal xz
Definition variables.h:139
PetscReal xy
Definition variables.h:139
BCFace
Identifies the six logical faces of a structured computational block.
Definition variables.h:287
@ BC_FACE_NEG_X
Definition variables.h:288
@ BC_FACE_POS_Z
Definition variables.h:290
@ BC_FACE_POS_Y
Definition variables.h:289
@ BC_FACE_NEG_Z
Definition variables.h:290
@ BC_FACE_POS_X
Definition variables.h:288
@ BC_FACE_NEG_Y
Definition variables.h:289
PetscInt j_periodic
Definition variables.h:953
A 3D point or vector with PetscScalar components.
Definition variables.h:121
Every user-selectable parameter of the LES closure.
Definition variables.h:629
Pre-clipping volume statistics captured by one dynamic-coefficient update.
Definition variables.h:654
The master context for the entire simulation.
Definition variables.h:859
A symmetric second-order tensor stored by its six independent components.
Definition variables.h:138
User-defined context containing data specific to a single computational grid level.
Definition variables.h:1074
double nu_t(double yplus)
Computes turbulent eddy viscosity ratio (ν_t / ν)