PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
test_poisson_rhs.c
Go to the documentation of this file.
1/**
2 * @file test_poisson_rhs.c
3 * @brief C unit tests for Poisson, RHS, body-force, and diffusivity helpers.
4 */
5
6#include "test_support.h"
7
8#include "poisson.h"
9#include "rhs.h"
10#include "setup.h"
12/**
13 * @brief Allocates Poisson/RHS support vectors required by the tests.
14 */
15
16static PetscErrorCode EnsurePoissonAndRhsVectors(UserCtx *user)
17{
18 PetscFunctionBeginUser;
19 if (!user->Phi) PetscCall(DMCreateGlobalVector(user->da, &user->Phi));
20 if (!user->lPhi) PetscCall(DMCreateLocalVector(user->da, &user->lPhi));
21 if (!user->Aj) PetscCall(DMCreateGlobalVector(user->da, &user->Aj));
22 if (!user->lAj) PetscCall(DMCreateLocalVector(user->da, &user->lAj));
23 if (!user->Diffusivity) PetscCall(DMCreateGlobalVector(user->da, &user->Diffusivity));
24 if (!user->lDiffusivity) PetscCall(DMCreateLocalVector(user->da, &user->lDiffusivity));
25 PetscFunctionReturn(0);
26}
27/**
28 * @brief Tests that pressure updates add the correction potential.
29 */
30
31static PetscErrorCode TestUpdatePressureAddsPhi(void)
32{
33 SimCtx *simCtx = NULL;
34 UserCtx *user = NULL;
35
36 PetscFunctionBeginUser;
37 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
38 PetscCall(EnsurePoissonAndRhsVectors(user));
39 PetscCall(VecSet(user->P, 1.0));
40 PetscCall(VecSet(user->Phi, 0.25));
41
42 PetscCall(UpdatePressure(user));
43 PetscCall(PicurvAssertVecConstant(user->P, 1.25, 1.0e-12, "UpdatePressure should add Phi into P"));
44
45 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
46 PetscFunctionReturn(0);
47}
48/**
49 * @brief Tests that the Poisson RHS is zero for zero-divergence velocity.
50 */
51
52static PetscErrorCode TestPoissonRHSZeroDivergence(void)
53{
54 SimCtx *simCtx = NULL;
55 UserCtx *user = NULL;
56 Vec B = NULL;
57
58 PetscFunctionBeginUser;
59 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
60 PetscCall(EnsurePoissonAndRhsVectors(user));
61
62 simCtx->dt = 0.5;
63 PetscCall(VecSet(user->Ucont, 0.0));
64 PetscCall(VecSet(user->Nvert, 0.0));
65 PetscCall(VecSet(user->Aj, 1.0));
66 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
67 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
68 PetscCall(DMGlobalToLocalBegin(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
69 PetscCall(DMGlobalToLocalEnd(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
70 PetscCall(DMGlobalToLocalBegin(user->da, user->Aj, INSERT_VALUES, user->lAj));
71 PetscCall(DMGlobalToLocalEnd(user->da, user->Aj, INSERT_VALUES, user->lAj));
72
73 PetscCall(VecDuplicate(user->P, &B));
74 PetscCall(ComputePoissonRHS(user, B));
75 PetscCall(PicurvAssertVecConstant(B, 0.0, 1.0e-12, "zero velocity divergence should produce zero Poisson RHS"));
76 PetscCall(PicurvAssertRealNear(0.0, simCtx->poissonSourceImbalance, 1.0e-12, "global Poisson RHS sum should be zero"));
77
78 PetscCall(VecDestroy(&B));
79 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
80 PetscFunctionReturn(0);
81}
82/**
83 * @brief Tests the body-force dispatcher across supported source terms.
84 */
85
86static PetscErrorCode TestComputeBodyForcesDispatcher(void)
87{
88 SimCtx *simCtx = NULL;
89 UserCtx *user = NULL;
90 Vec rct = NULL;
91 Cmpnts ***rct_arr = NULL;
92 Cmpnts ***l_csi = NULL;
93 Cmpnts ***l_eta = NULL;
94 Cmpnts ***l_zet = NULL;
95
96 PetscFunctionBeginUser;
97 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
98 PetscCall(VecDuplicate(user->Ucont, &rct));
99 PetscCall(VecZeroEntries(rct));
100
107 simCtx->bulkVelocityCorrection = 2.0;
108 simCtx->dt = 1.0;
109 simCtx->forceScalingFactor = 1.0;
110 simCtx->drivingForceMagnitude = 0.0;
111 PetscCall(VecSet(user->Nvert, 0.0));
112 PetscCall(DMGlobalToLocalBegin(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
113 PetscCall(DMGlobalToLocalEnd(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
114 PetscCall(DMDAVecGetArray(user->fda, user->lCsi, &l_csi));
115 PetscCall(DMDAVecGetArray(user->fda, user->lEta, &l_eta));
116 PetscCall(DMDAVecGetArray(user->fda, user->lZet, &l_zet));
117 l_csi[1][1][1].x = 1.0; l_csi[1][1][1].y = 0.0; l_csi[1][1][1].z = 0.0;
118 l_eta[1][1][1].x = 0.0; l_eta[1][1][1].y = 1.0; l_eta[1][1][1].z = 0.0;
119 l_zet[1][1][1].x = 0.0; l_zet[1][1][1].y = 0.0; l_zet[1][1][1].z = 1.0;
120 PetscCall(DMDAVecRestoreArray(user->fda, user->lZet, &l_zet));
121 PetscCall(DMDAVecRestoreArray(user->fda, user->lEta, &l_eta));
122 PetscCall(DMDAVecRestoreArray(user->fda, user->lCsi, &l_csi));
123
124 PetscCall(ComputeBodyForces(user, rct));
125 PetscCall(DMDAVecGetArrayRead(user->fda, rct, &rct_arr));
126 PetscCall(PicurvAssertRealNear(1.5, simCtx->drivingForceMagnitude, 1.0e-12,
127 "ComputeBodyForces should update the driven-flow controller magnitude"));
128 PetscCall(PicurvAssertRealNear(0.0, rct_arr[1][1][1].y, 1.0e-12, "ComputeBodyForces should keep y unchanged"));
129 PetscCall(PicurvAssertRealNear(0.0, rct_arr[1][1][1].z, 1.0e-12, "ComputeBodyForces should keep z unchanged"));
130 PetscCall(DMDAVecRestoreArrayRead(user->fda, rct, &rct_arr));
131
132 PetscCall(VecDestroy(&rct));
133 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
134 PetscFunctionReturn(0);
135}
136/**
137 * @brief Tests Eulerian diffusivity for the molecular-only configuration.
138 */
139
141{
142 SimCtx *simCtx = NULL;
143 UserCtx *user = NULL;
144 PetscReal ***diff_arr = NULL;
145
146 PetscFunctionBeginUser;
147 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
148 PetscCall(EnsurePoissonAndRhsVectors(user));
149
150 simCtx->ren = 2.0;
151 simCtx->schmidt_number = 4.0;
152 simCtx->les = PETSC_FALSE;
153 PetscCall(VecZeroEntries(user->Diffusivity));
154
155 PetscCall(ComputeEulerianDiffusivity(user));
156 PetscCall(DMDAVecGetArrayRead(user->da, user->Diffusivity, &diff_arr));
157 PetscCall(PicurvAssertRealNear(0.125, diff_arr[1][1][1], 1.0e-12, "molecular diffusivity interior value"));
158 PetscCall(DMDAVecRestoreArrayRead(user->da, user->Diffusivity, &diff_arr));
159
160 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
161 PetscFunctionReturn(0);
162}
163/**
164 * @brief Tests that convection vanishes for a quiescent field.
165 */
166
167static PetscErrorCode TestConvectionZeroField(void)
168{
169 SimCtx *simCtx = NULL;
170 UserCtx *user = NULL;
171 Vec conv = NULL;
172
173 PetscFunctionBeginUser;
174 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
175 PetscCall(VecDuplicate(user->lUcont, &conv));
176 PetscCall(VecSet(user->Ucont, 0.0));
177 PetscCall(VecSet(user->Ucat, 0.0));
178 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
179 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
180 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
181 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
182
183 PetscCall(Convection(user, user->lUcont, user->lUcat, conv));
184 PetscCall(PicurvAssertVecConstant(conv, 0.0, 1.0e-12, "Convection should vanish for a quiescent field"));
185
186 PetscCall(VecDestroy(&conv));
187 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
188 PetscFunctionReturn(0);
189}
190/**
191 * @brief Tests that viscous terms vanish for a uniform field.
192 */
193
194static PetscErrorCode TestViscousUniformField(void)
195{
196 SimCtx *simCtx = NULL;
197 UserCtx *user = NULL;
198 Vec visc = NULL;
199
200 PetscFunctionBeginUser;
201 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
202 PetscCall(VecDuplicate(user->lUcont, &visc));
203 PetscCall(VecSet(user->Ucont, 1.0));
204 PetscCall(VecSet(user->Ucat, 1.0));
205 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
206 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
207 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
208 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
209
210 PetscCall(Viscous(user, user->lUcont, user->lUcat, visc));
211 PetscCall(PicurvAssertVecConstant(visc, 0.0, 1.0e-12, "Viscous should vanish for a uniform field"));
212
213 PetscCall(VecDestroy(&visc));
214 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
215 PetscFunctionReturn(0);
216}
217/**
218 * @brief Rescales the minimal fixture's unit metrics to cubic cells of edge h.
219 */
220static PetscErrorCode ScaleMinimalMetricsToSpacing(UserCtx *user, PetscReal h)
221{
222 Vec area_vectors[12] = {user->Csi, user->Eta, user->Zet, user->ICsi, user->IEta, user->IZet,
223 user->JCsi, user->JEta, user->JZet, user->KCsi, user->KEta, user->KZet};
224 Vec local_area[12] = {user->lCsi, user->lEta, user->lZet, user->lICsi, user->lIEta, user->lIZet,
225 user->lJCsi, user->lJEta, user->lJZet, user->lKCsi, user->lKEta, user->lKZet};
226 Vec jacobians[4] = {user->Aj, user->IAj, user->JAj, user->KAj};
227 Vec local_jac[4] = {user->lAj, user->lIAj, user->lJAj, user->lKAj};
228
229 PetscFunctionBeginUser;
230 for (PetscInt v = 0; v < 12; ++v) {
231 PetscCall(VecScale(area_vectors[v], h * h)); /* a face of a cube of edge h */
232 PetscCall(DMGlobalToLocalBegin(user->fda, area_vectors[v], INSERT_VALUES, local_area[v]));
233 PetscCall(DMGlobalToLocalEnd(user->fda, area_vectors[v], INSERT_VALUES, local_area[v]));
234 }
235 for (PetscInt v = 0; v < 4; ++v) {
236 PetscCall(VecSet(jacobians[v], 1.0 / (h * h * h)));
237 PetscCall(DMGlobalToLocalBegin(user->da, jacobians[v], INSERT_VALUES, local_jac[v]));
238 PetscCall(DMGlobalToLocalEnd(user->da, jacobians[v], INSERT_VALUES, local_jac[v]));
239 }
240 PetscFunctionReturn(0);
241}
242
243/**
244 * @brief Returns the Clark contribution over the molecular viscous one, per unit x, at one cell.
245 *
246 * @details The field is u_x = x^2 on cubic cells of edge h, so du/dx = 2x and the exact
247 * gradient-model stress tau_11 = (h^2/12)(du/dx)^2 varies along x. Both the
248 * model's and viscosity's contributions leave Viscous() through the same flux
249 * differences, so their ratio does not depend on how Viscous() normalizes its
250 * output.
251 */
252static PetscErrorCode ClarkToMolecularRatioPerX(PetscReal h, PetscInt probe, PetscReal *ratio_per_x)
253{
254 SimCtx *simCtx = NULL;
255 UserCtx *user = NULL;
256 Vec molecular = NULL, with_clark = NULL;
257 Cmpnts ***ucat = NULL, ***visc0 = NULL, ***visc1 = NULL;
258 const PetscInt n = 10, mid = n / 2;
259
260 PetscFunctionBeginUser;
261 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, n, n, n));
262 PetscCall(ScaleMinimalMetricsToSpacing(user, h));
263 simCtx->ren = 1.0;
264 simCtx->les = NO_LES_MODEL;
265
266 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
267 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k)
268 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j)
269 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
270 const PetscReal x = (i - 0.5) * h; /* cell centre */
271 ucat[k][j][i].x = x * x; ucat[k][j][i].y = 0.0; ucat[k][j][i].z = 0.0;
272 }
273 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
274 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
275 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
276 PetscCall(VecSet(user->lUcont, 0.0));
277
278 PetscCall(VecDuplicate(user->lUcont, &molecular));
279 PetscCall(VecDuplicate(user->lUcont, &with_clark));
280 simCtx->les_gradient_model = 0;
281 PetscCall(Viscous(user, user->lUcont, user->lUcat, molecular));
282 simCtx->les_gradient_model = 1;
283 PetscCall(Viscous(user, user->lUcont, user->lUcat, with_clark));
284
285 PetscCall(DMDAVecGetArrayRead(user->fda, molecular, &visc0));
286 PetscCall(DMDAVecGetArrayRead(user->fda, with_clark, &visc1));
287 {
288 const PetscReal v_molecular = visc0[mid][mid][probe].x;
289 const PetscReal v_clark = visc1[mid][mid][probe].x - v_molecular;
290 PetscCheck(PetscAbsReal(v_molecular) > 0.0, PETSC_COMM_SELF, PETSC_ERR_PLIB,
291 "The molecular viscous term vanished at the probe; the fixture is wrong.");
292 *ratio_per_x = (v_clark / v_molecular) / ((probe - 0.5) * h);
293 }
294 PetscCall(DMDAVecRestoreArrayRead(user->fda, with_clark, &visc1));
295 PetscCall(DMDAVecRestoreArrayRead(user->fda, molecular, &visc0));
296 PetscCall(VecDestroy(&with_clark));
297 PetscCall(VecDestroy(&molecular));
298 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
299 PetscFunctionReturn(0);
300}
301
302/**
303 * @brief Tests that the Clark gradient model scales as the square of the filter width.
304 *
305 * @details The gradient-model stress is tau_ij = (Delta_k^2/12) du_i/dx_k du_j/dx_k, so
306 * for a fixed physical field its force grows as Delta^2 relative to the
307 * molecular viscous force. Halving the spacing must therefore divide the
308 * Clark-to-molecular ratio at a given x by four. Fixtures with unit cells hide
309 * any error in the power of Delta, which is why the spacing is varied here.
310 */
312{
313 PetscReal coarse = 0.0, fine = 0.0;
314
315 PetscFunctionBeginUser;
316 /* Probe cells chosen so the two spacings give comparable x without being on a
317 boundary: cell 5 at h = 0.2 and cell 5 at h = 0.1. The ratio is per unit x. */
318 PetscCall(ClarkToMolecularRatioPerX(0.2, 5, &coarse));
319 PetscCall(ClarkToMolecularRatioPerX(0.1, 5, &fine));
320 PetscCall(PicurvAssertBool((PetscBool)(coarse < 0.0 && fine < 0.0),
321 "the gradient model removes momentum where du/dx grows along x"));
322 PetscCall(PicurvAssertRealNear(4.0, coarse / fine, 1.0e-8,
323 "halving the filter width divides the gradient-model force by four"));
324 /* Exact value: the molecular flux carries both halves of the symmetric stress,
325 2 nu du/dx per unit area, so its difference across a cell is 4 nu h^3 for
326 u = x^2; the model's is -(2/3) h^5 x. Their ratio per unit x is -h^2/(6 nu),
327 which pins the 1/12 coefficient as well as the power of Delta. */
328 PetscCall(PicurvAssertRealNear(-0.2 * 0.2 / 6.0, coarse, 1.0e-10,
329 "gradient-model force matches (Delta^2/12) du/dx du/dx at h = 0.2"));
330 PetscCall(PicurvAssertRealNear(-0.1 * 0.1 / 6.0, fine, 1.0e-10,
331 "gradient-model force matches (Delta^2/12) du/dx du/dx at h = 0.1"));
332 PetscFunctionReturn(0);
333}
334
335/**
336 * @brief Tests that the full RHS remains zero without forcing on a quiescent field.
337 */
338
339static PetscErrorCode TestComputeRHSZeroFieldNoForcing(void)
340{
341 SimCtx *simCtx = NULL;
342 UserCtx *user = NULL;
343
344 PetscFunctionBeginUser;
345 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
346 PetscCall(VecSet(user->Ucont, 0.0));
347 PetscCall(VecSet(user->Ucat, 0.0));
348 PetscCall(VecSet(user->Nvert, 0.0));
349 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
350 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
351 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
352 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
353 PetscCall(DMGlobalToLocalBegin(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
354 PetscCall(DMGlobalToLocalEnd(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
355
356 PetscCall(ComputeRHS(user, user->Rhs));
357 PetscCall(PicurvAssertVecConstant(user->Rhs, 0.0, 1.0e-12, "ComputeRHS should remain zero without forcing on a quiescent field"));
358
359 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
360 PetscFunctionReturn(0);
361}
362/**
363 * @brief Tests diffusivity-gradient computation on a constant field.
364 */
365
367{
368 SimCtx *simCtx = NULL;
369 UserCtx *user = NULL;
370
371 PetscFunctionBeginUser;
372 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
373 PetscCall(VecSet(user->Diffusivity, 0.75));
374 PetscCall(DMGlobalToLocalBegin(user->da, user->Diffusivity, INSERT_VALUES, user->lDiffusivity));
375 PetscCall(DMGlobalToLocalEnd(user->da, user->Diffusivity, INSERT_VALUES, user->lDiffusivity));
376
377 PetscCall(ComputeEulerianDiffusivityGradient(user));
378 PetscCall(PicurvAssertVecConstant(user->DiffusivityGradient, 0.0, 1.0e-12, "constant diffusivity should yield zero diffusivity gradient"));
379
380 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
381 PetscFunctionReturn(0);
382}
383/**
384 * @brief Tests verification-driven linear diffusivity override and its gradient.
385 */
386
388{
389 SimCtx *simCtx = NULL;
390 UserCtx *user = NULL;
391 PetscReal ***diff_arr = NULL;
392 Cmpnts ***grad_arr = NULL;
393 Cmpnts ***cent = NULL;
394 const PetscReal gamma0 = 0.5;
395 const PetscReal slope_x = 0.25;
396
397 PetscFunctionBeginUser;
398 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
399 PetscCall(PetscStrncpy(simCtx->eulerianSource, "analytical", sizeof(simCtx->eulerianSource)));
400 simCtx->verificationDiffusivity.enabled = PETSC_TRUE;
401 PetscCall(PetscStrncpy(simCtx->verificationDiffusivity.mode, "analytical", sizeof(simCtx->verificationDiffusivity.mode)));
402 PetscCall(PetscStrncpy(simCtx->verificationDiffusivity.profile, "LINEAR_X", sizeof(simCtx->verificationDiffusivity.profile)));
403 simCtx->verificationDiffusivity.gamma0 = gamma0;
404 simCtx->verificationDiffusivity.slope_x = slope_x;
405
407 "verification diffusivity override should report as active"));
408 PetscCall(ComputeEulerianDiffusivity(user));
409 PetscCall(ComputeEulerianDiffusivityGradient(user));
410
411 PetscCall(DMDAVecGetArrayRead(user->da, user->Diffusivity, &diff_arr));
412 PetscCall(DMDAVecGetArrayRead(user->fda, user->Cent, &cent));
413 PetscCall(PicurvAssertRealNear(gamma0 + slope_x * cent[2][2][2].x, diff_arr[2][2][2], 1.0e-12,
414 "verification override should populate the linear diffusivity field"));
415 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Cent, &cent));
416 PetscCall(DMDAVecRestoreArrayRead(user->da, user->Diffusivity, &diff_arr));
417
418 PetscCall(DMDAVecGetArrayRead(user->fda, user->DiffusivityGradient, &grad_arr));
419 PetscCall(PicurvAssertRealNear(slope_x / user->IM, grad_arr[2][2][2].x, 1.0e-12,
420 "linear verification diffusivity should yield the current finite-difference x gradient"));
421 PetscCall(PicurvAssertRealNear(0.0, grad_arr[2][2][2].y, 1.0e-12,
422 "linear verification diffusivity should keep y gradient zero"));
423 PetscCall(PicurvAssertRealNear(0.0, grad_arr[2][2][2].z, 1.0e-12,
424 "linear verification diffusivity should keep z gradient zero"));
425 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->DiffusivityGradient, &grad_arr));
426
427 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
428 PetscFunctionReturn(0);
429}
430/**
431 * @brief Tests that Poisson matrix assembly produces a populated operator on a tiny Cartesian grid.
432 */
434{
435 SimCtx *simCtx = NULL;
436 UserCtx *user = NULL;
437 PetscInt rows = 0, cols = 0;
438 PetscInt ncols = 0;
439 const PetscInt *col_idx = NULL;
440 const PetscScalar *values = NULL;
441 PetscInt interior_row = 0;
442
443 PetscFunctionBeginUser;
444 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
445 PetscCall(VecSet(user->Nvert, 0.0));
446 PetscCall(DMGlobalToLocalBegin(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
447 PetscCall(DMGlobalToLocalEnd(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
448
449 PetscCall(AssemblePoissonOperator(user));
450 PetscCall(PicurvAssertBool((PetscBool)(user->A != NULL), "AssemblePoissonOperator should allocate the Poisson operator"));
451
452 PetscCall(MatGetSize(user->A, &rows, &cols));
453 PetscCall(PicurvAssertIntEqual(user->info.mx * user->info.my * user->info.mz, rows, "Poisson operator row count should match the DA node count"));
454 PetscCall(PicurvAssertIntEqual(rows, cols, "Poisson operator should be square"));
455
456 interior_row = (2 * user->info.my + 2) * user->info.mx + 2;
457 PetscCall(MatGetRow(user->A, interior_row, &ncols, &col_idx, &values));
458 PetscCall(PicurvAssertBool((PetscBool)(ncols > 1), "Interior Poisson row should couple to neighboring nodes"));
459 PetscCall(PicurvAssertBool((PetscBool)(values != NULL), "Interior Poisson row should expose non-null coefficients"));
460 PetscCall(MatRestoreRow(user->A, interior_row, &ncols, &col_idx, &values));
461
462 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
463 PetscFunctionReturn(0);
464}
465/**
466 * @brief Fills every face metric and Jacobian of the minimal fixture with reproducible
467 * random values, so each face gradient carries all of its cross terms.
468 */
469static PetscErrorCode RandomizeMinimalFaceMetrics(UserCtx *user)
470{
471 Vec face_metrics[9] = {user->ICsi, user->IEta, user->IZet, user->JCsi, user->JEta,
472 user->JZet, user->KCsi, user->KEta, user->KZet};
473 Vec local_metrics[9] = {user->lICsi, user->lIEta, user->lIZet, user->lJCsi, user->lJEta,
474 user->lJZet, user->lKCsi, user->lKEta, user->lKZet};
475 Vec jacobians[4] = {user->Aj, user->IAj, user->JAj, user->KAj};
476 Vec local_jac[4] = {user->lAj, user->lIAj, user->lJAj, user->lKAj};
477 PetscRandom rnd;
478
479 PetscFunctionBeginUser;
480 PetscCall(PetscRandomCreate(PETSC_COMM_WORLD, &rnd));
481 PetscCall(PetscRandomSetInterval(rnd, 0.5, 1.5));
482 PetscCall(PetscRandomSetSeed(rnd, 2026));
483 PetscCall(PetscRandomSeed(rnd));
484 for (PetscInt v = 0; v < 9; ++v) {
485 PetscCall(VecSetRandom(face_metrics[v], rnd));
486 PetscCall(DMGlobalToLocalBegin(user->fda, face_metrics[v], INSERT_VALUES, local_metrics[v]));
487 PetscCall(DMGlobalToLocalEnd(user->fda, face_metrics[v], INSERT_VALUES, local_metrics[v]));
488 }
489 for (PetscInt v = 0; v < 4; ++v) {
490 PetscCall(VecSetRandom(jacobians[v], rnd));
491 PetscCall(DMGlobalToLocalBegin(user->da, jacobians[v], INSERT_VALUES, local_jac[v]));
492 PetscCall(DMGlobalToLocalEnd(user->da, jacobians[v], INSERT_VALUES, local_jac[v]));
493 }
494 PetscCall(PetscRandomDestroy(&rnd));
495 PetscFunctionReturn(0);
496}
497
498/**
499 * @brief Tests that the operator is exactly the divergence of the projection's gradient.
500 * @details With random non-orthogonal metrics and one solid cell, projecting a zero flux
501 * with a random Phi and forming the right-hand side of the result must reproduce
502 * -A Phi on every fluid row. The identity holds for any metric values only if the
503 * operator and the projection use the same face gradient, including its one-sided
504 * forms beside walls and solid cells.
505 */
507{
508 SimCtx *simCtx = NULL;
509 UserCtx *user = NULL;
510 Vec B = NULL, APhi = NULL;
511 PetscRandom rnd;
512 PetscReal ***nvert;
513 const PetscReal ***b, ***aphi;
514 PetscReal worst = 0.0;
515
516 PetscFunctionBeginUser;
517 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 7, 7, 7));
518 PetscCall(EnsurePoissonAndRhsVectors(user));
519 simCtx->dt = 0.3;
520 PetscCall(RandomizeMinimalFaceMetrics(user));
521
522 PetscCall(VecSet(user->Nvert, 0.0));
523 PetscCall(DMDAVecGetArray(user->da, user->Nvert, &nvert));
524 if (user->info.xs <= 3 && 3 < user->info.xs + user->info.xm &&
525 user->info.ys <= 3 && 3 < user->info.ys + user->info.ym &&
526 user->info.zs <= 3 && 3 < user->info.zs + user->info.zm) nvert[3][3][3] = 1.0;
527 PetscCall(DMDAVecRestoreArray(user->da, user->Nvert, &nvert));
528 PetscCall(DMGlobalToLocalBegin(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
529 PetscCall(DMGlobalToLocalEnd(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
530
531 PetscCall(PetscRandomCreate(PETSC_COMM_WORLD, &rnd));
532 PetscCall(PetscRandomSetSeed(rnd, 7));
533 PetscCall(PetscRandomSeed(rnd));
534 PetscCall(VecSetRandom(user->Phi, rnd));
535 PetscCall(PetscRandomDestroy(&rnd));
536 PetscCall(DMGlobalToLocalBegin(user->da, user->Phi, INSERT_VALUES, user->lPhi));
537 PetscCall(DMGlobalToLocalEnd(user->da, user->Phi, INSERT_VALUES, user->lPhi));
538 PetscCall(VecSet(user->Ucont, 0.0));
539 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
540 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
541
542 PetscCall(AssemblePoissonOperator(user));
543 PetscCall(ProjectVelocity(user));
544 PetscCall(VecDuplicate(user->P, &B));
545 PetscCall(ComputePoissonRHS(user, B));
546 PetscCall(VecDuplicate(user->Phi, &APhi));
547 PetscCall(MatMult(user->A, user->Phi, APhi));
548
549 PetscCall(DMDAVecGetArrayRead(user->da, B, &b));
550 PetscCall(DMDAVecGetArrayRead(user->da, APhi, &aphi));
551 PetscCall(DMDAVecGetArray(user->da, user->Nvert, &nvert));
552 for (PetscInt k = PetscMax(user->info.zs, 1); k < PetscMin(user->info.zs + user->info.zm, user->info.mz - 1); k++) {
553 for (PetscInt j = PetscMax(user->info.ys, 1); j < PetscMin(user->info.ys + user->info.ym, user->info.my - 1); j++) {
554 for (PetscInt i = PetscMax(user->info.xs, 1); i < PetscMin(user->info.xs + user->info.xm, user->info.mx - 1); i++) {
555 if (nvert[k][j][i] > 0.1) continue;
556 worst = PetscMax(worst, PetscAbsReal(b[k][j][i] + aphi[k][j][i]) / (1.0 + PetscAbsReal(aphi[k][j][i])));
557 }
558 }
559 }
560 PetscCall(DMDAVecRestoreArray(user->da, user->Nvert, &nvert));
561 PetscCall(DMDAVecRestoreArrayRead(user->da, APhi, &aphi));
562 PetscCall(DMDAVecRestoreArrayRead(user->da, B, &b));
563 PetscCall(PicurvAssertRealNear(0.0, worst, 1.0e-12,
564 "the right-hand side of the projected flux should equal -A Phi on every fluid row"));
565
566 PetscCall(VecDestroy(&APhi));
567 PetscCall(VecDestroy(&B));
568 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
569 PetscFunctionReturn(0);
570}
571/**
572 * @brief Tests that projection leaves a zero pressure-correction field unchanged.
573 */
574
576{
577 SimCtx *simCtx = NULL;
578 UserCtx *user = NULL;
579
580 PetscFunctionBeginUser;
581 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
582 PetscCall(VecSet(user->Ucont, 0.0));
583 PetscCall(VecSet(user->Phi, 0.0));
584 PetscCall(VecSet(user->Nvert, 0.0));
585 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
586 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
587 PetscCall(DMGlobalToLocalBegin(user->da, user->Phi, INSERT_VALUES, user->lPhi));
588 PetscCall(DMGlobalToLocalEnd(user->da, user->Phi, INSERT_VALUES, user->lPhi));
589 PetscCall(DMGlobalToLocalBegin(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
590 PetscCall(DMGlobalToLocalEnd(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
591
592 PetscCall(ProjectVelocity(user));
593 PetscCall(PicurvAssertVecConstant(user->Ucont, 0.0, 1.0e-12, "ProjectVelocity should leave a zero-velocity field unchanged when Phi is zero"));
594
595 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
596 PetscFunctionReturn(0);
597}
598/**
599 * @brief Tests that projection applies the expected x-direction correction for a linear pressure field.
600 */
602{
603 SimCtx *simCtx = NULL;
604 UserCtx *user = NULL;
605 PetscReal ***phi = NULL;
606 Cmpnts ***ucont = NULL;
607
608 PetscFunctionBeginUser;
609 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
610 simCtx->dt = COEF_TIME_ACCURACY;
611 PetscCall(VecSet(user->Ucont, 0.0));
612 PetscCall(VecSet(user->Nvert, 0.0));
613 PetscCall(DMDAVecGetArray(user->da, user->Phi, &phi));
614 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k) {
615 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j) {
616 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
617 phi[k][j][i] = (PetscReal)i;
618 }
619 }
620 }
621 PetscCall(DMDAVecRestoreArray(user->da, user->Phi, &phi));
622 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
623 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
624 PetscCall(DMGlobalToLocalBegin(user->da, user->Phi, INSERT_VALUES, user->lPhi));
625 PetscCall(DMGlobalToLocalEnd(user->da, user->Phi, INSERT_VALUES, user->lPhi));
626 PetscCall(DMGlobalToLocalBegin(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
627 PetscCall(DMGlobalToLocalEnd(user->da, user->Nvert, INSERT_VALUES, user->lNvert));
628
629 PetscCall(ProjectVelocity(user));
630
631 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &ucont));
632 PetscCall(PicurvAssertRealNear(-1.0, ucont[2][2][2].x, 1.0e-10, "ProjectVelocity should subtract the x pressure gradient under identity metrics"));
633 PetscCall(PicurvAssertRealNear(0.0, ucont[2][2][2].y, 1.0e-10, "ProjectVelocity should leave the y component unchanged for an x-only gradient"));
634 PetscCall(PicurvAssertRealNear(0.0, ucont[2][2][2].z, 1.0e-10, "ProjectVelocity should leave the z component unchanged for an x-only gradient"));
635 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &ucont));
636
637 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
638 PetscFunctionReturn(0);
639}
640/**
641 * @brief Adds a smooth divergent perturbation to every interior face flux.
642 * @details Boundary faces are left alone, so the inflow still balances the outflow and
643 * the Neumann pressure problem stays solvable.
644 */
645static PetscErrorCode PerturbInteriorFaceFluxes(UserCtx *user, PetscReal amplitude)
646{
647 DMDALocalInfo info = user->info;
648 Cmpnts ***ucont;
649
650 PetscFunctionBeginUser;
651 PetscCall(DMDAVecGetArray(user->fda, user->Ucont, &ucont));
652 for (PetscInt k = PetscMax(info.zs, 1); k < PetscMin(info.zs + info.zm, info.mz - 1); k++) {
653 for (PetscInt j = PetscMax(info.ys, 1); j < PetscMin(info.ys + info.ym, info.my - 1); j++) {
654 for (PetscInt i = PetscMax(info.xs, 1); i < PetscMin(info.xs + info.xm, info.mx - 1); i++) {
655 if (i <= info.mx - 3) ucont[k][j][i].x += amplitude * PetscSinReal(1.3 * i + 0.7 * j + 0.4 * k);
656 if (j <= info.my - 3) ucont[k][j][i].y += amplitude * PetscCosReal(0.9 * i + 1.1 * j + 0.3 * k);
657 if (k <= info.mz - 3) ucont[k][j][i].z += amplitude * PetscSinReal(0.5 * i + 0.8 * j + 1.7 * k);
658 }
659 }
660 }
661 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucont, &ucont));
662 PetscCall(UpdateLocalGhosts(user, FIELD_ID_UCONT));
663 PetscFunctionReturn(0);
664}
665
666/**
667 * @brief Tests that the production multigrid Poisson solve and projection remove a divergence.
668 * @details Runs PoissonSolver_Multigrid through the real setup path with a three-level hierarchy,
669 * then the same UpdatePressure/ProjectVelocity pair the time loop uses, and requires
670 * the projected field to be divergence-free to the solve tolerance.
671 */
673{
674 SimCtx *simCtx = NULL;
675 UserCtx *user = NULL;
676 char tmpdir[PETSC_MAX_PATH_LEN];
677 PetscReal before;
678
679 PetscFunctionBeginUser;
681 NULL, PETSC_FALSE,
682 "-im 17\n-jm 17\n-km 17\n-mg_level 3\n"
683 "-ps_ksp_rtol 1.0e-12\n-ps_ksp_atol 1.0e-14\n-ps_ksp_max_it 200\n",
684 &simCtx, &user, tmpdir, sizeof(tmpdir)));
685 PetscCall(PicurvAssertIntEqual(3, simCtx->usermg.mglevels, "fixture should build a three-level hierarchy"));
686
687 PetscCall(PerturbInteriorFaceFluxes(user, 0.2));
688 PetscCall(ComputeDivergence(user));
689 before = simCtx->MaxDiv;
690 PetscCall(PicurvAssertBool((PetscBool)(before > 1.0e-2), "perturbation should make the field divergent"));
691
692 PetscCall(PoissonSolver_Multigrid(&simCtx->usermg));
693 PetscCall(UpdatePressure(user));
694 PetscCall(ProjectVelocity(user));
695 PetscCall(UpdateLocalGhosts(user, FIELD_ID_UCONT));
696 PetscCall(ComputeDivergence(user));
697 PetscCall(PicurvAssertBool((PetscBool)(simCtx->MaxDiv < 1.0e-9 * before),
698 "multigrid solve plus projection should leave no divergence above the solve tolerance"));
699
700 PetscCall(PicurvDestroyRuntimeContext(&simCtx));
701 PetscCall(PicurvRemoveTempDir(tmpdir));
702 PetscFunctionReturn(0);
703}
704
705/**
706 * @brief Counts the occurrences of @p needle in a text file; a missing file counts zero.
707 */
708static PetscErrorCode CountFileOccurrences(const char *path, const char *needle, PetscInt *count)
709{
710 FILE *file;
711 char line[1024];
712
713 PetscFunctionBeginUser;
714 *count = 0;
715 file = fopen(path, "r");
716 if (!file) PetscFunctionReturn(0);
717 while (fgets(line, sizeof(line), file)) {
718 if (strstr(line, needle)) (*count)++;
719 }
720 fclose(file);
721 PetscFunctionReturn(0);
722}
723
724/**
725 * @brief Tests that the multigrid solver is built once and reused on later steps.
726 * @details Two consecutive solves must use the same Krylov solver and operator, both must
727 * converge, and the convergence log must hold one header per step.
728 */
730{
731 SimCtx *simCtx = NULL;
732 UserCtx *user = NULL;
733 char tmpdir[PETSC_MAX_PATH_LEN], log_path[PETSC_MAX_PATH_LEN + 64];
734 KSP first_ksp;
735 Mat first_operator;
736 KSPConvergedReason reason;
737 PetscInt headers = 0;
738
739 PetscFunctionBeginUser;
741 NULL, PETSC_FALSE,
742 "-im 17\n-jm 17\n-km 17\n-mg_level 3\n"
743 "-ps_ksp_rtol 1.0e-12\n-ps_ksp_atol 1.0e-14\n-ps_ksp_max_it 200\n",
744 &simCtx, &user, tmpdir, sizeof(tmpdir)));
745
746 simCtx->step = simCtx->StartStep + 1;
747 PetscCall(PerturbInteriorFaceFluxes(user, 0.2));
748 PetscCall(PoissonSolver_Multigrid(&simCtx->usermg));
749 first_ksp = user->ksp;
750 first_operator = user->A;
751 PetscCall(PicurvAssertBool((PetscBool)(first_ksp != NULL), "the first solve should build and keep the solver"));
752 PetscCall(KSPGetConvergedReason(user->ksp, &reason));
753 PetscCall(PicurvAssertBool((PetscBool)(reason > 0), "the first solve should converge"));
754
755 simCtx->step++;
756 PetscCall(PerturbInteriorFaceFluxes(user, 0.1));
757 PetscCall(PoissonSolver_Multigrid(&simCtx->usermg));
758 PetscCall(PicurvAssertBool((PetscBool)(user->ksp == first_ksp), "the second solve should reuse the solver"));
759 PetscCall(PicurvAssertBool((PetscBool)(user->A == first_operator), "the second solve should reuse the operator"));
760 PetscCall(KSPGetConvergedReason(user->ksp, &reason));
761 PetscCall(PicurvAssertBool((PetscBool)(reason > 0), "the reused solver should converge"));
762
763 PetscCall(PetscSNPrintf(log_path, sizeof(log_path), "%s/Poisson_Solver_Convergence_History_Block_0.log", simCtx->log_dir));
764 PetscCall(CountFileOccurrences(log_path, "--- Convergence for Timestep", &headers));
765 PetscCall(PicurvAssertIntEqual(2, headers, "the convergence log should hold one header per solve"));
766
767 PetscCall(PicurvDestroyRuntimeContext(&simCtx));
768 PetscCall(PicurvRemoveTempDir(tmpdir));
769 PetscFunctionReturn(0);
770}
771
772/**
773 * @brief Tests that unequal pre- and post-smoothing counts give two distinct smoothers.
774 * @details With pre_sweeps 1 and post_sweeps 3 every smoothed level must run one sweep
775 * before the coarse correction and three after it, the post-smoother must keep the
776 * configured smoother type, and the solve must still remove the divergence. With
777 * equal counts the shared smoother runs the configured sweeps.
778 */
780{
781 const char *const configurations[2] = {
782 "-im 17\n-jm 17\n-km 17\n-mg_level 3\n-mg_pre_it 1\n-mg_post_it 3\n"
783 "-ps_mg_levels_1_ksp_type richardson\n-ps_mg_levels_2_ksp_type richardson\n"
784 "-ps_ksp_rtol 1.0e-12\n-ps_ksp_atol 1.0e-14\n-ps_ksp_max_it 200\n",
785 "-im 17\n-jm 17\n-km 17\n-mg_level 3\n-mg_pre_it 2\n-mg_post_it 2\n",
786 };
787
788 PetscFunctionBeginUser;
789 for (PetscInt c = 0; c < 2; c++) {
790 SimCtx *simCtx = NULL;
791 UserCtx *user = NULL;
792 char tmpdir[PETSC_MAX_PATH_LEN];
793 PC pc;
794 PetscReal before;
795
796 PetscCall(PicurvBuildTinyRuntimeContextWithOptions(NULL, PETSC_FALSE, configurations[c],
797 &simCtx, &user, tmpdir, sizeof(tmpdir)));
798 PetscCall(PerturbInteriorFaceFluxes(user, 0.2));
799 PetscCall(ComputeDivergence(user));
800 before = simCtx->MaxDiv;
801 PetscCall(PoissonSolver_Multigrid(&simCtx->usermg));
802 PetscCall(KSPGetPC(user->ksp, &pc));
803
804 for (PetscInt l = 1; l < simCtx->usermg.mglevels; l++) {
805 KSP pre_smoother, post_smoother;
806 PetscInt pre_its, post_its;
807 PetscBool is_richardson;
808
809 PetscCall(PCMGGetSmootherDown(pc, l, &pre_smoother));
810 PetscCall(KSPGetTolerances(pre_smoother, NULL, NULL, NULL, &pre_its));
811 if (c == 0) {
812 PetscCall(PCMGGetSmootherUp(pc, l, &post_smoother));
813 PetscCall(KSPGetTolerances(post_smoother, NULL, NULL, NULL, &post_its));
814 PetscCall(PicurvAssertBool((PetscBool)(pre_smoother != post_smoother), "unequal sweeps should give a separate post-smoother"));
815 PetscCall(PicurvAssertIntEqual(1, pre_its, "pre-smoothing should run pre_sweeps iterations"));
816 PetscCall(PicurvAssertIntEqual(3, post_its, "post-smoothing should run post_sweeps iterations"));
817 PetscCall(PetscObjectTypeCompare((PetscObject)post_smoother, KSPRICHARDSON, &is_richardson));
818 PetscCall(PicurvAssertBool(is_richardson, "the post-smoother should keep the configured smoother type"));
819 } else {
820 /* PCMGGetSmootherUp() would itself split a shared smoother, so only the
821 shared one is inspected here. */
822 PetscCall(PicurvAssertIntEqual(2, pre_its, "the shared smoother should run the configured sweeps"));
823 }
824 }
825
826 if (c == 0) {
827 PetscCall(UpdatePressure(user));
828 PetscCall(ProjectVelocity(user));
829 PetscCall(UpdateLocalGhosts(user, FIELD_ID_UCONT));
830 PetscCall(ComputeDivergence(user));
831 PetscCall(PicurvAssertBool((PetscBool)(simCtx->MaxDiv < 1.0e-9 * before),
832 "the solve with distinct sweeps should still remove the divergence"));
833 }
834 PetscCall(PicurvDestroyRuntimeContext(&simCtx));
835 PetscCall(PicurvRemoveTempDir(tmpdir));
836 }
837 PetscFunctionReturn(0);
838}
839
840/**
841 * @brief Tests the null space attached to the operator.
842 * @details Removing it from a constant must leave zero everywhere, and from a linear field
843 * must leave zero mean over the interior with zero dummy layers.
844 */
846{
847 SimCtx *simCtx = NULL;
848 UserCtx *user = NULL;
849 char tmpdir[PETSC_MAX_PATH_LEN];
850 MatNullSpace nullsp = NULL;
851 Vec x = NULL;
852 PetscReal ***xa, norm, sum;
853
854 PetscFunctionBeginUser;
856 NULL, PETSC_FALSE, "-im 9\n-jm 9\n-km 9\n-mg_level 2\n", &simCtx, &user, tmpdir, sizeof(tmpdir)));
857 PetscCall(PoissonSolver_Multigrid(&simCtx->usermg));
858 PetscCall(MatGetNullSpace(user->A, &nullsp));
859 PetscCall(PicurvAssertBool((PetscBool)(nullsp != NULL), "the operator should carry the Neumann null space"));
860
861 PetscCall(VecDuplicate(user->Phi, &x));
862 PetscCall(VecSet(x, 3.0));
863 PetscCall(MatNullSpaceRemove(nullsp, x));
864 PetscCall(VecNorm(x, NORM_INFINITY, &norm));
865 PetscCall(PicurvAssertRealNear(0.0, norm, 1.0e-12, "removing the null space should annihilate a constant"));
866
867 PetscCall(DMDAVecGetArray(user->da, x, &xa));
868 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; k++)
869 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; j++)
870 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; i++) xa[k][j][i] = (PetscReal)i;
871 PetscCall(DMDAVecRestoreArray(user->da, x, &xa));
872 PetscCall(MatNullSpaceRemove(nullsp, x));
873 PetscCall(VecSum(x, &sum));
874 PetscCall(PicurvAssertRealNear(0.0, sum, 1.0e-10, "the interior mean should vanish"));
875 PetscCall(DMDAVecGetArray(user->da, x, &xa));
876 if (user->info.xs == 0) PetscCall(PicurvAssertRealNear(0.0, xa[user->info.zs][user->info.ys][0], 1.0e-14, "dummy layers should be zero"));
877 PetscCall(DMDAVecRestoreArray(user->da, x, &xa));
878
879 PetscCall(VecDestroy(&x));
880 PetscCall(PicurvDestroyRuntimeContext(&simCtx));
881 PetscCall(PicurvRemoveTempDir(tmpdir));
882 PetscFunctionReturn(0);
883}
884
885/**
886 * @brief Tests that an over-coarsened hierarchy stops the run at the Poisson solve.
887 * @details Three levels on a 9-node grid leave a coarse operator the preconditioner cannot
888 * factor. The solve must return PETSC_ERR_NOT_CONVERGED rather than hand an
889 * unsolved Phi to the projection.
890 */
892{
893 SimCtx *simCtx = NULL;
894 UserCtx *user = NULL;
895 char tmpdir[PETSC_MAX_PATH_LEN];
896 PetscErrorCode solve_error;
897
898 PetscFunctionBeginUser;
900 NULL, PETSC_FALSE,
901 "-im 9\n-jm 9\n-km 9\n-mg_level 3\n"
902 /* The shipped profiles solve the coarse level with a replicated direct factor. */
903 "-ps_ksp_type fgmres\n-ps_pc_type mg\n"
904 "-ps_mg_coarse_ksp_type preonly\n-ps_mg_coarse_pc_type redundant\n",
905 &simCtx, &user, tmpdir, sizeof(tmpdir)));
906 PetscCall(PerturbInteriorFaceFluxes(user, 0.2));
907
908 PetscCall(PetscPushErrorHandler(PetscReturnErrorHandler, NULL));
909 solve_error = PoissonSolver_Multigrid(&simCtx->usermg);
910 PetscCall(PetscPopErrorHandler());
911 PetscCall(PicurvAssertIntEqual(PETSC_ERR_NOT_CONVERGED, (PetscInt)solve_error,
912 "an unfactorable coarse level should stop the solve, not reach the projection"));
913
914 PetscCall(PicurvDestroyRuntimeContext(&simCtx));
915 PetscCall(PicurvRemoveTempDir(tmpdir));
916 PetscFunctionReturn(0);
917}
918/**
919 * @brief Runs the unit-poisson-rhs PETSc test binary.
920 */
921
922int main(int argc, char **argv)
923{
924 PetscErrorCode ierr;
925 const PicurvTestCase cases[] = {
926 {"update-pressure-adds-phi", TestUpdatePressureAddsPhi},
927 {"poisson-rhs-zero-divergence", TestPoissonRHSZeroDivergence},
928 {"compute-body-forces-dispatcher", TestComputeBodyForcesDispatcher},
929 {"compute-eulerian-diffusivity-molecular-only", TestComputeEulerianDiffusivityMolecularOnly},
930 {"convection-zero-field", TestConvectionZeroField},
931 {"viscous-uniform-field", TestViscousUniformField},
932 {"clark-gradient-model-scales-with-filter-width-squared", TestClarkGradientModelScalesWithFilterWidthSquared},
933 {"compute-rhs-zero-field-no-forcing", TestComputeRHSZeroFieldNoForcing},
934 {"compute-eulerian-diffusivity-gradient-constant-field", TestComputeEulerianDiffusivityGradientConstantField},
935 {"compute-eulerian-diffusivity-verification-linear-x", TestComputeEulerianDiffusivityVerificationLinearX},
936 {"assemble-poisson-operator-populates-rows", TestAssemblePoissonOperatorPopulatesRows},
937 {"operator-and-projection-share-one-face-gradient", TestOperatorAndProjectionShareOneFaceGradient},
938 {"projection-zero-phi-leaves-velocity-unchanged", TestProjectionZeroPhiLeavesVelocityUnchanged},
939 {"projection-linear-phi-corrects-velocity", TestProjectionLinearPhiCorrectsVelocity},
940 {"poisson-solver-multigrid-projects-to-divergence-free", TestPoissonSolverMultigridProjectsToDivergenceFree},
941 {"poisson-solver-multigrid-reuses-its-solver", TestPoissonSolverMultigridReusesItsSolver},
942 {"poisson-solver-multigrid-honours-distinct-sweeps", TestPoissonSolverMultigridHonoursDistinctSweeps},
943 {"poisson-null-space-removes-the-interior-mean", TestPoissonNullSpaceRemovesTheInteriorMean},
944 {"poisson-solver-multigrid-refuses-an-overcoarsened-hierarchy", TestPoissonSolverMultigridRefusesAnOvercoarsenedHierarchy},
945 };
946
947 ierr = PetscInitialize(&argc, &argv, NULL, "PICurv Poisson/RHS tests");
948 if (ierr) {
949 return (int)ierr;
950 }
951
952 ierr = PicurvRunTests("unit-poisson-rhs", cases, sizeof(cases) / sizeof(cases[0]));
953 if (ierr) {
954 PetscFinalize();
955 return (int)ierr;
956 }
957
958 ierr = PetscFinalize();
959 return (int)ierr;
960}
@ FIELD_ID_UCONT
Pressure-Poisson projection: the multigrid pressure-correction solve, the pressure update,...
PetscErrorCode AssemblePoissonOperator(UserCtx *user)
Assembles the pressure-correction operator on one multigrid level.
Definition poisson.c:251
PetscErrorCode UpdatePressure(UserCtx *user)
Adds the pressure correction to the pressure, P += Phi, and refreshes both fields' periodic images an...
Definition poisson.c:416
PetscErrorCode ProjectVelocity(UserCtx *user)
Corrects the contravariant flux with the gradient of Phi.
Definition poisson.c:438
PetscErrorCode PoissonSolver_Multigrid(UserMG *usermg)
Solves the pressure-correction equation for every block with geometric multigrid.
Definition poisson.c:952
PetscErrorCode ComputePoissonRHS(UserCtx *user, Vec B)
Forms the right-hand side of the pressure-correction equation.
Definition poisson.c:353
PetscErrorCode ComputeBodyForces(UserCtx *user, Vec Rct)
General dispatcher for applying all active body forces (momentum sources).
Definition rhs.c:1129
PetscErrorCode Viscous(UserCtx *user, Vec Ucont, Vec Ucat, Vec Visc)
Computes the viscous contribution to the contravariant momentum RHS.
Definition rhs.c:435
PetscErrorCode ComputeEulerianDiffusivity(UserCtx *user)
Computes the effective diffusivity scalar field (Gamma_eff) on the Eulerian grid.
Definition rhs.c:1867
PetscErrorCode ComputeEulerianDiffusivityGradient(UserCtx *user)
Computes the Eulerian gradient of the effective diffusivity field.
Definition rhs.c:1999
PetscErrorCode Convection(UserCtx *user, Vec Ucont, Vec Ucat, Vec Conv)
Computes the convective contribution to the contravariant momentum RHS.
Definition rhs.c:14
PetscErrorCode ComputeRHS(UserCtx *user, Vec Rhs)
Computes the Right-Hand Side (RHS) of the momentum equations.
Definition rhs.c:1157
PetscErrorCode ComputeDivergence(UserCtx *user)
Computes the discrete divergence of the contravariant velocity field.
Definition setup.c:3685
PetscErrorCode UpdateLocalGhosts(UserCtx *user, FieldId field_id)
Updates the local vector (including ghost points) from its corresponding global vector.
Definition setup.c:2489
static PetscErrorCode TestPoissonSolverMultigridReusesItsSolver(void)
Tests that the multigrid solver is built once and reused on later steps.
static PetscErrorCode TestClarkGradientModelScalesWithFilterWidthSquared(void)
Tests that the Clark gradient model scales as the square of the filter width.
static PetscErrorCode TestComputeEulerianDiffusivityMolecularOnly(void)
Tests Eulerian diffusivity for the molecular-only configuration.
static PetscErrorCode TestComputeRHSZeroFieldNoForcing(void)
Tests that the full RHS remains zero without forcing on a quiescent field.
static PetscErrorCode TestViscousUniformField(void)
Tests that viscous terms vanish for a uniform field.
int main(int argc, char **argv)
Runs the unit-poisson-rhs PETSc test binary.
static PetscErrorCode TestConvectionZeroField(void)
Tests that convection vanishes for a quiescent field.
static PetscErrorCode TestComputeBodyForcesDispatcher(void)
Tests the body-force dispatcher across supported source terms.
static PetscErrorCode PerturbInteriorFaceFluxes(UserCtx *user, PetscReal amplitude)
Adds a smooth divergent perturbation to every interior face flux.
static PetscErrorCode RandomizeMinimalFaceMetrics(UserCtx *user)
Fills every face metric and Jacobian of the minimal fixture with reproducible random values,...
static PetscErrorCode TestUpdatePressureAddsPhi(void)
Tests that pressure updates add the correction potential.
static PetscErrorCode TestProjectionZeroPhiLeavesVelocityUnchanged(void)
Tests that projection leaves a zero pressure-correction field unchanged.
static PetscErrorCode TestOperatorAndProjectionShareOneFaceGradient(void)
Tests that the operator is exactly the divergence of the projection's gradient.
static PetscErrorCode TestPoissonSolverMultigridRefusesAnOvercoarsenedHierarchy(void)
Tests that an over-coarsened hierarchy stops the run at the Poisson solve.
static PetscErrorCode TestPoissonSolverMultigridProjectsToDivergenceFree(void)
Tests that the production multigrid Poisson solve and projection remove a divergence.
static PetscErrorCode TestPoissonSolverMultigridHonoursDistinctSweeps(void)
Tests that unequal pre- and post-smoothing counts give two distinct smoothers.
static PetscErrorCode ScaleMinimalMetricsToSpacing(UserCtx *user, PetscReal h)
Rescales the minimal fixture's unit metrics to cubic cells of edge h.
static PetscErrorCode TestPoissonRHSZeroDivergence(void)
Tests that the Poisson RHS is zero for zero-divergence velocity.
static PetscErrorCode TestComputeEulerianDiffusivityVerificationLinearX(void)
Tests verification-driven linear diffusivity override and its gradient.
static PetscErrorCode EnsurePoissonAndRhsVectors(UserCtx *user)
Allocates Poisson/RHS support vectors required by the tests.
static PetscErrorCode TestPoissonNullSpaceRemovesTheInteriorMean(void)
Tests the null space attached to the operator.
static PetscErrorCode TestAssemblePoissonOperatorPopulatesRows(void)
Tests that Poisson matrix assembly produces a populated operator on a tiny Cartesian grid.
static PetscErrorCode TestProjectionLinearPhiCorrectsVelocity(void)
Tests that projection applies the expected x-direction correction for a linear pressure field.
static PetscErrorCode CountFileOccurrences(const char *path, const char *needle, PetscInt *count)
Counts the occurrences of needle in a text file; a missing file counts zero.
static PetscErrorCode ClarkToMolecularRatioPerX(PetscReal h, PetscInt probe, PetscReal *ratio_per_x)
Returns the Clark contribution over the molecular viscous one, per unit x, at one cell.
static PetscErrorCode TestComputeEulerianDiffusivityGradientConstantField(void)
Tests diffusivity-gradient computation on a constant field.
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 PicurvDestroyRuntimeContext(SimCtx **simCtx_ptr)
Finalizes and frees a runtime context built by PicurvBuildTinyRuntimeContext.
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 PicurvBuildTinyRuntimeContextWithOptions(const char *bcs_contents, PetscBool enable_particles, const char *extra_options, SimCtx **simCtx_out, UserCtx **user_out, char *tmpdir, size_t tmpdir_len)
Builds the tiny runtime context with extra control-file options appended.
PetscErrorCode PicurvAssertVecConstant(Vec vec, PetscScalar expected, PetscReal tol, const char *context)
Asserts that a PETSc vector is spatially constant within tolerance.
PetscErrorCode PicurvAssertIntEqual(PetscInt expected, PetscInt actual, const char *context)
Asserts that two integer values are equal.
PetscErrorCode PicurvAssertBool(PetscBool value, const char *context)
Asserts that one boolean condition is true.
PetscErrorCode PicurvRemoveTempDir(const char *path)
Recursively removes a temporary directory created by PicurvMakeTempDir.
Shared declarations for the PICurv C test fixture and assertion layer.
Named test case descriptor consumed by PicurvRunTests.
@ NO_LES_MODEL
Definition variables.h:549
@ PERIODIC
Definition variables.h:318
PetscReal schmidt_number
Definition variables.h:948
BoundaryFaceConfig boundary_faces[6]
Definition variables.h:1099
Vec lIEta
Definition variables.h:1151
Vec lIZet
Definition variables.h:1151
Vec lNvert
Definition variables.h:1113
PetscReal forceScalingFactor
Definition variables.h:961
UserMG usermg
Definition variables.h:1015
@ BC_HANDLER_PERIODIC_DRIVEN_CONSTANT_FLUX
Definition variables.h:342
PetscReal ren
Definition variables.h:906
BCHandlerType handler_type
Definition variables.h:393
PetscReal poissonSourceImbalance
Definition variables.h:1026
Vec lKEta
Definition variables.h:1153
PetscReal dt
Definition variables.h:874
PetscReal bulkVelocityCorrection
Definition variables.h:973
Vec DiffusivityGradient
Definition variables.h:1117
Vec lJCsi
Definition variables.h:1152
Vec Ucont
Definition variables.h:1113
PetscInt StartStep
Definition variables.h:869
PetscScalar x
Definition variables.h:122
PetscReal MaxDiv
Definition variables.h:1027
Vec lKZet
Definition variables.h:1153
char log_dir[PETSC_MAX_PATH_LEN]
Definition variables.h:882
Vec lJEta
Definition variables.h:1152
PetscInt les_gradient_model
Add the Clark gradient (tensor-diffusivity) term to the viscous flux.
Definition variables.h:986
char eulerianSource[PETSC_MAX_PATH_LEN]
Definition variables.h:879
PetscScalar z
Definition variables.h:122
Vec lKCsi
Definition variables.h:1153
PetscInt mglevels
Definition variables.h:736
Vec lJZet
Definition variables.h:1152
Vec lUcont
Definition variables.h:1113
PetscInt step
Definition variables.h:867
Vec Diffusivity
Definition variables.h:1116
Vec lICsi
Definition variables.h:1151
DMDALocalInfo info
Definition variables.h:1086
Vec lUcat
Definition variables.h:1113
PetscScalar y
Definition variables.h:122
PetscInt IM
Definition variables.h:1088
PetscInt les
Active LES closure; an LESModelType value.
Definition variables.h:984
Vec Nvert
Definition variables.h:1113
Vec lDiffusivity
Definition variables.h:1116
BCType mathematical_type
Definition variables.h:392
#define COEF_TIME_ACCURACY
Coefficient controlling the temporal accuracy scheme (e.g., 1.5 for 2nd Order Backward Difference).
Definition variables.h:75
VerificationDiffusivityConfig verificationDiffusivity
Definition variables.h:939
PetscReal drivingForceMagnitude
Definition variables.h:961
@ BC_FACE_NEG_X
Definition variables.h:288
@ BC_FACE_POS_X
Definition variables.h:288
A 3D point or vector with PetscScalar components.
Definition variables.h:121
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
PetscBool VerificationDiffusivityOverrideActive(const SimCtx *simCtx)
Reports whether a verification-only diffusivity override is active.