PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
test_momentum_newton_krylov.c
Go to the documentation of this file.
1/**
2 * @file test_momentum_newton_krylov.c
3 * @brief Focused tests for the version-one matrix-free momentum solver.
4 *
5 * The implementation is included intentionally: its callback helpers remain
6 * private in production while this translation unit can verify them directly.
7 */
8
9#include "test_support.h"
10#include "initialcondition.h"
11
12/* Rename only the included public entry points. Private callback tests use this
13 * copy, while lifecycle tests below call the separately linked production object. */
14#define MomentumSolver_NewtonKrylov MomentumSolver_NewtonKrylov_PrivateCopy
15#define MomentumSolver_NewtonKrylov_Destroy MomentumSolver_NewtonKrylov_Destroy_PrivateCopy
16#include "../../src/momentum_newton_krylov.c"
17#undef MomentumSolver_NewtonKrylov
18#undef MomentumSolver_NewtonKrylov_Destroy
19
20static const char *geometric_periodic_bcs =
21 "-Xi PERIODIC geometric\n"
22 "+Xi PERIODIC geometric\n"
23 "-Eta WALL noslip\n"
24 "+Eta WALL noslip\n"
25 "-Zeta INLET prescribed_velocity source_id=0\n"
26 "+Zeta OUTLET conservation\n";
27
28/* Two free-slip walls facing two no-slip ones: the free-slip boundary value is read
29 * from the trial velocity, so the residual must still be a function of X alone. */
30static const char *free_slip_wall_bcs =
31 "-Xi WALL free_slip\n"
32 "+Xi WALL free_slip\n"
33 "-Eta WALL noslip\n"
34 "+Eta WALL noslip\n"
35 "-Zeta WALL noslip\n"
36 "+Zeta WALL noslip\n";
37
38static const char *fixed_wall_bcs =
39 "-Xi WALL noslip\n"
40 "+Xi WALL noslip\n"
41 "-Eta WALL noslip\n"
42 "+Eta WALL noslip\n"
43 "-Zeta WALL noslip\n"
44 "+Zeta WALL noslip\n";
45
46static const char *parabolic_bcs =
47 "-Xi WALL noslip\n"
48 "+Xi WALL noslip\n"
49 "-Eta WALL noslip\n"
50 "+Eta WALL noslip\n"
51 "-Zeta INLET prescribed_velocity source_id=1\n"
52 "+Zeta OUTLET conservation\n";
53
54static const char *periodic_x_bcs =
55 "-Xi PERIODIC geometric\n+Xi PERIODIC geometric\n"
56 "-Eta WALL noslip\n+Eta WALL noslip\n-Zeta WALL noslip\n+Zeta WALL noslip\n";
57
58static const char *periodic_y_bcs =
59 "-Xi WALL noslip\n+Xi WALL noslip\n"
60 "-Eta PERIODIC geometric\n+Eta PERIODIC geometric\n-Zeta WALL noslip\n+Zeta WALL noslip\n";
61
62static const char *periodic_z_bcs =
63 "-Xi WALL noslip\n+Xi WALL noslip\n-Eta WALL noslip\n+Eta WALL noslip\n"
64 "-Zeta PERIODIC geometric\n+Zeta PERIODIC geometric\n";
65
66static const char *periodic_xy_bcs =
67 "-Xi PERIODIC geometric\n+Xi PERIODIC geometric\n"
68 "-Eta PERIODIC geometric\n+Eta PERIODIC geometric\n"
69 "-Zeta WALL noslip\n+Zeta WALL noslip\n";
70
71static const char *periodic_xyz_bcs =
72 "-Xi PERIODIC geometric\n+Xi PERIODIC geometric\n"
73 "-Eta PERIODIC geometric\n+Eta PERIODIC geometric\n"
74 "-Zeta PERIODIC geometric\n+Zeta PERIODIC geometric\n";
75
76/** @brief Checks a structured log's row count and required text after a collective solve. */
77static PetscErrorCode AssertNewtonLog(const char *path, PetscInt expected_rows,
78 const char *needle_a, const char *needle_b)
79{
80 FILE *file = NULL;
81 char line[4096];
82 PetscInt rows = 0;
83 PetscBool found_a = needle_a ? PETSC_FALSE : PETSC_TRUE;
84 PetscBool found_b = needle_b ? PETSC_FALSE : PETSC_TRUE;
85
86 PetscFunctionBeginUser;
87 PetscCallMPI(MPI_Barrier(PETSC_COMM_WORLD));
88 file = fopen(path, "r");
89 PetscCheck(file != NULL, PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN,
90 "Expected Newton log does not exist: %s", path);
91 while (fgets(line, sizeof(line), file)) {
92 if (line[0] >= '0' && line[0] <= '9') rows++; /* CSV data rows; the header and comments are not */
93 if (needle_a && strstr(line, needle_a)) found_a = PETSC_TRUE;
94 if (needle_b && strstr(line, needle_b)) found_b = PETSC_TRUE;
95 }
96 fclose(file);
97 if (expected_rows >= 0) {
98 PetscCall(PicurvAssertIntEqual(expected_rows, rows,
99 "Newton log must contain one nonduplicated row per solve"));
100 } else {
101 PetscCall(PicurvAssertBool((PetscBool)(rows >= -expected_rows),
102 "enabled Newton history must contain the expected iteration rows"));
103 }
104 if (!found_a || !found_b) {
105 PetscCall(PetscPrintf(PETSC_COMM_SELF, "%s lacks '%s'\n", path, !found_a ? needle_a : needle_b));
106 }
107 PetscCall(PicurvAssertBool(found_a, "Newton log is missing required structured content"));
108 PetscCall(PicurvAssertBool(found_b, "Newton log is missing required structured content"));
109 PetscFunctionReturn(PETSC_SUCCESS);
110}
111
112/**
113 * @brief Builds and initializes a small runtime context for Newton tests.
114 * @param bcs Optional boundary configuration text.
115 * @param simCtx Returned simulation context.
116 * @param user Returned finest-level block context.
117 * @param tmpdir Returned temporary directory.
118 * @param tmpdir_len Capacity of tmpdir.
119 * @return PetscErrorCode 0 on success.
120 */
121static PetscErrorCode BuildNewtonFixture(const char *bcs, SimCtx **simCtx, UserCtx **user,
122 char *tmpdir, size_t tmpdir_len)
123{
124 PetscFunctionBeginUser;
125 PetscCall(PicurvBuildTinyRuntimeContext(bcs, PETSC_FALSE, simCtx, user, tmpdir, tmpdir_len));
126 PetscCall(InitializeEulerianState(*simCtx));
127 (*simCtx)->mom_solver_type = MOMENTUM_SOLVER_NEWTON_KRYLOV;
128 PetscCall(PicurvAssertBool((PetscBool)((*user)->Rhs == NULL),
129 "Newton fixture must enter with no persistent Rhs workspace"));
130 PetscFunctionReturn(PETSC_SUCCESS);
131}
132
133/**
134 * @brief Destroys a Newton test fixture and its temporary files.
135 * @param simCtx Fixture simulation context.
136 * @param tmpdir Fixture temporary directory.
137 * @return PetscErrorCode 0 on success.
138 */
139static PetscErrorCode DestroyNewtonFixture(SimCtx **simCtx, char *tmpdir)
140{
141 PetscFunctionBeginUser;
142 PetscCall(PicurvDestroyRuntimeContext(simCtx));
143 PetscCall(PicurvRemoveTempDir(tmpdir));
144 PetscFunctionReturn(PETSC_SUCCESS);
145}
146
147/**
148 * @brief Writes one static 5x5 PICSLICE profile used by the full runtime fixture.
149 * @param path Output profile path.
150 * @return PetscErrorCode 0 on success.
151 */
152static PetscErrorCode WriteNewtonPicSlice(const char *path)
153{
154 FILE *fd = NULL;
155
156 PetscFunctionBeginUser;
157 fd = fopen(path, "w");
158 PetscCheck(fd != NULL, PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN,
159 "Could not create Newton test PICSLICE %s.", path);
160 PetscCheck(fprintf(fd, "PICSLICE\n1\n5 5\n") >= 0,
161 PETSC_COMM_SELF, PETSC_ERR_FILE_WRITE, "Could not write PICSLICE header.");
162 for (PetscInt row = 0; row < 25; ++row) {
163 PetscCheck(fprintf(fd, "%.16e\n", 1.0 + 0.01 * (double)row) >= 0,
164 PETSC_COMM_SELF, PETSC_ERR_FILE_WRITE, "Could not write PICSLICE value.");
165 }
166 fclose(fd);
167 PetscFunctionReturn(PETSC_SUCCESS);
168}
169
170/**
171 * @brief Checks callback repeatability, diagnostic-state independence, and X integrity.
172 * @param bcs Boundary configuration text, or NULL for the standard inlet/outlet fixture.
173 * @param label Configuration label used in assertion diagnostics.
174 * @return PetscErrorCode 0 on success.
175 */
176static PetscErrorCode CheckResidualRepeatabilityForBC(const char *bcs, const char *label)
177{
178 SimCtx *simCtx = NULL;
179 UserCtx *user = NULL;
180 char tmpdir[PETSC_MAX_PATH_LEN] = "";
181 Vec x = NULL, x_copy = NULL, f1 = NULL, f2 = NULL, delta = NULL;
182 PetscReal norm = 0.0;
184
185 PetscFunctionBeginUser;
186 PetscCall(BuildNewtonFixture(bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
187 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
188 PetscCall(VecDuplicate(user->Ucont, &x));
189 PetscCall(VecDuplicate(user->Ucont, &x_copy));
190 PetscCall(VecDuplicate(user->Ucont, &f1));
191 PetscCall(VecDuplicate(user->Ucont, &f2));
192 PetscCall(VecDuplicate(user->Ucont, &delta));
193 PetscCall(VecCopy(user->Ucont, x));
194 PetscCall(VecShift(x, 0.125));
195 PetscCall(VecCopy(x, x_copy));
196 ctx.user = user;
197
198 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f1, &ctx));
199 /* Poison every piece of hidden state a non-deterministic residual could lean
200 * on: the boundary flux/area diagnostics AND the persistent Cartesian fields
201 * (Ucat/lUcat). The conservation-outlet handler reads lUcat during its first
202 * boundary sweep, so a callback that does not reconstruct the Cartesian state
203 * from X before applying boundary conditions would produce a different F here.
204 * This assertion therefore fails if the deterministic pre-boundary seed in
205 * MomentumNewtonKrylov_FormResidual() is ever removed. */
206 simCtx->FluxInSum = 1234.0;
207 simCtx->FluxOutSum = -4321.0;
208 simCtx->FarFluxInSum = 77.0;
209 simCtx->FarFluxOutSum = -88.0;
210 PetscCall(VecSet(user->Ucat, 7.0));
211 PetscCall(VecSet(user->lUcat, 7.0));
212 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f2, &ctx));
213 PetscCall(VecWAXPY(delta, -1.0, f1, f2));
214 PetscCall(VecNorm(delta, NORM_INFINITY, &norm));
215 PetscCheck(norm <= 1.0e-13, PETSC_COMM_WORLD, PETSC_ERR_PLIB,
216 "%s residual changed between identical evaluations (inf norm=%g).", label, (double)norm);
217 PetscCall(VecNorm(delta, NORM_2, &norm));
218 PetscCheck(norm <= 1.0e-12, PETSC_COMM_WORLD, PETSC_ERR_PLIB,
219 "%s residual changed between identical evaluations (L2 norm=%g).", label, (double)norm);
220 PetscCall(VecWAXPY(delta, -1.0, x_copy, x));
221 PetscCall(VecNorm(delta, NORM_INFINITY, &norm));
222 PetscCheck(norm == 0.0, PETSC_COMM_WORLD, PETSC_ERR_PLIB,
223 "%s residual callback modified X (norm=%g).", label, (double)norm);
224
225 PetscCall(VecDestroy(&delta));
226 PetscCall(VecDestroy(&f2));
227 PetscCall(VecDestroy(&f1));
228 PetscCall(VecDestroy(&x_copy));
229 PetscCall(VecDestroy(&x));
230 PetscCall(VecDestroy(&user->Rhs));
231 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
232 PetscFunctionReturn(PETSC_SUCCESS);
233}
234
235/** @brief Returns whether this rank owns one global DMDA grid point. */
236static PetscBool OwnsStoredPoint(UserCtx *user, PetscInt i, PetscInt j, PetscInt k)
237{
238 return (PetscBool)(i >= user->info.xs && i < user->info.xs + user->info.xm &&
239 j >= user->info.ys && j < user->info.ys + user->info.ym &&
240 k >= user->info.zs && k < user->info.zs + user->info.zm);
241}
242
243/**
244 * @brief Adds a scalar perturbation to one stored staggered component.
245 * @param user Block context defining vector ownership.
246 * @param vec Vector to modify.
247 * @param i Global i index.
248 * @param j Global j index.
249 * @param k Global k index.
250 * @param component Component 0=x, 1=y, 2=z.
251 * @param delta Increment to apply.
252 * @return PetscErrorCode 0 on success.
253 */
254static PetscErrorCode PerturbStoredValue(UserCtx *user, Vec vec, PetscInt i, PetscInt j,
255 PetscInt k, PetscInt component, PetscScalar delta)
256{
257 Cmpnts ***a = NULL;
258
259 PetscFunctionBeginUser;
260 if (OwnsStoredPoint(user, i, j, k)) {
261 PetscCall(DMDAVecGetArray(user->fda, vec, &a));
262 if (component == 0) a[k][j][i].x += delta;
263 else if (component == 1) a[k][j][i].y += delta;
264 else a[k][j][i].z += delta;
265 PetscCall(DMDAVecRestoreArray(user->fda, vec, &a));
266 }
267 PetscFunctionReturn(PETSC_SUCCESS);
268}
269
270/**
271 * @brief Reads one globally indexed stored component on any MPI decomposition.
272 * @param user Block context defining vector ownership.
273 * @param vec Vector to inspect.
274 * @param i Global i index.
275 * @param j Global j index.
276 * @param k Global k index.
277 * @param component Component 0=x, 1=y, 2=z.
278 * @param value Returned globally reduced scalar.
279 * @return PetscErrorCode 0 on success.
280 */
281static PetscErrorCode GetStoredValue(UserCtx *user, Vec vec, PetscInt i, PetscInt j,
282 PetscInt k, PetscInt component, PetscScalar *value)
283{
284 Cmpnts ***a = NULL;
285 PetscScalar local = 0.0;
286
287 PetscFunctionBeginUser;
288 if (OwnsStoredPoint(user, i, j, k)) {
289 PetscCall(DMDAVecGetArrayRead(user->fda, vec, &a));
290 local = component == 0 ? a[k][j][i].x : (component == 1 ? a[k][j][i].y : a[k][j][i].z);
291 PetscCall(DMDAVecRestoreArrayRead(user->fda, vec, &a));
292 }
293 PetscCallMPI(MPI_Allreduce(&local, value, 1, MPIU_SCALAR, MPIU_SUM, PETSC_COMM_WORLD));
294 PetscFunctionReturn(PETSC_SUCCESS);
295}
296
297/**
298 * @brief Finite-differences one callback row with respect to one stored unknown.
299 * @param user Active block context with allocated Rhs.
300 * @param x Base trial vector.
301 * @param row_i Residual-row i index.
302 * @param row_j Residual-row j index.
303 * @param row_k Residual-row k index.
304 * @param row_component Residual-row component.
305 * @param col_i Perturbed unknown i index.
306 * @param col_j Perturbed unknown j index.
307 * @param col_k Perturbed unknown k index.
308 * @param col_component Perturbed unknown component.
309 * @param derivative Returned finite-difference derivative of the row w.r.t. the unknown.
310 * @return PetscErrorCode 0 on success.
311 */
312static PetscErrorCode MeasureStoredDerivative(UserCtx *user, Vec x,
313 PetscInt row_i, PetscInt row_j, PetscInt row_k,
314 PetscInt row_component, PetscInt col_i, PetscInt col_j,
315 PetscInt col_k, PetscInt col_component,
316 PetscReal *derivative)
317{
318 const PetscReal epsilon = 1.0e-6;
319 Vec f0 = NULL, fp = NULL, xp = NULL;
320 PetscScalar base_value = 0.0, perturbed_value = 0.0;
321 MomentumNewtonKrylovContext ctx = {user};
322
323 PetscFunctionBeginUser;
324 PetscCall(VecDuplicate(x, &f0));
325 PetscCall(VecDuplicate(x, &fp));
326 PetscCall(VecDuplicate(x, &xp));
327 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f0, &ctx));
328 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f0, &ctx));
329 PetscCall(VecCopy(x, xp));
330 PetscCall(PerturbStoredValue(user, xp, col_i, col_j, col_k, col_component, epsilon));
331 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, xp, fp, &ctx));
332 PetscCall(GetStoredValue(user, f0, row_i, row_j, row_k, row_component, &base_value));
333 PetscCall(GetStoredValue(user, fp, row_i, row_j, row_k, row_component, &perturbed_value));
334 *derivative = PetscRealPart((perturbed_value - base_value) / epsilon);
335 PetscCall(VecDestroy(&xp));
336 PetscCall(VecDestroy(&fp));
337 PetscCall(VecDestroy(&f0));
338 PetscFunctionReturn(PETSC_SUCCESS);
339}
340
341/**
342 * @brief Asserts one finite-differenced callback row derivative equals an expected value.
343 * @param user Active block context with allocated Rhs.
344 * @param x Base trial vector.
345 * @param row_i Residual-row i index.
346 * @param row_j Residual-row j index.
347 * @param row_k Residual-row k index.
348 * @param row_component Residual-row component.
349 * @param col_i Perturbed unknown i index.
350 * @param col_j Perturbed unknown j index.
351 * @param col_k Perturbed unknown k index.
352 * @param col_component Perturbed unknown component.
353 * @param expected Expected derivative.
354 * @param tolerance Absolute derivative tolerance.
355 * @param label Assertion label.
356 * @return PetscErrorCode 0 on success.
357 */
358static PetscErrorCode CheckStoredDerivative(UserCtx *user, Vec x,
359 PetscInt row_i, PetscInt row_j, PetscInt row_k,
360 PetscInt row_component, PetscInt col_i, PetscInt col_j,
361 PetscInt col_k, PetscInt col_component,
362 PetscReal expected, PetscReal tolerance, const char *label)
363{
364 PetscReal derivative = 0.0;
365
366 PetscFunctionBeginUser;
367 PetscCall(MeasureStoredDerivative(user, x, row_i, row_j, row_k, row_component,
368 col_i, col_j, col_k, col_component, &derivative));
369 PetscCall(PicurvAssertRealNear(expected, derivative, tolerance, label));
370 PetscFunctionReturn(PETSC_SUCCESS);
371}
372
373/** @brief Verifies repeatable callback output and read-only trial input. */
375{
376 char profile_dir[PETSC_MAX_PATH_LEN] = "";
377 char profile_path[PETSC_MAX_PATH_LEN];
378 char file_bcs[2 * PETSC_MAX_PATH_LEN];
379
380 PetscFunctionBeginUser;
381 PetscCall(CheckResidualRepeatabilityForBC(fixed_wall_bcs, "fixed walls"));
382 PetscCall(CheckResidualRepeatabilityForBC(free_slip_wall_bcs, "free-slip walls"));
383 PetscCall(CheckResidualRepeatabilityForBC(NULL, "constant inlet/conservation outlet"));
384 PetscCall(CheckResidualRepeatabilityForBC(parabolic_bcs, "parabolic inlet/conservation outlet"));
385 PetscCall(CheckResidualRepeatabilityForBC(periodic_x_bcs, "x periodic"));
386 PetscCall(CheckResidualRepeatabilityForBC(periodic_y_bcs, "y periodic"));
387 PetscCall(CheckResidualRepeatabilityForBC(periodic_z_bcs, "z periodic"));
388 PetscCall(CheckResidualRepeatabilityForBC(periodic_xy_bcs, "mixed x-y periodic"));
389
390 PetscCall(PicurvMakeTempDir(profile_dir, sizeof(profile_dir)));
391 PetscCall(PetscSNPrintf(profile_path, sizeof(profile_path), "%s/inlet.picslice", profile_dir));
392 PetscCall(WriteNewtonPicSlice(profile_path));
393 PetscCall(PetscSNPrintf(file_bcs, sizeof(file_bcs),
394 "-Xi WALL noslip\n+Xi WALL noslip\n-Eta WALL noslip\n+Eta WALL noslip\n"
395 "-Zeta INLET prescribed_velocity source_id=2 source_file=%s\n+Zeta OUTLET conservation\n", profile_path));
396 PetscCall(CheckResidualRepeatabilityForBC(file_bcs, "file inlet/conservation outlet"));
397 PetscCall(PicurvRemoveTempDir(profile_dir));
398 PetscFunctionReturn(PETSC_SUCCESS);
399}
400
401/** @brief Verifies fixed, periodic-duplicate, and interior residual rows. */
402static PetscErrorCode TestConstraintRows(void)
403{
404 SimCtx *simCtx = NULL;
405 UserCtx *user = NULL;
406 char tmpdir[PETSC_MAX_PATH_LEN] = "";
407 Vec x = NULL, f = NULL;
408 Cmpnts ***xa = NULL, ***fa = NULL, ***conditioned = NULL, ***rhs = NULL;
410
411 PetscFunctionBeginUser;
412 PetscCall(BuildNewtonFixture(geometric_periodic_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
413 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
414 PetscCall(VecDuplicate(user->Ucont, &x));
415 PetscCall(VecDuplicate(user->Ucont, &f));
416 PetscCall(VecSet(x, 0.25));
417 PetscCall(DMDAVecGetArray(user->fda, x, &xa));
418 if (user->info.xs == 0 && 2 >= user->info.ys && 2 < user->info.ys + user->info.ym &&
419 2 >= user->info.zs && 2 < user->info.zs + user->info.zm) xa[2][2][0].x = 3.0;
420 if (user->info.xs + user->info.xm == user->info.mx &&
421 2 >= user->info.ys && 2 < user->info.ys + user->info.ym &&
422 2 >= user->info.zs && 2 < user->info.zs + user->info.zm) xa[2][2][user->info.mx - 2].x = 1.25;
423 PetscCall(DMDAVecRestoreArray(user->fda, x, &xa));
424 ctx.user = user;
425 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f, &ctx));
426
427 PetscCall(DMDAVecGetArrayRead(user->fda, x, &xa));
428 PetscCall(DMDAVecGetArrayRead(user->fda, f, &fa));
429 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &conditioned));
430 PetscCall(DMDAVecGetArrayRead(user->fda, user->Rhs, &rhs));
431 if (user->info.xs == 0 && 2 >= user->info.ys && 2 < user->info.ys + user->info.ym &&
432 2 >= user->info.zs && 2 < user->info.zs + user->info.zm) {
433 PetscCall(PicurvAssertRealNear(1.75, fa[2][2][0].x, 1.0e-12,
434 "periodic duplicate row must be Xdup-Xrep"));
435 }
436 if (user->info.ys == 0 && 2 >= user->info.xs && 2 < user->info.xs + user->info.xm &&
437 2 >= user->info.zs && 2 < user->info.zs + user->info.zm) {
438 PetscCall(PicurvAssertRealNear(xa[2][0][2].y - conditioned[2][0][2].y,
439 fa[2][0][2].y, 1.0e-12,
440 "fixed wall row must be X minus conditioned boundary value"));
441 }
442 if (2 >= user->info.xs && 2 < user->info.xs + user->info.xm &&
443 2 >= user->info.ys && 2 < user->info.ys + user->info.ym &&
444 2 >= user->info.zs && 2 < user->info.zs + user->info.zm) {
445 PetscCall(PicurvAssertRealNear(-rhs[2][2][2].z, fa[2][2][2].z, 1.0e-12,
446 "unconstrained interior row must retain the physical residual"));
447 }
448 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Rhs, &rhs));
449 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &conditioned));
450 PetscCall(DMDAVecRestoreArrayRead(user->fda, f, &fa));
451 PetscCall(DMDAVecRestoreArrayRead(user->fda, x, &xa));
452
453 PetscCall(VecDestroy(&f));
454 PetscCall(VecDestroy(&x));
455 PetscCall(VecDestroy(&user->Rhs));
456 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
457 PetscFunctionReturn(PETSC_SUCCESS);
458}
459
460/** @brief Proves unit derivatives for every nonperiodic stored-row category and face. */
461static PetscErrorCode TestFixedConstraintDerivativesAllFaces(void)
462{
463 SimCtx *simCtx = NULL;
464 UserCtx *user = NULL;
465 char tmpdir[PETSC_MAX_PATH_LEN] = "";
466 Vec x = NULL;
467 const PetscInt size[3] = {7, 7, 7};
468
469 PetscFunctionBeginUser;
470 PetscCall(BuildNewtonFixture(fixed_wall_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
471 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
472 PetscCall(VecDuplicate(user->Ucont, &x));
473 PetscCall(VecSet(x, 0.2));
474
475 for (PetscInt axis = 0; axis < 3; ++axis) {
476 PetscInt coord[3] = {2, 2, 2}, ri, rj, rk;
477 PetscInt tangent = (axis + 1) % 3;
478 MomentumRowType row;
479
480 coord[axis] = 0;
481 row = ClassifyMomentumRow(user, coord[0], coord[1], coord[2], axis, &ri, &rj, &rk);
483 "negative face normal row classification"));
484 PetscCall(CheckStoredDerivative(user, x, coord[0], coord[1], coord[2], axis,
485 coord[0], coord[1], coord[2], axis, 1.0, 1.0e-8,
486 "negative face normal fixed derivative"));
487
488 row = ClassifyMomentumRow(user, coord[0], coord[1], coord[2], tangent, &ri, &rj, &rk);
490 "negative face tangential row classification"));
491 PetscCall(CheckStoredDerivative(user, x, coord[0], coord[1], coord[2], tangent,
492 coord[0], coord[1], coord[2], tangent, 1.0, 1.0e-8,
493 "negative face tangential homogeneous derivative"));
494
495 coord[axis] = size[axis] - 2;
496 row = ClassifyMomentumRow(user, coord[0], coord[1], coord[2], axis, &ri, &rj, &rk);
498 "positive physical normal row classification"));
499 PetscCall(CheckStoredDerivative(user, x, coord[0], coord[1], coord[2], axis,
500 coord[0], coord[1], coord[2], axis, 1.0, 1.0e-8,
501 "positive physical normal fixed derivative"));
502 row = ClassifyMomentumRow(user, coord[0], coord[1], coord[2], tangent, &ri, &rj, &rk);
504 "positive physical tangential row classification"));
505
506 coord[axis] = size[axis] - 1;
507 for (PetscInt component = 0; component < 3; ++component) {
508 row = ClassifyMomentumRow(user, coord[0], coord[1], coord[2], component, &ri, &rj, &rk);
510 "positive dummy row classification"));
511 PetscCall(CheckStoredDerivative(user, x, coord[0], coord[1], coord[2], component,
512 coord[0], coord[1], coord[2], component, 1.0, 1.0e-8,
513 "positive dummy homogeneous derivative"));
514 }
515 }
516
517 PetscCall(VecDestroy(&x));
518 PetscCall(VecDestroy(&user->Rhs));
519 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
520 PetscFunctionReturn(PETSC_SUCCESS);
521}
522
523/** @brief Proves admitted inlet and outlet face-normal rows have unit self derivatives. */
524static PetscErrorCode TestInletOutletConstraintDerivatives(void)
525{
526 SimCtx *simCtx = NULL;
527 UserCtx *user = NULL;
528 char tmpdir[PETSC_MAX_PATH_LEN] = "";
529 Vec x = NULL;
530
531 PetscFunctionBeginUser;
532 PetscCall(BuildNewtonFixture(NULL, &simCtx, &user, tmpdir, sizeof(tmpdir)));
533 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
534 PetscCall(VecDuplicate(user->Ucont, &x));
535 PetscCall(VecCopy(user->Ucont, x));
536 PetscCall(VecShift(x, 0.1));
537 PetscCall(CheckStoredDerivative(user, x, 2, 2, 0, 2, 2, 2, 0, 2,
538 1.0, 1.0e-8, "constant inlet fixed derivative"));
539 /* The constant-velocity inlet imposes a value independent of X, so its
540 * conditioned row F = X - cv has an exact unit self derivative.
541 *
542 * The conservation outlet is different: cv is the corrected outlet flux,
543 * which the deterministic residual now reconstructs from the current X
544 * (Ucat is seeded from X before the first outlet pass). Perturbing the
545 * outlet-normal DOF therefore changes cv, so the self derivative is
546 * 1 - dcv/dX and is strictly less than one. A self derivative of exactly
547 * 1.0 here was an artifact of the pre-fix residual reading a stale
548 * Cartesian state, i.e. an outlet correction decoupled from X. Assert the
549 * derivative is (a) deterministic across independent evaluations -- the
550 * residual-purity property -- and (b) reflects real conservation coupling
551 * (0 < d < 1), rather than asserting a fixture-specific magic number. */
552 {
553 PetscReal d0 = 0.0, d1 = 0.0;
554 PetscCall(MeasureStoredDerivative(user, x, 2, 2, user->info.mz - 2, 2,
555 2, 2, user->info.mz - 2, 2, &d0));
556 PetscCall(MeasureStoredDerivative(user, x, 2, 2, user->info.mz - 2, 2,
557 2, 2, user->info.mz - 2, 2, &d1));
558 PetscCall(PicurvAssertRealNear(d0, d1, 1.0e-9,
559 "conservation outlet self derivative must be deterministic"));
560 PetscCheck(d0 > 1.0e-3 && d0 < 1.0 - 1.0e-3, PETSC_COMM_WORLD, PETSC_ERR_PLIB,
561 "conservation outlet self derivative must reflect X-coupling (0<d<1), got %g.",
562 (double)d0);
563 }
564 PetscCall(VecDestroy(&x));
565 PetscCall(VecDestroy(&user->Rhs));
566 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
567 PetscFunctionReturn(PETSC_SUCCESS);
568}
569
570/**
571 * @brief Checks one periodic configuration's endpoint derivatives on every component.
572 * @param bcs Boundary text selecting the periodic axis.
573 * @param axis Periodic axis index.
574 * @return PetscErrorCode 0 on success.
575 */
576static PetscErrorCode CheckSingleAxisPeriodicDerivatives(const char *bcs, PetscInt axis)
577{
578 SimCtx *simCtx = NULL;
579 UserCtx *user = NULL;
580 char tmpdir[PETSC_MAX_PATH_LEN] = "";
581 Vec x = NULL;
582 PetscInt size[3], dup[3] = {2, 2, 2}, rep[3] = {2, 2, 2}, unrelated[3] = {3, 3, 3};
583
584 PetscFunctionBeginUser;
585 PetscCall(BuildNewtonFixture(bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
586 size[0] = user->info.mx; size[1] = user->info.my; size[2] = user->info.mz;
587 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
588 PetscCall(VecDuplicate(user->Ucont, &x));
589 PetscCall(VecSet(x, 0.15));
590 for (PetscInt side = 0; side < 2; ++side) {
591 dup[axis] = side == 0 ? 0 : size[axis] - 1;
592 rep[axis] = side == 0 ? size[axis] - 2 : 1;
593 for (PetscInt component = 0; component < 3; ++component) {
594 PetscCall(CheckStoredDerivative(user, x, dup[0], dup[1], dup[2], component,
595 dup[0], dup[1], dup[2], component, 1.0, 1.0e-8,
596 "periodic duplicate self derivative"));
597 PetscCall(CheckStoredDerivative(user, x, dup[0], dup[1], dup[2], component,
598 rep[0], rep[1], rep[2], component, -1.0, 1.0e-8,
599 "periodic representative derivative"));
600 PetscCall(CheckStoredDerivative(user, x, dup[0], dup[1], dup[2], component,
601 unrelated[0], unrelated[1], unrelated[2], (component + 1) % 3, 0.0, 1.0e-8,
602 "periodic constraint unrelated derivative"));
603 }
604 }
605 PetscCall(VecDestroy(&x));
606 PetscCall(VecDestroy(&user->Rhs));
607 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
608 PetscFunctionReturn(PETSC_SUCCESS);
609}
610
611/** @brief Proves single-, double-, triple-, and mixed-boundary periodic equations. */
613{
614 SimCtx *simCtx = NULL;
615 UserCtx *user = NULL;
616 char tmpdir[PETSC_MAX_PATH_LEN] = "";
617 Vec x = NULL, f = NULL;
619 PetscScalar xdup, synced, residual;
620
621 PetscFunctionBeginUser;
625
626 PetscCall(BuildNewtonFixture(periodic_xy_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
627 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
628 PetscCall(VecDuplicate(user->Ucont, &x));
629 PetscCall(VecDuplicate(user->Ucont, &f));
630 PetscCall(VecSet(x, 0.0));
631 PetscCall(PerturbStoredValue(user, x, 0, 0, 2, 0, 3.0));
632 PetscCall(PerturbStoredValue(user, x, user->info.mx - 2, user->info.my - 2, 2, 0, 1.25));
633 PetscCall(VecCopy(x, user->Ucont));
634 { const FieldId fields[] = {FIELD_ID_UCONT}; PetscCall(SynchronizePeriodicStaggeredFields(user, 1, fields)); }
635 PetscCall(GetStoredValue(user, x, 0, 0, 2, 0, &xdup));
636 PetscCall(GetStoredValue(user, user->Ucont, 0, 0, 2, 0, &synced));
637 ctx.user = user;
638 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f, &ctx));
639 PetscCall(GetStoredValue(user, f, 0, 0, 2, 0, &residual));
640 PetscCall(PicurvAssertRealNear(PetscRealPart(xdup - synced), PetscRealPart(residual), 1.0e-12,
641 "doubly periodic edge must use production synchronized representative"));
642 PetscCall(CheckStoredDerivative(user, x, 0, 0, 2, 0, 0, 0, 2, 0, 1.0, 1.0e-8,
643 "doubly periodic edge self derivative"));
644 PetscCall(CheckStoredDerivative(user, x, 0, 0, 2, 0,
645 user->info.mx - 2, user->info.my - 2, 2, 0, -1.0, 1.0e-8,
646 "doubly periodic edge representative derivative"));
647 PetscCall(VecDestroy(&f)); PetscCall(VecDestroy(&x)); PetscCall(VecDestroy(&user->Rhs));
648 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
649
650 tmpdir[0] = '\0';
651 PetscCall(BuildNewtonFixture(periodic_xyz_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
652 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
653 PetscCall(VecDuplicate(user->Ucont, &x)); PetscCall(VecSet(x, 0.1));
654 PetscCall(CheckStoredDerivative(user, x, 0, 0, 0, 2, 0, 0, 0, 2, 1.0, 1.0e-8,
655 "fully periodic corner self derivative"));
656 PetscCall(CheckStoredDerivative(user, x, 0, 0, 0, 2,
657 user->info.mx - 2, user->info.my - 2, user->info.mz - 2, 2,
658 -1.0, 1.0e-8, "fully periodic corner representative derivative"));
659 PetscCall(VecDestroy(&x)); PetscCall(VecDestroy(&user->Rhs));
660 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
661
662 tmpdir[0] = '\0';
663 PetscCall(BuildNewtonFixture(periodic_x_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
664 PetscCall(VecDuplicate(user->Ucont, &user->Rhs)); PetscCall(VecDuplicate(user->Ucont, &x));
665 PetscCall(VecSet(x, 0.1));
666 PetscCall(CheckStoredDerivative(user, x, 0, 0, 2, 1, 0, 0, 2, 1, 1.0, 1.0e-8,
667 "periodic-wall intersection self derivative"));
668 PetscCall(CheckStoredDerivative(user, x, 0, 0, 2, 1,
669 user->info.mx - 2, 0, 2, 1, -1.0, 1.0e-8,
670 "periodic-wall intersection representative derivative"));
671 PetscCall(VecDestroy(&x)); PetscCall(VecDestroy(&user->Rhs));
672 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
673 PetscFunctionReturn(PETSC_SUCCESS);
674}
675
676/** @brief Compares PETSc's matrix-free action with direct differencing. */
677static PetscErrorCode TestMatrixFreeDerivative(void)
678{
679 SimCtx *simCtx = NULL;
680 UserCtx *user = NULL;
681 char tmpdir[PETSC_MAX_PATH_LEN] = "";
682 SNES snes = NULL;
683 Mat J = NULL;
684 Vec x = NULL, xp = NULL, f0 = NULL, fp = NULL, v = NULL, jv = NULL, fd = NULL;
685 PetscReal h = 0.0, error = 0.0, scale = 0.0;
687
688 PetscFunctionBeginUser;
689 PetscCall(BuildNewtonFixture(NULL, &simCtx, &user, tmpdir, sizeof(tmpdir)));
690 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
691 PetscCall(VecDuplicate(user->Ucont, &x));
692 PetscCall(VecDuplicate(user->Ucont, &xp));
693 PetscCall(VecDuplicate(user->Ucont, &f0));
694 PetscCall(VecDuplicate(user->Ucont, &fp));
695 PetscCall(VecDuplicate(user->Ucont, &v));
696 PetscCall(VecDuplicate(user->Ucont, &jv));
697 PetscCall(VecDuplicate(user->Ucont, &fd));
698 PetscCall(VecCopy(user->Ucont, x));
699 PetscCall(VecShift(x, 0.05));
700 PetscCall(VecSet(v, 0.5));
701 ctx.user = user;
702
703 PetscCall(SNESCreate(PETSC_COMM_WORLD, &snes));
704 PetscCall(SNESSetDM(snes, user->fda));
705 PetscCall(SNESSetFunction(snes, f0, MomentumNewtonKrylov_FormResidual, &ctx));
706 PetscCall(MatCreateSNESMF(snes, &J));
707 PetscCall(MomentumNewtonKrylov_FormResidual(snes, x, f0, &ctx));
708 PetscCall(MatMFFDSetBase(J, x, f0));
709 PetscCall(MatMult(J, v, jv));
710 PetscCall(MatMFFDGetH(J, &h));
711 PetscCall(VecWAXPY(xp, h, v, x));
712 PetscCall(MomentumNewtonKrylov_FormResidual(snes, xp, fp, &ctx));
713 PetscCall(VecWAXPY(fd, -1.0, f0, fp));
714 PetscCall(VecScale(fd, 1.0 / h));
715 PetscCall(VecAXPY(fd, -1.0, jv));
716 PetscCall(VecNorm(fd, NORM_2, &error));
717 PetscCall(VecNorm(jv, NORM_2, &scale));
718 PetscCall(PicurvAssertBool((PetscBool)(error <= 1.0e-9 * PetscMax(1.0, scale)),
719 "matrix-free Jv must match direct differencing"));
720
721 PetscCall(VecDestroy(&fd));
722 PetscCall(VecDestroy(&jv));
723 PetscCall(VecDestroy(&v));
724 PetscCall(VecDestroy(&fp));
725 PetscCall(VecDestroy(&f0));
726 PetscCall(VecDestroy(&xp));
727 PetscCall(VecDestroy(&x));
728 PetscCall(MatDestroy(&J));
729 PetscCall(SNESDestroy(&snes));
730 PetscCall(VecDestroy(&user->Rhs));
731 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
732 PetscFunctionReturn(PETSC_SUCCESS);
733}
734
735/**
736 * @brief Builds a compact all-wall operator fixture through real boundary handlers.
737 * @param simCtx Returned simulation context.
738 * @param user Returned block context.
739 * @param x_periodic Whether the x faces use geometric periodicity.
740 * @return PetscErrorCode 0 on success.
741 */
742static PetscErrorCode BuildMinimalWallOperatorFixture(SimCtx **simCtx, UserCtx **user,
743 PetscBool x_periodic)
744{
745 PetscFunctionBeginUser;
746 /* Request the production-width (3) DMDA stencil so the complete RHS is safe
747 across MPI partitions; boundary metadata below still selects physical walls. */
749 simCtx, user, 6, 6, 6, PETSC_TRUE, PETSC_TRUE, PETSC_TRUE));
750 (*simCtx)->i_periodic = x_periodic ? 1 : 0;
751 (*simCtx)->j_periodic = (*simCtx)->k_periodic = 0;
752 (*simCtx)->mom_solver_type = MOMENTUM_SOLVER_NEWTON_KRYLOV;
753 (*simCtx)->invicid = 1;
754 (*simCtx)->dt = 0.1;
755 (*simCtx)->step = 1;
756 (*simCtx)->StartStep = 0;
757 PetscCall(VecSet((*user)->Ucont, 0.0));
758 PetscCall(VecSet((*user)->Ucont_o, 0.0));
759 PetscCall(VecSet((*user)->Ucont_rm1, 0.0));
760 for (PetscInt face = 0; face < 6; ++face) {
761 PetscBool periodic_face = (PetscBool)(x_periodic &&
762 (face == BC_FACE_NEG_X || face == BC_FACE_POS_X));
763 (*user)->boundary_faces[face].face_id = (BCFace)face;
764 (*user)->boundary_faces[face].mathematical_type = periodic_face ? PERIODIC : WALL;
765 (*user)->boundary_faces[face].handler_type = periodic_face ?
767 PetscCall(BoundaryCondition_Create((*user)->boundary_faces[face].handler_type,
768 &(*user)->boundary_faces[face].handler));
769 }
770 PetscFunctionReturn(PETSC_SUCCESS);
771}
772
773/**
774 * @brief A Newton solve with free-slip walls converges, preconditioned and not.
775 *
776 * Free-slip walls are supported by Newton Krylov from the start: the matrix-free action
777 * carries the reflected boundary through the residual, and the point-block
778 * preconditioner approximates the operator without wall-specific terms.
779 */
780static PetscErrorCode TestFreeSlipWallsSolve(void)
781{
782 const char *models[] = {"none", "frozen_momentum_jacobian"};
783 const char *structures[] = {"none", "point_block"};
784
785 PetscFunctionBeginUser;
786 for (size_t n = 0; n < sizeof(models) / sizeof(models[0]); ++n) {
787 SimCtx *simCtx = NULL;
788 UserCtx *user = NULL;
789 char tmpdir[PETSC_MAX_PATH_LEN] = "";
790
791 PetscCall(BuildNewtonFixture(free_slip_wall_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
792 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_snes_rtol", "1e-4"));
793 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_snes_max_it", "20"));
794 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_ksp_rtol", "1e-6"));
795 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_model", models[n]));
796 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_structure", structures[n]));
797 PetscCall(VecSet(user->Ucont, 0.1));
798 PetscCall(MomentumSolver_NewtonKrylov(user, NULL, NULL));
799 PetscCall(PicurvAssertBool(simCtx->mom_last_converged, "a solve with free-slip walls must converge"));
800 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
801 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_preconditioner_model"));
802 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_preconditioner_structure"));
803 }
804 PetscFunctionReturn(PETSC_SUCCESS);
805}
806
807/**
808 * @brief A TwoD solve converges, preconditioned and not, and holds the masked component.
809 *
810 * ComputeRHS() zeroes the masked component's spatial terms, so its residual row is the
811 * time derivative alone. With the same value at every history level, BDF2's solution of
812 * that row is the value itself, so a nonzero xi flux must survive the solve unchanged, as
813 * the Picard solver's skipped row keeps it.
814 */
815static PetscErrorCode TestTwoDSolveHoldsMaskedComponent(void)
816{
817 const char *models[] = {"none", "frozen_momentum_jacobian"};
818 const char *structures[] = {"none", "point_block"};
819
820 PetscFunctionBeginUser;
821 for (size_t n = 0; n < sizeof(models) / sizeof(models[0]); ++n) {
822 SimCtx *simCtx = NULL;
823 UserCtx *user = NULL;
824 Cmpnts ***ucont = NULL;
825 PetscReal drift = 0.0, global_drift = 0.0;
826 char tmpdir[PETSC_MAX_PATH_LEN] = "";
827
828 PetscCall(BuildNewtonFixture(periodic_x_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
829 simCtx->TwoD = 1;
830 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_snes_rtol", "1e-8"));
831 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_snes_max_it", "20"));
832 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_ksp_rtol", "1e-10"));
833 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_model", models[n]));
834 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_structure", structures[n]));
835 PetscCall(VecSet(user->Ucont, 0.0));
836 PetscCall(VecStrideSet(user->Ucont, 0, 0.1));
837 PetscCall(VecStrideSet(user->Ucont, 2, 0.05));
838 PetscCall(VecCopy(user->Ucont, user->Ucont_o));
839 PetscCall(VecCopy(user->Ucont, user->Ucont_rm1));
840 PetscCall(MomentumSolver_NewtonKrylov(user, NULL, NULL));
841 PetscCall(PicurvAssertBool(simCtx->mom_last_converged, "a TwoD solve must converge"));
842
843 PetscCall(DMDAVecGetArrayRead(user->fda, user->Ucont, &ucont));
844 for (PetscInt k = PetscMax(user->info.zs, 1); k < PetscMin(user->info.zs + user->info.zm, user->info.mz - 2); ++k)
845 for (PetscInt j = PetscMax(user->info.ys, 1); j < PetscMin(user->info.ys + user->info.ym, user->info.my - 2); ++j)
846 for (PetscInt i = PetscMax(user->info.xs, 1); i < PetscMin(user->info.xs + user->info.xm, user->info.mx - 1); ++i)
847 drift = PetscMax(drift, PetscAbsReal(ucont[k][j][i].x - 0.1));
848 PetscCall(DMDAVecRestoreArrayRead(user->fda, user->Ucont, &ucont));
849 PetscCallMPI(MPI_Allreduce(&drift, &global_drift, 1, MPIU_REAL, MPI_MAX, PETSC_COMM_WORLD));
850 PetscCall(PicurvAssertRealNear(0.0, global_drift, 1.0e-12,
851 "the masked component must keep its time-constant value"));
852 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
853 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_preconditioner_model"));
854 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_preconditioner_structure"));
855 }
856 PetscFunctionReturn(PETSC_SUCCESS);
857}
858
859/**
860 * @brief Forms the complete direct FD Jacobian, checks every row, and compares MFFD actions.
861 */
862static PetscErrorCode TestWholeOperatorDirectJacobian(void)
863{
864 SimCtx *simCtx = NULL;
865 UserCtx *user = NULL;
866 SNES snes = NULL;
867 Mat J = NULL;
868 Vec x = NULL, xp = NULL, f0 = NULL, fp = NULL, column = NULL, square = NULL;
869 Vec row_norm_sq = NULL, v[2] = {NULL, NULL}, dense_v[2] = {NULL, NULL};
870 Vec mffd_v = NULL, error_vec = NULL;
871 PetscInt n_global, lo, hi;
872 PetscReal min_row_sq = 0.0, error = 0.0, reference = 0.0;
873 const PetscReal epsilon = 1.0e-7;
875
876 PetscFunctionBeginUser;
877 PetscCall(BuildMinimalWallOperatorFixture(&simCtx, &user, PETSC_FALSE));
878 PetscCall(VecDuplicate(user->Ucont, &x)); PetscCall(VecSet(x, 0.0));
879 PetscCall(VecDuplicate(x, &xp)); PetscCall(VecDuplicate(x, &f0));
880 PetscCall(VecDuplicate(x, &fp)); PetscCall(VecDuplicate(x, &column));
881 PetscCall(VecDuplicate(x, &square)); PetscCall(VecDuplicate(x, &row_norm_sq));
882 PetscCall(VecDuplicate(x, &v[0])); PetscCall(VecDuplicate(x, &v[1]));
883 PetscCall(VecDuplicate(x, &dense_v[0])); PetscCall(VecDuplicate(x, &dense_v[1]));
884 PetscCall(VecDuplicate(x, &mffd_v)); PetscCall(VecDuplicate(x, &error_vec));
885 PetscCall(VecZeroEntries(row_norm_sq)); PetscCall(VecZeroEntries(dense_v[0]));
886 PetscCall(VecZeroEntries(dense_v[1]));
887 PetscCall(VecGetSize(x, &n_global));
888 PetscCall(VecGetOwnershipRange(x, &lo, &hi));
889 for (PetscInt which = 0; which < 2; ++which) {
890 PetscScalar *a = NULL;
891 PetscCall(VecGetArray(v[which], &a));
892 for (PetscInt local = 0; local < hi - lo; ++local) {
893 PetscInt global = lo + local;
894 a[local] = which == 0 ? (PetscScalar)(1.0 + 0.05 * (global % 9))
895 : (PetscScalar)(((global % 2) ? -1.0 : 1.0) * (0.5 + 0.03 * (global % 7)));
896 }
897 PetscCall(VecRestoreArray(v[which], &a));
898 }
899
900 ctx.user = user;
901 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f0, &ctx));
902 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f0, &ctx));
903 for (PetscInt col = 0; col < n_global; ++col) {
904 PetscScalar coeff[2] = {
905 (PetscScalar)(1.0 + 0.05 * (col % 9)),
906 (PetscScalar)(((col % 2) ? -1.0 : 1.0) * (0.5 + 0.03 * (col % 7)))
907 };
908 PetscCall(VecCopy(x, xp));
909 if (col >= lo && col < hi) PetscCall(VecSetValue(xp, col, epsilon, ADD_VALUES));
910 PetscCall(VecAssemblyBegin(xp)); PetscCall(VecAssemblyEnd(xp));
911 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, xp, fp, &ctx));
912 PetscCall(VecWAXPY(column, -1.0, f0, fp));
913 PetscCall(VecScale(column, 1.0 / epsilon));
914 PetscCall(VecPointwiseMult(square, column, column));
915 PetscCall(VecAXPY(row_norm_sq, 1.0, square));
916 PetscCall(VecAXPY(dense_v[0], coeff[0], column));
917 PetscCall(VecAXPY(dense_v[1], coeff[1], column));
918 }
919 PetscCall(VecMin(row_norm_sq, NULL, &min_row_sq));
920 PetscCheck(min_row_sq > 0.5, PETSC_COMM_WORLD, PETSC_ERR_PLIB,
921 "Complete Newton Jacobian contains an unexplained zero/weak row (min squared norm=%g).",
922 (double)min_row_sq);
923 PetscCall(CheckStoredDerivative(user, x, 2, 2, 2, 0, 2, 2, 2, 0,
924 10.0, 1.0e-5, "interior BDF1 temporal diagonal"));
925
926 PetscCall(SNESCreate(PETSC_COMM_WORLD, &snes));
927 PetscCall(SNESSetDM(snes, user->fda));
928 PetscCall(SNESSetFunction(snes, f0, MomentumNewtonKrylov_FormResidual, &ctx));
929 PetscCall(MatCreateSNESMF(snes, &J));
930 PetscCall(MatMFFDSetBase(J, x, f0));
931 for (PetscInt which = 0; which < 2; ++which) {
932 PetscCall(MatMult(J, v[which], mffd_v));
933 PetscCall(VecWAXPY(error_vec, -1.0, dense_v[which], mffd_v));
934 PetscCall(VecNorm(error_vec, NORM_2, &error));
935 PetscCall(VecNorm(dense_v[which], NORM_2, &reference));
936 PetscCheck(error <= 2.0e-5 * PetscMax(1.0, reference), PETSC_COMM_WORLD, PETSC_ERR_PLIB,
937 "Independent dense FD Jv differs from PETSc MFFD action %" PetscInt_FMT ": error=%g reference=%g.",
938 which, (double)error, (double)reference);
939 }
940
941 for (PetscInt which = 0; which < 2; ++which) {
942 const PetscReal steps[3] = {1.0e-4, 1.0e-6, 1.0e-8};
943 PetscReal best = PETSC_MAX_REAL;
944 for (PetscInt s = 0; s < 3; ++s) {
945 PetscCall(VecWAXPY(xp, steps[s], v[which], x));
946 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, xp, fp, &ctx));
947 PetscCall(VecWAXPY(column, -1.0, f0, fp));
948 PetscCall(VecScale(column, 1.0 / steps[s]));
949 PetscCall(VecAXPY(column, -1.0, dense_v[which]));
950 PetscCall(VecNorm(column, NORM_2, &error));
951 best = PetscMin(best, error);
952 }
953 PetscCall(VecNorm(dense_v[which], NORM_2, &reference));
954 PetscCheck(best <= 2.0e-5 * PetscMax(1.0, reference), PETSC_COMM_WORLD, PETSC_ERR_PLIB,
955 "Direct directional differences show no accuracy plateau for vector %" PetscInt_FMT " (best=%g).",
956 which, (double)best);
957 }
958
959 PetscCall(MatDestroy(&J)); PetscCall(SNESDestroy(&snes));
960 PetscCall(VecDestroy(&error_vec)); PetscCall(VecDestroy(&mffd_v));
961 PetscCall(VecDestroy(&dense_v[1])); PetscCall(VecDestroy(&dense_v[0]));
962 PetscCall(VecDestroy(&v[1])); PetscCall(VecDestroy(&v[0]));
963 PetscCall(VecDestroy(&row_norm_sq)); PetscCall(VecDestroy(&square));
964 PetscCall(VecDestroy(&column)); PetscCall(VecDestroy(&fp)); PetscCall(VecDestroy(&f0));
965 PetscCall(VecDestroy(&xp)); PetscCall(VecDestroy(&x));
966 PetscCall(BoundarySystem_Destroy(user));
967 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
968 PetscFunctionReturn(PETSC_SUCCESS);
969}
970
971/** @brief Audits every row of a complete operator containing periodic duplicates. */
972static PetscErrorCode TestPeriodicOperatorHasNoZeroRows(void)
973{
974 SimCtx *simCtx = NULL;
975 UserCtx *user = NULL;
976 Vec x = NULL, xp = NULL, f0 = NULL, fp = NULL, column = NULL;
977 Vec square = NULL, row_norm_sq = NULL;
978 PetscInt n_global, lo, hi;
979 PetscReal min_row_sq = 0.0;
980 const PetscReal epsilon = 1.0e-7;
982
983 PetscFunctionBeginUser;
984 PetscCall(BuildMinimalWallOperatorFixture(&simCtx, &user, PETSC_TRUE));
985 PetscCall(VecDuplicate(user->Ucont, &x)); PetscCall(VecSet(x, 0.0));
986 PetscCall(VecDuplicate(x, &xp)); PetscCall(VecDuplicate(x, &f0));
987 PetscCall(VecDuplicate(x, &fp)); PetscCall(VecDuplicate(x, &column));
988 PetscCall(VecDuplicate(x, &square)); PetscCall(VecDuplicate(x, &row_norm_sq));
989 PetscCall(VecZeroEntries(row_norm_sq));
990 PetscCall(VecGetSize(x, &n_global)); PetscCall(VecGetOwnershipRange(x, &lo, &hi));
991 ctx.user = user;
992 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f0, &ctx));
993 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f0, &ctx));
994 for (PetscInt col = 0; col < n_global; ++col) {
995 PetscCall(VecCopy(x, xp));
996 if (col >= lo && col < hi) PetscCall(VecSetValue(xp, col, epsilon, ADD_VALUES));
997 PetscCall(VecAssemblyBegin(xp)); PetscCall(VecAssemblyEnd(xp));
998 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, xp, fp, &ctx));
999 PetscCall(VecWAXPY(column, -1.0, f0, fp)); PetscCall(VecScale(column, 1.0 / epsilon));
1000 PetscCall(VecPointwiseMult(square, column, column));
1001 PetscCall(VecAXPY(row_norm_sq, 1.0, square));
1002 }
1003 PetscCall(VecMin(row_norm_sq, NULL, &min_row_sq));
1004 PetscCheck(min_row_sq > 0.5, PETSC_COMM_WORLD, PETSC_ERR_PLIB,
1005 "Periodic Newton Jacobian contains a zero/weak row (min squared norm=%g).",
1006 (double)min_row_sq);
1007 PetscCall(VecDestroy(&row_norm_sq)); PetscCall(VecDestroy(&square));
1008 PetscCall(VecDestroy(&column)); PetscCall(VecDestroy(&fp)); PetscCall(VecDestroy(&f0));
1009 PetscCall(VecDestroy(&xp)); PetscCall(VecDestroy(&x));
1010 PetscCall(BoundarySystem_Destroy(user));
1011 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1012 PetscFunctionReturn(PETSC_SUCCESS);
1013}
1014
1015/** @brief Exercises a converged solve, a committed unconverged solve, a non-finite rollback, and cleanup. */
1016static PetscErrorCode TestSmallSolveAndRollback(void)
1017{
1018 SimCtx *simCtx = NULL;
1019 UserCtx *user = NULL;
1020 char tmpdir[PETSC_MAX_PATH_LEN] = "";
1021 Vec entry = NULL, delta = NULL;
1022 PetscErrorCode solve_ierr;
1023 PetscReal norm = 0.0;
1024 const FieldId fields[] = {FIELD_ID_UCONT};
1025 char summary_path[PETSC_MAX_PATH_LEN];
1026 char history_path[PETSC_MAX_PATH_LEN];
1027 char linear_history_path[PETSC_MAX_PATH_LEN];
1028
1029 PetscFunctionBeginUser;
1030 PetscCall(BuildNewtonFixture(fixed_wall_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
1031 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_snes_rtol", "1e-4"));
1032 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_snes_max_it", "20"));
1033 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_ksp_rtol", "1e-6"));
1034 simCtx->mom_nk_monitor_history = PETSC_TRUE;
1035 PetscCall(MomentumSolver_NewtonKrylov(user, NULL, NULL));
1036 PetscCall(PicurvAssertBool(simCtx->mom_last_converged, "small Newton solve must converge"));
1037 PetscCall(PicurvAssertBool((PetscBool)(user->Rhs != NULL && user->momentumNewtonKrylov != NULL),
1038 "successful solve must keep its workspace for the next step"));
1039 PetscCall(PetscSNPrintf(summary_path, sizeof(summary_path),
1040 "%s/Momentum_Solver_Newton_Krylov_Summary_Block_0.csv", simCtx->log_dir));
1041 PetscCall(PetscSNPrintf(history_path, sizeof(history_path),
1042 "%s/Momentum_Solver_Newton_Krylov_History_Block_0.csv", simCtx->log_dir));
1043 PetscCall(PetscSNPrintf(linear_history_path, sizeof(linear_history_path),
1044 "%s/Momentum_Solver_Newton_Krylov_Linear_History_Block_0.csv",
1045 simCtx->log_dir));
1046 PetscCall(AssertNewtonLog(summary_path, 1, ",Newton Krylov,", ",committed\n"));
1047 PetscCall(AssertNewtonLog(history_path, -2, "step,block,newton,nonlinear_norm", ",0,0,"));
1048 PetscCall(AssertNewtonLog(linear_history_path, -2, "krylov,requested_rtol,reported_residual_norm", ",0,0,0,"));
1049
1050 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
1051 tmpdir[0] = '\0';
1052 PetscCall(BuildNewtonFixture(fixed_wall_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
1053 PetscCall(VecSet(user->Ucont, 0.2));
1054 PetscCall(SynchronizePeriodicStaggeredFields(user, 1, fields));
1055 PetscCall(ApplyBoundaryConditions(user));
1056 PetscCall(VecDuplicate(user->Ucont, &entry));
1057 PetscCall(VecDuplicate(user->Ucont, &delta));
1058 PetscCall(VecCopy(user->Ucont, entry));
1059 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_snes_max_it", "0"));
1060 solve_ierr = MomentumSolver_NewtonKrylov(user, NULL, NULL);
1061 PetscCall(PicurvAssertIntEqual(0, solve_ierr,
1062 "a finite residual no larger than at entry must commit the step"));
1063 PetscCall(PicurvAssertBool((PetscBool)!simCtx->mom_last_converged,
1064 "a committed SNES failure must be reported as not converged"));
1065 PetscCall(VecWAXPY(delta, -1.0, entry, user->Ucont));
1066 PetscCall(VecNorm(delta, NORM_INFINITY, &norm));
1067 PetscCall(PicurvAssertRealNear(0.0, norm, 1.0e-12,
1068 "a zero-iteration failure must commit the unchanged entry state"));
1069 PetscCall(PicurvAssertBool((PetscBool)(user->Rhs != NULL && user->momentumNewtonKrylov != NULL),
1070 "committed unconverged solve must keep its workspace for the next step"));
1071 PetscCall(PetscSNPrintf(summary_path, sizeof(summary_path),
1072 "%s/Momentum_Solver_Newton_Krylov_Summary_Block_0.csv", simCtx->log_dir));
1073 PetscCall(AssertNewtonLog(summary_path, 1, ",DIVERGED_", ",committed_unconverged\n"));
1074
1075 /* A non-finite residual is the one failure that must still roll back and stop the run.
1076 Options are read when the solve context is built, so release it to apply the new limit. */
1077 PetscCall(MomentumSolver_NewtonKrylov_Destroy(user));
1078 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_snes_max_it", "20"));
1079 PetscCall(VecSet(user->Ucont, PETSC_MAX_REAL));
1080 PetscCall(SynchronizePeriodicStaggeredFields(user, 1, fields));
1081 PetscCall(ApplyBoundaryConditions(user));
1082 PetscCall(VecCopy(user->Ucont, entry));
1083 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1084 solve_ierr = MomentumSolver_NewtonKrylov(user, NULL, NULL);
1085 PetscCall(PetscPopErrorHandler());
1086 PetscCall(PicurvAssertIntEqual(PETSC_ERR_CONV_FAILED, solve_ierr,
1087 "a non-finite residual must report PETSC_ERR_CONV_FAILED"));
1088 PetscCall(PicurvAssertBool((PetscBool)!simCtx->mom_last_converged,
1089 "a rolled-back solve must be reported as not converged"));
1090 PetscCall(VecWAXPY(delta, -1.0, entry, user->Ucont));
1091 PetscCall(VecNorm(delta, NORM_INFINITY, &norm));
1092 PetscCall(PicurvAssertRealNear(0.0, norm, 1.0e-12,
1093 "failed Newton solve must restore the canonical entry state"));
1094 PetscCall(PicurvAssertBool((PetscBool)(user->Rhs == NULL), "failed solve must release Rhs"));
1095 PetscCall(AssertNewtonLog(summary_path, 2, ",DIVERGED_", ",rolled_back\n"));
1096
1097 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_snes_rtol"));
1098 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_snes_max_it"));
1099 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_ksp_rtol"));
1100 PetscCall(VecDestroy(&delta));
1101 PetscCall(VecDestroy(&entry));
1102 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
1103 PetscFunctionReturn(PETSC_SUCCESS);
1104}
1105
1106/** @brief Verifies the six-wall zero-velocity case logs zero Newton/Krylov work. */
1107static PetscErrorCode TestZeroIterationStructuredLogging(void)
1108{
1109 SimCtx *simCtx = NULL;
1110 UserCtx *user = NULL;
1111 char tmpdir[PETSC_MAX_PATH_LEN] = "";
1112 char summary_path[PETSC_MAX_PATH_LEN];
1113 const FieldId fields[] = {FIELD_ID_UCONT};
1114
1115 PetscFunctionBeginUser;
1116 PetscCall(BuildNewtonFixture(fixed_wall_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
1117 PetscCall(VecZeroEntries(user->Ucont));
1118 PetscCall(VecZeroEntries(user->Ucont_o));
1119 PetscCall(VecZeroEntries(user->Ucont_rm1));
1120 PetscCall(SynchronizePeriodicStaggeredFields(user, 1, fields));
1121 PetscCall(ApplyBoundaryConditions(user));
1122 PetscCall(MomentumSolver_NewtonKrylov(user, NULL, NULL));
1123 PetscCall(PetscSNPrintf(summary_path, sizeof(summary_path),
1124 "%s/Momentum_Solver_Newton_Krylov_Summary_Block_0.csv", simCtx->log_dir));
1125 PetscCall(AssertNewtonLog(summary_path, 1, ",0,1,0,",
1126 ",0.0000000000000000e+00,committed\n"));
1127 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
1128 PetscFunctionReturn(PETSC_SUCCESS);
1129}
1130
1131/**
1132 * @brief Exercises the straight-duct BDF1 startup path used by flat_channel.
1133 *
1134 * The conservation outlet consumes lUcat during its first boundary pass. This
1135 * test deliberately evaluates the callback at the initialized state before
1136 * installing SNES, then completes the first nonlinear solve with each shipped
1137 * preconditioner. It catches a missing Ucont -> Ucat -> lUcat seed as a
1138 * non-finite initial residual rather than hiding it behind later MFFD work.
1139 */
1140static PetscErrorCode CheckFlatChannelStartup(PetscBool use_point_block)
1141{
1142 SimCtx *simCtx = NULL;
1143 UserCtx *user = NULL;
1144 char tmpdir[PETSC_MAX_PATH_LEN] = "";
1145 Vec x = NULL, f = NULL;
1146 PetscReal initial_norm = 0.0;
1148
1149 PetscFunctionBeginUser;
1150 PetscCall(BuildNewtonFixture(NULL, &simCtx, &user, tmpdir, sizeof(tmpdir)));
1151 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
1152 PetscCall(VecDuplicate(user->Ucont, &x));
1153 PetscCall(VecDuplicate(user->Ucont, &f));
1154 PetscCall(VecCopy(user->Ucont, x));
1155 ctx.user = user;
1156 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f, &ctx));
1157 PetscCall(VecNorm(f, NORM_2, &initial_norm));
1158 PetscCheck(!PetscIsInfOrNanReal(initial_norm), PETSC_COMM_WORLD, PETSC_ERR_FP,
1159 "flat-channel BDF1 initial residual is non-finite (%g) with %s.",
1160 (double)initial_norm,
1161 use_point_block ? "frozen-momentum point-block" : "PCNONE");
1162 PetscCall(VecDestroy(&f));
1163 PetscCall(VecDestroy(&x));
1164 PetscCall(VecDestroy(&user->Rhs));
1165
1166 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_snes_rtol", "1e-4"));
1167 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_snes_max_it", "20"));
1168 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_ksp_rtol", "1e-6"));
1169 if (use_point_block) {
1170 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_model",
1171 "frozen_momentum_jacobian"));
1172 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_structure",
1173 "point_block"));
1174 }
1175 PetscCall(MomentumSolver_NewtonKrylov(user, NULL, NULL));
1176 PetscCall(PicurvAssertBool(simCtx->mom_last_converged,
1177 "flat-channel BDF1 Newton solve must converge"));
1178 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_preconditioner_model"));
1179 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_preconditioner_structure"));
1180 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_snes_rtol"));
1181 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_snes_max_it"));
1182 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_ksp_rtol"));
1183 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
1184 PetscFunctionReturn(PETSC_SUCCESS);
1185}
1186
1187/** @brief Guards flat_channel's initial BDF1 residual and both shipped NK PCs. */
1188static PetscErrorCode TestFlatChannelStartup(void)
1189{
1190 PetscFunctionBeginUser;
1191 PetscCall(CheckFlatChannelStartup(PETSC_FALSE));
1192 PetscCall(CheckFlatChannelStartup(PETSC_TRUE));
1193 PetscFunctionReturn(PETSC_SUCCESS);
1194}
1195
1196/** @brief Verifies restarted Newton solves with both supported preconditioners. */
1197static PetscErrorCode TestRestartAndContinuationSolve(void)
1198{
1199 SimCtx *simCtx = NULL;
1200 UserCtx *user = NULL;
1201 char tmpdir[PETSC_MAX_PATH_LEN] = "";
1202 const FieldId fields[] = {FIELD_ID_UCONT};
1203
1204 PetscFunctionBeginUser;
1205 PetscCall(BuildNewtonFixture(fixed_wall_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
1206 PetscCall(VecSet(user->Ucont, 0.2));
1207 PetscCall(VecZeroEntries(user->Ucont_o));
1208 PetscCall(VecZeroEntries(user->Ucont_rm1));
1209 PetscCall(SynchronizePeriodicStaggeredFields(user, 1, fields));
1210 PetscCall(ApplyBoundaryConditions(user));
1211 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_snes_rtol", "1e-4"));
1212 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_snes_max_it", "20"));
1213 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_ksp_rtol", "1e-6"));
1214
1215 /* AdvanceSimulation() solves StartStep+1 first. Use a nonzero checkpoint
1216 * state so that SNES takes a Newton/Krylov path rather than accepting a
1217 * trivial residual. */
1218 simCtx->StartStep = 7;
1219 simCtx->step = simCtx->StartStep + 1;
1220 PetscCall(MomentumSolver_NewtonKrylov(user, NULL, NULL));
1221 PetscCall(PicurvAssertBool(simCtx->mom_last_converged,
1222 "PCNONE Newton Krylov checkpoint restart must converge"));
1223
1224 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
1225 tmpdir[0] = '\0';
1226 PetscCall(BuildNewtonFixture(fixed_wall_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
1227 PetscCall(VecSet(user->Ucont, 0.2));
1228 PetscCall(VecZeroEntries(user->Ucont_o));
1229 PetscCall(VecZeroEntries(user->Ucont_rm1));
1230 PetscCall(SynchronizePeriodicStaggeredFields(user, 1, fields));
1231 PetscCall(ApplyBoundaryConditions(user));
1232 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_model",
1233 "frozen_momentum_jacobian"));
1234 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_structure", "point_block"));
1235
1236 simCtx->continueMode = PETSC_TRUE;
1237 simCtx->StartStep = 8;
1238 simCtx->step = simCtx->StartStep + 1;
1239 PetscCall(MomentumSolver_NewtonKrylov(user, NULL, NULL));
1240 PetscCall(PicurvAssertBool(simCtx->mom_last_converged,
1241 "frozen-momentum Newton Krylov --continue solve must converge"));
1242 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_preconditioner_model"));
1243 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_preconditioner_structure"));
1244 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_snes_rtol"));
1245 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_snes_max_it"));
1246 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_ksp_rtol"));
1247 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
1248 PetscFunctionReturn(PETSC_SUCCESS);
1249}
1250
1251/** @brief Confirms unsupported features fail before workspace allocation. */
1253{
1254 SimCtx *simCtx = NULL;
1255 UserCtx *user = NULL;
1256 char tmpdir[PETSC_MAX_PATH_LEN] = "";
1257 PetscErrorCode solve_ierr;
1258
1259 PetscFunctionBeginUser;
1260 PetscCall(BuildNewtonFixture(fixed_wall_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
1261 {
1262 /* The gradient (Clark) model and wall functions are no longer rejected: both
1263 reach the solver through the shared residual, which this solver already
1264 evaluates. Neither is verified under Newton-Krylov yet, so neither is
1265 claimed as supported; they are simply no longer refused. */
1266 PetscInt *unsupported_flags[] = {
1267 &simCtx->immersed, &simCtx->movefsi, &simCtx->rotatefsi,
1268 &simCtx->moveframe, &simCtx->rotateframe
1269 };
1270 for (size_t flag = 0; flag < sizeof(unsupported_flags) / sizeof(unsupported_flags[0]); ++flag) {
1271 *unsupported_flags[flag] = 1;
1272 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1273 solve_ierr = MomentumSolver_NewtonKrylov(user, NULL, NULL);
1274 PetscCall(PetscPopErrorHandler());
1275 PetscCall(PicurvAssertBool((PetscBool)(solve_ierr != PETSC_SUCCESS),
1276 "unsupported Newton feature flag must fail"));
1277 PetscCall(PicurvAssertBool((PetscBool)(user->Rhs == NULL),
1278 "feature validation must precede workspace allocation"));
1279 *unsupported_flags[flag] = 0;
1280 }
1281 }
1282 simCtx->block_number = 2;
1283 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1284 solve_ierr = MomentumSolver_NewtonKrylov(user, NULL, NULL);
1285 PetscCall(PetscPopErrorHandler());
1286 PetscCall(PicurvAssertBool((PetscBool)(solve_ierr != PETSC_SUCCESS), "multiblock must fail"));
1287 simCtx->block_number = 1;
1288 /* Solid cells are no longer a rejection. The residual already zeroes their rows, so
1289 they carry no unknown; the solver now gives them the same identity treatment as
1290 any other row without one, instead of refusing the whole solve. A fully masked
1291 domain is the degenerate case of that: every row is an identity row and the
1292 system is trivially consistent. This checks the structural contract only; the
1293 masked path has no physics verification because nothing in the tree currently
1294 populates Nvert with a nonzero value. */
1295 PetscCall(VecSet(user->Nvert, 1.0));
1296 PetscCall(VecSet(user->lNvert, 1.0));
1297 solve_ierr = MomentumSolver_NewtonKrylov(user, NULL, NULL);
1298 PetscCall(PicurvAssertBool((PetscBool)(solve_ierr == PETSC_SUCCESS),
1299 "a fully masked domain must resolve to identity rows, not a rejection"));
1300 /* That solve kept a context built for the all-solid mask; release it so the remaining
1301 checks see the fixture's unallocated workspace again. */
1302 PetscCall(MomentumSolver_NewtonKrylov_Destroy(user));
1303 PetscCall(VecDestroy(&user->Rhs));
1304 PetscCall(VecSet(user->Nvert, 0.0));
1305 PetscCall(VecSet(user->lNvert, 0.0));
1306 PetscCall(VecSet(user->Nvert, 0.0));
1309 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1310 solve_ierr = MomentumSolver_NewtonKrylov(user, NULL, NULL);
1311 PetscCall(PetscPopErrorHandler());
1312 PetscCall(PicurvAssertBool((PetscBool)(solve_ierr != PETSC_SUCCESS),
1313 "driven constant-flux controller must fail"));
1314 PetscCall(PicurvAssertBool((PetscBool)(user->Rhs == NULL),
1315 "all validation failures must precede workspace allocation"));
1316 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
1317 PetscFunctionReturn(PETSC_SUCCESS);
1318}
1319
1320/** @brief Verifies cleanup and rollback after an options failure following asset creation. */
1321static PetscErrorCode TestPostAllocationFailureCleanup(void)
1322{
1323 SimCtx *simCtx = NULL;
1324 UserCtx *user = NULL;
1325 char tmpdir[PETSC_MAX_PATH_LEN] = "";
1326 Vec entry = NULL, delta = NULL;
1327 PetscErrorCode solve_ierr;
1328 PetscReal norm = 0.0;
1329 const FieldId fields[] = {FIELD_ID_UCONT};
1330
1331 PetscFunctionBeginUser;
1332 PetscCall(BuildNewtonFixture(fixed_wall_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
1333 PetscCall(VecSet(user->Ucont, 0.2));
1334 PetscCall(SynchronizePeriodicStaggeredFields(user, 1, fields));
1335 PetscCall(ApplyBoundaryConditions(user));
1336 PetscCall(VecDuplicate(user->Ucont, &entry)); PetscCall(VecCopy(user->Ucont, entry));
1337 PetscCall(VecDuplicate(user->Ucont, &delta));
1338 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_pc_type", "jacobi"));
1339 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1340 solve_ierr = MomentumSolver_NewtonKrylov(user, NULL, NULL);
1341 PetscCall(PetscPopErrorHandler());
1342 PetscCall(PetscOptionsClearValue(NULL, "-mom_nk_pc_type"));
1343 PetscCall(PicurvAssertIntEqual(PETSC_ERR_SUP, solve_ierr,
1344 "non-PCNONE option must fail after setup"));
1345 PetscCall(PicurvAssertBool((PetscBool)(user->Rhs == NULL),
1346 "post-allocation failure must destroy Rhs"));
1347 PetscCall(VecWAXPY(delta, -1.0, entry, user->Ucont));
1348 PetscCall(VecNorm(delta, NORM_INFINITY, &norm));
1349 PetscCall(PicurvAssertRealNear(0.0, norm, 1.0e-12,
1350 "post-allocation failure must restore canonical entry"));
1351 PetscCall(VecDestroy(&delta)); PetscCall(VecDestroy(&entry));
1352 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
1353 PetscFunctionReturn(PETSC_SUCCESS);
1354}
1355
1356/** @brief Verifies finalized application-owned linearization option parsing. */
1357static PetscErrorCode TestLinearizationConfigParsing(void)
1358{
1359 MomentumNewtonJacobian jacobian = {0};
1360 MomentumPreconditionerDescription description = {0};
1361 PetscErrorCode config_ierr;
1362 const char *option_names[] = {
1363 "-mom_nk_jacobian_type",
1364 "-mom_nk_jacobian_fd_mode",
1365 "-mom_nk_preconditioner_model",
1366 "-mom_nk_preconditioner_structure"
1367 };
1368
1369 PetscFunctionBeginUser;
1370 for (size_t n = 0; n < sizeof(option_names) / sizeof(option_names[0]); ++n)
1371 PetscCall(PetscOptionsClearValue(NULL, option_names[n]));
1372
1373 PetscCall(MomentumNewtonKrylov_ReadLinearizationConfig(&jacobian, &description));
1375 "default Jacobian type must be finite difference"));
1377 jacobian.finite_difference_mode,
1378 "default finite-difference mode must be matrix free"));
1379 PetscCall(PicurvAssertIntEqual(MOM_NK_PC_MODEL_NONE, description.model,
1380 "default preconditioner model must be none"));
1382 "default preconditioner structure must be none"));
1383
1384 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_jacobian_type", "finite_difference"));
1385 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_jacobian_fd_mode", "matrix_free"));
1386 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_model", "none"));
1387 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_structure", "none"));
1388 PetscCall(MomentumNewtonKrylov_ReadLinearizationConfig(&jacobian, &description));
1390 "explicit baseline Jacobian type must parse"));
1392 jacobian.finite_difference_mode,
1393 "explicit baseline finite-difference mode must parse"));
1394 PetscCall(PicurvAssertIntEqual(MOM_NK_PC_MODEL_NONE, description.model,
1395 "explicit baseline preconditioner model must parse"));
1397 "explicit baseline preconditioner structure must parse"));
1398
1399 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_model",
1400 "frozen_momentum_jacobian"));
1401 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_structure", "point_block"));
1402 PetscCall(MomentumNewtonKrylov_ReadLinearizationConfig(&jacobian, &description));
1404 "explicit Jacobian type must parse"));
1406 jacobian.finite_difference_mode,
1407 "explicit finite-difference mode must parse"));
1409 description.model, "frozen model must parse"));
1411 description.structure, "point-block structure must parse"));
1412
1413 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_jacobian_type", "analytic"));
1414 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1415 config_ierr = MomentumNewtonKrylov_ReadLinearizationConfig(&jacobian, &description);
1416 PetscCall(PetscPopErrorHandler());
1417 PetscCall(PicurvAssertIntEqual(PETSC_ERR_ARG_WRONG, config_ierr,
1418 "unsupported Jacobian type must fail"));
1419 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_jacobian_type", "finite_difference"));
1420
1421 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_jacobian_fd_mode", "colored_sparse"));
1422 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1423 config_ierr = MomentumNewtonKrylov_ReadLinearizationConfig(&jacobian, &description);
1424 PetscCall(PetscPopErrorHandler());
1425 PetscCall(PicurvAssertIntEqual(PETSC_ERR_ARG_WRONG, config_ierr,
1426 "unsupported finite-difference mode must fail"));
1427 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_jacobian_fd_mode", "matrix_free"));
1428
1429 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_structure", "none"));
1430 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1431 config_ierr = MomentumNewtonKrylov_ReadLinearizationConfig(&jacobian, &description);
1432 PetscCall(PetscPopErrorHandler());
1433 PetscCall(PicurvAssertIntEqual(PETSC_ERR_SUP, config_ierr,
1434 "frozen model without point block must fail"));
1435 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_model", "none"));
1436 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_structure", "point_block"));
1437 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1438 config_ierr = MomentumNewtonKrylov_ReadLinearizationConfig(&jacobian, &description);
1439 PetscCall(PetscPopErrorHandler());
1440 PetscCall(PicurvAssertIntEqual(PETSC_ERR_SUP, config_ierr,
1441 "none model with point block must fail"));
1442
1443 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_model", "diagonal"));
1444 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_structure", "none"));
1445 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1446 config_ierr = MomentumNewtonKrylov_ReadLinearizationConfig(&jacobian, &description);
1447 PetscCall(PetscPopErrorHandler());
1448 PetscCall(PicurvAssertIntEqual(PETSC_ERR_ARG_WRONG, config_ierr,
1449 "unsupported preconditioner model must fail"));
1450 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_model",
1451 "frozen_momentum_jacobian"));
1452 PetscCall(PetscOptionsSetValue(NULL, "-mom_nk_preconditioner_structure", "line"));
1453 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
1454 config_ierr = MomentumNewtonKrylov_ReadLinearizationConfig(&jacobian, &description);
1455 PetscCall(PetscPopErrorHandler());
1456 PetscCall(PicurvAssertIntEqual(PETSC_ERR_ARG_WRONG, config_ierr,
1457 "unsupported preconditioner structure must fail"));
1458
1459 for (size_t n = 0; n < sizeof(option_names) / sizeof(option_names[0]); ++n)
1460 PetscCall(PetscOptionsClearValue(NULL, option_names[n]));
1461 PetscFunctionReturn(PETSC_SUCCESS);
1462}
1463
1464/** @brief Converts an in-domain or periodic-ghost DMDA stencil to PETSc ordering. */
1465static PetscErrorCode TestStencilToGlobal(UserCtx *user, MatStencil stencil,
1466 PetscInt *global_index)
1467{
1468 ISLocalToGlobalMapping local_to_global = NULL;
1469 PetscInt ghost_starts[3], ghost_sizes[3], local_index;
1470
1471 PetscFunctionBeginUser;
1472 PetscCall(DMDAGetGhostCorners(user->fda,
1473 &ghost_starts[0], &ghost_starts[1], &ghost_starts[2],
1474 &ghost_sizes[0], &ghost_sizes[1], &ghost_sizes[2]));
1475 PetscCall(PicurvAssertBool((PetscBool)(
1476 stencil.i >= ghost_starts[0] && stencil.i < ghost_starts[0] + ghost_sizes[0] &&
1477 stencil.j >= ghost_starts[1] && stencil.j < ghost_starts[1] + ghost_sizes[1] &&
1478 stencil.k >= ghost_starts[2] && stencil.k < ghost_starts[2] + ghost_sizes[2]),
1479 "test stencil must lie in the local DMDA ghost region"));
1480 local_index = stencil.c + 3 * (
1481 (stencil.i - ghost_starts[0]) + ghost_sizes[0] * (
1482 (stencil.j - ghost_starts[1]) + ghost_sizes[1] *
1483 (stencil.k - ghost_starts[2])));
1484 PetscCall(DMGetLocalToGlobalMapping(user->fda, &local_to_global));
1485 PetscCall(ISLocalToGlobalMappingApply(local_to_global, 1, &local_index,
1486 global_index));
1487 PetscFunctionReturn(PETSC_SUCCESS);
1488}
1489
1490/** @brief Reads one DMDA-stencil matrix entry through collective basis vectors. */
1491static PetscErrorCode PreconditionerMatrixStencilEntry(UserCtx *user,
1492 Mat preconditioning_matrix, MatStencil row,
1493 MatStencil col, PetscScalar *value)
1494{
1495 Vec column_basis = NULL, row_basis = NULL, product = NULL;
1496 AO ao = NULL;
1497 PetscInt mx, my, row_index, col_index;
1498 PetscFunctionBeginUser;
1499 PetscCall(DMDAGetInfo(user->fda, NULL, &mx, &my, NULL, NULL, NULL, NULL,
1500 NULL, NULL, NULL, NULL, NULL, NULL));
1501 row_index = row.c + 3 * (row.i + mx * (row.j + my * row.k));
1502 col_index = col.c + 3 * (col.i + mx * (col.j + my * col.k));
1503 PetscCall(DMDAGetAO(user->fda, &ao));
1504 PetscCall(AOApplicationToPetsc(ao, 1, &row_index));
1505 PetscCall(AOApplicationToPetsc(ao, 1, &col_index));
1506 PetscCall(DMCreateGlobalVector(user->fda, &column_basis));
1507 PetscCall(DMCreateGlobalVector(user->fda, &row_basis));
1508 PetscCall(DMCreateGlobalVector(user->fda, &product));
1509 PetscCall(VecSet(column_basis, 0.0)); PetscCall(VecSet(row_basis, 0.0));
1510 PetscCall(VecSetValue(column_basis, col_index, 1.0, INSERT_VALUES));
1511 PetscCall(VecSetValue(row_basis, row_index, 1.0, INSERT_VALUES));
1512 PetscCall(VecAssemblyBegin(column_basis)); PetscCall(VecAssemblyEnd(column_basis));
1513 PetscCall(VecAssemblyBegin(row_basis)); PetscCall(VecAssemblyEnd(row_basis));
1514 PetscCall(MatMult(preconditioning_matrix, column_basis, product));
1515 PetscCall(VecDot(row_basis, product, value));
1516 PetscCall(VecDestroy(&product)); PetscCall(VecDestroy(&row_basis));
1517 PetscCall(VecDestroy(&column_basis));
1518 PetscFunctionReturn(PETSC_SUCCESS);
1519}
1520
1521/** @brief Verifies the exact AIJ layout and preallocation derived from row classes. */
1523 UserCtx *user, Mat matrix, PetscBool require_offrank_periodic)
1524{
1525 DMDALocalInfo info;
1526 MatInfo matrix_info;
1527 PetscMPIInt comm_size = 1;
1528 PetscInt matrix_rows, matrix_cols, local_rows, local_cols;
1529 PetscInt vector_size, vector_local_size, block_size;
1530 PetscInt ownership_start, ownership_end;
1531 PetscInt expected_local = 0, expected_global = 0;
1532 PetscInt offrank_periodic_local = 0, offrank_periodic_global = 0;
1533 PetscBool is_seq_aij = PETSC_FALSE, is_mpi_aij = PETSC_FALSE;
1534
1535 PetscFunctionBeginUser;
1536 PetscCall(DMDAGetLocalInfo(user->fda, &info));
1537 PetscCall(MatGetSize(matrix, &matrix_rows, &matrix_cols));
1538 PetscCall(MatGetLocalSize(matrix, &local_rows, &local_cols));
1539 PetscCall(VecGetSize(user->Ucont, &vector_size));
1540 PetscCall(VecGetLocalSize(user->Ucont, &vector_local_size));
1541 PetscCall(VecGetOwnershipRange(user->Ucont, &ownership_start, &ownership_end));
1542 PetscCall(MatGetBlockSize(matrix, &block_size));
1543 PetscCall(PicurvAssertIntEqual(vector_size, matrix_rows,
1544 "point-block global row dimension"));
1545 PetscCall(PicurvAssertIntEqual(vector_size, matrix_cols,
1546 "point-block global column dimension"));
1547 PetscCall(PicurvAssertIntEqual(vector_local_size, local_rows,
1548 "point-block local row dimension"));
1549 PetscCall(PicurvAssertIntEqual(vector_local_size, local_cols,
1550 "point-block local column dimension"));
1551 PetscCall(PicurvAssertIntEqual(3, block_size, "point-block logical block size"));
1552 PetscCallMPI(MPI_Comm_size(PetscObjectComm((PetscObject)matrix), &comm_size));
1553 PetscCall(PetscObjectTypeCompare((PetscObject)matrix, MATSEQAIJ, &is_seq_aij));
1554 PetscCall(PetscObjectTypeCompare((PetscObject)matrix, MATMPIAIJ, &is_mpi_aij));
1555 PetscCall(PicurvAssertBool(
1556 comm_size == 1 ? is_seq_aij : is_mpi_aij,
1557 "point-block matrix must use the expected AIJ implementation"));
1558
1559 for (PetscInt k = info.zs; k < info.zs + info.zm; ++k) {
1560 for (PetscInt j = info.ys; j < info.ys + info.ym; ++j) {
1561 for (PetscInt i = info.xs; i < info.xs + info.xm; ++i) {
1562 for (PetscInt component = 0; component < 3; ++component) {
1563 PetscInt ri, rj, rk;
1565 user, i, j, k, component, &ri, &rj, &rk);
1566 expected_local += type == MOM_ROW_PHYSICAL ? 3 :
1567 type == MOM_ROW_PERIODIC_DUPLICATE ? 2 : 1;
1568 if (type == MOM_ROW_PERIODIC_DUPLICATE) {
1569 PetscInt representative;
1570 PetscCall(TestStencilToGlobal(user,
1571 (MatStencil){.i = ri, .j = rj, .k = rk, .c = component},
1572 &representative));
1573 if (representative < ownership_start || representative >= ownership_end)
1574 ++offrank_periodic_local;
1575 }
1576 }
1577 }
1578 }
1579 }
1580 PetscCallMPI(MPI_Allreduce(&expected_local, &expected_global, 1, MPIU_INT,
1581 MPI_SUM, PetscObjectComm((PetscObject)matrix)));
1582 PetscCallMPI(MPI_Allreduce(&offrank_periodic_local, &offrank_periodic_global,
1583 1, MPIU_INT, MPI_SUM,
1584 PetscObjectComm((PetscObject)matrix)));
1585 PetscCall(MatGetInfo(matrix, MAT_GLOBAL_SUM, &matrix_info));
1586 PetscCall(PicurvAssertRealNear((PetscReal)expected_global,
1587 (PetscReal)matrix_info.nz_allocated, 0.0,
1588 "point-block matrix must allocate exactly the classified scalar pattern"));
1589 PetscCall(PicurvAssertRealNear((PetscReal)expected_global,
1590 (PetscReal)matrix_info.nz_used, 0.0,
1591 "point-block assembly must insert every classified structural entry"));
1592 PetscCall(PicurvAssertRealNear(0.0, (PetscReal)matrix_info.mallocs, 0.0,
1593 "point-block insertion must not reallocate matrix storage"));
1594 if (require_offrank_periodic && comm_size > 1)
1595 PetscCall(PicurvAssertBool((PetscBool)(offrank_periodic_global > 0),
1596 "MPI periodic fixture must exercise off-rank preallocation"));
1597 PetscFunctionReturn(PETSC_SUCCESS);
1598}
1599
1600/**
1601 * @brief Verifies the point-block model and common preconditioning-engine wiring.
1602 * @return PETSc error code.
1603 */
1604static PetscErrorCode TestPointBlockPreconditionerEngine(void)
1605{
1606 SimCtx *simCtx = NULL;
1607 UserCtx *user = NULL;
1608 char tmpdir[PETSC_MAX_PATH_LEN] = "";
1610 MomentumPreconditionerDescription description = {
1613 0,
1614 0
1615 };
1616 MomentumPreconditionerEngine engine = {0};
1617 Mat preconditioning_matrix = NULL;
1618 Vec x = NULL, f = NULL;
1619 PetscInt block_size = 0, velocity_dof = 0;
1620 PetscReal matrix_norm = 0.0, reassembled_norm = 0.0, difference_norm = 0.0;
1621 MatStencil conditioned = {.i = 0, .j = 2, .k = 3, .c = 0};
1622 MatStencil homogeneous = {.i = 0, .j = 2, .k = 3, .c = 1};
1623 Mat saved_matrix = NULL, mffd = NULL;
1624 SNES snes = NULL;
1625 Vec direction = NULL, product = NULL, px = NULL;
1626 PetscReal px_norm = 0.0;
1627 KSP ksp = NULL;
1628 PC pc = NULL;
1629 Vec pc_rhs = NULL, pc_solution = NULL;
1630 MatInfo initial_allocation_info, repeated_allocation_info;
1631
1632 PetscFunctionBeginUser;
1633 PetscCall(BuildNewtonFixture(fixed_wall_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
1634 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
1635 PetscCall(VecDuplicate(user->Ucont, &x));
1636 PetscCall(VecDuplicate(user->Ucont, &f));
1637 PetscCall(VecCopy(user->Ucont, x));
1638 PetscCall(VecStrideSet(x, 0, 0.2));
1639 PetscCall(VecStrideSet(x, 1, 0.3));
1640 PetscCall(VecStrideSet(x, 2, 0.4));
1641 ctx.user = user;
1642 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f, &ctx));
1643 PetscCall(MomentumPreconditionerEngine_Create(user, NULL, &description, &engine));
1644 preconditioning_matrix = engine.preconditioning_matrix;
1645 PetscCall(PicurvAssertBool((PetscBool)(engine.model_ops == &frozen_momentum_point_block_ops),
1646 "engine must select the frozen-momentum model callbacks"));
1648 "point-block engine must own its separate matrix"));
1649 PetscCall(PicurvAssertBool((PetscBool)!engine.aliases_jacobian_operator,
1650 "point-block matrix must not alias the Jacobian operator"));
1651 PetscCall(MomentumPreconditionerEngine_Assemble(&engine, user, x));
1653 user, preconditioning_matrix, PETSC_FALSE));
1654 PetscCall(MatGetInfo(preconditioning_matrix, MAT_GLOBAL_SUM,
1655 &initial_allocation_info));
1656 PetscCall(MatNorm(preconditioning_matrix, NORM_FROBENIUS, &matrix_norm));
1657 PetscCall(PicurvAssertBool((PetscBool)(matrix_norm > 0.0),
1658 "model callback and common rows must insert matrix entries"));
1659 PetscCall(MatGetBlockSize(preconditioning_matrix, &block_size));
1660 PetscCall(PicurvAssertIntEqual(3, block_size, "point-block matrix block size"));
1661 PetscCall(DMDAGetInfo(user->fda, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
1662 &velocity_dof, NULL, NULL, NULL, NULL, NULL));
1663 PetscCall(PicurvAssertIntEqual(3, velocity_dof, "Newton velocity DMDA dof"));
1664
1665 /* The coefficient formulas define columns by the differentiated Ucont
1666 component. Check an asymmetric pair so a row/column transpose cannot pass. */
1667 {
1668 const MatStencil row_i = {.i = 2, .j = 2, .k = 2, .c = 0};
1669 const MatStencil row_j = {.i = 2, .j = 2, .k = 2, .c = 1};
1670 MatStencil col_i = row_i, col_j = row_j;
1671 PetscReal ***aj = NULL;
1672 PetscReal AJip, AJjp;
1673 PetscScalar dFi_dUj = 0.0, dFj_dUi = 0.0;
1674
1675 PetscCall(DMDAVecGetArrayRead(user->da, user->lAj, &aj));
1676 AJip = 0.5 * (aj[2][2][2] + aj[2][2][3]);
1677 AJjp = 0.5 * (aj[2][2][2] + aj[2][3][2]);
1678 PetscCall(DMDAVecRestoreArrayRead(user->da, user->lAj, &aj));
1679 PetscCall(PreconditionerMatrixStencilEntry(user, preconditioning_matrix,
1680 row_i, col_j, &dFi_dUj));
1681 PetscCall(PreconditionerMatrixStencilEntry(user, preconditioning_matrix,
1682 row_j, col_i, &dFj_dUi));
1683 PetscCall(PicurvAssertRealNear(0.5 * AJjp * 0.2,
1684 PetscRealPart(dFi_dUj), 1e-12,
1685 "row i, column j must differentiate with respect to U-j"));
1686 PetscCall(PicurvAssertRealNear(0.5 * AJip * 0.3,
1687 PetscRealPart(dFj_dUi), 1e-12,
1688 "row j, column i must differentiate with respect to U-i"));
1689 }
1690
1691 /* Both fixed categories are exact identity rows, with no same-cell coupling. */
1692 for (PetscInt cc = 0; cc < 3; ++cc) {
1693 MatStencil col = conditioned; col.c = cc;
1694 PetscScalar conditioned_value = 0.0, homogeneous_value = 0.0;
1695 PetscCall(PreconditionerMatrixStencilEntry(user, preconditioning_matrix,
1696 conditioned, col, &conditioned_value));
1697 col = homogeneous; col.c = cc;
1698 PetscCall(PreconditionerMatrixStencilEntry(user, preconditioning_matrix,
1699 homogeneous, col, &homogeneous_value));
1700 PetscCall(PicurvAssertRealNear(cc == conditioned.c ? 1.0 : 0.0,
1701 PetscRealPart(conditioned_value), 1e-14, "conditioned row must be exact identity"));
1702 PetscCall(PicurvAssertRealNear(cc == homogeneous.c ? 1.0 : 0.0,
1703 PetscRealPart(homogeneous_value), 1e-14, "homogeneous row must be exact identity"));
1704 }
1705
1706 /* A real residual and MFFD product cannot change subsequent assembly. */
1707 PetscCall(MatDuplicate(preconditioning_matrix, MAT_COPY_VALUES, &saved_matrix));
1708 PetscCall(SNESCreate(PETSC_COMM_WORLD, &snes));
1709 PetscCall(SNESSetDM(snes, user->fda));
1710 PetscCall(SNESSetFunction(snes, f, MomentumNewtonKrylov_FormResidual, &ctx));
1711 PetscCall(MatCreateSNESMF(snes, &mffd));
1712 PetscCall(VecDuplicate(x, &direction)); PetscCall(VecDuplicate(x, &product));
1713 PetscCall(VecSet(direction, 0.375));
1714 PetscCall(MomentumNewtonKrylov_FormResidual(snes, x, f, &ctx));
1715 PetscCall(MatMFFDSetBase(mffd, x, f));
1716 PetscCall(MatMult(mffd, direction, product));
1717 PetscCall(MomentumPreconditionerEngine_Assemble(&engine, user, x));
1718 PetscCall(MatAXPY(saved_matrix, -1.0, preconditioning_matrix, SAME_NONZERO_PATTERN));
1719 PetscCall(MatNorm(saved_matrix, NORM_FROBENIUS, &difference_norm));
1720 PetscCall(PicurvAssertRealNear(0.0, difference_norm, 1e-12,
1721 "assembly must be unchanged after residual and MFFD products"));
1722 PetscCall(VecDestroy(&product)); PetscCall(VecDestroy(&direction));
1723 PetscCall(MatDestroy(&mffd)); PetscCall(SNESDestroy(&snes)); PetscCall(MatDestroy(&saved_matrix));
1724
1725 PetscCall(VecDuplicate(x, &px));
1726 PetscCall(MatMult(preconditioning_matrix, x, px));
1727 PetscCall(VecNorm(px, NORM_2, &px_norm));
1728 PetscCall(PicurvAssertBool((PetscBool)(!PetscIsInfOrNanReal(px_norm) && px_norm > 0.0),
1729 "point-block product must be finite and nonzero"));
1730 PetscCall(VecDestroy(&px));
1731
1732 {
1733 PetscBool assembled = PETSC_FALSE;
1734 PetscCall(MatAssembled(preconditioning_matrix, &assembled));
1735 PetscCall(PicurvAssertBool(assembled, "engine must perform final matrix assembly"));
1736 }
1737 PetscCall(MatNorm(preconditioning_matrix, NORM_FROBENIUS, &matrix_norm));
1738 PetscCall(MatShift(preconditioning_matrix, 7.0));
1739 PetscCall(MomentumPreconditionerEngine_Assemble(&engine, user, x));
1740 PetscCall(MatGetInfo(preconditioning_matrix, MAT_GLOBAL_SUM,
1741 &repeated_allocation_info));
1742 PetscCall(PicurvAssertRealNear((PetscReal)initial_allocation_info.nz_allocated,
1743 (PetscReal)repeated_allocation_info.nz_allocated, 0.0,
1744 "repeated assembly must retain exact allocated storage"));
1745 PetscCall(PicurvAssertRealNear((PetscReal)initial_allocation_info.mallocs,
1746 (PetscReal)repeated_allocation_info.mallocs, 0.0,
1747 "repeated assembly must not add insertion reallocations"));
1748 PetscCall(MatNorm(preconditioning_matrix, NORM_FROBENIUS, &reassembled_norm));
1749 PetscCall(PicurvAssertRealNear(matrix_norm, reassembled_norm, 1e-12,
1750 "repeated engine assembly must clear old entries"));
1751 {
1752 PetscScalar value = 0.0;
1753 PetscCall(PreconditionerMatrixStencilEntry(user, preconditioning_matrix,
1754 conditioned, conditioned, &value));
1755 PetscCall(PicurvAssertRealNear(1.0, PetscRealPart(value), 1e-14,
1756 "repeated engine assembly must clear old entries"));
1757 }
1758 PetscCall(KSPCreate(PETSC_COMM_WORLD, &ksp));
1759 PetscCall(KSPSetOperators(ksp, preconditioning_matrix, preconditioning_matrix));
1760 PetscCall(KSPGetPC(ksp, &pc));
1761 PetscCall(MomentumPreconditionerEngine_ConfigurePetscPC(&engine, pc));
1762 PetscCall(MomentumPreconditionerEngine_ValidatePetscPC(&engine, pc));
1763 PetscCall(KSPSetUp(ksp));
1764 PetscCall(DMCreateGlobalVector(user->fda, &pc_rhs));
1765 PetscCall(DMCreateGlobalVector(user->fda, &pc_solution));
1766 PetscCall(VecSet(pc_rhs, 1.0));
1767 PetscCall(PCApply(pc, pc_rhs, pc_solution));
1768 PetscCall(VecDestroy(&pc_solution)); PetscCall(VecDestroy(&pc_rhs));
1769 PetscCall(KSPDestroy(&ksp));
1770 PetscCall(MomentumPreconditionerEngine_Destroy(&engine));
1771 PetscCall(PicurvAssertBool((PetscBool)(engine.preconditioning_matrix == NULL),
1772 "engine destroy must clear its owned matrix"));
1773 PetscCall(VecDestroy(&f)); PetscCall(VecDestroy(&x)); PetscCall(VecDestroy(&user->Rhs));
1774 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
1775 PetscFunctionReturn(PETSC_SUCCESS);
1776}
1777
1778/** @brief Proves that one periodic matrix row has only its exact +1/-1 pair. */
1779static PetscErrorCode AssertPeriodicPreconditionerRow(UserCtx *user, Mat matrix,
1780 MatStencil row, const char *message)
1781{
1782 AO ao = NULL;
1783 PetscInt ri, rj, rk, global_row, global_rep, lo, hi, local_checked = 0, checked = 0;
1784 PetscInt ncols = 0;
1785 const PetscInt *cols = NULL;
1786 const PetscScalar *values = NULL;
1787 MomentumRowType type;
1788
1789 PetscFunctionBeginUser;
1790 type = ClassifyMomentumRow(user, row.i, row.j, row.k, row.c, &ri, &rj, &rk);
1791 PetscCall(PicurvAssertIntEqual(MOM_ROW_PERIODIC_DUPLICATE, type, message));
1792 global_row = row.c + 3 * (row.i + user->info.mx * (row.j + user->info.my * row.k));
1793 PetscCall(DMDAGetAO(user->fda, &ao));
1794 PetscCall(AOApplicationToPetsc(ao, 1, &global_row));
1795 PetscCall(MatGetOwnershipRange(matrix, &lo, &hi));
1796 if (global_row >= lo && global_row < hi) {
1797 PetscBool found_self = PETSC_FALSE, found_rep = PETSC_FALSE;
1798 PetscInt nonzero_entries = 0;
1799 PetscCall(TestStencilToGlobal(user,
1800 (MatStencil){.i = ri, .j = rj, .k = rk, .c = row.c}, &global_rep));
1801 PetscCall(MatGetRow(matrix, global_row, &ncols, &cols, &values));
1802 for (PetscInt n = 0; n < ncols; ++n) {
1803 if (PetscAbsScalar(values[n]) <= 1e-14) continue;
1804 nonzero_entries++;
1805 if (cols[n] == global_row) {
1806 found_self = PETSC_TRUE;
1807 PetscCall(PicurvAssertRealNear(1.0, PetscRealPart(values[n]), 1e-14,
1808 "periodic row self entry"));
1809 } else if (cols[n] == global_rep) {
1810 found_rep = PETSC_TRUE;
1811 PetscCall(PicurvAssertRealNear(-1.0, PetscRealPart(values[n]), 1e-14,
1812 "periodic row representative entry"));
1813 } else {
1814 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_PLIB,
1815 "Periodic row contains an unintended nonzero column.");
1816 }
1817 }
1818 PetscCall(PicurvAssertIntEqual(2, nonzero_entries,
1819 "periodic row must contain exactly two numerical entries"));
1820 PetscCall(PicurvAssertBool((PetscBool)(found_self && found_rep), message));
1821 PetscCall(MatRestoreRow(matrix, global_row, &ncols, &cols, &values));
1822 local_checked = 1;
1823 }
1824 PetscCallMPI(MPI_Allreduce(&local_checked, &checked, 1, MPIU_INT, MPI_SUM, PETSC_COMM_WORLD));
1825 PetscCall(PicurvAssertIntEqual(1, checked, "periodic row must have exactly one matrix owner"));
1826 PetscFunctionReturn(PETSC_SUCCESS);
1827}
1828
1829/** @brief Verifies Jacobian creation/registration and baseline alias ownership. */
1830static PetscErrorCode TestJacobianInterfaceAndBaselineAlias(void)
1831{
1832 SimCtx *simCtx = NULL;
1833 UserCtx *user = NULL;
1834 char tmpdir[PETSC_MAX_PATH_LEN] = "";
1836 MomentumPreconditionerDescription description = {
1838 };
1839 SNES snes = NULL;
1840 Mat jacobian_operator = NULL, preconditioning_matrix = NULL;
1841 KSP ksp = NULL;
1842 PC pc = NULL;
1843 PetscInt rows = 0, cols = 0;
1844 const char *jacobian_prefix = NULL;
1845 const char *pc_type = NULL;
1846 PetscBool prefix_matches = PETSC_FALSE;
1847 PetscBool pc_is_none = PETSC_FALSE;
1848
1849 PetscFunctionBeginUser;
1850 PetscCall(BuildNewtonFixture(fixed_wall_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
1851 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
1852 PetscCall(SNESCreate(PETSC_COMM_WORLD, &snes));
1853 PetscCall(SNESSetDM(snes, user->fda));
1854 ctx.user = user;
1857 PetscCall(SNESSetFunction(snes, NULL, MomentumNewtonKrylov_FormResidual, &ctx));
1858 PetscCall(MomentumNewtonJacobian_Create(snes, &ctx.jacobian));
1859 PetscCall(MatGetOptionsPrefix(ctx.jacobian.jacobian_operator, &jacobian_prefix));
1860 PetscCall(PetscStrcmp(jacobian_prefix, "mom_nk_", &prefix_matches));
1861 PetscCall(PicurvAssertBool(prefix_matches,
1862 "Jacobian interface must apply the application prefix"));
1864 user, ctx.jacobian.jacobian_operator, &description, &ctx.preconditioning_engine));
1866 snes, &ctx.jacobian, &ctx.preconditioning_engine, &ctx));
1867 PetscCall(SNESGetKSP(snes, &ksp));
1868 PetscCall(KSPGetPC(ksp, &pc));
1870 &ctx.preconditioning_engine, pc));
1872 &ctx.preconditioning_engine, pc));
1873 PetscCall(PCGetType(pc, &pc_type));
1874 PetscCall(PetscStrcmp(pc_type, PCNONE, &pc_is_none));
1875 PetscCall(PicurvAssertBool(pc_is_none,
1876 "baseline engine must derive PETSc PCNONE"));
1877 PetscCall(SNESGetJacobian(snes, &jacobian_operator, &preconditioning_matrix, NULL, NULL));
1878 PetscCall(PicurvAssertBool(
1879 (PetscBool)(jacobian_operator == ctx.jacobian.jacobian_operator),
1880 "SNES must receive the Jacobian-interface MFFD operator"));
1881 PetscCall(PicurvAssertBool((PetscBool)(preconditioning_matrix == jacobian_operator),
1882 "baseline preconditioning matrix must alias the Jacobian"));
1884 "baseline engine must record matrix aliasing"));
1886 "baseline engine must not own the Jacobian alias"));
1888 PetscCall(MatGetSize(ctx.jacobian.jacobian_operator, &rows, &cols));
1889 PetscCall(PicurvAssertBool((PetscBool)(rows > 0 && cols > 0),
1890 "destroying an alias engine must preserve the Jacobian"));
1892 PetscCall(SNESDestroy(&snes));
1893 PetscCall(VecDestroy(&user->Rhs));
1894 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
1895 PetscFunctionReturn(PETSC_SUCCESS);
1896}
1897
1898/** @brief Exercises engine-owned periodic duplicate rows on every MPI layout. */
1899static PetscErrorCode TestPointBlockPeriodicAssembly(void)
1900{
1901 SimCtx *simCtx = NULL;
1902 UserCtx *user = NULL;
1903 char tmpdir[PETSC_MAX_PATH_LEN] = "";
1905 MomentumPreconditionerDescription description = {
1908 0,
1909 0
1910 };
1911 Vec x = NULL, f = NULL;
1912 PetscReal matrix_norm = 0.0;
1913
1914 PetscFunctionBeginUser;
1915 PetscCall(BuildNewtonFixture(periodic_xyz_bcs, &simCtx, &user, tmpdir, sizeof(tmpdir)));
1916 PetscCall(VecDuplicate(user->Ucont, &user->Rhs));
1917 PetscCall(VecDuplicate(user->Ucont, &x));
1918 PetscCall(VecDuplicate(user->Ucont, &f));
1919 PetscCall(VecCopy(user->Ucont, x));
1920 ctx.user = user;
1921 PetscCall(MomentumNewtonKrylov_FormResidual(NULL, x, f, &ctx));
1923 user, NULL, &description, &ctx.preconditioning_engine));
1926 user, ctx.preconditioning_engine.preconditioning_matrix, PETSC_TRUE));
1927 PetscCall(MatNorm(ctx.preconditioning_engine.preconditioning_matrix,
1928 NORM_FROBENIUS, &matrix_norm));
1929 PetscCall(PicurvAssertBool((PetscBool)(matrix_norm > 0.0),
1930 "periodic point-block assembly must produce a nonzero matrix"));
1931 PetscCall(AssertPeriodicPreconditionerRow(user,
1933 (MatStencil){.i = 0, .j = 2, .k = 3, .c = 0},
1934 "single-axis periodic row must contain exact +1/-1 entries"));
1935 PetscCall(AssertPeriodicPreconditionerRow(user,
1937 (MatStencil){.i = 0, .j = 0, .k = 3, .c = 1},
1938 "periodic intersection must contain exact +1/-1 entries"));
1939 PetscCall(AssertPeriodicPreconditionerRow(user,
1941 (MatStencil){.i = 0, .j = 0, .k = 0, .c = 2},
1942 "periodic origin intersection must contain exact +1/-1 entries"));
1944 PetscCall(VecDestroy(&f));
1945 PetscCall(VecDestroy(&x));
1946 PetscCall(VecDestroy(&user->Rhs));
1947 PetscCall(DestroyNewtonFixture(&simCtx, tmpdir));
1948 PetscFunctionReturn(PETSC_SUCCESS);
1949}
1950
1951/**
1952 * @brief Runs the focused Newton--Krylov unit suite.
1953 * @param argc Command-line argument count.
1954 * @param argv Command-line argument vector.
1955 * @return Process exit status.
1956 */
1957int main(int argc, char **argv)
1958{
1959 PetscErrorCode ierr;
1960 const PicurvTestCase cases[] = {
1961 {"residual-repeatability-and-input-integrity", TestResidualRepeatabilityAndInputIntegrity},
1962 {"constraint-rows", TestConstraintRows},
1963 {"fixed-constraint-derivatives-all-faces", TestFixedConstraintDerivativesAllFaces},
1964 {"inlet-outlet-constraint-derivatives", TestInletOutletConstraintDerivatives},
1965 {"periodic-constraint-derivatives-and-intersections", TestPeriodicConstraintDerivativesAndIntersections},
1966 {"matrix-free-derivative", TestMatrixFreeDerivative},
1967 {"whole-operator-direct-jacobian", TestWholeOperatorDirectJacobian},
1968 {"periodic-operator-has-no-zero-rows", TestPeriodicOperatorHasNoZeroRows},
1969 {"zero-iteration-structured-logging", TestZeroIterationStructuredLogging},
1970 {"flat-channel-bdf1-startup", TestFlatChannelStartup},
1971 {"restart-and-continuation-solve", TestRestartAndContinuationSolve},
1972 {"small-solve-and-rollback", TestSmallSolveAndRollback},
1973 {"free-slip-walls-solve", TestFreeSlipWallsSolve},
1974 {"two-d-solve-holds-masked-component", TestTwoDSolveHoldsMaskedComponent},
1975 {"unsupported-configuration-fails-before-allocation", TestUnsupportedConfigurationFailsBeforeAllocation},
1976 {"post-allocation-failure-cleanup", TestPostAllocationFailureCleanup},
1977 {"linearization-config-parsing", TestLinearizationConfigParsing},
1978 {"point-block-preconditioner-engine", TestPointBlockPreconditionerEngine},
1979 {"jacobian-interface-and-baseline-alias", TestJacobianInterfaceAndBaselineAlias},
1980 {"point-block-periodic-assembly", TestPointBlockPeriodicAssembly},
1981 };
1982
1983 ierr = PetscInitialize(&argc, &argv, NULL, "PICurv Newton Krylov tests");
1984 if (ierr) return (int)ierr;
1985 ierr = PicurvRunTests("unit-newton-krylov", cases, sizeof(cases) / sizeof(cases[0]));
1986 if (PetscFinalize()) return 1;
1987 return (int)ierr;
1988}
MomentumRowType
Classification of one staggered momentum row (location + component).
Definition Boundaries.h:191
@ MOM_ROW_FIXED_HOMOGENEOUS
Dummy/tangential row carrying no unknown at all.
Definition Boundaries.h:194
@ MOM_ROW_PHYSICAL
Independent unknown governed by the momentum equation.
Definition Boundaries.h:192
@ MOM_ROW_PERIODIC_DUPLICATE
Duplicate of a wrapped representative row (see ri, rj, rk).
Definition Boundaries.h:195
@ MOM_ROW_FIXED_CONDITIONED
Strong Dirichlet row; the value comes from ApplyBoundaryConditions().
Definition Boundaries.h:193
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:139
PetscErrorCode BoundaryCondition_Create(BCHandlerType handler_type, BoundaryCondition **new_bc_ptr)
(Private) Creates and configures a specific BoundaryCondition handler object.
Definition Boundaries.c:265
PetscErrorCode SynchronizePeriodicStaggeredFields(UserCtx *user, PetscInt num_fields, const FieldId field_ids[])
Synchronizes persistent component-staggered vector fields.
PetscErrorCode ApplyBoundaryConditions(UserCtx *user)
Main boundary-condition orchestrator executed during solver timestepping.
PetscErrorCode BoundarySystem_Destroy(UserCtx *user)
Cleans up and destroys all boundary system resources.
FieldId
Compile-time identity for a catalogued Eulerian field.
@ FIELD_ID_UCONT
PetscErrorCode InitializeEulerianState(SimCtx *simCtx)
High-level orchestrator to set the complete initial state of the Eulerian solver.
static PetscErrorCode MomentumNewtonJacobian_Register(SNES snes, MomentumNewtonJacobian *jacobian, MomentumPreconditionerEngine *engine, MomentumNewtonKrylovContext *ctx)
Registers the application orchestration callback and both SNES matrices.
static const MomentumPreconditionerModelOps frozen_momentum_point_block_ops
const MomentumPreconditionerModelOps * model_ops
static PetscErrorCode MomentumPreconditionerEngine_ConfigurePetscPC(MomentumPreconditionerEngine *engine, PC pc)
Applies the validated model/structure-to-PETSc-PC mapping.
static PetscErrorCode MomentumPreconditionerEngine_ValidatePetscPC(MomentumPreconditionerEngine *engine, PC pc)
Rejects raw options that select an unvalidated PETSc PC backend.
@ MOM_NK_PC_STRUCTURE_POINT_BLOCK
@ MOM_NK_PC_STRUCTURE_NONE
static PetscErrorCode MomentumPreconditionerEngine_Create(UserCtx *user, Mat jacobian_operator, const MomentumPreconditionerDescription *requested, MomentumPreconditionerEngine *engine)
Validates a model/structure and creates or aliases its matrix.
MomentumNewtonFiniteDifferenceMode finite_difference_mode
MomentumNewtonJacobianType type
static PetscErrorCode MomentumNewtonKrylov_ReadLinearizationConfig(MomentumNewtonJacobian *jacobian, MomentumPreconditionerDescription *description)
Reads application-owned Jacobian and preconditioner mathematics.
static PetscErrorCode MomentumPreconditionerEngine_Assemble(MomentumPreconditionerEngine *engine, UserCtx *user, Vec current_solution)
Runs model insertion, common row handling, and final assembly.
static PetscErrorCode MomentumNewtonJacobian_Destroy(MomentumNewtonJacobian *jacobian)
Destroys a partially or fully created Jacobian operator.
static PetscErrorCode MomentumNewtonJacobian_Create(SNES snes, MomentumNewtonJacobian *jacobian)
Creates the selected Jacobian operator; currently PETSc MFFD only.
MomentumPreconditionerStructure structure
@ MOM_NK_JACOBIAN_FINITE_DIFFERENCE
static PetscErrorCode MomentumPreconditionerEngine_Destroy(MomentumPreconditionerEngine *engine)
Destroys only a separately owned preconditioning matrix.
MomentumPreconditionerEngine preconditioning_engine
static PetscErrorCode MomentumNewtonKrylov_FormResidual(SNES snes, Vec X, Vec F, void *ctx)
Adapts a PETSc trial vector to the existing momentum residual path.
@ MOM_NK_FD_MODE_MATRIX_FREE
@ MOM_NK_PC_MODEL_FROZEN_MOMENTUM_JACOBIAN
@ MOM_NK_PC_MODEL_NONE
static PetscErrorCode PreconditionerMatrixStencilEntry(UserCtx *user, Mat preconditioning_matrix, MatStencil row, MatStencil col, PetscScalar *value)
Reads one DMDA-stencil matrix entry through collective basis vectors.
static const char * fixed_wall_bcs
static PetscErrorCode TestRestartAndContinuationSolve(void)
Verifies restarted Newton solves with both supported preconditioners.
static PetscErrorCode BuildNewtonFixture(const char *bcs, SimCtx **simCtx, UserCtx **user, char *tmpdir, size_t tmpdir_len)
Builds and initializes a small runtime context for Newton tests.
static PetscErrorCode TestUnsupportedConfigurationFailsBeforeAllocation(void)
Confirms unsupported features fail before workspace allocation.
static PetscErrorCode TestPeriodicOperatorHasNoZeroRows(void)
Audits every row of a complete operator containing periodic duplicates.
#define MomentumSolver_NewtonKrylov_Destroy
static PetscErrorCode TestSmallSolveAndRollback(void)
Exercises a converged solve, a committed unconverged solve, a non-finite rollback,...
static const char * periodic_xy_bcs
static PetscErrorCode TestConstraintRows(void)
Verifies fixed, periodic-duplicate, and interior residual rows.
static const char * periodic_xyz_bcs
static const char * periodic_x_bcs
static const char * periodic_y_bcs
#define MomentumSolver_NewtonKrylov
static PetscErrorCode TestPointBlockPreconditionerEngine(void)
Verifies the point-block model and common preconditioning-engine wiring.
static PetscErrorCode TestFlatChannelStartup(void)
Guards flat_channel's initial BDF1 residual and both shipped NK PCs.
static const char * periodic_z_bcs
static PetscErrorCode MeasureStoredDerivative(UserCtx *user, Vec x, PetscInt row_i, PetscInt row_j, PetscInt row_k, PetscInt row_component, PetscInt col_i, PetscInt col_j, PetscInt col_k, PetscInt col_component, PetscReal *derivative)
Finite-differences one callback row with respect to one stored unknown.
int main(int argc, char **argv)
Runs the focused Newton–Krylov unit suite.
static PetscErrorCode WriteNewtonPicSlice(const char *path)
Writes one static 5x5 PICSLICE profile used by the full runtime fixture.
static PetscErrorCode AssertPeriodicPreconditionerRow(UserCtx *user, Mat matrix, MatStencil row, const char *message)
Proves that one periodic matrix row has only its exact +1/-1 pair.
static PetscErrorCode CheckFlatChannelStartup(PetscBool use_point_block)
Exercises the straight-duct BDF1 startup path used by flat_channel.
static PetscBool OwnsStoredPoint(UserCtx *user, PetscInt i, PetscInt j, PetscInt k)
Returns whether this rank owns one global DMDA grid point.
static PetscErrorCode CheckSingleAxisPeriodicDerivatives(const char *bcs, PetscInt axis)
Checks one periodic configuration's endpoint derivatives on every component.
static PetscErrorCode TestResidualRepeatabilityAndInputIntegrity(void)
Verifies repeatable callback output and read-only trial input.
static PetscErrorCode TestTwoDSolveHoldsMaskedComponent(void)
A TwoD solve converges, preconditioned and not, and holds the masked component.
static PetscErrorCode TestPeriodicConstraintDerivativesAndIntersections(void)
Proves single-, double-, triple-, and mixed-boundary periodic equations.
static PetscErrorCode TestStencilToGlobal(UserCtx *user, MatStencil stencil, PetscInt *global_index)
Converts an in-domain or periodic-ghost DMDA stencil to PETSc ordering.
static PetscErrorCode AssertExactPointBlockMatrixAllocation(UserCtx *user, Mat matrix, PetscBool require_offrank_periodic)
Verifies the exact AIJ layout and preallocation derived from row classes.
static const char * geometric_periodic_bcs
static PetscErrorCode TestFreeSlipWallsSolve(void)
A Newton solve with free-slip walls converges, preconditioned and not.
static PetscErrorCode TestWholeOperatorDirectJacobian(void)
Forms the complete direct FD Jacobian, checks every row, and compares MFFD actions.
static PetscErrorCode TestLinearizationConfigParsing(void)
Verifies finalized application-owned linearization option parsing.
static PetscErrorCode TestFixedConstraintDerivativesAllFaces(void)
Proves unit derivatives for every nonperiodic stored-row category and face.
static PetscErrorCode TestPostAllocationFailureCleanup(void)
Verifies cleanup and rollback after an options failure following asset creation.
static PetscErrorCode CheckStoredDerivative(UserCtx *user, Vec x, PetscInt row_i, PetscInt row_j, PetscInt row_k, PetscInt row_component, PetscInt col_i, PetscInt col_j, PetscInt col_k, PetscInt col_component, PetscReal expected, PetscReal tolerance, const char *label)
Asserts one finite-differenced callback row derivative equals an expected value.
static PetscErrorCode TestJacobianInterfaceAndBaselineAlias(void)
Verifies Jacobian creation/registration and baseline alias ownership.
static PetscErrorCode AssertNewtonLog(const char *path, PetscInt expected_rows, const char *needle_a, const char *needle_b)
Checks a structured log's row count and required text after a collective solve.
static PetscErrorCode TestPointBlockPeriodicAssembly(void)
Exercises engine-owned periodic duplicate rows on every MPI layout.
static const char * parabolic_bcs
static PetscErrorCode GetStoredValue(UserCtx *user, Vec vec, PetscInt i, PetscInt j, PetscInt k, PetscInt component, PetscScalar *value)
Reads one globally indexed stored component on any MPI decomposition.
static PetscErrorCode TestMatrixFreeDerivative(void)
Compares PETSc's matrix-free action with direct differencing.
static PetscErrorCode PerturbStoredValue(UserCtx *user, Vec vec, PetscInt i, PetscInt j, PetscInt k, PetscInt component, PetscScalar delta)
Adds a scalar perturbation to one stored staggered component.
static const char * free_slip_wall_bcs
static PetscErrorCode DestroyNewtonFixture(SimCtx **simCtx, char *tmpdir)
Destroys a Newton test fixture and its temporary files.
static PetscErrorCode TestZeroIterationStructuredLogging(void)
Verifies the six-wall zero-velocity case logs zero Newton/Krylov work.
static PetscErrorCode BuildMinimalWallOperatorFixture(SimCtx **simCtx, UserCtx **user, PetscBool x_periodic)
Builds a compact all-wall operator fixture through real boundary handlers.
static PetscErrorCode CheckResidualRepeatabilityForBC(const char *bcs, const char *label)
Checks callback repeatability, diagnostic-state independence, and X integrity.
static PetscErrorCode TestInletOutletConstraintDerivatives(void)
Proves admitted inlet and outlet face-normal rows have unit self derivatives.
PetscErrorCode PicurvMakeTempDir(char *path, size_t path_len)
Creates a unique temporary directory for one test case.
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 PicurvDestroyRuntimeContext(SimCtx **simCtx_ptr)
Finalizes and frees a runtime context built by PicurvBuildTinyRuntimeContext.
PetscErrorCode PicurvRunTests(const char *suite_name, const PicurvTestCase *cases, size_t case_count)
Runs a named C test suite and prints pass/fail progress markers.
PetscErrorCode PicurvBuildTinyRuntimeContext(const char *bcs_contents, PetscBool enable_particles, SimCtx **simCtx_out, UserCtx **user_out, char *tmpdir, size_t tmpdir_len)
Builds a tiny runtime context through the real setup path for behavior-level tests.
PetscErrorCode PicurvAssertIntEqual(PetscInt expected, PetscInt actual, const char *context)
Asserts that two integer values are equal.
PetscErrorCode PicurvAssertBool(PetscBool value, const char *context)
Asserts that one boolean condition is true.
PetscErrorCode PicurvRemoveTempDir(const char *path)
Recursively removes a temporary directory created by PicurvMakeTempDir.
Shared declarations for the PICurv C test fixture and assertion layer.
Named test case descriptor consumed by PicurvRunTests.
PetscBool mom_nk_monitor_history
Definition variables.h:975
PetscReal FarFluxInSum
Definition variables.h:1016
PetscInt movefsi
Definition variables.h:950
@ PERIODIC
Definition variables.h:325
@ WALL
Definition variables.h:319
PetscBool continueMode
Definition variables.h:935
PetscInt moveframe
Definition variables.h:951
PetscInt TwoD
Definition variables.h:951
PetscReal FarFluxOutSum
Definition variables.h:1016
BoundaryFaceConfig boundary_faces[6]
Definition variables.h:1182
PetscInt block_number
Definition variables.h:1009
Vec lNvert
Definition variables.h:1197
PetscReal FluxOutSum
Definition variables.h:1016
PetscBool mom_last_converged
Definition variables.h:971
@ BC_HANDLER_PERIODIC_GEOMETRIC
Definition variables.h:352
@ BC_HANDLER_PERIODIC_DRIVEN_CONSTANT_FLUX
Definition variables.h:354
@ BC_HANDLER_WALL_NOSLIP
Definition variables.h:345
BCHandlerType handler_type
Definition variables.h:427
Vec Ucont
Definition variables.h:1197
PetscInt StartStep
Definition variables.h:926
PetscInt rotatefsi
Refused at setup: immersed boundaries and moving bodies are not implemented.
Definition variables.h:950
@ MOMENTUM_SOLVER_NEWTON_KRYLOV
Definition variables.h:732
PetscScalar x
Definition variables.h:145
char log_dir[PETSC_MAX_PATH_LEN]
Definition variables.h:941
PetscReal FluxInSum
Definition variables.h:1016
PetscScalar z
Definition variables.h:145
Vec Ucont_o
Definition variables.h:1204
struct MomentumNewtonKrylovContext * momentumNewtonKrylov
Definition variables.h:1235
Vec Ucont_rm1
Definition variables.h:1207
PetscInt step
Definition variables.h:924
DMDALocalInfo info
Definition variables.h:1169
Vec lUcat
Definition variables.h:1197
PetscScalar y
Definition variables.h:145
Vec Nvert
Definition variables.h:1197
BCType mathematical_type
Definition variables.h:426
PetscInt rotateframe
moveframe/rotateframe are refused at setup.
Definition variables.h:951
PetscInt immersed
Definition variables.h:950
BCFace
Identifies the six logical faces of a structured computational block.
Definition variables.h:308
@ BC_FACE_NEG_X
Definition variables.h:309
@ BC_FACE_POS_X
Definition variables.h:309
A 3D point or vector with PetscScalar components.
Definition variables.h:144
The master context for the entire simulation.
Definition variables.h:916
User-defined context containing data specific to a single computational grid level.
Definition variables.h:1157