PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
test_les.c
Go to the documentation of this file.
1/**
2 * @file test_les.c
3 * @brief C unit tests for the LES subgrid-scale closure.
4 *
5 * The suite is layered the way the module is. Pure kernels are checked against hand
6 * computed values with no PETSc objects at all; the Germano pieces are checked on
7 * fields whose answers are known in closed form; and the driver routines are checked
8 * on a small DMDA fixture.
9 *
10 * Two of the cases exist because of specific defects and are worth naming. The
11 * "filtered-product" case pins the distinction between the filter of a product and
12 * the product of filtered factors, which the model tensor once collapsed. The
13 * "duplicate-plane" case pins the requirement that a spatial average count each
14 * physical cell once on a periodic block, where the two layout boundary planes are
15 * copies of interior cells.
16 */
17
18#include "test_support.h"
19
20#include "les.h"
21#include "setup.h"
22#include "statistics_target.h"
23
24/*================================================================================*
25 * KERNEL-LEVEL TESTS *
26 *================================================================================*/
27
28/** @brief Tests the symmetric-tensor primitives against hand-computed values. */
29static PetscErrorCode TestSymTensorAlgebra(void)
30{
31 const Cmpnts v = {1.0, 2.0, 3.0};
32 const SymTensor a = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
33 const SymTensor b = {2.0, 0.0, 0.0, 2.0, 0.0, 2.0};
34 SymTensor outer, deviator, combined;
35
36 PetscFunctionBeginUser;
37
38 outer = SymTensorSelfOuter(v);
39 PetscCall(PicurvAssertRealNear(1.0, outer.xx, 1.0e-14, "self outer product should square the x component"));
40 PetscCall(PicurvAssertRealNear(2.0, outer.xy, 1.0e-14, "self outer product should cross x and y"));
41 PetscCall(PicurvAssertRealNear(6.0, outer.yz, 1.0e-14, "self outer product should cross y and z"));
42 PetscCall(PicurvAssertRealNear(9.0, outer.zz, 1.0e-14, "self outer product should square the z component"));
43
44 /* trace(a) = 1 + 4 + 6 = 11 */
45 PetscCall(PicurvAssertRealNear(11.0, SymTensorTrace(a), 1.0e-14, "trace should sum the diagonal"));
46
47 deviator = SymTensorDeviator(a);
48 PetscCall(PicurvAssertRealNear(0.0, SymTensorTrace(deviator), 1.0e-13,
49 "the deviator should be trace free"));
50 PetscCall(PicurvAssertRealNear(a.xy, deviator.xy, 1.0e-14,
51 "the deviator should leave off-diagonal components alone"));
52
53 /* a:b doubles the off-diagonal terms; b is diagonal, so a:b = 2*(1 + 4 + 6) = 22. */
54 PetscCall(PicurvAssertRealNear(22.0, SymTensorContract(a, b), 1.0e-13,
55 "contraction against a diagonal tensor should scale its trace"));
56 /* a:a = 1 + 16 + 36 + 2*(4 + 9 + 25) = 53 + 76 = 129 */
57 PetscCall(PicurvAssertRealNear(129.0, SymTensorNormSq(a), 1.0e-13,
58 "the squared norm must count off-diagonal components twice"));
59
60 combined = SymTensorCombine(2.0, a, -1.0, b);
61 PetscCall(PicurvAssertRealNear(0.0, combined.xx, 1.0e-14, "combine should scale and subtract"));
62 PetscCall(PicurvAssertRealNear(4.0, combined.xy, 1.0e-14, "combine should scale off-diagonal terms"));
63
64 PetscFunctionReturn(0);
65}
66
67/** @brief Tests strain-rate assembly and its magnitude for a known velocity gradient. */
68static PetscErrorCode TestStrainRateFromGradients(void)
69{
70 /* du/dy = 2 and every other derivative zero: pure shear. */
71 const Cmpnts dudx = {0.0, 2.0, 0.0};
72 const Cmpnts dvdx = {0.0, 0.0, 0.0};
73 const Cmpnts dwdx = {0.0, 0.0, 0.0};
74 SymTensor strain;
75 PetscReal magnitude;
76
77 PetscFunctionBeginUser;
78 PetscCall(StrainRateFromGradients(dudx, dvdx, dwdx, &strain, &magnitude));
79
80 PetscCall(PicurvAssertRealNear(1.0, strain.xy, 1.0e-14,
81 "pure shear should give S_xy = half the velocity gradient"));
82 PetscCall(PicurvAssertRealNear(0.0, strain.xx, 1.0e-14, "pure shear should have no normal strain"));
83 PetscCall(PicurvAssertRealNear(0.0, SymTensorTrace(strain), 1.0e-14,
84 "an incompressible gradient should give a trace-free strain"));
85 /* |S| = sqrt(2 S_ij S_ij) = sqrt(2 * 2 * 1) = 2 */
86 PetscCall(PicurvAssertRealNear(2.0, magnitude, 1.0e-13,
87 "the strain magnitude must count both off-diagonal entries"));
88
89 PetscFunctionReturn(0);
90}
91
92/** @brief Tests that the three filter-width models separate on an anisotropic cell. */
94{
95 /* Cartesian-aligned metrics for a cell of size 4 x 1 x 1: the face area vectors
96 are the products of the two spanning edges, and the Jacobian is 1/volume. */
97 const Cmpnts csi = {1.0, 0.0, 0.0};
98 const Cmpnts eta = {0.0, 4.0, 0.0};
99 const Cmpnts zet = {0.0, 0.0, 4.0};
100 const PetscReal aj = 0.25;
101 PetscReal cube_root = 0.0, geometric = 0.0, max_edge = 0.0;
102
103 PetscFunctionBeginUser;
104
105 PetscCall(ComputeCellFilterWidth(LES_FILTER_WIDTH_CUBE_ROOT_VOLUME, aj, csi, eta, zet, &cube_root));
106 PetscCall(ComputeCellFilterWidth(LES_FILTER_WIDTH_GEOMETRIC_MEAN, aj, csi, eta, zet, &geometric));
107 PetscCall(ComputeCellFilterWidth(LES_FILTER_WIDTH_MAX_EDGE, aj, csi, eta, zet, &max_edge));
108
109 /* Volume is 4, so the cube-root model gives 4^(1/3). */
110 PetscCall(PicurvAssertRealNear(PetscPowReal(4.0, 1.0 / 3.0), cube_root, 1.0e-12,
111 "the cube-root model should depend only on cell volume"));
112 PetscCall(PicurvAssertRealNear(PetscPowReal(4.0, 1.0 / 3.0), geometric, 1.0e-12,
113 "the geometric mean of the extents should recover the same volume"));
114 PetscCall(PicurvAssertRealNear(4.0, max_edge, 1.0e-12,
115 "the max-edge model should report the long direction"));
116 PetscCall(PicurvAssertBool((PetscBool)(max_edge > cube_root),
117 "a stretched cell should make the max-edge width the larger of the two"));
118
119 PetscFunctionReturn(0);
120}
121
122/** @brief Tests that the Leonard stress vanishes on a uniform velocity field. */
123static PetscErrorCode TestLeonardStressVanishesOnUniformFlow(void)
124{
125 const Cmpnts velocity = {1.5, -0.5, 2.0};
126 const SymTensor product = SymTensorSelfOuter(velocity);
127 SymTensor leonard;
128
129 PetscFunctionBeginUser;
130 /* Filtering a constant returns the constant, so both filtered inputs are exact. */
131 leonard = LeonardStress(velocity, product);
132
133 PetscCall(PicurvAssertRealNear(0.0, SymTensorNormSq(leonard), 1.0e-24,
134 "a uniform field carries no stress between the two filter widths"));
135 PetscFunctionReturn(0);
136}
137
138/**
139 * @brief Tests the model tensor where the two filter terms provably coincide.
140 *
141 * On a constant strain field the test filter is the identity, so the filter of the
142 * product equals the product of the filtered factors and the model tensor collapses
143 * to a closed form, `-2 Delta^2 (alpha - 1) |S| S_ij`, made trace free. This is the
144 * one configuration in which the corrected tensor and the collapsed expression that
145 * preceded it agree, which is what makes it a clean analytic anchor.
146 */
147static PetscErrorCode TestGermanoModelTensorOnConstantStrain(void)
148{
149 const PetscReal delta = 0.5;
150 const PetscReal alpha = 4.0;
151 Cmpnts dudx = {0.0, 2.0, 0.0}, dvdx = {0.0, 0.0, 0.0}, dwdx = {0.0, 0.0, 0.0};
152 SymTensor strain, product, model, expected;
153 PetscReal magnitude;
154
155 PetscFunctionBeginUser;
156 PetscCall(StrainRateFromGradients(dudx, dvdx, dwdx, &strain, &magnitude));
157
158 product = SymTensorCombine(magnitude, strain, 0.0, strain);
159 model = GermanoModelTensor(delta, alpha, magnitude, strain, product);
160
161 expected = SymTensorDeviator(SymTensorCombine(-2.0 * delta * delta * (alpha - 1.0) * magnitude,
162 strain, 0.0, strain));
163
164 PetscCall(PicurvAssertRealNear(expected.xy, model.xy, 1.0e-13,
165 "constant strain should give the closed-form model tensor"));
166 PetscCall(PicurvAssertRealNear(0.0, SymTensorTrace(model), 1.0e-13,
167 "the model tensor must be trace free"));
168 PetscFunctionReturn(0);
169}
170
171/**
172 * @brief Tests that the model tensor uses the filtered product, not the filtered factors.
173 *
174 * Builds a stencil whose strain magnitude varies across it, so the test filter of
175 * `|S| S_ij` genuinely differs from `|S|^ S^_ij`. The model tensor must be built from
176 * the former. Collapsing it onto the latter yields `-2 Delta^2 (alpha - 1) |S|^ S^_ij`,
177 * which is well scaled, responds to the flow, and is not the Germano-Lilly tensor;
178 * this case fails if the two are ever conflated again.
179 */
181{
183 const PetscReal delta = 1.0;
184 const PetscReal alpha = 4.0;
185 double magnitude_stencil[3][3][3], weights[3][3][3];
186 SymTensor strain_stencil[3][3][3], product_stencil[3][3][3];
187 SymTensor strain_filtered, product_filtered, model, collapsed;
188 PetscReal magnitude_filtered, separation;
189
190 PetscFunctionBeginUser;
191
192 for (PetscInt k = 0; k < 3; ++k)
193 for (PetscInt j = 0; j < 3; ++j)
194 for (PetscInt i = 0; i < 3; ++i) {
195 /* A strain that grows across the stencil: filtering no longer commutes with
196 forming the product, which is exactly the regime the identity relies on. */
197 const PetscReal shear = 1.0 + 0.5 * (PetscReal)(i + j + k);
198 Cmpnts dudx = {0.0, 2.0 * shear, 0.0};
199 Cmpnts dvdx = {0.0, 0.0, 0.0};
200 Cmpnts dwdx = {0.0, 0.0, 0.0};
201 SymTensor strain;
202 PetscReal magnitude;
203
204 PetscCall(StrainRateFromGradients(dudx, dvdx, dwdx, &strain, &magnitude));
205 strain_stencil[k][j][i] = strain;
206 magnitude_stencil[k][j][i] = magnitude;
207 product_stencil[k][j][i] = SymTensorCombine(magnitude, strain, 0.0, strain);
208 weights[k][j][i] = 1.0;
209 }
210
211 magnitude_filtered = ApplyLESTestFilter(kernel, magnitude_stencil, weights);
212 PetscCall(ApplyLESTestFilterSymTensor(kernel, strain_stencil, weights, &strain_filtered));
213 PetscCall(ApplyLESTestFilterSymTensor(kernel, product_stencil, weights, &product_filtered));
214
215 /* The filter of the product must not equal the product of the filtered factors. */
216 separation = PetscAbsReal(product_filtered.xy - magnitude_filtered * strain_filtered.xy);
217 PetscCall(PicurvAssertBool((PetscBool)(separation > 1.0e-6),
218 "a varying strain field must separate the filtered product from "
219 "the product of the filtered factors"));
220
221 model = GermanoModelTensor(delta, alpha, magnitude_filtered, strain_filtered, product_filtered);
222 collapsed = SymTensorDeviator(SymTensorCombine(-2.0 * delta * delta * (alpha - 1.0) * magnitude_filtered,
223 strain_filtered, 0.0, strain_filtered));
224
225 PetscCall(PicurvAssertBool((PetscBool)(PetscAbsReal(model.xy - collapsed.xy) > 1.0e-6),
226 "the model tensor must be built from the filtered product, not from "
227 "the product of separately filtered quantities"));
228 PetscFunctionReturn(0);
229}
230
231/** @brief Tests each limiting mode, including the sign that carries backscatter. */
232static PetscErrorCode TestClipModelCoefficientModes(void)
233{
234 LESConfig config;
235 PetscBool limited = PETSC_FALSE;
236
237 PetscFunctionBeginUser;
238 PetscCall(LESConfigSetDefaults(&config));
239 config.max_cs = 0.2; /* ceiling on Cs, so the coefficient ceiling is 0.04 */
240
241 config.clip_mode = LES_CLIP_CLAMP;
242 PetscCall(PicurvAssertRealNear(0.0, ClipModelCoefficient(-0.01, &config, &limited), 1.0e-14,
243 "clamping should discard a negative coefficient"));
244 PetscCall(PicurvAssertBool(limited, "clamping a negative coefficient counts as limiting"));
245 PetscCall(PicurvAssertRealNear(0.04, ClipModelCoefficient(1.0, &config, &limited), 1.0e-14,
246 "clamping should cap the coefficient at max_cs squared"));
247 PetscCall(PicurvAssertRealNear(0.01, ClipModelCoefficient(0.01, &config, &limited), 1.0e-14,
248 "clamping should pass an admissible coefficient through"));
249 PetscCall(PicurvAssertBool((PetscBool)(!limited), "an admissible coefficient is not limited"));
250
252 PetscCall(PicurvAssertRealNear(0.0, ClipModelCoefficient(-0.01, &config, &limited), 1.0e-14,
253 "clip_negative should discard a negative coefficient"));
254 PetscCall(PicurvAssertRealNear(1.0, ClipModelCoefficient(1.0, &config, &limited), 1.0e-14,
255 "clip_negative should impose no ceiling"));
256
257 config.clip_mode = LES_CLIP_NONE;
258 PetscCall(PicurvAssertRealNear(-0.01, ClipModelCoefficient(-0.01, &config, &limited), 1.0e-14,
259 "no limiting should preserve the sign that carries backscatter"));
260 PetscCall(PicurvAssertBool((PetscBool)(!limited), "an unlimited coefficient is not modified"));
261 PetscFunctionReturn(0);
262}
263
264/** @brief Tests eddy-viscosity assembly and the total-viscosity floor. */
266{
267 const PetscReal molecular = 0.01;
268
269 PetscFunctionBeginUser;
270
271 /* C = 0.04, Delta = 2, |S| = 3 -> nu_t = 0.04 * 4 * 3 = 0.48 */
272 PetscCall(PicurvAssertRealNear(0.48, EddyViscosityFromCoefficient(0.04, 2.0, 3.0, molecular, 0.0),
273 1.0e-13, "eddy viscosity should be the coefficient times Delta^2 |S|"));
274
275 /* A backscattering coefficient may drive nu_t negative, but only down to the point
276 where the total viscosity would vanish. */
277 PetscCall(PicurvAssertRealNear(-molecular,
278 EddyViscosityFromCoefficient(-100.0, 2.0, 3.0, molecular, 0.0),
279 1.0e-14, "the floor should stop nu_t at minus the molecular value"));
280 PetscCall(PicurvAssertRealNear(-0.5 * molecular,
281 EddyViscosityFromCoefficient(-100.0, 2.0, 3.0, molecular, 0.5),
282 1.0e-14, "a ratio of one half should keep half the molecular viscosity"));
283 PetscCall(PicurvAssertRealNear(-0.006,
284 EddyViscosityFromCoefficient(-0.0005, 2.0, 3.0, molecular, 0.0),
285 1.0e-14, "a mild backscatter should survive the floor untouched"));
286 PetscFunctionReturn(0);
287}
288
289/** @brief Tests the Yoshizawa subgrid kinetic energy against its closed form. */
290static PetscErrorCode TestSubgridKineticEnergy(void)
291{
292 PetscFunctionBeginUser;
293 /* 2 * 0.09 * 4 * 9 = 6.48 */
294 PetscCall(PicurvAssertRealNear(6.48, SubgridKineticEnergy(0.09, 2.0, 3.0), 1.0e-13,
295 "the Yoshizawa relation should scale with Delta^2 |S|^2"));
296 PetscFunctionReturn(0);
297}
298
299/*================================================================================*
300 * FIXTURE-LEVEL TESTS *
301 *================================================================================*/
302
303/**
304 * @brief Fills a ghosted local scalar field over the whole local array.
305 *
306 * Writes the halo as well as the owned cells, so a test can poison the layout
307 * boundary planes and observe whether an averaging routine counts them.
308 */
309static PetscErrorCode FillLocalScalar(UserCtx *user, Vec local, PetscReal value)
310{
311 PetscFunctionBeginUser;
312 PetscCall(VecSet(local, value));
313 (void)user;
314 PetscFunctionReturn(0);
315}
316
317/**
318 * @brief Writes a scalar into every cell the closure treats as owned and interior.
319 */
320static PetscErrorCode SetInteriorScalar(UserCtx *user, Vec local, PetscReal value)
321{
322 DMDALocalInfo info;
323 PetscReal ***array = NULL;
324 PetscInt xs, xe, ys, ye, zs, ze;
325
326 PetscFunctionBeginUser;
327 PetscCall(DMDAGetLocalInfo(user->da, &info));
328 xs = (info.xs == 0) ? 1 : info.xs;
329 ys = (info.ys == 0) ? 1 : info.ys;
330 zs = (info.zs == 0) ? 1 : info.zs;
331 xe = (info.xs + info.xm == info.mx) ? info.mx - 1 : info.xs + info.xm;
332 ye = (info.ys + info.ym == info.my) ? info.my - 1 : info.ys + info.ym;
333 ze = (info.zs + info.zm == info.mz) ? info.mz - 1 : info.zs + info.zm;
334
335 PetscCall(DMDAVecGetArray(user->da, local, &array));
336 for (PetscInt k = zs; k < ze; ++k)
337 for (PetscInt j = ys; j < ye; ++j)
338 for (PetscInt i = xs; i < xe; ++i) array[k][j][i] = value;
339 PetscCall(DMDAVecRestoreArray(user->da, local, &array));
340 PetscFunctionReturn(0);
341}
342
343/** @brief Reads one cell of a ghosted local scalar field. */
344static PetscErrorCode ReadLocalScalar(UserCtx *user, Vec local, PetscInt i, PetscInt j, PetscInt k,
345 PetscReal *value)
346{
347 PetscReal ***array = NULL;
348
349 PetscFunctionBeginUser;
350 PetscCall(DMDAVecGetArrayRead(user->da, local, &array));
351 *value = array[k][j][i];
352 PetscCall(DMDAVecRestoreArrayRead(user->da, local, &array));
353 PetscFunctionReturn(0);
354}
355
356/**
357 * @brief Declares periodic boundary pairs on the axes a test wants homogeneous.
358 *
359 * The shared fixture builds a periodic DMDA from the `SimCtx` flags but leaves the
360 * boundary face configuration untouched. The periodic field synchronization reads the
361 * faces rather than the flags, so a test that synchronizes has to declare them. The
362 * closure's own periodicity questions are answered from the flags, so tests that only
363 * ask those do not call this.
364 */
365static void DeclarePeriodicFaces(UserCtx *user, PetscBool xi, PetscBool eta, PetscBool zeta)
366{
367 const BCFace negative[3] = {BC_FACE_NEG_X, BC_FACE_NEG_Y, BC_FACE_NEG_Z};
368 const BCFace positive[3] = {BC_FACE_POS_X, BC_FACE_POS_Y, BC_FACE_POS_Z};
369 const PetscBool selected[3] = {xi, eta, zeta};
370
371 for (PetscInt axis = 0; axis < 3; ++axis) {
372 if (!selected[axis]) continue;
373 user->boundary_faces[negative[axis]].mathematical_type = PERIODIC;
374 user->boundary_faces[positive[axis]].mathematical_type = PERIODIC;
375 }
376}
377
378/** @brief Tests that an empty direction set reproduces the pointwise local model. */
379static PetscErrorCode TestAverageRatioLocalIsPointwise(void)
380{
381 SimCtx *simCtx = NULL;
382 UserCtx *user = NULL;
383 Vec numerator, denominator, ratio;
385 PetscReal ***num = NULL;
386 PetscReal value = 0.0;
387 const PetscBool none[3] = {PETSC_FALSE, PETSC_FALSE, PETSC_FALSE};
388
389 PetscFunctionBeginUser;
390 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
391 PetscCall(VecSet(user->lNvert, 0.0));
392 PetscCall(VecSet(user->lAj, 1.0));
393
394 PetscCall(VecDuplicate(user->lNvert, &numerator));
395 PetscCall(VecDuplicate(user->lNvert, &denominator));
396 PetscCall(VecDuplicate(user->lNvert, &ratio));
397 PetscCall(FillLocalScalar(user, denominator, 4.0));
398 PetscCall(FillLocalScalar(user, ratio, 0.0));
399 PetscCall(FillLocalScalar(user, numerator, 0.0));
400
401 /* A numerator that varies cell by cell, so a pointwise result cannot be confused
402 with an averaged one. */
403 PetscCall(DMDAVecGetArray(user->da, numerator, &num));
404 for (PetscInt k = 1; k < 6; ++k)
405 for (PetscInt j = 1; j < 6; ++j)
406 for (PetscInt i = 1; i < 6; ++i) num[k][j][i] = (PetscReal)(i + j + k);
407 PetscCall(DMDAVecRestoreArray(user->da, numerator, &num));
408
409 PetscCall(SpatialTargetPlanCreate(user, FIELD_ID_CS,
411 PetscCall(PicurvSpatialRatioAverage(user, &plan, numerator, denominator, NULL, none,
412 PETSC_COMM_WORLD, ratio, NULL));
413
414 PetscCall(ReadLocalScalar(user, ratio, 2, 3, 4, &value));
415 PetscCall(PicurvAssertRealNear((2.0 + 3.0 + 4.0) / 4.0, value, 1.0e-13,
416 "with no averaging direction the quotient should stay pointwise"));
417
418 PetscCall(VecDestroy(&numerator));
419 PetscCall(VecDestroy(&denominator));
420 PetscCall(VecDestroy(&ratio));
421 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
422 PetscFunctionReturn(0);
423}
424
425/**
426 * @brief Tests that averaging divides summed numerators by summed denominators.
427 *
428 * The distinction matters: the mean of the quotients differs from the quotient of the
429 * means, and Lilly's least-squares closure calls for the latter. Separating them needs
430 * a denominator that varies too. Three cells carry 1/1 and two carry 6/2, so every
431 * pointwise quotient is 1 or 3 and their mean is 9/5, while the ratio of the sums is
432 * 15/7. Only the second answer can come from averaging the two fields first.
433 */
434static PetscErrorCode TestAverageRatioDividesSummedFields(void)
435{
436 SimCtx *simCtx = NULL;
437 UserCtx *user = NULL;
438 Vec numerator, denominator, ratio;
440 PetscReal ***num = NULL, ***den = NULL;
441 PetscReal value = 0.0;
442 const PetscBool all[3] = {PETSC_TRUE, PETSC_TRUE, PETSC_TRUE};
443
444 PetscFunctionBeginUser;
445 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
446 PetscCall(VecSet(user->lNvert, 0.0));
447 PetscCall(VecSet(user->lAj, 1.0));
448
449 PetscCall(VecDuplicate(user->lNvert, &numerator));
450 PetscCall(VecDuplicate(user->lNvert, &denominator));
451 PetscCall(VecDuplicate(user->lNvert, &ratio));
452 PetscCall(FillLocalScalar(user, numerator, 0.0));
453 PetscCall(FillLocalScalar(user, denominator, 0.0));
454 PetscCall(FillLocalScalar(user, ratio, 0.0));
455
456 PetscCall(DMDAVecGetArray(user->da, numerator, &num));
457 PetscCall(DMDAVecGetArray(user->da, denominator, &den));
458 for (PetscInt k = 1; k < 6; ++k)
459 for (PetscInt j = 1; j < 6; ++j)
460 for (PetscInt i = 1; i < 6; ++i) {
461 const PetscBool low = (PetscBool)(i < 4);
462
463 num[k][j][i] = low ? 1.0 : 6.0;
464 den[k][j][i] = low ? 1.0 : 2.0;
465 }
466 PetscCall(DMDAVecRestoreArray(user->da, numerator, &num));
467 PetscCall(DMDAVecRestoreArray(user->da, denominator, &den));
468
469 PetscCall(SpatialTargetPlanCreate(user, FIELD_ID_CS,
471 PetscCall(PicurvSpatialRatioAverage(user, &plan, numerator, denominator, NULL, all,
472 PETSC_COMM_WORLD, ratio, NULL));
473
474 /* 15/7, the ratio of the sums. Averaging the pointwise quotients would give 9/5. */
475 PetscCall(ReadLocalScalar(user, ratio, 2, 3, 4, &value));
476 PetscCall(PicurvAssertRealNear(15.0 / 7.0, value, 1.0e-13,
477 "the global average should divide summed numerators by summed "
478 "denominators, not average the pointwise quotients"));
479 PetscCall(ReadLocalScalar(user, ratio, 4, 2, 1, &value));
480 PetscCall(PicurvAssertRealNear(15.0 / 7.0, value, 1.0e-13,
481 "a global average should be the same number in every cell"));
482
483 PetscCall(VecDestroy(&numerator));
484 PetscCall(VecDestroy(&denominator));
485 PetscCall(VecDestroy(&ratio));
486 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
487 PetscFunctionReturn(0);
488}
489
490/**
491 * @brief Tests that averaging over two directions leaves a profile along the third.
492 */
494{
495 SimCtx *simCtx = NULL;
496 UserCtx *user = NULL;
497 Vec numerator, denominator, ratio;
499 PetscReal ***num = NULL;
500 PetscReal at_low = 0.0, at_high = 0.0;
501 /* Average over xi and zeta, retain eta: the channel-flow arrangement. */
502 const PetscBool ik[3] = {PETSC_TRUE, PETSC_FALSE, PETSC_TRUE};
503
504 PetscFunctionBeginUser;
505 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
506 PetscCall(VecSet(user->lNvert, 0.0));
507 PetscCall(VecSet(user->lAj, 1.0));
508
509 PetscCall(VecDuplicate(user->lNvert, &numerator));
510 PetscCall(VecDuplicate(user->lNvert, &denominator));
511 PetscCall(VecDuplicate(user->lNvert, &ratio));
512 PetscCall(FillLocalScalar(user, numerator, 0.0));
513 PetscCall(FillLocalScalar(user, denominator, 0.0));
514 PetscCall(FillLocalScalar(user, ratio, 0.0));
515 PetscCall(SetInteriorScalar(user, denominator, 1.0));
516
517 /* Depends on eta alone, so averaging over xi and zeta must leave it untouched. */
518 PetscCall(DMDAVecGetArray(user->da, numerator, &num));
519 for (PetscInt k = 1; k < 6; ++k)
520 for (PetscInt j = 1; j < 6; ++j)
521 for (PetscInt i = 1; i < 6; ++i) num[k][j][i] = (PetscReal)j;
522 PetscCall(DMDAVecRestoreArray(user->da, numerator, &num));
523
524 PetscCall(SpatialTargetPlanCreate(user, FIELD_ID_CS,
526 PetscCall(PicurvSpatialRatioAverage(user, &plan, numerator, denominator, NULL, ik,
527 PETSC_COMM_WORLD, ratio, NULL));
528
529 PetscCall(ReadLocalScalar(user, ratio, 2, 1, 3, &at_low));
530 PetscCall(ReadLocalScalar(user, ratio, 4, 5, 2, &at_high));
531 PetscCall(PicurvAssertRealNear(1.0, at_low, 1.0e-13,
532 "averaging over xi and zeta should preserve the eta profile"));
533 PetscCall(PicurvAssertRealNear(5.0, at_high, 1.0e-13,
534 "each retained eta plane should keep its own average"));
535
536 PetscCall(VecDestroy(&numerator));
537 PetscCall(VecDestroy(&denominator));
538 PetscCall(VecDestroy(&ratio));
539 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
540 PetscFunctionReturn(0);
541}
542
543/**
544 * @brief Tests that a spatial average ignores the periodic duplicate planes.
545 *
546 * On a periodic block, index 0 and index `m-1` hold copies of interior cells. Counting
547 * them would weight those cells twice. The test poisons both planes and requires the
548 * average not to move.
549 */
551{
552 SimCtx *simCtx = NULL;
553 UserCtx *user = NULL;
554 Vec numerator, denominator, ratio;
556 PetscReal ***num = NULL;
557 DMDALocalInfo info;
558 PetscReal clean = 0.0, poisoned = 0.0;
559 const PetscBool all[3] = {PETSC_TRUE, PETSC_TRUE, PETSC_TRUE};
560
561 PetscFunctionBeginUser;
562 PetscCall(PicurvCreateMinimalContextsWithPeriodicity(&simCtx, &user, 6, 6, 6,
563 PETSC_TRUE, PETSC_TRUE, PETSC_TRUE));
564 DeclarePeriodicFaces(user, PETSC_TRUE, PETSC_TRUE, PETSC_TRUE);
565 PetscCall(DMDAGetLocalInfo(user->da, &info));
566 PetscCall(VecSet(user->lNvert, 0.0));
567 PetscCall(VecSet(user->lAj, 1.0));
568
569 PetscCall(VecDuplicate(user->lNvert, &numerator));
570 PetscCall(VecDuplicate(user->lNvert, &denominator));
571 PetscCall(VecDuplicate(user->lNvert, &ratio));
572 PetscCall(FillLocalScalar(user, numerator, 0.0));
573 PetscCall(FillLocalScalar(user, denominator, 0.0));
574 PetscCall(FillLocalScalar(user, ratio, 0.0));
575 PetscCall(SetInteriorScalar(user, numerator, 2.0));
576 PetscCall(SetInteriorScalar(user, denominator, 1.0));
577
578 PetscCall(SpatialTargetPlanCreate(user, FIELD_ID_CS,
580 PetscCall(PicurvSpatialRatioAverage(user, &plan, numerator, denominator, NULL, all,
581 PETSC_COMM_WORLD, ratio, NULL));
582 PetscCall(ReadLocalScalar(user, ratio, 2, 2, 2, &clean));
583
584 /* Poison both layout boundary planes in every direction. */
585 PetscCall(DMDAVecGetArray(user->da, numerator, &num));
586 for (PetscInt k = 0; k < info.mz; ++k)
587 for (PetscInt j = 0; j < info.my; ++j)
588 for (PetscInt i = 0; i < info.mx; ++i) {
589 const PetscBool duplicate = (PetscBool)(i == 0 || j == 0 || k == 0 ||
590 i == info.mx - 1 || j == info.my - 1 || k == info.mz - 1);
591
592 if (duplicate) num[k][j][i] = 1.0e6;
593 }
594 PetscCall(DMDAVecRestoreArray(user->da, numerator, &num));
595
596 PetscCall(SpatialTargetPlanCreate(user, FIELD_ID_CS,
598 PetscCall(PicurvSpatialRatioAverage(user, &plan, numerator, denominator, NULL, all,
599 PETSC_COMM_WORLD, ratio, NULL));
600 PetscCall(ReadLocalScalar(user, ratio, 2, 2, 2, &poisoned));
601
602 PetscCall(PicurvAssertRealNear(2.0, clean, 1.0e-13,
603 "the interior average should be the interior value"));
604 PetscCall(PicurvAssertRealNear(clean, poisoned, 1.0e-13,
605 "the duplicate planes must not contribute to a spatial average"));
606
607 PetscCall(VecDestroy(&numerator));
608 PetscCall(VecDestroy(&denominator));
609 PetscCall(VecDestroy(&ratio));
610 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
611 PetscFunctionReturn(0);
612}
613
614/** @brief Tests that homogeneous averaging falls back to the block's periodic axes. */
616{
617 SimCtx *simCtx = NULL;
618 UserCtx *user = NULL;
619 PetscBool direction[3];
620
621 PetscFunctionBeginUser;
622 /* Periodic in xi and zeta only: the channel arrangement. The fixture sets the
623 resolved periodicity flags, which are what the closure reads; the boundary faces
624 are left alone here so the test cannot pass by consulting them instead. */
625 PetscCall(PicurvCreateMinimalContextsWithPeriodicity(&simCtx, &user, 6, 6, 6,
626 PETSC_TRUE, PETSC_FALSE, PETSC_TRUE));
628
629 PetscCall(ResolveLESAveragingDirections(user, direction));
630 PetscCall(PicurvAssertBool(direction[0], "a periodic xi axis should be averaged over"));
631 PetscCall(PicurvAssertBool((PetscBool)(!direction[1]),
632 "a wall-normal axis should keep its own coefficient profile"));
633 PetscCall(PicurvAssertBool(direction[2], "a periodic zeta axis should be averaged over"));
634
635 /* An explicit selection overrides the periodic default. */
636 simCtx->les_config.averaging_direction[0] = PETSC_TRUE;
637 simCtx->les_config.averaging_direction[1] = PETSC_FALSE;
638 simCtx->les_config.averaging_direction[2] = PETSC_FALSE;
639 PetscCall(ResolveLESAveragingDirections(user, direction));
640 PetscCall(PicurvAssertBool(direction[0], "an explicit direction should be honoured"));
641 PetscCall(PicurvAssertBool((PetscBool)(!direction[2]),
642 "an explicit selection should replace the periodic default"));
643
644 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
645 PetscFunctionReturn(0);
646}
647
648/** @brief Tests that local and global averaging ignore the configured direction list. */
650{
651 SimCtx *simCtx = NULL;
652 UserCtx *user = NULL;
653 PetscBool direction[3];
654
655 PetscFunctionBeginUser;
656 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
657 simCtx->les_config.averaging_direction[0] = PETSC_TRUE;
658
660 PetscCall(ResolveLESAveragingDirections(user, direction));
661 PetscCall(PicurvAssertBool((PetscBool)(!direction[0] && !direction[1] && !direction[2]),
662 "local averaging should select no direction at all"));
663
665 PetscCall(ResolveLESAveragingDirections(user, direction));
666 PetscCall(PicurvAssertBool((PetscBool)(direction[0] && direction[1] && direction[2]),
667 "global averaging should select every direction"));
668
669 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
670 PetscFunctionReturn(0);
671}
672
673/**
674 * @brief Tests that the constant model builds its viscosity without a coefficient field.
675 *
676 * `UserCtx::CS` and `UserCtx::lCs` are deliberately left unallocated. If the constant
677 * path ever reaches for them again this case crashes rather than silently reintroducing
678 * a field of one repeated number.
679 */
681{
682 SimCtx *simCtx = NULL;
683 UserCtx *user = NULL;
684 Cmpnts ***ucat = NULL;
685 PetscReal ***nu_t = NULL;
686 const PetscReal constant_cs = 0.2;
687 /* u = x gives |S| = sqrt(2); with Aj = 1 the filter width is 1. */
688 const PetscReal expected = constant_cs * constant_cs * PetscSqrtReal(2.0);
689
690 PetscFunctionBeginUser;
691 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 5, 5, 5));
692 simCtx->les = CONSTANT_SMAGORINSKY;
693 simCtx->les_config.constant_cs = constant_cs;
694
695 PetscCall(DMCreateGlobalVector(user->da, &user->Nu_t));
696 PetscCall(DMCreateLocalVector(user->da, &user->lNu_t));
697 PetscCall(VecSet(user->Aj, 1.0));
698 PetscCall(VecSet(user->Nu_t, 0.0));
699
700 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
701 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k)
702 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j)
703 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
704 ucat[k][j][i].x = (PetscReal)i;
705 ucat[k][j][i].y = 0.0;
706 ucat[k][j][i].z = 0.0;
707 }
708 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
709 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
710 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
711 PetscCall(DMGlobalToLocalBegin(user->da, user->Aj, INSERT_VALUES, user->lAj));
712 PetscCall(DMGlobalToLocalEnd(user->da, user->Aj, INSERT_VALUES, user->lAj));
713
714 PetscCall(PicurvAssertBool((PetscBool)(user->lCs == NULL),
715 "the constant model should allocate no coefficient field"));
716 PetscCall(ComputeEddyViscosityLES(user));
717
718 PetscCall(DMDAVecGetArrayRead(user->da, user->Nu_t, &nu_t));
719 PetscCall(PicurvAssertRealNear(expected, nu_t[2][2][2], 1.0e-9,
720 "the constant model should read its coefficient from configuration"));
721 PetscCall(DMDAVecRestoreArrayRead(user->da, user->Nu_t, &nu_t));
722
723 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
724 PetscFunctionReturn(0);
725}
726
727/** @brief Tests that the dynamic procedure refuses to run for the constant model. */
729{
730 SimCtx *simCtx = NULL;
731 UserCtx *user = NULL;
732 PetscErrorCode call_ierr;
733
734 PetscFunctionBeginUser;
735 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 5, 5, 5));
736 simCtx->les = CONSTANT_SMAGORINSKY;
737 simCtx->step = 5;
738 simCtx->StartStep = 0;
739
740 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
741 call_ierr = ComputeSmagorinskyConstant(user);
742 PetscCall(PetscPopErrorHandler());
743
744 PetscCall(PicurvAssertBool((PetscBool)(call_ierr != 0),
745 "the dynamic procedure should reject a model that has no "
746 "coefficient field to fill"));
747
748 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
749 PetscFunctionReturn(0);
750}
751
752/**
753 * @brief Tests that the dynamic procedure returns a zero coefficient on uniform flow.
754 *
755 * Exercises the whole path: the strain precompute and its halo, the stencil gather,
756 * every test filter, both contractions, the averaging, and the limiting. A uniform
757 * field has no strain and no stress between the two filter widths, so the Germano
758 * identity has nothing to fit and the coefficient must come out exactly zero.
759 */
761{
762 SimCtx *simCtx = NULL;
763 UserCtx *user = NULL;
764 Cmpnts ***ucat = NULL;
765 PetscReal ***coefficient = NULL;
766
767 PetscFunctionBeginUser;
768 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
769 simCtx->les = DYNAMIC_SMAGORINSKY;
770 simCtx->step = 5;
771 simCtx->StartStep = 0;
772
773 PetscCall(DMCreateGlobalVector(user->da, &user->CS));
774 PetscCall(DMCreateLocalVector(user->da, &user->lCs));
775 PetscCall(VecSet(user->Aj, 1.0));
776 PetscCall(VecSet(user->lAj, 1.0));
777 PetscCall(VecSet(user->lNvert, 0.0));
778
779 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
780 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k)
781 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j)
782 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
783 ucat[k][j][i].x = 1.25;
784 ucat[k][j][i].y = -0.5;
785 ucat[k][j][i].z = 3.0;
786 }
787 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
788 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
789 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
790
791 PetscCall(ComputeSmagorinskyConstant(user));
792
793 PetscCall(DMDAVecGetArrayRead(user->da, user->CS, &coefficient));
794 for (PetscInt k = 1; k < 6; ++k)
795 for (PetscInt j = 1; j < 6; ++j)
796 for (PetscInt i = 1; i < 6; ++i) {
797 PetscCall(PicurvAssertRealNear(0.0, coefficient[k][j][i], 1.0e-20,
798 "a uniform field should give the dynamic procedure "
799 "nothing to fit"));
800 }
801 PetscCall(DMDAVecRestoreArrayRead(user->da, user->CS, &coefficient));
802
803 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
804 PetscFunctionReturn(0);
805}
806
807/**
808 * @brief Tests the dynamic procedure end to end on a periodic sheared field.
809 *
810 * Runs the full procedure on a block that is periodic in all three directions, where
811 * the strain precompute has to reach across the wrap correctly, and asks for global
812 * averaging. Two things must hold: every cell receives the same finite coefficient,
813 * because a global average is one number for the block; and the coefficient stays
814 * inside the configured ceiling.
815 */
817{
818 SimCtx *simCtx = NULL;
819 UserCtx *user = NULL;
820 Cmpnts ***ucat = NULL;
821 PetscReal ***coefficient = NULL;
822 PetscReal reference = 0.0;
823 const FieldId cell_fields[] = {FIELD_ID_UCAT};
824 const PetscReal wavenumber = 2.0 * PETSC_PI / 5.0; /* five distinct cells per direction */
825
826 PetscFunctionBeginUser;
827 PetscCall(PicurvCreateMinimalContextsWithPeriodicity(&simCtx, &user, 6, 6, 6,
828 PETSC_TRUE, PETSC_TRUE, PETSC_TRUE));
829 DeclarePeriodicFaces(user, PETSC_TRUE, PETSC_TRUE, PETSC_TRUE);
830 simCtx->les = DYNAMIC_SMAGORINSKY;
831 simCtx->step = 5;
832 simCtx->StartStep = 0;
834
835 PetscCall(DMCreateGlobalVector(user->da, &user->CS));
836 PetscCall(DMCreateLocalVector(user->da, &user->lCs));
837 PetscCall(VecSet(user->Aj, 1.0));
838 PetscCall(VecSet(user->lAj, 1.0));
839 PetscCall(VecSet(user->lNvert, 0.0));
840
841 /* A divergence-free periodic shear: enough structure that both contractions are
842 nonzero, and smooth enough that the coefficient stays in range. */
843 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
844 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k)
845 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j)
846 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
847 ucat[k][j][i].x = PetscSinReal(wavenumber * (PetscReal)j);
848 ucat[k][j][i].y = PetscSinReal(wavenumber * (PetscReal)k);
849 ucat[k][j][i].z = PetscSinReal(wavenumber * (PetscReal)i);
850 }
851 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
852 PetscCall(SynchronizePeriodicCellFields(user, 1, cell_fields));
853 PetscCall(UpdateLocalGhosts(user, FIELD_ID_UCAT));
854
855 PetscCall(ComputeSmagorinskyConstant(user));
856
857 PetscCall(DMDAVecGetArrayRead(user->da, user->CS, &coefficient));
858 reference = coefficient[1][1][1];
859 PetscCall(PicurvAssertBool((PetscBool)(PetscIsNormalReal(reference) || reference == 0.0),
860 "the dynamic coefficient must be finite"));
861 PetscCall(PicurvAssertBool((PetscBool)(reference >= 0.0 &&
862 reference <= simCtx->les_config.max_cs *
863 simCtx->les_config.max_cs),
864 "clamping should keep the coefficient inside its ceiling"));
865 for (PetscInt k = 1; k < 6; ++k)
866 for (PetscInt j = 1; j < 6; ++j)
867 for (PetscInt i = 1; i < 6; ++i) {
868 PetscCall(PicurvAssertRealNear(reference, coefficient[k][j][i], 1.0e-13,
869 "global averaging should give the block one coefficient"));
870 }
871 PetscCall(DMDAVecRestoreArrayRead(user->da, user->CS, &coefficient));
872
873 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
874 PetscFunctionReturn(0);
875}
876
877/**
878 * @brief Entry point for the LES closure suite.
879 */
880int main(int argc, char **argv)
881{
882 PetscErrorCode ierr;
883 const PicurvTestCase cases[] = {
884 {"sym-tensor-algebra", TestSymTensorAlgebra},
885 {"strain-rate-from-gradients", TestStrainRateFromGradients},
886 {"filter-width-models-separate-on-stretched-cell", TestFilterWidthModelsSeparateOnStretchedCell},
887 {"leonard-stress-vanishes-on-uniform-flow", TestLeonardStressVanishesOnUniformFlow},
888 {"germano-model-tensor-on-constant-strain", TestGermanoModelTensorOnConstantStrain},
889 {"germano-model-tensor-uses-filtered-product", TestGermanoModelTensorUsesFilteredProduct},
890 {"clip-model-coefficient-modes", TestClipModelCoefficientModes},
891 {"eddy-viscosity-floor-bounds-total-viscosity", TestEddyViscosityFloorBoundsTotalViscosity},
892 {"subgrid-kinetic-energy", TestSubgridKineticEnergy},
893 {"average-ratio-local-is-pointwise", TestAverageRatioLocalIsPointwise},
894 {"average-ratio-divides-summed-fields", TestAverageRatioDividesSummedFields},
895 {"average-ratio-retains-unaveraged-direction", TestAverageRatioRetainsUnaveragedDirection},
896 {"average-ratio-ignores-periodic-duplicate-planes", TestAverageRatioIgnoresPeriodicDuplicatePlanes},
897 {"homogeneous-averaging-derives-periodic-axes", TestHomogeneousAveragingDerivesPeriodicAxes},
898 {"averaging-modes-select-their-own-directions", TestAveragingModesSelectTheirOwnDirections},
899 {"constant-model-needs-no-coefficient-field", TestConstantModelNeedsNoCoefficientField},
900 {"dynamic-procedure-rejects-constant-model", TestDynamicProcedureRejectsConstantModel},
901 {"dynamic-procedure-vanishes-on-uniform-flow", TestDynamicProcedureVanishesOnUniformFlow},
902 {"dynamic-procedure-global-average-is-uniform", TestDynamicProcedureGlobalAverageIsUniform},
903 };
904
905 ierr = PetscInitialize(&argc, &argv, NULL, "PICurv LES closure tests");
906 if (ierr) {
907 return (int)ierr;
908 }
909
910 ierr = PicurvRunTests("unit-les", cases, sizeof(cases) / sizeof(cases[0]));
911 if (ierr) {
912 PetscFinalize();
913 return (int)ierr;
914 }
915
916 ierr = PetscFinalize();
917 return (int)ierr;
918}
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
FieldId
Compile-time identity for a catalogued Eulerian field.
@ FIELD_ID_UCAT
@ FIELD_ID_CS
PetscReal SymTensorNormSq(SymTensor t)
Returns the squared Frobenius norm t_ij t_ij.
Definition les.c:250
PetscErrorCode ComputeEddyViscosityLES(UserCtx *user)
Computes the turbulent eddy viscosity for one block.
Definition les.c:851
SymTensor SymTensorCombine(PetscReal a, SymTensor x, PetscReal b, SymTensor y)
Forms the linear combination a*x + b*y.
Definition les.c:165
SymTensor SymTensorSelfOuter(Cmpnts v)
Forms a symmetric tensor from a vector's outer product with itself.
Definition les.c:144
SymTensor SymTensorDeviator(SymTensor t)
Removes the isotropic part, returning t_ij - (1/3) delta_ij t_kk.
Definition les.c:199
PetscReal SubgridKineticEnergy(PetscReal yoshizawa_ci, PetscReal delta, PetscReal strain_magnitude)
Returns the modelled subgrid kinetic energy at a cell.
Definition les.c:503
PetscReal SymTensorTrace(SymTensor t)
Returns the trace t_kk.
Definition les.c:186
PetscReal EddyViscosityFromCoefficient(PetscReal coefficient, PetscReal delta, PetscReal strain_magnitude, PetscReal molecular_viscosity, PetscReal min_viscosity_ratio)
Builds the eddy viscosity from a model coefficient.
Definition les.c:480
SymTensor GermanoModelTensor(PetscReal delta, PetscReal alpha, PetscReal strain_magnitude_filtered, SymTensor strain_filtered, SymTensor strain_product_filtered)
Forms the deviatoric Germano model tensor M_ij.
Definition les.c:357
PetscReal ClipModelCoefficient(PetscReal coefficient, const LESConfig *config, PetscBool *limited)
Applies the configured admissible range to one model coefficient.
Definition les.c:449
PetscErrorCode ComputeCellFilterWidth(LESFilterWidthModel model, PetscReal aj, Cmpnts csi, Cmpnts eta, Cmpnts zet, PetscReal *delta)
Computes one cell's grid filter width under the selected width model.
Definition les.c:296
PetscErrorCode StrainRateFromGradients(Cmpnts dudx, Cmpnts dvdx, Cmpnts dwdx, SymTensor *strain, PetscReal *magnitude)
Builds the strain-rate tensor and its magnitude from a velocity gradient.
Definition les.c:267
PetscErrorCode ResolveLESAveragingDirections(UserCtx *user, PetscBool direction[3])
Resolves which logical directions the dynamic coefficient is averaged over.
Definition les.c:393
SymTensor LeonardStress(Cmpnts velocity_filtered, SymTensor velocity_product_filtered)
Forms the Leonard stress L_ij = (u_i u_j)^ - u^_i u^_j.
Definition les.c:342
PetscReal SymTensorContract(SymTensor a, SymTensor b)
Contracts two symmetric tensors as a_ij b_ij.
Definition les.c:217
PetscErrorCode ComputeSmagorinskyConstant(UserCtx *user)
Computes the dynamic Smagorinsky coefficient field for one block.
Definition les.c:623
PetscErrorCode UpdateLocalGhosts(UserCtx *user, FieldId field_id)
Updates the local vector (including ghost points) from its corresponding global vector.
Definition setup.c:2451
PetscErrorCode LESConfigSetDefaults(LESConfig *config)
Installs the shipped defaults for every LES closure parameter.
Definition setup.c:180
Spatial target resolution for the field-statistics pipeline.
@ PICURV_STATISTICS_MASK_FLUID
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.
static PetscErrorCode SetInteriorScalar(UserCtx *user, Vec local, PetscReal value)
Writes a scalar into every cell the closure treats as owned and interior.
Definition test_les.c:320
static PetscErrorCode TestAverageRatioDividesSummedFields(void)
Tests that averaging divides summed numerators by summed denominators.
Definition test_les.c:434
static PetscErrorCode TestAverageRatioRetainsUnaveragedDirection(void)
Tests that averaging over two directions leaves a profile along the third.
Definition test_les.c:493
static PetscErrorCode TestHomogeneousAveragingDerivesPeriodicAxes(void)
Tests that homogeneous averaging falls back to the block's periodic axes.
Definition test_les.c:615
static PetscErrorCode TestGermanoModelTensorOnConstantStrain(void)
Tests the model tensor where the two filter terms provably coincide.
Definition test_les.c:147
static void DeclarePeriodicFaces(UserCtx *user, PetscBool xi, PetscBool eta, PetscBool zeta)
Declares periodic boundary pairs on the axes a test wants homogeneous.
Definition test_les.c:365
int main(int argc, char **argv)
Entry point for the LES closure suite.
Definition test_les.c:880
static PetscErrorCode TestEddyViscosityFloorBoundsTotalViscosity(void)
Tests eddy-viscosity assembly and the total-viscosity floor.
Definition test_les.c:265
static PetscErrorCode TestDynamicProcedureVanishesOnUniformFlow(void)
Tests that the dynamic procedure returns a zero coefficient on uniform flow.
Definition test_les.c:760
static PetscErrorCode TestLeonardStressVanishesOnUniformFlow(void)
Tests that the Leonard stress vanishes on a uniform velocity field.
Definition test_les.c:123
static PetscErrorCode FillLocalScalar(UserCtx *user, Vec local, PetscReal value)
Fills a ghosted local scalar field over the whole local array.
Definition test_les.c:309
static PetscErrorCode TestSymTensorAlgebra(void)
Tests the symmetric-tensor primitives against hand-computed values.
Definition test_les.c:29
static PetscErrorCode TestConstantModelNeedsNoCoefficientField(void)
Tests that the constant model builds its viscosity without a coefficient field.
Definition test_les.c:680
static PetscErrorCode TestAverageRatioIgnoresPeriodicDuplicatePlanes(void)
Tests that a spatial average ignores the periodic duplicate planes.
Definition test_les.c:550
static PetscErrorCode TestStrainRateFromGradients(void)
Tests strain-rate assembly and its magnitude for a known velocity gradient.
Definition test_les.c:68
static PetscErrorCode ReadLocalScalar(UserCtx *user, Vec local, PetscInt i, PetscInt j, PetscInt k, PetscReal *value)
Reads one cell of a ghosted local scalar field.
Definition test_les.c:344
static PetscErrorCode TestAveragingModesSelectTheirOwnDirections(void)
Tests that local and global averaging ignore the configured direction list.
Definition test_les.c:649
static PetscErrorCode TestSubgridKineticEnergy(void)
Tests the Yoshizawa subgrid kinetic energy against its closed form.
Definition test_les.c:290
static PetscErrorCode TestFilterWidthModelsSeparateOnStretchedCell(void)
Tests that the three filter-width models separate on an anisotropic cell.
Definition test_les.c:93
static PetscErrorCode TestDynamicProcedureRejectsConstantModel(void)
Tests that the dynamic procedure refuses to run for the constant model.
Definition test_les.c:728
static PetscErrorCode TestGermanoModelTensorUsesFilteredProduct(void)
Tests that the model tensor uses the filtered product, not the filtered factors.
Definition test_les.c:180
static PetscErrorCode TestAverageRatioLocalIsPointwise(void)
Tests that an empty direction set reproduces the pointwise local model.
Definition test_les.c:379
static PetscErrorCode TestDynamicProcedureGlobalAverageIsUniform(void)
Tests the dynamic procedure end to end on a periodic sheared field.
Definition test_les.c:816
static PetscErrorCode TestClipModelCoefficientModes(void)
Tests each limiting mode, including the sign that carries backscatter.
Definition test_les.c:232
PetscErrorCode PicurvCreateMinimalContexts(SimCtx **simCtx_out, UserCtx **user_out, PetscInt mx, PetscInt my, PetscInt mz)
Builds minimal SimCtx and UserCtx fixtures for C unit tests.
PetscErrorCode PicurvAssertRealNear(PetscReal expected, PetscReal actual, PetscReal tol, const char *context)
Asserts that two real values agree within tolerance.
PetscErrorCode PicurvDestroyMinimalContexts(SimCtx **simCtx_ptr, UserCtx **user_ptr)
Destroys minimal SimCtx/UserCtx fixtures and all owned PETSc objects.
PetscErrorCode PicurvCreateMinimalContextsWithPeriodicity(SimCtx **simCtx_out, UserCtx **user_out, PetscInt mx, PetscInt my, PetscInt mz, PetscBool x_periodic, PetscBool y_periodic, PetscBool z_periodic)
Builds minimal SimCtx and UserCtx fixtures for C unit tests with configurable periodicity.
PetscErrorCode PicurvRunTests(const char *suite_name, const PicurvTestCase *cases, size_t case_count)
Runs a named C test suite and prints pass/fail progress markers.
PetscErrorCode PicurvAssertBool(PetscBool value, const char *context)
Asserts that one boolean condition is true.
Shared declarations for the PICurv C test fixture and assertion layer.
Named test case descriptor consumed by PicurvRunTests.
@ DYNAMIC_SMAGORINSKY
Definition variables.h:557
@ CONSTANT_SMAGORINSKY
Definition variables.h:556
@ PERIODIC
Definition variables.h:322
BoundaryFaceConfig boundary_faces[6]
Definition variables.h:1096
Vec lNvert
Definition variables.h:1111
LESConfig les_config
Parameters of the LES closure selected by les.
Definition variables.h:989
@ LES_CLIP_CLIP_NEGATIVE
Definition variables.h:621
@ LES_CLIP_CLAMP
Definition variables.h:620
@ LES_CLIP_NONE
Definition variables.h:622
LESTestFilterKernel
Selects the discrete test-filter kernel used by the dynamic procedure.
Definition variables.h:593
@ LES_TEST_FILTER_VOLUME_WEIGHTED_BOX
Definition variables.h:594
PetscReal max_cs
Ceiling on Cs under LES_CLIP_CLAMP.
Definition variables.h:641
LESClipMode clip_mode
Admissible range for the coefficient.
Definition variables.h:640
PetscInt StartStep
Definition variables.h:868
PetscScalar x
Definition variables.h:121
PetscBool averaging_direction[3]
Averaged-over logical directions (xi, eta, zeta).
Definition variables.h:639
Vec lNu_t
Definition variables.h:1154
PetscScalar z
Definition variables.h:121
@ LES_AVERAGING_LOCAL
Definition variables.h:607
@ LES_AVERAGING_GLOBAL
Definition variables.h:609
@ LES_AVERAGING_HOMOGENEOUS
Definition variables.h:608
PetscReal constant_cs
Fixed Cs for CONSTANT_SMAGORINSKY; unused by the dynamic model.
Definition variables.h:634
PetscReal xx
Definition variables.h:138
LESAveragingMode averaging_mode
Averaging set for the Germano contractions.
Definition variables.h:638
PetscInt step
Definition variables.h:866
DMDALocalInfo info
Definition variables.h:1083
Vec lUcat
Definition variables.h:1111
PetscScalar y
Definition variables.h:121
PetscInt les
Active LES closure; an LESModelType value.
Definition variables.h:985
BCType mathematical_type
Definition variables.h:398
PetscReal yz
Definition variables.h:138
PetscReal zz
Definition variables.h:138
@ LES_FILTER_WIDTH_GEOMETRIC_MEAN
Definition variables.h:584
@ LES_FILTER_WIDTH_CUBE_ROOT_VOLUME
Definition variables.h:583
@ LES_FILTER_WIDTH_MAX_EDGE
Definition variables.h:585
PetscReal xy
Definition variables.h:138
BCFace
Identifies the six logical faces of a structured computational block.
Definition variables.h:291
@ BC_FACE_NEG_X
Definition variables.h:292
@ BC_FACE_POS_Z
Definition variables.h:294
@ BC_FACE_POS_Y
Definition variables.h:293
@ BC_FACE_NEG_Z
Definition variables.h:294
@ BC_FACE_POS_X
Definition variables.h:292
@ BC_FACE_NEG_Y
Definition variables.h:293
A 3D point or vector with PetscScalar components.
Definition variables.h:120
Every user-selectable parameter of the LES closure.
Definition variables.h:632
The master context for the entire simulation.
Definition variables.h:858
A symmetric second-order tensor stored by its six independent components.
Definition variables.h:137
User-defined context containing data specific to a single computational grid level.
Definition variables.h:1071
double nu_t(double yplus)
Computes turbulent eddy viscosity ratio (ν_t / ν)