68 @brief Locate one statistics-window payload inside a checkpoint bundle.
69 @param[in] checkpoint_dir Checkpoint directory holding the window payloads.
70 @param[in] window Window name from monitor.yml.
71 @param[in] field Field name, e.g. Ucat.
72 @param[in] moment Moment name, e.g. first or second.
73 @param[in] block Block index.
74 @return Absolute path to the payload file.
76 wanted = (window.lower(), field.lower(), moment.lower())
78 for root, _dirs, files
in os.walk(checkpoint_dir):
80 if not name.endswith(
".dat"):
82 haystack = os.path.join(root, name).lower()
83 if all(token
in haystack
for token
in wanted)
and f
"block_{block:04d}" in haystack:
84 candidates.append(os.path.join(root, name))
87 f
"no payload found for window={window!r} field={field!r} moment={moment!r} "
88 f
"block={block} under {checkpoint_dir}.\n"
89 "Check that monitor.yml requested this moment and that the window reached "
90 "'active' or 'complete' before the checkpoint was written."
92 if len(candidates) > 1:
93 raise SystemExit(
"ambiguous payloads:\n " +
"\n ".join(sorted(candidates)))
112 @param[in] argv Command-line style argument list supplied to the function.
113 @return Process exit status.
115 parser = argparse.ArgumentParser(description=__doc__,
116 formatter_class=argparse.RawDescriptionHelpFormatter)
117 parser.add_argument(
"--checkpoint", required=
True,
118 help=
"Checkpoint directory containing the statistics window payloads.")
119 parser.add_argument(
"--grid", required=
True, help=
"Canonical PICGRID path for the run.")
120 parser.add_argument(
"--window", default=
"stationary", help=
"Statistics window name.")
121 parser.add_argument(
"--block", type=int, default=0, help=
"Block index.")
122 parser.add_argument(
"--wall-axis", default=
"Eta", choices=sorted(AXIS_TO_KJI),
123 help=
"Wall-normal axis token.")
124 parser.add_argument(
"--stream-axis", default=
"Zeta", choices=sorted(AXIS_TO_KJI),
125 help=
"Streamwise (driven) axis token.")
126 parser.add_argument(
"--viscosity", type=float, required=
True,
127 help=
"Kinematic viscosity in solver units (1/Re for length_ref=velocity_ref=1).")
128 parser.add_argument(
"--half-height", type=float, default=1.0,
129 help=
"Channel half-height h in solver units.")
130 parser.add_argument(
"--body-force", type=float,
131 help=
"Converged driving body force f. If given, u_tau = sqrt(f*h) is used "
132 "instead of the wall-shear estimate, which is the exact mean force balance.")
133 parser.add_argument(
"--output", required=
True, help=
"Output CSV path.")
134 args = parser.parse_args(argv)
137 numpy = helpers.require_numpy()
139 blocks = helpers.read_picgrid_blocks(args.grid)
140 if args.block >= len(blocks):
141 raise SystemExit(f
"block {args.block} is out of range; the grid holds {len(blocks)}.")
142 node_dims = blocks[args.block][
"dims"]
143 nodes = blocks[args.block][
"coords"]
145 wall_kji = AXIS_TO_KJI[args.wall_axis]
146 stream_c = AXIS_TO_COMPONENT[args.stream_axis]
147 wall_c = AXIS_TO_COMPONENT[args.wall_axis]
148 span_c = ({0, 1, 2} - {stream_c, wall_c}).pop()
150 mean_path =
find_payload(args.checkpoint, args.window,
"Ucat",
"first", args.block)
151 second_path =
find_payload(args.checkpoint, args.window,
"Ucat",
"second", args.block)
152 mean = helpers.extract_interior_cells(helpers.read_petsc_vec_binary(mean_path), node_dims)
153 second = helpers.extract_interior_cells(helpers.read_petsc_vec_binary(second_path), node_dims)
161 variance = numpy.maximum(second_p - mean_p ** 2, 0.0)
165 axis_nodes = {0: nodes[:, 0, 0, :], 1: nodes[0, :, 0, :], 2: nodes[0, 0, :, :]}[wall_kji]
166 axis_coord = axis_nodes[:, wall_c]
167 y_centres = 0.5 * (axis_coord[:-1] + axis_coord[1:])
168 if y_centres.size != mean_p.shape[0]:
169 raise SystemExit(f
"wall-normal cell count mismatch: grid gives {y_centres.size}, "
170 f
"statistics give {mean_p.shape[0]}.")
172 nu, h = args.viscosity, args.half_height
173 if args.body_force
is not None:
174 u_tau = math.sqrt(args.body_force * h)
175 source = f
"sqrt(f*h) with f={args.body_force:.8e}"
178 dudy = abs(mean_p[0, stream_c]) / abs(y_centres[0] - axis_coord[0])
179 u_tau = math.sqrt(nu * dudy)
180 source =
"sqrt(nu*du/dy) from the first cell (approximate; prefer --body-force)"
182 raise SystemExit(
"computed a non-positive friction velocity; check the inputs.")
184 wall_distance = numpy.minimum(y_centres - axis_coord[0], axis_coord[-1] - y_centres)
185 with open(args.output,
"w", encoding=
"utf-8")
as handle:
186 handle.write(f
"# u_tau = {u_tau:.10e} ({source})\n")
187 handle.write(f
"# Re_tau = u_tau*h/nu = {u_tau * h / nu:.6f}\n")
188 handle.write(f
"# nu = {nu:.10e}, h = {h:.10e}, window = {args.window}\n")
189 handle.write(
"y,y_plus,U_plus,u_rms_plus,v_rms_plus,w_rms_plus,"
190 "uv_plus,U_plus_loglaw,U_plus_sublayer\n")
191 for index
in range(y_centres.size):
192 y_plus = wall_distance[index] * u_tau / nu
193 loglaw = (1.0 / 0.41) * math.log(y_plus) + 5.2
if y_plus > 0.0
else float(
"nan")
195 f
"{y_centres[index]:.10e},{y_plus:.10e},"
196 f
"{mean_p[index, stream_c] / u_tau:.10e},"
197 f
"{math.sqrt(variance[index, stream_c]) / u_tau:.10e},"
198 f
"{math.sqrt(variance[index, wall_c]) / u_tau:.10e},"
199 f
"{math.sqrt(variance[index, span_c]) / u_tau:.10e},"
200 f
"{(second_p[index, stream_c] - mean_p[index, stream_c] ** 2) / (u_tau ** 2):.10e},"
201 f
"{loglaw:.10e},{y_plus:.10e}\n")
203 print(f
"u_tau = {u_tau:.10e} ({source})")
204 print(f
"Re_tau = {u_tau * h / nu:.4f}")
205 print(f
"wrote {y_centres.size} wall-normal stations to {args.output}")