PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
test_boundaries.c
Go to the documentation of this file.
1/**
2 * @file test_boundaries.c
3 * @brief C unit tests for boundary factories, inlet ownership, and face helpers.
4 */
5
6#include "test_support.h"
7
8#include "Boundaries.h"
9#include "wallfunction.h"
10
11/**
12 * @brief Appends one key/value pair to a linked list of boundary-condition parameters.
13 */
14static PetscErrorCode AppendBCParam(BC_Param **head, const char *key, const char *value)
15{
16 BC_Param *node = NULL;
17 BC_Param **cursor = head;
18
19 PetscFunctionBeginUser;
20 PetscCheck(head != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "BC param list head cannot be NULL.");
21 PetscCall(PetscCalloc1(1, &node));
22 PetscCall(PetscStrallocpy(key, &node->key));
23 PetscCall(PetscStrallocpy(value, &node->value));
24 while (*cursor) {
25 cursor = &(*cursor)->next;
26 }
27 *cursor = node;
28 PetscFunctionReturn(0);
29}
30/**
31 * @brief Destroys one boundary-condition handler allocated by a boundary test.
32 */
33
34static PetscErrorCode DestroyBoundaryHandler(BoundaryCondition **bc_ptr)
35{
36 BoundaryCondition *bc = NULL;
37 PetscFunctionBeginUser;
38 if (!bc_ptr || !*bc_ptr) PetscFunctionReturn(0);
39
40 bc = *bc_ptr;
41 if (bc->Destroy) {
42 PetscCall(bc->Destroy(bc));
43 }
44 PetscCall(PetscFree(bc));
45 *bc_ptr = NULL;
46 PetscFunctionReturn(0);
47}
48
49/**
50 * @brief Chooses a stable interior node index for face-sample assertions on tiny test grids.
51 */
52static PetscInt InteriorSampleIndex(PetscInt nodes)
53{
54 PetscInt index = 3;
55
56 if (index >= nodes - 1) {
57 index = nodes - 2;
58 }
59 if (index < 1) {
60 index = 1;
61 }
62 return index;
63}
64
65/**
66 * @brief Extracts the velocity component aligned with the supplied boundary-face normal.
67 */
68static PetscReal GetFaceNormalComponent(Cmpnts value, BCFace face)
69{
70 switch (face) {
71 case BC_FACE_NEG_X:
72 case BC_FACE_POS_X:
73 return value.x;
74 case BC_FACE_NEG_Y:
75 case BC_FACE_POS_Y:
76 return value.y;
77 case BC_FACE_NEG_Z:
78 case BC_FACE_POS_Z:
79 return value.z;
80 }
81 return 0.0;
82}
83
84/**
85 * @brief Returns the sign convention used for face-normal flux expectations.
86 */
87static PetscReal GetFaceOrientationSign(BCFace face)
88{
89 switch (face) {
90 case BC_FACE_NEG_X:
91 case BC_FACE_NEG_Y:
92 case BC_FACE_NEG_Z:
93 return 1.0;
94 case BC_FACE_POS_X:
95 case BC_FACE_POS_Y:
96 case BC_FACE_POS_Z:
97 return -1.0;
98 }
99 return 0.0;
100}
101
102/**
103 * @brief Maps an inlet face to the matching configuration key used by the handler parser.
104 */
105static const char *GetInletParamKey(BCFace face)
106{
107 switch (face) {
108 case BC_FACE_NEG_X:
109 case BC_FACE_POS_X:
110 return "vx";
111 case BC_FACE_NEG_Y:
112 case BC_FACE_POS_Y:
113 return "vy";
114 case BC_FACE_NEG_Z:
115 case BC_FACE_POS_Z:
116 return "vz";
117 }
118 return "";
119}
120
121/**
122 * @brief Computes the number of face-interior sample points for a given boundary face.
123 */
124static PetscReal GetFaceInteriorPointCount(const UserCtx *user, BCFace face)
125{
126 switch (face) {
127 case BC_FACE_NEG_X:
128 case BC_FACE_POS_X:
129 return (PetscReal)(user->info.my - 2) * (PetscReal)(user->info.mz - 2);
130 case BC_FACE_NEG_Y:
131 case BC_FACE_POS_Y:
132 return (PetscReal)(user->info.mx - 2) * (PetscReal)(user->info.mz - 2);
133 case BC_FACE_NEG_Z:
134 case BC_FACE_POS_Z:
135 return (PetscReal)(user->info.mx - 2) * (PetscReal)(user->info.my - 2);
136 }
137 return 0.0;
138}
139
140/**
141 * @brief Selects representative `Ucont` and `Ubcs` slots for face-matrix assertions.
142 */
143static PetscErrorCode GetRepresentativeFaceSlots(const UserCtx *user,
144 BCFace face,
145 PetscInt *ucont_k,
146 PetscInt *ucont_j,
147 PetscInt *ucont_i,
148 PetscInt *ubcs_k,
149 PetscInt *ubcs_j,
150 PetscInt *ubcs_i)
151{
152 const PetscInt sample_i = InteriorSampleIndex(user->info.mx);
153 const PetscInt sample_j = InteriorSampleIndex(user->info.my);
154 const PetscInt sample_k = InteriorSampleIndex(user->info.mz);
155
156 PetscFunctionBeginUser;
157 switch (face) {
158 case BC_FACE_NEG_X:
159 *ucont_k = sample_k; *ucont_j = sample_j; *ucont_i = 0;
160 *ubcs_k = sample_k; *ubcs_j = sample_j; *ubcs_i = 0;
161 break;
162 case BC_FACE_POS_X:
163 *ucont_k = sample_k; *ucont_j = sample_j; *ucont_i = user->info.mx - 2;
164 *ubcs_k = sample_k; *ubcs_j = sample_j; *ubcs_i = user->info.mx - 1;
165 break;
166 case BC_FACE_NEG_Y:
167 *ucont_k = sample_k; *ucont_j = 0; *ucont_i = sample_i;
168 *ubcs_k = sample_k; *ubcs_j = 0; *ubcs_i = sample_i;
169 break;
170 case BC_FACE_POS_Y:
171 *ucont_k = sample_k; *ucont_j = user->info.my - 2; *ucont_i = sample_i;
172 *ubcs_k = sample_k; *ubcs_j = user->info.my - 1; *ubcs_i = sample_i;
173 break;
174 case BC_FACE_NEG_Z:
175 *ucont_k = 0; *ucont_j = sample_j; *ucont_i = sample_i;
176 *ubcs_k = 0; *ubcs_j = sample_j; *ubcs_i = sample_i;
177 break;
178 case BC_FACE_POS_Z:
179 *ucont_k = user->info.mz - 2; *ucont_j = sample_j; *ucont_i = sample_i;
180 *ubcs_k = user->info.mz - 1; *ubcs_j = sample_j; *ubcs_i = sample_i;
181 break;
182 }
183 PetscFunctionReturn(0);
184}
185
186/**
187 * @brief Selects center, off-center, and wall-adjacent slots for parabolic-face checks.
188 */
189static PetscErrorCode GetParabolicSampleSlots(const UserCtx *user,
190 BCFace face,
191 PetscInt *center_k,
192 PetscInt *center_j,
193 PetscInt *center_i,
194 PetscInt *off_k,
195 PetscInt *off_j,
196 PetscInt *off_i,
197 PetscInt *wall_k,
198 PetscInt *wall_j,
199 PetscInt *wall_i,
200 PetscInt *ubcs_center_k,
201 PetscInt *ubcs_center_j,
202 PetscInt *ubcs_center_i)
203{
204 PetscFunctionBeginUser;
205 switch (face) {
206 case BC_FACE_NEG_X:
207 *center_k = 3; *center_j = 3; *center_i = 0;
208 *off_k = 2; *off_j = 2; *off_i = 0;
209 *wall_k = 1; *wall_j = 1; *wall_i = 0;
210 *ubcs_center_k = 3; *ubcs_center_j = 3; *ubcs_center_i = 0;
211 break;
212 case BC_FACE_POS_X:
213 *center_k = 3; *center_j = 3; *center_i = user->info.mx - 2;
214 *off_k = 2; *off_j = 2; *off_i = user->info.mx - 2;
215 *wall_k = 1; *wall_j = 1; *wall_i = user->info.mx - 2;
216 *ubcs_center_k = 3; *ubcs_center_j = 3; *ubcs_center_i = user->info.mx - 1;
217 break;
218 case BC_FACE_NEG_Y:
219 *center_k = 3; *center_j = 0; *center_i = 3;
220 *off_k = 2; *off_j = 0; *off_i = 2;
221 *wall_k = 1; *wall_j = 0; *wall_i = 1;
222 *ubcs_center_k = 3; *ubcs_center_j = 0; *ubcs_center_i = 3;
223 break;
224 case BC_FACE_POS_Y:
225 *center_k = 3; *center_j = user->info.my - 2; *center_i = 3;
226 *off_k = 2; *off_j = user->info.my - 2; *off_i = 2;
227 *wall_k = 1; *wall_j = user->info.my - 2; *wall_i = 1;
228 *ubcs_center_k = 3; *ubcs_center_j = user->info.my - 1; *ubcs_center_i = 3;
229 break;
230 case BC_FACE_NEG_Z:
231 *center_k = 0; *center_j = 3; *center_i = 3;
232 *off_k = 0; *off_j = 2; *off_i = 2;
233 *wall_k = 0; *wall_j = 1; *wall_i = 1;
234 *ubcs_center_k = 0; *ubcs_center_j = 3; *ubcs_center_i = 3;
235 break;
236 case BC_FACE_POS_Z:
237 *center_k = user->info.mz - 2; *center_j = 3; *center_i = 3;
238 *off_k = user->info.mz - 2; *off_j = 2; *off_i = 2;
239 *wall_k = user->info.mz - 2; *wall_j = 1; *wall_i = 1;
240 *ubcs_center_k = user->info.mz - 1; *ubcs_center_j = 3; *ubcs_center_i = 3;
241 break;
242 }
243 PetscFunctionReturn(0);
244}
245
246/**
247 * @brief Resets one boundary-face configuration entry to a neutral test-local baseline.
248 */
250{
253 cfg->face_id = BC_FACE_NEG_X;
254 if (cfg->params) {
256 cfg->params = NULL;
257 }
258}
259/**
260 * @brief Tests that face-service detection matches a defined inlet face.
261 */
262
264{
265 SimCtx *simCtx = NULL;
266 UserCtx *user = NULL;
267 PetscBool generic_face = PETSC_FALSE;
268 PetscBool inlet_face = PETSC_FALSE;
269 PetscBool any_face_serviceable = PETSC_FALSE;
270
271 PetscFunctionBeginUser;
272 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
273
274 for (BCFace face = BC_FACE_NEG_X; face <= BC_FACE_POS_Z; face++) {
275 PetscCall(CanRankServiceFace(&user->info, user->IM, user->JM, user->KM, face, &generic_face));
276 if (generic_face) {
277 any_face_serviceable = PETSC_TRUE;
278 }
279
280 user->inletFaceDefined = PETSC_TRUE;
281 user->identifiedInletBCFace = face;
282 PetscCall(CanRankServiceInletFace(user, &user->info, user->IM, user->JM, user->KM, &inlet_face));
283 PetscCall(PicurvAssertBool((PetscBool)(inlet_face == generic_face), "inlet face service check should match generic face check"));
284 }
285 PetscCall(PicurvAssertBool(any_face_serviceable, "at least one global face should be serviceable in a non-degenerate domain"));
286
287 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
288 PetscFunctionReturn(0);
289}
290/**
291 * @brief Tests that inlet-face service requires an inlet face to be defined.
292 */
293
295{
296 SimCtx *simCtx = NULL;
297 UserCtx *user = NULL;
298 PetscBool can_service = PETSC_FALSE;
299
300 PetscFunctionBeginUser;
301 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
302
303 user->inletFaceDefined = PETSC_FALSE;
304 for (BCFace face = BC_FACE_NEG_X; face <= BC_FACE_POS_Z; face++) {
305 user->identifiedInletBCFace = face;
306 PetscCall(CanRankServiceInletFace(user, &user->info, user->IM, user->JM, user->KM, &can_service));
307 PetscCall(PicurvAssertBool((PetscBool)!can_service, "undefined inlet face should not be serviceable"));
308 }
309
310 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
311 PetscFunctionReturn(0);
312}
313/**
314 * @brief Tests the boundary-condition factory assignments for representative handlers.
315 */
316
318{
319 BoundaryCondition *wall = NULL;
320 BoundaryCondition *inlet = NULL;
321 BoundaryCondition *parabolic = NULL;
322 BoundaryCondition *profile = NULL;
323
324 PetscFunctionBeginUser;
326 PetscCall(PicurvAssertBool((PetscBool)(wall != NULL), "factory should return wall handler"));
327 PetscCall(PicurvAssertIntEqual(BC_PRIORITY_WALL, wall->priority, "wall handler priority"));
328 PetscCall(PicurvAssertBool((PetscBool)(wall->Apply != NULL), "wall handler should expose Apply"));
329 PetscCall(PicurvAssertBool((PetscBool)(wall->Destroy == NULL), "wall handler should not require destroy hook"));
330 PetscCall(DestroyBoundaryHandler(&wall));
331
333 PetscCall(PicurvAssertBool((PetscBool)(inlet != NULL), "factory should return inlet handler"));
334 PetscCall(PicurvAssertIntEqual(BC_PRIORITY_INLET, inlet->priority, "inlet handler priority"));
335 PetscCall(PicurvAssertBool((PetscBool)(inlet->Initialize != NULL), "inlet handler should expose Initialize"));
336 PetscCall(PicurvAssertBool((PetscBool)(inlet->Apply != NULL), "inlet handler should expose Apply"));
337 PetscCall(PicurvAssertBool((PetscBool)(inlet->Destroy != NULL), "inlet handler should expose its destroy hook"));
340 PetscCall(PicurvAssertBool((PetscBool)(inlet->Apply == parabolic->Apply &&
341 inlet->Apply == profile->Apply),
342 "all profile-producing inlet modes should share one application hook"));
343 PetscCall(DestroyBoundaryHandler(&inlet));
344 PetscCall(DestroyBoundaryHandler(&parabolic));
345 PetscCall(DestroyBoundaryHandler(&profile));
346
347 PetscFunctionReturn(0);
348}
349
350/**
351 * @brief Reapplies a static inlet through the first three time-step contexts.
352 */
354{
355 PetscFunctionBeginUser;
356 for (PetscInt step = 0; step < 3; step++) {
357 PetscReal local_inflow = 0.0, local_outflow = 0.0;
358 ctx->user->simCtx->step = step;
359 ctx->user->simCtx->ti = 0.1 * (PetscReal)step;
360 PetscCall(VecSet(ctx->user->Ucont, -91.0));
361 PetscCall(VecSet(ctx->user->Bcs.Ubcs, -92.0));
362 PetscCall(bc->PreStep(bc, ctx, &local_inflow, &local_outflow));
363 PetscCall(bc->Apply(bc, ctx));
364 }
365 PetscFunctionReturn(0);
366}
367/**
368 * @brief Tests the implemented-handler matrix exposed by the factory.
369 */
370
372{
373 struct HandlerExpectation {
374 BCHandlerType handler;
375 BCPriorityType priority;
376 PetscBool expect_apply;
377 PetscBool expect_initialize;
378 };
379 const struct HandlerExpectation expectations[] = {
380 {BC_HANDLER_WALL_NOSLIP, BC_PRIORITY_WALL, PETSC_TRUE, PETSC_FALSE},
381 {BC_HANDLER_INLET_CONSTANT_VELOCITY, BC_PRIORITY_INLET, PETSC_TRUE, PETSC_TRUE},
382 {BC_HANDLER_INLET_PARABOLIC, BC_PRIORITY_INLET, PETSC_TRUE, PETSC_TRUE},
383 {BC_HANDLER_INLET_PROFILE_FROM_FILE, BC_PRIORITY_INLET, PETSC_TRUE, PETSC_TRUE},
384 {BC_HANDLER_OUTLET_CONSERVATION, BC_PRIORITY_OUTLET, PETSC_TRUE, PETSC_FALSE},
385 };
386
387 PetscFunctionBeginUser;
388 for (size_t i = 0; i < sizeof(expectations) / sizeof(expectations[0]); ++i) {
389 BoundaryCondition *bc = NULL;
390 PetscCall(BoundaryCondition_Create(expectations[i].handler, &bc));
391 PetscCall(PicurvAssertBool((PetscBool)(bc != NULL), "factory should allocate a handler object"));
392 PetscCall(PicurvAssertIntEqual(expectations[i].priority, bc->priority, "handler priority should match expectation"));
393 PetscCall(PicurvAssertBool((PetscBool)((bc->Apply != NULL) == expectations[i].expect_apply), "Apply hook expectation mismatch"));
394 PetscCall(PicurvAssertBool((PetscBool)((bc->Initialize != NULL) == expectations[i].expect_initialize), "Initialize hook expectation mismatch"));
395 PetscCall(DestroyBoundaryHandler(&bc));
396 }
397 PetscFunctionReturn(0);
398}
399
400/**
401 * @brief Writes a tiny canonical PICSLICE profile for boundary handler tests.
402 */
403static PetscErrorCode WritePicSliceForTests(const char *path, PetscInt n1, PetscInt n2, PetscReal base)
404{
405 FILE *fd = NULL;
406
407 PetscFunctionBeginUser;
408 fd = fopen(path, "w");
409 PetscCheck(fd != NULL, PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Could not open %s for writing.", path);
410 PetscCheck(fprintf(fd, "PICSLICE\n1\n%d %d\n", (int)n1, (int)n2) >= 0,
411 PETSC_COMM_SELF, PETSC_ERR_FILE_WRITE, "Could not write PICSLICE header.");
412 for (PetscInt a = 0; a < n1; a++) {
413 for (PetscInt b = 0; b < n2; b++) {
414 PetscReal value = base + (PetscReal)(10 * a + b);
415 PetscCheck(fprintf(fd, "%.16e\n", (double)value) >= 0,
416 PETSC_COMM_SELF, PETSC_ERR_FILE_WRITE, "Could not write PICSLICE value.");
417 }
418 }
419 fclose(fd);
420 PetscFunctionReturn(0);
421}
422
423/**
424 * @brief Tests prescribed-flow inlet handler loading and applying one Z-face profile.
425 */
427{
428 SimCtx *simCtx = NULL;
429 UserCtx *user = NULL;
430 BoundaryCondition *bc = NULL;
431 BCContext ctx;
432 Cmpnts ***ucont = NULL;
433 Cmpnts ***ubcs = NULL;
434 char profile_path[PETSC_MAX_PATH_LEN];
435 PetscReal local_inflow = 0.0;
436 PetscReal local_outflow = 0.0;
437
438 PetscFunctionBeginUser;
439 PetscCall(PetscMemzero(&ctx, sizeof(ctx)));
440 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 5, 5, 5));
441 PetscCall(PicurvPopulateIdentityMetrics(user));
442
443 PetscCall(PetscSNPrintf(profile_path, sizeof(profile_path), "/tmp/picurv_unit_profile_%d.picslice", (int)getpid()));
444 PetscCall(WritePicSliceForTests(profile_path, user->JM - 1, user->IM - 1, 2.0));
445
449 PetscCall(AppendBCParam(&user->boundary_faces[BC_FACE_NEG_Z].params, "source_file", profile_path));
450
451 PetscCall(VecSet(user->Ucont, 0.0));
452 PetscCall(VecSet(user->Bcs.Ubcs, 0.0));
453
454 ctx.user = user;
457 PetscCall(bc->PreStep(bc, &ctx, &local_inflow, &local_outflow));
458 PetscCall(PicurvAssertRealNear(0.0, local_inflow, 1.0e-12, "profile inlet PreStep should leave inflow unchanged"));
459 PetscCall(PicurvAssertRealNear(0.0, local_outflow, 1.0e-12, "profile inlet PreStep should leave outflow unchanged"));
460 PetscCall(bc->Initialize(bc, &ctx));
461 PetscCall(ApplyInletAcrossFirstSteps(bc, &ctx));
462 PetscCall(bc->PostStep(bc, &ctx, &local_inflow, &local_outflow));
463
464 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &ucont));
465 PetscCall(DMDAVecGetArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
466 PetscCall(PicurvAssertRealNear(2.0, ucont[0][1][1].z, 1.0e-12, "profile inlet should apply first scalar speed"));
467 PetscCall(PicurvAssertRealNear(13.0, ucont[0][2][2].z, 1.0e-12, "profile inlet should preserve PICSLICE ordering"));
468 PetscCall(PicurvAssertRealNear(24.0, ubcs[0][3][3].z, 1.0e-12, "profile inlet should write boundary velocity"));
469 PetscCall(PicurvAssertRealNear(35.0, ucont[0][4][4].z, 1.0e-12, "profile inlet should consume the last face-cell slot"));
470 PetscCall(PicurvAssertRealNear(35.0, ubcs[0][4][4].z, 1.0e-12, "profile inlet should write the last face-cell boundary value"));
471 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &ucont));
472 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
473 PetscCall(PicurvAssertBool((PetscBool)(local_inflow > 0.0), "profile inlet PostStep should accumulate positive negative-face flux"));
474 PetscCall(PicurvAssertRealNear(0.0, local_outflow, 1.0e-12, "profile inlet should not add to outflow"));
475
476 PetscCall(DestroyBoundaryHandler(&bc));
478 user->boundary_faces[BC_FACE_NEG_Z].params = NULL;
479 remove(profile_path);
480 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
481 PetscFunctionReturn(0);
482}
483/**
484 * @brief Tests that unsupported handler types are rejected by the factory.
485 */
486
488{
489 BoundaryCondition *bc = NULL;
490 PetscErrorCode ierr_create;
491
492 PetscFunctionBeginUser;
493 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
495 PetscCall(PetscPopErrorHandler());
496
497 PetscCall(PicurvAssertBool((PetscBool)(ierr_create != 0), "unsupported handler should return a non-zero error code"));
498 if (bc) {
499 PetscCall(PetscFree(bc));
500 }
501 PetscFunctionReturn(0);
502}
503/**
504 * @brief Tests deterministic inlet-face grid-location helpers across all faces.
505 */
506
508{
509 SimCtx *simCtx = NULL;
510 UserCtx *user = NULL;
511
512 PetscFunctionBeginUser;
513 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 8, 8, 8));
514 simCtx->np = 128;
515
516 for (BCFace face = BC_FACE_NEG_X; face <= BC_FACE_POS_Z; ++face) {
517 PetscInt ci = -1, cj = -1, ck = -1;
518 PetscReal xi = -1.0, eta = -1.0, zta = -1.0;
519 PetscBool placed = PETSC_FALSE;
520
521 user->identifiedInletBCFace = face;
523 user, &user->info,
524 user->info.xs, user->info.ys, user->info.zs,
525 user->info.mx, user->info.my, user->info.mz,
526 0,
527 &ci, &cj, &ck, &xi, &eta, &zta, &placed));
528
529 PetscCall(PicurvAssertBool(placed, "single-rank deterministic face placement should succeed"));
530 PetscCall(PicurvAssertBool((PetscBool)(ci >= user->info.xs && ci < user->info.xs + user->info.xm), "ci must map to owned node window"));
531 PetscCall(PicurvAssertBool((PetscBool)(cj >= user->info.ys && cj < user->info.ys + user->info.ym), "cj must map to owned node window"));
532 PetscCall(PicurvAssertBool((PetscBool)(ck >= user->info.zs && ck < user->info.zs + user->info.zm), "ck must map to owned node window"));
533 PetscCall(PicurvAssertBool((PetscBool)(xi >= 0.0 && xi < 1.0), "xi should be in [0,1)"));
534 PetscCall(PicurvAssertBool((PetscBool)(eta >= 0.0 && eta < 1.0), "eta should be in [0,1)"));
535 PetscCall(PicurvAssertBool((PetscBool)(zta >= 0.0 && zta < 1.0), "zta should be in [0,1)"));
536
537 if (face == BC_FACE_NEG_X || face == BC_FACE_POS_X) {
538 PetscCall(PicurvAssertRealNear(0.5, xi, 1.0e-10, "deterministic x-face placement should sit halfway into boundary-adjacent cell"));
539 } else if (face == BC_FACE_NEG_Y || face == BC_FACE_POS_Y) {
540 PetscCall(PicurvAssertRealNear(0.5, eta, 1.0e-10, "deterministic y-face placement should sit halfway into boundary-adjacent cell"));
541 } else {
542 PetscCall(PicurvAssertRealNear(0.5, zta, 1.0e-10, "deterministic z-face placement should sit halfway into boundary-adjacent cell"));
543 }
544 }
545
546 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
547 PetscFunctionReturn(0);
548}
549/**
550 * @brief Tests random inlet-face cell selection across all supported faces.
551 */
552
554{
555 SimCtx *simCtx = NULL;
556 UserCtx *user = NULL;
557 PetscRandom rand_i = NULL, rand_j = NULL, rand_k = NULL;
558
559 PetscFunctionBeginUser;
560 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 8, 8, 8));
561
562 PetscCall(PetscRandomCreate(PETSC_COMM_WORLD, &rand_i));
563 PetscCall(PetscRandomCreate(PETSC_COMM_WORLD, &rand_j));
564 PetscCall(PetscRandomCreate(PETSC_COMM_WORLD, &rand_k));
565 PetscCall(PetscRandomSetInterval(rand_i, 0.0, 1.0));
566 PetscCall(PetscRandomSetInterval(rand_j, 0.0, 1.0));
567 PetscCall(PetscRandomSetInterval(rand_k, 0.0, 1.0));
568 PetscCall(PetscRandomSetFromOptions(rand_i));
569 PetscCall(PetscRandomSetFromOptions(rand_j));
570 PetscCall(PetscRandomSetFromOptions(rand_k));
571
572 for (BCFace face = BC_FACE_NEG_X; face <= BC_FACE_POS_Z; ++face) {
573 PetscInt ci = -1, cj = -1, ck = -1;
574 PetscReal xi = -1.0, eta = -1.0, zta = -1.0;
575 const PetscReal boundary_eps = 5.0e-4;
576
577 user->identifiedInletBCFace = face;
579 user, &user->info,
580 user->info.xs, user->info.ys, user->info.zs,
581 user->info.mx, user->info.my, user->info.mz,
582 &rand_i, &rand_j, &rand_k,
583 &ci, &cj, &ck,
584 &xi, &eta, &zta));
585
586 PetscCall(PicurvAssertBool((PetscBool)(ci >= user->info.xs && ci < user->info.xs + user->info.xm), "ci must map to owned node window"));
587 PetscCall(PicurvAssertBool((PetscBool)(cj >= user->info.ys && cj < user->info.ys + user->info.ym), "cj must map to owned node window"));
588 PetscCall(PicurvAssertBool((PetscBool)(ck >= user->info.zs && ck < user->info.zs + user->info.zm), "ck must map to owned node window"));
589 PetscCall(PicurvAssertBool((PetscBool)(xi >= 0.0 && xi <= 1.0), "xi should be in [0,1]"));
590 PetscCall(PicurvAssertBool((PetscBool)(eta >= 0.0 && eta <= 1.0), "eta should be in [0,1]"));
591 PetscCall(PicurvAssertBool((PetscBool)(zta >= 0.0 && zta <= 1.0), "zta should be in [0,1]"));
592
593 switch (face) {
594 case BC_FACE_NEG_X:
595 PetscCall(PicurvAssertBool((PetscBool)(xi <= boundary_eps), "NEG_X should pin xi near 0"));
596 break;
597 case BC_FACE_POS_X:
598 PetscCall(PicurvAssertBool((PetscBool)(xi >= 1.0 - boundary_eps), "POS_X should pin xi near 1"));
599 break;
600 case BC_FACE_NEG_Y:
601 PetscCall(PicurvAssertBool((PetscBool)(eta <= boundary_eps), "NEG_Y should pin eta near 0"));
602 break;
603 case BC_FACE_POS_Y:
604 PetscCall(PicurvAssertBool((PetscBool)(eta >= 1.0 - boundary_eps), "POS_Y should pin eta near 1"));
605 break;
606 case BC_FACE_NEG_Z:
607 PetscCall(PicurvAssertBool((PetscBool)(zta <= boundary_eps), "NEG_Z should pin zta near 0"));
608 break;
609 case BC_FACE_POS_Z:
610 PetscCall(PicurvAssertBool((PetscBool)(zta >= 1.0 - boundary_eps), "POS_Z should pin zta near 1"));
611 break;
612 }
613 }
614
615 PetscCall(PetscRandomDestroy(&rand_i));
616 PetscCall(PetscRandomDestroy(&rand_j));
617 PetscCall(PetscRandomDestroy(&rand_k));
618 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
619 PetscFunctionReturn(0);
620}
621/**
622 * @brief Tests constant inlet handler initialization and face writes on a Z inlet.
623 */
624
626{
627 SimCtx *simCtx = NULL;
628 UserCtx *user = NULL;
629 BoundaryCondition *bc = NULL;
630 BCContext ctx;
631 Cmpnts ***ucont = NULL;
632 Cmpnts ***ubcs = NULL;
633 PetscReal local_inflow = 0.0;
634 PetscReal local_outflow = 0.0;
635 PetscReal expected_flux = 0.0;
636
637 PetscFunctionBeginUser;
638 PetscCall(PetscMemzero(&ctx, sizeof(ctx)));
639 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
640 PetscCall(VecSet(user->Ucont, 0.0));
641 PetscCall(VecSet(user->Bcs.Ubcs, 0.0));
642 PetscCall(PicurvPopulateIdentityMetrics(user));
643
647 PetscCall(AppendBCParam(&user->boundary_faces[BC_FACE_NEG_Z].params, "vz", "2.5"));
648
649 ctx.user = user;
652 PetscCall(bc->Initialize(bc, &ctx));
653 PetscCall(ApplyInletAcrossFirstSteps(bc, &ctx));
654 PetscCall(bc->PostStep(bc, &ctx, &local_inflow, &local_outflow));
655
656 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &ucont));
657 PetscCall(DMDAVecGetArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
658 PetscCall(PicurvAssertRealNear(2.5, ucont[0][3][3].z, 1.0e-12, "constant inlet should set Ucont normal component"));
659 PetscCall(PicurvAssertRealNear(2.5, ubcs[0][3][3].z, 1.0e-12, "constant inlet should set Ubcs normal component"));
660 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &ucont));
661 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
662
663 expected_flux = (PetscReal)(user->info.mx - 2) * (PetscReal)(user->info.my - 2) * 2.5;
664 PetscCall(PicurvAssertRealNear(expected_flux, local_inflow, 1.0e-12, "constant inlet PostStep should sum the face flux"));
665 PetscCall(PicurvAssertRealNear(0.0, local_outflow, 1.0e-12, "constant inlet should not contribute to outflow accumulation"));
666
667 PetscCall(DestroyBoundaryHandler(&bc));
669 user->boundary_faces[BC_FACE_NEG_Z].params = NULL;
670 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
671 PetscFunctionReturn(0);
672}
673
674/**
675 * @brief Verifies curvilinear projection and Cartesian dummy reflection for the shared inlet path.
676 */
678{
679 SimCtx *simCtx = NULL;
680 UserCtx *user = NULL;
681 BoundaryCondition *bc = NULL;
682 BCContext ctx;
683 Cmpnts ***zet = NULL, ***ucont = NULL, ***ubcs = NULL, ***ucat = NULL;
684
685 PetscFunctionBeginUser;
686 PetscCall(PetscMemzero(&ctx, sizeof(ctx)));
687 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
688 PetscCall(PicurvPopulateIdentityMetrics(user));
689 PetscCall(DMDAVecGetArray(user->fda, user->lZet, &zet));
690 zet[0][3][3].x = 0.0;
691 zet[0][3][3].y = 3.0;
692 zet[0][3][3].z = 4.0;
693 PetscCall(DMDAVecRestoreArray(user->fda, user->lZet, &zet));
694
698 PetscCall(AppendBCParam(&user->boundary_faces[BC_FACE_NEG_Z].params, "vz", "2.0"));
699 ctx.user = user;
702 PetscCall(bc->Initialize(bc, &ctx));
703 PetscCall(bc->Apply(bc, &ctx));
704
705 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &ucont));
706 PetscCall(DMDAVecGetArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
707 PetscCall(PicurvAssertRealNear(10.0, ucont[0][3][3].z, 1.0e-12,
708 "Ucont should be the Cartesian inlet velocity dotted with the face metric"));
709 PetscCall(PicurvAssertRealNear(0.0, ubcs[0][3][3].x, 1.0e-12,
710 "curvilinear normal velocity should preserve its Cartesian x component"));
711 PetscCall(PicurvAssertRealNear(1.2, ubcs[0][3][3].y, 1.0e-12,
712 "curvilinear normal velocity should include Cartesian y"));
713 PetscCall(PicurvAssertRealNear(1.6, ubcs[0][3][3].z, 1.0e-12,
714 "curvilinear normal velocity should include Cartesian z"));
715 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &ucont));
716 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
717
718 PetscCall(VecSet(user->Ucat, 1.0));
719 PetscCall(UpdateDummyCells(user, FIELD_ID_UCAT));
720 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucat, &ucat));
721 PetscCall(PicurvAssertRealNear(-1.0, ucat[0][3][3].x, 1.0e-12,
722 "dummy reflection should act on Cartesian x"));
723 PetscCall(PicurvAssertRealNear(1.4, ucat[0][3][3].y, 1.0e-12,
724 "dummy reflection should act on Cartesian y"));
725 PetscCall(PicurvAssertRealNear(2.2, ucat[0][3][3].z, 1.0e-12,
726 "dummy reflection should act on Cartesian z"));
727 PetscCall(PicurvAssertRealNear(1.0, ucat[1][3][3].y, 1.0e-12,
728 "dummy reflection should not overwrite the adjacent interior value"));
729 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucat, &ucat));
730
731 PetscCall(DestroyBoundaryHandler(&bc));
733 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
734 PetscFunctionReturn(0);
735}
736
737/**
738 * @brief Tests wall no-slip application across the full face matrix.
739 */
740static PetscErrorCode TestWallNoSlipHandlerFaceMatrix(void)
741{
742 SimCtx *simCtx = NULL;
743 UserCtx *user = NULL;
744 BoundaryCondition *bc = NULL;
745 BCContext ctx;
746 Cmpnts ***ucont = NULL;
747 Cmpnts ***ubcs = NULL;
748 const BCFace faces[] = {
752 };
753
754 PetscFunctionBeginUser;
755 PetscCall(PetscMemzero(&ctx, sizeof(ctx)));
756 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
758
759 for (size_t n = 0; n < sizeof(faces) / sizeof(faces[0]); ++n) {
760 const BCFace face = faces[n];
761 PetscInt ucont_k, ucont_j, ucont_i, ubcs_k, ubcs_j, ubcs_i;
762
763 PetscCall(VecSet(user->Ucont, 5.0));
764 PetscCall(VecSet(user->Bcs.Ubcs, 7.0));
765 ctx.user = user;
766 ctx.face_id = face;
767 PetscCall(bc->Apply(bc, &ctx));
768
769 PetscCall(GetRepresentativeFaceSlots(user, face, &ucont_k, &ucont_j, &ucont_i, &ubcs_k, &ubcs_j, &ubcs_i));
770 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &ucont));
771 PetscCall(DMDAVecGetArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
772 PetscCall(PicurvAssertRealNear(0.0, GetFaceNormalComponent(ucont[ucont_k][ucont_j][ucont_i], face), 1.0e-12, "wall no-slip should zero the normal flux component"));
773 PetscCall(PicurvAssertRealNear(0.0, ubcs[ubcs_k][ubcs_j][ubcs_i].x, 1.0e-12, "wall no-slip should zero Ubcs.x"));
774 PetscCall(PicurvAssertRealNear(0.0, ubcs[ubcs_k][ubcs_j][ubcs_i].y, 1.0e-12, "wall no-slip should zero Ubcs.y"));
775 PetscCall(PicurvAssertRealNear(0.0, ubcs[ubcs_k][ubcs_j][ubcs_i].z, 1.0e-12, "wall no-slip should zero Ubcs.z"));
776 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &ucont));
777 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
778 }
779
780 PetscCall(DestroyBoundaryHandler(&bc));
781 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
782 PetscFunctionReturn(0);
783}
784
785/**
786 * @brief Tests constant-inlet initialization, flux accounting, and face writes across all faces.
787 */
789{
790 struct ConstantInletCase {
791 BCFace face;
792 PetscReal velocity;
793 const char *value_text;
794 };
795 const struct ConstantInletCase cases[] = {
796 {BC_FACE_NEG_X, 1.5, "1.5"},
797 {BC_FACE_POS_X, 2.0, "2.0"},
798 {BC_FACE_NEG_Y, 2.5, "2.5"},
799 {BC_FACE_POS_Y, 3.0, "3.0"},
800 {BC_FACE_NEG_Z, 3.5, "3.5"},
801 {BC_FACE_POS_Z, 4.0, "4.0"},
802 };
803 SimCtx *simCtx = NULL;
804 UserCtx *user = NULL;
805 BCContext ctx;
806 Cmpnts ***ucont = NULL;
807 Cmpnts ***ubcs = NULL;
808
809 PetscFunctionBeginUser;
810 PetscCall(PetscMemzero(&ctx, sizeof(ctx)));
811 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
812 PetscCall(PicurvPopulateIdentityMetrics(user));
813
814 for (size_t n = 0; n < sizeof(cases) / sizeof(cases[0]); ++n) {
815 const struct ConstantInletCase *test_case = &cases[n];
816 BoundaryCondition *bc = NULL;
817 PetscReal local_inflow = 0.0;
818 PetscReal local_outflow = 0.0;
819 PetscInt ucont_k, ucont_j, ucont_i, ubcs_k, ubcs_j, ubcs_i;
820 const PetscReal expected_value = GetFaceOrientationSign(test_case->face) * test_case->velocity;
821 const PetscReal expected_flux = GetFaceInteriorPointCount(user, test_case->face) * expected_value;
822
823 PetscCall(VecSet(user->Ucont, 0.0));
824 PetscCall(VecSet(user->Bcs.Ubcs, 0.0));
825 ResetBoundaryFaceConfig(&user->boundary_faces[test_case->face]);
826 user->boundary_faces[test_case->face].face_id = test_case->face;
827 user->boundary_faces[test_case->face].mathematical_type = INLET;
829 PetscCall(AppendBCParam(&user->boundary_faces[test_case->face].params,
830 GetInletParamKey(test_case->face),
831 test_case->value_text));
832
833 ctx.user = user;
834 ctx.face_id = test_case->face;
836 PetscCall(bc->PreStep(bc, &ctx, &local_inflow, &local_outflow));
837 PetscCall(PicurvAssertRealNear(0.0, local_inflow, 1.0e-12, "constant inlet PreStep should leave inflow unchanged"));
838 PetscCall(PicurvAssertRealNear(0.0, local_outflow, 1.0e-12, "constant inlet PreStep should leave outflow unchanged"));
839 PetscCall(bc->Initialize(bc, &ctx));
840 PetscCall(bc->Apply(bc, &ctx));
841 PetscCall(bc->PostStep(bc, &ctx, &local_inflow, &local_outflow));
842
843 PetscCall(GetRepresentativeFaceSlots(user, test_case->face, &ucont_k, &ucont_j, &ucont_i, &ubcs_k, &ubcs_j, &ubcs_i));
844 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &ucont));
845 PetscCall(DMDAVecGetArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
846 PetscCall(PicurvAssertRealNear(expected_value, GetFaceNormalComponent(ucont[ucont_k][ucont_j][ucont_i], test_case->face), 1.0e-12, "constant inlet should set the face-normal Ucont component"));
847 PetscCall(PicurvAssertRealNear(expected_value, GetFaceNormalComponent(ubcs[ubcs_k][ubcs_j][ubcs_i], test_case->face), 1.0e-12, "constant inlet should set the face-normal Ubcs component"));
848 PetscCall(PicurvAssertRealNear((test_case->face == BC_FACE_NEG_X || test_case->face == BC_FACE_POS_X) ? expected_value : 0.0, ubcs[ubcs_k][ubcs_j][ubcs_i].x, 1.0e-12, "constant inlet should only set the expected Ubcs axis"));
849 PetscCall(PicurvAssertRealNear((test_case->face == BC_FACE_NEG_Y || test_case->face == BC_FACE_POS_Y) ? expected_value : 0.0, ubcs[ubcs_k][ubcs_j][ubcs_i].y, 1.0e-12, "constant inlet should only set the expected Ubcs axis"));
850 PetscCall(PicurvAssertRealNear((test_case->face == BC_FACE_NEG_Z || test_case->face == BC_FACE_POS_Z) ? expected_value : 0.0, ubcs[ubcs_k][ubcs_j][ubcs_i].z, 1.0e-12, "constant inlet should only set the expected Ubcs axis"));
851 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &ucont));
852 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
853 PetscCall(PicurvAssertRealNear(expected_flux, local_inflow, 1.0e-12, "constant inlet PostStep should integrate the face flux with orientation"));
854 PetscCall(PicurvAssertRealNear(0.0, local_outflow, 1.0e-12, "constant inlet should not add to outflow"));
855
856 PetscCall(DestroyBoundaryHandler(&bc));
857 ResetBoundaryFaceConfig(&user->boundary_faces[test_case->face]);
858 }
859
860 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
861 PetscFunctionReturn(0);
862}
863/**
864 * @brief Tests parabolic inlet handler shape on a tiny Z-face.
865 */
866
868{
869 SimCtx *simCtx = NULL;
870 UserCtx *user = NULL;
871 BoundaryCondition *bc = NULL;
872 BCContext ctx;
873 Cmpnts ***ucont = NULL;
874
875 PetscFunctionBeginUser;
876 PetscCall(PetscMemzero(&ctx, sizeof(ctx)));
877 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
878 PetscCall(PicurvPopulateIdentityMetrics(user));
879
883 PetscCall(AppendBCParam(&user->boundary_faces[BC_FACE_NEG_Z].params, "v_max", "4.0"));
884
885 ctx.user = user;
888 PetscCall(bc->Initialize(bc, &ctx));
889 PetscCall(ApplyInletAcrossFirstSteps(bc, &ctx));
890
891 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &ucont));
892 PetscCall(PicurvAssertRealNear(4.0, ucont[0][3][3].z, 1.0e-12, "parabolic inlet should peak at the face centerline"));
893 /* Five cells per axis: the wall-adjacent centre sits at s = -0.8, so each factor is 0.36. */
894 PetscCall(PicurvAssertRealNear(4.0 * 0.36 * 0.36, ucont[0][1][1].z, 1.0e-12,
895 "parabolic inlet should vanish on the wall, not at the wall-adjacent centre"));
896 PetscCall(PicurvAssertBool((PetscBool)(ucont[0][3][3].z > ucont[0][2][2].z), "parabolic inlet centerline should exceed off-center velocity"));
897 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &ucont));
898
899 PetscCall(DestroyBoundaryHandler(&bc));
901 user->boundary_faces[BC_FACE_NEG_Z].params = NULL;
902 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
903 PetscFunctionReturn(0);
904}
905
906/**
907 * @brief Tests that the parabolic inlet is uniform along a periodic cross-stream axis.
908 * @details A channel periodic in i has walls only in j, so its inlet is the exact
909 * one-dimensional parabola in j.
910 */
912{
913 SimCtx *simCtx = NULL;
914 UserCtx *user = NULL;
915 BoundaryCondition *bc = NULL;
916 BCContext ctx;
917 Cmpnts ***ucont = NULL;
918
919 PetscFunctionBeginUser;
920 PetscCall(PetscMemzero(&ctx, sizeof(ctx)));
921 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
922 PetscCall(PicurvPopulateIdentityMetrics(user));
923 simCtx->i_periodic = 1;
924
928 PetscCall(AppendBCParam(&user->boundary_faces[BC_FACE_NEG_Z].params, "v_max", "4.0"));
929 ctx.user = user;
932 PetscCall(bc->Initialize(bc, &ctx));
933
934 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &ucont));
935 PetscCall(PicurvAssertRealNear(4.0, ucont[0][3][1].z, 1.0e-12, "periodic axis carries no parabola factor"));
936 PetscCall(PicurvAssertRealNear(ucont[0][3][1].z, ucont[0][3][4].z, 1.0e-12, "profile is uniform along the periodic axis"));
937 PetscCall(PicurvAssertRealNear(4.0 * 0.36, ucont[0][1][3].z, 1.0e-12, "wall-bounded axis keeps its parabola"));
938 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &ucont));
939
940 PetscCall(DestroyBoundaryHandler(&bc));
942 user->boundary_faces[BC_FACE_NEG_Z].params = NULL;
943 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
944 PetscFunctionReturn(0);
945}
946
947/**
948 * @brief Tests parabolic-inlet centerline, wall, and flux behavior across all faces.
949 */
951{
952 const BCFace faces[] = {
956 };
957 SimCtx *simCtx = NULL;
958 UserCtx *user = NULL;
959 BCContext ctx;
960 Cmpnts ***ucont = NULL;
961 Cmpnts ***ubcs = NULL;
962
963 PetscFunctionBeginUser;
964 PetscCall(PetscMemzero(&ctx, sizeof(ctx)));
965 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
966 PetscCall(PicurvPopulateIdentityMetrics(user));
967
968 for (size_t n = 0; n < sizeof(faces) / sizeof(faces[0]); ++n) {
969 const BCFace face = faces[n];
970 BoundaryCondition *bc = NULL;
971 PetscReal local_inflow = 0.0;
972 PetscReal local_outflow = 0.0;
973 PetscInt center_k, center_j, center_i;
974 PetscInt off_k, off_j, off_i;
975 PetscInt wall_k, wall_j, wall_i;
976 PetscInt ubcs_center_k, ubcs_center_j, ubcs_center_i;
977 const PetscReal expected_center = GetFaceOrientationSign(face) * 4.0;
978
979 PetscCall(VecSet(user->Ucont, 0.0));
980 PetscCall(VecSet(user->Bcs.Ubcs, 0.0));
982 user->boundary_faces[face].face_id = face;
985 PetscCall(AppendBCParam(&user->boundary_faces[face].params, "v_max", "4.0"));
986
987 ctx.user = user;
988 ctx.face_id = face;
990 PetscCall(bc->PreStep(bc, &ctx, &local_inflow, &local_outflow));
991 PetscCall(PicurvAssertRealNear(0.0, local_inflow, 1.0e-12, "parabolic inlet PreStep should leave inflow unchanged"));
992 PetscCall(PicurvAssertRealNear(0.0, local_outflow, 1.0e-12, "parabolic inlet PreStep should leave outflow unchanged"));
993 PetscCall(bc->Initialize(bc, &ctx));
994 PetscCall(bc->Apply(bc, &ctx));
995 PetscCall(bc->PostStep(bc, &ctx, &local_inflow, &local_outflow));
996
997 PetscCall(GetParabolicSampleSlots(user, face,
998 &center_k, &center_j, &center_i,
999 &off_k, &off_j, &off_i,
1000 &wall_k, &wall_j, &wall_i,
1001 &ubcs_center_k, &ubcs_center_j, &ubcs_center_i));
1002 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &ucont));
1003 PetscCall(DMDAVecGetArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
1004 PetscCall(PicurvAssertRealNear(expected_center, GetFaceNormalComponent(ucont[center_k][center_j][center_i], face), 1.0e-12, "parabolic inlet should peak at the face centerline"));
1005 PetscCall(PicurvAssertRealNear(expected_center, GetFaceNormalComponent(ubcs[ubcs_center_k][ubcs_center_j][ubcs_center_i], face), 1.0e-12, "parabolic inlet should write the centerline boundary velocity"));
1006 PetscCall(PicurvAssertRealNear(expected_center * 0.36 * 0.36, GetFaceNormalComponent(ucont[wall_k][wall_j][wall_i], face), 1.0e-12,
1007 "parabolic inlet should take its wall-adjacent value, vanishing only on the wall"));
1008 PetscCall(PicurvAssertBool((PetscBool)(PetscAbsReal(GetFaceNormalComponent(ucont[center_k][center_j][center_i], face)) >
1009 PetscAbsReal(GetFaceNormalComponent(ucont[off_k][off_j][off_i], face))),
1010 "parabolic inlet centerline magnitude should exceed the off-center magnitude"));
1011 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &ucont));
1012 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
1013 PetscCall(PicurvAssertBool((PetscBool)(PetscAbsReal(local_inflow) > 0.0), "parabolic inlet PostStep should accumulate a non-zero flux"));
1014 PetscCall(PicurvAssertBool((PetscBool)((local_inflow > 0.0) == (GetFaceOrientationSign(face) > 0.0)),
1015 "parabolic inlet PostStep should preserve the face orientation"));
1016 PetscCall(PicurvAssertRealNear(0.0, local_outflow, 1.0e-12, "parabolic inlet should not add to outflow"));
1017
1018 PetscCall(DestroyBoundaryHandler(&bc));
1020 }
1021
1022 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1023 PetscFunctionReturn(0);
1024}
1025/**
1026 * @brief Tests outlet conservation handler correction and post-step flux accounting.
1027 */
1028
1029static PetscErrorCode TestOutletConservationHandlerBehavior(void)
1030{
1031 SimCtx *simCtx = NULL;
1032 UserCtx *user = NULL;
1033 BoundaryCondition *bc = NULL;
1034 BCContext ctx;
1035 PetscReal global_inflow = 30.0;
1036 PetscReal global_farfield_in = 0.0;
1037 PetscReal global_farfield_out = 0.0;
1038 PetscReal global_outflow = 25.0;
1039 PetscReal local_inflow = 0.0;
1040 PetscReal local_outflow = 0.0;
1041 Cmpnts ***ucont = NULL;
1042 Cmpnts ***ucat = NULL;
1043
1044 PetscFunctionBeginUser;
1045 PetscCall(PetscMemzero(&ctx, sizeof(ctx)));
1046 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
1047 PetscCall(PicurvPopulateIdentityMetrics(user));
1048 PetscCall(VecSet(user->Ucat, 1.0));
1049 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
1050 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
1051 simCtx->AreaOutSum = (PetscReal)(user->info.mx - 2) * (PetscReal)(user->info.my - 2);
1052
1056
1057 ctx.user = user;
1058 ctx.face_id = BC_FACE_POS_Z;
1059 ctx.global_inflow_sum = &global_inflow;
1060 ctx.global_farfield_inflow_sum = &global_farfield_in;
1061 ctx.global_farfield_outflow_sum = &global_farfield_out;
1062 ctx.global_outflow_sum = &global_outflow;
1063
1065 PetscCall(bc->Apply(bc, &ctx));
1066 PetscCall(bc->PostStep(bc, &ctx, &local_inflow, &local_outflow));
1067
1068 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &ucont));
1069 PetscCall(DMDAVecGetArrayRead(user->fda, user->lUcat, &ucat));
1070 PetscCall(PicurvAssertRealNear(1.2, ucont[5][3][3].z, 1.0e-12, "outlet correction should add the expected flux trim"));
1071 PetscCall(PicurvAssertRealNear(1.0, ucat[5][3][3].z, 1.0e-12, "outlet handler should preserve the interior Ucat reference"));
1072 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &ucont));
1073 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->lUcat, &ucat));
1074
1075 PetscCall(PicurvAssertRealNear(30.0, local_outflow, 1.0e-12, "outlet PostStep should report corrected flux"));
1076 PetscCall(PicurvAssertRealNear(0.0, local_inflow, 1.0e-12, "outlet handler should not accumulate inflow"));
1077
1078 PetscCall(DestroyBoundaryHandler(&bc));
1079 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1080 PetscFunctionReturn(0);
1081}
1082
1083/**
1084 * @brief Tests outlet-conservation correction and flux accounting across all outlet faces.
1085 */
1087{
1088 const BCFace faces[] = {
1092 };
1093 SimCtx *simCtx = NULL;
1094 UserCtx *user = NULL;
1095 BCContext ctx;
1096 Cmpnts ***ucont = NULL;
1097 Cmpnts ***ubcs = NULL;
1098
1099 PetscFunctionBeginUser;
1100 PetscCall(PetscMemzero(&ctx, sizeof(ctx)));
1101 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
1102 PetscCall(PicurvPopulateIdentityMetrics(user));
1103 PetscCall(VecSet(user->Ucat, 1.0));
1104 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
1105 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
1106
1107 for (size_t n = 0; n < sizeof(faces) / sizeof(faces[0]); ++n) {
1108 const BCFace face = faces[n];
1109 BoundaryCondition *bc = NULL;
1110 PetscReal global_inflow = 0.0;
1111 PetscReal global_farfield_in = 0.0;
1112 PetscReal global_farfield_out = 0.0;
1113 PetscReal global_outflow = 0.0;
1114 PetscReal local_inflow = 0.0;
1115 PetscReal local_outflow = 0.0;
1116 PetscReal area = GetFaceInteriorPointCount(user, face);
1117 PetscInt ucont_k, ucont_j, ucont_i, ubcs_k, ubcs_j, ubcs_i;
1118
1119 PetscCall(VecSet(user->Ucont, 0.0));
1120 PetscCall(VecSet(user->Bcs.Ubcs, 0.0));
1122 user->boundary_faces[face].face_id = face;
1125 user->simCtx->AreaOutSum = area;
1126
1127 ctx.user = user;
1128 ctx.face_id = face;
1129 ctx.global_inflow_sum = &global_inflow;
1130 ctx.global_farfield_inflow_sum = &global_farfield_in;
1131 ctx.global_farfield_outflow_sum = &global_farfield_out;
1132 ctx.global_outflow_sum = &global_outflow;
1133
1135 PetscCall(bc->PreStep(bc, &ctx, &local_inflow, &local_outflow));
1136 PetscCall(PicurvAssertRealNear(area, local_outflow, 1.0e-12, "outlet PreStep should measure the uncorrected face flux"));
1137
1138 global_inflow = area + 0.2 * area;
1139 global_outflow = local_outflow;
1140 local_outflow = 0.0;
1141 PetscCall(bc->Apply(bc, &ctx));
1142 PetscCall(bc->PostStep(bc, &ctx, &local_inflow, &local_outflow));
1143
1144 PetscCall(GetRepresentativeFaceSlots(user, face, &ucont_k, &ucont_j, &ucont_i, &ubcs_k, &ubcs_j, &ubcs_i));
1145 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &ucont));
1146 PetscCall(DMDAVecGetArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
1147 PetscCall(PicurvAssertRealNear(1.2, GetFaceNormalComponent(ucont[ucont_k][ucont_j][ucont_i], face), 1.0e-12, "outlet correction should add the configured flux trim"));
1148 PetscCall(PicurvAssertRealNear(1.0, ubcs[ubcs_k][ubcs_j][ubcs_i].x, 1.0e-12, "outlet handler should copy Ucat into Ubcs.x"));
1149 PetscCall(PicurvAssertRealNear(1.0, ubcs[ubcs_k][ubcs_j][ubcs_i].y, 1.0e-12, "outlet handler should copy Ucat into Ubcs.y"));
1150 PetscCall(PicurvAssertRealNear(1.0, ubcs[ubcs_k][ubcs_j][ubcs_i].z, 1.0e-12, "outlet handler should copy Ucat into Ubcs.z"));
1151 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &ucont));
1152 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Bcs.Ubcs, &ubcs));
1153 PetscCall(PicurvAssertRealNear(0.0, local_inflow, 1.0e-12, "outlet handler should not add to inflow"));
1154 PetscCall(PicurvAssertRealNear(1.2 * area, local_outflow, 1.0e-12, "outlet PostStep should report the corrected flux"));
1155
1156 PetscCall(DestroyBoundaryHandler(&bc));
1158 }
1159
1160 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1161 PetscFunctionReturn(0);
1162}
1163
1164/**
1165 * @brief Tests non-periodic cell-field finalization without modifying Ucont.
1166 */
1168{
1169 SimCtx *simCtx = NULL;
1170 UserCtx *user = NULL;
1171 Cmpnts ***ucat = NULL;
1172 Cmpnts ***lucat = NULL;
1173
1174 PetscFunctionBeginUser;
1175 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
1176 PetscCall(VecSet(user->Ucat, 4.0));
1177 PetscCall(VecSet(user->Bcs.Ubcs, 1.0));
1178 PetscCall(VecSet(user->P, 3.0));
1179 PetscCall(VecSet(user->Ucont, 7.0));
1180
1181 PetscCall(FinalizePostProjectionCellFields(user));
1182
1183 PetscCall(PicurvAssertVecConstant(user->Ucont, 7.0, 1.0e-12,
1184 "post-projection cell finalization must preserve non-periodic Ucont"));
1185 PetscCall(PicurvAssertVecConstant(user->P, 3.0, 1.0e-12,
1186 "constant pressure should remain unchanged during cell finalization"));
1187
1188 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucat, &ucat));
1189 PetscCall(DMDAVecGetArrayRead(user->fda, user->lUcat, &lucat));
1190 PetscCall(PicurvAssertRealNear(-2.0, ucat[2][2][0].x, 1.0e-12,
1191 "non-periodic Ucat dummy face should use Ubcs extrapolation"));
1192 PetscCall(PicurvAssertRealNear(-2.0, lucat[2][2][0].x, 1.0e-12,
1193 "non-periodic local Ucat should contain the finalized dummy face"));
1194 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucat, &ucat));
1195 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->lUcat, &lucat));
1196
1197 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1198 PetscFunctionReturn(0);
1199}
1200/**
1201 * @brief EnforceRHSBoundaryConditions() must zero a row exactly when it is not an unknown.
1202 *
1203 * This is the invariant that ties the residual path to ClassifyMomentumRow(). It is
1204 * checked across a non-periodic, a singly-periodic and a fully-periodic configuration
1205 * because the defect it guards against was visible only when an axis was periodic:
1206 * the duplicate column at index 0 was left unzeroed, so ComputeTotalResidual()'s BDF
1207 * term accumulated there on every call and the residual norm stopped describing the
1208 * state. A sentinel fill makes an untouched row detectable rather than accidentally
1209 * correct.
1210 */
1212{
1213 const struct {
1214 const char *label;
1215 PetscBool x_periodic, y_periodic, z_periodic;
1216 } configs[] = {
1217 {"non-periodic", PETSC_FALSE, PETSC_FALSE, PETSC_FALSE},
1218 {"xi-periodic", PETSC_TRUE, PETSC_FALSE, PETSC_FALSE},
1219 {"fully-periodic", PETSC_TRUE, PETSC_TRUE, PETSC_TRUE },
1220 };
1221 const PetscScalar sentinel = 7.0;
1222
1223 PetscFunctionBeginUser;
1224 for (size_t c = 0; c < sizeof(configs) / sizeof(configs[0]); ++c) {
1225 SimCtx *simCtx = NULL;
1226 UserCtx *user = NULL;
1227 Cmpnts ***rhs = NULL;
1228 const PetscBool periodic[3] = {configs[c].x_periodic, configs[c].y_periodic, configs[c].z_periodic};
1229 const BCFace neg_face[3] = {BC_FACE_NEG_X, BC_FACE_NEG_Y, BC_FACE_NEG_Z};
1230 const BCFace pos_face[3] = {BC_FACE_POS_X, BC_FACE_POS_Y, BC_FACE_POS_Z};
1231
1232 PetscCall(PicurvCreateMinimalContextsWithPeriodicity(&simCtx, &user, 6, 6, 6,
1233 configs[c].x_periodic,
1234 configs[c].y_periodic,
1235 configs[c].z_periodic));
1236 for (PetscInt axis = 0; axis < 3; ++axis) {
1237 const BCType type = periodic[axis] ? PERIODIC : WALL;
1238 user->boundary_faces[neg_face[axis]].mathematical_type = type;
1239 user->boundary_faces[pos_face[axis]].mathematical_type = type;
1240 }
1241
1242 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
1243 PetscCall(VecSet(user->Rhs, sentinel));
1244 PetscCall(EnforceRHSBoundaryConditions(user));
1245
1246 PetscCall(DMDAVecGetArrayRead(user->fda, user->Rhs, &rhs));
1247 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k) {
1248 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j) {
1249 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
1250 const PetscScalar *row = &rhs[k][j][i].x;
1251 for (PetscInt component = 0; component < 3; ++component) {
1252 PetscInt ri, rj, rk;
1253 const MomentumRowType type =
1254 ClassifyMomentumRow(user, i, j, k, component, &ri, &rj, &rk);
1255 const PetscScalar expected = (type == MOM_ROW_PHYSICAL) ? sentinel : 0.0;
1256 char context[192];
1257
1258 PetscCall(PetscSNPrintf(context, sizeof(context),
1259 "%s: row (i=%d,j=%d,k=%d,c=%d) classified %d must be %s",
1260 configs[c].label, (int)i, (int)j, (int)k, (int)component, (int)type,
1261 (type == MOM_ROW_PHYSICAL) ? "left untouched" : "zeroed"));
1262 PetscCall(PicurvAssertRealNear((PetscReal)expected, (PetscReal)row[component],
1263 1.0e-14, context));
1264 }
1265 }
1266 }
1267 }
1268 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Rhs, &rhs));
1269
1270 PetscCall(VecDestroy(&user->Rhs));
1271 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1272 }
1273 PetscFunctionReturn(0);
1274}
1275
1276/**
1277 * @brief Allocates the wall-model storage the way production allocates it.
1278 *
1279 * Production creates each global on `da` and ghosts it into the local view. A fixture
1280 * that allocates differently - a different DM, only one of a pair, or only one of the
1281 * fields the same block creates - tests a layout the solver never uses, which is how a
1282 * DM/dof mismatch survived in `ApplyWallFunction` while these cases passed. The wall
1283 * eddy viscosity is allocated in that same block and cleared at the top of every pass,
1284 * so a fixture omitting it dereferences a null vector before reaching any assertion.
1285 * Every wall case routes through here so the two cannot drift.
1286 */
1287static PetscErrorCode SeedWallFrictionVelocityStorage(UserCtx *user)
1288{
1289 PetscFunctionBeginUser;
1290 PetscCall(DMCreateGlobalVector(user->da, &user->Friction_Velocity));
1291 PetscCall(VecSet(user->Friction_Velocity, 0.0));
1292 PetscCall(DMCreateLocalVector(user->da, &user->lFriction_Velocity));
1293 PetscCall(VecSet(user->lFriction_Velocity, 0.0));
1294 PetscCall(DMCreateGlobalVector(user->da, &user->Nu_Wall));
1295 PetscCall(VecSet(user->Nu_Wall, 0.0));
1296 PetscCall(DMCreateLocalVector(user->da, &user->lNu_Wall));
1297 PetscCall(VecSet(user->lNu_Wall, 0.0));
1298 PetscFunctionReturn(0);
1299}
1300
1301/**
1302 * @brief Seeds the fields ApplyWallFunction() reads on a single wall face.
1303 *
1304 * A uniform tangential velocity and unit metrics make the wall distances and the
1305 * face normal analytically known, so the test can reproduce the model's own
1306 * arithmetic without copying the integration's index choices.
1307 */
1308static PetscErrorCode SeedWallFunctionFixture(UserCtx *user, PetscReal tangential_speed)
1309{
1310 Cmpnts ***ucat = NULL, ***csi = NULL;
1311 PetscReal ***aj = NULL;
1312 DMDALocalInfo info = user->info;
1313
1314 PetscFunctionBeginUser;
1315 PetscCall(VecSet(user->Aj, 1.0));
1316 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
1317 PetscCall(DMDAVecGetArray(user->fda, user->Csi, &csi));
1318 PetscCall(DMDAVecGetArray(user->da, user->Aj, &aj));
1319 for (PetscInt k = info.zs; k < info.zs + info.zm; ++k)
1320 for (PetscInt j = info.ys; j < info.ys + info.ym; ++j)
1321 for (PetscInt i = info.xs; i < info.xs + info.xm; ++i) {
1322 /* Flow along eta, so the whole velocity is tangential to a xi wall. */
1323 ucat[k][j][i] = (Cmpnts){.x = 0.0, .y = tangential_speed, .z = 0.0};
1324 csi[k][j][i] = (Cmpnts){.x = 1.0, .y = 0.0, .z = 0.0};
1325 aj[k][j][i] = 1.0;
1326 }
1327 PetscCall(DMDAVecRestoreArray(user->da, user->Aj, &aj));
1328 PetscCall(DMDAVecRestoreArray(user->fda, user->Csi, &csi));
1329 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
1330 PetscCall(DMGlobalToLocalBegin(user->da, user->Aj, INSERT_VALUES, user->lAj));
1331 PetscCall(DMGlobalToLocalEnd(user->da, user->Aj, INSERT_VALUES, user->lAj));
1332 PetscCall(DMGlobalToLocalBegin(user->fda, user->Csi, INSERT_VALUES, user->lCsi));
1333 PetscCall(DMGlobalToLocalEnd(user->fda, user->Csi, INSERT_VALUES, user->lCsi));
1334 PetscFunctionReturn(PETSC_SUCCESS);
1335}
1336
1337/**
1338 * @brief Tests that the wall-function integration reproduces the model it dispatches to.
1339 *
1340 * The kernels are covered separately; what is unverified without this is the wiring
1341 * around them, where the plausible failures are silent. Feeding the reference and
1342 * boundary wall distances in the wrong order, taking the reference velocity from the
1343 * wrong cell, or normalising the face normal incorrectly all still produce a
1344 * plausible-looking corrected velocity. The check here is that the corrected
1345 * first-interior velocity is exactly what the log-law model predicts at that cell's
1346 * distance, for the friction velocity the integration itself stored.
1347 */
1349{
1350 SimCtx *simCtx = NULL;
1351 UserCtx *user = NULL;
1352 Cmpnts ***ucat = NULL, ***ubcs = NULL;
1353 PetscReal ***utau = NULL;
1354 const PetscReal speed = 1.0;
1355 const PetscReal roughness = 1.0e-16; /* smooth wall */
1356 PetscReal kinematic_viscosity, distance_first, distance_second;
1357 PetscReal stored_utau, corrected_speed, expected_speed, reference_check;
1358 PetscReal untouched_before = 0.0, untouched_after = 0.0;
1359
1360 PetscFunctionBeginUser;
1361 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
1362 simCtx->ren = 1000.0;
1363 simCtx->wall_roughness_height = roughness;
1364 kinematic_viscosity = 1.0 / simCtx->ren;
1365
1366 PetscCall(SeedWallFrictionVelocityStorage(user));
1368 PetscCall(SeedWallFunctionFixture(user, speed));
1369
1370 /* With unit Jacobians and a unit face area the model's distances are exact:
1371 the first cell centre sits half a cell from the wall, the second one and a
1372 half. Reproducing them here rather than reading them back is what makes a
1373 swapped-distance wiring error detectable. */
1374 distance_first = 0.5;
1375 distance_second = 2.0 * distance_first + 0.5;
1376
1377 /* Disabled: the routine must not touch the field at all. */
1378 simCtx->wallfunction = 0;
1379 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucat, (const Cmpnts ***)&ucat));
1380 untouched_before = ucat[2][2][1].y;
1381 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucat, (const Cmpnts ***)&ucat));
1382 PetscCall(ApplyWallFunction(user));
1383 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucat, (const Cmpnts ***)&ucat));
1384 untouched_after = ucat[2][2][1].y;
1385 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucat, (const Cmpnts ***)&ucat));
1386 PetscCall(PicurvAssertRealNear(untouched_before, untouched_after, 0.0,
1387 "a disabled wall function must leave the velocity untouched"));
1388
1389 /* Enabled: the first interior cell is corrected onto the modelled profile. */
1390 simCtx->wallfunction = 1;
1391 PetscCall(ApplyWallFunction(user));
1392
1393 PetscCall(DMDAVecGetArrayRead(user->da, user->Friction_Velocity, (const PetscReal ***)&utau));
1394 stored_utau = utau[2][2][1];
1395 PetscCall(DMDAVecRestoreArrayRead(user->da, user->Friction_Velocity, (const PetscReal ***)&utau));
1396 PetscCall(PicurvAssertBool((PetscBool)(stored_utau > 0.0),
1397 "the integration must store a positive friction velocity"));
1398
1399 /* The stored friction velocity must be the one that reproduces the *reference*
1400 cell's speed at the *reference* distance. Swapping the two distances passes the
1401 next check but fails this one. */
1402 reference_check = u_hydset_roughness(kinematic_viscosity, distance_second,
1403 stored_utau, roughness);
1404 PetscCall(PicurvAssertRealNear(speed, reference_check, 1.0e-6,
1405 "the friction velocity must invert the log law at the "
1406 "reference cell's distance"));
1407
1408 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucat, (const Cmpnts ***)&ucat));
1409 corrected_speed = ucat[2][2][1].y;
1410 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucat, (const Cmpnts ***)&ucat));
1411
1412 expected_speed = u_hydset_roughness(kinematic_viscosity, distance_first,
1413 stored_utau, roughness);
1414 PetscCall(PicurvAssertRealNear(expected_speed, corrected_speed, 1.0e-9,
1415 "the corrected velocity must be the modelled profile at "
1416 "the first interior cell's distance"));
1417 PetscCall(PicurvAssertBool((PetscBool)(corrected_speed < speed),
1418 "a log-law wall correction must slow the first interior cell "
1419 "relative to the reference cell"));
1420
1421 /* The ghost row carries no boundary velocity of its own. */
1422 PetscCall(DMDAVecGetArrayRead(user->fda, user->Bcs.Ubcs, (const Cmpnts ***)&ubcs));
1423 PetscCall(PicurvAssertRealNear(0.0, ubcs[2][2][0].y, 0.0,
1424 "the wall ghost row must carry a zero boundary velocity"));
1425 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Bcs.Ubcs, (const Cmpnts ***)&ubcs));
1426
1427 PetscCall(VecDestroy(&user->Friction_Velocity));
1428 PetscCall(VecDestroy(&user->lFriction_Velocity));
1429 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1430 PetscFunctionReturn(0);
1431}
1432
1433/**
1434 * @brief Tests that the model selector actually changes which law is applied.
1435 *
1436 * The selector was previously validated in the configuration layer and then dropped, so
1437 * every case ran the log law whatever it asked for. Distinct laws must produce distinct
1438 * corrections from identical inputs, or the selector is decorative again.
1439 */
1441{
1442 SimCtx *simCtx = NULL;
1443 UserCtx *user = NULL;
1444 Cmpnts ***ucat = NULL;
1445 PetscReal corrected[4] = {0.0, 0.0, 0.0, 0.0};
1446 const PetscInt models[3] = {WALL_FUNCTION_LOG_LAW, WALL_FUNCTION_WERNER, WALL_FUNCTION_CABOT};
1447
1448 PetscFunctionBeginUser;
1449 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
1450 simCtx->ren = 1000.0;
1451 simCtx->wall_roughness_height = 1.0e-16;
1452 PetscCall(SeedWallFrictionVelocityStorage(user));
1454
1455 for (PetscInt m = 0; m < 3; ++m) {
1456 PetscCall(SeedWallFunctionFixture(user, 1.0));
1457 simCtx->wallfunction = models[m];
1458 PetscCall(ApplyWallFunction(user));
1459 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucat, (const Cmpnts ***)&ucat));
1460 corrected[m] = ucat[2][2][1].y;
1461 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucat, (const Cmpnts ***)&ucat));
1462 PetscCall(PicurvAssertBool((PetscBool)(corrected[m] > 0.0 && corrected[m] < 1.0),
1463 "every wall model must slow the first interior cell"));
1464 }
1465
1466 /* Each law has its own profile, so no two may coincide. */
1467 PetscCall(PicurvAssertBool((PetscBool)(PetscAbsReal(corrected[0] - corrected[1]) > 1.0e-9),
1468 "the Werner-Wengle model must differ from the log law"));
1469 PetscCall(PicurvAssertBool((PetscBool)(PetscAbsReal(corrected[0] - corrected[2]) > 1.0e-9),
1470 "the Cabot model must differ from the log law"));
1471
1472 PetscCall(VecDestroy(&user->Friction_Velocity));
1473 PetscCall(VecDestroy(&user->lFriction_Velocity));
1474 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1475 PetscFunctionReturn(0);
1476}
1477
1478/**
1479 * @brief Tests that a face which is not a wall is left alone by the wall function.
1480 */
1482{
1483 SimCtx *simCtx = NULL;
1484 UserCtx *user = NULL;
1485 Cmpnts ***ucat = NULL;
1486 PetscReal before = 0.0, after = 0.0;
1487
1488 PetscFunctionBeginUser;
1489 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
1490 simCtx->ren = 1000.0;
1491 simCtx->wallfunction = 1;
1492 simCtx->wall_roughness_height = 1.0e-16;
1493 PetscCall(SeedWallFrictionVelocityStorage(user));
1494 /* Every face left at its default, so none is a wall. */
1495 PetscCall(SeedWallFunctionFixture(user, 1.0));
1496
1497 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucat, (const Cmpnts ***)&ucat));
1498 before = ucat[2][2][1].y;
1499 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucat, (const Cmpnts ***)&ucat));
1500 PetscCall(ApplyWallFunction(user));
1501 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucat, (const Cmpnts ***)&ucat));
1502 after = ucat[2][2][1].y;
1503 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucat, (const Cmpnts ***)&ucat));
1504
1505 PetscCall(PicurvAssertRealNear(before, after, 0.0,
1506 "a face that is not a wall must not be corrected"));
1507
1508 PetscCall(VecDestroy(&user->Friction_Velocity));
1509 PetscCall(VecDestroy(&user->lFriction_Velocity));
1510 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1511 PetscFunctionReturn(0);
1512}
1513
1514/**
1515 * @brief Tests that every wall law's friction velocity responds to the flow it is given.
1516 *
1517 * A law that ignores the resolved velocity and reports a fixed number still slows the
1518 * first cell and still differs from the other laws, so the selector test above passes
1519 * over it. Driving a law at two different speeds and requiring the friction velocity to
1520 * follow is what distinguishes a model from a constant.
1521 *
1522 * All three laws are covered. Werner-Wengle is the reason this case exists: it used to
1523 * report its Newton initial guess rather than solving, so every wall cell in a run
1524 * carried the same u_tau while still slowing the cell and still differing from the other
1525 * laws, which is everything the selector test above checks.
1526 */
1528{
1529 SimCtx *simCtx = NULL;
1530 UserCtx *user = NULL;
1531 PetscReal ***utau = NULL;
1532 const PetscInt models[3] = {WALL_FUNCTION_LOG_LAW, WALL_FUNCTION_WERNER,
1534 const char *names[3] = {"the log law must respond to the flow it is given",
1535 "Werner-Wengle must respond to the flow it is given",
1536 "Cabot must respond to the flow it is given"};
1537 const PetscReal speeds[2] = {0.5, 2.0};
1538
1539 PetscFunctionBeginUser;
1540 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
1541 simCtx->ren = 1000.0;
1542 simCtx->wall_roughness_height = 1.0e-16;
1543 PetscCall(SeedWallFrictionVelocityStorage(user));
1545
1546 for (PetscInt m = 0; m < 3; ++m) {
1547 PetscReal stored[2] = {0.0, 0.0};
1548
1549 simCtx->wallfunction = models[m];
1550 for (PetscInt s = 0; s < 2; ++s) {
1551 PetscCall(SeedWallFunctionFixture(user, speeds[s]));
1552 PetscCall(ApplyWallFunction(user));
1553 PetscCall(DMDAVecGetArrayRead(user->da, user->Friction_Velocity,
1554 (const PetscReal ***)&utau));
1555 stored[s] = utau[2][2][1];
1556 PetscCall(DMDAVecRestoreArrayRead(user->da, user->Friction_Velocity,
1557 (const PetscReal ***)&utau));
1558 PetscCall(PicurvAssertBool((PetscBool)(stored[s] > 0.0),
1559 "a wall law must report a positive friction velocity"));
1560 }
1561
1562 /* Faster flow, more wall stress. Any law that reports its initial guess, or any
1563 other constant, fails here whatever that constant is. */
1564 PetscCall(PicurvAssertBool((PetscBool)(stored[1] > stored[0] * 1.05),
1565 names[m]));
1566 }
1567
1568 PetscCall(VecDestroy(&user->Friction_Velocity));
1569 PetscCall(VecDestroy(&user->lFriction_Velocity));
1570 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1571 PetscFunctionReturn(0);
1572}
1573
1574/**
1575 * @brief Tests that the friction-velocity storage matches what the field catalog declares.
1576 *
1577 * The wall model reaches its storage through a DM, and PETSc only rejects a mismatched
1578 * DM/vector pairing at the moment of access - inside a branch that runs only when wall
1579 * functions are enabled on a case that has a WALL face. That is how storage created from
1580 * a dof-3 DM and read through a dof-1 one stayed latent. Comparing the allocation against
1581 * the catalog descriptor turns that into a failure here.
1582 */
1584{
1585 SimCtx *simCtx = NULL;
1586 UserCtx *user = NULL;
1587 const FieldDescriptor *descriptor = NULL;
1588 FieldView view;
1589 PetscInt global_size = 0, local_size = 0;
1590 PetscInt xm = 0, ym = 0, zm = 0, gxm = 0, gym = 0, gzm = 0;
1591 PetscReal ***utau = NULL;
1592
1593 PetscFunctionBeginUser;
1594 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
1595 PetscCall(SeedWallFrictionVelocityStorage(user));
1596
1597 PetscCall(FieldGetDescriptor(FIELD_ID_U_TAU, &descriptor));
1598 PetscCall(PicurvAssertBool((PetscBool)(descriptor->dof == 1),
1599 "the friction velocity is one scalar per cell"));
1600 PetscCall(PicurvAssertBool((PetscBool)(descriptor->dm_kind == FIELD_DM_DA),
1601 "a scalar cell field belongs on the dof-1 DM"));
1602
1603 PetscCall(FieldGetView(user, FIELD_ID_U_TAU, &view));
1604 PetscCall(PicurvAssertBool((PetscBool)(view.global_vec == user->Friction_Velocity &&
1605 view.local_vec == user->lFriction_Velocity),
1606 "the catalog must resolve to the vectors setup allocated"));
1607
1608 /* The sizes the DM implies, against the sizes the vectors actually have. */
1609 PetscCall(DMDAGetCorners(user->da, NULL, NULL, NULL, &xm, &ym, &zm));
1610 PetscCall(DMDAGetGhostCorners(user->da, NULL, NULL, NULL, &gxm, &gym, &gzm));
1611 PetscCall(VecGetLocalSize(user->Friction_Velocity, &global_size));
1612 PetscCall(VecGetLocalSize(user->lFriction_Velocity, &local_size));
1613 PetscCall(PicurvAssertBool((PetscBool)(global_size == xm * ym * zm * descriptor->dof),
1614 "the global friction velocity must match the owned extent"));
1615 PetscCall(PicurvAssertBool((PetscBool)(local_size == gxm * gym * gzm * descriptor->dof),
1616 "the local friction velocity must match the ghosted extent"));
1617
1618 /* The access the wall model itself performs. A mismatched pairing fails right here. */
1619 PetscCall(DMDAVecGetArray(user->da, user->Friction_Velocity, &utau));
1620 PetscCall(DMDAVecRestoreArray(user->da, user->Friction_Velocity, &utau));
1621
1622 PetscCall(VecDestroy(&user->Friction_Velocity));
1623 PetscCall(VecDestroy(&user->lFriction_Velocity));
1624 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1625 PetscFunctionReturn(0);
1626}
1627
1628/**
1629 * @brief Tests that the wall pass records the statistics its diagnostics report.
1630 *
1631 * `y+` cannot be recovered downstream, so it is formed where the wall distance is still
1632 * in hand. This checks that what is accumulated there is the same quantity the reported
1633 * column claims to be, and that a pass over no wall face records nothing rather than
1634 * recording zeros.
1635 */
1637{
1638 SimCtx *simCtx = NULL;
1639 UserCtx *user = NULL;
1640 PetscReal kinematic_viscosity = 0.0;
1641 PetscReal expected_y_plus = 0.0;
1642 const PetscReal distance_first = 0.5;
1643
1644 PetscFunctionBeginUser;
1645 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
1646 simCtx->ren = 1000.0;
1647 simCtx->wall_roughness_height = 1.0e-16;
1648 kinematic_viscosity = 1.0 / simCtx->ren;
1649 PetscCall(SeedWallFrictionVelocityStorage(user));
1651 PetscCall(SeedWallFunctionFixture(user, 1.0));
1652
1653 /* Disabled: nothing is corrected, so nothing is recorded. */
1654 simCtx->wallfunction = 0;
1655 PetscCall(ApplyWallFunction(user));
1656 PetscCall(PicurvAssertBool((PetscBool)(user->wall_diagnostics.cells == 0),
1657 "a disabled wall model must record no corrected cells"));
1658
1659 simCtx->wallfunction = 1;
1660 PetscCall(ApplyWallFunction(user));
1661 PetscCall(PicurvAssertBool((PetscBool)(user->wall_diagnostics.cells > 0),
1662 "an enabled wall model must record the cells it corrected"));
1663
1664 /* One face, so the sums are one cell's values and y+ is exactly u_tau * y / nu.
1665 Reproducing it from the stored friction velocity is what makes a wrong distance
1666 or a wrong viscosity in the accumulation visible. */
1667 expected_y_plus = user->wall_diagnostics.friction_velocity_sum * distance_first /
1668 kinematic_viscosity / (PetscReal)user->wall_diagnostics.cells;
1670 (PetscReal)user->wall_diagnostics.cells,
1671 expected_y_plus, 1.0e-9,
1672 "y+ must be formed from the same distance the "
1673 "correction used"));
1675 (PetscReal)user->wall_diagnostics.cells,
1676 distance_first, 1.0e-9,
1677 "the recorded wall distance must be the first cell's"));
1678 PetscCall(PicurvAssertBool((PetscBool)(user->wall_diagnostics.friction_velocity_min <=
1680 "the recorded extrema must bracket each other"));
1681
1682 /* A second pass must describe that pass alone, not both. */
1683 {
1684 const PetscInt first_pass_cells = user->wall_diagnostics.cells;
1685
1686 PetscCall(ApplyWallFunction(user));
1687 PetscCall(PicurvAssertBool((PetscBool)(user->wall_diagnostics.cells == first_pass_cells),
1688 "each pass must replace the previous pass's sample set"));
1689 }
1690
1691 PetscCall(VecDestroy(&user->Friction_Velocity));
1692 PetscCall(VecDestroy(&user->lFriction_Velocity));
1693 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1694 PetscFunctionReturn(0);
1695}
1696
1697/**
1698 * @brief Checks each cell-centred field's dummy-cell rule, faces then edges.
1699 * @details Interior cells carry index-unique sentinels and every dummy cell starts
1700 * poisoned, so a wrong offset or a missed face shows. Velocity dummies must put
1701 * the face average on `Ubcs`; pressure and the particle scalar copy the adjacent
1702 * cell (zero normal gradient); each edge dummy is the mean of its two face
1703 * neighbours; and a field with no rule, or one that is not cell-centred, is refused.
1704 */
1706{
1707 SimCtx *simCtx = NULL;
1708 UserCtx *user = NULL;
1709 const FieldId scalars[] = {FIELD_ID_P, FIELD_ID_PSI};
1710
1711 PetscFunctionBeginUser;
1712 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 5, 6, 7));
1713 const DMDALocalInfo info = user->info;
1714 const PetscInt mx = info.mx, my = info.my, mz = info.mz;
1715 PetscCall(VecSet(user->Bcs.Ubcs, 0.0));
1716 {
1717 PetscReal ***p = NULL, ***psi = NULL;
1718 Cmpnts ***ucat = NULL, ***ubcs = NULL;
1719
1720 PetscCall(DMDAVecGetArray(user->da, user->P, &p));
1721 PetscCall(DMDAVecGetArray(user->da, user->Psi, &psi));
1722 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
1723 PetscCall(DMDAVecGetArray(user->fda, user->Bcs.Ubcs, &ubcs));
1724 for (PetscInt k = info.zs; k < info.zs + info.zm; k++)
1725 for (PetscInt j = info.ys; j < info.ys + info.ym; j++)
1726 for (PetscInt i = info.xs; i < info.xs + info.xm; i++) {
1727 const PetscBool interior = (PetscBool)(i > 0 && i < mx - 1 && j > 0 && j < my - 1 && k > 0 && k < mz - 1);
1728 const PetscReal sentinel = 100.0 * k + 10.0 * j + i;
1729
1730 p[k][j][i] = interior ? sentinel : -999.0;
1731 psi[k][j][i] = interior ? -sentinel : -999.0;
1732 ucat[k][j][i].x = interior ? sentinel : -999.0;
1733 ucat[k][j][i].y = interior ? 2.0 * sentinel : -999.0;
1734 ucat[k][j][i].z = interior ? 3.0 * sentinel : -999.0;
1735 ubcs[k][j][i] = (Cmpnts){1.0, 2.0, 3.0};
1736 }
1737 PetscCall(DMDAVecRestoreArray(user->fda, user->Bcs.Ubcs, &ubcs));
1738 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
1739 PetscCall(DMDAVecRestoreArray(user->da, user->Psi, &psi));
1740 PetscCall(DMDAVecRestoreArray(user->da, user->P, &p));
1741 }
1742
1743 PetscCall(UpdateDummyCells(user, FIELD_ID_UCAT));
1744 PetscCall(UpdateCornerNodes(user, FIELD_ID_UCAT));
1745 for (size_t n = 0; n < sizeof(scalars) / sizeof(scalars[0]); ++n) {
1746 PetscCall(UpdateDummyCells(user, scalars[n]));
1747 PetscCall(UpdateCornerNodes(user, scalars[n]));
1748 }
1749
1750 for (size_t n = 0; n < sizeof(scalars) / sizeof(scalars[0]); ++n) {
1751 FieldView view;
1752 PetscReal ***f = NULL;
1753
1754 PetscCall(FieldGetView(user, scalars[n], &view));
1755 PetscCall(DMDAVecGetArrayRead(user->da, view.global_vec, &f));
1756 for (PetscInt k = 1; k < mz - 1; k++) for (PetscInt j = 1; j < my - 1; j++) {
1757 PetscCall(PicurvAssertRealNear(f[k][j][1], f[k][j][0], 0.0, "-x dummy copies the adjacent cell"));
1758 PetscCall(PicurvAssertRealNear(f[k][j][mx - 2], f[k][j][mx - 1], 0.0, "+x dummy copies the adjacent cell"));
1759 }
1760 for (PetscInt k = 1; k < mz - 1; k++) for (PetscInt i = 1; i < mx - 1; i++) {
1761 PetscCall(PicurvAssertRealNear(f[k][1][i], f[k][0][i], 0.0, "-y dummy copies the adjacent cell"));
1762 PetscCall(PicurvAssertRealNear(f[k][my - 2][i], f[k][my - 1][i], 0.0, "+y dummy copies the adjacent cell"));
1763 }
1764 for (PetscInt j = 1; j < my - 1; j++) for (PetscInt i = 1; i < mx - 1; i++) {
1765 PetscCall(PicurvAssertRealNear(f[1][j][i], f[0][j][i], 0.0, "-z dummy copies the adjacent cell"));
1766 PetscCall(PicurvAssertRealNear(f[mz - 2][j][i], f[mz - 1][j][i], 0.0, "+z dummy copies the adjacent cell"));
1767 }
1768 for (PetscInt j = 0; j < my; j++) {
1769 PetscCall(PicurvAssertRealNear(0.5 * (f[1][j][0] + f[0][j][1]), f[0][j][0], 0.0,
1770 "a (-z,-x) edge dummy is the mean of its face neighbours"));
1771 }
1772 PetscCall(PicurvAssertBool((PetscBool)(f[0][0][0] != -999.0 && f[mz - 1][my - 1][mx - 1] != -999.0),
1773 "corner dummies are written"));
1774 PetscCall(DMDAVecRestoreArrayRead(user->da, view.global_vec, &f));
1775 }
1776 {
1777 const Cmpnts ***ucat = NULL;
1778
1779 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucat, &ucat));
1780 for (PetscInt k = 1; k < mz - 1; k++) for (PetscInt j = 1; j < my - 1; j++) {
1781 PetscCall(PicurvAssertRealNear(1.0, 0.5 * (ucat[k][j][0].x + ucat[k][j][1].x), 1.0e-12,
1782 "a velocity dummy puts the face average on Ubcs"));
1783 PetscCall(PicurvAssertRealNear(3.0, 0.5 * (ucat[k][j][mx - 1].z + ucat[k][j][mx - 2].z), 1.0e-12,
1784 "a velocity dummy puts the face average on Ubcs"));
1785 }
1786 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucat, &ucat));
1787 }
1788
1789 {
1790 PetscErrorCode refused;
1791
1792 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1793 refused = UpdateDummyCells(user, FIELD_ID_NVERT);
1794 PetscCall(PetscPopErrorHandler());
1795 PetscCall(PicurvAssertIntEqual(PETSC_ERR_SUP, refused, "a field with no boundary rule is refused"));
1796 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1797 refused = UpdateDummyCells(user, FIELD_ID_COORDINATES);
1798 PetscCall(PetscPopErrorHandler());
1799 PetscCall(PicurvAssertIntEqual(PETSC_ERR_ARG_WRONG, refused, "a field that is not cell-centred is refused"));
1800 }
1801
1802 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1803 PetscFunctionReturn(0);
1804}
1805
1806/**
1807 * @brief Runs the unit-boundaries PETSc test binary.
1808 */
1809
1810int main(int argc, char **argv)
1811{
1812 PetscErrorCode ierr;
1813 const PicurvTestCase cases[] = {
1814 {"can-rank-service-face-matches-inlet-when-defined", TestCanRankServiceFaceMatchesInletWhenDefined},
1815 {"can-rank-service-inlet-face-requires-definition", TestCanRankServiceInletFaceRequiresDefinition},
1816 {"boundary-condition-factory-assignments", TestBoundaryConditionFactoryAssignments},
1817 {"boundary-condition-factory-implemented-handler-matrix", TestBoundaryConditionFactoryImplementedHandlerMatrix},
1818 {"boundary-condition-factory-rejects-unsupported-handler", TestBoundaryConditionFactoryRejectsUnsupportedHandler},
1819 {"deterministic-face-grid-location-matrix", TestGetDeterministicFaceGridLocationFaceMatrix},
1820 {"random-inlet-face-location-matrix", TestGetRandomCellAndLogicalCoordsOnInletFaceMatrix},
1821 {"wall-no-slip-handler-face-matrix", TestWallNoSlipHandlerFaceMatrix},
1822 {"dummy-cells-follow-each-fields-boundary-rule", TestDummyCellsFollowEachFieldsBoundaryRule},
1823 {"inlet-constant-velocity-handler-behavior", TestInletConstantVelocityHandlerBehavior},
1824 {"inlet-common-application-curvilinear-metric", TestInletCommonApplicationCurvilinearMetric},
1825 {"inlet-constant-velocity-handler-face-matrix", TestInletConstantVelocityHandlerFaceMatrix},
1826 {"inlet-parabolic-profile-handler-behavior", TestInletParabolicProfileHandlerBehavior},
1827 {"inlet-parabolic-profile-handler-face-matrix", TestInletParabolicProfileHandlerFaceMatrix},
1828 {"inlet-parabolic-profile-uniform-along-periodic-axis", TestInletParabolicProfileUniformAlongPeriodicAxis},
1829 {"inlet-profile-from-file-handler-behavior", TestInletProfileFromFileHandlerBehavior},
1830 {"outlet-conservation-handler-behavior", TestOutletConservationHandlerBehavior},
1831 {"outlet-conservation-handler-face-matrix", TestOutletConservationHandlerFaceMatrix},
1832 {"finalize-post-projection-cell-fields-non-periodic", TestFinalizePostProjectionCellFieldsNonPeriodic},
1833 {"enforce-rhs-bcs-matches-row-classification", TestEnforceRHSBoundaryConditionsMatchesRowClassification},
1834 {"apply-wall-function-matches-the-log-law", TestApplyWallFunctionMatchesTheLogLawItDispatches},
1835 {"apply-wall-function-skips-non-wall-faces", TestApplyWallFunctionSkipsNonWallFaces},
1836 {"wall-function-model-selector-chooses-the-law", TestWallFunctionModelSelectorChoosesTheLaw},
1837 {"wall-model-friction-velocity-responds-to-the-flow", TestWallModelFrictionVelocityRespondsToTheFlow},
1838 {"wall-friction-velocity-storage-matches-the-catalog", TestWallFrictionVelocityStorageMatchesTheCatalog},
1839 {"wall-model-diagnostics-record-the-corrected-cells", TestWallModelDiagnosticsRecordTheCorrectedCells},
1840 };
1841
1842 ierr = PetscInitialize(&argc, &argv, NULL, "PICurv boundary tests");
1843 if (ierr) {
1844 return (int)ierr;
1845 }
1846
1847 ierr = PicurvRunTests("unit-boundaries", cases, sizeof(cases) / sizeof(cases[0]));
1848 if (ierr) {
1849 PetscFinalize();
1850 return (int)ierr;
1851 }
1852
1853 ierr = PetscFinalize();
1854 return (int)ierr;
1855}
MomentumRowType
Classification of one staggered momentum row (location + component).
Definition Boundaries.h:252
@ MOM_ROW_PHYSICAL
Independent unknown governed by the momentum equation.
Definition Boundaries.h:253
PetscErrorCode GetRandomCellAndLogicalCoordsOnInletFace(UserCtx *user, const DMDALocalInfo *info, PetscInt xs_gnode_rank, PetscInt ys_gnode_rank, PetscInt zs_gnode_rank, PetscInt IM_nodes_global, PetscInt JM_nodes_global, PetscInt KM_nodes_global, PetscRandom *rand_logic_i_ptr, PetscRandom *rand_logic_j_ptr, PetscRandom *rand_logic_k_ptr, PetscInt *ci_metric_lnode_out, PetscInt *cj_metric_lnode_out, PetscInt *ck_metric_lnode_out, PetscReal *xi_metric_logic_out, PetscReal *eta_metric_logic_out, PetscReal *zta_metric_logic_out)
Assuming the current rank services the inlet face, this function selects a random cell (owned by this...
Definition Boundaries.c:400
PetscErrorCode ApplyWallFunction(UserCtx *user)
Applies wall function modeling to near-wall velocities for all wall-type boundaries.
MomentumRowType ClassifyMomentumRow(UserCtx *user, PetscInt i, PetscInt j, PetscInt k, PetscInt component, PetscInt *ri, PetscInt *rj, PetscInt *rk)
Single source of truth for "which staggered momentum rows are unknowns".
Definition Boundaries.c:597
PetscErrorCode EnforceRHSBoundaryConditions(UserCtx *user)
Zeroes every momentum RHS row that does not carry an independent unknown.
Definition Boundaries.c:686
PetscErrorCode UpdateDummyCells(UserCtx *user, FieldId field_id)
Fills the dummy cells on the non-periodic faces of one cell-centred field.
PetscErrorCode BoundaryCondition_Create(BCHandlerType handler_type, BoundaryCondition **new_bc_ptr)
(Private) Creates and configures a specific BoundaryCondition handler object.
Definition Boundaries.c:729
PetscErrorCode CanRankServiceInletFace(UserCtx *user, const DMDALocalInfo *info, PetscInt IM_nodes_global, PetscInt JM_nodes_global, PetscInt KM_nodes_global, PetscBool *can_service_inlet_out)
Determines if the current MPI rank owns any part of the globally defined inlet face,...
Definition Boundaries.c:11
PetscErrorCode FinalizePostProjectionCellFields(UserCtx *user)
Finalizes cell-centered fields after the projection step.
PetscErrorCode UpdateCornerNodes(UserCtx *user, FieldId field_id)
Fills the edge and corner dummy cells of one cell-centred field by averaging.
PetscErrorCode CanRankServiceFace(const DMDALocalInfo *info, PetscInt IM_nodes_global, PetscInt JM_nodes_global, PetscInt KM_nodes_global, BCFace face_id, PetscBool *can_service_out)
Determines if the current MPI rank owns any part of a specified global face.
Definition Boundaries.c:127
PetscErrorCode GetDeterministicFaceGridLocation(UserCtx *user, const DMDALocalInfo *info, PetscInt xs_gnode_rank, PetscInt ys_gnode_rank, PetscInt zs_gnode_rank, PetscInt IM_cells_global, PetscInt JM_cells_global, PetscInt KM_cells_global, PetscInt64 particle_global_id, PetscInt *ci_metric_lnode_out, PetscInt *cj_metric_lnode_out, PetscInt *ck_metric_lnode_out, PetscReal *xi_metric_logic_out, PetscReal *eta_metric_logic_out, PetscReal *zta_metric_logic_out, PetscBool *placement_successful_out)
Places particles in a deterministic grid/raster pattern on a specified domain face.
Definition Boundaries.c:213
PetscErrorCode FieldGetView(UserCtx *user, FieldId field_id, FieldView *view)
Resolve the existing DM and global/local vectors for one field.
FieldDMKind dm_kind
PetscErrorCode FieldGetDescriptor(FieldId field_id, const FieldDescriptor **descriptor)
Return immutable metadata for a valid field identifier.
@ FIELD_DM_DA
FieldId
Compile-time identity for a catalogued Eulerian field.
@ FIELD_ID_PSI
@ FIELD_ID_NVERT
@ FIELD_ID_UCAT
@ FIELD_ID_COORDINATES
@ FIELD_ID_U_TAU
@ FIELD_ID_P
Immutable metadata for one field identity.
Non-owning runtime objects resolved for one field and UserCtx.
void FreeBC_ParamList(BC_Param *head)
Frees an entire linked list of boundary-condition parameters.
Definition io.c:672
The "virtual table" struct for a boundary condition handler object.
Definition variables.h:377
PetscErrorCode(* PostStep)(BoundaryCondition *self, BCContext *ctx, PetscReal *local_inflow, PetscReal *local_outflow)
Definition variables.h:384
PetscErrorCode(* PreStep)(BoundaryCondition *self, BCContext *ctx, PetscReal *local_inflow, PetscReal *local_outflow)
Definition variables.h:382
PetscErrorCode(* Destroy)(BoundaryCondition *self)
Definition variables.h:386
PetscErrorCode(* Initialize)(BoundaryCondition *self, BCContext *ctx)
Definition variables.h:381
PetscErrorCode(* Apply)(BoundaryCondition *self, BCContext *ctx)
Definition variables.h:383
BCPriorityType priority
Definition variables.h:379
static PetscErrorCode TestOutletConservationHandlerFaceMatrix(void)
Tests outlet-conservation correction and flux accounting across all outlet faces.
static PetscErrorCode TestCanRankServiceFaceMatchesInletWhenDefined(void)
Tests that face-service detection matches a defined inlet face.
static PetscErrorCode TestInletCommonApplicationCurvilinearMetric(void)
Verifies curvilinear projection and Cartesian dummy reflection for the shared inlet path.
static PetscErrorCode ApplyInletAcrossFirstSteps(BoundaryCondition *bc, BCContext *ctx)
Reapplies a static inlet through the first three time-step contexts.
static PetscErrorCode TestInletParabolicProfileUniformAlongPeriodicAxis(void)
Tests that the parabolic inlet is uniform along a periodic cross-stream axis.
static PetscErrorCode TestBoundaryConditionFactoryAssignments(void)
Tests the boundary-condition factory assignments for representative handlers.
static PetscErrorCode AppendBCParam(BC_Param **head, const char *key, const char *value)
Appends one key/value pair to a linked list of boundary-condition parameters.
static PetscErrorCode TestApplyWallFunctionMatchesTheLogLawItDispatches(void)
Tests that the wall-function integration reproduces the model it dispatches to.
static PetscReal GetFaceInteriorPointCount(const UserCtx *user, BCFace face)
Computes the number of face-interior sample points for a given boundary face.
static PetscErrorCode TestInletProfileFromFileHandlerBehavior(void)
Tests prescribed-flow inlet handler loading and applying one Z-face profile.
static PetscErrorCode TestDummyCellsFollowEachFieldsBoundaryRule(void)
Checks each cell-centred field's dummy-cell rule, faces then edges.
int main(int argc, char **argv)
Runs the unit-boundaries PETSc test binary.
static PetscErrorCode TestInletParabolicProfileHandlerBehavior(void)
Tests parabolic inlet handler shape on a tiny Z-face.
static PetscErrorCode TestWallModelDiagnosticsRecordTheCorrectedCells(void)
Tests that the wall pass records the statistics its diagnostics report.
static PetscReal GetFaceOrientationSign(BCFace face)
Returns the sign convention used for face-normal flux expectations.
static PetscErrorCode TestGetDeterministicFaceGridLocationFaceMatrix(void)
Tests deterministic inlet-face grid-location helpers across all faces.
static PetscErrorCode TestInletConstantVelocityHandlerBehavior(void)
Tests constant inlet handler initialization and face writes on a Z inlet.
static PetscErrorCode SeedWallFrictionVelocityStorage(UserCtx *user)
Allocates the wall-model storage the way production allocates it.
static PetscErrorCode TestBoundaryConditionFactoryRejectsUnsupportedHandler(void)
Tests that unsupported handler types are rejected by the factory.
static PetscErrorCode TestGetRandomCellAndLogicalCoordsOnInletFaceMatrix(void)
Tests random inlet-face cell selection across all supported faces.
static PetscErrorCode TestWallFunctionModelSelectorChoosesTheLaw(void)
Tests that the model selector actually changes which law is applied.
static PetscErrorCode TestBoundaryConditionFactoryImplementedHandlerMatrix(void)
Tests the implemented-handler matrix exposed by the factory.
static PetscErrorCode WritePicSliceForTests(const char *path, PetscInt n1, PetscInt n2, PetscReal base)
Writes a tiny canonical PICSLICE profile for boundary handler tests.
static PetscErrorCode TestCanRankServiceInletFaceRequiresDefinition(void)
Tests that inlet-face service requires an inlet face to be defined.
static PetscErrorCode TestInletParabolicProfileHandlerFaceMatrix(void)
Tests parabolic-inlet centerline, wall, and flux behavior across all faces.
static PetscErrorCode TestWallNoSlipHandlerFaceMatrix(void)
Tests wall no-slip application across the full face matrix.
static PetscErrorCode TestInletConstantVelocityHandlerFaceMatrix(void)
Tests constant-inlet initialization, flux accounting, and face writes across all faces.
static PetscErrorCode SeedWallFunctionFixture(UserCtx *user, PetscReal tangential_speed)
Seeds the fields ApplyWallFunction() reads on a single wall face.
static PetscErrorCode TestFinalizePostProjectionCellFieldsNonPeriodic(void)
Tests non-periodic cell-field finalization without modifying Ucont.
static PetscErrorCode TestWallModelFrictionVelocityRespondsToTheFlow(void)
Tests that every wall law's friction velocity responds to the flow it is given.
static const char * GetInletParamKey(BCFace face)
Maps an inlet face to the matching configuration key used by the handler parser.
static PetscErrorCode DestroyBoundaryHandler(BoundaryCondition **bc_ptr)
Destroys one boundary-condition handler allocated by a boundary test.
static PetscErrorCode TestApplyWallFunctionSkipsNonWallFaces(void)
Tests that a face which is not a wall is left alone by the wall function.
static PetscErrorCode TestOutletConservationHandlerBehavior(void)
Tests outlet conservation handler correction and post-step flux accounting.
static void ResetBoundaryFaceConfig(BoundaryFaceConfig *cfg)
Resets one boundary-face configuration entry to a neutral test-local baseline.
static PetscReal GetFaceNormalComponent(Cmpnts value, BCFace face)
Extracts the velocity component aligned with the supplied boundary-face normal.
static PetscInt InteriorSampleIndex(PetscInt nodes)
Chooses a stable interior node index for face-sample assertions on tiny test grids.
static PetscErrorCode TestEnforceRHSBoundaryConditionsMatchesRowClassification(void)
EnforceRHSBoundaryConditions() must zero a row exactly when it is not an unknown.
static PetscErrorCode GetRepresentativeFaceSlots(const UserCtx *user, BCFace face, PetscInt *ucont_k, PetscInt *ucont_j, PetscInt *ucont_i, PetscInt *ubcs_k, PetscInt *ubcs_j, PetscInt *ubcs_i)
Selects representative Ucont and Ubcs slots for face-matrix assertions.
static PetscErrorCode TestWallFrictionVelocityStorageMatchesTheCatalog(void)
Tests that the friction-velocity storage matches what the field catalog declares.
static PetscErrorCode GetParabolicSampleSlots(const UserCtx *user, BCFace face, PetscInt *center_k, PetscInt *center_j, PetscInt *center_i, PetscInt *off_k, PetscInt *off_j, PetscInt *off_i, PetscInt *wall_k, PetscInt *wall_j, PetscInt *wall_i, PetscInt *ubcs_center_k, PetscInt *ubcs_center_j, PetscInt *ubcs_center_i)
Selects center, off-center, and wall-adjacent slots for parabolic-face checks.
PetscErrorCode PicurvCreateMinimalContexts(SimCtx **simCtx_out, UserCtx **user_out, PetscInt mx, PetscInt my, PetscInt mz)
Builds minimal SimCtx and UserCtx fixtures for C unit tests.
PetscErrorCode PicurvAssertRealNear(PetscReal expected, PetscReal actual, PetscReal tol, const char *context)
Asserts that two real values agree within tolerance.
PetscErrorCode PicurvDestroyMinimalContexts(SimCtx **simCtx_ptr, UserCtx **user_ptr)
Destroys minimal SimCtx/UserCtx fixtures and all owned PETSc objects.
PetscErrorCode PicurvCreateMinimalContextsWithPeriodicity(SimCtx **simCtx_out, UserCtx **user_out, PetscInt mx, PetscInt my, PetscInt mz, PetscBool x_periodic, PetscBool y_periodic, PetscBool z_periodic)
Builds minimal SimCtx and UserCtx fixtures for C unit tests with configurable periodicity.
PetscErrorCode PicurvRunTests(const char *suite_name, const PicurvTestCase *cases, size_t case_count)
Runs a named C test suite and prints pass/fail progress markers.
PetscErrorCode 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 PicurvPopulateIdentityMetrics(UserCtx *user)
Populates identity metric vectors on the minimal grid fixture.
PetscErrorCode PicurvAssertBool(PetscBool value, const char *context)
Asserts that one boolean condition is true.
Shared declarations for the PICurv C test fixture and assertion layer.
Named test case descriptor consumed by PicurvRunTests.
Vec lFriction_Velocity
Definition variables.h:1106
const PetscReal * global_outflow_sum
Definition variables.h:373
BCType
Defines the general mathematical/physical Category of a boundary.
Definition variables.h:309
@ INLET
Definition variables.h:316
@ INTERFACE
Definition variables.h:311
@ OUTLET
Definition variables.h:315
@ PERIODIC
Definition variables.h:318
@ WALL
Definition variables.h:312
PetscBool inletFaceDefined
Definition variables.h:1100
Vec lNu_Wall
Definition variables.h:1110
BoundaryFaceConfig boundary_faces[6]
Definition variables.h:1099
BCFace identifiedInletBCFace
Definition variables.h:1101
SimCtx * simCtx
Back-pointer to the master simulation context.
Definition variables.h:1077
struct BC_Param_s * next
Definition variables.h:363
PetscInt KM
Definition variables.h:1088
BCHandlerType
Defines the specific computational "strategy" for a boundary handler.
Definition variables.h:329
@ BC_HANDLER_INLET_PARABOLIC
Definition variables.h:335
@ BC_HANDLER_INLET_CONSTANT_VELOCITY
Definition variables.h:334
@ BC_HANDLER_INLET_PROFILE_FROM_FILE
Definition variables.h:336
@ BC_HANDLER_WALL_NOSLIP
Definition variables.h:331
@ BC_HANDLER_OUTLET_CONSERVATION
Definition variables.h:338
@ BC_HANDLER_OUTLET_PRESSURE
Definition variables.h:339
@ BC_HANDLER_UNDEFINED
Definition variables.h:330
PetscReal ren
Definition variables.h:906
BCHandlerType handler_type
Definition variables.h:393
Vec Nu_Wall
Definition variables.h:1110
const PetscReal * global_farfield_outflow_sum
Definition variables.h:372
@ WALL_FUNCTION_CABOT
Definition variables.h:569
@ WALL_FUNCTION_LOG_LAW
Definition variables.h:567
@ WALL_FUNCTION_WERNER
Definition variables.h:568
PetscInt np
Definition variables.h:990
BCFace face_id
Definition variables.h:369
Vec Ucont
Definition variables.h:1113
Vec Ubcs
Physical Cartesian velocity at boundary faces. Full 3D array but only boundary-face entries are meani...
Definition variables.h:149
PetscScalar x
Definition variables.h:122
PetscReal wall_distance_sum
Sum of the first-cell wall distance.
Definition variables.h:677
UserCtx * user
Definition variables.h:368
PetscReal friction_velocity_min
Smallest u_tau this rank corrected.
Definition variables.h:675
const PetscReal * global_inflow_sum
Definition variables.h:370
PetscReal friction_velocity_max
Largest u_tau this rank corrected.
Definition variables.h:676
BC_Param * params
Definition variables.h:394
PetscReal y_plus_sum
Sum of u_tau * y / nu over corrected cells.
Definition variables.h:678
PetscReal wall_roughness_height
Definition variables.h:947
PetscReal friction_velocity_sum
Sum of u_tau over corrected cells.
Definition variables.h:673
PetscScalar z
Definition variables.h:122
PetscInt JM
Definition variables.h:1088
PetscInt wallfunction
Enable wall functions on WALL faces.
Definition variables.h:985
const PetscReal * global_farfield_inflow_sum
Definition variables.h:371
PetscInt cells
Number of cells this rank corrected.
Definition variables.h:681
Vec Friction_Velocity
Definition variables.h:1106
PetscInt i_periodic
Definition variables.h:953
PetscInt step
Definition variables.h:867
PetscReal AreaOutSum
Definition variables.h:979
DMDALocalInfo info
Definition variables.h:1086
BCPriorityType
Definition variables.h:347
@ BC_PRIORITY_OUTLET
Definition variables.h:352
@ BC_PRIORITY_WALL
Definition variables.h:351
@ BC_PRIORITY_INLET
Definition variables.h:349
Vec lUcat
Definition variables.h:1113
PetscScalar y
Definition variables.h:122
PetscInt IM
Definition variables.h:1088
BCType mathematical_type
Definition variables.h:392
PetscReal ti
Definition variables.h:868
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
WallModelDiagnosticsState wall_diagnostics
Near-wall statistics from the last wall-model pass.
Definition variables.h:1158
Provides execution context for a boundary condition handler.
Definition variables.h:367
A node in a linked list for storing key-value parameters from the bcs.dat file.
Definition variables.h:360
Holds the complete configuration for one of the six boundary faces.
Definition variables.h:390
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
A generic C-style linked list node for integers.
Definition variables.h:463
double u_hydset_roughness(double kinematic_viscosity, double wall_distance, double friction_velocity, double roughness_height)
Computes velocity from log-law for rough walls.