PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
Functions
test_les.c File Reference

C unit tests for the LES subgrid-scale closure. More...

#include "test_support.h"
#include "les.h"
#include "setup.h"
#include "statistics_target.h"
Include dependency graph for test_les.c:

Go to the source code of this file.

Functions

static PetscErrorCode TestSymTensorAlgebra (void)
 Tests the symmetric-tensor primitives against hand-computed values.
 
static PetscErrorCode TestStrainRateFromGradients (void)
 Tests strain-rate assembly and its magnitude for a known velocity gradient.
 
static PetscErrorCode TestFilterWidthModelsSeparateOnStretchedCell (void)
 Tests that the three filter-width models separate on an anisotropic cell.
 
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.
 
static PetscErrorCode TestFilterWidthIsIndependentOfCellOrientation (void)
 Tests that a filter width is a property of the cell, not of its orientation.
 
static PetscErrorCode TestScottiFilterWidthMatchesClosedForm (void)
 Tests the Scotti aspect-ratio correction against its closed form.
 
static Cmpnts RotateForInvarianceTest (Cmpnts v)
 Rotates a vector by Rz(40 deg) then Rx(25 deg).
 
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, dwdx.
 
static PetscErrorCode TestWALEAndVremanKernels (void)
 Tests the WALE and Vreman kernels against independent evaluations.
 
static PetscErrorCode TestLeonardStressVanishesOnUniformFlow (void)
 Tests that the Leonard stress vanishes on a uniform velocity field.
 
static PetscErrorCode TestGermanoModelTensorOnConstantStrain (void)
 Tests the model tensor where the two filter terms provably coincide.
 
static PetscErrorCode TestGermanoModelTensorUsesFilteredProduct (void)
 Tests that the model tensor uses the filtered product, not the filtered factors.
 
static PetscErrorCode TestClipModelCoefficientModes (void)
 Tests each limiting mode, including the sign that carries backscatter.
 
static PetscErrorCode TestEddyViscosityFloorBoundsTotalViscosity (void)
 Tests eddy-viscosity assembly and the total-viscosity floor.
 
static PetscErrorCode TestSubgridKineticEnergy (void)
 Tests the Yoshizawa subgrid kinetic energy against its closed form.
 
static PetscErrorCode FillLocalScalar (UserCtx *user, Vec local, PetscReal value)
 Fills a ghosted local scalar field over the whole local array.
 
static PetscErrorCode SetInteriorScalar (UserCtx *user, Vec local, PetscReal value)
 Writes a scalar into every cell the closure treats as owned and interior.
 
static PetscErrorCode ReadLocalScalar (UserCtx *user, Vec local, PetscInt i, PetscInt j, PetscInt k, PetscReal *value)
 Reads one cell of a ghosted local scalar field.
 
static void DeclarePeriodicFaces (UserCtx *user, PetscBool xi, PetscBool eta, PetscBool zeta)
 Declares periodic boundary pairs on the axes a test wants homogeneous.
 
static PetscErrorCode TestAverageRatioLocalIsPointwise (void)
 Tests that an empty direction set reproduces the pointwise local model.
 
static PetscErrorCode TestAverageRatioDividesSummedFields (void)
 Tests that averaging divides summed numerators by summed denominators.
 
static PetscErrorCode TestAverageRatioRetainsUnaveragedDirection (void)
 Tests that averaging over two directions leaves a profile along the third.
 
static PetscErrorCode TestAverageRatioIgnoresPeriodicDuplicatePlanes (void)
 Tests that a spatial average ignores the periodic duplicate planes.
 
static PetscErrorCode TestHomogeneousAveragingDerivesPeriodicAxes (void)
 Tests that homogeneous averaging falls back to the block's periodic axes.
 
static PetscErrorCode TestAveragingModesSelectTheirOwnDirections (void)
 Tests that local and global averaging ignore the configured direction list.
 
static PetscErrorCode TestConstantModelNeedsNoCoefficientField (void)
 Tests that the constant model builds its viscosity without a coefficient field.
 
static PetscErrorCode TestDynamicProcedureRejectsConstantModel (void)
 Tests that the dynamic procedure refuses to run for the constant model.
 
static PetscErrorCode TestDynamicProcedureVanishesOnUniformFlow (void)
 Tests that the dynamic procedure returns a zero coefficient on uniform flow.
 
static PetscErrorCode TestDynamicProcedureGlobalAverageIsUniform (void)
 Tests the dynamic procedure end to end on a periodic sheared field.
 
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 at the block centre.
 
static PetscErrorCode TestDynamicProcedureSimpsonRatioCountsFilteredDirections (void)
 Tests that the Simpson kernel's width ratio applies to the two directions it filters.
 
int main (int argc, char **argv)
 Entry point for the LES closure suite.
 

Detailed Description

C unit tests for the LES subgrid-scale closure.

The suite is layered the way the module is. Pure kernels are checked against hand computed values with no PETSc objects at all; the Germano pieces are checked on fields whose answers are known in closed form; and the driver routines are checked on a small DMDA fixture.

Two of the cases exist because of specific defects and are worth naming. The "filtered-product" case pins the distinction between the filter of a product and the product of filtered factors, which the model tensor once collapsed. The "duplicate-plane" case pins the requirement that a spatial average count each physical cell once on a periodic block, where the two layout boundary planes are copies of interior cells.

Definition in file test_les.c.

Function Documentation

◆ TestSymTensorAlgebra()

static PetscErrorCode TestSymTensorAlgebra ( void  )
static

Tests the symmetric-tensor primitives against hand-computed values.

Definition at line 29 of file test_les.c.

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}
PetscReal SymTensorNormSq(SymTensor t)
Returns the squared Frobenius norm t_ij t_ij.
Definition les.c:250
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 SymTensorTrace(SymTensor t)
Returns the trace t_kk.
Definition les.c:186
PetscReal SymTensorContract(SymTensor a, SymTensor b)
Contracts two symmetric tensors as a_ij b_ij.
Definition les.c:217
PetscErrorCode PicurvAssertRealNear(PetscReal expected, PetscReal actual, PetscReal tol, const char *context)
Asserts that two real values agree within tolerance.
PetscReal xx
Definition variables.h:139
PetscReal yz
Definition variables.h:139
PetscReal zz
Definition variables.h:139
PetscReal xy
Definition variables.h:139
A 3D point or vector with PetscScalar components.
Definition variables.h:121
A symmetric second-order tensor stored by its six independent components.
Definition variables.h:138
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestStrainRateFromGradients()

static PetscErrorCode TestStrainRateFromGradients ( void  )
static

Tests strain-rate assembly and its magnitude for a known velocity gradient.

Definition at line 68 of file test_les.c.

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}
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
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestFilterWidthModelsSeparateOnStretchedCell()

static PetscErrorCode TestFilterWidthModelsSeparateOnStretchedCell ( void  )
static

Tests that the three filter-width models separate on an anisotropic cell.

Definition at line 93 of file test_les.c.

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}
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 PicurvAssertBool(PetscBool value, const char *context)
Asserts that one boolean condition is true.
@ 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
Here is the call graph for this function:
Here is the caller graph for this function:

◆ ParallelepipedMetrics()

static void ParallelepipedMetrics ( Cmpnts  a,
Cmpnts  b,
Cmpnts  c,
Cmpnts *  csi,
Cmpnts *  eta,
Cmpnts *  zet,
PetscReal *  aj 
)
static

Face-area vectors and inverse volume of the parallelepiped spanned by a, b, c.

Definition at line 123 of file test_les.c.

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}
PetscScalar x
Definition variables.h:122
PetscScalar z
Definition variables.h:122
PetscScalar y
Definition variables.h:122
Here is the caller graph for this function:

◆ TestFilterWidthIsIndependentOfCellOrientation()

static PetscErrorCode TestFilterWidthIsIndependentOfCellOrientation ( void  )
static

Tests that a filter width is a property of the cell, not of its orientation.

The widths used to come from the Cartesian components of the cell diagonal. On the Humphrey bend an identical wall cell got a geometric-mean width three times larger, and so nine times the eddy viscosity, merely because the bend had turned it through 45 degrees relative to the x and y axes.

Definition at line 142 of file test_les.c.

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}
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
LESFilterWidthModel
Selects how a cell's grid filter width is derived from its metrics.
Definition variables.h:579
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestScottiFilterWidthMatchesClosedForm()

static PetscErrorCode TestScottiFilterWidthMatchesClosedForm ( void  )
static

Tests the Scotti aspect-ratio correction against its closed form.

Scotti, Meneveau & Lilly (1993) multiply the cube root of the extents by f(a1, a2) = cosh(sqrt((4/27)(ln^2 a1 - ln a1 ln a2 + ln^2 a2))), with a1, a2 the two shorter extents over the longest. f is exactly 1 for a cube, so the width must equal the cube root there, and it must be unchanged by rotating the cell.

Definition at line 186 of file test_les.c.

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}
@ LES_FILTER_WIDTH_SCOTTI
Definition variables.h:583
Here is the call graph for this function:
Here is the caller graph for this function:

◆ RotateForInvarianceTest()

static Cmpnts RotateForInvarianceTest ( Cmpnts  v)
static

Rotates a vector by Rz(40 deg) then Rx(25 deg).

Definition at line 217 of file test_les.c.

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}
Here is the caller graph for this function:

◆ RotateGradientForInvarianceTest()

static void RotateGradientForInvarianceTest ( const Cmpnts  rows[3],
Cmpnts  out[3] 
)
static

Rotates a velocity gradient g_ij = du_i/dx_j as R g R^T, rows passed as dudx, dvdx, dwdx.

Definition at line 226 of file test_les.c.

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}
static Cmpnts RotateForInvarianceTest(Cmpnts v)
Rotates a vector by Rz(40 deg) then Rx(25 deg).
Definition test_les.c:217
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestWALEAndVremanKernels()

static PetscErrorCode TestWALEAndVremanKernels ( void  )
static

Tests the WALE and Vreman kernels against independent evaluations.

Reference values come from a separate numpy evaluation of the published formulas for the gradient below. Both models must vanish in pure shear, which is the property that lets them stay silent in a laminar shear layer, and both must be unchanged when the velocity gradient and the cell rotate together.

Definition at line 252 of file test_les.c.

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}
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
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
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
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestLeonardStressVanishesOnUniformFlow()

static PetscErrorCode TestLeonardStressVanishesOnUniformFlow ( void  )
static

Tests that the Leonard stress vanishes on a uniform velocity field.

Definition at line 292 of file test_les.c.

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}
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
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestGermanoModelTensorOnConstantStrain()

static PetscErrorCode TestGermanoModelTensorOnConstantStrain ( void  )
static

Tests the model tensor where the two filter terms provably coincide.

On a constant strain field the test filter is the identity, so the filter of the product equals the product of the filtered factors and the model tensor collapses to a closed form, -2 Delta^2 (alpha - 1) |S| S_ij, made trace free. This is the one configuration in which the corrected tensor and the collapsed expression that preceded it agree, which is what makes it a clean analytic anchor.

Definition at line 316 of file test_les.c.

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}
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
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestGermanoModelTensorUsesFilteredProduct()

static PetscErrorCode TestGermanoModelTensorUsesFilteredProduct ( void  )
static

Tests that the model tensor uses the filtered product, not the filtered factors.

Builds a stencil whose strain magnitude varies across it, so the test filter of |S| S_ij genuinely differs from |S|^ S^_ij. The model tensor must be built from the former. Collapsing it onto the latter yields -2 Delta^2 (alpha - 1) |S|^ S^_ij, which is well scaled, responds to the flow, and is not the Germano-Lilly tensor; this case fails if the two are ever conflated again.

Definition at line 349 of file test_les.c.

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}
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
LESTestFilterKernel
Selects the discrete test-filter kernel used by the dynamic procedure.
Definition variables.h:591
@ LES_TEST_FILTER_VOLUME_WEIGHTED_BOX
Definition variables.h:592
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestClipModelCoefficientModes()

static PetscErrorCode TestClipModelCoefficientModes ( void  )
static

Tests each limiting mode, including the sign that carries backscatter.

Definition at line 401 of file test_les.c.

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}
PetscReal ClipModelCoefficient(PetscReal coefficient, const LESConfig *config, PetscBool *limited)
Applies the configured admissible range to one model coefficient.
Definition les.c:466
PetscErrorCode LESConfigSetDefaults(LESConfig *config)
Installs the shipped defaults for every LES closure parameter.
Definition setup.c:206
@ LES_CLIP_CLIP_NEGATIVE
Definition variables.h:619
@ LES_CLIP_CLAMP
Definition variables.h:618
@ LES_CLIP_NONE
Definition variables.h:620
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
Every user-selectable parameter of the LES closure.
Definition variables.h:629
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestEddyViscosityFloorBoundsTotalViscosity()

static PetscErrorCode TestEddyViscosityFloorBoundsTotalViscosity ( void  )
static

Tests eddy-viscosity assembly and the total-viscosity floor.

Definition at line 434 of file test_les.c.

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}
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
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestSubgridKineticEnergy()

static PetscErrorCode TestSubgridKineticEnergy ( void  )
static

Tests the Yoshizawa subgrid kinetic energy against its closed form.

Definition at line 459 of file test_les.c.

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}
PetscReal SubgridKineticEnergy(PetscReal yoshizawa_ci, PetscReal delta, PetscReal strain_magnitude)
Returns the modelled subgrid kinetic energy at a cell.
Definition les.c:595
Here is the call graph for this function:
Here is the caller graph for this function:

◆ FillLocalScalar()

static PetscErrorCode FillLocalScalar ( UserCtx *  user,
Vec  local,
PetscReal  value 
)
static

Fills a ghosted local scalar field over the whole local array.

Writes the halo as well as the owned cells, so a test can poison the layout boundary planes and observe whether an averaging routine counts them.

Definition at line 478 of file test_les.c.

479{
480 PetscFunctionBeginUser;
481 PetscCall(VecSet(local, value));
482 (void)user;
483 PetscFunctionReturn(0);
484}
Here is the caller graph for this function:

◆ SetInteriorScalar()

static PetscErrorCode SetInteriorScalar ( UserCtx *  user,
Vec  local,
PetscReal  value 
)
static

Writes a scalar into every cell the closure treats as owned and interior.

Definition at line 489 of file test_les.c.

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}
Here is the caller graph for this function:

◆ ReadLocalScalar()

static PetscErrorCode ReadLocalScalar ( UserCtx *  user,
Vec  local,
PetscInt  i,
PetscInt  j,
PetscInt  k,
PetscReal *  value 
)
static

Reads one cell of a ghosted local scalar field.

Definition at line 513 of file test_les.c.

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}
Here is the caller graph for this function:

◆ DeclarePeriodicFaces()

static void DeclarePeriodicFaces ( UserCtx *  user,
PetscBool  xi,
PetscBool  eta,
PetscBool  zeta 
)
static

Declares periodic boundary pairs on the axes a test wants homogeneous.

The shared fixture builds a periodic DMDA from the SimCtx flags but leaves the boundary face configuration untouched. The periodic field synchronization reads the faces rather than the flags, so a test that synchronizes has to declare them. The closure's own periodicity questions are answered from the flags, so tests that only ask those do not call this.

Definition at line 534 of file test_les.c.

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}
@ PERIODIC
Definition variables.h:318
BoundaryFaceConfig boundary_faces[6]
Definition variables.h:1099
BCType mathematical_type
Definition variables.h:392
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
Here is the caller graph for this function:

◆ TestAverageRatioLocalIsPointwise()

static PetscErrorCode TestAverageRatioLocalIsPointwise ( void  )
static

Tests that an empty direction set reproduces the pointwise local model.

Definition at line 548 of file test_les.c.

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}
@ FIELD_ID_CS
@ 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 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 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
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 PicurvDestroyMinimalContexts(SimCtx **simCtx_ptr, UserCtx **user_ptr)
Destroys minimal SimCtx/UserCtx fixtures and all owned PETSc objects.
Vec lNvert
Definition variables.h:1113
The master context for the entire simulation.
Definition variables.h:859
User-defined context containing data specific to a single computational grid level.
Definition variables.h:1074
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestAverageRatioDividesSummedFields()

static PetscErrorCode TestAverageRatioDividesSummedFields ( void  )
static

Tests that averaging divides summed numerators by summed denominators.

The distinction matters: the mean of the quotients differs from the quotient of the means, and Lilly's least-squares closure calls for the latter. Separating them needs a denominator that varies too. Three cells carry 1/1 and two carry 6/2, so every pointwise quotient is 1 or 3 and their mean is 9/5, while the ratio of the sums is 15/7. Only the second answer can come from averaging the two fields first.

Definition at line 603 of file test_les.c.

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}
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestAverageRatioRetainsUnaveragedDirection()

static PetscErrorCode TestAverageRatioRetainsUnaveragedDirection ( void  )
static

Tests that averaging over two directions leaves a profile along the third.

Definition at line 662 of file test_les.c.

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}
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
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestAverageRatioIgnoresPeriodicDuplicatePlanes()

static PetscErrorCode TestAverageRatioIgnoresPeriodicDuplicatePlanes ( void  )
static

Tests that a spatial average ignores the periodic duplicate planes.

On a periodic block, index 0 and index m-1 hold copies of interior cells. Counting them would weight those cells twice. The test poisons both planes and requires the average not to move.

Definition at line 719 of file test_les.c.

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}
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
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.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestHomogeneousAveragingDerivesPeriodicAxes()

static PetscErrorCode TestHomogeneousAveragingDerivesPeriodicAxes ( void  )
static

Tests that homogeneous averaging falls back to the block's periodic axes.

Definition at line 784 of file test_les.c.

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}
PetscErrorCode ResolveLESAveragingDirections(UserCtx *user, PetscBool direction[3])
Resolves which logical directions the dynamic coefficient is averaged over.
Definition les.c:410
LESConfig les_config
Parameters of the LES closure selected by les.
Definition variables.h:987
PetscBool averaging_direction[3]
Averaged-over logical directions (xi, eta, zeta).
Definition variables.h:638
@ LES_AVERAGING_HOMOGENEOUS
Definition variables.h:606
LESAveragingMode averaging_mode
Averaging set for the Germano contractions.
Definition variables.h:637
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestAveragingModesSelectTheirOwnDirections()

static PetscErrorCode TestAveragingModesSelectTheirOwnDirections ( void  )
static

Tests that local and global averaging ignore the configured direction list.

Definition at line 818 of file test_les.c.

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}
@ LES_AVERAGING_LOCAL
Definition variables.h:605
@ LES_AVERAGING_GLOBAL
Definition variables.h:607
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestConstantModelNeedsNoCoefficientField()

static PetscErrorCode TestConstantModelNeedsNoCoefficientField ( void  )
static

Tests that the constant model builds its viscosity without a coefficient field.

UserCtx::CS and UserCtx::lCs are deliberately left unallocated. If the constant path ever reaches for them again this case crashes rather than silently reintroducing a field of one repeated number.

Definition at line 849 of file test_les.c.

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}
PetscErrorCode ComputeEddyViscosityLES(UserCtx *user)
Computes the turbulent eddy viscosity for one block.
Definition les.c:952
@ CONSTANT_SMAGORINSKY
Definition variables.h:550
Vec lNu_t
Definition variables.h:1156
PetscReal constant_cs
Fixed Cs for CONSTANT_SMAGORINSKY; unused by the dynamic model.
Definition variables.h:631
DMDALocalInfo info
Definition variables.h:1086
Vec lUcat
Definition variables.h:1113
PetscInt les
Active LES closure; an LESModelType value.
Definition variables.h:984
double nu_t(double yplus)
Computes turbulent eddy viscosity ratio (ν_t / ν)
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestDynamicProcedureRejectsConstantModel()

static PetscErrorCode TestDynamicProcedureRejectsConstantModel ( void  )
static

Tests that the dynamic procedure refuses to run for the constant model.

Definition at line 897 of file test_les.c.

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}
PetscErrorCode ComputeSmagorinskyConstant(UserCtx *user)
Computes the dynamic Smagorinsky coefficient field for one block.
Definition les.c:715
PetscInt StartStep
Definition variables.h:869
PetscInt step
Definition variables.h:867
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestDynamicProcedureVanishesOnUniformFlow()

static PetscErrorCode TestDynamicProcedureVanishesOnUniformFlow ( void  )
static

Tests that the dynamic procedure returns a zero coefficient on uniform flow.

Exercises the whole path: the strain precompute and its halo, the stencil gather, every test filter, both contractions, the averaging, and the limiting. A uniform field has no strain and no stress between the two filter widths, so the Germano identity has nothing to fit and the coefficient must come out exactly zero.

Definition at line 929 of file test_les.c.

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}
@ DYNAMIC_SMAGORINSKY
Definition variables.h:551
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestDynamicProcedureGlobalAverageIsUniform()

static PetscErrorCode TestDynamicProcedureGlobalAverageIsUniform ( void  )
static

Tests the dynamic procedure end to end on a periodic sheared field.

Runs the full procedure on a block that is periodic in all three directions, where the strain precompute has to reach across the wrap correctly, and asks for global averaging. Two things must hold: every cell receives the same finite coefficient, because a global average is one number for the block; and the coefficient stays inside the configured ceiling.

Definition at line 985 of file test_les.c.

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}
PetscErrorCode SynchronizePeriodicCellFields(UserCtx *user, PetscInt num_fields, const FieldId field_ids[])
Synchronizes periodic endpoint cells for a list of cell-centered fields.
FieldId
Compile-time identity for a catalogued Eulerian field.
@ FIELD_ID_UCAT
PetscErrorCode UpdateLocalGhosts(UserCtx *user, FieldId field_id)
Updates the local vector (including ghost points) from its corresponding global vector.
Definition setup.c:2489
Here is the call graph for this function:
Here is the caller graph for this function:

◆ SimpsonCoefficientOnLinearField()

static PetscErrorCode SimpsonCoefficientOnLinearField ( PetscReal  width_ratio,
PetscReal *  coefficient 
)
static

Runs the dynamic procedure with the Simpson kernel on a linear field and returns the raw coefficient at the block centre.

Parameters
[in]width_ratioConfigured test-to-grid width ratio.
[out]coefficientThe unclipped dynamic coefficient at cell (4, 4, 4).

Definition at line 1052 of file test_les.c.

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}
LESTestFilterKernel test_filter_kernel
Discrete test-filter stencil.
Definition variables.h:635
@ LES_TEST_FILTER_SIMPSON_IK
Definition variables.h:593
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
Here is the call graph for this function:
Here is the caller graph for this function:

◆ TestDynamicProcedureSimpsonRatioCountsFilteredDirections()

static PetscErrorCode TestDynamicProcedureSimpsonRatioCountsFilteredDirections ( void  )
static

Tests that the Simpson kernel's width ratio applies to the two directions it filters.

Simpson filters i and k only, so the cube-root test-to-grid width ratio is ratio^(2/3) and alpha is ratio^(4/3), not ratio^2. On a linear field the coefficient is proportional to 1 / (alpha - 1), so two ratios fix the exponent without depending on any other convention in the procedure.

Definition at line 1108 of file test_les.c.

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}
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
Here is the call graph for this function:
Here is the caller graph for this function:

◆ main()

int main ( int  argc,
char **  argv 
)

Entry point for the LES closure suite.

Definition at line 1128 of file test_les.c.

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}
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 PetscErrorCode TestEddyViscosityFloorBoundsTotalViscosity(void)
Tests eddy-viscosity assembly and the total-viscosity floor.
Definition test_les.c:434
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 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 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 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 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 PicurvRunTests(const char *suite_name, const PicurvTestCase *cases, size_t case_count)
Runs a named C test suite and prints pass/fail progress markers.
Named test case descriptor consumed by PicurvRunTests.
Here is the call graph for this function: