PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
test_logging.c
Go to the documentation of this file.
1/**
2 * @file test_logging.c
3 * @brief C unit tests for runtime log-level, allow-list, conversion, and profiling helpers.
4 */
5
6#include "test_support.h"
7
8#include "logging.h"
9#include "interpolation.h"
10#include "setup.h"
12#include "statistics_window.h"
13
14#include <fcntl.h>
15#include <math.h>
16#include <stdio.h>
17#include <stdlib.h>
18#include <string.h>
19#include <unistd.h>
20/**
21 * @brief Asserts that one text file contains a required substring.
22 */
23
24static PetscErrorCode AssertFileContains(const char *path, const char *needle, const char *context)
25{
26 FILE *fp = NULL;
27 long file_size = 0;
28 char *buffer = NULL;
29
30 PetscFunctionBeginUser;
31 fp = fopen(path, "rb");
32 if (!fp) {
33 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to open '%s' for assertion.", path);
34 }
35 if (fseek(fp, 0, SEEK_END) != 0) {
36 fclose(fp);
37 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to seek '%s'.", path);
38 }
39 file_size = ftell(fp);
40 if (file_size < 0) {
41 fclose(fp);
42 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to measure '%s'.", path);
43 }
44 if (fseek(fp, 0, SEEK_SET) != 0) {
45 fclose(fp);
46 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to rewind '%s'.", path);
47 }
48
49 PetscCall(PetscMalloc1((size_t)file_size + 1, &buffer));
50 if (file_size > 0) {
51 size_t bytes_read = fread(buffer, 1, (size_t)file_size, fp);
52 if (bytes_read != (size_t)file_size) {
53 fclose(fp);
54 PetscCall(PetscFree(buffer));
55 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to read '%s'.", path);
56 }
57 }
58 buffer[file_size] = '\0';
59 fclose(fp);
60
61 PetscCall(PicurvAssertBool((PetscBool)(strstr(buffer, needle) != NULL), context));
62 PetscCall(PetscFree(buffer));
63 PetscFunctionReturn(0);
64}
65/**
66 * @brief Asserts that one text file does not contain an excluded substring.
67 */
68
69static PetscErrorCode AssertFileNotContains(const char *path, const char *needle, const char *context)
70{
71 FILE *fp = NULL;
72 long file_size = 0;
73 char *buffer = NULL;
74
75 PetscFunctionBeginUser;
76 fp = fopen(path, "rb");
77 if (!fp) {
78 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to open '%s' for assertion.", path);
79 }
80 if (fseek(fp, 0, SEEK_END) != 0) {
81 fclose(fp);
82 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to seek '%s'.", path);
83 }
84 file_size = ftell(fp);
85 if (file_size < 0) {
86 fclose(fp);
87 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to measure '%s'.", path);
88 }
89 if (fseek(fp, 0, SEEK_SET) != 0) {
90 fclose(fp);
91 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to rewind '%s'.", path);
92 }
93
94 PetscCall(PetscMalloc1((size_t)file_size + 1, &buffer));
95 if (file_size > 0) {
96 size_t bytes_read = fread(buffer, 1, (size_t)file_size, fp);
97 if (bytes_read != (size_t)file_size) {
98 fclose(fp);
99 PetscCall(PetscFree(buffer));
100 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to read '%s'.", path);
101 }
102 }
103 buffer[file_size] = '\0';
104 fclose(fp);
105
106 PetscCall(PicurvAssertBool((PetscBool)(strstr(buffer, needle) == NULL), context));
107 PetscCall(PetscFree(buffer));
108 PetscFunctionReturn(0);
109}
110
111/**
112 * @brief Reads the CSV header and the requested 1-based data row from a text file.
113 */
114static PetscErrorCode ReadCsvHeaderAndRow(const char *path,
115 PetscInt row_index,
116 char *header,
117 size_t header_len,
118 char *row,
119 size_t row_len)
120{
121 FILE *fp = NULL;
122 PetscInt current_row = 0;
123
124 PetscFunctionBeginUser;
125 PetscCheck(path != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "CSV path cannot be NULL.");
126 PetscCheck(header != NULL && header_len > 0, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "CSV header buffer cannot be NULL or empty.");
127 PetscCheck(row != NULL && row_len > 0, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "CSV row buffer cannot be NULL or empty.");
128 PetscCheck(row_index >= 1, PETSC_COMM_SELF, PETSC_ERR_ARG_OUTOFRANGE, "CSV row index must be >= 1.");
129
130 fp = fopen(path, "r");
131 PetscCheck(fp != NULL, PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to open CSV '%s'.", path);
132 PetscCheck(fgets(header, (int)header_len, fp) != NULL, PETSC_COMM_SELF, PETSC_ERR_FILE_READ, "CSV header missing in '%s'.", path);
133
134 while (fgets(row, (int)row_len, fp) != NULL) {
135 current_row++;
136 if (current_row == row_index) {
137 fclose(fp);
138 PetscFunctionReturn(0);
139 }
140 }
141
142 fclose(fp);
143 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_READ, "CSV '%s' does not contain data row %d.", path, (int)row_index);
144}
145
146/**
147 * @brief Returns the zero-based column index for one CSV header field.
148 */
149static PetscErrorCode CsvFindColumnIndex(const char *header, const char *column_name, PetscInt *index_out)
150{
151 char local_header[4096];
152 char *saveptr = NULL;
153 char *token = NULL;
154 PetscInt index = 0;
155
156 PetscFunctionBeginUser;
157 PetscCheck(header != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "CSV header cannot be NULL.");
158 PetscCheck(column_name != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "CSV column name cannot be NULL.");
159 PetscCheck(index_out != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "CSV column index output cannot be NULL.");
160 PetscCheck(strlen(header) < sizeof(local_header), PETSC_COMM_SELF, PETSC_ERR_ARG_SIZ, "CSV header is too long for the local parser buffer.");
161
162 PetscCall(PetscStrncpy(local_header, header, sizeof(local_header)));
163 token = strtok_r(local_header, ",\r\n", &saveptr);
164 while (token != NULL) {
165 if (strcmp(token, column_name) == 0) {
166 *index_out = index;
167 PetscFunctionReturn(0);
168 }
169 token = strtok_r(NULL, ",\r\n", &saveptr);
170 index++;
171 }
172
173 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_ARG_WRONG, "CSV column '%s' was not found.", column_name);
174}
175
176/**
177 * @brief Extracts one CSV cell as text by header name.
178 */
179static PetscErrorCode CsvGetColumnText(const char *header,
180 const char *row,
181 const char *column_name,
182 char *value,
183 size_t value_len)
184{
185 char local_row[4096];
186 char *saveptr = NULL;
187 char *token = NULL;
188 PetscInt target_index = -1;
189 PetscInt index = 0;
190
191 PetscFunctionBeginUser;
192 PetscCheck(row != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "CSV row cannot be NULL.");
193 PetscCheck(value != NULL && value_len > 0, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "CSV value buffer cannot be NULL or empty.");
194 PetscCheck(strlen(row) < sizeof(local_row), PETSC_COMM_SELF, PETSC_ERR_ARG_SIZ, "CSV row is too long for the local parser buffer.");
195
196 PetscCall(CsvFindColumnIndex(header, column_name, &target_index));
197 PetscCall(PetscStrncpy(local_row, row, sizeof(local_row)));
198
199 token = strtok_r(local_row, ",\r\n", &saveptr);
200 while (token != NULL) {
201 if (index == target_index) {
202 PetscCall(PetscStrncpy(value, token, value_len));
203 PetscFunctionReturn(0);
204 }
205 token = strtok_r(NULL, ",\r\n", &saveptr);
206 index++;
207 }
208
209 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_ARG_WRONG, "CSV row is missing column '%s'.", column_name);
210}
211
212/**
213 * @brief Extracts one CSV cell as an integer by header name.
214 */
215static PetscErrorCode CsvGetColumnInt(const char *header, const char *row, const char *column_name, PetscInt *value_out)
216{
217 char text[256];
218 char *endptr = NULL;
219 long parsed = 0;
220
221 PetscFunctionBeginUser;
222 PetscCheck(value_out != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "CSV integer output cannot be NULL.");
223 PetscCall(CsvGetColumnText(header, row, column_name, text, sizeof(text)));
224 parsed = strtol(text, &endptr, 10);
225 PetscCheck(endptr != text, PETSC_COMM_SELF, PETSC_ERR_ARG_WRONG, "CSV column '%s' did not contain an integer.", column_name);
226 *value_out = (PetscInt)parsed;
227 PetscFunctionReturn(0);
228}
229
230/**
231 * @brief Extracts one CSV cell as a real by header name.
232 */
233static PetscErrorCode CsvGetColumnReal(const char *header, const char *row, const char *column_name, PetscReal *value_out)
234{
235 char text[256];
236 char *endptr = NULL;
237 double parsed = 0.0;
238
239 PetscFunctionBeginUser;
240 PetscCheck(value_out != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "CSV real output cannot be NULL.");
241 PetscCall(CsvGetColumnText(header, row, column_name, text, sizeof(text)));
242 parsed = strtod(text, &endptr);
243 PetscCheck(endptr != text, PETSC_COMM_SELF, PETSC_ERR_ARG_WRONG, "CSV column '%s' did not contain a real value.", column_name);
244 *value_out = (PetscReal)parsed;
245 PetscFunctionReturn(0);
246}
247
248/**
249 * @brief Reads the column header and the requested 1-based data row from a
250 * pipe-delimited solution-convergence log file.
251 *
252 * Lines starting with '=' (banner) or '-' (separator) are skipped. The first
253 * non-skipped line is treated as the column header; subsequent non-skipped
254 * lines are data rows numbered from 1.
255 */
256static PetscErrorCode ReadLogHeaderAndRow(const char *path,
257 PetscInt row_index,
258 char *header,
259 size_t header_len,
260 char *row,
261 size_t row_len)
262{
263 FILE *fp = NULL;
264 PetscInt current_row = 0;
265 char line[8192];
266 PetscBool header_found = PETSC_FALSE;
267
268 PetscFunctionBeginUser;
269 PetscCheck(path != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Log path cannot be NULL.");
270 PetscCheck(header != NULL && header_len > 0, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Log header buffer cannot be NULL.");
271 PetscCheck(row != NULL && row_len > 0, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Log row buffer cannot be NULL.");
272 PetscCheck(row_index >= 1, PETSC_COMM_SELF, PETSC_ERR_ARG_OUTOFRANGE, "Log row index must be >= 1.");
273
274 fp = fopen(path, "r");
275 PetscCheck(fp != NULL, PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to open log '%s'.", path);
276
277 while (fgets(line, sizeof(line), fp) != NULL) {
278 if (line[0] == '=' || line[0] == '-' || line[0] == '\n' || line[0] == '\r') continue;
279 if (!header_found) {
280 PetscCall(PetscStrncpy(header, line, header_len));
281 header_found = PETSC_TRUE;
282 continue;
283 }
284 current_row++;
285 if (current_row == row_index) {
286 PetscCall(PetscStrncpy(row, line, row_len));
287 fclose(fp);
288 PetscFunctionReturn(0);
289 }
290 }
291
292 fclose(fp);
293 if (!header_found) SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_READ, "Log '%s' has no column header.", path);
294 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_FILE_READ, "Log '%s' does not contain data row %d.", path, (int)row_index);
295}
296
297/**
298 * @brief Returns the zero-based column index for one pipe-delimited header field.
299 */
300static PetscErrorCode LogFindColumnIndex(const char *header, const char *column_name, PetscInt *index_out)
301{
302 char local_header[8192];
303 char *saveptr = NULL;
304 char *token = NULL;
305 PetscInt index = 0;
306
307 PetscFunctionBeginUser;
308 PetscCheck(header != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Log header cannot be NULL.");
309 PetscCheck(column_name != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Log column name cannot be NULL.");
310 PetscCheck(index_out != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Log column index output cannot be NULL.");
311
312 PetscCall(PetscStrncpy(local_header, header, sizeof(local_header)));
313 token = strtok_r(local_header, "|\r\n", &saveptr);
314 while (token != NULL) {
315 while (*token == ' ') token++;
316 char *end = token + strlen(token) - 1;
317 while (end > token && (*end == ' ' || *end == '\r' || *end == '\n')) end--;
318 *(end + 1) = '\0';
319 if (strcmp(token, column_name) == 0) {
320 *index_out = index;
321 PetscFunctionReturn(0);
322 }
323 token = strtok_r(NULL, "|\r\n", &saveptr);
324 index++;
325 }
326 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_ARG_WRONG, "Log column '%s' was not found.", column_name);
327}
328
329/**
330 * @brief Extracts one pipe-delimited log cell as text by header name.
331 */
332static PetscErrorCode LogGetColumnText(const char *header,
333 const char *row,
334 const char *column_name,
335 char *value,
336 size_t value_len)
337{
338 char local_row[8192];
339 char *saveptr = NULL;
340 char *token = NULL;
341 PetscInt target_index = -1;
342 PetscInt index = 0;
343
344 PetscFunctionBeginUser;
345 PetscCheck(row != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Log row cannot be NULL.");
346 PetscCheck(value != NULL && value_len > 0, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Log value buffer cannot be NULL.");
347
348 PetscCall(LogFindColumnIndex(header, column_name, &target_index));
349 PetscCall(PetscStrncpy(local_row, row, sizeof(local_row)));
350
351 token = strtok_r(local_row, "|\r\n", &saveptr);
352 while (token != NULL) {
353 if (index == target_index) {
354 while (*token == ' ') token++;
355 char *end = token + strlen(token) - 1;
356 while (end > token && (*end == ' ' || *end == '\r' || *end == '\n')) end--;
357 *(end + 1) = '\0';
358 PetscCall(PetscStrncpy(value, token, value_len));
359 PetscFunctionReturn(0);
360 }
361 token = strtok_r(NULL, "|\r\n", &saveptr);
362 index++;
363 }
364 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_ARG_WRONG, "Log row is missing column '%s'.", column_name);
365}
366
367/**
368 * @brief Extracts one pipe-delimited log cell as an integer by header name.
369 */
370static PetscErrorCode LogGetColumnInt(const char *header, const char *row, const char *column_name, PetscInt *value_out)
371{
372 char text[256];
373 char *endptr = NULL;
374 long parsed = 0;
375
376 PetscFunctionBeginUser;
377 PetscCheck(value_out != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Log integer output cannot be NULL.");
378 PetscCall(LogGetColumnText(header, row, column_name, text, sizeof(text)));
379 parsed = strtol(text, &endptr, 10);
380 PetscCheck(endptr != text, PETSC_COMM_SELF, PETSC_ERR_ARG_WRONG, "Log column '%s' did not contain an integer.", column_name);
381 *value_out = (PetscInt)parsed;
382 PetscFunctionReturn(0);
383}
384
385/**
386 * @brief Extracts one pipe-delimited log cell as a real by header name.
387 */
388static PetscErrorCode LogGetColumnReal(const char *header, const char *row, const char *column_name, PetscReal *value_out)
389{
390 char text[256];
391 char *endptr = NULL;
392 double parsed = 0.0;
393
394 PetscFunctionBeginUser;
395 PetscCheck(value_out != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Log real output cannot be NULL.");
396 PetscCall(LogGetColumnText(header, row, column_name, text, sizeof(text)));
397 parsed = strtod(text, &endptr);
398 PetscCheck(endptr != text, PETSC_COMM_SELF, PETSC_ERR_ARG_WRONG, "Log column '%s' did not contain a real value.", column_name);
399 *value_out = (PetscReal)parsed;
400 PetscFunctionReturn(0);
401}
402
403typedef PetscErrorCode (*CapturedLoggingFn)(UserCtx *user, SimCtx *simCtx, void *ctx);
404
409
410/**
411 * @brief Captures stdout emitted by one logging helper into a temporary file-backed buffer.
412 */
413static PetscErrorCode CaptureLoggingOutput(UserCtx *user,
414 SimCtx *simCtx,
416 void *ctx,
417 char *captured,
418 size_t captured_len)
419{
420 char tmpdir[PETSC_MAX_PATH_LEN];
421 char capture_path[PETSC_MAX_PATH_LEN];
422 FILE *capture_file = NULL;
423 int saved_stdout = -1;
424 int capture_fd = -1;
425 size_t bytes_read = 0;
426 PetscErrorCode ierr;
427
428 PetscFunctionBeginUser;
429 PetscCheck(fn != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Capture callback cannot be NULL.");
430 PetscCheck(captured != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Capture buffer cannot be NULL.");
431 PetscCheck(captured_len > 0, PETSC_COMM_SELF, PETSC_ERR_ARG_SIZ, "Capture buffer must be non-empty.");
432
433 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
434 PetscCall(PetscSNPrintf(capture_path, sizeof(capture_path), "%s/logging.out", tmpdir));
435
436 fflush(stdout);
437 saved_stdout = dup(STDOUT_FILENO);
438 PetscCheck(saved_stdout >= 0, PETSC_COMM_SELF, PETSC_ERR_SYS, "dup(STDOUT_FILENO) failed.");
439 capture_fd = open(capture_path, O_CREAT | O_TRUNC | O_WRONLY, 0600);
440 PetscCheck(capture_fd >= 0, PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to open capture file '%s'.", capture_path);
441 PetscCheck(dup2(capture_fd, STDOUT_FILENO) >= 0, PETSC_COMM_SELF, PETSC_ERR_SYS, "dup2() failed while redirecting stdout.");
442 close(capture_fd);
443 capture_fd = -1;
444
445 ierr = fn(user, simCtx, ctx);
446 fflush(stdout);
447 PetscCheck(dup2(saved_stdout, STDOUT_FILENO) >= 0, PETSC_COMM_SELF, PETSC_ERR_SYS, "dup2() failed while restoring stdout.");
448 close(saved_stdout);
449 saved_stdout = -1;
450 PetscCall(ierr);
451
452 capture_file = fopen(capture_path, "r");
453 PetscCheck(capture_file != NULL, PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to read capture file '%s'.", capture_path);
454 bytes_read = fread(captured, 1, captured_len - 1, capture_file);
455 captured[bytes_read] = '\0';
456 fclose(capture_file);
457 PetscCall(PicurvRemoveTempDir(tmpdir));
458 PetscFunctionReturn(0);
459}
460
461/**
462 * @brief Adapts `LOG_PARTICLE_FIELDS()` to the generic stdout-capture callback shape.
463 */
464static PetscErrorCode InvokeParticleFieldLog(UserCtx *user, SimCtx *simCtx, void *ctx)
465{
466 PetscInt print_interval = *((PetscInt *)ctx);
467
468 PetscFunctionBeginUser;
469 (void)simCtx;
470 PetscCall(LOG_PARTICLE_FIELDS(user, print_interval));
471 PetscFunctionReturn(0);
472}
473
474/**
475 * @brief Adapts `EmitParticleConsoleSnapshot()` to the generic stdout-capture callback shape.
476 */
477static PetscErrorCode InvokeParticleConsoleSnapshot(UserCtx *user, SimCtx *simCtx, void *ctx)
478{
479 PetscInt step = *((PetscInt *)ctx);
480
481 PetscFunctionBeginUser;
482 PetscCall(EmitParticleConsoleSnapshot(user, simCtx, step));
483 PetscFunctionReturn(0);
484}
485
486/**
487 * @brief Adapts `EmitStatisticsConsoleSnapshot()` to the generic stdout-capture callback shape.
488 */
489static PetscErrorCode InvokeStatisticsConsoleSnapshot(UserCtx *user, SimCtx *simCtx, void *ctx)
490{
491 PetscInt step = *((PetscInt *)ctx);
492
493 PetscFunctionBeginUser;
494 PetscCall(EmitStatisticsConsoleSnapshot(user, simCtx, step));
495 PetscFunctionReturn(0);
496}
497
498/**
499 * @brief Adapts `LOG_FIELD_ANATOMY()` to the generic stdout-capture callback shape.
500 */
501static PetscErrorCode InvokeFieldAnatomyLog(UserCtx *user, SimCtx *simCtx, void *ctx)
502{
503 const AnatomyCaptureCtx *anatomy_ctx = (const AnatomyCaptureCtx *)ctx;
504
505 PetscFunctionBeginUser;
506 (void)simCtx;
507 PetscCall(LOG_FIELD_ANATOMY(user, anatomy_ctx->field_id, anatomy_ctx->stage_name));
508 PetscFunctionReturn(0);
509}
510
511/**
512 * @brief Creates a small particle-bearing runtime fixture used by logging tests.
513 */
514static PetscErrorCode SeedLoggingParticleFixture(SimCtx **simCtx_out, UserCtx **user_out)
515{
516 PetscReal *positions = NULL;
517 PetscReal *velocities = NULL;
518 PetscReal *weights = NULL;
519 PetscInt *cell_ids = NULL;
520 PetscInt *status = NULL;
521 PetscReal *psi = NULL;
522
523 PetscFunctionBeginUser;
524 PetscCall(PicurvCreateMinimalContexts(simCtx_out, user_out, 4, 4, 4));
525 PetscCall(PicurvCreateSwarmPair(*user_out, 2, "ske"));
526 (*simCtx_out)->np = 2;
527 (*simCtx_out)->particleConsoleOutputFreq = 2;
528 (*simCtx_out)->LoggingFrequency = 1;
529
530 PetscCall(DMSwarmGetField((*user_out)->swarm, "position", NULL, NULL, (void **)&positions));
531 PetscCall(DMSwarmGetField((*user_out)->swarm, "velocity", NULL, NULL, (void **)&velocities));
532 PetscCall(DMSwarmGetField((*user_out)->swarm, "weight", NULL, NULL, (void **)&weights));
533 PetscCall(DMSwarmGetField((*user_out)->swarm, "DMSwarm_CellID", NULL, NULL, (void **)&cell_ids));
534 PetscCall(DMSwarmGetField((*user_out)->swarm, "DMSwarm_location_status", NULL, NULL, (void **)&status));
535 PetscCall(DMSwarmGetField((*user_out)->swarm, "Psi", NULL, NULL, (void **)&psi));
536
537 positions[0] = 0.25; positions[1] = 0.50; positions[2] = 0.75;
538 positions[3] = 0.50; positions[4] = 0.50; positions[5] = 0.50;
539 velocities[0] = 1.0; velocities[1] = 2.0; velocities[2] = 3.0;
540 velocities[3] = 4.0; velocities[4] = 5.0; velocities[5] = 6.0;
541 weights[0] = 0.2; weights[1] = 0.3; weights[2] = 0.4;
542 weights[3] = 0.5; weights[4] = 0.5; weights[5] = 0.5;
543 cell_ids[0] = 0; cell_ids[1] = 0; cell_ids[2] = 0;
544 cell_ids[3] = 1; cell_ids[4] = 1; cell_ids[5] = 1;
545 status[0] = ACTIVE_AND_LOCATED;
546 status[1] = ACTIVE_AND_LOCATED;
547 psi[0] = 1.0;
548 psi[1] = 3.0;
549
550 PetscCall(DMSwarmRestoreField((*user_out)->swarm, "Psi", NULL, NULL, (void **)&psi));
551 PetscCall(DMSwarmRestoreField((*user_out)->swarm, "DMSwarm_location_status", NULL, NULL, (void **)&status));
552 PetscCall(DMSwarmRestoreField((*user_out)->swarm, "DMSwarm_CellID", NULL, NULL, (void **)&cell_ids));
553 PetscCall(DMSwarmRestoreField((*user_out)->swarm, "weight", NULL, NULL, (void **)&weights));
554 PetscCall(DMSwarmRestoreField((*user_out)->swarm, "velocity", NULL, NULL, (void **)&velocities));
555 PetscCall(DMSwarmRestoreField((*user_out)->swarm, "position", NULL, NULL, (void **)&positions));
556 PetscFunctionReturn(0);
557}
558
559/**
560 * @brief Fills one Cartesian velocity field with a uniform constant state.
561 */
562static PetscErrorCode SetUniformVelocityField(UserCtx *user, Vec field, PetscReal ux, PetscReal uy, PetscReal uz)
563{
564 Cmpnts ***arr = NULL;
565
566 PetscFunctionBeginUser;
567 PetscCheck(user != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "UserCtx cannot be NULL.");
568 PetscCheck(field != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Velocity field cannot be NULL.");
569
570 PetscCall(DMDAVecGetArray(user->fda, field, &arr));
571 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k) {
572 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j) {
573 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
574 arr[k][j][i].x = ux;
575 arr[k][j][i].y = uy;
576 arr[k][j][i].z = uz;
577 }
578 }
579 }
580 PetscCall(DMDAVecRestoreArray(user->fda, field, &arr));
581 PetscFunctionReturn(0);
582}
583
584/**
585 * @brief Fills one scalar field with a uniform constant state.
586 */
587static PetscErrorCode SetUniformScalarField(UserCtx *user, Vec field, PetscReal value)
588{
589 PetscReal ***arr = NULL;
590
591 PetscFunctionBeginUser;
592 PetscCheck(user != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "UserCtx cannot be NULL.");
593 PetscCheck(field != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Scalar field cannot be NULL.");
594
595 PetscCall(DMDAVecGetArray(user->da, field, &arr));
596 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k) {
597 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j) {
598 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
599 arr[k][j][i] = value;
600 }
601 }
602 }
603 PetscCall(DMDAVecRestoreArray(user->da, field, &arr));
604 PetscFunctionReturn(0);
605}
606/**
607 * @brief Fills one Cartesian velocity field with u=0, v=v_amp*sin(2π*k/km), w=w_const.
608 *
609 * Sets the sinusoidal profile at ALL owned nodes (including the periodic duplicate endpoint),
610 * matching how the IC routines populate fields before any periodic-endpoint fix is applied.
611 */
612static PetscErrorCode SetSinusoidalVZField(UserCtx *user, Vec field, PetscReal v_amp, PetscReal w_const)
613{
614 Cmpnts ***arr = NULL;
615 /* Production layout: physical cells occupy 1..mz-2; 0 and mz-1 are ghost layers,
616 which on a periodic axis hold copies of the opposite physical cell. */
617 const PetscInt ncells = user->info.mz - 2;
618
619 PetscFunctionBeginUser;
620 PetscCheck(user != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "UserCtx cannot be NULL.");
621 PetscCheck(field != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Velocity field cannot be NULL.");
622
623 PetscCall(DMDAVecGetArray(user->fda, field, &arr));
624 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k) {
625 const PetscInt cell = (k == 0) ? ncells : (k == user->info.mz - 1) ? 1 : k;
626 PetscReal z_phase = (2.0 * PETSC_PI * (PetscReal)(cell - 1)) / (PetscReal)ncells;
627 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j) {
628 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
629 arr[k][j][i].x = 0.0;
630 arr[k][j][i].y = v_amp * PetscSinReal(z_phase);
631 arr[k][j][i].z = w_const;
632 }
633 }
634 }
635 PetscCall(DMDAVecRestoreArray(user->fda, field, &arr));
636 PetscFunctionReturn(0);
637}
638
639/**
640 * @brief Tests string-conversion helpers for configured enums and unknown values.
641 */
642
643static PetscErrorCode TestStringConversionHelpers(void)
644{
645 PetscFunctionBeginUser;
646 PetscCall(PicurvAssertBool(strcmp(BCFaceToString(BC_FACE_NEG_X), "-Xi (I-Min)") == 0,
647 "BCFaceToString should report the negative-x face"));
648 PetscCall(PicurvAssertBool(strcmp(InitialConditionModeToString(IC_MODE_ZERO), "Zero") == 0,
649 "InitialConditionModeToString should report the zero mode"));
650 PetscCall(PicurvAssertBool(strcmp(InitialConditionModeToString(IC_MODE_CONSTANT_CARTESIAN), "Cartesian Constant") == 0,
651 "InitialConditionModeToString should report the Cartesian constant mode"));
652 PetscCall(PicurvAssertBool(strcmp(InitialConditionModeToString(IC_MODE_CONSTANT_STREAMWISE), "Streamwise Constant") == 0,
653 "InitialConditionModeToString should report the streamwise constant mode"));
654 PetscCall(PicurvAssertBool(strcmp(InitialConditionModeToString(IC_MODE_POISEUILLE), "Poiseuille") == 0,
655 "InitialConditionModeToString should report the Poiseuille mode"));
656 PetscCall(PicurvAssertBool(strcmp(InitialConditionModeToString(IC_MODE_FILE), "File") == 0,
657 "InitialConditionModeToString should report the file mode"));
658 PetscCall(PicurvAssertBool(strcmp(InitialConditionModeToString((InitialConditionMode)99), "Unknown Initial Condition") == 0,
659 "InitialConditionModeToString should reject unknown selectors"));
661 "ParticleInitializationToString should report the volume mode"));
662 PetscCall(PicurvAssertBool(strcmp(LESModelToString(CONSTANT_SMAGORINSKY), "Constant Smagorinsky") == 0,
663 "LESModelToString should report the constant model"));
664 PetscCall(PicurvAssertBool(strcmp(MomentumSolverTypeToString(MOMENTUM_SOLVER_EXPLICIT_RK), "Explicit 4 stage Runge-Kutta ") == 0,
665 "MomentumSolverTypeToString should report the explicit solver"));
666 PetscCall(PicurvAssertBool(strcmp(MomentumSolverTypeToString(MOMENTUM_SOLVER_NEWTON_KRYLOV), "Newton Krylov") == 0,
667 "MomentumSolverTypeToString should report the Newton Krylov solver"));
668 PetscCall(PicurvAssertBool(strcmp(BCTypeToString(PERIODIC), "PERIODIC") == 0,
669 "BCTypeToString should report periodic boundaries"));
671 "BCHandlerTypeToString should report the driven periodic handler"));
672 PetscCall(PicurvAssertBool(strcmp(ParticleLocationStatusToString(LOST), "LOST") == 0,
673 "ParticleLocationStatusToString should report LOST state"));
674 PetscFunctionReturn(0);
675}
676/**
677 * @brief Tests that log level selection honors the environment variable.
678 */
679
680static PetscErrorCode TestGetLogLevelFromEnvironment(void)
681{
682 PetscFunctionBeginUser;
684 "get_log_level should honor LOG_LEVEL=INFO in this test binary"));
685 PetscCall(print_log_level());
686 PetscFunctionReturn(0);
687}
688/**
689 * @brief Tests the function allow-list filter used by the logging layer.
690 */
691
692static PetscErrorCode TestAllowedFunctionsFilter(void)
693{
694 const char *allow_list[] = {"ComputeSpecificKE", "WriteEulerianFile"};
695
696 PetscFunctionBeginUser;
697 set_allowed_functions(allow_list, 2);
698 PetscCall(PicurvAssertBool(is_function_allowed("ComputeSpecificKE"),
699 "Allowed list should include ComputeSpecificKE"));
700 PetscCall(PicurvAssertBool((PetscBool)!is_function_allowed("UnlistedFunction"),
701 "Allowed list should exclude unknown function names"));
702
703 set_allowed_functions(NULL, 0);
704 PetscCall(PicurvAssertBool(is_function_allowed("AnyFunction"),
705 "Empty allow-list should permit all functions"));
706 PetscFunctionReturn(0);
707}
708/**
709 * @brief Tests periodic particle console snapshot enablement and cadence.
710 */
711
712static PetscErrorCode TestParticleConsoleSnapshotCadence(void)
713{
714 SimCtx simCtx;
715
716 PetscFunctionBeginUser;
717 PetscCall(PetscMemzero(&simCtx, sizeof(simCtx)));
718 simCtx.np = 32;
719 simCtx.particleConsoleOutputFreq = 4;
720
722 "Particle snapshot contract should be enabled when particles and cadence are configured"));
724 "Snapshot should emit on cadence-aligned completed steps"));
725 PetscCall(PicurvAssertBool((PetscBool)!ShouldEmitPeriodicParticleConsoleSnapshot(&simCtx, 7),
726 "Snapshot should not emit off-cadence"));
727
728 simCtx.particleConsoleOutputFreq = 0;
729 PetscCall(PicurvAssertBool((PetscBool)!IsParticleConsoleSnapshotEnabled(&simCtx),
730 "Zero cadence should disable periodic particle snapshots"));
731 PetscCall(PicurvAssertBool((PetscBool)!ShouldEmitPeriodicParticleConsoleSnapshot(NULL, 4),
732 "NULL SimCtx should never emit periodic snapshots"));
733 PetscFunctionReturn(0);
734}
735/**
736 * @brief Builds the window definition backing this suite's statistics console fixture.
737 *
738 * One bounded pressure window on a per-step cadence: bounded so the snapshot exercises
739 * its percentage-progress branch, and a single field so the fixture stays cheap.
740 */
742{
743 PicurvWindowDefinition definition;
744
745 memset(&definition, 0, sizeof(definition));
746 strncpy(definition.name, "console_window", PICURV_WINDOW_NAME_LENGTH - 1);
749 definition.step_cadence = 1;
750 definition.bounded = PETSC_TRUE;
751 definition.end_time = 4.0;
752 definition.field_count = 1;
753 definition.fields[0].field_id = FIELD_ID_P;
754 return definition;
755}
756/**
757 * @brief Tests periodic statistics console snapshot enablement and cadence.
758 */
759
760static PetscErrorCode TestStatisticsConsoleSnapshotCadence(void)
761{
762 SimCtx simCtx;
763 PicurvWindow window;
765
766 PetscFunctionBeginUser;
767 PetscCall(PetscMemzero(&simCtx, sizeof(simCtx)));
768 PetscCall(PicurvWindowInit(&window, &definition));
769 simCtx.fieldStatisticsEnabled = PETSC_TRUE;
771 simCtx.fieldStatisticsWindows = &window;
773
775 "Statistics snapshot contract should be enabled when a window and cadence are configured"));
777 "Snapshot should emit on cadence-aligned completed steps"));
778 PetscCall(PicurvAssertBool((PetscBool)!ShouldEmitPeriodicStatisticsConsoleSnapshot(&simCtx, 7),
779 "Snapshot should not emit off-cadence"));
780 PetscCall(PicurvAssertBool((PetscBool)!ShouldEmitPeriodicStatisticsConsoleSnapshot(&simCtx, -1),
781 "Snapshot should not emit for a step that has not completed"));
782
784 PetscCall(PicurvAssertBool((PetscBool)!IsStatisticsConsoleSnapshotEnabled(&simCtx),
785 "Zero cadence should disable periodic statistics snapshots"));
786 PetscCall(PicurvAssertBool((PetscBool)!ShouldEmitPeriodicStatisticsConsoleSnapshot(&simCtx, 0),
787 "Zero cadence should not emit even on the step that opens a run"));
788
789 /* The console cadence is a reporting contract layered on the subsystem gate: with
790 * nothing accumulating there is nothing to report, whatever the cadence says. */
793 PetscCall(PicurvAssertBool((PetscBool)!IsStatisticsConsoleSnapshotEnabled(&simCtx),
794 "A configured cadence should not enable snapshots without an accumulating window"));
795
797 simCtx.fieldStatisticsEnabled = PETSC_FALSE;
798 PetscCall(PicurvAssertBool((PetscBool)!IsStatisticsConsoleSnapshotEnabled(&simCtx),
799 "A disabled subsystem should not emit statistics snapshots"));
800
802 "NULL SimCtx should never emit periodic snapshots"));
803 PetscFunctionReturn(0);
804}
805/**
806 * @brief Tests logging-side file parsing, helper formatting, and progress utilities.
807 */
808
810{
811 char tmpdir[PETSC_MAX_PATH_LEN];
812 char allow_path[PETSC_MAX_PATH_LEN];
813 char dual_log_path[PETSC_MAX_PATH_LEN];
814 FILE *file = NULL;
815 char **funcs = NULL;
816 PetscInt nfuncs = 0;
817 Cell cell;
818 PetscReal distances[6] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
819 DualMonitorCtx *monctx = NULL;
820 void *ctx = NULL;
821
822 PetscFunctionBeginUser;
823 PetscCall(PetscMemzero(&cell, sizeof(cell)));
824 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
825 PetscCall(PetscSNPrintf(allow_path, sizeof(allow_path), "%s/allowed_functions.txt", tmpdir));
826 PetscCall(PetscSNPrintf(dual_log_path, sizeof(dual_log_path), "%s/dual-monitor.log", tmpdir));
827
828 file = fopen(allow_path, "w");
829 PetscCheck(file != NULL, PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to create allow-list file '%s'.", allow_path);
830 fputs(" ComputeSpecificKE \n", file);
831 fputs("# comment-only line\n", file);
832 for (PetscInt i = 0; i < 17; ++i) {
833 fprintf(file, "Helper_%02d # trailing comment\n", (int)i);
834 }
835 fclose(file);
836 file = NULL;
837
838 PetscCall(LoadAllowedFunctionsFromFile(allow_path, &funcs, &nfuncs));
839 PetscCall(PicurvAssertIntEqual(18, nfuncs, "LoadAllowedFunctionsFromFile should trim comments and keep all identifiers"));
840 PetscCall(PicurvAssertBool((PetscBool)(strcmp(funcs[0], "ComputeSpecificKE") == 0),
841 "LoadAllowedFunctionsFromFile should trim leading and trailing whitespace"));
842 PetscCall(PicurvAssertBool((PetscBool)(strcmp(funcs[17], "Helper_16") == 0),
843 "LoadAllowedFunctionsFromFile should grow past the initial pointer capacity"));
844 PetscCall(FreeAllowedFunctions(funcs, nfuncs));
845
846 for (PetscInt i = 0; i < 8; ++i) {
847 cell.vertices[i].x = (PetscReal)i;
848 cell.vertices[i].y = (PetscReal)(i + 1);
849 cell.vertices[i].z = (PetscReal)(i + 2);
850 }
851 PetscCall(LOG_CELL_VERTICES(&cell, 0));
852 PetscCall(LOG_FACE_DISTANCES(distances));
853
854 PetscCall(PetscCalloc1(1, &monctx));
855 monctx->file_handle = fopen(dual_log_path, "w");
856 PetscCheck(monctx->file_handle != NULL, PETSC_COMM_SELF, PETSC_ERR_FILE_OPEN, "Failed to create dual-monitor log '%s'.", dual_log_path);
857 ctx = monctx;
858 PetscCall(DualMonitorDestroy(&ctx));
859 PetscCall(PicurvAssertBool((PetscBool)(ctx == NULL),
860 "DualMonitorDestroy should clear the caller-owned context pointer"));
861
862 PrintProgressBar(0, 0, 4, 0.10);
863 PrintProgressBar(3, 0, 4, 0.40);
864 PrintProgressBar(0, 0, 0, 0.00);
865 PetscCall(PetscPrintf(PETSC_COMM_SELF, "\n"));
866
867 PetscCall(PicurvRemoveTempDir(tmpdir));
868 PetscFunctionReturn(0);
869}
870/**
871 * @brief Tests continuity, min/max, and anatomy logging helpers on minimal runtime fixtures.
872 */
873
875{
876 SimCtx *simCtx = NULL;
877 UserCtx *user = NULL;
878 char tmpdir[PETSC_MAX_PATH_LEN];
879 char continuity_path[PETSC_MAX_PATH_LEN];
880 PetscReal ***p = NULL;
881 Cmpnts ***ucat = NULL;
882 Cmpnts ***ucont = NULL;
883 PetscErrorCode ierr_minmax = 0;
884
885 PetscFunctionBeginUser;
886 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
887 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
888 PetscCall(PetscStrncpy(simCtx->log_dir, tmpdir, sizeof(simCtx->log_dir)));
889
890 simCtx->StartStep = 0;
891 simCtx->step = 1;
892 simCtx->MaxDiv = 1.25;
893 simCtx->MaxDivx = 1;
894 simCtx->MaxDivy = 2;
895 simCtx->MaxDivz = 3;
896 simCtx->MaxDivFlatArg = 17;
897 simCtx->poissonSourceImbalance = 8.5;
898 simCtx->FluxInSum = 5.0;
899 simCtx->FluxOutSum = 3.25;
900 PetscCall(LOG_CONTINUITY_METRICS(user));
901
902 simCtx->step = 2;
903 simCtx->MaxDiv = 0.75;
904 simCtx->poissonSourceImbalance = 4.5;
905 simCtx->FluxInSum = 2.5;
906 simCtx->FluxOutSum = 1.0;
907 PetscCall(LOG_CONTINUITY_METRICS(user));
908
909 PetscCall(PetscSNPrintf(continuity_path, sizeof(continuity_path), "%s/Continuity_Metrics.log", simCtx->log_dir));
910 PetscCall(PicurvAssertFileExists(continuity_path, "continuity metrics log should be written"));
911 PetscCall(AssertFileContains(continuity_path, "Timestep", "continuity metrics log should include the header"));
912 PetscCall(AssertFileContains(continuity_path, "([3][2][1] = 17)", "continuity metrics log should include the divergence location"));
913 PetscCall(AssertFileContains(continuity_path, "2 | 0", "continuity metrics log should append later timesteps"));
914
915 PetscCall(DMDAVecGetArray(user->da, user->P, &p));
916 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
917 PetscCall(DMDAVecGetArray(user->fda, user->Ucont, &ucont));
918 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k) {
919 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j) {
920 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
921 p[k][j][i] = (PetscReal)(i + j + k);
922 ucat[k][j][i].x = (PetscReal)i;
923 ucat[k][j][i].y = (PetscReal)(-j);
924 ucat[k][j][i].z = (PetscReal)(2 * k);
925 ucont[k][j][i].x = (PetscReal)(10 + i);
926 ucont[k][j][i].y = (PetscReal)(20 + j);
927 ucont[k][j][i].z = (PetscReal)(30 + k);
928 }
929 }
930 }
931 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucont, &ucont));
932 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
933 PetscCall(DMDAVecRestoreArray(user->da, user->P, &p));
934
935 PetscCall(DMGlobalToLocalBegin(user->da, user->P, INSERT_VALUES, user->lP));
936 PetscCall(DMGlobalToLocalEnd(user->da, user->P, INSERT_VALUES, user->lP));
937 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
938 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucat, INSERT_VALUES, user->lUcat));
939 PetscCall(DMGlobalToLocalBegin(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
940 PetscCall(DMGlobalToLocalEnd(user->fda, user->Ucont, INSERT_VALUES, user->lUcont));
941
942 PetscCall(LOG_FIELD_MIN_MAX(user, FIELD_ID_P));
943 PetscCall(LOG_FIELD_MIN_MAX(user, FIELD_ID_UCAT));
944 PetscCall(LOG_FIELD_MIN_MAX(user, FIELD_ID_COORDINATES));
945 PetscCall(LOG_FIELD_MIN_MAX(user, FIELD_ID_UCONT));
946
947 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
948 ierr_minmax = LOG_FIELD_MIN_MAX(user, FIELD_ID_INVALID);
949 PetscCall(PetscPopErrorHandler());
950 PetscCall(PicurvAssertBool((PetscBool)(ierr_minmax != 0),
951 "LOG_FIELD_MIN_MAX should reject invalid field IDs"));
952
953 PetscCall(PicurvRemoveTempDir(tmpdir));
954 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
955 PetscFunctionReturn(0);
956}
957/**
958 * @brief Tests interpolation-error logging against an analytically matched particle field.
959 */
960
961static PetscErrorCode TestInterpolationErrorLogging(void)
962{
963 SimCtx *simCtx = NULL;
964 UserCtx *user = NULL;
965 PetscReal (*pos_arr)[3] = NULL;
966 PetscReal (*vel_arr)[3] = NULL;
967 Vec position_vec = NULL;
968 Vec analytical_vec = NULL;
969 const PetscScalar *analytical_arr = NULL;
970 char tmpdir[PETSC_MAX_PATH_LEN];
971 char metrics_path[PETSC_MAX_PATH_LEN];
972
973 PetscFunctionBeginUser;
974 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
975 PetscCall(PicurvCreateSwarmPair(user, 2, "ske"));
976 PetscCall(PetscStrncpy(simCtx->AnalyticalSolutionType, "TGV3D", sizeof(simCtx->AnalyticalSolutionType)));
977 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
978 PetscCall(PetscStrncpy(simCtx->analysis_dir, tmpdir, sizeof(simCtx->analysis_dir)));
979 simCtx->ren = 1.0;
980 /* ti is physical time, not a step count. The CSV once wrote ti * dt, which read
981 2.5e-03 here instead of 0.25. */
982 simCtx->step = 25;
983 simCtx->dt = 0.01;
984 simCtx->ti = 0.25;
985
986 PetscCall(DMSwarmGetField(user->swarm, "position", NULL, NULL, (void *)&pos_arr));
987 pos_arr[0][0] = 0.5 * PETSC_PI; pos_arr[0][1] = 0.0; pos_arr[0][2] = 0.0;
988 pos_arr[1][0] = 0.0; pos_arr[1][1] = 0.5 * PETSC_PI; pos_arr[1][2] = 0.0;
989 PetscCall(DMSwarmRestoreField(user->swarm, "position", NULL, NULL, (void *)&pos_arr));
990
991 PetscCall(DMSwarmCreateGlobalVectorFromField(user->swarm, "position", &position_vec));
992 PetscCall(VecDuplicate(position_vec, &analytical_vec));
993 PetscCall(VecCopy(position_vec, analytical_vec));
994 PetscCall(SetAnalyticalSolutionForParticles(analytical_vec, simCtx));
995
996 PetscCall(DMSwarmGetField(user->swarm, "velocity", NULL, NULL, (void *)&vel_arr));
997 PetscCall(VecGetArrayRead(analytical_vec, &analytical_arr));
998 for (PetscInt particle = 0; particle < 2; ++particle) {
999 vel_arr[particle][0] = PetscRealPart(analytical_arr[3 * particle + 0]);
1000 vel_arr[particle][1] = PetscRealPart(analytical_arr[3 * particle + 1]);
1001 vel_arr[particle][2] = PetscRealPart(analytical_arr[3 * particle + 2]);
1002 }
1003 PetscCall(VecRestoreArrayRead(analytical_vec, &analytical_arr));
1004 PetscCall(DMSwarmRestoreField(user->swarm, "velocity", NULL, NULL, (void *)&vel_arr));
1005
1006 PetscCall(VecDestroy(&analytical_vec));
1007 PetscCall(DMSwarmDestroyGlobalVectorFromField(user->swarm, "position", &position_vec));
1008 PetscCall(LOG_INTERPOLATION_ERROR(user));
1009
1010 PetscCall(PetscSNPrintf(metrics_path, sizeof(metrics_path), "%s/interpolation_error.csv", simCtx->analysis_dir));
1011 PetscCall(PicurvAssertFileExists(metrics_path, "LOG_INTERPOLATION_ERROR should write interpolation_error.csv"));
1012 PetscCall(AssertFileContains(metrics_path, "25,2.500000e-01,",
1013 "interpolation_error.csv should record physical time, not time multiplied by dt"));
1014 PetscCall(PicurvRemoveTempDir(tmpdir));
1015 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1016 PetscFunctionReturn(0);
1017}
1018/**
1019 * @brief Tests file-backed scatter metrics logging against a fully occupied constant field.
1020 */
1021
1022static PetscErrorCode TestScatterMetricsLogging(void)
1023{
1024 SimCtx *simCtx = NULL;
1025 UserCtx *user = NULL;
1026 char tmpdir[PETSC_MAX_PATH_LEN];
1027 char metrics_path[PETSC_MAX_PATH_LEN];
1028 PetscReal *positions = NULL;
1029 PetscReal *psi = NULL;
1030 PetscInt *cell_ids = NULL;
1031 PetscInt *status = NULL;
1032 PetscInt particle = 0;
1033
1034 PetscFunctionBeginUser;
1035 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
1036 PetscCall(PicurvCreateSwarmPair(user, 27, "ske"));
1037 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
1038 PetscCall(PetscStrncpy(simCtx->log_dir, tmpdir, sizeof(simCtx->log_dir)));
1039 simCtx->np = 27;
1040 simCtx->step = 3;
1041 simCtx->ti = 0.3;
1042 simCtx->verificationScalar.enabled = PETSC_TRUE;
1043 PetscCall(PetscStrncpy(simCtx->verificationScalar.mode,
1044 "analytical",
1045 sizeof(simCtx->verificationScalar.mode)));
1046 PetscCall(PetscStrncpy(simCtx->verificationScalar.profile,
1047 "CONSTANT",
1048 sizeof(simCtx->verificationScalar.profile)));
1049 simCtx->verificationScalar.value = 2.0;
1050
1051 PetscCall(DMSwarmGetField(user->swarm, "position", NULL, NULL, (void **)&positions));
1052 PetscCall(DMSwarmGetField(user->swarm, "DMSwarm_CellID", NULL, NULL, (void **)&cell_ids));
1053 PetscCall(DMSwarmGetField(user->swarm, "DMSwarm_location_status", NULL, NULL, (void **)&status));
1054 PetscCall(DMSwarmGetField(user->swarm, "Psi", NULL, NULL, (void **)&psi));
1055
1056 for (PetscInt k = 0; k < 3; ++k) {
1057 for (PetscInt j = 0; j < 3; ++j) {
1058 for (PetscInt i = 0; i < 3; ++i) {
1059 positions[3 * particle + 0] = (i + 0.5) / 4.0;
1060 positions[3 * particle + 1] = (j + 0.5) / 4.0;
1061 positions[3 * particle + 2] = (k + 0.5) / 4.0;
1062 cell_ids[3 * particle + 0] = i;
1063 cell_ids[3 * particle + 1] = j;
1064 cell_ids[3 * particle + 2] = k;
1065 status[particle] = ACTIVE_AND_LOCATED;
1066 psi[particle] = 2.0;
1067 ++particle;
1068 }
1069 }
1070 }
1071
1072 PetscCall(DMSwarmRestoreField(user->swarm, "Psi", NULL, NULL, (void **)&psi));
1073 PetscCall(DMSwarmRestoreField(user->swarm, "DMSwarm_location_status", NULL, NULL, (void **)&status));
1074 PetscCall(DMSwarmRestoreField(user->swarm, "DMSwarm_CellID", NULL, NULL, (void **)&cell_ids));
1075 PetscCall(DMSwarmRestoreField(user->swarm, "position", NULL, NULL, (void **)&positions));
1076
1078 PetscCall(LOG_SCATTER_METRICS(user));
1079
1080 PetscCall(PetscSNPrintf(metrics_path, sizeof(metrics_path), "%s/scatter_metrics.csv", simCtx->analysis_dir));
1081 PetscCall(PicurvAssertFileExists(metrics_path, "LOG_SCATTER_METRICS should write scatter_metrics.csv"));
1082 PetscCall(AssertFileContains(metrics_path, "relative_L2_error",
1083 "Scatter metrics CSV header should include relative_L2_error"));
1084 PetscCall(AssertFileContains(metrics_path,
1085 "3,3.000000e-01,27,27,27,1.000000e+00,1.000000e+00,5.400000e+01,5.400000e+01,0.000000e+00,0.000000e+00,0.000000e+00,0.000000e+00,0.000000e+00",
1086 "Scatter metrics CSV should record the expected constant-field zero-error row"));
1087
1088 PetscCall(PicurvRemoveTempDir(tmpdir));
1089 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1090 PetscFunctionReturn(0);
1091}
1092/**
1093 * @brief Tests stdout particle-table logging on a production-like swarm fixture.
1094 */
1095
1096static PetscErrorCode TestParticleFieldTableLogging(void)
1097{
1098 SimCtx *simCtx = NULL;
1099 UserCtx *user = NULL;
1100 PetscInt print_interval = 1;
1101 char captured[8192];
1102
1103 PetscFunctionBeginUser;
1104 PetscCall(SeedLoggingParticleFixture(&simCtx, &user));
1105 PetscCall(CaptureLoggingOutput(user, simCtx, InvokeParticleFieldLog, &print_interval, captured, sizeof(captured)));
1106 PetscCall(PicurvAssertBool((PetscBool)(strstr(captured, "Position (x,y,z)") != NULL),
1107 "LOG_PARTICLE_FIELDS should print the particle table header"));
1108 PetscCall(PicurvAssertBool((PetscBool)(strstr(captured, "Weights (a1,a2,a3)") != NULL),
1109 "LOG_PARTICLE_FIELDS should print the weight-column header"));
1110 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1111 PetscFunctionReturn(0);
1112}
1113/**
1114 * @brief Tests console snapshot logging against the public periodic-snapshot helper.
1115 */
1116
1117static PetscErrorCode TestParticleConsoleSnapshotLogging(void)
1118{
1119 SimCtx *simCtx = NULL;
1120 UserCtx *user = NULL;
1121 PetscInt step = 4;
1122 char captured[8192];
1123
1124 PetscFunctionBeginUser;
1125 PetscCall(SeedLoggingParticleFixture(&simCtx, &user));
1126 PetscCall(CaptureLoggingOutput(user, simCtx, InvokeParticleConsoleSnapshot, &step, captured, sizeof(captured)));
1127 PetscCall(PicurvAssertBool((PetscBool)(strstr(captured, "Particle states at step 4") != NULL),
1128 "EmitParticleConsoleSnapshot should print the step banner"));
1129 PetscCall(PicurvAssertBool((PetscBool)(strstr(captured, "Position (x,y,z)") != NULL),
1130 "EmitParticleConsoleSnapshot should reuse the particle table output"));
1131 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1132 PetscFunctionReturn(0);
1133}
1134/**
1135 * @brief Tests statistics console snapshot content, and its silence when disabled.
1136 *
1137 * Covers both halves of the monitoring contract from one fixture: an active window
1138 * reports its window-level scalars, and the identical call on a disabled subsystem
1139 * writes nothing at all, which is the observable form of the gate the runloop relies
1140 * on to keep a non-statistics run's log untouched.
1141 */
1142
1143static PetscErrorCode TestStatisticsConsoleSnapshotLogging(void)
1144{
1145 SimCtx *simCtx = NULL;
1146 UserCtx *user = NULL;
1147 PicurvWindow window;
1148 PicurvWindowStorage storage;
1150 PetscInt step = 2;
1151 char captured[8192];
1152
1153 PetscFunctionBeginUser;
1154 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
1155 PetscCall(VecSet(user->Nvert, 0.0));
1156 PetscCall(PicurvWindowInit(&window, &definition));
1157 PetscCall(PicurvWindowStorageCreate(user, &definition, &storage));
1158
1159 simCtx->fieldStatisticsEnabled = PETSC_TRUE;
1160 simCtx->fieldStatisticsWindowCount = 1;
1161 simCtx->fieldStatisticsWindows = &window;
1162 simCtx->statisticsConsoleOutputFreq = 1;
1163 user->fieldStatisticsStorage = &storage;
1164
1165 /* Step 0 anchors the origin without sampling; steps 1 and 2 each represent one
1166 * unit of time, so the snapshot has two accepted samples to report. */
1167 for (PetscInt offered = 0; offered <= step; ++offered) {
1168 PetscCall(FieldStatisticsUpdateWindows(simCtx, offered, (PetscReal)offered));
1169 }
1170
1171 PetscCall(CaptureLoggingOutput(user, simCtx, InvokeStatisticsConsoleSnapshot, &step,
1172 captured, sizeof(captured)));
1173 PetscCall(PicurvAssertBool((PetscBool)(strstr(captured, "Statistics windows at step 2") != NULL),
1174 "EmitStatisticsConsoleSnapshot should print the step banner"));
1175 PetscCall(PicurvAssertBool((PetscBool)(strstr(captured, "console_window") != NULL),
1176 "EmitStatisticsConsoleSnapshot should name each configured window"));
1177 PetscCall(PicurvAssertBool((PetscBool)(strstr(captured, "samples=2") != NULL),
1178 "EmitStatisticsConsoleSnapshot should report the accepted sample count"));
1179 PetscCall(PicurvAssertBool((PetscBool)(strstr(captured, "valid=[") != NULL),
1180 "EmitStatisticsConsoleSnapshot should report mask coverage once storage carries samples"));
1181
1182 simCtx->fieldStatisticsEnabled = PETSC_FALSE;
1183 PetscCall(CaptureLoggingOutput(user, simCtx, InvokeStatisticsConsoleSnapshot, &step,
1184 captured, sizeof(captured)));
1185 PetscCall(PicurvAssertIntEqual(0, (PetscInt)strlen(captured),
1186 "a disabled subsystem emits no statistics console output"));
1187
1188 simCtx->fieldStatisticsWindows = NULL;
1189 simCtx->fieldStatisticsWindowCount = 0;
1190 user->fieldStatisticsStorage = NULL;
1191 PetscCall(PicurvWindowStorageDestroy(&storage));
1192 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1193 PetscFunctionReturn(0);
1194}
1195/**
1196 * @brief Sets the pressure field to a uniform value across the owned range.
1197 */
1198static PetscErrorCode SetUniformPressure(UserCtx *user, PetscReal value)
1199{
1200 PetscReal ***p = NULL;
1201 const DMDALocalInfo info = user->info;
1202
1203 PetscFunctionBeginUser;
1204 PetscCall(DMDAVecGetArray(user->da, user->P, &p));
1205 for (PetscInt k = info.zs; k < info.zs + info.zm; ++k)
1206 for (PetscInt j = info.ys; j < info.ys + info.ym; ++j)
1207 for (PetscInt i = info.xs; i < info.xs + info.xm; ++i) p[k][j][i] = value;
1208 PetscCall(DMDAVecRestoreArray(user->da, user->P, &p));
1209 PetscFunctionReturn(0);
1210}
1211/**
1212 * @brief Drives one accumulation run at a given console cadence, in the runloop's order.
1213 *
1214 * Reproduces the sequence the run loop applies for each completed step so the console
1215 * observes exactly the states a real run would present to it.
1216 */
1217static PetscErrorCode AccumulateAtConsoleCadence(SimCtx *simCtx, UserCtx *user,
1218 PicurvWindow *window,
1219 PicurvWindowStorage *storage,
1220 const PicurvWindowDefinition *definition,
1221 PetscInt console_frequency)
1222{
1223 PetscFunctionBeginUser;
1224 PetscCall(PicurvWindowInit(window, definition));
1225 simCtx->fieldStatisticsEnabled = PETSC_TRUE;
1226 simCtx->fieldStatisticsWindowCount = 1;
1227 simCtx->fieldStatisticsWindows = window;
1228 simCtx->statisticsConsoleOutputFreq = console_frequency;
1229 user->fieldStatisticsStorage = storage;
1230
1231 for (PetscInt step = 0; step <= 3; ++step) {
1232 PetscCall(SetUniformPressure(user, 1.0 + (PetscReal)step));
1233 PetscCall(FieldStatisticsUpdateWindows(simCtx, step, (PetscReal)step));
1235 PetscCall(EmitStatisticsConsoleSnapshot(user, simCtx, step));
1236 }
1237 }
1238 PetscFunctionReturn(0);
1239}
1240/**
1241 * @brief Asserts two accumulator vectors are bit-identical.
1242 */
1243static PetscErrorCode AssertVecsIdentical(Vec expected, Vec actual, const char *context)
1244{
1245 PetscBool equal = PETSC_FALSE;
1246
1247 PetscFunctionBeginUser;
1248 PetscCall(VecEqual(expected, actual, &equal));
1249 PetscCall(PicurvAssertBool(equal, context));
1250 PetscFunctionReturn(0);
1251}
1252/**
1253 * @brief Tests that the console cadence has no effect on any accumulated result.
1254 *
1255 * This is the observable form of the console cadence's exclusion from the window
1256 * definition hash: two runs over an identical field series, one reporting every step
1257 * and one reporting never, must leave bit-identical accumulator state. Running it
1258 * where the log level is at least `LOG_INFO` matters, because a snapshot that silently
1259 * declined to emit would make the comparison vacuous.
1260 */
1261
1263{
1264 SimCtx *simCtx = NULL;
1265 UserCtx *user = NULL;
1266 PicurvWindow reported, silent;
1267 PicurvWindowStorage reported_storage, silent_storage;
1269 char captured[8192];
1270 PetscInt step = 3;
1271
1272 PetscFunctionBeginUser;
1273 definition.fields[0].want_second = PETSC_TRUE;
1274 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
1275 PetscCall(VecSet(user->Nvert, 0.0));
1276 PetscCall(PicurvWindowStorageCreate(user, &definition, &reported_storage));
1277 PetscCall(PicurvWindowStorageCreate(user, &definition, &silent_storage));
1278
1279 PetscCall(AccumulateAtConsoleCadence(simCtx, user, &reported, &reported_storage, &definition, 1));
1280 PetscCall(AccumulateAtConsoleCadence(simCtx, user, &silent, &silent_storage, &definition, 0));
1281
1282 PetscCall(PicurvAssertIntEqual(reported.sample_count, silent.sample_count,
1283 "both cadences accept the same states"));
1284 PetscCall(PicurvAssertRealNear(reported.total_weight, silent.total_weight, 0.0,
1285 "both cadences accumulate the same total weight"));
1286 PetscCall(AssertVecsIdentical(reported_storage.count, silent_storage.count,
1287 "per-point sample counts are independent of the console cadence"));
1288 PetscCall(AssertVecsIdentical(reported_storage.weight, silent_storage.weight,
1289 "per-point weights are independent of the console cadence"));
1290 PetscCall(AssertVecsIdentical(reported_storage.mean[0], silent_storage.mean[0],
1291 "means are independent of the console cadence"));
1292 PetscCall(AssertVecsIdentical(reported_storage.m2[0], silent_storage.m2[0],
1293 "second moments are independent of the console cadence"));
1294
1295 /* The reporting cadence did drive real output, so the comparison above compared a
1296 * reported run against a silent one rather than two silent ones. */
1297 simCtx->statisticsConsoleOutputFreq = 1;
1298 simCtx->fieldStatisticsWindows = &reported;
1299 user->fieldStatisticsStorage = &reported_storage;
1300 PetscCall(CaptureLoggingOutput(user, simCtx, InvokeStatisticsConsoleSnapshot, &step,
1301 captured, sizeof(captured)));
1302 PetscCall(PicurvAssertBool((PetscBool)(strstr(captured, "console_window") != NULL),
1303 "the reporting cadence emits console output at this log level"));
1304
1305 simCtx->fieldStatisticsEnabled = PETSC_FALSE;
1306 simCtx->fieldStatisticsWindows = NULL;
1307 simCtx->fieldStatisticsWindowCount = 0;
1308 user->fieldStatisticsStorage = NULL;
1309 PetscCall(PicurvWindowStorageDestroy(&silent_storage));
1310 PetscCall(PicurvWindowStorageDestroy(&reported_storage));
1311 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1312 PetscFunctionReturn(0);
1313}
1314/**
1315 * @brief Tests file-backed particle metrics logging after derived metrics are computed.
1316 */
1317
1318static PetscErrorCode TestParticleMetricsLogging(void)
1319{
1320 SimCtx *simCtx = NULL;
1321 UserCtx *user = NULL;
1322 char tmpdir[PETSC_MAX_PATH_LEN];
1323 char metrics_path[PETSC_MAX_PATH_LEN];
1324
1325 PetscFunctionBeginUser;
1326 PetscCall(SeedLoggingParticleFixture(&simCtx, &user));
1327 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
1328 PetscCall(PetscStrncpy(simCtx->log_dir, tmpdir, sizeof(simCtx->log_dir)));
1329 simCtx->StartStep = 0;
1330 simCtx->step = 1;
1331 simCtx->particlesLostLastStep = 1;
1332 simCtx->particlesLostCumulative = 7;
1333 simCtx->particlesMigratedLastStep = 2;
1334 simCtx->migrationPassesLastStep = 3;
1335
1336 PetscCall(CalculateParticleCountPerCell(user));
1337 PetscCall(CalculateAdvancedParticleMetrics(user));
1338 PetscCall(LOG_PARTICLE_METRICS(user, "Timestep Metrics"));
1339
1340 PetscCall(PetscSNPrintf(metrics_path, sizeof(metrics_path), "%s/Particle_Metrics.log", simCtx->log_dir));
1341 PetscCall(PicurvAssertFileExists(metrics_path, "LOG_PARTICLE_METRICS should write Particle_Metrics.log"));
1342 PetscCall(AssertFileContains(metrics_path, "Timestep Metrics", "Particle metrics log should include the caller-provided stage label"));
1343 PetscCall(AssertFileContains(metrics_path, "Occupied Cells", "Particle metrics log should include the metrics table header"));
1344 PetscCall(AssertFileContains(metrics_path, "Lost Total", "Particle metrics log should include the cumulative-loss column"));
1345 PetscCall(AssertFileContains(metrics_path, "| 1 | 7 | 2", "Particle metrics log should record both per-step and cumulative loss values"));
1346 PetscCall(PicurvRemoveTempDir(tmpdir));
1347 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1348 PetscFunctionReturn(0);
1349}
1350/**
1351 * @brief Tests file-backed search metrics logging with the compact CSV contract.
1352 */
1353
1354static PetscErrorCode TestSearchMetricsLogging(void)
1355{
1356 SimCtx *simCtx = NULL;
1357 UserCtx *user = NULL;
1358 char tmpdir[PETSC_MAX_PATH_LEN];
1359 char metrics_path[PETSC_MAX_PATH_LEN];
1360
1361 PetscFunctionBeginUser;
1362 PetscCall(SeedLoggingParticleFixture(&simCtx, &user));
1363 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
1364 PetscCall(PetscStrncpy(simCtx->log_dir, tmpdir, sizeof(simCtx->log_dir)));
1365 simCtx->step = 2;
1366 simCtx->ti = 0.2;
1367 simCtx->particlesLostLastStep = 1;
1368 simCtx->particlesLostCumulative = 7;
1369 simCtx->particlesMigratedLastStep = 2;
1370 simCtx->migrationPassesLastStep = 3;
1371 simCtx->particleLoadImbalance = 1.5;
1372 simCtx->searchMetrics.searchAttempts = 4;
1373 simCtx->searchMetrics.searchPopulation = 2;
1375 simCtx->searchMetrics.searchLostCount = 1;
1376 simCtx->searchMetrics.traversalStepsSum = 10;
1377 simCtx->searchMetrics.reSearchCount = 2;
1378 simCtx->searchMetrics.maxTraversalSteps = 6;
1380 simCtx->searchMetrics.tieBreakCount = 1;
1385
1386 PetscCall(LOG_SEARCH_METRICS(user));
1387
1388 PetscCall(PetscSNPrintf(metrics_path, sizeof(metrics_path), "%s/search_metrics.csv", simCtx->analysis_dir));
1389 PetscCall(PicurvAssertFileExists(metrics_path, "LOG_SEARCH_METRICS should write search_metrics.csv"));
1390 PetscCall(AssertFileContains(metrics_path, "search_attempts", "Search metrics CSV header should include search_attempts"));
1391 PetscCall(AssertFileContains(metrics_path, "search_population", "Search metrics CSV header should include search_population"));
1392 PetscCall(AssertFileContains(metrics_path, "max_particle_pass_depth", "Search metrics CSV header should include max_particle_pass_depth"));
1393 PetscCall(AssertFileContains(metrics_path, "search_work_index", "Search metrics CSV header should include search_work_index"));
1394 PetscCall(AssertFileContains(metrics_path, "re_search_fraction", "Search metrics CSV header should include re_search_fraction"));
1395 PetscCall(AssertFileContains(metrics_path, "lost_cumulative", "Search metrics CSV header should include the cumulative-loss column"));
1396 PetscCall(AssertFileContains(metrics_path, "2,2.000000e-01,2,1,7,2,3,4,2.500000e+00,6,1,2,3,1,3,1.500000e+00,2,1,1,10,2,1,5.000000e-01,5.000000e+00,1.000000e+00", "Search metrics CSV should record the V2 raw and derived search metrics"));
1397 PetscCall(PicurvRemoveTempDir(tmpdir));
1398 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1399 PetscFunctionReturn(0);
1400}
1401/**
1402 * @brief Tests stdout field-anatomy logging on the corrected production-like DM fixture.
1403 */
1404
1405static PetscErrorCode TestFieldAnatomyLogging(void)
1406{
1407 SimCtx *simCtx = NULL;
1408 UserCtx *user = NULL;
1409 char captured[8192];
1410 AnatomyCaptureCtx anatomy_ctx = {FIELD_ID_P, "unit-test"};
1411
1412 PetscFunctionBeginUser;
1413 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
1414 PetscCall(VecSet(user->P, 7.0));
1415 PetscCall(DMGlobalToLocalBegin(user->da, user->P, INSERT_VALUES, user->lP));
1416 PetscCall(DMGlobalToLocalEnd(user->da, user->P, INSERT_VALUES, user->lP));
1417 PetscCall(CaptureLoggingOutput(user, simCtx, InvokeFieldAnatomyLog, &anatomy_ctx, captured, sizeof(captured)));
1418 PetscCall(PicurvAssertBool((PetscBool)(strstr(captured, "Field Anatomy Log: [P]") != NULL),
1419 "LOG_FIELD_ANATOMY should print the requested field name"));
1420 PetscCall(PicurvAssertBool((PetscBool)(strstr(captured, "Layout: [Cell-Centered]") != NULL),
1421 "LOG_FIELD_ANATOMY should report the inferred data layout"));
1422 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1423 PetscFunctionReturn(0);
1424}
1425/**
1426 * @brief Tests profiling helper lifecycle logging for timestep and final-summary outputs.
1427 */
1428
1429static PetscErrorCode TestProfilingLifecycleHelpers(void)
1430{
1431 SimCtx simCtx;
1432 char tmpdir[PETSC_MAX_PATH_LEN];
1433 char timestep_path[PETSC_MAX_PATH_LEN];
1434 char summary_path[PETSC_MAX_PATH_LEN];
1435 static char selected_name[] = "FlowSolver";
1436 char *selected_funcs[] = {selected_name};
1437
1438 PetscFunctionBeginUser;
1439 PetscCall(PetscMemzero(&simCtx, sizeof(simCtx)));
1440 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
1441 PetscCall(PetscStrncpy(simCtx.log_dir, tmpdir, sizeof(simCtx.log_dir)));
1442 PetscCall(PetscStrncpy(simCtx.profilingTimestepMode, "selected", sizeof(simCtx.profilingTimestepMode)));
1443 PetscCall(PetscStrncpy(simCtx.profilingTimestepFile, "Profiling_Timestep_Summary.csv", sizeof(simCtx.profilingTimestepFile)));
1444 simCtx.rank = 0;
1445 simCtx.exec_mode = EXEC_MODE_SOLVER;
1446 simCtx.StartStep = 0;
1447 simCtx.nProfilingSelectedFuncs = 1;
1448 simCtx.profilingSelectedFuncs = selected_funcs;
1449 simCtx.profilingFinalSummary = PETSC_TRUE;
1450
1451 PetscCall(ProfilingInitialize(&simCtx));
1452
1453 _ProfilingStart("FlowSolver");
1454 _ProfilingEnd("FlowSolver");
1455 _ProfilingStart("UnselectedHelper");
1456 _ProfilingEnd("UnselectedHelper");
1457 PetscCall(ProfilingLogTimestepSummary(&simCtx, 1));
1458
1459 _ProfilingStart("FlowSolver");
1460 _ProfilingEnd("FlowSolver");
1461 PetscCall(ProfilingResetTimestepCounters());
1462 PetscCall(ProfilingLogTimestepSummary(&simCtx, 2));
1463 PetscCall(ProfilingFinalize(&simCtx));
1464
1465 PetscCall(PetscSNPrintf(timestep_path, sizeof(timestep_path), "%s/%s", simCtx.log_dir, simCtx.profilingTimestepFile));
1466 PetscCall(PetscSNPrintf(summary_path, sizeof(summary_path), "%s/ProfilingSummary_Solver.log", simCtx.log_dir));
1467 PetscCall(PicurvAssertFileExists(timestep_path, "profiling timestep summary should be written"));
1468 PetscCall(PicurvAssertFileExists(summary_path, "profiling final summary should be written"));
1469 PetscCall(AssertFileContains(timestep_path, "step,function,calls,step_time_s",
1470 "profiling timestep summary should contain the CSV header"));
1471 PetscCall(AssertFileContains(timestep_path, "1,FlowSolver,1,",
1472 "profiling timestep summary should log selected functions"));
1473 PetscCall(AssertFileNotContains(timestep_path, "UnselectedHelper",
1474 "profiling timestep summary should omit unselected functions in selected mode"));
1475 PetscCall(AssertFileContains(summary_path, "FINAL PROFILING SUMMARY",
1476 "profiling final summary should include its table banner"));
1477 PetscCall(AssertFileContains(summary_path, "FlowSolver",
1478 "profiling final summary should include selected functions"));
1479 PetscCall(AssertFileContains(summary_path, "UnselectedHelper",
1480 "profiling final summary should include total-time entries for unselected functions"));
1481 PetscCall(PicurvRemoveTempDir(tmpdir));
1482 PetscFunctionReturn(0);
1483}
1484
1485/**
1486 * @brief Tests runtime memory log header, step rows, final rows, and disabled mode.
1487 */
1488static PetscErrorCode TestRuntimeMemoryLogHelpers(void)
1489{
1490 SimCtx simCtx;
1491 char tmpdir[PETSC_MAX_PATH_LEN];
1492 char memory_path[PETSC_MAX_PATH_LEN];
1493
1494 PetscFunctionBeginUser;
1495 PetscCall(PetscMemzero(&simCtx, sizeof(simCtx)));
1496 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
1497 PetscCall(PetscStrncpy(simCtx.log_dir, tmpdir, sizeof(simCtx.log_dir)));
1498 PetscCall(PetscStrncpy(simCtx.runtimeMemoryLogFile, "Runtime_Memory.log", sizeof(simCtx.runtimeMemoryLogFile)));
1499 simCtx.rank = 0;
1500 simCtx.runtimeMemoryLogEnabled = PETSC_TRUE;
1501 simCtx.runtimeMemoryLogStarted = PETSC_FALSE;
1502 simCtx.runtimeMemoryLogHasPrevious = PETSC_FALSE;
1503 simCtx.continueMode = PETSC_FALSE;
1504 simCtx.StartStep = 0;
1505 PetscCall(PetscMemorySetGetMaximumUsage());
1506
1507 PetscCall(RuntimeMemoryLogSample(&simCtx, 1, "Step", "-"));
1508 PetscCall(RuntimeMemoryLogSample(&simCtx, 1, "Final", "Complete"));
1509 PetscCall(PetscSNPrintf(memory_path, sizeof(memory_path), "%s/%s", simCtx.log_dir, simCtx.runtimeMemoryLogFile));
1510 PetscCall(PicurvAssertFileExists(memory_path, "runtime memory log should be written"));
1511 PetscCall(AssertFileContains(memory_path, "Process Current MB Max",
1512 "runtime memory log should contain readable column labels"));
1513 PetscCall(AssertFileContains(memory_path, "Step",
1514 "runtime memory log should contain a step row"));
1515 PetscCall(AssertFileContains(memory_path, "Final",
1516 "runtime memory log should contain a final row"));
1517 PetscCall(AssertFileContains(memory_path, "Complete",
1518 "runtime memory log should record final reason"));
1519
1520 simCtx.runtimeMemoryLogEnabled = PETSC_FALSE;
1521 PetscCall(PetscStrncpy(simCtx.runtimeMemoryLogFile, "Runtime_Memory_Disabled.log", sizeof(simCtx.runtimeMemoryLogFile)));
1522 PetscCall(RuntimeMemoryLogSample(&simCtx, 2, "Step", "-"));
1523 PetscCall(PetscSNPrintf(memory_path, sizeof(memory_path), "%s/%s", simCtx.log_dir, simCtx.runtimeMemoryLogFile));
1524 PetscCall(PicurvAssertBool((PetscBool)(access(memory_path, F_OK) != 0),
1525 "disabled runtime memory log should not create a file"));
1526 PetscCall(PicurvRemoveTempDir(tmpdir));
1527 PetscFunctionReturn(0);
1528}
1529
1530/**
1531 * @brief Verifies -solution_convergence_enabled=false suppresses the convergence writer.
1532 */
1533static PetscErrorCode TestSolutionConvergenceDisabled(void)
1534{
1535 SimCtx *simCtx = NULL;
1536 UserCtx *user = NULL;
1537 char tmpdir[PETSC_MAX_PATH_LEN];
1538 char log_path[PETSC_MAX_PATH_LEN];
1539 PetscBool exists = PETSC_FALSE;
1540
1541 PetscFunctionBeginUser;
1542 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
1543 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
1544 PetscCall(PetscStrncpy(simCtx->log_dir, tmpdir, sizeof(simCtx->log_dir)));
1545 simCtx->solutionConvergenceEnabled = PETSC_FALSE;
1546
1547 PetscCall(InitializeSolutionConvergenceState(simCtx));
1548 PetscCall(LOG_SOLUTION_CONVERGENCE(simCtx));
1549 PetscCall(PetscSNPrintf(log_path, sizeof(log_path), "%s/solution_convergence.log", simCtx->log_dir));
1550 PetscCall(PetscTestFile(log_path, 'r', &exists));
1551 PetscCall(PicurvAssertBool((PetscBool)!exists,
1552 "disabled solution convergence must not create a log"));
1553
1554 PetscCall(PicurvRemoveTempDir(tmpdir));
1555 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1556 PetscFunctionReturn(0);
1557}
1558
1559/**
1560 * @brief Tests steady solution-convergence log output, IBM masking, and gauge-invariant pressure drift.
1561 */
1562static PetscErrorCode TestSolutionConvergenceSteadyLogging(void)
1563{
1564 SimCtx *simCtx = NULL;
1565 UserCtx *user = NULL;
1566 char tmpdir[PETSC_MAX_PATH_LEN];
1567 char log_path[PETSC_MAX_PATH_LEN];
1568 char header[4096];
1569 char row1[4096];
1570 char row2[4096];
1571 char mode[128];
1572 Cmpnts ***ucat = NULL;
1573 PetscReal ***nvert = NULL;
1574 PetscReal mean_speed = NAN;
1575 PetscReal mean_speed_ref = NAN;
1576 PetscReal mean_speed_abs = NAN;
1577 PetscReal p_abs = NAN;
1578 PetscInt has_reference_1 = 0;
1579 PetscInt has_reference_2 = 0;
1580
1581 PetscFunctionBeginUser;
1582 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
1583 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
1584 PetscCall(PetscStrncpy(simCtx->log_dir, tmpdir, sizeof(simCtx->log_dir)));
1586
1587 PetscCall(InitializeSolutionConvergenceState(simCtx));
1588
1589 simCtx->step = 1;
1590 simCtx->ti = 0.1;
1591 PetscCall(SetUniformVelocityField(user, user->Ucat, 1.0, 0.0, 0.0));
1592 PetscCall(SetUniformScalarField(user, user->P, 11.0));
1593 PetscCall(DMDAVecGetArray(user->da, user->Nvert, &nvert));
1594 nvert[1][1][1] = 1.0;
1595 PetscCall(DMDAVecRestoreArray(user->da, user->Nvert, &nvert));
1596 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
1597 ucat[1][1][1].x = 999.0;
1598 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
1599 PetscCall(LOG_SOLUTION_CONVERGENCE(simCtx));
1600
1601 simCtx->step = 2;
1602 simCtx->ti = 0.2;
1603 PetscCall(SetUniformVelocityField(user, user->Ucat, 1.0, 0.0, 0.0));
1604 PetscCall(SetUniformVelocityField(user, user->Ucat_o, 0.5, 0.0, 0.0));
1605 PetscCall(SetUniformScalarField(user, user->P, 11.0));
1606 PetscCall(SetUniformScalarField(user, user->P_o, 7.0));
1607 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &ucat));
1608 ucat[1][1][1].x = 999.0;
1609 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &ucat));
1610 PetscCall(LOG_SOLUTION_CONVERGENCE(simCtx));
1611
1612 PetscCall(PetscSNPrintf(log_path, sizeof(log_path), "%s/solution_convergence.log", simCtx->log_dir));
1613 PetscCall(PicurvAssertFileExists(log_path, "steady solution-convergence logging should write the log"));
1614 PetscCall(ReadLogHeaderAndRow(log_path, 1, header, sizeof(header), row1, sizeof(row1)));
1615 PetscCall(ReadLogHeaderAndRow(log_path, 2, header, sizeof(header), row2, sizeof(row2)));
1616 PetscCall(LogGetColumnText(header, row1, "mode", mode, sizeof(mode)));
1617 PetscCall(LogGetColumnInt(header, row1, "ref", &has_reference_1));
1618 PetscCall(LogGetColumnInt(header, row2, "ref", &has_reference_2));
1619 PetscCall(LogGetColumnReal(header, row2, "mean_speed", &mean_speed));
1620 PetscCall(LogGetColumnReal(header, row2, "spd_ref", &mean_speed_ref));
1621 PetscCall(LogGetColumnReal(header, row2, "spd_abs", &mean_speed_abs));
1622 PetscCall(LogGetColumnReal(header, row2, "p_abs_l2", &p_abs));
1623
1624 PetscCall(PicurvAssertBool((PetscBool)(strcmp(mode, "steady_deterministic") == 0),
1625 "steady solution convergence row should record the mode name"));
1626 PetscCall(PicurvAssertIntEqual(0, has_reference_1,
1627 "the first steady solution-convergence row should be a warmup row"));
1628 PetscCall(PicurvAssertIntEqual(1, has_reference_2,
1629 "steady solution convergence should compare against the previous solved step"));
1630 PetscCall(PicurvAssertRealNear(1.0, mean_speed, 1.0e-12,
1631 "steady solution convergence should mask IBM-marked solid cells"));
1632 PetscCall(PicurvAssertRealNear(0.5, mean_speed_ref, 1.0e-12,
1633 "steady solution convergence should report the previous-step mean speed"));
1634 PetscCall(PicurvAssertRealNear(0.5, mean_speed_abs, 1.0e-12,
1635 "steady solution convergence should report mean-speed drift"));
1636 PetscCall(PicurvAssertRealNear(0.0, p_abs, 1.0e-12,
1637 "steady solution convergence pressure drift should be gauge invariant"));
1638
1639 PetscCall(PicurvRemoveTempDir(tmpdir));
1640 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1641 PetscFunctionReturn(0);
1642}
1643
1644/**
1645 * @brief Tests periodic solution-convergence warmup and phase-aligned reference reuse.
1646 */
1648{
1649 SimCtx *simCtx = NULL;
1650 UserCtx *user = NULL;
1651 char tmpdir[PETSC_MAX_PATH_LEN];
1652 char log_path[PETSC_MAX_PATH_LEN];
1653 char header[4096];
1654 char row1[4096];
1655 char row2[4096];
1656 char row3[4096];
1657 PetscInt has_reference_1 = 0;
1658 PetscInt has_reference_2 = 0;
1659 PetscInt has_reference_3 = 0;
1660 PetscInt phase_step_1 = -1;
1661 PetscInt phase_step_2 = -1;
1662 PetscInt phase_step_3 = -1;
1663 PetscReal mean_speed_ref_2 = NAN;
1664 PetscReal mean_speed_abs_2 = NAN;
1665
1666 PetscFunctionBeginUser;
1667 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
1668 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
1669 PetscCall(PetscStrncpy(simCtx->log_dir, tmpdir, sizeof(simCtx->log_dir)));
1672
1673 PetscCall(InitializeSolutionConvergenceState(simCtx));
1674
1675 simCtx->step = 1;
1676 simCtx->ti = 0.1;
1677 PetscCall(SetUniformVelocityField(user, user->Ucat, 1.0, 0.0, 0.0));
1678 PetscCall(SetUniformScalarField(user, user->P, 2.0));
1679 PetscCall(LOG_SOLUTION_CONVERGENCE(simCtx));
1680
1681 simCtx->step = 2;
1682 simCtx->ti = 0.2;
1683 PetscCall(SetUniformVelocityField(user, user->Ucat, 2.0, 0.0, 0.0));
1684 PetscCall(SetUniformScalarField(user, user->P, 5.0));
1685 PetscCall(LOG_SOLUTION_CONVERGENCE(simCtx));
1686
1687 simCtx->step = 3;
1688 simCtx->ti = 0.3;
1689 PetscCall(SetUniformVelocityField(user, user->Ucat, 1.25, 0.0, 0.0));
1690 PetscCall(SetUniformScalarField(user, user->P, 9.0));
1691 PetscCall(LOG_SOLUTION_CONVERGENCE(simCtx));
1692
1693 PetscCall(PetscSNPrintf(log_path, sizeof(log_path), "%s/solution_convergence.log", simCtx->log_dir));
1694 PetscCall(PicurvAssertFileExists(log_path, "periodic solution-convergence logging should write the log"));
1695 PetscCall(ReadLogHeaderAndRow(log_path, 1, header, sizeof(header), row1, sizeof(row1)));
1696 PetscCall(ReadLogHeaderAndRow(log_path, 2, header, sizeof(header), row2, sizeof(row2)));
1697 PetscCall(ReadLogHeaderAndRow(log_path, 3, header, sizeof(header), row3, sizeof(row3)));
1698 PetscCall(LogGetColumnInt(header, row1, "ref", &has_reference_1));
1699 PetscCall(LogGetColumnInt(header, row2, "ref", &has_reference_2));
1700 PetscCall(LogGetColumnInt(header, row3, "ref", &has_reference_3));
1701 PetscCall(LogGetColumnInt(header, row1, "ph", &phase_step_1));
1702 PetscCall(LogGetColumnInt(header, row2, "ph", &phase_step_2));
1703 PetscCall(LogGetColumnInt(header, row3, "ph", &phase_step_3));
1704 PetscCall(LogGetColumnReal(header, row3, "spd_ref", &mean_speed_ref_2));
1705 PetscCall(LogGetColumnReal(header, row3, "spd_abs", &mean_speed_abs_2));
1706
1707 PetscCall(PicurvAssertIntEqual(0, has_reference_1,
1708 "the first periodic phase visit should log warmup without a reference"));
1709 PetscCall(PicurvAssertIntEqual(0, has_reference_2,
1710 "the first periodic cycle should fully warm up before comparisons begin"));
1711 PetscCall(PicurvAssertIntEqual(1, has_reference_3,
1712 "the repeated periodic phase visit should compare against the stored reference"));
1713 PetscCall(PicurvAssertIntEqual(1, phase_step_1,
1714 "periodic solution convergence should log the current phase slot"));
1715 PetscCall(PicurvAssertIntEqual(0, phase_step_2,
1716 "periodic solution convergence should log distinct phase slots during warmup"));
1717 PetscCall(PicurvAssertIntEqual(1, phase_step_3,
1718 "periodic solution convergence should reuse the same phase slot on later cycles"));
1719 PetscCall(PicurvAssertRealNear(1.0, mean_speed_ref_2, 1.0e-12,
1720 "periodic solution convergence should report the stored phase-aligned reference"));
1721 PetscCall(PicurvAssertRealNear(0.25, mean_speed_abs_2, 1.0e-12,
1722 "periodic solution convergence should report the phase-aligned drift"));
1723
1724 PetscCall(PicurvRemoveTempDir(tmpdir));
1725 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1726 PetscFunctionReturn(0);
1727}
1728
1729/**
1730 * @brief Tests statistical solution-convergence sliding-window metrics.
1731 */
1733{
1734 SimCtx *simCtx = NULL;
1735 UserCtx *user = NULL;
1736 char tmpdir[PETSC_MAX_PATH_LEN];
1737 char log_path[PETSC_MAX_PATH_LEN];
1738 char header[4096];
1739 char row[4096];
1740 PetscReal mean_speed_window = NAN;
1741 PetscReal mean_speed_window_prev = NAN;
1742 PetscReal mean_speed_window_abs = NAN;
1743 PetscReal mean_speed_rms_window = NAN;
1744 PetscReal mean_ke_window = NAN;
1745 PetscReal mean_ke_window_prev = NAN;
1746 PetscReal mean_ke_window_abs = NAN;
1747 PetscReal mean_ke_rms_window_abs = NAN;
1748 PetscInt has_reference = 0;
1749 const PetscReal speed_samples[4] = {1.0, 3.0, 2.0, 4.0};
1750
1751 PetscFunctionBeginUser;
1752 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 4, 4, 4));
1753 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
1754 PetscCall(PetscStrncpy(simCtx->log_dir, tmpdir, sizeof(simCtx->log_dir)));
1757
1758 PetscCall(InitializeSolutionConvergenceState(simCtx));
1759 for (PetscInt step = 0; step < 4; ++step) {
1760 simCtx->step = step + 1;
1761 simCtx->ti = 0.1 * (PetscReal)(step + 1);
1762 PetscCall(SetUniformVelocityField(user, user->Ucat, speed_samples[step], 0.0, 0.0));
1763 PetscCall(LOG_SOLUTION_CONVERGENCE(simCtx));
1764 }
1765
1766 PetscCall(PetscSNPrintf(log_path, sizeof(log_path), "%s/solution_convergence.log", simCtx->log_dir));
1767 PetscCall(PicurvAssertFileExists(log_path, "statistical solution-convergence logging should write the log"));
1768 PetscCall(ReadLogHeaderAndRow(log_path, 4, header, sizeof(header), row, sizeof(row)));
1769 PetscCall(LogGetColumnInt(header, row, "ref", &has_reference));
1770 PetscCall(LogGetColumnReal(header, row, "spd_win", &mean_speed_window));
1771 PetscCall(LogGetColumnReal(header, row, "spd_win_prev", &mean_speed_window_prev));
1772 PetscCall(LogGetColumnReal(header, row, "spd_win_abs", &mean_speed_window_abs));
1773 PetscCall(LogGetColumnReal(header, row, "spd_rms_win", &mean_speed_rms_window));
1774 PetscCall(LogGetColumnReal(header, row, "ke_win", &mean_ke_window));
1775 PetscCall(LogGetColumnReal(header, row, "ke_win_prev", &mean_ke_window_prev));
1776 PetscCall(LogGetColumnReal(header, row, "ke_win_abs", &mean_ke_window_abs));
1777 PetscCall(LogGetColumnReal(header, row, "ke_rms_abs", &mean_ke_rms_window_abs));
1778
1779 PetscCall(PicurvAssertIntEqual(1, has_reference,
1780 "statistical solution convergence should emit adjacent-window drift once two windows exist"));
1781 PetscCall(PicurvAssertRealNear(3.0, mean_speed_window, 1.0e-12,
1782 "statistical solution convergence should report the current mean-speed window"));
1783 PetscCall(PicurvAssertRealNear(2.0, mean_speed_window_prev, 1.0e-12,
1784 "statistical solution convergence should report the previous mean-speed window"));
1785 PetscCall(PicurvAssertRealNear(1.0, mean_speed_window_abs, 1.0e-12,
1786 "statistical solution convergence should report mean-speed window drift"));
1787 PetscCall(PicurvAssertRealNear(1.0, mean_speed_rms_window, 1.0e-12,
1788 "statistical solution convergence should report current mean-speed RMS"));
1789 PetscCall(PicurvAssertRealNear(5.0, mean_ke_window, 1.0e-12,
1790 "statistical solution convergence should report the current kinetic-energy window"));
1791 PetscCall(PicurvAssertRealNear(2.5, mean_ke_window_prev, 1.0e-12,
1792 "statistical solution convergence should report the previous kinetic-energy window"));
1793 PetscCall(PicurvAssertRealNear(2.5, mean_ke_window_abs, 1.0e-12,
1794 "statistical solution convergence should report kinetic-energy window drift"));
1795 PetscCall(PicurvAssertRealNear(1.0, mean_ke_rms_window_abs, 1.0e-12,
1796 "statistical solution convergence should report RMS kinetic-energy drift"));
1797
1798 PetscCall(PicurvRemoveTempDir(tmpdir));
1799 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1800 PetscFunctionReturn(0);
1801}
1802/**
1803 * @brief Regression test: convergence observables ignore the ghost layers of a walled box.
1804 *
1805 * Interior cells carry speed 1 and every ghost layer carries speed 100, as a
1806 * boundary-condition image may. The volume-averaged mean speed must be exactly 1: the
1807 * loops once ran over the ghost layers too, which put wall images into every mean.
1808 */
1810{
1811 SimCtx *simCtx = NULL;
1812 UserCtx *user = NULL;
1813 char tmpdir[PETSC_MAX_PATH_LEN];
1814 char log_path[PETSC_MAX_PATH_LEN];
1815 char header[4096];
1816 char row[4096];
1817 PetscReal mean_speed = NAN;
1818 Cmpnts ***arr = NULL;
1819
1820 PetscFunctionBeginUser;
1821 PetscCall(PicurvCreateMinimalContexts(&simCtx, &user, 6, 6, 6));
1822 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
1823 PetscCall(PetscStrncpy(simCtx->log_dir, tmpdir, sizeof(simCtx->log_dir)));
1824 PetscCall(PicurvPopulateIdentityMetrics(user));
1825 PetscCall(DMDAVecGetArray(user->fda, user->Ucat, &arr));
1826 for (PetscInt k = user->info.zs; k < user->info.zs + user->info.zm; ++k) {
1827 for (PetscInt j = user->info.ys; j < user->info.ys + user->info.ym; ++j) {
1828 for (PetscInt i = user->info.xs; i < user->info.xs + user->info.xm; ++i) {
1829 const PetscBool ghost = (PetscBool)(i == 0 || j == 0 || k == 0 || i == user->info.mx - 1 ||
1830 j == user->info.my - 1 || k == user->info.mz - 1);
1831 arr[k][j][i].x = 0.0;
1832 arr[k][j][i].y = 0.0;
1833 arr[k][j][i].z = ghost ? 100.0 : 1.0;
1834 }
1835 }
1836 }
1837 PetscCall(DMDAVecRestoreArray(user->fda, user->Ucat, &arr));
1838
1840 PetscCall(InitializeSolutionConvergenceState(simCtx));
1841 simCtx->step = 1;
1842 simCtx->ti = 0.1;
1843 PetscCall(LOG_SOLUTION_CONVERGENCE(simCtx));
1844
1845 PetscCall(PetscSNPrintf(log_path, sizeof(log_path), "%s/solution_convergence.log", simCtx->log_dir));
1846 PetscCall(ReadLogHeaderAndRow(log_path, 1, header, sizeof(header), row, sizeof(row)));
1847 PetscCall(LogGetColumnReal(header, row, "mean_speed", &mean_speed));
1848 PetscCall(PicurvAssertRealNear(1.0, mean_speed, 1.0e-12,
1849 "mean speed must average physical cells only, not wall ghost images"));
1850
1851 PetscCall(PicurvRemoveTempDir(tmpdir));
1852 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1853 PetscFunctionReturn(0);
1854}
1855/**
1856 * @brief Regression test: volume-averaged mean KE of a sinusoidal field in a fully periodic domain.
1857 *
1858 * For u=0, v=0.1*sin(2πz), w=1 on a uniform [0,1]³ grid the exact continuous mean KE is 0.5025.
1859 * With identity metrics (Aj=1) the discrete mean over the N physical cells also equals 0.5025
1860 * for any N >= 3, because Σ_{k=0}^{N-1} sin²(2πk/N) = N/2.
1861 *
1862 * The physical cells are 1..mz-2. The ghost layers at 0 and mz-1 hold periodic copies of the
1863 * opposite cells, so counting either one weights a cell twice. The loops once counted the
1864 * upper ghost, and later still counted the lower one; this test requires both excluded.
1865 */
1866static PetscErrorCode TestPeriodicSinusoidalMeanKE(void)
1867{
1868 const PetscInt km_values[] = {8, 16};
1869 const PetscInt n_cases = (PetscInt)(sizeof(km_values) / sizeof(km_values[0]));
1870
1871 PetscFunctionBeginUser;
1872 for (PetscInt t = 0; t < n_cases; ++t) {
1873 const PetscInt km = km_values[t];
1874 SimCtx *simCtx = NULL;
1875 UserCtx *user = NULL;
1876 char tmpdir[PETSC_MAX_PATH_LEN];
1877 char log_path[PETSC_MAX_PATH_LEN];
1878 char header[4096];
1879 char row[4096];
1880 PetscReal mean_ke = NAN;
1881
1882 PetscCall(PicurvCreateMinimalContextsWithPeriodicity(&simCtx, &user, km, km, km,
1883 PETSC_TRUE, PETSC_TRUE, PETSC_TRUE));
1884 PetscCall(PicurvMakeTempDir(tmpdir, sizeof(tmpdir)));
1885 PetscCall(PetscStrncpy(simCtx->log_dir, tmpdir, sizeof(simCtx->log_dir)));
1886 PetscCall(PicurvPopulateIdentityMetrics(user));
1887 PetscCall(SetSinusoidalVZField(user, user->Ucat, 0.1, 1.0));
1888
1891 PetscCall(InitializeSolutionConvergenceState(simCtx));
1892
1893 simCtx->step = 1;
1894 simCtx->ti = 0.1;
1895 PetscCall(LOG_SOLUTION_CONVERGENCE(simCtx));
1896
1897 PetscCall(PetscSNPrintf(log_path, sizeof(log_path), "%s/solution_convergence.log", simCtx->log_dir));
1898 PetscCall(ReadLogHeaderAndRow(log_path, 1, header, sizeof(header), row, sizeof(row)));
1899 PetscCall(LogGetColumnReal(header, row, "mean_ke", &mean_ke));
1900 PetscCall(PicurvAssertRealNear(0.5025, mean_ke, 1.0e-10,
1901 "periodic sinusoidal field mean KE must equal 0.5025 (no duplicate endpoint)"));
1902
1903 PetscCall(PicurvRemoveTempDir(tmpdir));
1904 PetscCall(PicurvDestroyMinimalContexts(&simCtx, &user));
1905 }
1906 PetscFunctionReturn(0);
1907}
1908
1909/**
1910 * @brief Runs the unit-logging PETSc test binary.
1911 */
1912
1913int main(int argc, char **argv)
1914{
1915 PetscErrorCode ierr;
1916 const PicurvTestCase cases[] = {
1917 {"string-conversion-helpers", TestStringConversionHelpers},
1918 {"get-log-level-from-environment", TestGetLogLevelFromEnvironment},
1919 {"allowed-functions-filter", TestAllowedFunctionsFilter},
1920 {"particle-console-snapshot-cadence", TestParticleConsoleSnapshotCadence},
1921 {"statistics-console-snapshot-cadence", TestStatisticsConsoleSnapshotCadence},
1922 {"logging-file-parsing-and-formatting-helpers", TestLoggingFileParsingAndFormattingHelpers},
1923 {"logging-continuity-and-field-diagnostics", TestLoggingContinuityAndFieldDiagnostics},
1924 {"interpolation-error-logging", TestInterpolationErrorLogging},
1925 {"scatter-metrics-logging", TestScatterMetricsLogging},
1926 {"particle-field-table-logging", TestParticleFieldTableLogging},
1927 {"particle-console-snapshot-logging", TestParticleConsoleSnapshotLogging},
1928 {"statistics-console-snapshot-logging", TestStatisticsConsoleSnapshotLogging},
1929 {"console-cadence-does-not-change-accumulation", TestConsoleCadenceDoesNotChangeAccumulation},
1930 {"particle-metrics-logging", TestParticleMetricsLogging},
1931 {"search-metrics-logging", TestSearchMetricsLogging},
1932 {"field-anatomy-logging", TestFieldAnatomyLogging},
1933 {"profiling-lifecycle-helpers", TestProfilingLifecycleHelpers},
1934 {"runtime-memory-log-helpers", TestRuntimeMemoryLogHelpers},
1935 {"solution-convergence-disabled", TestSolutionConvergenceDisabled},
1936 {"solution-convergence-steady-logging", TestSolutionConvergenceSteadyLogging},
1937 {"solution-convergence-periodic-logging", TestSolutionConvergencePeriodicLogging},
1938 {"solution-convergence-statistical-logging", TestSolutionConvergenceStatisticalLogging},
1939 {"periodic-sinusoidal-mean-ke", TestPeriodicSinusoidalMeanKE},
1940 {"solution-convergence-means-exclude-ghost-layers", TestSolutionConvergenceMeansExcludeGhostLayers},
1941 };
1942
1943 (void)setenv("LOG_LEVEL", "INFO", 1);
1944
1945 ierr = PetscInitialize(&argc, &argv, NULL, "PICurv logging tests");
1946 if (ierr) {
1947 return (int)ierr;
1948 }
1949
1950 ierr = PicurvRunTests("unit-logging", cases, sizeof(cases) / sizeof(cases[0]));
1951 if (ierr) {
1952 PetscFinalize();
1953 return (int)ierr;
1954 }
1955
1956 set_allowed_functions(NULL, 0);
1957
1958 ierr = PetscFinalize();
1959 return (int)ierr;
1960}
PetscErrorCode SetAnalyticalSolutionForParticles(Vec tempVec, SimCtx *simCtx)
Applies the analytical solution to particle velocity vector.
PetscErrorCode CalculateParticleCountPerCell(UserCtx *user)
Counts particles in each cell of the DMDA 'da' and stores the result in user->ParticleCount.
FieldId
Compile-time identity for a catalogued Eulerian field.
@ FIELD_ID_UCAT
@ FIELD_ID_COORDINATES
@ FIELD_ID_UCONT
@ FIELD_ID_INVALID
@ FIELD_ID_P
PetscErrorCode ScatterAllParticleFieldsToEulerFields(UserCtx *user)
Scatters a predefined set of particle fields to their corresponding Eulerian fields.
Logging utilities and macros for PETSc-based applications.
PetscErrorCode LOG_FIELD_MIN_MAX(UserCtx *user, FieldId field_id)
Computes and logs the local and global min/max values of a 3-component vector field.
Definition logging.c:2458
void set_allowed_functions(const char **functionList, int count)
Sets the global list of function names that are allowed to log.
Definition logging.c:155
PetscErrorCode LOG_PARTICLE_METRICS(UserCtx *user, const char *stageName)
Logs particle swarm metrics, adapting its behavior based on a boolean flag in SimCtx.
Definition logging.c:3458
PetscBool ShouldEmitPeriodicStatisticsConsoleSnapshot(const struct SimCtx *simCtx, PetscInt completed_step)
Reports whether a completed step falls on the console snapshot cadence.
const char * BCHandlerTypeToString(BCHandlerType handler_type)
Converts a BCHandlerType enum to its string representation.
Definition logging.c:889
PetscBool is_function_allowed(const char *functionName)
Checks if a given function is in the allow-list.
Definition logging.c:186
PetscErrorCode DualMonitorDestroy(void **ctx)
Destroys the DualMonitorCtx.
Definition logging.c:926
PetscBool IsStatisticsConsoleSnapshotEnabled(const struct SimCtx *simCtx)
Reports whether the periodic statistics console snapshot is enabled.
PetscErrorCode LOG_INTERPOLATION_ERROR(UserCtx *user)
Logs the interpolation error between the analytical and computed solutions.
Definition logging.c:2974
PetscBool ShouldEmitPeriodicParticleConsoleSnapshot(const SimCtx *simCtx, PetscInt completed_step)
Returns whether a particle console snapshot should be emitted for the.
Definition logging.c:545
const char * BCFaceToString(BCFace face)
Returns the canonical log token for a boundary-face enum value.
Definition logging.c:671
PetscErrorCode FreeAllowedFunctions(char **funcs, PetscInt n)
Free an array previously returned by LoadAllowedFunctionsFromFile().
Definition logging.c:652
PetscBool IsParticleConsoleSnapshotEnabled(const SimCtx *simCtx)
Returns whether periodic particle console snapshots are enabled.
Definition logging.c:528
PetscErrorCode print_log_level(void)
Prints the current logging level to the console.
Definition logging.c:119
PetscErrorCode EmitParticleConsoleSnapshot(UserCtx *user, SimCtx *simCtx, PetscInt step)
Emits one particle console snapshot into the main solver log.
Definition logging.c:559
PetscErrorCode ProfilingFinalize(SimCtx *simCtx)
the profiling excercise and build a profiling summary which is then printed to a log file.
Definition logging.c:2305
PetscErrorCode LoadAllowedFunctionsFromFile(const char filename[], char ***funcsOut, PetscInt *nOut)
Load function names from a text file.
Definition logging.c:598
PetscErrorCode EmitStatisticsConsoleSnapshot(UserCtx *user, const struct SimCtx *simCtx, PetscInt step)
Emits one console snapshot of window progress.
void PrintProgressBar(PetscInt step, PetscInt startStep, PetscInt totalSteps, PetscReal currentTime)
Prints a progress bar to the console.
Definition logging.c:2411
PetscErrorCode RuntimeMemoryLogSample(SimCtx *simCtx, PetscInt step, const char *event, const char *reason)
Append a reduced runtime memory sample to the configured memory log.
Definition logging.c:2195
LogLevel get_log_level()
Retrieves the current logging level from the environment variable LOG_LEVEL.
Definition logging.c:87
PetscErrorCode ProfilingLogTimestepSummary(SimCtx *simCtx, PetscInt step)
Logs the performance summary for the current timestep and resets timers.
Definition logging.c:2100
PetscErrorCode LOG_FACE_DISTANCES(PetscReal *d)
Prints the signed distances to each face of the cell.
Definition logging.c:233
PetscErrorCode LOG_PARTICLE_FIELDS(UserCtx *user, PetscInt printInterval)
Prints particle fields in a table that automatically adjusts its column widths.
Definition logging.c:400
void _ProfilingEnd(const char *func_name)
Internal profiling hook invoked by PROFILE_FUNCTION_END.
Definition logging.c:2061
const char * BCTypeToString(BCType type)
Returns the canonical log token for a boundary mathematical type.
Definition logging.c:869
PetscErrorCode CalculateAdvancedParticleMetrics(UserCtx *user)
Computes advanced particle statistics and stores them in SimCtx.
Definition logging.c:3404
const char * ParticleLocationStatusToString(ParticleLocationStatus level)
A function that outputs the name of the current level in the ParticleLocation enum.
Definition logging.c:1953
PetscErrorCode LOG_SCATTER_METRICS(UserCtx *user)
Logs particle-to-grid scatter verification metrics for the prescribed scalar truth path.
Definition logging.c:3056
PetscErrorCode LOG_SOLUTION_CONVERGENCE(SimCtx *simCtx)
Logs physical solution-convergence metrics once per completed timestep.
Definition logging.c:1695
PetscErrorCode LOG_CONTINUITY_METRICS(UserCtx *user)
Logs continuity metrics for a single block to a file.
Definition logging.c:1891
PetscErrorCode LOG_FIELD_ANATOMY(UserCtx *user, FieldId field_id, const char *stage_name)
Logs the anatomy of a specified field at key boundary locations, respecting the solver's specific gri...
Definition logging.c:2861
PetscErrorCode LOG_SEARCH_METRICS(UserCtx *user)
Writes compact runtime search metrics to CSV and optionally to console.
Definition logging.c:3245
const char * InitialConditionModeToString(InitialConditionMode mode)
Convert an initial-condition mode to a string representation.
Definition logging.c:689
PetscErrorCode ProfilingInitialize(SimCtx *simCtx)
Initializes the custom profiling system using configuration from SimCtx.
Definition logging.c:2023
@ LOG_INFO
Informational messages about program execution.
Definition logging.h:31
const char * LESModelToString(LESModelType LESFlag)
Returns the canonical log token for an LES model selector.
Definition logging.c:741
PetscErrorCode LOG_CELL_VERTICES(const Cell *cell, PetscMPIInt rank)
Prints the coordinates of a cell's vertices.
Definition logging.c:208
PetscErrorCode ProfilingResetTimestepCounters(void)
Resets per-timestep profiling counters for the next solver step.
Definition logging.c:2083
const char * MomentumSolverTypeToString(MomentumSolverType SolverFlag)
Returns the canonical log token for a momentum-solver selector.
Definition logging.c:853
FILE * file_handle
Definition logging.h:57
const char * ParticleInitializationToString(ParticleInitializationType ParticleInitialization)
Returns the canonical log token for a particle-initialization mode.
Definition logging.c:724
void _ProfilingStart(const char *func_name)
Internal profiling hook invoked by PROFILE_FUNCTION_BEGIN.
Definition logging.c:2047
Context for a dual-purpose KSP monitor.
Definition logging.h:56
PetscErrorCode InitializeSolutionConvergenceState(SimCtx *simCtx)
Allocates any runtime storage required by solution-convergence logging.
Definition setup.c:96
Per-window PETSc accumulator storage and pointwise application.
Vec weight
Per-point valid weight.
Vec * mean
One per field, matching that field's layout.
PetscErrorCode PicurvWindowStorageCreate(UserCtx *user, const PicurvWindowDefinition *definition, PicurvWindowStorage *storage)
Allocates the accumulator state one window owns on one block.
Vec * m2
One per field; NULL when no second moment was requested.
Vec count
Per-point accepted sample count.
PetscErrorCode PicurvWindowStorageDestroy(PicurvWindowStorage *storage)
Releases accumulator state previously created for one window.
Independent accumulator state for one window on one block.
Window lifecycle, scheduling, and weighting for the field-statistics pipeline.
PetscInt sample_count
PicurvWindowFieldRequest fields[16]
PetscErrorCode FieldStatisticsUpdateWindows(struct SimCtx *simCtx, PetscInt step, PetscReal time)
Offers one completed state to every configured window.
PetscReal end_time
Requested end; ignored when bounded is false.
PicurvCadenceKind cadence_kind
PetscReal total_weight
PetscInt step_cadence
Used when cadence_kind is step; must be positive.
#define PICURV_WINDOW_NAME_LENGTH
Maximum stored length of a window name, including the terminator.
PetscErrorCode PicurvWindowInit(PicurvWindow *window, const PicurvWindowDefinition *definition)
Validates a definition and initializes a window to the pending state.
PetscBool want_second
Also keep the centered second moment.
PetscBool bounded
False for an open-ended window.
PetscInt field_id
Catalogued Eulerian field identity.
@ PICURV_WEIGHTING_SAMPLE
Equal weight per accepted state.
@ PICURV_CADENCE_STEP
Every n completed steps from activation.
Runtime state of one window.
The scientifically immutable definition of one window.
static PetscErrorCode TestParticleConsoleSnapshotCadence(void)
Tests periodic particle console snapshot enablement and cadence.
static PetscErrorCode SetUniformScalarField(UserCtx *user, Vec field, PetscReal value)
Fills one scalar field with a uniform constant state.
static PetscErrorCode TestFieldAnatomyLogging(void)
Tests stdout field-anatomy logging on the corrected production-like DM fixture.
static PetscErrorCode LogGetColumnText(const char *header, const char *row, const char *column_name, char *value, size_t value_len)
Extracts one pipe-delimited log cell as text by header name.
static PetscErrorCode InvokeParticleConsoleSnapshot(UserCtx *user, SimCtx *simCtx, void *ctx)
Adapts EmitParticleConsoleSnapshot() to the generic stdout-capture callback shape.
static PetscErrorCode TestStatisticsConsoleSnapshotLogging(void)
Tests statistics console snapshot content, and its silence when disabled.
static PetscErrorCode TestParticleConsoleSnapshotLogging(void)
Tests console snapshot logging against the public periodic-snapshot helper.
static PetscErrorCode CsvGetColumnText(const char *header, const char *row, const char *column_name, char *value, size_t value_len)
Extracts one CSV cell as text by header name.
static PetscErrorCode TestStatisticsConsoleSnapshotCadence(void)
Tests periodic statistics console snapshot enablement and cadence.
static PetscErrorCode CaptureLoggingOutput(UserCtx *user, SimCtx *simCtx, CapturedLoggingFn fn, void *ctx, char *captured, size_t captured_len)
Captures stdout emitted by one logging helper into a temporary file-backed buffer.
static PetscErrorCode ReadCsvHeaderAndRow(const char *path, PetscInt row_index, char *header, size_t header_len, char *row, size_t row_len)
Reads the CSV header and the requested 1-based data row from a text file.
static PetscErrorCode TestGetLogLevelFromEnvironment(void)
Tests that log level selection honors the environment variable.
int main(int argc, char **argv)
Runs the unit-logging PETSc test binary.
static PetscErrorCode TestLoggingContinuityAndFieldDiagnostics(void)
Tests continuity, min/max, and anatomy logging helpers on minimal runtime fixtures.
static PetscErrorCode AssertVecsIdentical(Vec expected, Vec actual, const char *context)
Asserts two accumulator vectors are bit-identical.
static PetscErrorCode TestPeriodicSinusoidalMeanKE(void)
Regression test: volume-averaged mean KE of a sinusoidal field in a fully periodic domain.
PetscErrorCode(* CapturedLoggingFn)(UserCtx *user, SimCtx *simCtx, void *ctx)
static PetscErrorCode TestSolutionConvergenceStatisticalLogging(void)
Tests statistical solution-convergence sliding-window metrics.
static PetscErrorCode SetUniformPressure(UserCtx *user, PetscReal value)
Sets the pressure field to a uniform value across the owned range.
static PetscErrorCode AssertFileNotContains(const char *path, const char *needle, const char *context)
Asserts that one text file does not contain an excluded substring.
static PetscErrorCode AccumulateAtConsoleCadence(SimCtx *simCtx, UserCtx *user, PicurvWindow *window, PicurvWindowStorage *storage, const PicurvWindowDefinition *definition, PetscInt console_frequency)
Drives one accumulation run at a given console cadence, in the runloop's order.
static PicurvWindowDefinition LoggingStatisticsDefinition(void)
Builds the window definition backing this suite's statistics console fixture.
static PetscErrorCode InvokeFieldAnatomyLog(UserCtx *user, SimCtx *simCtx, void *ctx)
Adapts LOG_FIELD_ANATOMY() to the generic stdout-capture callback shape.
static PetscErrorCode TestSolutionConvergenceMeansExcludeGhostLayers(void)
Regression test: convergence observables ignore the ghost layers of a walled box.
static PetscErrorCode TestSolutionConvergencePeriodicLogging(void)
Tests periodic solution-convergence warmup and phase-aligned reference reuse.
static PetscErrorCode TestStringConversionHelpers(void)
Tests string-conversion helpers for configured enums and unknown values.
static PetscErrorCode SetSinusoidalVZField(UserCtx *user, Vec field, PetscReal v_amp, PetscReal w_const)
Fills one Cartesian velocity field with u=0, v=v_amp*sin(2π*k/km), w=w_const.
static PetscErrorCode TestLoggingFileParsingAndFormattingHelpers(void)
Tests logging-side file parsing, helper formatting, and progress utilities.
static PetscErrorCode TestAllowedFunctionsFilter(void)
Tests the function allow-list filter used by the logging layer.
static PetscErrorCode TestInterpolationErrorLogging(void)
Tests interpolation-error logging against an analytically matched particle field.
static PetscErrorCode SeedLoggingParticleFixture(SimCtx **simCtx_out, UserCtx **user_out)
Creates a small particle-bearing runtime fixture used by logging tests.
static PetscErrorCode TestScatterMetricsLogging(void)
Tests file-backed scatter metrics logging against a fully occupied constant field.
static PetscErrorCode TestParticleMetricsLogging(void)
Tests file-backed particle metrics logging after derived metrics are computed.
static PetscErrorCode CsvGetColumnReal(const char *header, const char *row, const char *column_name, PetscReal *value_out)
Extracts one CSV cell as a real by header name.
static PetscErrorCode TestSearchMetricsLogging(void)
Tests file-backed search metrics logging with the compact CSV contract.
static PetscErrorCode InvokeStatisticsConsoleSnapshot(UserCtx *user, SimCtx *simCtx, void *ctx)
Adapts EmitStatisticsConsoleSnapshot() to the generic stdout-capture callback shape.
static PetscErrorCode TestParticleFieldTableLogging(void)
Tests stdout particle-table logging on a production-like swarm fixture.
static PetscErrorCode CsvFindColumnIndex(const char *header, const char *column_name, PetscInt *index_out)
Returns the zero-based column index for one CSV header field.
static PetscErrorCode TestSolutionConvergenceDisabled(void)
Verifies -solution_convergence_enabled=false suppresses the convergence writer.
static PetscErrorCode LogFindColumnIndex(const char *header, const char *column_name, PetscInt *index_out)
Returns the zero-based column index for one pipe-delimited header field.
static PetscErrorCode SetUniformVelocityField(UserCtx *user, Vec field, PetscReal ux, PetscReal uy, PetscReal uz)
Fills one Cartesian velocity field with a uniform constant state.
static PetscErrorCode ReadLogHeaderAndRow(const char *path, PetscInt row_index, char *header, size_t header_len, char *row, size_t row_len)
Reads the column header and the requested 1-based data row from a pipe-delimited solution-convergence...
static PetscErrorCode AssertFileContains(const char *path, const char *needle, const char *context)
Asserts that one text file contains a required substring.
static PetscErrorCode CsvGetColumnInt(const char *header, const char *row, const char *column_name, PetscInt *value_out)
Extracts one CSV cell as an integer by header name.
static PetscErrorCode LogGetColumnReal(const char *header, const char *row, const char *column_name, PetscReal *value_out)
Extracts one pipe-delimited log cell as a real by header name.
static PetscErrorCode TestSolutionConvergenceSteadyLogging(void)
Tests steady solution-convergence log output, IBM masking, and gauge-invariant pressure drift.
static PetscErrorCode TestProfilingLifecycleHelpers(void)
Tests profiling helper lifecycle logging for timestep and final-summary outputs.
static PetscErrorCode LogGetColumnInt(const char *header, const char *row, const char *column_name, PetscInt *value_out)
Extracts one pipe-delimited log cell as an integer by header name.
static PetscErrorCode TestConsoleCadenceDoesNotChangeAccumulation(void)
Tests that the console cadence has no effect on any accumulated result.
const char * stage_name
static PetscErrorCode InvokeParticleFieldLog(UserCtx *user, SimCtx *simCtx, void *ctx)
Adapts LOG_PARTICLE_FIELDS() to the generic stdout-capture callback shape.
static PetscErrorCode TestRuntimeMemoryLogHelpers(void)
Tests runtime memory log header, step rows, final rows, and disabled mode.
PetscErrorCode PicurvMakeTempDir(char *path, size_t path_len)
Creates a unique temporary directory for one test case.
PetscErrorCode PicurvCreateMinimalContexts(SimCtx **simCtx_out, UserCtx **user_out, PetscInt mx, PetscInt my, PetscInt mz)
Builds minimal SimCtx and UserCtx fixtures for C unit tests.
PetscErrorCode PicurvAssertRealNear(PetscReal expected, PetscReal actual, PetscReal tol, const char *context)
Asserts that two real values agree within tolerance.
PetscErrorCode PicurvDestroyMinimalContexts(SimCtx **simCtx_ptr, UserCtx **user_ptr)
Destroys minimal SimCtx/UserCtx fixtures and all owned PETSc objects.
PetscErrorCode PicurvCreateMinimalContextsWithPeriodicity(SimCtx **simCtx_out, UserCtx **user_out, PetscInt mx, PetscInt my, PetscInt mz, PetscBool x_periodic, PetscBool y_periodic, PetscBool z_periodic)
Builds minimal SimCtx and UserCtx fixtures for C unit tests with configurable periodicity.
PetscErrorCode PicurvCreateSwarmPair(UserCtx *user, PetscInt nlocal, const char *post_field_name)
Creates matched solver and post-processing swarms for tests.
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 PicurvAssertFileExists(const char *path, const char *context)
Asserts that a filesystem path exists as a readable file.
PetscErrorCode PicurvAssertIntEqual(PetscInt expected, PetscInt actual, const char *context)
Asserts that two integer values are equal.
PetscErrorCode PicurvPopulateIdentityMetrics(UserCtx *user)
Populates identity metric vectors on the minimal grid fixture.
PetscErrorCode PicurvAssertBool(PetscBool value, const char *context)
Asserts that one boolean condition is true.
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.
@ CONSTANT_SMAGORINSKY
Definition variables.h:550
PetscInt fieldStatisticsWindowCount
Definition variables.h:932
@ PERIODIC
Definition variables.h:318
PetscBool continueMode
Definition variables.h:876
PetscBool profilingFinalSummary
Definition variables.h:1036
PetscMPIInt rank
Definition variables.h:862
char profilingTimestepFile[PETSC_MAX_PATH_LEN]
Definition variables.h:1035
PetscInt64 searchLocatedCount
Definition variables.h:267
PetscInt statisticsConsoleOutputFreq
Definition variables.h:934
PetscInt64 searchLostCount
Definition variables.h:268
@ PARTICLE_INIT_VOLUME
Random volumetric distribution across the domain.
Definition variables.h:711
@ LOST
Definition variables.h:167
@ ACTIVE_AND_LOCATED
Definition variables.h:165
PetscReal FluxOutSum
Definition variables.h:959
PetscBool runtimeMemoryLogEnabled
Enable the rank-reduced runtime memory log.
Definition variables.h:1051
PetscInt64 boundaryClampCount
Definition variables.h:274
PetscInt particlesLostLastStep
Definition variables.h:997
PetscInt64 traversalStepsSum
Definition variables.h:269
@ BC_HANDLER_PERIODIC_DRIVEN_CONSTANT_FLUX
Definition variables.h:342
PetscReal ren
Definition variables.h:906
PetscReal poissonSourceImbalance
Definition variables.h:1026
PetscInt64 searchPopulation
Definition variables.h:266
PetscBool solutionConvergenceEnabled
Definition variables.h:923
PetscReal dt
Definition variables.h:874
char runtimeMemoryLogFile[PETSC_MAX_PATH_LEN]
File name written under log_dir.
Definition variables.h:1052
PetscBool runtimeMemoryLogStarted
True after rank 0 writes the log header.
Definition variables.h:1053
char profilingTimestepMode[32]
Definition variables.h:1034
PetscInt np
Definition variables.h:990
PetscBool fieldStatisticsEnabled
Definition variables.h:931
Vec Ucont
Definition variables.h:1113
PetscInt StartStep
Definition variables.h:869
@ MOMENTUM_SOLVER_EXPLICIT_RK
Definition variables.h:693
@ MOMENTUM_SOLVER_NEWTON_KRYLOV
Definition variables.h:695
PetscInt solutionConvergencePeriodSteps
Definition variables.h:925
PetscScalar x
Definition variables.h:122
PetscInt64 reSearchCount
Definition variables.h:270
PetscReal MaxDiv
Definition variables.h:1027
PetscInt64 bboxGuessFallbackCount
Definition variables.h:276
VerificationScalarConfig verificationScalar
Definition variables.h:940
Vec Ucat_o
Definition variables.h:1120
PetscInt MaxDivx
Definition variables.h:1028
PetscInt MaxDivy
Definition variables.h:1028
char analysis_dir[PETSC_MAX_PATH_LEN]
Definition variables.h:883
PetscInt64 bboxGuessSuccessCount
Definition variables.h:275
PetscInt MaxDivz
Definition variables.h:1028
struct PicurvWindow * fieldStatisticsWindows
Definition variables.h:933
char log_dir[PETSC_MAX_PATH_LEN]
Definition variables.h:882
PetscInt MaxDivFlatArg
Definition variables.h:1028
PetscReal FluxInSum
Definition variables.h:959
PetscInt64 maxParticlePassDepth
Definition variables.h:277
PetscInt64 maxTraversalSteps
Definition variables.h:271
PetscScalar z
Definition variables.h:122
PetscBool runtimeMemoryLogHasPrevious
True after the first process-memory sample.
Definition variables.h:1054
char ** profilingSelectedFuncs
Definition variables.h:1032
PetscInt solutionConvergenceWindowSteps
Definition variables.h:926
PetscInt particlesLostCumulative
Definition variables.h:998
PetscInt nProfilingSelectedFuncs
Definition variables.h:1033
PetscInt particlesMigratedLastStep
Definition variables.h:1002
struct PicurvWindowStorage * fieldStatisticsStorage
Definition variables.h:1136
char AnalyticalSolutionType[PETSC_MAX_PATH_LEN]
Definition variables.h:893
InitialConditionMode
Selects the algorithm used to populate a fresh Eulerian velocity field.
Definition variables.h:177
@ IC_MODE_CONSTANT_CARTESIAN
Definition variables.h:179
@ IC_MODE_POISEUILLE
Definition variables.h:180
@ IC_MODE_CONSTANT_STREAMWISE
Definition variables.h:181
@ IC_MODE_FILE
Definition variables.h:182
@ IC_MODE_ZERO
Definition variables.h:178
PetscInt particleConsoleOutputFreq
Definition variables.h:872
SearchMetricsState searchMetrics
Definition variables.h:1005
Vec lUcont
Definition variables.h:1113
PetscInt step
Definition variables.h:867
DMDALocalInfo info
Definition variables.h:1086
Vec lUcat
Definition variables.h:1113
PetscInt migrationPassesLastStep
Definition variables.h:1001
PetscScalar y
Definition variables.h:122
@ EXEC_MODE_SOLVER
Definition variables.h:832
Vec Nvert
Definition variables.h:1113
@ SOLUTION_CONVERGENCE_PERIODIC_DETERMINISTIC
Definition variables.h:703
@ SOLUTION_CONVERGENCE_STATISTICAL_STEADY
Definition variables.h:704
@ SOLUTION_CONVERGENCE_STEADY_DETERMINISTIC
Definition variables.h:702
SolutionConvergenceMode solutionConvergenceMode
Definition variables.h:924
PetscInt64 searchAttempts
Definition variables.h:265
ExecutionMode exec_mode
Definition variables.h:878
PetscInt64 tieBreakCount
Definition variables.h:273
PetscReal ti
Definition variables.h:868
PetscInt64 maxTraversalFailCount
Definition variables.h:272
Cmpnts vertices[8]
Coordinates of the eight vertices of the cell.
Definition variables.h:204
PetscReal particleLoadImbalance
Definition variables.h:1004
@ BC_FACE_NEG_X
Definition variables.h:288
Defines the vertices of a single hexahedral grid cell.
Definition variables.h:203
A 3D point or vector with PetscScalar components.
Definition variables.h:121
The master context for the entire simulation.
Definition variables.h:859
User-defined context containing data specific to a single computational grid level.
Definition variables.h:1074