PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
statistics_accumulator.c
Go to the documentation of this file.
1/**
2 * @file statistics_accumulator.c
3 * @brief Per-window PETSc accumulator storage and pointwise application.
4 *
5 * Full API contract is documented with the declarations in
6 * `include/statistics_accumulator.h`.
7 */
8
10#include "statistics_moments.h"
11#include "statistics_target.h"
12#include "field_catalog.h"
13#include "io.h"
14#include "logging.h"
15
16#include <stddef.h>
17
18/** @brief Longest output list a recipe may request. */
19#define STATISTICS_DERIVED_OUTPUT_LENGTH 256
20
21/**
22 * @brief Upper-triangular row-major component pairs for a three-vector self-product.
23 *
24 * This pair table is the single definition of the symmetric component order. The
25 * diagonal positions and the two-axis component labels are both derived from it
26 * rather than restated, because accumulation reads the table while derivation reads
27 * what follows from it: a hand-written copy that drifted would mislabel every
28 * Reynolds stress and take the root of the wrong component, with nothing failing.
29 */
30static const PetscInt kProductFirst[6] = {0, 0, 0, 1, 1, 2};
31static const PetscInt kProductSecond[6] = {0, 1, 2, 1, 2, 2};
32
33/* That pair order is (xx, xy, xz, yy, yz, zz), which is exactly the order `SymTensor`
34 * declares its members in. The correspondence is not incidental: it is what lets a
35 * six-component accumulator and a `SymTensor` name the same entry by the same index,
36 * the way a three-component vector field and `Cmpnts` already do. Nothing in the type
37 * system enforces it, so it is asserted here; if either order is ever changed
38 * independently, this fails at compile time rather than silently mislabelling every
39 * Reynolds stress. */
40_Static_assert(offsetof(SymTensor, xx) == 0 * sizeof(PetscReal), "SymTensor order: xx is component 0");
41_Static_assert(offsetof(SymTensor, xy) == 1 * sizeof(PetscReal), "SymTensor order: xy is component 1");
42_Static_assert(offsetof(SymTensor, xz) == 2 * sizeof(PetscReal), "SymTensor order: xz is component 2");
43_Static_assert(offsetof(SymTensor, yy) == 3 * sizeof(PetscReal), "SymTensor order: yy is component 3");
44_Static_assert(offsetof(SymTensor, yz) == 4 * sizeof(PetscReal), "SymTensor order: yz is component 4");
45_Static_assert(offsetof(SymTensor, zz) == 5 * sizeof(PetscReal), "SymTensor order: zz is component 5");
46
47/** @brief Axis labels indexing the pair table above. */
48static const char *const kAxisName[3] = {"x", "y", "z"};
49
50/**
51 * @brief Internal helper: the product index carrying one component's own variance.
52 * @details Local to this translation unit. Found by searching the pair table for the
53 * entry pairing a component with itself, so the diagonal cannot be stated
54 * separately from the order it belongs to.
55 */
56static PetscInt ProductDiagonalIndex(PetscInt component)
57{
58 for (PetscInt c = 0; c < 6; ++c) {
59 if (kProductFirst[c] == component && kProductSecond[c] == component) return c;
60 }
61 return -1;
62}
63
64/**
65 * @brief Internal helper: writes the two-axis label of one product component.
66 * @details Local to this translation unit. Built from the pair table, so "xy" and the
67 * slot it names can never refer to different pairs.
68 */
69static PetscErrorCode ProductComponentLabel(PetscInt index, char *out, size_t size)
70{
71 PetscFunctionBeginUser;
72 PetscCheck(index >= 0 && index < 6, PETSC_COMM_SELF, PETSC_ERR_ARG_OUTOFRANGE,
73 "Product component %" PetscInt_FMT " is outside the symmetric set.", index);
74 PetscCall(PetscSNPrintf(out, size, "%s%s", kAxisName[kProductFirst[index]],
75 kAxisName[kProductSecond[index]]));
76 PetscFunctionReturn(0);
77}
78
79/**
80 * @brief Implementation of \ref PicurvProductComponentCount().
81 * @see PicurvProductComponentCount()
82 */
83PetscErrorCode PicurvProductComponentCount(PetscInt dof, PetscInt *count)
84{
85 PetscFunctionBeginUser;
86 PetscCheck(count != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Count output is required.");
87 PetscCheck(dof == 1 || dof == 3, PETSC_COMM_SELF, PETSC_ERR_ARG_OUTOFRANGE,
88 "Self-products are supported for scalar and three-vector fields, got dof %" PetscInt_FMT ".", dof);
89 *count = (dof == 1) ? 1 : 6;
90 PetscFunctionReturn(0);
91}
92
93/**
94 * @brief Implementation of \ref PicurvCovarianceComponentCount().
95 * @see PicurvCovarianceComponentCount()
96 */
97PetscErrorCode PicurvCovarianceComponentCount(PetscInt dof_a, PetscInt dof_b, PetscInt *count)
98{
99 PetscFunctionBeginUser;
100 PetscCheck(count != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Count output is required.");
101 /* Scalar-scalar and vector-scalar pairs only; a vector-vector cross product
102 * would need a nine-component carrier that nothing allocates. */
103 PetscCheck((dof_a == 1 && dof_b == 1) || (dof_a == 3 && dof_b == 1) || (dof_a == 1 && dof_b == 3),
104 PETSC_COMM_SELF, PETSC_ERR_ARG_OUTOFRANGE,
105 "Covariance is supported for scalar-scalar and vector-scalar pairs, got dof %" PetscInt_FMT
106 " and %" PetscInt_FMT ".", dof_a, dof_b);
107 *count = (dof_a == 3 || dof_b == 3) ? 3 : 1;
108 PetscFunctionReturn(0);
109}
110
111/**
112 * @brief Implementation of \ref PicurvStatisticsComponentDM().
113 * @see PicurvStatisticsComponentDM()
114 */
115PetscErrorCode PicurvStatisticsComponentDM(UserCtx *user, PetscInt components, DM *dm)
116{
117 PetscFunctionBeginUser;
118 PetscCheck(user != NULL && dm != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL,
119 "Context and DM output are required.");
120 switch (components) {
121 case 1: *dm = user->da; break;
122 case 3: *dm = user->fda; break;
123 case 6: *dm = user->fda6; break;
124 default:
125 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_ARG_OUTOFRANGE,
126 "No block DM carries %" PetscInt_FMT " accumulator components.", components);
127 }
128 PetscCheck(*dm != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_WRONGSTATE,
129 "The block DM for %" PetscInt_FMT " accumulator components was never created.",
130 components);
131 PetscFunctionReturn(0);
132}
133
134#undef __FUNCT__
135#define __FUNCT__ "PicurvWindowStorageCreate"
136/**
137 * @brief Implementation of \ref PicurvWindowStorageCreate().
138 * @see PicurvWindowStorageCreate()
139 */
140PetscErrorCode PicurvWindowStorageCreate(UserCtx *user, const PicurvWindowDefinition *definition,
141 PicurvWindowStorage *storage)
142{
143 PetscFunctionBeginUser;
145 PetscCheck(user != NULL && definition != NULL && storage != NULL,
146 PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Context, definition, and storage are required.");
147 PetscCall(PetscMemzero(storage, sizeof(*storage)));
148 storage->field_count = definition->field_count;
149 storage->covariance_count = definition->covariance_count;
150
151 /* Occupancy is per point and scalar regardless of what is accumulated. */
152 PetscCall(DMCreateGlobalVector(user->da, &storage->count));
153 PetscCall(VecSet(storage->count, 0.0));
154 PetscCall(DMCreateGlobalVector(user->da, &storage->weight));
155 PetscCall(VecSet(storage->weight, 0.0));
156 PetscCall(DMCreateGlobalVector(user->da, &storage->weight_sq));
157 PetscCall(VecSet(storage->weight_sq, 0.0));
158
159 if (definition->field_count > 0) {
160 PetscCall(PetscCalloc1((size_t)definition->field_count, &storage->mean));
161 PetscCall(PetscCalloc1((size_t)definition->field_count, &storage->m2));
162 }
163 for (PetscInt field_index = 0; field_index < definition->field_count; ++field_index) {
164 FieldView view;
165 PetscInt components = 0;
166 DM product_dm = NULL;
167
168 PetscCall(FieldGetView(user, (FieldId)definition->fields[field_index].field_id, &view));
169 /* The mean matches the source field's own layout, so it comes from that
170 * field's DM and inherits its decomposition. */
171 PetscCall(DMCreateGlobalVector(view.dm, &storage->mean[field_index]));
172 PetscCall(VecSet(storage->mean[field_index], 0.0));
173 if (!definition->fields[field_index].want_second) continue;
174 PetscCall(PicurvProductComponentCount(view.descriptor->dof, &components));
175 PetscCall(PicurvStatisticsComponentDM(user, components, &product_dm));
176 PetscCall(DMCreateGlobalVector(product_dm, &storage->m2[field_index]));
177 PetscCall(VecSet(storage->m2[field_index], 0.0));
178 }
179
180 if (definition->covariance_count > 0) {
181 PetscCall(PetscCalloc1((size_t)definition->covariance_count, &storage->cm));
182 }
183 for (PetscInt pair_index = 0; pair_index < definition->covariance_count; ++pair_index) {
184 FieldView view_a, view_b;
185 PetscInt components = 0;
186 DM pair_dm = NULL;
187
188 PetscCall(FieldGetView(user, (FieldId)definition->covariances[pair_index].first, &view_a));
189 PetscCall(FieldGetView(user, (FieldId)definition->covariances[pair_index].second, &view_b));
190 PetscCall(PicurvCovarianceComponentCount(view_a.descriptor->dof, view_b.descriptor->dof, &components));
191 PetscCall(PicurvStatisticsComponentDM(user, components, &pair_dm));
192 PetscCall(DMCreateGlobalVector(pair_dm, &storage->cm[pair_index]));
193 PetscCall(VecSet(storage->cm[pair_index], 0.0));
194 }
195
196 /* Report what this window costs and what it covers, once per block at setup.
197 * The point count is a function of layout, dimensions, and periodicity alone, so
198 * it tells an operator what "per point" will mean before any sample is taken. */
199 {
201 PetscInt payloads = 0;
202 PetscInt points = 0;
203
204 PetscCall(PicurvWindowStoragePayloadCount(storage, &payloads));
205 PetscCall(SpatialTargetPlanCreate(user, (FieldId)definition->fields[0].field_id,
207 PetscCall(SpatialTargetPlanGlobalPointCount(&plan, PETSC_COMM_WORLD, &points));
209 "Statistics window '%s' block %d: %d accumulator vector(s) over %d point(s).\n",
210 definition->name, (int)user->_this, (int)payloads, (int)points);
211 }
213 PetscFunctionReturn(0);
214}
215
216/**
217 * @brief Implementation of \ref PicurvWindowStorageDestroy().
218 * @see PicurvWindowStorageDestroy()
219 */
221{
222 PetscFunctionBeginUser;
223 if (storage == NULL) PetscFunctionReturn(0);
224 if (storage->count) PetscCall(VecDestroy(&storage->count));
225 if (storage->weight) PetscCall(VecDestroy(&storage->weight));
226 if (storage->weight_sq) PetscCall(VecDestroy(&storage->weight_sq));
227 for (PetscInt field_index = 0; storage->mean && field_index < storage->field_count; ++field_index) {
228 if (storage->mean[field_index]) PetscCall(VecDestroy(&storage->mean[field_index]));
229 }
230 for (PetscInt field_index = 0; storage->m2 && field_index < storage->field_count; ++field_index) {
231 if (storage->m2[field_index]) PetscCall(VecDestroy(&storage->m2[field_index]));
232 }
233 for (PetscInt pair_index = 0; storage->cm && pair_index < storage->covariance_count; ++pair_index) {
234 if (storage->cm[pair_index]) PetscCall(VecDestroy(&storage->cm[pair_index]));
235 }
236 if (storage->mean) PetscCall(PetscFree(storage->mean));
237 if (storage->m2) PetscCall(PetscFree(storage->m2));
238 if (storage->cm) PetscCall(PetscFree(storage->cm));
239 PetscCall(PetscMemzero(storage, sizeof(*storage)));
240 PetscFunctionReturn(0);
241}
242
243/**
244 * @brief Internal helper: locates the storage slot holding one field's running mean.
245 * @details Local to this translation unit. A covariance member must also appear in
246 * the window's field list, because the co-moment update needs that field's
247 * running mean; this is where that requirement is enforced.
248 */
249static PetscErrorCode FindFieldSlot(const PicurvWindowDefinition *definition, PetscInt field_id,
250 PetscInt *slot)
251{
252 PetscFunctionBeginUser;
253 for (PetscInt field_index = 0; field_index < definition->field_count; ++field_index) {
254 if (definition->fields[field_index].field_id == field_id) {
255 *slot = field_index;
256 PetscFunctionReturn(0);
257 }
258 }
259 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_ARG_WRONGSTATE,
260 "Window '%s' requests a covariance over field '%s', which is not in its field list.",
261 definition->name, FieldCanonicalName((FieldId)field_id));
262}
263
264/**
265 * @brief Implementation of \ref PicurvWindowStoragePayloadCount().
266 * @see PicurvWindowStoragePayloadCount()
267 */
268PetscErrorCode PicurvWindowStoragePayloadCount(const PicurvWindowStorage *storage, PetscInt *count)
269{
270 PetscFunctionBeginUser;
271 PetscCheck(storage != NULL && count != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL,
272 "Storage and count output are required.");
273 *count = 3 + storage->field_count + storage->covariance_count;
274 /* A field without a requested second moment holds no product vector, so the
275 * enumeration skips it rather than emitting an empty payload. */
276 for (PetscInt field_index = 0; storage->m2 && field_index < storage->field_count; ++field_index) {
277 if (storage->m2[field_index]) *count += 1;
278 }
279 PetscFunctionReturn(0);
280}
281
282/**
283 * @brief Implementation of \ref PicurvWindowStoragePayload().
284 * @see PicurvWindowStoragePayload()
285 */
286PetscErrorCode PicurvWindowStoragePayload(UserCtx *user, const PicurvWindowDefinition *definition,
287 const PicurvWindowStorage *storage, PetscInt index,
289{
290 PetscInt total = 0;
291 PetscInt cursor = index;
292
293 PetscFunctionBeginUser;
294 PetscCheck(user != NULL && definition != NULL && storage != NULL && payload != NULL,
295 PETSC_COMM_SELF, PETSC_ERR_ARG_NULL,
296 "Context, definition, storage, and payload output are required.");
297 PetscCall(PicurvWindowStoragePayloadCount(storage, &total));
298 PetscCheck(index >= 0 && index < total, PETSC_COMM_SELF, PETSC_ERR_ARG_OUTOFRANGE,
299 "Payload index %" PetscInt_FMT " is outside [0, %" PetscInt_FMT ").", index, total);
300 PetscCall(PetscMemzero(payload, sizeof(*payload)));
301
302 /* Occupancy first, then means, then products, then co-moments. The order is the
303 * persistence contract: the manifest inventory, the writer, and the reader all
304 * walk it identically. */
305 if (cursor < 3) {
306 static const char *const occupancy_name[3] = {"count", "weight", "weight_sq"};
307 Vec occupancy[3];
308
309 occupancy[0] = storage->count;
310 occupancy[1] = storage->weight;
311 occupancy[2] = storage->weight_sq;
312 PetscCall(PetscStrncpy(payload->name, occupancy_name[cursor], sizeof(payload->name)));
313 payload->vec = occupancy[cursor];
314 payload->components = 1;
315 payload->role = "occupancy";
317 PetscFunctionReturn(0);
318 }
319 cursor -= 3;
320
321 if (cursor < storage->field_count) {
322 const FieldDescriptor *descriptor = NULL;
323
324 PetscCall(FieldGetDescriptor((FieldId)definition->fields[cursor].field_id, &descriptor));
325 PetscCall(PetscSNPrintf(payload->name, sizeof(payload->name), "%s_mean",
326 descriptor->canonical_name));
327 payload->vec = storage->mean[cursor];
328 payload->components = descriptor->dof;
329 payload->role = "mean";
330 payload->layout = FieldLayoutName(descriptor->layout);
331 PetscFunctionReturn(0);
332 }
333 cursor -= storage->field_count;
334
335 {
336 PetscInt product_count = 0;
337 PetscInt seen = 0;
338
339 for (PetscInt field_index = 0; storage->m2 && field_index < storage->field_count; ++field_index) {
340 if (storage->m2[field_index]) ++product_count;
341 }
342 if (cursor < product_count) {
343 for (PetscInt field_index = 0; field_index < storage->field_count; ++field_index) {
344 const FieldDescriptor *descriptor = NULL;
345
346 if (!storage->m2[field_index]) continue;
347 if (seen++ != cursor) continue;
348 PetscCall(FieldGetDescriptor((FieldId)definition->fields[field_index].field_id, &descriptor));
349 PetscCall(PetscSNPrintf(payload->name, sizeof(payload->name), "%s_m2",
350 descriptor->canonical_name));
351 payload->vec = storage->m2[field_index];
352 PetscCall(PicurvProductComponentCount(descriptor->dof, &payload->components));
353 payload->role = "second_moment";
354 payload->layout = FieldLayoutName(descriptor->layout);
355 PetscFunctionReturn(0);
356 }
357 }
358 cursor -= product_count;
359 }
360
361 {
362 const FieldDescriptor *first = NULL;
363 const FieldDescriptor *second = NULL;
364
365 PetscCheck(cursor >= 0 && cursor < storage->covariance_count, PETSC_COMM_SELF, PETSC_ERR_PLIB,
366 "Payload index %" PetscInt_FMT " fell through the storage enumeration.", index);
367 PetscCall(FieldGetDescriptor((FieldId)definition->covariances[cursor].first, &first));
368 PetscCall(FieldGetDescriptor((FieldId)definition->covariances[cursor].second, &second));
369 PetscCall(PetscSNPrintf(payload->name, sizeof(payload->name), "%s_%s_cm",
370 first->canonical_name, second->canonical_name));
371 payload->vec = storage->cm[cursor];
372 PetscCall(PicurvCovarianceComponentCount(first->dof, second->dof, &payload->components));
373 payload->role = "co_moment";
374 payload->layout = FieldLayoutName(first->layout);
375 }
376 PetscFunctionReturn(0);
377}
378
379/** @brief Output kinds a postprocessing recipe may request, in enumeration order. */
388
389/** @brief Recipe spellings of the output kinds. */
390static const char *const kDerivedKindName[DERIVED_KIND_COUNT] = {
391 "mean", "reynolds_stress", "rms", "tke", "flux"
392};
393
394/**
395 * @brief Power of the source field's reference scale each derived kind carries.
396 *
397 * A first moment and a standard deviation have the field's own units; a covariance,
398 * its trace, and a co-moment flux are quadratic in it. This is what the per-field
399 * scaling table alone cannot express, and why derived statistics were previously left
400 * non-dimensional rather than scaled by a velocity that would have been wrong for
401 * three of the five kinds.
402 */
404 1, /* mean - first moment, the field's own units */
405 2, /* reynolds_stress - covariance of one field with itself */
406 1, /* rms - standard deviation */
407 2, /* tke - half the trace of that covariance */
408 0 /* flux - a co-moment of two possibly different fields, so its
409 factor is their product and cannot be an exponent of
410 one scale; DERIVED_FLUX resolves it directly. */
411};
412
413/**
414 * @brief Internal helper: reports which output kinds a recipe requested.
415 * @details Local to this translation unit.
416 */
417static PetscErrorCode ParseDerivedKinds(const char *outputs, PetscBool wanted[DERIVED_KIND_COUNT])
418{
420 char *cursor = buffer;
421
422 PetscFunctionBeginUser;
423 for (PetscInt k = 0; k < DERIVED_KIND_COUNT; ++k) wanted[k] = PETSC_FALSE;
424 PetscCheck(outputs != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Output list is required.");
425 PetscCall(PetscStrncpy(buffer, outputs, sizeof(buffer)));
426
427 while (cursor && *cursor) {
428 char *comma = strchr(cursor, ',');
429 PetscBool matched = PETSC_FALSE;
430
431 if (comma) *comma = '\0';
432 TrimWhitespace(cursor);
433 if (cursor[0] != '\0') {
434 for (PetscInt k = 0; k < DERIVED_KIND_COUNT; ++k) {
435 if (strcmp(cursor, kDerivedKindName[k])) continue;
436 wanted[k] = PETSC_TRUE;
437 matched = PETSC_TRUE;
438 break;
439 }
440 PetscCheck(matched, PETSC_COMM_SELF, PETSC_ERR_ARG_WRONG,
441 "Unknown field-statistics output '%s'. Available outputs are "
442 "mean, reynolds_stress, rms, tke, and flux.", cursor);
443 }
444 cursor = comma ? comma + 1 : NULL;
445 }
446 PetscFunctionReturn(0);
447}
448
449/**
450 * @brief Internal helper: counts the derived fields each kind contributes.
451 * @details Local to this translation unit. A kind contributes nothing when the state
452 * it needs was never accumulated, so a recipe may name every output without
453 * having to know which window carries which moment.
454 */
455static PetscErrorCode DerivedKindExtent(const PicurvWindowDefinition *definition,
456 const PicurvWindowStorage *storage,
457 DerivedKind kind, PetscInt *count)
458{
459 PetscFunctionBeginUser;
460 *count = 0;
461 switch (kind) {
462 case DERIVED_MEAN:
463 *count = storage->field_count;
464 break;
466 case DERIVED_RMS:
467 for (PetscInt field_index = 0; storage->m2 && field_index < storage->field_count; ++field_index) {
468 const FieldDescriptor *descriptor = NULL;
469 PetscInt components = 0;
470
471 if (!storage->m2[field_index]) continue;
472 PetscCall(FieldGetDescriptor((FieldId)definition->fields[field_index].field_id, &descriptor));
473 PetscCall(PicurvProductComponentCount(descriptor->dof, &components));
474 /* A stress tensor emits every component; an RMS emits only the
475 * diagonal, because an off-diagonal has no square root to take. */
476 *count += (kind == DERIVED_REYNOLDS_STRESS) ? components : descriptor->dof;
477 }
478 break;
479 case DERIVED_TKE:
480 /* Turbulent kinetic energy is the trace of a three-vector's stress tensor,
481 * so it exists only for a vector field carrying a second moment. */
482 for (PetscInt field_index = 0; storage->m2 && field_index < storage->field_count; ++field_index) {
483 const FieldDescriptor *descriptor = NULL;
484
485 if (!storage->m2[field_index]) continue;
486 PetscCall(FieldGetDescriptor((FieldId)definition->fields[field_index].field_id, &descriptor));
487 if (descriptor->dof == 3) *count += 1;
488 }
489 break;
490 default:
491 *count = storage->covariance_count;
492 break;
493 }
494 PetscFunctionReturn(0);
495}
496
497/**
498 * @brief Implementation of \ref PicurvWindowDerivedCount().
499 * @see PicurvWindowDerivedCount()
500 */
501PetscErrorCode PicurvWindowDerivedCount(const PicurvWindowDefinition *definition,
502 const PicurvWindowStorage *storage,
503 const char *outputs, PetscInt *count)
504{
505 PetscBool wanted[DERIVED_KIND_COUNT];
506
507 PetscFunctionBeginUser;
508 PetscCheck(definition != NULL && storage != NULL && count != NULL,
509 PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Definition, storage, and count are required.");
510 PetscCall(ParseDerivedKinds(outputs, wanted));
511 *count = 0;
512 for (PetscInt k = 0; k < DERIVED_KIND_COUNT; ++k) {
513 PetscInt extent = 0;
514
515 if (!wanted[k]) continue;
516 PetscCall(DerivedKindExtent(definition, storage, (DerivedKind)k, &extent));
517 *count += extent;
518 }
519 PetscFunctionReturn(0);
520}
521
522/**
523 * @brief Internal helper: takes a square root of a variance that may be barely negative.
524 * @details Local to this translation unit. Centered accumulation can leave a variance
525 * a few ulps below zero when the signal is nearly constant. Clamping is
526 * confined to this one place, applies only under a root, and never touches
527 * stored state; a genuinely negative variance is a defect and is reported.
528 */
529static PetscErrorCode SafeStandardDeviation(PetscReal variance, const char *label, PetscReal *result)
530{
531 PetscFunctionBeginUser;
532 if (variance >= 0.0) {
533 *result = PetscSqrtReal(variance);
534 PetscFunctionReturn(0);
535 }
536 PetscCheck(variance >= -PICURV_STATISTICS_VARIANCE_FLOOR, PETSC_COMM_SELF, PETSC_ERR_FP,
537 "Derived variance for '%s' is %g, which is too negative to be floating-point "
538 "cancellation; the accumulated state is inconsistent.", label, (double)variance);
539 *result = 0.0;
540 PetscFunctionReturn(0);
541}
542
543/**
544 * @brief Internal helper: resolves which kind and member one derived index selects.
545 * @details Local to this translation unit. Walks the same order `DerivedKindExtent`
546 * counts, so the enumeration and the count cannot disagree.
547 */
548static PetscErrorCode ResolveDerivedIndex(const PicurvWindowDefinition *definition,
549 const PicurvWindowStorage *storage,
550 const PetscBool wanted[DERIVED_KIND_COUNT],
551 PetscInt index, DerivedKind *kind, PetscInt *offset)
552{
553 PetscFunctionBeginUser;
554 for (PetscInt k = 0; k < DERIVED_KIND_COUNT; ++k) {
555 PetscInt extent = 0;
556
557 if (!wanted[k]) continue;
558 PetscCall(DerivedKindExtent(definition, storage, (DerivedKind)k, &extent));
559 if (index < extent) {
560 *kind = (DerivedKind)k;
561 *offset = index;
562 PetscFunctionReturn(0);
563 }
564 index -= extent;
565 }
566 SETERRQ(PETSC_COMM_SELF, PETSC_ERR_ARG_OUTOFRANGE,
567 "Derived index is past the end of the requested output set.");
568}
569
570#undef __FUNCT__
571#define __FUNCT__ "PicurvWindowDerive"
572/**
573 * @brief Implementation of \ref PicurvWindowDerive().
574 * @see PicurvWindowDerive()
575 */
576PetscErrorCode PicurvWindowDerive(UserCtx *user, const PicurvWindowDefinition *definition,
577 const PicurvWindowStorage *storage, const char *outputs,
578 PetscInt index, Vec scalar_target, Vec vector_target,
579 PicurvDerivedField *field)
580{
581 PetscBool wanted[DERIVED_KIND_COUNT];
584 PetscReal ***weight_arr = NULL, ***weight_sq_arr = NULL, ***count_arr = NULL;
585 PetscScalar ****source = NULL, ****target = NULL;
586 const FieldDescriptor *descriptor = NULL;
587 DM source_dm = NULL;
588 Vec source_vec = NULL;
589 PetscInt offset = 0, slot = 0, member = 0;
590 /* Resolved where the branch knows which fields it read; 1.0 leaves the result in
591 * solver units, which is also what an undeclared scale falls back to. */
592 PetscReal dimensional_factor = 1.0;
593 /* The source's component count is not the output's: a scalar RMS reads a
594 * six-component tensor, so each needs its own DM. */
595 PetscInt source_components = 0;
596
597 PetscFunctionBeginUser;
599 PetscCheck(user != NULL && definition != NULL && storage != NULL && field != NULL,
600 PETSC_COMM_SELF, PETSC_ERR_ARG_NULL,
601 "Context, definition, storage, and output are required.");
602 PetscCheck(index >= 0, PETSC_COMM_SELF, PETSC_ERR_ARG_OUTOFRANGE,
603 "Derived index %" PetscInt_FMT " is negative.", index);
604 PetscCall(PetscMemzero(field, sizeof(*field)));
605 /* The output list is parsed once and the extents walked once. Resolving the
606 * index is what bounds-checks it, so counting first would repeat both. */
607 PetscCall(ParseDerivedKinds(outputs, wanted));
608 PetscCall(ResolveDerivedIndex(definition, storage, wanted, index, &kind, &offset));
609
610 /* Locate the accumulator slot and component this index selects. */
611 switch (kind) {
612 case DERIVED_MEAN:
613 slot = offset;
614 PetscCall(FieldGetDescriptor((FieldId)definition->fields[slot].field_id, &descriptor));
615 source_vec = storage->mean[slot];
616 field->components = descriptor->dof;
617 source_components = descriptor->dof;
618 PetscCall(PetscSNPrintf(field->name, sizeof(field->name), "%s_%s_mean",
619 definition->name, descriptor->canonical_name));
620 break;
621 case DERIVED_TKE:
622 for (slot = 0; slot < storage->field_count; ++slot) {
623 if (!storage->m2[slot]) continue;
624 PetscCall(FieldGetDescriptor((FieldId)definition->fields[slot].field_id, &descriptor));
625 if (descriptor->dof != 3) continue;
626 if (offset-- == 0) break;
627 }
628 source_vec = storage->m2[slot];
629 field->components = 1;
630 PetscCall(PicurvProductComponentCount(descriptor->dof, &source_components));
631 PetscCall(PetscSNPrintf(field->name, sizeof(field->name), "%s_%s_tke",
632 definition->name, descriptor->canonical_name));
633 break;
634 case DERIVED_FLUX:
635 slot = offset;
636 {
637 const FieldDescriptor *first = NULL;
638 const FieldDescriptor *second = NULL;
639
640 PetscCall(FieldGetDescriptor((FieldId)definition->covariances[slot].first, &first));
641 PetscCall(FieldGetDescriptor((FieldId)definition->covariances[slot].second, &second));
642 PetscCall(PicurvCovarianceComponentCount(first->dof, second->dof, &field->components));
643 source_components = field->components;
644 source_vec = storage->cm[slot];
645 if (user->simCtx->pps && user->simCtx->pps->dimensionalize) {
646 PetscReal first_scale = 1.0, second_scale = 1.0;
647
648 /* A co-moment carries the product of its two fields' dimensions. */
649 PetscCall(PicurvFieldReferenceScale(user->simCtx, first->canonical_name,
650 &first_scale, NULL, 0));
651 PetscCall(PicurvFieldReferenceScale(user->simCtx, second->canonical_name,
652 &second_scale, NULL, 0));
653 dimensional_factor = first_scale * second_scale;
654 }
655 PetscCall(PetscSNPrintf(field->name, sizeof(field->name), "%s_%s_%s_flux",
656 definition->name, first->canonical_name, second->canonical_name));
657 }
658 break;
659 default:
660 /* Stress and RMS both walk the fields carrying a product; stress emits every
661 * symmetric component, RMS only the diagonal. */
662 for (slot = 0; slot < storage->field_count; ++slot) {
663 PetscInt extent = 0;
664
665 if (!storage->m2[slot]) continue;
666 PetscCall(FieldGetDescriptor((FieldId)definition->fields[slot].field_id, &descriptor));
667 PetscCall(PicurvProductComponentCount(descriptor->dof, &extent));
668 if (kind == DERIVED_RMS) extent = descriptor->dof;
669 if (offset < extent) { member = offset; break; }
670 offset -= extent;
671 }
672 source_vec = storage->m2[slot];
673 field->components = 1;
674 PetscCall(PicurvProductComponentCount(descriptor->dof, &source_components));
675 if (kind == DERIVED_RMS) {
676 PetscCall(PetscSNPrintf(field->name, sizeof(field->name), "%s_%s_rms%s",
677 definition->name, descriptor->canonical_name,
678 descriptor->dof == 1 ? "" : kAxisName[member]));
679 } else if (descriptor->dof == 1) {
680 PetscCall(PetscSNPrintf(field->name, sizeof(field->name), "%s_%s_variance",
681 definition->name, descriptor->canonical_name));
682 } else {
683 char label[8];
684
685 PetscCall(ProductComponentLabel(member, label, sizeof(label)));
686 PetscCall(PetscSNPrintf(field->name, sizeof(field->name), "%s_%s_R_%s",
687 definition->name, descriptor->canonical_name, label));
688 }
689 break;
690 }
691
692 PetscCall(SpatialTargetPlanCreate(user, (FieldId)definition->fields[0].field_id,
694 PetscCall(PicurvStatisticsComponentDM(user, source_components, &source_dm));
695 {
696 Vec destination = (field->components == 1) ? scalar_target : vector_target;
697 DM destination_dm = NULL;
698
699 PetscCheck(destination != NULL, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL,
700 "Derived field '%s' needs a %d-component destination.",
701 field->name, (int)field->components);
702 PetscCall(PicurvStatisticsComponentDM(user, field->components, &destination_dm));
703 PetscCall(VecZeroEntries(destination));
704
705 PetscCall(DMDAVecGetArrayRead(user->da, storage->weight, &weight_arr));
706 PetscCall(DMDAVecGetArrayRead(user->da, storage->weight_sq, &weight_sq_arr));
707 PetscCall(DMDAVecGetArrayRead(user->da, storage->count, &count_arr));
708 PetscCall(DMDAVecGetArrayDOFRead(source_dm, source_vec, &source));
709 PetscCall(DMDAVecGetArrayDOF(destination_dm, destination, &target));
710
711 for (PetscInt k = plan.start[2]; k < plan.end[2]; ++k) {
712 for (PetscInt j = plan.start[1]; j < plan.end[1]; ++j) {
713 for (PetscInt i = plan.start[0]; i < plan.end[0]; ++i) {
715
716 /* A point the window never sampled has no average at all, so it
717 * is left at zero rather than divided by a zero weight. */
718 if (weight_arr[k][j][i] <= 0.0) continue;
719 pair.count = count_arr[k][j][i];
720 pair.weight = weight_arr[k][j][i];
721 pair.weight_sq = weight_sq_arr[k][j][i];
722 pair.mean_x = 0.0;
723 pair.mean_y = 0.0;
724
725 if (kind == DERIVED_MEAN) {
726 for (PetscInt c = 0; c < field->components; ++c) {
727 target[k][j][i][c] = source[k][j][i][c];
728 }
729 } else if (kind == DERIVED_TKE) {
730 /* The trace of the stress tensor, halved: components 0, 3
731 * and 5 are xx, yy and zz in the stored symmetric order. */
732 PetscReal trace = 0.0;
733
734 for (PetscInt c = 0; c < 3; ++c) {
735 pair.cm = source[k][j][i][ProductDiagonalIndex(c)];
736 trace += PicurvCoMomentStateCovariance(&pair);
737 }
738 target[k][j][i][0] = 0.5 * trace;
739 } else if (kind == DERIVED_RMS) {
740 PetscReal deviation = 0.0;
741
742 pair.cm = source[k][j][i][(descriptor->dof == 1)
743 ? 0 : ProductDiagonalIndex(member)];
745 field->name, &deviation));
746 target[k][j][i][0] = deviation;
747 } else if (kind == DERIVED_FLUX) {
748 for (PetscInt c = 0; c < field->components; ++c) {
749 pair.cm = source[k][j][i][c];
750 target[k][j][i][c] = PicurvCoMomentStateCovariance(&pair);
751 }
752 } else {
753 pair.cm = source[k][j][i][member];
754 target[k][j][i][0] = PicurvCoMomentStateCovariance(&pair);
755 }
756 }
757 }
758 }
759
760 PetscCall(DMDAVecRestoreArrayDOF(destination_dm, destination, &target));
761 PetscCall(DMDAVecRestoreArrayDOFRead(source_dm, source_vec, &source));
762 PetscCall(DMDAVecRestoreArrayRead(user->da, storage->count, &count_arr));
763 PetscCall(DMDAVecRestoreArrayRead(user->da, storage->weight_sq, &weight_sq_arr));
764 PetscCall(DMDAVecRestoreArrayRead(user->da, storage->weight, &weight_arr));
765
766 /* Applied once, here, so the VTK field and the convergence-history CSV - which
767 * both read this staging vector - cannot end up in different unit systems. */
768 if (user->simCtx->pps && user->simCtx->pps->dimensionalize) {
769 if (kind != DERIVED_FLUX && descriptor) {
770 PetscReal base = 1.0;
771
772 PetscCall(PicurvFieldReferenceScale(user->simCtx, descriptor->canonical_name,
773 &base, NULL, 0));
774 dimensional_factor = PetscPowRealInt(base, kDerivedKindScaleExponent[kind]);
775 }
776 if (PetscAbsReal(dimensional_factor - 1.0) > PETSC_MACHINE_EPSILON) {
777 PetscCall(VecScale(destination, dimensional_factor));
778 LOG_ALLOW(GLOBAL, LOG_DEBUG, "Scaled derived '%s' by %.4e.\n",
779 kDerivedKindName[kind], (double)dimensional_factor);
780 }
781 }
782 }
784 PetscFunctionReturn(0);
785}
786
787#undef __FUNCT__
788#define __FUNCT__ "PicurvWindowSpatialMean"
789/**
790 * @brief Implementation of \ref PicurvWindowSpatialMean().
791 * @see PicurvWindowSpatialMean()
792 */
793PetscErrorCode PicurvWindowSpatialMean(UserCtx *user, const PicurvWindowDefinition *definition,
794 const PicurvWindowStorage *storage, Vec field,
795 PetscReal *mean)
796{
798 /* Every direction collapses: a window mean is one number for the domain. */
799 const PetscBool whole_domain[3] = {PETSC_TRUE, PETSC_TRUE, PETSC_TRUE};
800
801 PetscFunctionBeginUser;
803 PetscCheck(user != NULL && definition != NULL && storage != NULL && field != NULL && mean != NULL,
804 PETSC_COMM_SELF, PETSC_ERR_ARG_NULL,
805 "Context, definition, storage, field, and output are required.");
806 *mean = 0.0;
807 if (definition->field_count == 0) { PROFILE_FUNCTION_END; PetscFunctionReturn(0); }
808
809 PetscCall(SpatialTargetPlanCreate(user, (FieldId)definition->fields[0].field_id,
811 /* No denominator means each admitted point counts once, so the result is the mean
812 * of the field over those points rather than a volume-weighted average. The
813 * accumulated weight is passed as the inclusion mask: a point the window never
814 * sampled holds a zero that means "never measured", and averaging it in would
815 * scale the answer down by the fraction of the domain the window never covered. */
816 PetscCall(PicurvSpatialRatioAverage(user, &plan, field, NULL, storage->weight,
817 whole_domain, PETSC_COMM_WORLD, NULL, mean));
819 PetscFunctionReturn(0);
820}
821
822#undef __FUNCT__
823#define __FUNCT__ "PicurvWindowValidFractionRange"
824/**
825 * @brief Implementation of \ref PicurvWindowValidFractionRange().
826 * @see PicurvWindowValidFractionRange()
827 */
828PetscErrorCode PicurvWindowValidFractionRange(UserCtx *user, const PicurvWindowDefinition *definition,
829 const PicurvWindowStorage *storage, PetscInt sample_count,
830 PetscReal *minimum, PetscReal *maximum)
831{
833 PetscReal ***nvert = NULL;
834 PetscReal ***count_arr = NULL;
835 PetscReal local_min = PETSC_MAX_REAL;
836 PetscReal local_max = 0.0;
837 PetscReal reduced_min = 0.0, reduced_max = 0.0;
838
839 PetscFunctionBeginUser;
841 PetscCheck(user != NULL && definition != NULL && storage != NULL &&
842 minimum != NULL && maximum != NULL,
843 PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Context, definition, storage, and outputs are required.");
844 *minimum = 1.0;
845 *maximum = 0.0;
846 if (definition->field_count == 0 || sample_count <= 0) { PROFILE_FUNCTION_END; PetscFunctionReturn(0); }
847
848 PetscCall(SpatialTargetPlanCreate(user, (FieldId)definition->fields[0].field_id,
850 PetscCall(DMDAVecGetArrayRead(user->da, user->Nvert, &nvert));
851 PetscCall(DMDAVecGetArrayRead(user->da, storage->count, &count_arr));
852 for (PetscInt k = plan.start[2]; k < plan.end[2]; ++k) {
853 for (PetscInt j = plan.start[1]; j < plan.end[1]; ++j) {
854 for (PetscInt i = plan.start[0]; i < plan.end[0]; ++i) {
855 /* The mask is evaluated at the current state, but a point excluded
856 * now may have contributed earlier, so its stored count is what
857 * decides its fraction rather than its present eligibility. */
858 const PetscReal fraction = count_arr[k][j][i] / (PetscReal)sample_count;
859
860 local_min = PetscMin(local_min, fraction);
861 local_max = PetscMax(local_max, fraction);
862 }
863 }
864 }
865 PetscCall(DMDAVecRestoreArrayRead(user->da, storage->count, &count_arr));
866 PetscCall(DMDAVecRestoreArrayRead(user->da, user->Nvert, &nvert));
867
868 /* A rank owning no targeted point must not drag the minimum down, so its
869 * sentinel is neutral under the reduction rather than zero. */
870 PetscCallMPI(MPI_Allreduce(&local_min, &reduced_min, 1, MPIU_REAL, MPIU_MIN, PETSC_COMM_WORLD));
871 PetscCallMPI(MPI_Allreduce(&local_max, &reduced_max, 1, MPIU_REAL, MPIU_MAX, PETSC_COMM_WORLD));
872 *minimum = (reduced_min == PETSC_MAX_REAL) ? 1.0 : reduced_min;
873 *maximum = reduced_max;
875 PetscFunctionReturn(0);
876}
877
878#undef __FUNCT__
879#define __FUNCT__ "PicurvWindowAccumulate"
880/**
881 * @brief Implementation of \ref PicurvWindowAccumulate().
882 * @see PicurvWindowAccumulate()
883 */
884PetscErrorCode PicurvWindowAccumulate(UserCtx *user, const PicurvWindowDefinition *definition,
885 PicurvWindowStorage *storage, PetscReal weight)
886{
888 PetscReal ***nvert = NULL;
889 PetscReal ***count_arr = NULL, ***weight_arr = NULL, ***weight_sq_arr = NULL;
890
891 PetscFunctionBeginUser;
893 PetscCheck(user != NULL && definition != NULL && storage != NULL,
894 PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, "Context, definition, and storage are required.");
895 PetscCheck(weight > 0.0, PETSC_COMM_SELF, PETSC_ERR_ARG_OUTOFRANGE,
896 "Accepted states carry a positive weight, got %g.", (double)weight);
897 if (definition->field_count == 0) { PROFILE_FUNCTION_END; PetscFunctionReturn(0); }
898
899 /* Every requested field shares the pointwise cell-centered domain, so the plan
900 * is resolved once rather than per field. */
901 PetscCall(SpatialTargetPlanCreate(user, (FieldId)definition->fields[0].field_id,
903
904 PetscCall(DMDAVecGetArrayRead(user->da, user->Nvert, &nvert));
905 PetscCall(DMDAVecGetArray(user->da, storage->count, &count_arr));
906 PetscCall(DMDAVecGetArray(user->da, storage->weight, &weight_arr));
907 PetscCall(DMDAVecGetArray(user->da, storage->weight_sq, &weight_sq_arr));
908
909 /* Pass one advances per-point occupancy, so every field pass afterwards can
910 * recover the pre-state weight uniformly as (stored weight - this weight).
911 * Occupancy is per point and per accepted state, never per field. */
912 for (PetscInt k = plan.start[2]; k < plan.end[2]; ++k) {
913 for (PetscInt j = plan.start[1]; j < plan.end[1]; ++j) {
914 for (PetscInt i = plan.start[0]; i < plan.end[0]; ++i) {
915 if (!SpatialTargetPlanMaskAllows(&plan, nvert[k][j][i])) continue;
916 count_arr[k][j][i] += 1.0;
917 weight_arr[k][j][i] += weight;
918 weight_sq_arr[k][j][i] += weight * weight;
919 }
920 }
921 }
922
923 /* Pass two updates every cross-field co-moment. It runs before any mean is
924 * written back, because a co-moment needs the pre-update mean of *both*
925 * members, and pass three overwrites them field by field. */
926 for (PetscInt pair = 0; pair < definition->covariance_count; ++pair) {
927 FieldView view_a, view_b;
928 PetscScalar ****src_a = NULL, ****mean_a = NULL;
929 PetscScalar ****src_b = NULL, ****mean_b = NULL;
930 PetscScalar ****co_moment = NULL;
931 DM pair_dm = NULL;
932 PetscInt slot_a = 0, slot_b = 0, dof_a = 0, dof_b = 0, components = 0;
933
934 PetscCall(FindFieldSlot(definition, definition->covariances[pair].first, &slot_a));
935 PetscCall(FindFieldSlot(definition, definition->covariances[pair].second, &slot_b));
936 PetscCall(FieldGetView(user, (FieldId)definition->covariances[pair].first, &view_a));
937 PetscCall(FieldGetView(user, (FieldId)definition->covariances[pair].second, &view_b));
938 dof_a = view_a.descriptor->dof;
939 dof_b = view_b.descriptor->dof;
940 PetscCall(PicurvCovarianceComponentCount(dof_a, dof_b, &components));
941 PetscCall(PicurvStatisticsComponentDM(user, components, &pair_dm));
942
943 /* A component-indexed view serves every degree of freedom, so the loop
944 * below needs no scalar-versus-vector branching. */
945 PetscCall(DMDAVecGetArrayDOFRead(view_a.dm, view_a.global_vec, &src_a));
946 PetscCall(DMDAVecGetArrayDOFRead(view_a.dm, storage->mean[slot_a], &mean_a));
947 PetscCall(DMDAVecGetArrayDOFRead(view_b.dm, view_b.global_vec, &src_b));
948 PetscCall(DMDAVecGetArrayDOFRead(view_b.dm, storage->mean[slot_b], &mean_b));
949 PetscCall(DMDAVecGetArrayDOF(pair_dm, storage->cm[pair], &co_moment));
950
951 for (PetscInt k = plan.start[2]; k < plan.end[2]; ++k) {
952 for (PetscInt j = plan.start[1]; j < plan.end[1]; ++j) {
953 for (PetscInt i = plan.start[0]; i < plan.end[0]; ++i) {
954 if (!SpatialTargetPlanMaskAllows(&plan, nvert[k][j][i])) continue;
955 for (PetscInt c = 0; c < components; ++c) {
956 /* A scalar member contributes its single value against every
957 * component of a vector member, which is what makes a
958 * vector-scalar covariance a three-component object. */
959 const PetscInt component_a = (dof_a == 3) ? c : 0;
960 const PetscInt component_b = (dof_b == 3) ? c : 0;
962
963 state.count = count_arr[k][j][i] - 1.0;
964 state.weight = weight_arr[k][j][i] - weight;
965 state.weight_sq = weight_sq_arr[k][j][i] - weight * weight;
966 state.mean_x = mean_a[k][j][i][component_a];
967 state.mean_y = mean_b[k][j][i][component_b];
968 state.cm = co_moment[k][j][i][c];
969 PetscCall(PicurvCoMomentStateUpdate(&state,
970 src_a[k][j][i][component_a],
971 src_b[k][j][i][component_b], weight));
972 co_moment[k][j][i][c] = state.cm;
973 }
974 }
975 }
976 }
977
978 PetscCall(DMDAVecRestoreArrayDOF(pair_dm, storage->cm[pair], &co_moment));
979 PetscCall(DMDAVecRestoreArrayDOFRead(view_b.dm, storage->mean[slot_b], &mean_b));
980 PetscCall(DMDAVecRestoreArrayDOFRead(view_b.dm, view_b.global_vec, &src_b));
981 PetscCall(DMDAVecRestoreArrayDOFRead(view_a.dm, storage->mean[slot_a], &mean_a));
982 PetscCall(DMDAVecRestoreArrayDOFRead(view_a.dm, view_a.global_vec, &src_a));
983 }
984
985 /* Pass three updates each requested field's mean and, when asked, its centered
986 * self-product. A three-vector's self-product is the six symmetric co-moments
987 * between component pairs, not three per-component variances, so the co-moment
988 * kernel drives every product component including the diagonal. */
989 for (PetscInt field_index = 0; field_index < definition->field_count; ++field_index) {
990 FieldView view;
991 PetscScalar ****src = NULL, ****mean = NULL, ****product = NULL;
992 DM product_dm = NULL;
993 PetscInt dof = 0, components = 0;
994 const PetscBool second = definition->fields[field_index].want_second;
995
996 PetscCall(FieldGetView(user, (FieldId)definition->fields[field_index].field_id, &view));
997 dof = view.descriptor->dof;
998 PetscCall(DMDAVecGetArrayDOFRead(view.dm, view.global_vec, &src));
999 PetscCall(DMDAVecGetArrayDOF(view.dm, storage->mean[field_index], &mean));
1000 if (second) {
1001 PetscCall(PicurvProductComponentCount(dof, &components));
1002 PetscCall(PicurvStatisticsComponentDM(user, components, &product_dm));
1003 PetscCall(DMDAVecGetArrayDOF(product_dm, storage->m2[field_index], &product));
1004 }
1005
1006 for (PetscInt k = plan.start[2]; k < plan.end[2]; ++k) {
1007 for (PetscInt j = plan.start[1]; j < plan.end[1]; ++j) {
1008 for (PetscInt i = plan.start[0]; i < plan.end[0]; ++i) {
1009 PetscReal prior_weight = 0.0;
1010 PetscReal prior_weight_sq = 0.0;
1011 PetscReal prior_count = 0.0;
1012
1013 if (!SpatialTargetPlanMaskAllows(&plan, nvert[k][j][i])) continue;
1014
1015 prior_weight = weight_arr[k][j][i] - weight;
1016 prior_weight_sq = weight_sq_arr[k][j][i] - weight * weight;
1017 prior_count = count_arr[k][j][i] - 1.0;
1018
1019 /* Products are updated before the means are written back,
1020 * because the co-moment kernel needs the pre-update means. */
1021 for (PetscInt c = 0; c < components; ++c) {
1022 const PetscInt a = (dof == 1) ? 0 : kProductFirst[c];
1023 const PetscInt b = (dof == 1) ? 0 : kProductSecond[c];
1024 PicurvCoMomentState state;
1025
1026 state.count = prior_count;
1027 state.weight = prior_weight;
1028 state.weight_sq = prior_weight_sq;
1029 state.mean_x = mean[k][j][i][a];
1030 state.mean_y = mean[k][j][i][b];
1031 state.cm = product[k][j][i][c];
1032 PetscCall(PicurvCoMomentStateUpdate(&state, src[k][j][i][a],
1033 src[k][j][i][b], weight));
1034 product[k][j][i][c] = state.cm;
1035 }
1036
1037 for (PetscInt c = 0; c < dof; ++c) {
1038 PicurvMomentState moment;
1039
1040 moment.count = prior_count;
1041 moment.weight = prior_weight;
1042 moment.weight_sq = prior_weight_sq;
1043 moment.mean = mean[k][j][i][c];
1044 moment.m2 = 0.0;
1045 PetscCall(PicurvMomentStateUpdate(&moment, src[k][j][i][c], weight));
1046 mean[k][j][i][c] = moment.mean;
1047 }
1048 }
1049 }
1050 }
1051
1052 if (second) PetscCall(DMDAVecRestoreArrayDOF(product_dm, storage->m2[field_index], &product));
1053 PetscCall(DMDAVecRestoreArrayDOF(view.dm, storage->mean[field_index], &mean));
1054 PetscCall(DMDAVecRestoreArrayDOFRead(view.dm, view.global_vec, &src));
1055 }
1056
1057 PetscCall(DMDAVecRestoreArray(user->da, storage->weight_sq, &weight_sq_arr));
1058 PetscCall(DMDAVecRestoreArray(user->da, storage->weight, &weight_arr));
1059 PetscCall(DMDAVecRestoreArray(user->da, storage->count, &count_arr));
1060 PetscCall(DMDAVecRestoreArrayRead(user->da, user->Nvert, &nvert));
1062 PetscFunctionReturn(0);
1063}
Authoritative identities and storage metadata for persistent Eulerian fields.
FieldLayout layout
const FieldDescriptor * descriptor
const char * FieldCanonicalName(FieldId field_id)
Return the canonical printable name for an ID.
PetscErrorCode FieldGetView(UserCtx *user, FieldId field_id, FieldView *view)
Resolve the existing DM and global/local vectors for one field.
@ FIELD_LAYOUT_CELL_CENTERED
const char * canonical_name
const char * FieldLayoutName(FieldLayout layout)
Return a stable printable label for a field layout.
PetscErrorCode FieldGetDescriptor(FieldId field_id, const FieldDescriptor **descriptor)
Return immutable metadata for a valid field identifier.
FieldId
Compile-time identity for a catalogued Eulerian field.
Immutable metadata for one field identity.
Non-owning runtime objects resolved for one field and UserCtx.
Public interface for data input/output routines.
PetscErrorCode PicurvFieldReferenceScale(SimCtx *simCtx, const char *field_name, PetscReal *scale, char *description, size_t description_length)
Physical scale one field is multiplied by to leave non-dimensional form.
Definition io.c:3197
void TrimWhitespace(char *str)
Removes leading and trailing ASCII whitespace from a mutable string.
Definition io.c:393
Logging utilities and macros for PETSc-based applications.
#define LOCAL
Logging scope definitions for controlling message output.
Definition logging.h:45
#define GLOBAL
Scope for global logging across all processes.
Definition logging.h:46
#define LOG_ALLOW(scope, level, fmt,...)
Logging macro that checks both the log level and whether the calling function is in the allowed-funct...
Definition logging.h:200
#define PROFILE_FUNCTION_END
Marks the end of a profiled code block.
Definition logging.h:894
@ LOG_DEBUG
Detailed debugging information.
Definition logging.h:32
#define PROFILE_FUNCTION_BEGIN
Marks the beginning of a profiled code block (typically a function).
Definition logging.h:885
static const PetscInt kProductFirst[6]
Upper-triangular row-major component pairs for a three-vector self-product.
PetscErrorCode PicurvProductComponentCount(PetscInt dof, PetscInt *count)
Implementation of PicurvProductComponentCount().
static const char *const kDerivedKindName[DERIVED_KIND_COUNT]
Recipe spellings of the output kinds.
static const char *const kAxisName[3]
Axis labels indexing the pair table above.
#define STATISTICS_DERIVED_OUTPUT_LENGTH
Longest output list a recipe may request.
static PetscErrorCode ParseDerivedKinds(const char *outputs, PetscBool wanted[DERIVED_KIND_COUNT])
Internal helper: reports which output kinds a recipe requested.
static PetscInt ProductDiagonalIndex(PetscInt component)
Internal helper: the product index carrying one component's own variance.
PetscErrorCode PicurvWindowDerive(UserCtx *user, const PicurvWindowDefinition *definition, const PicurvWindowStorage *storage, const char *outputs, PetscInt index, Vec scalar_target, Vec vector_target, PicurvDerivedField *field)
Implementation of PicurvWindowDerive().
static PetscErrorCode FindFieldSlot(const PicurvWindowDefinition *definition, PetscInt field_id, PetscInt *slot)
Internal helper: locates the storage slot holding one field's running mean.
static PetscErrorCode ResolveDerivedIndex(const PicurvWindowDefinition *definition, const PicurvWindowStorage *storage, const PetscBool wanted[DERIVED_KIND_COUNT], PetscInt index, DerivedKind *kind, PetscInt *offset)
Internal helper: resolves which kind and member one derived index selects.
DerivedKind
Output kinds a postprocessing recipe may request, in enumeration order.
@ DERIVED_REYNOLDS_STRESS
@ DERIVED_KIND_COUNT
PetscErrorCode PicurvWindowDerivedCount(const PicurvWindowDefinition *definition, const PicurvWindowStorage *storage, const char *outputs, PetscInt *count)
Implementation of PicurvWindowDerivedCount().
static PetscErrorCode ProductComponentLabel(PetscInt index, char *out, size_t size)
Internal helper: writes the two-axis label of one product component.
PetscErrorCode PicurvWindowStorageCreate(UserCtx *user, const PicurvWindowDefinition *definition, PicurvWindowStorage *storage)
Implementation of PicurvWindowStorageCreate().
PetscErrorCode PicurvCovarianceComponentCount(PetscInt dof_a, PetscInt dof_b, PetscInt *count)
Implementation of PicurvCovarianceComponentCount().
PetscErrorCode PicurvWindowStoragePayload(UserCtx *user, const PicurvWindowDefinition *definition, const PicurvWindowStorage *storage, PetscInt index, PicurvStatisticsPayload *payload)
Implementation of PicurvWindowStoragePayload().
static const PetscInt kProductSecond[6]
PetscErrorCode PicurvWindowValidFractionRange(UserCtx *user, const PicurvWindowDefinition *definition, const PicurvWindowStorage *storage, PetscInt sample_count, PetscReal *minimum, PetscReal *maximum)
Implementation of PicurvWindowValidFractionRange().
static const PetscInt kDerivedKindScaleExponent[DERIVED_KIND_COUNT]
Power of the source field's reference scale each derived kind carries.
static PetscErrorCode SafeStandardDeviation(PetscReal variance, const char *label, PetscReal *result)
Internal helper: takes a square root of a variance that may be barely negative.
PetscErrorCode PicurvStatisticsComponentDM(UserCtx *user, PetscInt components, DM *dm)
Implementation of PicurvStatisticsComponentDM().
PetscErrorCode PicurvWindowSpatialMean(UserCtx *user, const PicurvWindowDefinition *definition, const PicurvWindowStorage *storage, Vec field, PetscReal *mean)
Implementation of PicurvWindowSpatialMean().
static PetscErrorCode DerivedKindExtent(const PicurvWindowDefinition *definition, const PicurvWindowStorage *storage, DerivedKind kind, PetscInt *count)
Internal helper: counts the derived fields each kind contributes.
PetscErrorCode PicurvWindowAccumulate(UserCtx *user, const PicurvWindowDefinition *definition, PicurvWindowStorage *storage, PetscReal weight)
Implementation of PicurvWindowAccumulate().
PetscErrorCode PicurvWindowStorageDestroy(PicurvWindowStorage *storage)
Implementation of PicurvWindowStorageDestroy().
PetscErrorCode PicurvWindowStoragePayloadCount(const PicurvWindowStorage *storage, PetscInt *count)
Implementation of PicurvWindowStoragePayloadCount().
Per-window PETSc accumulator storage and pointwise application.
Vec weight
Per-point valid weight.
PetscInt components
Degrees of freedom the vector carries.
PetscInt components
One or three.
Vec weight_sq
Per-point squared-weight sum.
PetscInt field_count
Fields accumulated.
PetscInt covariance_count
Covariance pairs accumulated.
char name[96]
Output field name, window qualified.
#define PICURV_STATISTICS_VARIANCE_FLOOR
Tolerance within which a negative variance is treated as floating-point noise.
Vec * mean
One per field, matching that field's layout.
Vec vec
Borrowed accumulator vector; never owned by the caller.
Vec * m2
One per field; NULL when no second moment was requested.
const char * role
Inventory role: occupancy, mean, second_moment, co_moment.
Vec count
Per-point accepted sample count.
char name[96]
File basename, no extension.
const char * layout
Catalog layout name for the inventory entry.
Vec * cm
One per covariance pair.
One derived output field, resolved by enumeration index.
One checkpointable accumulator vector, resolved by enumeration index.
Independent accumulator state for one window on one block.
Weighted centered-moment kernels for the field-statistics pipeline.
PetscReal weight_sq
Sum of squared weights W2.
PetscReal weight_sq
Sum of squared weights W2.
PetscErrorCode PicurvCoMomentStateUpdate(PicurvCoMomentState *state, PetscReal value_x, PetscReal value_y, PetscReal weight)
Applies one weighted paired sample to a co-moment accumulator.
PetscReal mean_y
Weighted mean of the second member.
PetscReal weight
Total weight W.
PetscReal count
Number of accepted samples.
PetscReal cm
Centered co-moment sum C.
PetscReal m2
Centered second-moment sum M2.
PetscReal PicurvCoMomentStateCovariance(const PicurvCoMomentState *state)
Returns the weighted covariance C/W, or zero when no weight accumulated.
PetscReal mean
Weighted mean mu.
PetscReal mean_x
Weighted mean of the first member.
PetscErrorCode PicurvMomentStateUpdate(PicurvMomentState *state, PetscReal value, PetscReal weight)
Applies one weighted sample to a scalar moment accumulator.
PetscReal weight
Total weight W.
PetscReal count
Number of accepted samples.
Weighted centered co-moment state for one ordered pair of quantities.
Weighted centered state for one scalar quantity at one point.
Spatial target resolution for the field-statistics pipeline.
PetscBool SpatialTargetPlanMaskAllows(const SpatialTargetPlan *plan, PetscReal nvert_value)
Reports whether a point passes the plan's mask.
@ PICURV_STATISTICS_MASK_FLUID
PetscInt end[3]
Exclusive end per dimension (i, j, k).
PetscInt start[3]
Inclusive start per dimension (i, j, k).
PetscErrorCode SpatialTargetPlanGlobalPointCount(const SpatialTargetPlan *plan, MPI_Comm comm, PetscInt *count)
Counts the points contributed across a communicator.
PetscErrorCode SpatialTargetPlanCreate(UserCtx *user, FieldId field_id, PicurvStatisticsMask mask, SpatialTargetPlan *plan)
Resolves the iteration domain for one field on one block.
PetscErrorCode PicurvSpatialRatioAverage(UserCtx *user, const SpatialTargetPlan *plan, Vec numerator, Vec denominator, Vec inclusion, const PetscBool average_direction[3], MPI_Comm comm, Vec ratio, PetscReal *scalar)
Averages two fields over a target domain and divides the results.
Resolved iteration domain for one field on one block.
PetscInt first
First member; must also appear in the field list.
PicurvWindowFieldRequest fields[16]
PetscBool want_second
Also keep the centered second moment.
PetscInt second
Second member; must also appear in the field list.
PicurvWindowCovarianceRequest covariances[16]
PetscInt field_id
Catalogued Eulerian field identity.
The scientifically immutable definition of one window.
SimCtx * simCtx
Back-pointer to the master simulation context.
Definition variables.h:1076
PetscBool dimensionalize
Whether derived output leaves non-dimensional form, from global_operations.dimensionalize.
Definition variables.h:796
PetscInt _this
Definition variables.h:1091
PostProcessParams * pps
Definition variables.h:1057
Vec Nvert
Definition variables.h:1113
A symmetric second-order tensor stored by its six independent components.
Definition variables.h:138
User-defined context containing data specific to a single computational grid level.
Definition variables.h:1073