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