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 Face-area vectors and inverse volume of the parallelepiped spanned by a, b, c. */
124 Cmpnts *csi, Cmpnts *eta, Cmpnts *zet, PetscReal *aj)
125{
126 /* The same orientation convention as ComputeFaceMetrics(): each face-area vector
127 is the cross product of the two edges spanning that face. */
128 csi->x = b.y*c.z - b.z*c.y; csi->y = b.z*c.x - b.x*c.z; csi->z = b.x*c.y - b.y*c.x;
129 eta->x = c.y*a.z - c.z*a.y; eta->y = c.z*a.x - c.x*a.z; eta->z = c.x*a.y - c.y*a.x;
130 zet->x = a.y*b.z - a.z*b.y; zet->y = a.z*b.x - a.x*b.z; zet->z = a.x*b.y - a.y*b.x;
131 *aj = 1.0 / (a.x*csi->x + a.y*csi->y + a.z*csi->z);
132}
133
134/**
135 * @brief Tests that a filter width is a property of the cell, not of its orientation.
136 *
137 * @details The widths used to come from the Cartesian components of the cell diagonal.
138 * On the Humphrey bend an identical wall cell got a geometric-mean width three
139 * times larger, and so nine times the eddy viscosity, merely because the bend
140 * had turned it through 45 degrees relative to the x and y axes.
141 */
143{
144 /* A thin, long cell - 0.25 x 1 x 8 - first axis-aligned, then turned about z by
145 45 degrees and tilted about x by 30 degrees. */
146 const PetscReal c45 = PetscCosReal(PETSC_PI / 4.0), s45 = PetscSinReal(PETSC_PI / 4.0);
147 const PetscReal c30 = PetscCosReal(PETSC_PI / 6.0), s30 = PetscSinReal(PETSC_PI / 6.0);
148 const Cmpnts aligned[3] = {{0.25, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 8.0}};
149 const LESFilterWidthModel models[3] = {
151 Cmpnts rotated[3];
152 Cmpnts csi, eta, zet;
153 PetscReal aj, reference[3], turned[3];
154
155 PetscFunctionBeginUser;
156 for (PetscInt e = 0; e < 3; ++e) {
157 /* Rz(45) then Rx(30). */
158 const Cmpnts v = aligned[e];
159 const Cmpnts z = {c45*v.x - s45*v.y, s45*v.x + c45*v.y, v.z};
160 rotated[e] = (Cmpnts){z.x, c30*z.y - s30*z.z, s30*z.y + c30*z.z};
161 }
162
163 ParallelepipedMetrics(aligned[0], aligned[1], aligned[2], &csi, &eta, &zet, &aj);
164 for (PetscInt m = 0; m < 3; ++m) PetscCall(ComputeCellFilterWidth(models[m], aj, csi, eta, zet, &reference[m]));
165 ParallelepipedMetrics(rotated[0], rotated[1], rotated[2], &csi, &eta, &zet, &aj);
166 for (PetscInt m = 0; m < 3; ++m) PetscCall(ComputeCellFilterWidth(models[m], aj, csi, eta, zet, &turned[m]));
167
168 PetscCall(PicurvAssertRealNear(reference[0], turned[0], 1.0e-12, "cube-root width is orientation independent"));
169 PetscCall(PicurvAssertRealNear(reference[1], turned[1], 1.0e-12, "geometric-mean width is orientation independent"));
170 PetscCall(PicurvAssertRealNear(reference[2], turned[2], 1.0e-12, "max-edge width is orientation independent"));
171 PetscCall(PicurvAssertRealNear(8.0, turned[2], 1.0e-12, "max-edge width is the cell's longest extent"));
172 PetscCall(PicurvAssertRealNear(PetscCbrtReal(2.0), turned[1], 1.0e-12,
173 "geometric-mean width of a 0.25 x 1 x 8 cell is the cube root of its volume"));
174 PetscFunctionReturn(0);
175}
176
177/**
178 * @brief Tests the Scotti aspect-ratio correction against its closed form.
179 *
180 * @details Scotti, Meneveau & Lilly (1993) multiply the cube root of the extents by
181 * f(a1, a2) = cosh(sqrt((4/27)(ln^2 a1 - ln a1 ln a2 + ln^2 a2))), with a1, a2
182 * the two shorter extents over the longest. f is exactly 1 for a cube, so the
183 * width must equal the cube root there, and it must be unchanged by rotating
184 * the cell.
185 */
186static PetscErrorCode TestScottiFilterWidthMatchesClosedForm(void)
187{
188 const Cmpnts cube[3] = {{1.5, 0.0, 0.0}, {0.0, 1.5, 0.0}, {0.0, 0.0, 1.5}};
189 const Cmpnts brick[3] = {{1.0, 0.0, 0.0}, {0.0, 2.0, 0.0}, {0.0, 0.0, 8.0}};
190 const PetscReal c45 = PetscCosReal(PETSC_PI / 4.0), s45 = PetscSinReal(PETSC_PI / 4.0);
191 Cmpnts turned[3], csi, eta, zet;
192 PetscReal aj, delta, delta_turned, cube_root;
193
194 PetscFunctionBeginUser;
195 ParallelepipedMetrics(cube[0], cube[1], cube[2], &csi, &eta, &zet, &aj);
196 PetscCall(ComputeCellFilterWidth(LES_FILTER_WIDTH_SCOTTI, aj, csi, eta, zet, &delta));
197 PetscCall(ComputeCellFilterWidth(LES_FILTER_WIDTH_CUBE_ROOT_VOLUME, aj, csi, eta, zet, &cube_root));
198 PetscCall(PicurvAssertRealNear(cube_root, delta, 1.0e-12, "the Scotti correction is 1 for a cube"));
199
200 /* 1 x 2 x 8: a1 = 1/8, a2 = 1/4, f = 1.2596408907321084. */
201 ParallelepipedMetrics(brick[0], brick[1], brick[2], &csi, &eta, &zet, &aj);
202 PetscCall(ComputeCellFilterWidth(LES_FILTER_WIDTH_SCOTTI, aj, csi, eta, zet, &delta));
203 PetscCall(PicurvAssertRealNear(3.1740961470834224, delta, 1.0e-12,
204 "Scotti width of a 1 x 2 x 8 cell matches the closed form"));
205
206 for (PetscInt e = 0; e < 3; ++e) {
207 const Cmpnts v = brick[e];
208 turned[e] = (Cmpnts){c45 * v.x - s45 * v.z, v.y, s45 * v.x + c45 * v.z};
209 }
210 ParallelepipedMetrics(turned[0], turned[1], turned[2], &csi, &eta, &zet, &aj);
211 PetscCall(ComputeCellFilterWidth(LES_FILTER_WIDTH_SCOTTI, aj, csi, eta, zet, &delta_turned));
212 PetscCall(PicurvAssertRealNear(delta, delta_turned, 1.0e-12, "the Scotti width is orientation independent"));
213 PetscFunctionReturn(0);
214}
215
216/** @brief Rotates a vector by Rz(40 deg) then Rx(25 deg). */
218{
219 const PetscReal cz = PetscCosReal(40.0 * PETSC_PI / 180.0), sz = PetscSinReal(40.0 * PETSC_PI / 180.0);
220 const PetscReal cx = PetscCosReal(25.0 * PETSC_PI / 180.0), sx = PetscSinReal(25.0 * PETSC_PI / 180.0);
221 const Cmpnts z = {cz * v.x - sz * v.y, sz * v.x + cz * v.y, v.z};
222 return (Cmpnts){z.x, cx * z.y - sx * z.z, sx * z.y + cx * z.z};
223}
224
225/** @brief Rotates a velocity gradient g_ij = du_i/dx_j as R g R^T, rows passed as dudx, dvdx, dwdx. */
226static void RotateGradientForInvarianceTest(const Cmpnts rows[3], Cmpnts out[3])
227{
228 Cmpnts column_rotated[3], cols[3];
229 /* g R^T: rotate each row as a covector, i.e. rotate every row vector. */
230 for (PetscInt a = 0; a < 3; ++a) column_rotated[a] = RotateForInvarianceTest(rows[a]);
231 /* R (g R^T): rotate the columns, which are the components across rows. */
232 for (PetscInt b = 0; b < 3; ++b) {
233 const Cmpnts column = {b == 0 ? column_rotated[0].x : (b == 1 ? column_rotated[0].y : column_rotated[0].z),
234 b == 0 ? column_rotated[1].x : (b == 1 ? column_rotated[1].y : column_rotated[1].z),
235 b == 0 ? column_rotated[2].x : (b == 1 ? column_rotated[2].y : column_rotated[2].z)};
236 cols[b] = RotateForInvarianceTest(column);
237 }
238 for (PetscInt a = 0; a < 3; ++a)
239 out[a] = (Cmpnts){a == 0 ? cols[0].x : (a == 1 ? cols[0].y : cols[0].z),
240 a == 0 ? cols[1].x : (a == 1 ? cols[1].y : cols[1].z),
241 a == 0 ? cols[2].x : (a == 1 ? cols[2].y : cols[2].z)};
242}
243
244/**
245 * @brief Tests the WALE and Vreman kernels against independent evaluations.
246 *
247 * @details Reference values come from a separate numpy evaluation of the published
248 * formulas for the gradient below. Both models must vanish in pure shear, which
249 * is the property that lets them stay silent in a laminar shear layer, and both
250 * must be unchanged when the velocity gradient and the cell rotate together.
251 */
252static PetscErrorCode TestWALEAndVremanKernels(void)
253{
254 const Cmpnts g[3] = {{0.3, 1.2, -0.4}, {0.7, -0.5, 0.9}, {-0.2, 0.6, 0.2}};
255 const Cmpnts shear[3] = {{0.0, 2.5, 0.0}, {0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}};
256 const Cmpnts aligned_edges[3] = {{0.1, 0.0, 0.0}, {0.0, 0.2, 0.0}, {0.0, 0.0, 0.8}};
257 Cmpnts rotated_edges[3], rotated_g[3], csi, eta, zet, recovered[3];
258 PetscReal aj;
259
260 PetscFunctionBeginUser;
261 PetscCall(PicurvAssertRealNear(0.0002799988905128629, WALEEddyViscosity(g[0], g[1], g[2], 0.1, 0.5),
262 1.0e-15, "WALE matches the independent evaluation"));
263 PetscCall(PicurvAssertRealNear(0.007234428329928329,
264 VremanEddyViscosity(g[0], g[1], g[2], aligned_edges, 0.07),
265 1.0e-15, "Vreman on aligned edges matches the Cartesian formula"));
266 PetscCall(PicurvAssertRealNear(0.0, WALEEddyViscosity(shear[0], shear[1], shear[2], 0.1, 0.5),
267 1.0e-15, "WALE vanishes in pure shear"));
268 PetscCall(PicurvAssertRealNear(0.0, VremanEddyViscosity(shear[0], shear[1], shear[2], aligned_edges, 0.07),
269 1.0e-15, "Vreman vanishes in pure shear"));
270
271 for (PetscInt m = 0; m < 3; ++m) rotated_edges[m] = RotateForInvarianceTest(aligned_edges[m]);
273 PetscCall(PicurvAssertRealNear(VremanEddyViscosity(g[0], g[1], g[2], aligned_edges, 0.07),
274 VremanEddyViscosity(rotated_g[0], rotated_g[1], rotated_g[2], rotated_edges, 0.07),
275 1.0e-14, "Vreman is unchanged when the flow and the cell rotate together"));
276 PetscCall(PicurvAssertRealNear(WALEEddyViscosity(g[0], g[1], g[2], 0.1, 0.5),
277 WALEEddyViscosity(rotated_g[0], rotated_g[1], rotated_g[2], 0.1, 0.5),
278 1.0e-15, "WALE is unchanged when the flow rotates"));
279
280 /* The edge vectors recovered from a turned cell's metrics are its actual edges. */
281 ParallelepipedMetrics(rotated_edges[0], rotated_edges[1], rotated_edges[2], &csi, &eta, &zet, &aj);
282 PetscCall(ComputeCellEdgeVectors(aj, csi, eta, zet, recovered));
283 for (PetscInt m = 0; m < 3; ++m) {
284 PetscCall(PicurvAssertRealNear(rotated_edges[m].x, recovered[m].x, 1.0e-14, "edge x recovered from metrics"));
285 PetscCall(PicurvAssertRealNear(rotated_edges[m].y, recovered[m].y, 1.0e-14, "edge y recovered from metrics"));
286 PetscCall(PicurvAssertRealNear(rotated_edges[m].z, recovered[m].z, 1.0e-14, "edge z recovered from metrics"));
287 }
288 PetscFunctionReturn(0);
289}
290
291/** @brief Tests that the Leonard stress vanishes on a uniform velocity field. */
292static PetscErrorCode TestLeonardStressVanishesOnUniformFlow(void)
293{
294 const Cmpnts velocity = {1.5, -0.5, 2.0};
295 const SymTensor product = SymTensorSelfOuter(velocity);
296 SymTensor leonard;
297
298 PetscFunctionBeginUser;
299 /* Filtering a constant returns the constant, so both filtered inputs are exact. */
300 leonard = LeonardStress(velocity, product);
301
302 PetscCall(PicurvAssertRealNear(0.0, SymTensorNormSq(leonard), 1.0e-24,
303 "a uniform field carries no stress between the two filter widths"));
304 PetscFunctionReturn(0);
305}
306
307/**
308 * @brief Tests the model tensor where the two filter terms provably coincide.
309 *
310 * On a constant strain field the test filter is the identity, so the filter of the
311 * product equals the product of the filtered factors and the model tensor collapses
312 * to a closed form, `-2 Delta^2 (alpha - 1) |S| S_ij`, made trace free. This is the
313 * one configuration in which the corrected tensor and the collapsed expression that
314 * preceded it agree, which is what makes it a clean analytic anchor.
315 */
316static PetscErrorCode TestGermanoModelTensorOnConstantStrain(void)
317{
318 const PetscReal delta = 0.5;
319 const PetscReal alpha = 4.0;
320 Cmpnts dudx = {0.0, 2.0, 0.0}, dvdx = {0.0, 0.0, 0.0}, dwdx = {0.0, 0.0, 0.0};
321 SymTensor strain, product, model, expected;
322 PetscReal magnitude;
323
324 PetscFunctionBeginUser;
325 PetscCall(StrainRateFromGradients(dudx, dvdx, dwdx, &strain, &magnitude));
326
327 product = SymTensorCombine(magnitude, strain, 0.0, strain);
328 model = GermanoModelTensor(delta, alpha, magnitude, strain, product);
329
330 expected = SymTensorDeviator(SymTensorCombine(-2.0 * delta * delta * (alpha - 1.0) * magnitude,
331 strain, 0.0, strain));
332
333 PetscCall(PicurvAssertRealNear(expected.xy, model.xy, 1.0e-13,
334 "constant strain should give the closed-form model tensor"));
335 PetscCall(PicurvAssertRealNear(0.0, SymTensorTrace(model), 1.0e-13,
336 "the model tensor must be trace free"));
337 PetscFunctionReturn(0);
338}
339
340/**
341 * @brief Tests that the model tensor uses the filtered product, not the filtered factors.
342 *
343 * Builds a stencil whose strain magnitude varies across it, so the test filter of
344 * `|S| S_ij` genuinely differs from `|S|^ S^_ij`. The model tensor must be built from
345 * the former. Collapsing it onto the latter yields `-2 Delta^2 (alpha - 1) |S|^ S^_ij`,
346 * which is well scaled, responds to the flow, and is not the Germano-Lilly tensor;
347 * this case fails if the two are ever conflated again.
348 */
350{
352 const PetscReal delta = 1.0;
353 const PetscReal alpha = 4.0;
354 double magnitude_stencil[3][3][3], weights[3][3][3];
355 SymTensor strain_stencil[3][3][3], product_stencil[3][3][3];
356 SymTensor strain_filtered, product_filtered, model, collapsed;
357 PetscReal magnitude_filtered, separation;
358
359 PetscFunctionBeginUser;
360
361 for (PetscInt k = 0; k < 3; ++k)
362 for (PetscInt j = 0; j < 3; ++j)
363 for (PetscInt i = 0; i < 3; ++i) {
364 /* A strain that grows across the stencil: filtering no longer commutes with
365 forming the product, which is exactly the regime the identity relies on. */
366 const PetscReal shear = 1.0 + 0.5 * (PetscReal)(i + j + k);
367 Cmpnts dudx = {0.0, 2.0 * shear, 0.0};
368 Cmpnts dvdx = {0.0, 0.0, 0.0};
369 Cmpnts dwdx = {0.0, 0.0, 0.0};
370 SymTensor strain;
371 PetscReal magnitude;
372
373 PetscCall(StrainRateFromGradients(dudx, dvdx, dwdx, &strain, &magnitude));
374 strain_stencil[k][j][i] = strain;
375 magnitude_stencil[k][j][i] = magnitude;
376 product_stencil[k][j][i] = SymTensorCombine(magnitude, strain, 0.0, strain);
377 weights[k][j][i] = 1.0;
378 }
379
380 magnitude_filtered = ApplyLESTestFilter(kernel, magnitude_stencil, weights);
381 PetscCall(ApplyLESTestFilterSymTensor(kernel, strain_stencil, weights, &strain_filtered));
382 PetscCall(ApplyLESTestFilterSymTensor(kernel, product_stencil, weights, &product_filtered));
383
384 /* The filter of the product must not equal the product of the filtered factors. */
385 separation = PetscAbsReal(product_filtered.xy - magnitude_filtered * strain_filtered.xy);
386 PetscCall(PicurvAssertBool((PetscBool)(separation > 1.0e-6),
387 "a varying strain field must separate the filtered product from "
388 "the product of the filtered factors"));
389
390 model = GermanoModelTensor(delta, alpha, magnitude_filtered, strain_filtered, product_filtered);
391 collapsed = SymTensorDeviator(SymTensorCombine(-2.0 * delta * delta * (alpha - 1.0) * magnitude_filtered,
392 strain_filtered, 0.0, strain_filtered));
393
394 PetscCall(PicurvAssertBool((PetscBool)(PetscAbsReal(model.xy - collapsed.xy) > 1.0e-6),
395 "the model tensor must be built from the filtered product, not from "
396 "the product of separately filtered quantities"));
397 PetscFunctionReturn(0);
398}
399
400/** @brief Tests each limiting mode, including the sign that carries backscatter. */
401static PetscErrorCode TestClipModelCoefficientModes(void)
402{
403 LESConfig config;
404 PetscBool limited = PETSC_FALSE;
405
406 PetscFunctionBeginUser;
407 PetscCall(LESConfigSetDefaults(&config));
408 config.max_cs = 0.2; /* ceiling on Cs, so the coefficient ceiling is 0.04 */
409
410 config.clip_mode = LES_CLIP_CLAMP;
411 PetscCall(PicurvAssertRealNear(0.0, ClipModelCoefficient(-0.01, &config, &limited), 1.0e-14,
412 "clamping should discard a negative coefficient"));
413 PetscCall(PicurvAssertBool(limited, "clamping a negative coefficient counts as limiting"));
414 PetscCall(PicurvAssertRealNear(0.04, ClipModelCoefficient(1.0, &config, &limited), 1.0e-14,
415 "clamping should cap the coefficient at max_cs squared"));
416 PetscCall(PicurvAssertRealNear(0.01, ClipModelCoefficient(0.01, &config, &limited), 1.0e-14,
417 "clamping should pass an admissible coefficient through"));
418 PetscCall(PicurvAssertBool((PetscBool)(!limited), "an admissible coefficient is not limited"));
419
421 PetscCall(PicurvAssertRealNear(0.0, ClipModelCoefficient(-0.01, &config, &limited), 1.0e-14,
422 "clip_negative should discard a negative coefficient"));
423 PetscCall(PicurvAssertRealNear(1.0, ClipModelCoefficient(1.0, &config, &limited), 1.0e-14,
424 "clip_negative should impose no ceiling"));
425
426 config.clip_mode = LES_CLIP_NONE;
427 PetscCall(PicurvAssertRealNear(-0.01, ClipModelCoefficient(-0.01, &config, &limited), 1.0e-14,
428 "no limiting should preserve the sign that carries backscatter"));
429 PetscCall(PicurvAssertBool((PetscBool)(!limited), "an unlimited coefficient is not modified"));
430 PetscFunctionReturn(0);
431}
432
433/** @brief Tests eddy-viscosity assembly and the total-viscosity floor. */
435{
436 const PetscReal molecular = 0.01;
437
438 PetscFunctionBeginUser;
439
440 /* C = 0.04, Delta = 2, |S| = 3 -> nu_t = 0.04 * 4 * 3 = 0.48 */
441 PetscCall(PicurvAssertRealNear(0.48, EddyViscosityFromCoefficient(0.04, 2.0, 3.0, molecular, 0.0),
442 1.0e-13, "eddy viscosity should be the coefficient times Delta^2 |S|"));
443
444 /* A backscattering coefficient may drive nu_t negative, but only down to the point
445 where the total viscosity would vanish. */
446 PetscCall(PicurvAssertRealNear(-molecular,
447 EddyViscosityFromCoefficient(-100.0, 2.0, 3.0, molecular, 0.0),
448 1.0e-14, "the floor should stop nu_t at minus the molecular value"));
449 PetscCall(PicurvAssertRealNear(-0.5 * molecular,
450 EddyViscosityFromCoefficient(-100.0, 2.0, 3.0, molecular, 0.5),
451 1.0e-14, "a ratio of one half should keep half the molecular viscosity"));
452 PetscCall(PicurvAssertRealNear(-0.006,
453 EddyViscosityFromCoefficient(-0.0005, 2.0, 3.0, molecular, 0.0),
454 1.0e-14, "a mild backscatter should survive the floor untouched"));
455 PetscFunctionReturn(0);
456}
457
458/** @brief Tests the Yoshizawa subgrid kinetic energy against its closed form. */
459static PetscErrorCode TestSubgridKineticEnergy(void)
460{
461 PetscFunctionBeginUser;
462 /* 2 * 0.09 * 4 * 9 = 6.48 */
463 PetscCall(PicurvAssertRealNear(6.48, SubgridKineticEnergy(0.09, 2.0, 3.0), 1.0e-13,
464 "the Yoshizawa relation should scale with Delta^2 |S|^2"));
465 PetscFunctionReturn(0);
466}
467
468/*================================================================================*
469 * FIXTURE-LEVEL TESTS *
470 *================================================================================*/
471
472/**
473 * @brief Fills a ghosted local scalar field over the whole local array.
474 *
475 * Writes the halo as well as the owned cells, so a test can poison the layout
476 * boundary planes and observe whether an averaging routine counts them.
477 */
478static PetscErrorCode FillLocalScalar(UserCtx *user, Vec local, PetscReal value)
479{
480 PetscFunctionBeginUser;
481 PetscCall(VecSet(local, value));
482 (void)user;
483 PetscFunctionReturn(0);
484}
485
486/**
487 * @brief Writes a scalar into every cell the closure treats as owned and interior.
488 */
489static PetscErrorCode SetInteriorScalar(UserCtx *user, Vec local, PetscReal value)
490{
491 DMDALocalInfo info;
492 PetscReal ***array = NULL;
493 PetscInt xs, xe, ys, ye, zs, ze;
494
495 PetscFunctionBeginUser;
496 PetscCall(DMDAGetLocalInfo(user->da, &info));
497 xs = (info.xs == 0) ? 1 : info.xs;
498 ys = (info.ys == 0) ? 1 : info.ys;
499 zs = (info.zs == 0) ? 1 : info.zs;
500 xe = (info.xs + info.xm == info.mx) ? info.mx - 1 : info.xs + info.xm;
501 ye = (info.ys + info.ym == info.my) ? info.my - 1 : info.ys + info.ym;
502 ze = (info.zs + info.zm == info.mz) ? info.mz - 1 : info.zs + info.zm;
503
504 PetscCall(DMDAVecGetArray(user->da, local, &array));
505 for (PetscInt k = zs; k < ze; ++k)
506 for (PetscInt j = ys; j < ye; ++j)
507 for (PetscInt i = xs; i < xe; ++i) array[k][j][i] = value;
508 PetscCall(DMDAVecRestoreArray(user->da, local, &array));
509 PetscFunctionReturn(0);
510}
511
512/** @brief Reads one cell of a ghosted local scalar field. */
513static PetscErrorCode ReadLocalScalar(UserCtx *user, Vec local, PetscInt i, PetscInt j, PetscInt k,
514 PetscReal *value)
515{
516 PetscReal ***array = NULL;
517
518 PetscFunctionBeginUser;
519 PetscCall(DMDAVecGetArrayRead(user->da, local, &array));
520 *value = array[k][j][i];
521 PetscCall(DMDAVecRestoreArrayRead(user->da, local, &array));
522 PetscFunctionReturn(0);
523}
524
525/**
526 * @brief Declares periodic boundary pairs on the axes a test wants homogeneous.
527 *
528 * The shared fixture builds a periodic DMDA from the `SimCtx` flags but leaves the
529 * boundary face configuration untouched. The periodic field synchronization reads the
530 * faces rather than the flags, so a test that synchronizes has to declare them. The
531 * closure's own periodicity questions are answered from the flags, so tests that only
532 * ask those do not call this.
533 */
534static void DeclarePeriodicFaces(UserCtx *user, PetscBool xi, PetscBool eta, PetscBool zeta)
535{
536 const BCFace negative[3] = {BC_FACE_NEG_X, BC_FACE_NEG_Y, BC_FACE_NEG_Z};
537 const BCFace positive[3] = {BC_FACE_POS_X, BC_FACE_POS_Y, BC_FACE_POS_Z};
538 const PetscBool selected[3] = {xi, eta, zeta};
539
540 for (PetscInt axis = 0; axis < 3; ++axis) {
541 if (!selected[axis]) continue;
542 user->boundary_faces[negative[axis]].mathematical_type = PERIODIC;
543 user->boundary_faces[positive[axis]].mathematical_type = PERIODIC;
544 }
545}
546
547/** @brief Tests that an empty direction set reproduces the pointwise local model. */
548static PetscErrorCode TestAverageRatioLocalIsPointwise(void)
549{
550 SimCtx *simCtx = NULL;
551 UserCtx *user = NULL;
552 Vec numerator, denominator, ratio;
554 PetscReal ***num = NULL;
555 PetscReal value = 0.0;
556 const PetscBool none[3] = {PETSC_FALSE, PETSC_FALSE, PETSC_FALSE};
557
558 PetscFunctionBeginUser;
559 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
560 PetscCall(VecSet(user->lNvert, 0.0));
561 PetscCall(VecSet(user->lAj, 1.0));
562
563 PetscCall(VecDuplicate(user->lNvert, &numerator));
564 PetscCall(VecDuplicate(user->lNvert, &denominator));
565 PetscCall(VecDuplicate(user->lNvert, &ratio));
566 PetscCall(FillLocalScalar(user, denominator, 4.0));
567 PetscCall(FillLocalScalar(user, ratio, 0.0));
568 PetscCall(FillLocalScalar(user, numerator, 0.0));
569
570 /* A numerator that varies cell by cell, so a pointwise result cannot be confused
571 with an averaged one. */
572 PetscCall(DMDAVecGetArray(user->da, numerator, &num));
573 for (PetscInt k = 1; k < 6; ++k)
574 for (PetscInt j = 1; j < 6; ++j)
575 for (PetscInt i = 1; i < 6; ++i) num[k][j][i] = (PetscReal)(i + j + k);
576 PetscCall(DMDAVecRestoreArray(user->da, numerator, &num));
577
578 PetscCall(SpatialTargetPlanCreate(user, FIELD_ID_CS,
580 PetscCall(PicurvSpatialRatioAverage(user, &plan, numerator, denominator, NULL, none,
581 PETSC_COMM_WORLD, ratio, NULL));
582
583 PetscCall(ReadLocalScalar(user, ratio, 2, 3, 4, &value));
584 PetscCall(PicurvAssertRealNear((2.0 + 3.0 + 4.0) / 4.0, value, 1.0e-13,
585 "with no averaging direction the quotient should stay pointwise"));
586
587 PetscCall(VecDestroy(&numerator));
588 PetscCall(VecDestroy(&denominator));
589 PetscCall(VecDestroy(&ratio));
590 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
591 PetscFunctionReturn(0);
592}
593
594/**
595 * @brief Tests that averaging divides summed numerators by summed denominators.
596 *
597 * The distinction matters: the mean of the quotients differs from the quotient of the
598 * means, and Lilly's least-squares closure calls for the latter. Separating them needs
599 * a denominator that varies too. Three cells carry 1/1 and two carry 6/2, so every
600 * pointwise quotient is 1 or 3 and their mean is 9/5, while the ratio of the sums is
601 * 15/7. Only the second answer can come from averaging the two fields first.
602 */
603static PetscErrorCode TestAverageRatioDividesSummedFields(void)
604{
605 SimCtx *simCtx = NULL;
606 UserCtx *user = NULL;
607 Vec numerator, denominator, ratio;
609 PetscReal ***num = NULL, ***den = NULL;
610 PetscReal value = 0.0;
611 const PetscBool all[3] = {PETSC_TRUE, PETSC_TRUE, PETSC_TRUE};
612
613 PetscFunctionBeginUser;
614 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
615 PetscCall(VecSet(user->lNvert, 0.0));
616 PetscCall(VecSet(user->lAj, 1.0));
617
618 PetscCall(VecDuplicate(user->lNvert, &numerator));
619 PetscCall(VecDuplicate(user->lNvert, &denominator));
620 PetscCall(VecDuplicate(user->lNvert, &ratio));
621 PetscCall(FillLocalScalar(user, numerator, 0.0));
622 PetscCall(FillLocalScalar(user, denominator, 0.0));
623 PetscCall(FillLocalScalar(user, ratio, 0.0));
624
625 PetscCall(DMDAVecGetArray(user->da, numerator, &num));
626 PetscCall(DMDAVecGetArray(user->da, denominator, &den));
627 for (PetscInt k = 1; k < 6; ++k)
628 for (PetscInt j = 1; j < 6; ++j)
629 for (PetscInt i = 1; i < 6; ++i) {
630 const PetscBool low = (PetscBool)(i < 4);
631
632 num[k][j][i] = low ? 1.0 : 6.0;
633 den[k][j][i] = low ? 1.0 : 2.0;
634 }
635 PetscCall(DMDAVecRestoreArray(user->da, numerator, &num));
636 PetscCall(DMDAVecRestoreArray(user->da, denominator, &den));
637
638 PetscCall(SpatialTargetPlanCreate(user, FIELD_ID_CS,
640 PetscCall(PicurvSpatialRatioAverage(user, &plan, numerator, denominator, NULL, all,
641 PETSC_COMM_WORLD, ratio, NULL));
642
643 /* 15/7, the ratio of the sums. Averaging the pointwise quotients would give 9/5. */
644 PetscCall(ReadLocalScalar(user, ratio, 2, 3, 4, &value));
645 PetscCall(PicurvAssertRealNear(15.0 / 7.0, value, 1.0e-13,
646 "the global average should divide summed numerators by summed "
647 "denominators, not average the pointwise quotients"));
648 PetscCall(ReadLocalScalar(user, ratio, 4, 2, 1, &value));
649 PetscCall(PicurvAssertRealNear(15.0 / 7.0, value, 1.0e-13,
650 "a global average should be the same number in every cell"));
651
652 PetscCall(VecDestroy(&numerator));
653 PetscCall(VecDestroy(&denominator));
654 PetscCall(VecDestroy(&ratio));
655 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
656 PetscFunctionReturn(0);
657}
658
659/**
660 * @brief Tests that averaging over two directions leaves a profile along the third.
661 */
663{
664 SimCtx *simCtx = NULL;
665 UserCtx *user = NULL;
666 Vec numerator, denominator, ratio;
668 PetscReal ***num = NULL;
669 PetscReal at_low = 0.0, at_high = 0.0;
670 /* Average over xi and zeta, retain eta: the channel-flow arrangement. */
671 const PetscBool ik[3] = {PETSC_TRUE, PETSC_FALSE, PETSC_TRUE};
672
673 PetscFunctionBeginUser;
674 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
675 PetscCall(VecSet(user->lNvert, 0.0));
676 PetscCall(VecSet(user->lAj, 1.0));
677
678 PetscCall(VecDuplicate(user->lNvert, &numerator));
679 PetscCall(VecDuplicate(user->lNvert, &denominator));
680 PetscCall(VecDuplicate(user->lNvert, &ratio));
681 PetscCall(FillLocalScalar(user, numerator, 0.0));
682 PetscCall(FillLocalScalar(user, denominator, 0.0));
683 PetscCall(FillLocalScalar(user, ratio, 0.0));
684 PetscCall(SetInteriorScalar(user, denominator, 1.0));
685
686 /* Depends on eta alone, so averaging over xi and zeta must leave it untouched. */
687 PetscCall(DMDAVecGetArray(user->da, numerator, &num));
688 for (PetscInt k = 1; k < 6; ++k)
689 for (PetscInt j = 1; j < 6; ++j)
690 for (PetscInt i = 1; i < 6; ++i) num[k][j][i] = (PetscReal)j;
691 PetscCall(DMDAVecRestoreArray(user->da, numerator, &num));
692
693 PetscCall(SpatialTargetPlanCreate(user, FIELD_ID_CS,
695 PetscCall(PicurvSpatialRatioAverage(user, &plan, numerator, denominator, NULL, ik,
696 PETSC_COMM_WORLD, ratio, NULL));
697
698 PetscCall(ReadLocalScalar(user, ratio, 2, 1, 3, &at_low));
699 PetscCall(ReadLocalScalar(user, ratio, 4, 5, 2, &at_high));
700 PetscCall(PicurvAssertRealNear(1.0, at_low, 1.0e-13,
701 "averaging over xi and zeta should preserve the eta profile"));
702 PetscCall(PicurvAssertRealNear(5.0, at_high, 1.0e-13,
703 "each retained eta plane should keep its own average"));
704
705 PetscCall(VecDestroy(&numerator));
706 PetscCall(VecDestroy(&denominator));
707 PetscCall(VecDestroy(&ratio));
708 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
709 PetscFunctionReturn(0);
710}
711
712/**
713 * @brief Tests that a spatial average ignores the periodic duplicate planes.
714 *
715 * On a periodic block, index 0 and index `m-1` hold copies of interior cells. Counting
716 * them would weight those cells twice. The test poisons both planes and requires the
717 * average not to move.
718 */
720{
721 SimCtx *simCtx = NULL;
722 UserCtx *user = NULL;
723 Vec numerator, denominator, ratio;
725 PetscReal ***num = NULL;
726 DMDALocalInfo info;
727 PetscReal clean = 0.0, poisoned = 0.0;
728 const PetscBool all[3] = {PETSC_TRUE, PETSC_TRUE, PETSC_TRUE};
729
730 PetscFunctionBeginUser;
731 PetscCall(PicurvCreateMinimalContextsWithPeriodicity(&simCtx, &user, 6, 6, 6,
732 PETSC_TRUE, PETSC_TRUE, PETSC_TRUE));
733 DeclarePeriodicFaces(user, PETSC_TRUE, PETSC_TRUE, PETSC_TRUE);
734 PetscCall(DMDAGetLocalInfo(user->da, &info));
735 PetscCall(VecSet(user->lNvert, 0.0));
736 PetscCall(VecSet(user->lAj, 1.0));
737
738 PetscCall(VecDuplicate(user->lNvert, &numerator));
739 PetscCall(VecDuplicate(user->lNvert, &denominator));
740 PetscCall(VecDuplicate(user->lNvert, &ratio));
741 PetscCall(FillLocalScalar(user, numerator, 0.0));
742 PetscCall(FillLocalScalar(user, denominator, 0.0));
743 PetscCall(FillLocalScalar(user, ratio, 0.0));
744 PetscCall(SetInteriorScalar(user, numerator, 2.0));
745 PetscCall(SetInteriorScalar(user, denominator, 1.0));
746
747 PetscCall(SpatialTargetPlanCreate(user, FIELD_ID_CS,
749 PetscCall(PicurvSpatialRatioAverage(user, &plan, numerator, denominator, NULL, all,
750 PETSC_COMM_WORLD, ratio, NULL));
751 PetscCall(ReadLocalScalar(user, ratio, 2, 2, 2, &clean));
752
753 /* Poison both layout boundary planes in every direction. */
754 PetscCall(DMDAVecGetArray(user->da, numerator, &num));
755 for (PetscInt k = 0; k < info.mz; ++k)
756 for (PetscInt j = 0; j < info.my; ++j)
757 for (PetscInt i = 0; i < info.mx; ++i) {
758 const PetscBool duplicate = (PetscBool)(i == 0 || j == 0 || k == 0 ||
759 i == info.mx - 1 || j == info.my - 1 || k == info.mz - 1);
760
761 if (duplicate) num[k][j][i] = 1.0e6;
762 }
763 PetscCall(DMDAVecRestoreArray(user->da, numerator, &num));
764
765 PetscCall(SpatialTargetPlanCreate(user, FIELD_ID_CS,
767 PetscCall(PicurvSpatialRatioAverage(user, &plan, numerator, denominator, NULL, all,
768 PETSC_COMM_WORLD, ratio, NULL));
769 PetscCall(ReadLocalScalar(user, ratio, 2, 2, 2, &poisoned));
770
771 PetscCall(PicurvAssertRealNear(2.0, clean, 1.0e-13,
772 "the interior average should be the interior value"));
773 PetscCall(PicurvAssertRealNear(clean, poisoned, 1.0e-13,
774 "the duplicate planes must not contribute to a spatial average"));
775
776 PetscCall(VecDestroy(&numerator));
777 PetscCall(VecDestroy(&denominator));
778 PetscCall(VecDestroy(&ratio));
779 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
780 PetscFunctionReturn(0);
781}
782
783/** @brief Tests that homogeneous averaging falls back to the block's periodic axes. */
785{
786 SimCtx *simCtx = NULL;
787 UserCtx *user = NULL;
788 PetscBool direction[3];
789
790 PetscFunctionBeginUser;
791 /* Periodic in xi and zeta only: the channel arrangement. The fixture sets the
792 resolved periodicity flags, which are what the closure reads; the boundary faces
793 are left alone here so the test cannot pass by consulting them instead. */
794 PetscCall(PicurvCreateMinimalContextsWithPeriodicity(&simCtx, &user, 6, 6, 6,
795 PETSC_TRUE, PETSC_FALSE, PETSC_TRUE));
797
798 PetscCall(ResolveLESAveragingDirections(user, direction));
799 PetscCall(PicurvAssertBool(direction[0], "a periodic xi axis should be averaged over"));
800 PetscCall(PicurvAssertBool((PetscBool)(!direction[1]),
801 "a wall-normal axis should keep its own coefficient profile"));
802 PetscCall(PicurvAssertBool(direction[2], "a periodic zeta axis should be averaged over"));
803
804 /* An explicit selection overrides the periodic default. */
805 simCtx->les_config.averaging_direction[0] = PETSC_TRUE;
806 simCtx->les_config.averaging_direction[1] = PETSC_FALSE;
807 simCtx->les_config.averaging_direction[2] = PETSC_FALSE;
808 PetscCall(ResolveLESAveragingDirections(user, direction));
809 PetscCall(PicurvAssertBool(direction[0], "an explicit direction should be honoured"));
810 PetscCall(PicurvAssertBool((PetscBool)(!direction[2]),
811 "an explicit selection should replace the periodic default"));
812
813 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
814 PetscFunctionReturn(0);
815}
816
817/** @brief Tests that local and global averaging ignore the configured direction list. */
819{
820 SimCtx *simCtx = NULL;
821 UserCtx *user = NULL;
822 PetscBool direction[3];
823
824 PetscFunctionBeginUser;
825 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
826 simCtx->les_config.averaging_direction[0] = PETSC_TRUE;
827
829 PetscCall(ResolveLESAveragingDirections(user, direction));
830 PetscCall(PicurvAssertBool((PetscBool)(!direction[0] && !direction[1] && !direction[2]),
831 "local averaging should select no direction at all"));
832
834 PetscCall(ResolveLESAveragingDirections(user, direction));
835 PetscCall(PicurvAssertBool((PetscBool)(direction[0] && direction[1] && direction[2]),
836 "global averaging should select every direction"));
837
838 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
839 PetscFunctionReturn(0);
840}
841
842/**
843 * @brief Tests that the constant model builds its viscosity without a coefficient field.
844 *
845 * `UserCtx::CS` and `UserCtx::lCs` are deliberately left unallocated. If the constant
846 * path ever reaches for them again this case crashes rather than silently reintroducing
847 * a field of one repeated number.
848 */
850{
851 SimCtx *simCtx = NULL;
852 UserCtx *user = NULL;
853 Cmpnts ***ucat = NULL;
854 PetscReal ***nu_t = NULL;
855 const PetscReal constant_cs = 0.2;
856 /* u = x gives |S| = sqrt(2); with Aj = 1 the filter width is 1. */
857 const PetscReal expected = constant_cs * constant_cs * PetscSqrtReal(2.0);
858
859 PetscFunctionBeginUser;
860 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 5, 5, 5));
861 simCtx->les = CONSTANT_SMAGORINSKY;
862 simCtx->les_config.constant_cs = constant_cs;
863
864 PetscCall(DMCreateGlobalVector(user->da, &user->Nu_t));
865 PetscCall(DMCreateLocalVector(user->da, &user->lNu_t));
866 PetscCall(VecSet(user->Aj, 1.0));
867 PetscCall(VecSet(user->Nu_t, 0.0));
868
869 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
870 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k)
871 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j)
872 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
873 ucat[k][j][i].x = (PetscReal)i;
874 ucat[k][j][i].y = 0.0;
875 ucat[k][j][i].z = 0.0;
876 }
877 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
878 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
879 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
880 PetscCall(DMGlobalToLocalBegin(user->da, user->Aj, INSERT_VALUES, user->lAj));
881 PetscCall(DMGlobalToLocalEnd(user->da, user->Aj, INSERT_VALUES, user->lAj));
882
883 PetscCall(PicurvAssertBool((PetscBool)(user->lCs == NULL),
884 "the constant model should allocate no coefficient field"));
885 PetscCall(ComputeEddyViscosityLES(user));
886
887 PetscCall(DMDAVecGetArrayRead(user->da, user->Nu_t, &nu_t));
888 PetscCall(PicurvAssertRealNear(expected, nu_t[2][2][2], 1.0e-9,
889 "the constant model should read its coefficient from configuration"));
890 PetscCall(DMDAVecRestoreArrayRead(user->da, user->Nu_t, &nu_t));
891
892 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
893 PetscFunctionReturn(0);
894}
895
896/** @brief Tests that the dynamic procedure refuses to run for the constant model. */
898{
899 SimCtx *simCtx = NULL;
900 UserCtx *user = NULL;
901 PetscErrorCode call_ierr;
902
903 PetscFunctionBeginUser;
904 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 5, 5, 5));
905 simCtx->les = CONSTANT_SMAGORINSKY;
906 simCtx->step = 5;
907 simCtx->StartStep = 0;
908
909 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
910 call_ierr = ComputeSmagorinskyConstant(user);
911 PetscCall(PetscPopErrorHandler());
912
913 PetscCall(PicurvAssertBool((PetscBool)(call_ierr != 0),
914 "the dynamic procedure should reject a model that has no "
915 "coefficient field to fill"));
916
917 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
918 PetscFunctionReturn(0);
919}
920
921/**
922 * @brief Tests that the dynamic procedure returns a zero coefficient on uniform flow.
923 *
924 * Exercises the whole path: the strain precompute and its halo, the stencil gather,
925 * every test filter, both contractions, the averaging, and the limiting. A uniform
926 * field has no strain and no stress between the two filter widths, so the Germano
927 * identity has nothing to fit and the coefficient must come out exactly zero.
928 */
930{
931 SimCtx *simCtx = NULL;
932 UserCtx *user = NULL;
933 Cmpnts ***ucat = NULL;
934 PetscReal ***coefficient = NULL;
935
936 PetscFunctionBeginUser;
937 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
938 simCtx->les = DYNAMIC_SMAGORINSKY;
939 simCtx->step = 5;
940 simCtx->StartStep = 0;
941
942 PetscCall(DMCreateGlobalVector(user->da, &user->CS));
943 PetscCall(DMCreateLocalVector(user->da, &user->lCs));
944 PetscCall(VecSet(user->Aj, 1.0));
945 PetscCall(VecSet(user->lAj, 1.0));
946 PetscCall(VecSet(user->lNvert, 0.0));
947
948 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
949 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k)
950 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j)
951 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
952 ucat[k][j][i].x = 1.25;
953 ucat[k][j][i].y = -0.5;
954 ucat[k][j][i].z = 3.0;
955 }
956 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
957 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
958 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
959
960 PetscCall(ComputeSmagorinskyConstant(user));
961
962 PetscCall(DMDAVecGetArrayRead(user->da, user->CS, &coefficient));
963 for (PetscInt k = 1; k < 6; ++k)
964 for (PetscInt j = 1; j < 6; ++j)
965 for (PetscInt i = 1; i < 6; ++i) {
966 PetscCall(PicurvAssertRealNear(0.0, coefficient[k][j][i], 1.0e-20,
967 "a uniform field should give the dynamic procedure "
968 "nothing to fit"));
969 }
970 PetscCall(DMDAVecRestoreArrayRead(user->da, user->CS, &coefficient));
971
972 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
973 PetscFunctionReturn(0);
974}
975
976/**
977 * @brief Tests the dynamic procedure end to end on a periodic sheared field.
978 *
979 * Runs the full procedure on a block that is periodic in all three directions, where
980 * the strain precompute has to reach across the wrap correctly, and asks for global
981 * averaging. Two things must hold: every cell receives the same finite coefficient,
982 * because a global average is one number for the block; and the coefficient stays
983 * inside the configured ceiling.
984 */
986{
987 SimCtx *simCtx = NULL;
988 UserCtx *user = NULL;
989 Cmpnts ***ucat = NULL;
990 PetscReal ***coefficient = NULL;
991 PetscReal reference = 0.0;
992 const FieldId cell_fields[] = {FIELD_ID_UCAT};
993 const PetscReal wavenumber = 2.0 * PETSC_PI / 5.0; /* five distinct cells per direction */
994
995 PetscFunctionBeginUser;
996 PetscCall(PicurvCreateMinimalContextsWithPeriodicity(&simCtx, &user, 6, 6, 6,
997 PETSC_TRUE, PETSC_TRUE, PETSC_TRUE));
998 DeclarePeriodicFaces(user, PETSC_TRUE, PETSC_TRUE, PETSC_TRUE);
999 simCtx->les = DYNAMIC_SMAGORINSKY;
1000 simCtx->step = 5;
1001 simCtx->StartStep = 0;
1003
1004 PetscCall(DMCreateGlobalVector(user->da, &user->CS));
1005 PetscCall(DMCreateLocalVector(user->da, &user->lCs));
1006 PetscCall(VecSet(user->Aj, 1.0));
1007 PetscCall(VecSet(user->lAj, 1.0));
1008 PetscCall(VecSet(user->lNvert, 0.0));
1009
1010 /* A divergence-free periodic shear: enough structure that both contractions are
1011 nonzero, and smooth enough that the coefficient stays in range. */
1012 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
1013 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k)
1014 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j)
1015 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
1016 ucat[k][j][i].x = PetscSinReal(wavenumber * (PetscReal)j);
1017 ucat[k][j][i].y = PetscSinReal(wavenumber * (PetscReal)k);
1018 ucat[k][j][i].z = PetscSinReal(wavenumber * (PetscReal)i);
1019 }
1020 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
1021 PetscCall(SynchronizePeriodicCellFields(user, 1, cell_fields));
1022 PetscCall(UpdateLocalGhosts(user, FIELD_ID_UCAT));
1023
1024 PetscCall(ComputeSmagorinskyConstant(user));
1025
1026 PetscCall(DMDAVecGetArrayRead(user->da, user->CS, &coefficient));
1027 reference = coefficient[1][1][1];
1028 PetscCall(PicurvAssertBool((PetscBool)(PetscIsNormalReal(reference) || reference == 0.0),
1029 "the dynamic coefficient must be finite"));
1030 PetscCall(PicurvAssertBool((PetscBool)(reference >= 0.0 &&
1031 reference <= simCtx->les_config.max_cs *
1032 simCtx->les_config.max_cs),
1033 "clamping should keep the coefficient inside its ceiling"));
1034 for (PetscInt k = 1; k < 6; ++k)
1035 for (PetscInt j = 1; j < 6; ++j)
1036 for (PetscInt i = 1; i < 6; ++i) {
1037 PetscCall(PicurvAssertRealNear(reference, coefficient[k][j][i], 1.0e-13,
1038 "global averaging should give the block one coefficient"));
1039 }
1040 PetscCall(DMDAVecRestoreArrayRead(user->da, user->CS, &coefficient));
1041
1042 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1043 PetscFunctionReturn(0);
1044}
1045
1046/**
1047 * @brief Runs the dynamic procedure with the Simpson kernel on a linear field and returns
1048 * the raw coefficient at the block centre.
1049 * @param[in] width_ratio Configured test-to-grid width ratio.
1050 * @param[out] coefficient The unclipped dynamic coefficient at cell (4, 4, 4).
1051 */
1052static PetscErrorCode SimpsonCoefficientOnLinearField(PetscReal width_ratio, PetscReal *coefficient)
1053{
1054 SimCtx *simCtx = NULL;
1055 UserCtx *user = NULL;
1056 Cmpnts ***ucat = NULL;
1057 PetscReal ***cs = NULL;
1058 const PetscReal gamma = 0.1;
1059
1060 PetscFunctionBeginUser;
1061 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 8, 8, 8));
1062 simCtx->les = DYNAMIC_SMAGORINSKY;
1063 simCtx->step = 5;
1064 simCtx->StartStep = 0;
1066 simCtx->les_config.test_filter_width_ratio = width_ratio;
1069
1070 PetscCall(DMCreateGlobalVector(user->da, &user->CS));
1071 PetscCall(DMCreateLocalVector(user->da, &user->lCs));
1072 PetscCall(VecSet(user->Aj, 1.0));
1073 PetscCall(VecSet(user->lAj, 1.0));
1074 PetscCall(VecSet(user->lNvert, 0.0));
1075
1076 /* A divergence-free linear strain: the strain and both filtered strain terms are
1077 uniform, so the model tensor scales exactly with (alpha - 1), while the Leonard
1078 stress is the stencil's second moment along i and does not depend on alpha. */
1079 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
1080 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k)
1081 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j)
1082 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
1083 ucat[k][j][i].x = -gamma * (PetscReal)i;
1084 ucat[k][j][i].y = gamma * (PetscReal)j;
1085 ucat[k][j][i].z = 0.0;
1086 }
1087 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
1088 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
1089 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
1090
1091 PetscCall(ComputeSmagorinskyConstant(user));
1092
1093 PetscCall(DMDAVecGetArrayRead(user->da, user->CS, &cs));
1094 *coefficient = cs[4][4][4];
1095 PetscCall(DMDAVecRestoreArrayRead(user->da, user->CS, &cs));
1096 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1097 PetscFunctionReturn(0);
1098}
1099
1100/**
1101 * @brief Tests that the Simpson kernel's width ratio applies to the two directions it filters.
1102 *
1103 * Simpson filters i and k only, so the cube-root test-to-grid width ratio is
1104 * ratio^(2/3) and alpha is ratio^(4/3), not ratio^2. On a linear field the coefficient
1105 * is proportional to 1 / (alpha - 1), so two ratios fix the exponent without depending
1106 * on any other convention in the procedure.
1107 */
1109{
1110 PetscReal coefficient_2 = 0.0, coefficient_4 = 0.0;
1111 const PetscReal alpha_2 = PetscPowReal(2.0, 4.0 / 3.0);
1112 const PetscReal alpha_4 = PetscPowReal(4.0, 4.0 / 3.0);
1113
1114 PetscFunctionBeginUser;
1115 PetscCall(SimpsonCoefficientOnLinearField(2.0, &coefficient_2));
1116 PetscCall(SimpsonCoefficientOnLinearField(4.0, &coefficient_4));
1117 PetscCall(PicurvAssertBool((PetscBool)(coefficient_2 > 0.0 && coefficient_4 > 0.0),
1118 "a linear strain should give a positive Simpson coefficient"));
1119 PetscCall(PicurvAssertRealNear((alpha_4 - 1.0) / (alpha_2 - 1.0), coefficient_2 / coefficient_4,
1120 1.0e-10,
1121 "the Simpson coefficient should scale as 1/(ratio^(4/3) - 1)"));
1122 PetscFunctionReturn(0);
1123}
1124
1125/**
1126 * @brief Entry point for the LES closure suite.
1127 */
1128int main(int argc, char **argv)
1129{
1130 PetscErrorCode ierr;
1131 const PicurvTestCase cases[] = {
1132 {"sym-tensor-algebra", TestSymTensorAlgebra},
1133 {"strain-rate-from-gradients", TestStrainRateFromGradients},
1134 {"filter-width-models-separate-on-stretched-cell", TestFilterWidthModelsSeparateOnStretchedCell},
1135 {"filter-width-is-independent-of-cell-orientation", TestFilterWidthIsIndependentOfCellOrientation},
1136 {"scotti-filter-width-matches-closed-form", TestScottiFilterWidthMatchesClosedForm},
1137 {"wale-and-vreman-kernels", TestWALEAndVremanKernels},
1138 {"leonard-stress-vanishes-on-uniform-flow", TestLeonardStressVanishesOnUniformFlow},
1139 {"germano-model-tensor-on-constant-strain", TestGermanoModelTensorOnConstantStrain},
1140 {"germano-model-tensor-uses-filtered-product", TestGermanoModelTensorUsesFilteredProduct},
1141 {"clip-model-coefficient-modes", TestClipModelCoefficientModes},
1142 {"eddy-viscosity-floor-bounds-total-viscosity", TestEddyViscosityFloorBoundsTotalViscosity},
1143 {"subgrid-kinetic-energy", TestSubgridKineticEnergy},
1144 {"average-ratio-local-is-pointwise", TestAverageRatioLocalIsPointwise},
1145 {"average-ratio-divides-summed-fields", TestAverageRatioDividesSummedFields},
1146 {"average-ratio-retains-unaveraged-direction", TestAverageRatioRetainsUnaveragedDirection},
1147 {"average-ratio-ignores-periodic-duplicate-planes", TestAverageRatioIgnoresPeriodicDuplicatePlanes},
1148 {"homogeneous-averaging-derives-periodic-axes", TestHomogeneousAveragingDerivesPeriodicAxes},
1149 {"averaging-modes-select-their-own-directions", TestAveragingModesSelectTheirOwnDirections},
1150 {"constant-model-needs-no-coefficient-field", TestConstantModelNeedsNoCoefficientField},
1151 {"dynamic-procedure-rejects-constant-model", TestDynamicProcedureRejectsConstantModel},
1152 {"dynamic-procedure-vanishes-on-uniform-flow", TestDynamicProcedureVanishesOnUniformFlow},
1153 {"dynamic-procedure-global-average-is-uniform", TestDynamicProcedureGlobalAverageIsUniform},
1154 {"dynamic-procedure-simpson-ratio-counts-filtered-directions", TestDynamicProcedureSimpsonRatioCountsFilteredDirections},
1155 };
1156
1157 ierr = PetscInitialize(&argc, &argv, NULL, "PICurv LES closure tests");
1158 if (ierr) {
1159 return (int)ierr;
1160 }
1161
1162 ierr = PicurvRunTests("unit-les", cases, sizeof(cases) / sizeof(cases[0]));
1163 if (ierr) {
1164 PetscFinalize();
1165 return (int)ierr;
1166 }
1167
1168 ierr = PetscFinalize();
1169 return (int)ierr;
1170}
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
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:952
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:595
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:497
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:374
PetscReal ClipModelCoefficient(PetscReal coefficient, const LESConfig *config, PetscBool *limited)
Applies the configured admissible range to one model coefficient.
Definition les.c:466
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:410
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:359
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:715
PetscReal WALEEddyViscosity(Cmpnts dudx, Cmpnts dvdx, Cmpnts dwdx, PetscReal delta, PetscReal coefficient)
WALE eddy viscosity (Nicoud & Ducros 1999) from the resolved velocity gradient.
Definition les.c:520
PetscReal VremanEddyViscosity(Cmpnts dudx, Cmpnts dvdx, Cmpnts dwdx, const Cmpnts edges[3], PetscReal coefficient)
Vreman eddy viscosity (Vreman 2004), weighting each grid direction by its own spacing.
Definition les.c:557
PetscErrorCode UpdateLocalGhosts(UserCtx *user, FieldId field_id)
Updates the local vector (including ghost points) from its corresponding global vector.
Definition setup.c:2489
PetscErrorCode LESConfigSetDefaults(LESConfig *config)
Installs the shipped defaults for every LES closure parameter.
Definition setup.c:206
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:489
static PetscErrorCode SimpsonCoefficientOnLinearField(PetscReal width_ratio, PetscReal *coefficient)
Runs the dynamic procedure with the Simpson kernel on a linear field and returns the raw coefficient ...
Definition test_les.c:1052
static PetscErrorCode TestAverageRatioDividesSummedFields(void)
Tests that averaging divides summed numerators by summed denominators.
Definition test_les.c:603
static PetscErrorCode TestAverageRatioRetainsUnaveragedDirection(void)
Tests that averaging over two directions leaves a profile along the third.
Definition test_les.c:662
static PetscErrorCode TestHomogeneousAveragingDerivesPeriodicAxes(void)
Tests that homogeneous averaging falls back to the block's periodic axes.
Definition test_les.c:784
static PetscErrorCode TestGermanoModelTensorOnConstantStrain(void)
Tests the model tensor where the two filter terms provably coincide.
Definition test_les.c:316
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:534
int main(int argc, char **argv)
Entry point for the LES closure suite.
Definition test_les.c:1128
static PetscErrorCode TestEddyViscosityFloorBoundsTotalViscosity(void)
Tests eddy-viscosity assembly and the total-viscosity floor.
Definition test_les.c:434
static Cmpnts RotateForInvarianceTest(Cmpnts v)
Rotates a vector by Rz(40 deg) then Rx(25 deg).
Definition test_les.c:217
static PetscErrorCode TestDynamicProcedureVanishesOnUniformFlow(void)
Tests that the dynamic procedure returns a zero coefficient on uniform flow.
Definition test_les.c:929
static PetscErrorCode TestScottiFilterWidthMatchesClosedForm(void)
Tests the Scotti aspect-ratio correction against its closed form.
Definition test_les.c:186
static PetscErrorCode TestLeonardStressVanishesOnUniformFlow(void)
Tests that the Leonard stress vanishes on a uniform velocity field.
Definition test_les.c:292
static PetscErrorCode FillLocalScalar(UserCtx *user, Vec local, PetscReal value)
Fills a ghosted local scalar field over the whole local array.
Definition test_les.c:478
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:849
static PetscErrorCode TestAverageRatioIgnoresPeriodicDuplicatePlanes(void)
Tests that a spatial average ignores the periodic duplicate planes.
Definition test_les.c:719
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:513
static PetscErrorCode TestDynamicProcedureSimpsonRatioCountsFilteredDirections(void)
Tests that the Simpson kernel's width ratio applies to the two directions it filters.
Definition test_les.c:1108
static PetscErrorCode TestAveragingModesSelectTheirOwnDirections(void)
Tests that local and global averaging ignore the configured direction list.
Definition test_les.c:818
static PetscErrorCode TestSubgridKineticEnergy(void)
Tests the Yoshizawa subgrid kinetic energy against its closed form.
Definition test_les.c:459
static void RotateGradientForInvarianceTest(const Cmpnts rows[3], Cmpnts out[3])
Rotates a velocity gradient g_ij = du_i/dx_j as R g R^T, rows passed as dudx, dvdx,...
Definition test_les.c:226
static PetscErrorCode TestFilterWidthModelsSeparateOnStretchedCell(void)
Tests that the three filter-width models separate on an anisotropic cell.
Definition test_les.c:93
static PetscErrorCode TestWALEAndVremanKernels(void)
Tests the WALE and Vreman kernels against independent evaluations.
Definition test_les.c:252
static PetscErrorCode TestDynamicProcedureRejectsConstantModel(void)
Tests that the dynamic procedure refuses to run for the constant model.
Definition test_les.c:897
static PetscErrorCode TestGermanoModelTensorUsesFilteredProduct(void)
Tests that the model tensor uses the filtered product, not the filtered factors.
Definition test_les.c:349
static void ParallelepipedMetrics(Cmpnts a, Cmpnts b, Cmpnts c, Cmpnts *csi, Cmpnts *eta, Cmpnts *zet, PetscReal *aj)
Face-area vectors and inverse volume of the parallelepiped spanned by a, b, c.
Definition test_les.c:123
static PetscErrorCode TestFilterWidthIsIndependentOfCellOrientation(void)
Tests that a filter width is a property of the cell, not of its orientation.
Definition test_les.c:142
static PetscErrorCode TestAverageRatioLocalIsPointwise(void)
Tests that an empty direction set reproduces the pointwise local model.
Definition test_les.c:548
static PetscErrorCode TestDynamicProcedureGlobalAverageIsUniform(void)
Tests the dynamic procedure end to end on a periodic sheared field.
Definition test_les.c:985
static PetscErrorCode TestClipModelCoefficientModes(void)
Tests each limiting mode, including the sign that carries backscatter.
Definition test_les.c:401
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:551
@ CONSTANT_SMAGORINSKY
Definition variables.h:550
@ PERIODIC
Definition variables.h:318
BoundaryFaceConfig boundary_faces[6]
Definition variables.h:1099
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
@ LES_CLIP_CLIP_NEGATIVE
Definition variables.h:619
@ LES_CLIP_CLAMP
Definition variables.h:618
@ LES_CLIP_NONE
Definition variables.h:620
LESTestFilterKernel
Selects the discrete test-filter kernel used by the dynamic procedure.
Definition variables.h:591
@ LES_TEST_FILTER_SIMPSON_IK
Definition variables.h:593
@ LES_TEST_FILTER_VOLUME_WEIGHTED_BOX
Definition variables.h:592
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
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
Vec lNu_t
Definition variables.h:1156
PetscScalar z
Definition variables.h:122
@ LES_AVERAGING_LOCAL
Definition variables.h:605
@ LES_AVERAGING_GLOBAL
Definition variables.h:607
@ LES_AVERAGING_HOMOGENEOUS
Definition variables.h:606
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 step
Definition variables.h:867
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
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 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
A 3D point or vector with PetscScalar components.
Definition variables.h:121
Every user-selectable parameter of the LES closure.
Definition variables.h:629
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 / ν)