PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
Loading...
Searching...
No Matches
test_statistics_window.c
Go to the documentation of this file.
1/**
2 * @file test_statistics_window.c
3 * @brief C unit tests for window lifecycle, scheduling, and weighting.
4 *
5 * Covers the Stage 3 acceptance items in
6 * @ref 60_Field_Statistics_Phase2_Implementation_Specification section 14:
7 * cadence stride, start and end clipping, variable timestep weighting, duplicate
8 * event rejection, off-schedule no-ops, and the property that sample and
9 * physical-time weighting agree on a constant-timestep run.
10 */
11
12#include "test_support.h"
13
14#include "statistics_window.h"
15#include "field_catalog.h"
16
17/** @brief Builds a step-cadence definition. */
18static PicurvWindowDefinition StepWindow(const char *name, PetscReal start, PetscReal end,
19 PetscBool bounded, PicurvWeighting weighting,
20 PetscInt cadence)
21{
23 memset(&d, 0, sizeof(d));
24 strncpy(d.name, name, PICURV_WINDOW_NAME_LENGTH - 1);
25 d.start_time = start; d.end_time = end; d.bounded = bounded;
26 d.weighting = weighting; d.cadence_kind = PICURV_CADENCE_STEP; d.step_cadence = cadence;
27 return d;
28}
29
30/** @brief Drives a uniform-dt sequence and returns the accumulated weight and count. */
31static PetscErrorCode RunUniform(PicurvWindow *w, PetscInt steps, PetscReal dt,
32 PetscReal *total_weight, PetscInt *count)
33{
34 PetscFunctionBeginUser;
35 for (PetscInt s = 0; s <= steps; ++s) {
36 PetscBool accepted = PETSC_FALSE;
37 PetscReal weight = 0.0;
38 PetscCall(PicurvWindowOfferState(w, s, (PetscReal)s * dt, &accepted, &weight));
39 }
40 *total_weight = w->total_weight;
41 *count = w->sample_count;
42 PetscFunctionReturn(0);
43}
44
45/**
46 * @brief The two weightings must agree on a constant-timestep run.
47 *
48 * This is the property that fixed the initial-state rule: counting the state at
49 * the window origin under sample weighting alone would make the two disagree by
50 * O(1/N) for no physical reason.
51 */
52static PetscErrorCode TestWeightingsAgreeAtConstantTimestep(void)
53{
54 PicurvWindow sample_w, time_w;
55 PetscReal sample_weight = 0.0, time_weight = 0.0;
56 PetscInt sample_count = 0, time_count = 0;
57 const PetscReal dt = 0.25;
58 const PetscInt steps = 20;
59
60 PetscFunctionBeginUser;
61 {
62 PicurvWindowDefinition d = StepWindow("s", 0.0, 0.0, PETSC_FALSE, PICURV_WEIGHTING_SAMPLE, 1);
63 PetscCall(PicurvWindowInit(&sample_w, &d));
64 }
65 {
66 PicurvWindowDefinition d = StepWindow("t", 0.0, 0.0, PETSC_FALSE, PICURV_WEIGHTING_PHYSICAL_TIME, 1);
67 PetscCall(PicurvWindowInit(&time_w, &d));
68 }
69 PetscCall(RunUniform(&sample_w, steps, dt, &sample_weight, &sample_count));
70 PetscCall(RunUniform(&time_w, steps, dt, &time_weight, &time_count));
71
72 /* The state at t=0 anchors the origin and is not a sample under either rule. */
73 PetscCall(PicurvAssertIntEqual(steps, sample_count, "sample weighting counts one per interval"));
74 PetscCall(PicurvAssertIntEqual(steps, time_count, "time weighting counts one per interval"));
75 PetscCall(PicurvAssertIntEqual(sample_count, time_count,
76 "both weightings must accept exactly the same states"));
77 PetscCall(PicurvAssertRealNear((PetscReal)steps, sample_weight, 1.0e-12, "sample total weight"));
78 PetscCall(PicurvAssertRealNear((PetscReal)steps * dt, time_weight, 1.0e-12, "time total weight"));
79 /* A mean is weight-normalized, so equal counts with proportional weights means
80 * the two weightings produce the identical mean for a uniform-dt run. */
81 PetscCall(PicurvAssertRealNear(dt, time_weight / sample_weight, 1.0e-12,
82 "weights differ only by the constant timestep"));
83 PetscCall(PicurvAssertRealNear((PetscReal)steps * dt, time_w.represented_time, 1.0e-12,
84 "represented time spans the whole run"));
85 PetscFunctionReturn(0);
86}
87
88/** @brief A state at the window origin anchors without becoming a sample. */
89static PetscErrorCode TestOriginStateAnchorsWithoutSampling(void)
90{
92 PicurvWindowDefinition d = StepWindow("anchor", 5.0, 0.0, PETSC_FALSE,
94 PetscBool accepted = PETSC_FALSE;
95 PetscReal weight = 0.0;
96
97 PetscFunctionBeginUser;
98 PetscCall(PicurvWindowInit(&w, &d));
99 PetscCall(PicurvAssertIntEqual(PICURV_WINDOW_PENDING, w.state, "window starts pending"));
100
101 /* Before the start: no effect at all. */
102 PetscCall(PicurvWindowOfferState(&w, 10, 4.5, &accepted, &weight));
103 PetscCall(PicurvAssertBool((PetscBool)!accepted, "states before the start are ignored"));
104 PetscCall(PicurvAssertIntEqual(PICURV_WINDOW_PENDING, w.state, "an early state does not activate"));
105
106 /* Exactly at the start: activates and anchors, but is not a sample. */
107 PetscCall(PicurvWindowOfferState(&w, 11, 5.0, &accepted, &weight));
108 PetscCall(PicurvAssertBool((PetscBool)!accepted, "the origin state is not a sample"));
109 PetscCall(PicurvAssertIntEqual(PICURV_WINDOW_ACTIVE, w.state, "the origin state activates the window"));
110 PetscCall(PicurvAssertIntEqual(0, w.sample_count, "anchoring records no sample"));
111
112 /* Next state carries the interval back to the origin. */
113 PetscCall(PicurvWindowOfferState(&w, 12, 5.5, &accepted, &weight));
114 PetscCall(PicurvAssertBool(accepted, "the state after the origin is a sample"));
115 PetscCall(PicurvAssertRealNear(0.5, weight, 1.0e-12, "first sample carries the interval from the origin"));
116 PetscFunctionReturn(0);
117}
118
119/** @brief A window first seen after its requested start moves its origin forward. */
120static PetscErrorCode TestLateFirstObservationMovesOrigin(void)
121{
122 PicurvWindow w;
123 PicurvWindowDefinition d = StepWindow("late", 5.0, 0.0, PETSC_FALSE,
125 PetscBool accepted = PETSC_FALSE;
126 PetscReal weight = 0.0;
127
128 PetscFunctionBeginUser;
129 PetscCall(PicurvWindowInit(&w, &d));
130 /* Resumed at t=8 with no earlier observation: the window must not claim [5,8]. */
131 PetscCall(PicurvWindowOfferState(&w, 40, 8.0, &accepted, &weight));
132 PetscCall(PicurvAssertBool((PetscBool)!accepted, "the first observed state anchors the moved origin"));
133 PetscCall(PicurvAssertRealNear(8.0, w.effective_start, 1.0e-12,
134 "effective start moves to the first observed time"));
135 PetscCall(PicurvWindowOfferState(&w, 41, 8.5, &accepted, &weight));
136 PetscCall(PicurvAssertBool(accepted, "the next state is a sample"));
137 PetscCall(PicurvAssertRealNear(0.5, weight, 1.0e-12, "weight is measured from the moved origin"));
138 PetscCall(PicurvAssertRealNear(0.5, w.represented_time, 1.0e-12,
139 "a window never claims time it did not observe"));
140 PetscFunctionReturn(0);
141}
142
143/** @brief Variable timestep weighting follows the actual elapsed intervals. */
144static PetscErrorCode TestVariableTimestepWeighting(void)
145{
146 PicurvWindow w;
147 PicurvWindowDefinition d = StepWindow("vardt", 0.0, 0.0, PETSC_FALSE,
149 const PetscReal times[4] = {0.0, 0.5, 2.0, 2.25};
150 const PetscReal expected[4] = {0.0, 0.5, 1.5, 0.25};
151 PetscBool accepted = PETSC_FALSE;
152 PetscReal weight = 0.0;
153
154 PetscFunctionBeginUser;
155 PetscCall(PicurvWindowInit(&w, &d));
156 for (PetscInt i = 0; i < 4; ++i) {
157 PetscCall(PicurvWindowOfferState(&w, i, times[i], &accepted, &weight));
158 PetscCall(PicurvAssertRealNear(expected[i], weight, 1.0e-12,
159 "variable-dt weight equals the elapsed interval"));
160 }
161 PetscCall(PicurvAssertIntEqual(3, w.sample_count, "three intervals become samples"));
162 PetscCall(PicurvAssertRealNear(2.25, w.total_weight, 1.0e-12, "weights sum to the span"));
163 PetscFunctionReturn(0);
164}
165
166/** @brief Stride skips states, and the accepted weight still spans the whole gap. */
167static PetscErrorCode TestStrideAndOffScheduleNoOp(void)
168{
169 PicurvWindow w;
170 PicurvWindowDefinition d = StepWindow("stride", 0.0, 0.0, PETSC_FALSE,
172 PetscBool accepted = PETSC_FALSE;
173 PetscReal weight = 0.0;
174 const PetscReal dt = 0.1;
175
176 PetscFunctionBeginUser;
177 PetscCall(PicurvWindowInit(&w, &d));
178 for (PetscInt s = 0; s <= 6; ++s) {
179 PetscReal before_weight = w.total_weight;
180 PetscInt before_count = w.sample_count;
181 PetscCall(PicurvWindowOfferState(&w, s, (PetscReal)s * dt, &accepted, &weight));
182 if (s % 3 != 0) {
183 PetscCall(PicurvAssertBool((PetscBool)!accepted, "off-schedule states are not sampled"));
184 PetscCall(PicurvAssertRealNear(before_weight, w.total_weight, 1.0e-15,
185 "an off-schedule state changes no scientific state"));
186 PetscCall(PicurvAssertIntEqual(before_count, w.sample_count,
187 "an off-schedule state records no sample"));
188 }
189 }
190 /* Steps 3 and 6 are sampled; each represents three timesteps. */
191 PetscCall(PicurvAssertIntEqual(2, w.sample_count, "stride 3 over 6 steps yields two samples"));
192 PetscCall(PicurvAssertRealNear(0.6, w.total_weight, 1.0e-12,
193 "strided weights still cover the full elapsed span"));
194 PetscFunctionReturn(0);
195}
196
197/** @brief A bounded window clips its final interval and then accepts nothing. */
198static PetscErrorCode TestEndClippingAndCompletion(void)
199{
200 PicurvWindow w;
201 PicurvWindowDefinition d = StepWindow("bounded", 0.0, 1.0, PETSC_TRUE,
203 PetscBool accepted = PETSC_FALSE;
204 PetscReal weight = 0.0;
205
206 PetscFunctionBeginUser;
207 PetscCall(PicurvWindowInit(&w, &d));
208 PetscCall(PicurvWindowOfferState(&w, 0, 0.0, &accepted, &weight)); /* anchor */
209 PetscCall(PicurvWindowOfferState(&w, 1, 0.4, &accepted, &weight));
210 PetscCall(PicurvAssertRealNear(0.4, weight, 1.0e-12, "interior interval"));
211 PetscCall(PicurvWindowOfferState(&w, 2, 0.8, &accepted, &weight));
212 PetscCall(PicurvAssertRealNear(0.4, weight, 1.0e-12, "interior interval"));
213
214 /* Overshoots the bound: the final weight clips to the requested end. */
215 PetscCall(PicurvWindowOfferState(&w, 3, 1.3, &accepted, &weight));
216 PetscCall(PicurvAssertBool(accepted, "the overshooting state still contributes its clipped interval"));
217 PetscCall(PicurvAssertRealNear(0.2, weight, 1.0e-12, "final interval clips to the requested end"));
218 PetscCall(PicurvAssertIntEqual(PICURV_WINDOW_COMPLETE, w.state, "reaching the bound completes the window"));
219 PetscCall(PicurvAssertRealNear(1.0, w.represented_time, 1.0e-12,
220 "represented time equals the requested span exactly"));
221 PetscCall(PicurvAssertRealNear(1.0, PicurvWindowProgress(&w), 1.0e-12, "a complete window reports full progress"));
222
223 /* Nothing further is accepted. */
224 PetscCall(PicurvWindowOfferState(&w, 4, 1.5, &accepted, &weight));
225 PetscCall(PicurvAssertBool((PetscBool)!accepted, "a complete window accepts nothing further"));
226 PetscCall(PicurvAssertIntEqual(3, w.sample_count, "completion does not add samples"));
227 PetscFunctionReturn(0);
228}
229
230/** @brief The same completed step offered twice is counted once. */
231static PetscErrorCode TestDuplicateEventRejected(void)
232{
233 PicurvWindow w;
234 PicurvWindowDefinition d = StepWindow("dup", 0.0, 0.0, PETSC_FALSE,
236 PetscBool accepted = PETSC_FALSE;
237 PetscReal weight = 0.0;
238
239 PetscFunctionBeginUser;
240 PetscCall(PicurvWindowInit(&w, &d));
241 PetscCall(PicurvWindowOfferState(&w, 0, 0.0, &accepted, &weight));
242 PetscCall(PicurvWindowOfferState(&w, 1, 0.5, &accepted, &weight));
243 PetscCall(PicurvAssertBool(accepted, "first offer of a step is accepted"));
244
245 PetscCall(PicurvWindowOfferState(&w, 1, 0.5, &accepted, &weight));
246 PetscCall(PicurvAssertBool((PetscBool)!accepted, "the same step must not be counted twice"));
247 PetscCall(PicurvAssertIntEqual(1, w.sample_count, "a duplicate offer records no extra sample"));
248 PetscCall(PicurvAssertRealNear(0.5, w.total_weight, 1.0e-12, "a duplicate offer adds no weight"));
249 PetscFunctionReturn(0);
250}
251
252/**
253 * @brief A step overshooting several time targets is accepted once, losing no time.
254 *
255 * This is the self-correcting property of right-rectangle weighting: the accepted
256 * weight is the actual elapsed interval, not the nominal cadence, so skipped
257 * targets neither drop nor double-count represented time.
258 */
259static PetscErrorCode TestTimeCadenceOvershootAcceptedOnce(void)
260{
261 PicurvWindow w;
263 PetscBool accepted = PETSC_FALSE;
264 PetscReal weight = 0.0;
265
266 PetscFunctionBeginUser;
267 memset(&d, 0, sizeof(d));
268 strncpy(d.name, "tcad", PICURV_WINDOW_NAME_LENGTH - 1);
269 d.start_time = 0.0; d.bounded = PETSC_FALSE;
272 PetscCall(PicurvWindowInit(&w, &d));
273
274 PetscCall(PicurvWindowOfferState(&w, 0, 0.0, &accepted, &weight)); /* anchor */
275 PetscCall(PicurvAssertBool((PetscBool)!accepted, "origin anchors under time cadence too"));
276
277 /* One large step jumps past targets 0.1, 0.2, 0.3, 0.4 and lands on 0.45. */
278 PetscCall(PicurvWindowOfferState(&w, 1, 0.45, &accepted, &weight));
279 PetscCall(PicurvAssertBool(accepted, "the overshooting state is accepted"));
280 PetscCall(PicurvAssertRealNear(0.45, weight, 1.0e-12,
281 "weight is the actual elapsed interval, not the nominal cadence"));
282 PetscCall(PicurvAssertIntEqual(1, w.sample_count, "several skipped targets yield exactly one sample"));
283
284 /* The schedule resumes on the absolute grid rather than drifting from 0.45. */
285 PetscCall(PicurvWindowOfferState(&w, 2, 0.46, &accepted, &weight));
286 PetscCall(PicurvAssertBool((PetscBool)!accepted, "the next target has not been reached yet"));
287 PetscCall(PicurvWindowOfferState(&w, 3, 0.51, &accepted, &weight));
288 PetscCall(PicurvAssertBool(accepted, "the state reaching the 0.5 target is accepted"));
289 PetscCall(PicurvAssertRealNear(0.06, weight, 1.0e-12, "weight resumes from the last accepted state"));
290 PetscCall(PicurvAssertRealNear(0.51, w.represented_time, 1.0e-12,
291 "no represented time is lost or double counted across the overshoot"));
292 PetscFunctionReturn(0);
293}
294
295/** @brief Invalid definitions are rejected at initialization. */
296static PetscErrorCode TestInvalidDefinitionsRejected(void)
297{
298 PicurvWindow w;
299 PetscErrorCode ierr_cadence = 0, ierr_name = 0, ierr_span = 0;
300
301 PetscFunctionBeginUser;
302 {
303 PicurvWindowDefinition d = StepWindow("zero", 0.0, 0.0, PETSC_FALSE, PICURV_WEIGHTING_SAMPLE, 0);
304 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
305 ierr_cadence = PicurvWindowInit(&w, &d);
306 PetscCall(PetscPopErrorHandler());
307 }
308 PetscCall(PicurvAssertIntEqual(PETSC_ERR_ARG_OUTOFRANGE, ierr_cadence, "non-positive stride is rejected"));
309 {
310 PicurvWindowDefinition d = StepWindow("", 0.0, 0.0, PETSC_FALSE, PICURV_WEIGHTING_SAMPLE, 1);
311 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
312 ierr_name = PicurvWindowInit(&w, &d);
313 PetscCall(PetscPopErrorHandler());
314 }
315 PetscCall(PicurvAssertIntEqual(PETSC_ERR_ARG_OUTOFRANGE, ierr_name, "an empty window name is rejected"));
316 {
317 PicurvWindowDefinition d = StepWindow("bad", 5.0, 5.0, PETSC_TRUE, PICURV_WEIGHTING_SAMPLE, 1);
318 PetscCall(PetscPushErrorHandler(PetscIgnoreErrorHandler, NULL));
319 ierr_span = PicurvWindowInit(&w, &d);
320 PetscCall(PetscPopErrorHandler());
321 }
322 PetscCall(PicurvAssertIntEqual(PETSC_ERR_ARG_OUTOFRANGE, ierr_span, "end must exceed start"));
323 PetscFunctionReturn(0);
324}
325
326/** @brief Hashes a definition and reports which property group changed against a baseline. */
327static PetscErrorCode HashAndCompare(const PicurvWindowDefinition *baseline,
328 const PicurvWindowDefinition *variant,
329 PetscBool *same, PetscInt *first_difference)
330{
331 char base_digest[65], variant_digest[65];
334
335 PetscFunctionBeginUser;
336 PetscCall(PicurvWindowComputeHash(baseline, base_digest, base_groups));
337 PetscCall(PicurvWindowComputeHash(variant, variant_digest, variant_groups));
338 PetscCall(PetscStrcmp(base_digest, variant_digest, same));
339 *first_difference = -1;
340 for (PetscInt group = 0; group < PICURV_WINDOW_HASH_GROUP_COUNT; ++group) {
341 PetscBool group_same = PETSC_FALSE;
342
343 PetscCall(PetscStrcmp(base_groups[group], variant_groups[group], &group_same));
344 if (!group_same) { *first_difference = group; break; }
345 }
346 PetscFunctionReturn(0);
347}
348
349/** @brief Builds the hash fixture: a Ucat/P window with a second moment and a covariance. */
351{
352 PicurvWindowDefinition d = StepWindow("production", 10.0, 20.0, PETSC_TRUE,
354 d.field_count = 2;
355 d.fields[0].field_id = FIELD_ID_UCAT; d.fields[0].want_second = PETSC_TRUE;
356 d.fields[1].field_id = FIELD_ID_P; d.fields[1].want_second = PETSC_FALSE;
357 d.covariance_count = 1;
360 return d;
361}
362
363/** @brief The hash is stable, and excludes exactly the properties the spec excludes. */
364static PetscErrorCode TestHashExclusionsAndStability(void)
365{
366 const PicurvWindowDefinition baseline = HashWindow();
368 PetscBool same = PETSC_FALSE;
369 PetscInt group = -1;
370
371 PetscFunctionBeginUser;
372 /* Stability: hashing the same definition twice gives the same digest. */
373 variant = baseline;
374 PetscCall(HashAndCompare(&baseline, &variant, &same, &group));
375 PetscCall(PicurvAssertBool(same, "hashing an unchanged definition is stable"));
376
377 /* end_time is excluded so a bounded window can be extended forward. */
378 variant = baseline;
379 variant.end_time = 40.0;
380 PetscCall(HashAndCompare(&baseline, &variant, &same, &group));
381 PetscCall(PicurvAssertBool(same, "extending end_time does not change the identity"));
382
383 /* Listing order is excluded so a reordered configuration still continues. */
384 variant = baseline;
385 variant.fields[0] = baseline.fields[1];
386 variant.fields[1] = baseline.fields[0];
387 PetscCall(HashAndCompare(&baseline, &variant, &same, &group));
388 PetscCall(PicurvAssertBool(same, "reordering the field list does not change the identity"));
389
390 /* A covariance pair is symmetric, so its member order is excluded too. */
391 variant = baseline;
392 variant.covariances[0].first = FIELD_ID_P;
393 variant.covariances[0].second = FIELD_ID_UCAT;
394 PetscCall(HashAndCompare(&baseline, &variant, &same, &group));
395 PetscCall(PicurvAssertBool(same, "swapping covariance members does not change the identity"));
396 PetscFunctionReturn(0);
397}
398
399/** @brief Every hashed property changes the digest and is named by its group digest. */
400static PetscErrorCode TestHashDetectsEachProperty(void)
401{
402 const PicurvWindowDefinition baseline = HashWindow();
404 PetscBool same = PETSC_FALSE;
405 PetscInt group = -1;
406 PetscBool named = PETSC_FALSE;
407
408 PetscFunctionBeginUser;
409 variant = baseline;
410 strncpy(variant.name, "other", PICURV_WINDOW_NAME_LENGTH - 1);
411 PetscCall(HashAndCompare(&baseline, &variant, &same, &group));
412 PetscCall(PicurvAssertBool((PetscBool)!same, "a renamed window is a different window"));
413 PetscCall(PicurvAssertIntEqual(0, group, "the name group reports the difference"));
414
415 variant = baseline;
416 variant.start_time = 11.0;
417 PetscCall(HashAndCompare(&baseline, &variant, &same, &group));
418 PetscCall(PicurvAssertBool((PetscBool)!same, "a moved start is a different window"));
419 PetscCall(PicurvAssertIntEqual(1, group, "the start_time group reports the difference"));
420
421 variant = baseline;
423 PetscCall(HashAndCompare(&baseline, &variant, &same, &group));
424 PetscCall(PicurvAssertBool((PetscBool)!same, "a changed weighting is a different window"));
425 PetscCall(PicurvAssertIntEqual(2, group, "the weighting group reports the difference"));
426
427 variant = baseline;
428 variant.step_cadence = 7;
429 PetscCall(HashAndCompare(&baseline, &variant, &same, &group));
430 PetscCall(PicurvAssertBool((PetscBool)!same, "a changed cadence is a different window"));
431 PetscCall(PicurvAssertIntEqual(3, group, "the cadence group reports the difference"));
432
433 /* Dropping a requested moment changes what the saved state means. */
434 variant = baseline;
435 variant.fields[0].want_second = PETSC_FALSE;
436 PetscCall(HashAndCompare(&baseline, &variant, &same, &group));
437 PetscCall(PicurvAssertBool((PetscBool)!same, "a changed moment set is a different window"));
438 PetscCall(PicurvAssertIntEqual(4, group, "the fields group reports the difference"));
439
440 variant = baseline;
441 variant.covariance_count = 0;
442 PetscCall(HashAndCompare(&baseline, &variant, &same, &group));
443 PetscCall(PicurvAssertBool((PetscBool)!same, "a dropped covariance is a different window"));
444 PetscCall(PicurvAssertIntEqual(5, group, "the covariances group reports the difference"));
445
446 /* Group names exist for every group and are bounded. */
447 for (PetscInt g = 0; g < PICURV_WINDOW_HASH_GROUP_COUNT; ++g) {
448 PetscCall(PetscStrcmp(PicurvWindowHashGroupName(g), "unknown", &named));
449 PetscCall(PicurvAssertBool((PetscBool)!named, "every hashed group has a stable name"));
450 }
451 PetscCall(PetscStrcmp(PicurvWindowHashGroupName(PICURV_WINDOW_HASH_GROUP_COUNT), "unknown", &named));
452 PetscCall(PicurvAssertBool(named, "an out-of-range group index is named unknown"));
453 PetscFunctionReturn(0);
454}
455
456/** @brief Serializes a definition's group digests the way a checkpoint records them. */
457static PetscErrorCode SerializeGroupDigests(const PicurvWindowDefinition *definition,
458 char *out, size_t out_size)
459{
460 char digest[65];
462 size_t used = 0;
463
464 PetscFunctionBeginUser;
465 out[0] = '\0';
466 PetscCall(PicurvWindowComputeHash(definition, digest, groups));
467 for (PetscInt group = 0; group < PICURV_WINDOW_HASH_GROUP_COUNT; ++group) {
468 PetscCall(PetscStrlen(out, &used));
469 PetscCall(PetscSNPrintf(out + used, out_size - used, "%s%s", group ? "," : "", groups[group]));
470 }
471 PetscFunctionReturn(0);
472}
473
474/**
475 * @brief Saved group digests must localize a change to the property that caused it.
476 *
477 * This is the only path that turns a refused continuation into an actionable
478 * message, and it is reachable at runtime only through a fatal error, so it is
479 * exercised directly here rather than through that path.
480 */
481static PetscErrorCode TestFirstHashDifferenceLocalization(void)
482{
483 const PicurvWindowDefinition baseline = HashWindow();
486 PetscInt group = 0;
487
488 PetscFunctionBeginUser;
489 PetscCall(SerializeGroupDigests(&baseline, saved, sizeof(saved)));
490
491 /* An unchanged definition reports no differing group. */
492 PetscCall(PicurvWindowFirstHashDifference(&baseline, saved, &group));
493 PetscCall(PicurvAssertIntEqual(-1, group, "an unchanged definition localizes to no group"));
494
495 /* Each changed property is localized to its own group. */
496 variant = baseline;
498 PetscCall(PicurvWindowFirstHashDifference(&variant, saved, &group));
499 PetscCall(PicurvAssertIntEqual(2, group, "a changed weighting localizes to the weighting group"));
500
501 variant = baseline;
502 variant.covariance_count = 0;
503 PetscCall(PicurvWindowFirstHashDifference(&variant, saved, &group));
504 PetscCall(PicurvAssertIntEqual(5, group, "a dropped covariance localizes to the covariances group"));
505
506 /* The earliest differing group wins when several changed at once. */
507 variant = baseline;
508 variant.start_time = 99.0;
509 variant.step_cadence = 3;
510 PetscCall(PicurvWindowFirstHashDifference(&variant, saved, &group));
511 PetscCall(PicurvAssertIntEqual(1, group, "the first differing group is the one reported"));
512
513 /* Degenerate inputs report no localization rather than a wrong property. */
514 PetscCall(PicurvWindowFirstHashDifference(&baseline, NULL, &group));
515 PetscCall(PicurvAssertIntEqual(-1, group, "absent group digests localize to nothing"));
516 PetscCall(PicurvWindowFirstHashDifference(&baseline, "", &group));
517 PetscCall(PicurvAssertIntEqual(-1, group, "an empty digest list localizes to nothing"));
518
519 variant = baseline;
520 variant.covariance_count = 0;
521 PetscCall(SerializeGroupDigests(&baseline, saved, sizeof(saved)));
522 saved[40] = '\0'; /* truncate mid-digest, so the last segment is short */
523 PetscCall(PicurvWindowFirstHashDifference(&variant, saved, &group));
524 PetscCall(PicurvAssertIntEqual(-1, group,
525 "a truncated digest list reports no property rather than a wrong one"));
526 PetscFunctionReturn(0);
527}
528
529/**
530 * @brief Entry point for the window lifecycle suite.
531 */
532int main(int argc, char **argv)
533{
534 PetscErrorCode ierr;
535 const PicurvTestCase cases[] = {
536 {"weightings-agree-at-constant-timestep", TestWeightingsAgreeAtConstantTimestep},
537 {"origin-state-anchors-without-sampling", TestOriginStateAnchorsWithoutSampling},
538 {"late-first-observation-moves-origin", TestLateFirstObservationMovesOrigin},
539 {"variable-timestep-weighting", TestVariableTimestepWeighting},
540 {"stride-and-off-schedule-no-op", TestStrideAndOffScheduleNoOp},
541 {"end-clipping-and-completion", TestEndClippingAndCompletion},
542 {"duplicate-event-rejected", TestDuplicateEventRejected},
543 {"time-cadence-overshoot-accepted-once", TestTimeCadenceOvershootAcceptedOnce},
544 {"invalid-definitions-rejected", TestInvalidDefinitionsRejected},
545 {"hash-exclusions-and-stability", TestHashExclusionsAndStability},
546 {"hash-detects-each-property", TestHashDetectsEachProperty},
547 {"first-hash-difference-localization", TestFirstHashDifferenceLocalization},
548 };
549
550 ierr = PetscInitialize(&argc, &argv, NULL, "PICurv statistics window tests");
551 if (ierr) return (int)ierr;
552 ierr = PicurvRunTests("unit-statistics-window", cases, sizeof(cases) / sizeof(cases[0]));
553 if (ierr) { PetscFinalize(); return (int)ierr; }
554 ierr = PetscFinalize();
555 return (int)ierr;
556}
Authoritative identities and storage metadata for persistent Eulerian fields.
@ FIELD_ID_UCAT
@ FIELD_ID_P
Window lifecycle, scheduling, and weighting for the field-statistics pipeline.
PetscErrorCode PicurvWindowComputeHash(const PicurvWindowDefinition *definition, char digest_hex[65], char group_digest_hex[][17])
Computes the resolved identity hash of one window definition.
#define PICURV_WINDOW_HASH_GROUP_COUNT
Number of independently hashed property groups in a window definition.
#define PICURV_WINDOW_HASH_GROUP_LENGTH
Stored length of one truncated group digest, including the terminator.
PetscReal effective_start
Origin of the first represented interval.
const char * PicurvWindowHashGroupName(PetscInt group)
Returns the stable name of one hashed property group.
PetscInt sample_count
PicurvWindowState state
PetscInt first
First member; must also appear in the field list.
PicurvWindowFieldRequest fields[16]
PetscReal time_cadence
Used when cadence_kind is time; must be positive.
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.
@ PICURV_WINDOW_PENDING
Requested start not yet reached.
@ PICURV_WINDOW_COMPLETE
Bounded end reached; accepts nothing further.
@ PICURV_WINDOW_ACTIVE
Accepting due states.
PetscErrorCode PicurvWindowInit(PicurvWindow *window, const PicurvWindowDefinition *definition)
Validates a definition and initializes a window to the pending state.
PetscErrorCode PicurvWindowOfferState(PicurvWindow *window, PetscInt step, PetscReal time, PetscBool *accepted, PetscReal *weight)
Offers one completed state to a window and reports the decision.
PetscBool want_second
Also keep the centered second moment.
PetscInt second
Second member; must also appear in the field list.
PicurvWindowCovarianceRequest covariances[16]
PetscBool bounded
False for an open-ended window.
PetscReal start_time
Requested start.
PetscInt field_id
Catalogued Eulerian field identity.
PetscReal PicurvWindowProgress(const PicurvWindow *window)
Reports the fraction of a bounded window's span that has been represented.
PicurvWeighting
How an accepted state's weight is determined.
@ PICURV_WEIGHTING_PHYSICAL_TIME
Weight is the represented interval.
@ PICURV_WEIGHTING_SAMPLE
Equal weight per accepted state.
@ PICURV_CADENCE_TIME
First state at or past each nominal time target.
@ PICURV_CADENCE_STEP
Every n completed steps from activation.
PetscReal represented_time
Physical time the window covers.
PetscErrorCode PicurvWindowFirstHashDifference(const PicurvWindowDefinition *definition, const char *saved_group_digests, PetscInt *group)
Reports which hashed property group first differs from saved group digests.
Runtime state of one window.
The scientifically immutable definition of one window.
static PetscErrorCode TestStrideAndOffScheduleNoOp(void)
Stride skips states, and the accepted weight still spans the whole gap.
static PetscErrorCode TestEndClippingAndCompletion(void)
A bounded window clips its final interval and then accepts nothing.
static PetscErrorCode HashAndCompare(const PicurvWindowDefinition *baseline, const PicurvWindowDefinition *variant, PetscBool *same, PetscInt *first_difference)
Hashes a definition and reports which property group changed against a baseline.
static PetscErrorCode TestDuplicateEventRejected(void)
The same completed step offered twice is counted once.
int main(int argc, char **argv)
Entry point for the window lifecycle suite.
static PetscErrorCode TestInvalidDefinitionsRejected(void)
Invalid definitions are rejected at initialization.
static PetscErrorCode TestOriginStateAnchorsWithoutSampling(void)
A state at the window origin anchors without becoming a sample.
static PetscErrorCode TestLateFirstObservationMovesOrigin(void)
A window first seen after its requested start moves its origin forward.
static PicurvWindowDefinition HashWindow(void)
Builds the hash fixture: a Ucat/P window with a second moment and a covariance.
static PetscErrorCode SerializeGroupDigests(const PicurvWindowDefinition *definition, char *out, size_t out_size)
Serializes a definition's group digests the way a checkpoint records them.
static PetscErrorCode TestHashExclusionsAndStability(void)
The hash is stable, and excludes exactly the properties the spec excludes.
static PetscErrorCode TestVariableTimestepWeighting(void)
Variable timestep weighting follows the actual elapsed intervals.
static PicurvWindowDefinition StepWindow(const char *name, PetscReal start, PetscReal end, PetscBool bounded, PicurvWeighting weighting, PetscInt cadence)
Builds a step-cadence definition.
static PetscErrorCode TestFirstHashDifferenceLocalization(void)
Saved group digests must localize a change to the property that caused it.
static PetscErrorCode TestHashDetectsEachProperty(void)
Every hashed property changes the digest and is named by its group digest.
static PetscErrorCode TestTimeCadenceOvershootAcceptedOnce(void)
A step overshooting several time targets is accepted once, losing no time.
static PetscErrorCode TestWeightingsAgreeAtConstantTimestep(void)
The two weightings must agree on a constant-timestep run.
static PetscErrorCode RunUniform(PicurvWindow *w, PetscInt steps, PetscReal dt, PetscReal *total_weight, PetscInt *count)
Drives a uniform-dt sequence and returns the accumulated weight and count.
PetscErrorCode PicurvAssertRealNear(PetscReal expected, PetscReal actual, PetscReal tol, const char *context)
Asserts that two real values agree within tolerance.
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 PicurvAssertIntEqual(PetscInt expected, PetscInt actual, const char *context)
Asserts that two integer values are equal.
PetscErrorCode PicurvAssertBool(PetscBool value, const char *context)
Asserts that one boolean condition is true.
Shared declarations for the PICurv C test fixture and assertion layer.
Named test case descriptor consumed by PicurvRunTests.