43 @brief Read appended raw Float64 point arrays from a PICurv `.vts`.
44 @param[in] path VTK structured-grid file path.
45 @return Tuple of the node extent per direction and a name-to-array mapping.
48 raw = open(path,
"rb").read()
49 head = raw[:8192].decode(
"utf-8",
"replace")
50 extent = re.search(
r'WholeExtent="0 (\d+) 0 (\d+) 0 (\d+)"', head)
52 raise ValueError(f
"{path}: no WholeExtent in header.")
53 nodes = tuple(int(extent.group(i)) + 1
for i
in (1, 2, 3))
54 marker = raw.find(b
"<AppendedData")
56 raise ValueError(f
"{path}: no appended data section.")
57 start = raw.find(b
"_", marker) + 1
59 for match
in re.finditer(
60 r'Name="([^"]+)" NumberOfComponents="(\d+)" format="appended" offset="(\d+)"', head
62 name, ncomp, offset = match.group(1), int(match.group(2)), int(match.group(3))
64 nbytes = int(numpy.frombuffer(raw, dtype=
"<u4", count=1, offset=base)[0])
65 values = numpy.frombuffer(raw, dtype=
"<f8", count=nbytes // 8, offset=base + 4)
66 arrays[name] = values.reshape(-1, ncomp)
if ncomp > 1
else values
89 @brief Locate the highest-step derived statistics file for one window.
90 @param[in] viz_dir Directory holding the post-processing output.
91 @param[in] window Window name.
92 @return Path to the selected file.
94 pattern = re.compile(rf
"_statistics_{re.escape(window)}_(\d+)\.vts$")
95 best, best_step =
None, -1
96 for name
in sorted(os.listdir(viz_dir)):
97 match = pattern.search(name)
98 if match
and int(match.group(1)) > best_step:
99 best, best_step = os.path.join(viz_dir, name), int(match.group(1))
101 raise ValueError(f
"no derived statistics VTK output for window '{window}' in {viz_dir}.")
107 @brief Verify every derived array wraps across each periodic layout boundary.
109 @details Independent of whether the flow is turbulent, so a steady case still guards
110 the layout boundary. An interior-only producer leaves the two boundary
111 planes holding different partial averages, which this detects directly.
113 @param[in] arrays Name-to-array mapping from `read_vts_point_arrays()`.
114 @param[in] nodes Node extent per direction, in i, j, k order.
115 @param[in] periodic Per-direction periodicity in i, j, k order.
116 @return Zero when every array wraps, or one on the first disagreement.
120 for name, values
in sorted(arrays.items()):
121 if name ==
"Position":
123 ncomp = values.shape[1]
if values.ndim > 1
else 1
124 field = values.reshape(nodes[2], nodes[1], nodes[0], ncomp)
126 for flag, axis, label
in zip(periodic, (2, 1, 0), (
"i",
"j",
"k")):
129 low = numpy.take(field, 0, axis=axis)
130 high = numpy.take(field, field.shape[axis] - 1, axis=axis)
131 if not numpy.allclose(low, high):
132 print(f
"[FAIL] '{name}': the two layout boundary planes in the periodic "
133 f
"{label} direction differ, so the wrap was not applied. See "
134 f
"ExtendToLayoutBoundary().", file=sys.stderr)
137 print(f
"[INFO] periodic layout boundary wraps verified on {checked} array/direction pair(s).")
143 @brief Run the consistency check.
144 @param[in] argv Optional argument override for tests.
145 @return Process exit code.
148 args =
list(sys.argv[1:]
if argv
is None else argv)
150 print(USAGE, file=sys.stderr)
152 run_dir, viz_subdir, window = args
153 viz_dir = os.path.join(run_dir, viz_subdir)
160 if periodic
and any(periodic):
164 tke_name = next((n
for n
in arrays
if n.endswith(
"_tke")),
None)
166 print(f
"[SKIP] {os.path.basename(vts_path)} carries no turbulent kinetic energy field.")
168 field = arrays[tke_name].reshape(nodes[2], nodes[1], nodes[0])
171 (os.path.join(viz_dir, n)
for n
in sorted(os.listdir(viz_dir))
172 if n.endswith(f
"_statistics_{window}.csv")),
None
175 raise ValueError(f
"no convergence-history CSV for window '{window}' in {viz_dir}.")
176 rows =
list(csv.DictReader(open(csv_path,
"r", encoding=
"utf-8")))
177 if not rows
or "mean_tke" not in rows[0]:
178 print(f
"[SKIP] {os.path.basename(csv_path)} records no mean_tke column.")
180 reference = float(rows[-1][
"mean_tke"])
182 print(f
"[SKIP] window '{window}' has no accumulated energy to compare.")
187 interior = field[1:-1, 1:-1, 1:-1].mean()
188 deviation = abs(interior / reference - 1.0)
190 print(f
"[INFO] {window}: nodal interior mean {interior:.6e} vs CSV mean_tke "
191 f
"{reference:.6e} ({100*deviation:+.2f}%)")
192 if deviation > tolerance:
194 f
"[FAIL] derived statistics VTK and convergence CSV disagree by "
195 f
"{100*deviation:.2f}%, above the {100*tolerance:.0f}% interpolation "
196 f
"allowance. A layout boundary left unwritten by an interior-only "
197 f
"producer is the usual cause; see ExtendToLayoutBoundary().",
202 if periodic
and all(periodic):
208 whole_deviation = abs(whole / reference - 1.0)
209 print(f
"[INFO] {window}: whole-domain nodal mean {whole:.6e} "
210 f
"({100*whole_deviation:+.2f}%)")
211 if whole_deviation > tolerance:
213 f
"[FAIL] every boundary node is defined on a fully periodic layout, "
214 f
"but the whole-domain nodal mean is off by {100*whole_deviation:.2f}%. "
215 f
"The layout boundary was left unwritten; see ExtendToLayoutBoundary().",
220 print(
"[PASS] derived statistics VTK is consistent with the convergence CSV.")
222 except Exception
as exc:
223 print(f
"[ERROR] statistics nodal consistency check failed: {exc}", file=sys.stderr)