PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
Functions | Variables
check_statistics_nodal_consistency Namespace Reference

Functions

str sole_recipe_dir (str run_dir, str canonical_relative)
 Resolve the one per-recipe subdirectory beneath a canonical output home.
 
 require_numpy ()
 Import NumPy, which this checker needs to read the binary VTK payload.
 
 read_vts_point_arrays (path)
 Read appended raw Float64 point arrays from a PICurv .vts.
 
 read_checkpoint_periodicity (run_dir)
 Read the periodicity the solver recorded in any committed checkpoint.
 
 newest_window_vts (viz_dir, window)
 Locate the highest-step derived statistics file for one window.
 
 check_periodic_wrap (arrays, nodes, periodic)
 Verify every derived array wraps across each periodic layout boundary.
 
 main (argv=None)
 Run the consistency check.
 

Variables

str USAGE = "usage: check_statistics_nodal_consistency.py RUN_DIR WINDOW_NAME"
 

Function Documentation

◆ sole_recipe_dir()

str check_statistics_nodal_consistency.sole_recipe_dir ( str  run_dir,
str  canonical_relative 
)

Resolve the one per-recipe subdirectory beneath a canonical output home.

Parameters
[in]run_dirRun directory being inspected.
[in]canonical_relativeRun-relative canonical home, such as output/visualization.
Returns
Absolute path to the single recipe subdirectory.
Exceptions
ValueErrorwhen the home is absent or holds anything but one recipe.

Definition at line 32 of file check_statistics_nodal_consistency.py.

32def sole_recipe_dir(run_dir: str, canonical_relative: str) -> str:
33 """!
34 @brief Resolve the one per-recipe subdirectory beneath a canonical output home.
35 @param[in] run_dir Run directory being inspected.
36 @param[in] canonical_relative Run-relative canonical home, such as output/visualization.
37 @return Absolute path to the single recipe subdirectory.
38 @throws ValueError when the home is absent or holds anything but one recipe.
39 """
40 root = os.path.join(run_dir, canonical_relative)
41 if not os.path.isdir(root):
42 raise ValueError(f"{canonical_relative} does not exist under {run_dir}.")
43 entries = sorted(name for name in os.listdir(root)
44 if os.path.isdir(os.path.join(root, name)))
45 if len(entries) != 1:
46 raise ValueError(
47 f"expected exactly one recipe directory under {root}, found {entries}."
48 )
49 return os.path.join(root, entries[0])
50
51
Here is the caller graph for this function:

◆ require_numpy()

check_statistics_nodal_consistency.require_numpy ( )

Import NumPy, which this checker needs to read the binary VTK payload.

Returns
Imported NumPy module.

Definition at line 52 of file check_statistics_nodal_consistency.py.

52def require_numpy():
53 """!
54 @brief Import NumPy, which this checker needs to read the binary VTK payload.
55 @return Imported NumPy module.
56 """
57 import numpy
58 return numpy
59
60
Here is the caller graph for this function:

◆ read_vts_point_arrays()

check_statistics_nodal_consistency.read_vts_point_arrays (   path)

Read appended raw Float64 point arrays from a PICurv .vts.

Parameters
[in]pathVTK structured-grid file path.
Returns
Tuple of the node extent per direction and a name-to-array mapping.

Definition at line 61 of file check_statistics_nodal_consistency.py.

61def read_vts_point_arrays(path):
62 """!
63 @brief Read appended raw Float64 point arrays from a PICurv `.vts`.
64 @param[in] path VTK structured-grid file path.
65 @return Tuple of the node extent per direction and a name-to-array mapping.
66 """
67 numpy = require_numpy()
68 raw = open(path, "rb").read()
69 head = raw[:8192].decode("utf-8", "replace")
70 extent = re.search(r'WholeExtent="0 (\d+) 0 (\d+) 0 (\d+)"', head)
71 if not extent:
72 raise ValueError(f"{path}: no WholeExtent in header.")
73 nodes = tuple(int(extent.group(i)) + 1 for i in (1, 2, 3))
74 marker = raw.find(b"<AppendedData")
75 if marker < 0:
76 raise ValueError(f"{path}: no appended data section.")
77 start = raw.find(b"_", marker) + 1
78 arrays = {}
79 for match in re.finditer(
80 r'Name="([^"]+)" NumberOfComponents="(\d+)" format="appended" offset="(\d+)"', head
81 ):
82 name, ncomp, offset = match.group(1), int(match.group(2)), int(match.group(3))
83 base = start + offset
84 nbytes = int(numpy.frombuffer(raw, dtype="<u4", count=1, offset=base)[0])
85 values = numpy.frombuffer(raw, dtype="<f8", count=nbytes // 8, offset=base + 4)
86 arrays[name] = values.reshape(-1, ncomp) if ncomp > 1 else values
87 return nodes, arrays
88
89
Here is the call graph for this function:
Here is the caller graph for this function:

◆ read_checkpoint_periodicity()

check_statistics_nodal_consistency.read_checkpoint_periodicity (   run_dir)

Read the periodicity the solver recorded in any committed checkpoint.

Parameters
[in]run_dirRun directory to search.
Returns
Tuple of three booleans for i, j, k, or None when no checkpoint is present.

Definition at line 90 of file check_statistics_nodal_consistency.py.

90def read_checkpoint_periodicity(run_dir):
91 """!
92 @brief Read the periodicity the solver recorded in any committed checkpoint.
93 @param[in] run_dir Run directory to search.
94 @return Tuple of three booleans for i, j, k, or None when no checkpoint is present.
95 """
96 for root, _dirs, files in os.walk(os.path.join(run_dir, "output")):
97 if "checkpoint.meta" not in files:
98 continue
99 with open(os.path.join(root, "checkpoint.meta"), "r", encoding="utf-8") as stream:
100 for line in stream:
101 if line.startswith("-checkpoint_periodic "):
102 flags = line.split(None, 1)[1].strip().split(",")
103 return tuple(token.strip() == "1" for token in flags)
104 return None
105
106
Here is the caller graph for this function:

◆ newest_window_vts()

check_statistics_nodal_consistency.newest_window_vts (   viz_dir,
  window 
)

Locate the highest-step derived statistics file for one window.

Parameters
[in]viz_dirDirectory holding the post-processing output.
[in]windowWindow name.
Returns
Path to the selected file.

Definition at line 107 of file check_statistics_nodal_consistency.py.

107def newest_window_vts(viz_dir, window):
108 """!
109 @brief Locate the highest-step derived statistics file for one window.
110 @param[in] viz_dir Directory holding the post-processing output.
111 @param[in] window Window name.
112 @return Path to the selected file.
113 """
114 pattern = re.compile(rf"_statistics_{re.escape(window)}_(\d+)\.vts$")
115 best, best_step = None, -1
116 for name in sorted(os.listdir(viz_dir)):
117 match = pattern.search(name)
118 if match and int(match.group(1)) > best_step:
119 best, best_step = os.path.join(viz_dir, name), int(match.group(1))
120 if best is None:
121 raise ValueError(f"no derived statistics VTK output for window '{window}' in {viz_dir}.")
122 return best
123
124
Here is the caller graph for this function:

◆ check_periodic_wrap()

check_statistics_nodal_consistency.check_periodic_wrap (   arrays,
  nodes,
  periodic 
)

Verify every derived array wraps across each periodic layout boundary.

Independent of whether the flow is turbulent, so a steady case still guards the layout boundary. An interior-only producer leaves the two boundary planes holding different partial averages, which this detects directly.

Parameters
[in]arraysName-to-array mapping from read_vts_point_arrays().
[in]nodesNode extent per direction, in i, j, k order.
[in]periodicPer-direction periodicity in i, j, k order.
Returns
Zero when every array wraps, or one on the first disagreement.

Definition at line 125 of file check_statistics_nodal_consistency.py.

125def check_periodic_wrap(arrays, nodes, periodic):
126 """!
127 @brief Verify every derived array wraps across each periodic layout boundary.
128
129 @details Independent of whether the flow is turbulent, so a steady case still guards
130 the layout boundary. An interior-only producer leaves the two boundary
131 planes holding different partial averages, which this detects directly.
132
133 @param[in] arrays Name-to-array mapping from `read_vts_point_arrays()`.
134 @param[in] nodes Node extent per direction, in i, j, k order.
135 @param[in] periodic Per-direction periodicity in i, j, k order.
136 @return Zero when every array wraps, or one on the first disagreement.
137 """
138 numpy = require_numpy()
139 checked = 0
140 for name, values in sorted(arrays.items()):
141 if name == "Position":
142 continue
143 ncomp = values.shape[1] if values.ndim > 1 else 1
144 field = values.reshape(nodes[2], nodes[1], nodes[0], ncomp)
145 # checkpoint_periodic is recorded in i, j, k order; the array is k, j, i.
146 for flag, axis, label in zip(periodic, (2, 1, 0), ("i", "j", "k")):
147 if not flag:
148 continue
149 low = numpy.take(field, 0, axis=axis)
150 high = numpy.take(field, field.shape[axis] - 1, axis=axis)
151 if not numpy.allclose(low, high):
152 print(f"[FAIL] '{name}': the two layout boundary planes in the periodic "
153 f"{label} direction differ, so the wrap was not applied. See "
154 f"SynchronizePeriodicCellFields().", file=sys.stderr)
155 return 1
156 checked += 1
157 print(f"[INFO] periodic layout boundary wraps verified on {checked} array/direction pair(s).")
158 return 0
159
160
Here is the call graph for this function:
Here is the caller graph for this function:

◆ main()

check_statistics_nodal_consistency.main (   argv = None)

Run the consistency check.

Parameters
[in]argvOptional argument override for tests.
Returns
Process exit code.

Definition at line 161 of file check_statistics_nodal_consistency.py.

161def main(argv=None):
162 """!
163 @brief Run the consistency check.
164 @param[in] argv Optional argument override for tests.
165 @return Process exit code.
166 """
167 numpy = require_numpy()
168 args = list(sys.argv[1:] if argv is None else argv)
169 if len(args) != 2:
170 print(USAGE, file=sys.stderr)
171 return 2
172 run_dir, window = args
173 # The two artifacts no longer share a directory: derived VTK lands in the run's
174 # visualization home and the convergence history in its statistics home, each
175 # under the producing recipe's own identity.
176 viz_dir = sole_recipe_dir(run_dir, os.path.join("output", "visualization"))
177 stats_dir = sole_recipe_dir(run_dir, os.path.join("output", "analysis", "statistics"))
178
179 try:
180 vts_path = newest_window_vts(viz_dir, window)
181 nodes, arrays = read_vts_point_arrays(vts_path)
182
183 periodic = read_checkpoint_periodicity(run_dir)
184 if periodic and any(periodic):
185 if check_periodic_wrap(arrays, nodes, periodic) != 0:
186 return 1
187
188 tke_name = next((n for n in arrays if n.endswith("_tke")), None)
189 if tke_name is None:
190 print(f"[SKIP] {os.path.basename(vts_path)} carries no turbulent kinetic energy field.")
191 return 0
192 field = arrays[tke_name].reshape(nodes[2], nodes[1], nodes[0])
193
194 csv_path = next(
195 (os.path.join(stats_dir, n) for n in sorted(os.listdir(stats_dir))
196 if n.endswith(f"_statistics_{window}.csv")), None
197 )
198 if csv_path is None:
199 raise ValueError(f"no convergence-history CSV for window '{window}' in {stats_dir}.")
200 rows = list(csv.DictReader(open(csv_path, "r", encoding="utf-8")))
201 if not rows or "mean_tke" not in rows[0]:
202 print(f"[SKIP] {os.path.basename(csv_path)} records no mean_tke column.")
203 return 0
204 reference = float(rows[-1]["mean_tke"])
205 if reference <= 0.0:
206 print(f"[SKIP] window '{window}' has no accumulated energy to compare.")
207 return 0
208
209 # Interior nodes only: the outermost node layer is a periodic duplicate of the
210 # opposite face, so counting it weights that plane twice.
211 interior = field[1:-1, 1:-1, 1:-1].mean()
212 deviation = abs(interior / reference - 1.0)
213 tolerance = 0.05
214 print(f"[INFO] {window}: nodal interior mean {interior:.6e} vs CSV mean_tke "
215 f"{reference:.6e} ({100*deviation:+.2f}%)")
216 if deviation > tolerance:
217 print(
218 f"[FAIL] derived statistics VTK and convergence CSV disagree by "
219 f"{100*deviation:.2f}%, above the {100*tolerance:.0f}% interpolation "
220 f"allowance. A layout boundary left unwritten by an interior-only "
221 f"producer is the usual cause; see SynchronizePeriodicCellFields().",
222 file=sys.stderr,
223 )
224 return 1
225
226 if periodic and all(periodic):
227 # On a fully periodic layout every boundary node is defined, so the whole
228 # domain must also agree. This is the assertion that catches an unwritten
229 # layout boundary: the interior stays correct either way, so an
230 # interior-only comparison would pass straight through the defect.
231 whole = field.mean()
232 whole_deviation = abs(whole / reference - 1.0)
233 print(f"[INFO] {window}: whole-domain nodal mean {whole:.6e} "
234 f"({100*whole_deviation:+.2f}%)")
235 if whole_deviation > tolerance:
236 print(
237 f"[FAIL] every boundary node is defined on a fully periodic layout, "
238 f"but the whole-domain nodal mean is off by {100*whole_deviation:.2f}%. "
239 f"The layout boundary was left unwritten; see SynchronizePeriodicCellFields().",
240 file=sys.stderr,
241 )
242 return 1
243
244 print("[PASS] derived statistics VTK is consistent with the convergence CSV.")
245 return 0
246 except Exception as exc: # noqa: BLE001 - a check reports rather than raises
247 print(f"[ERROR] statistics nodal consistency check failed: {exc}", file=sys.stderr)
248 return 1
249
250
int main(int argc, char **argv)
Entry point for the postprocessor executable.
Head of a generic C-style linked list.
Definition variables.h:476
Here is the call graph for this function:
Here is the caller graph for this function:

Variable Documentation

◆ USAGE

str check_statistics_nodal_consistency.USAGE = "usage: check_statistics_nodal_consistency.py RUN_DIR WINDOW_NAME"

Definition at line 24 of file check_statistics_nodal_consistency.py.