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

Functions

 find_repo_root ()
 Locate the PICurv checkout from the shipped example or from a case picurv init made.
 
 load_spectra_helpers ()
 Import the PICGRID and PETSc-Vec readers from generators/spectra.gen.
 
 read_window (checkpoint_dir, window, block)
 Resolve a statistics window by name inside one committed checkpoint bundle.
 
 homogeneous_statistics (checkpoint_dir, window_name, grid_path, block, homogeneous)
 Reduce one statistics window's velocity moments over homogeneous directions.
 
 friction_velocity_from_wall_model (csv_path, start, end)
 Mean wall shear the wall model applied over a time interval, as a velocity.
 
 main (argv=None)
 Entry point.
 

Variables

 REPO_ROOT = find_repo_root()
 
 SPECTRA_GEN = os.path.join(REPO_ROOT, "generators", "spectra.gen")
 
dict AXIS_TO_KJI = {"Xi": 2, "Eta": 1, "Zeta": 0}
 
dict AXIS_TO_COMPONENT = {"Xi": 0, "Eta": 1, "Zeta": 2}
 
tuple M2_PAIRS = ((0, 0), (0, 1), (0, 2), (1, 1), (1, 2), (2, 2))
 

Function Documentation

◆ find_repo_root()

wall_normal_profile.find_repo_root ( )

Locate the PICurv checkout from the shipped example or from a case picurv init made.

In the repository this file sits four levels below the root. A copy inside an initialized case does not, so the case's .picurv-origin.json, found by walking up from this file, supplies the checkout it was made from.

Returns
Absolute path to the checkout.

Definition at line 68 of file wall_normal_profile.py.

68def find_repo_root():
69 """!
70 @brief Locate the PICurv checkout from the shipped example or from a case `picurv init` made.
71 @details In the repository this file sits four levels below the root. A copy inside an
72 initialized case does not, so the case's `.picurv-origin.json`, found by
73 walking up from this file, supplies the checkout it was made from.
74 @return Absolute path to the checkout.
75 """
76 here = os.path.dirname(os.path.abspath(__file__))
77 shipped = os.path.abspath(os.path.join(here, "..", "..", "..", ".."))
78 if os.path.isfile(os.path.join(shipped, "generators", "spectra.gen")):
79 return shipped
80 current = here
81 while True:
82 origin = os.path.join(current, ".picurv-origin.json")
83 if os.path.isfile(origin):
84 with open(origin, encoding="utf-8") as handle:
85 return json.load(handle).get("source_repo_root", shipped)
86 parent = os.path.dirname(current)
87 if parent == current:
88 return shipped
89 current = parent
90
91

◆ load_spectra_helpers()

wall_normal_profile.load_spectra_helpers ( )

Import the PICGRID and PETSc-Vec readers from generators/spectra.gen.

Returns
The loaded module.

Definition at line 103 of file wall_normal_profile.py.

103def load_spectra_helpers():
104 """!
105 @brief Import the PICGRID and PETSc-Vec readers from generators/spectra.gen.
106 @return The loaded module.
107 """
108 if not os.path.isfile(SPECTRA_GEN):
109 raise SystemExit(f"cannot find {SPECTRA_GEN}; run this from a PICurv checkout "
110 "or a case initialized from one.")
111 loader = importlib.machinery.SourceFileLoader("picurv_spectra_gen", SPECTRA_GEN)
112 spec = importlib.util.spec_from_loader("picurv_spectra_gen", loader)
113 module = importlib.util.module_from_spec(spec)
114 loader.exec_module(module)
115 return module
116
117
Here is the caller graph for this function:

◆ read_window()

wall_normal_profile.read_window (   checkpoint_dir,
  window,
  block 
)

Resolve a statistics window by name inside one committed checkpoint bundle.

Parameters
[in]checkpoint_dirCheckpoint bundle directory holding checkpoint.meta.
[in]windowWindow name from monitor.yml.
[in]blockBlock index.
Returns
Tuple of (payload directory, window metadata dict, block node dims or None).

Definition at line 118 of file wall_normal_profile.py.

118def read_window(checkpoint_dir, window, block):
119 """!
120 @brief Resolve a statistics window by name inside one committed checkpoint bundle.
121 @param[in] checkpoint_dir Checkpoint bundle directory holding `checkpoint.meta`.
122 @param[in] window Window name from monitor.yml.
123 @param[in] block Block index.
124 @return Tuple of (payload directory, window metadata dict, block node dims or None).
125 """
126 sys.path.insert(0, REPO_ROOT)
127 from picurv_cli.core import _read_checkpoint_options
128
129 metadata = os.path.join(checkpoint_dir, "checkpoint.meta")
130 if not os.path.isfile(metadata):
131 raise SystemExit(f"{checkpoint_dir} is not a committed checkpoint bundle: no checkpoint.meta.")
132 options = _read_checkpoint_options(metadata)
133 names = []
134 for index in range(int(options.get("checkpoint_statistics_window_count", 0) or 0)):
135 prefix = f"checkpoint_statistics_window_{index}_"
136 names.append(options.get(prefix + "name"))
137 if names[-1] != window:
138 continue
139 info = {key[len(prefix):]: value for key, value in options.items() if key.startswith(prefix)}
140 payload_dir = os.path.join(checkpoint_dir, "statistics", f"window_{index:04d}", f"block_{block:04d}")
141 if not os.path.isdir(payload_dir):
142 raise SystemExit(f"window {window!r} has no payloads for block {block} under {payload_dir}.")
143 dims = None
144 if f"checkpoint_block_{block}_im" in options:
145 dims = tuple(int(options[f"checkpoint_block_{block}_{axis}"]) for axis in ("im", "jm", "km"))
146 return payload_dir, info, dims
147 raise SystemExit(f"no statistics window named {window!r} in {metadata}; it holds {names}.")
148
149
Here is the caller graph for this function:

◆ homogeneous_statistics()

wall_normal_profile.homogeneous_statistics (   checkpoint_dir,
  window_name,
  grid_path,
  block,
  homogeneous 
)

Reduce one statistics window's velocity moments over homogeneous directions.

The covariance over the homogeneous set is the mean of the per-cell temporal covariance M2/W plus the spatial covariance of the per-cell time means: <u_a' u_b'> = mean(M2_ab / W) + mean(U_a U_b) - mean(U_a) mean(U_b).

Parameters
[in]checkpoint_dirCommitted checkpoint bundle directory.
[in]window_nameStatistics window name from monitor.yml.
[in]grid_pathCanonical PICGRID path for the run.
[in]blockBlock index.
[in]homogeneousAxes to average over, as indices in (k, j, i) order.
Returns
Dict with numpy, nodes ((KM, JM, IM, 3) node coordinates), window (its checkpoint.meta scalars), mean (remaining axes + (3,)) and covariance (remaining axes + (6,), in M2_PAIRS order).

Definition at line 150 of file wall_normal_profile.py.

150def homogeneous_statistics(checkpoint_dir, window_name, grid_path, block, homogeneous):
151 """!
152 @brief Reduce one statistics window's velocity moments over homogeneous directions.
153 @details The covariance over the homogeneous set is the mean of the per-cell
154 temporal covariance M2/W plus the spatial covariance of the per-cell time
155 means: <u_a' u_b'> = mean(M2_ab / W) + mean(U_a U_b) - mean(U_a) mean(U_b).
156 @param[in] checkpoint_dir Committed checkpoint bundle directory.
157 @param[in] window_name Statistics window name from monitor.yml.
158 @param[in] grid_path Canonical PICGRID path for the run.
159 @param[in] block Block index.
160 @param[in] homogeneous Axes to average over, as indices in (k, j, i) order.
161 @return Dict with `numpy`, `nodes` ((KM, JM, IM, 3) node coordinates), `window`
162 (its checkpoint.meta scalars), `mean` (remaining axes + (3,)) and
163 `covariance` (remaining axes + (6,), in M2_PAIRS order).
164 """
165 helpers = load_spectra_helpers()
166 numpy = helpers.require_numpy()
167 payload_dir, window, meta_dims = read_window(checkpoint_dir, window_name, block)
168 blocks = helpers.read_picgrid_blocks(grid_path)
169 if block >= len(blocks):
170 raise SystemExit(f"block {block} is out of range; the grid holds {len(blocks)}.")
171 node_dims = blocks[block]["dims"]
172 if meta_dims is not None and tuple(meta_dims) != tuple(node_dims):
173 raise SystemExit(f"grid dimensions {node_dims} do not match the checkpoint's {meta_dims}.")
174
175 def payload(name, components):
176 values = helpers.read_petsc_vec_binary(os.path.join(payload_dir, f"{name}.dat"))
177 return helpers.extract_interior_cells(values, node_dims, components)
178
179 mean = payload("Ucat_mean", 3)
180 m2 = payload("Ucat_m2", 6)
181 weight = payload("weight", 1)[..., 0]
182 if not numpy.all(weight > 0.0):
183 raise SystemExit(f"window {window_name!r} has cells with no accepted weight.")
184
185 reduced_mean = numpy.mean(mean, axis=homogeneous)
186 temporal = numpy.mean(m2 / weight[..., None], axis=homogeneous)
187 spatial = numpy.stack(
188 [numpy.mean(mean[..., a] * mean[..., b], axis=homogeneous)
189 - reduced_mean[..., a] * reduced_mean[..., b] for a, b in M2_PAIRS], axis=-1)
190 return {"numpy": numpy, "nodes": blocks[block]["coords"], "window": window,
191 "mean": reduced_mean, "covariance": temporal + spatial}
192
193
Here is the call graph for this function:
Here is the caller graph for this function:

◆ friction_velocity_from_wall_model()

wall_normal_profile.friction_velocity_from_wall_model (   csv_path,
  start,
  end 
)

Mean wall shear the wall model applied over a time interval, as a velocity.

Each wall_model.csv row carries the wall-face mean and standard deviation of u_tau, so the row's mean of u_tau^2 is mean^2 + rms^2.

Parameters
[in]csv_pathPath to the run's wall_model.csv.
[in]startWindow effective start, in solver time.
[in]endWindow effective end, in solver time.
Returns
Tuple of (sqrt(<u_tau^2>), number of rows used).

Definition at line 194 of file wall_normal_profile.py.

194def friction_velocity_from_wall_model(csv_path, start, end):
195 """!
196 @brief Mean wall shear the wall model applied over a time interval, as a velocity.
197 @details Each wall_model.csv row carries the wall-face mean and standard deviation
198 of u_tau, so the row's mean of u_tau^2 is mean^2 + rms^2.
199 @param[in] csv_path Path to the run's wall_model.csv.
200 @param[in] start Window effective start, in solver time.
201 @param[in] end Window effective end, in solver time.
202 @return Tuple of (sqrt(<u_tau^2>), number of rows used).
203 """
204 total, rows = 0.0, 0
205 with open(csv_path, newline="", encoding="utf-8") as handle:
206 for row in csv.DictReader(handle):
207 if start <= float(row["time"]) <= end:
208 total += float(row["u_tau_mean"]) ** 2 + float(row["u_tau_rms"]) ** 2
209 rows += 1
210 if rows == 0:
211 raise SystemExit(f"{csv_path} has no rows with time in the window [{start}, {end}].")
212 return math.sqrt(total / rows), rows
213
214
Here is the caller graph for this function:

◆ main()

wall_normal_profile.main (   argv = None)

Entry point.

Parameters
[in]argvCommand-line style argument list supplied to the function.
Returns
Process exit status.

Definition at line 215 of file wall_normal_profile.py.

215def main(argv=None):
216 """!
217 @brief Entry point.
218 @param[in] argv Command-line style argument list supplied to the function.
219 @return Process exit status.
220 """
221 parser = argparse.ArgumentParser(description=__doc__,
222 formatter_class=argparse.RawDescriptionHelpFormatter)
223 parser.add_argument("--checkpoint", required=True,
224 help="Committed checkpoint bundle directory (output/checkpoints/step_NNN).")
225 parser.add_argument("--grid", required=True, help="Canonical PICGRID path for the run.")
226 parser.add_argument("--window", default="stationary", help="Statistics window name.")
227 parser.add_argument("--block", type=int, default=0, help="Block index.")
228 parser.add_argument("--wall-axis", default="Eta", choices=sorted(AXIS_TO_KJI),
229 help="Wall-normal axis token.")
230 parser.add_argument("--stream-axis", default="Zeta", choices=sorted(AXIS_TO_KJI),
231 help="Streamwise (driven) axis token.")
232 parser.add_argument("--viscosity", type=float, required=True,
233 help="Kinematic viscosity in solver units (1/Re for length_ref=velocity_ref=1).")
234 parser.add_argument("--half-height", type=float, default=1.0,
235 help="Channel half-height h in solver units.")
236 source = parser.add_mutually_exclusive_group()
237 source.add_argument("--body-force", type=float,
238 help="Converged driving body force f; u_tau = sqrt(f*h), the exact mean "
239 "force balance.")
240 source.add_argument("--wall-model-csv",
241 help="The run's wall_model.csv; u_tau = sqrt(<u_tau^2>) over the window.")
242 source.add_argument("--u-tau", type=float, help="Friction velocity supplied directly.")
243 parser.add_argument("--output", required=True, help="Output CSV path.")
244 args = parser.parse_args(argv)
245
246 wall_kji = AXIS_TO_KJI[args.wall_axis]
247 stream_c = AXIS_TO_COMPONENT[args.stream_axis]
248 wall_c = AXIS_TO_COMPONENT[args.wall_axis]
249 span_c = ({0, 1, 2} - {stream_c, wall_c}).pop()
250 homogeneous = tuple(axis for axis in (0, 1, 2) if axis != wall_kji)
251
252 stats = homogeneous_statistics(args.checkpoint, args.window, args.grid, args.block, homogeneous)
253 numpy, nodes, window = stats["numpy"], stats["nodes"], stats["window"]
254 plane_mean = stats["mean"]
255
256 def covariance(a, b):
257 return stats["covariance"][:, M2_PAIRS.index(tuple(sorted((a, b))))]
258
259 # Wall-normal cell-centre coordinates, taken from the node coordinates of the
260 # wall-normal axis. nodes is (KM, JM, IM, 3) in the same (k, j, i) order.
261 axis_nodes = {0: nodes[:, 0, 0, :], 1: nodes[0, :, 0, :], 2: nodes[0, 0, :, :]}[wall_kji]
262 axis_coord = axis_nodes[:, wall_c]
263 y_centres = 0.5 * (axis_coord[:-1] + axis_coord[1:])
264 if y_centres.size != plane_mean.shape[0]:
265 raise SystemExit(f"wall-normal cell count mismatch: grid gives {y_centres.size}, "
266 f"statistics give {plane_mean.shape[0]}.")
267
268 # Fold the upper half onto the lower one. Reflection reverses the wall-normal
269 # velocity, so <u'v'> changes sign; the orientation term makes the reported
270 # value the shear stress relative to the wall the point is measured from.
271 orientation = 1.0 if axis_coord[-1] > axis_coord[0] else -1.0
272 half = (y_centres.size + 1) // 2
273
274 def fold(values, sign=1.0):
275 return 0.5 * (values[:half] + sign * values[::-1][:half])
276
277 wall_distance = fold(numpy.minimum(numpy.abs(y_centres - axis_coord[0]),
278 numpy.abs(axis_coord[-1] - y_centres)))
279 U = fold(plane_mean[:, stream_c])
280 uu = fold(covariance(stream_c, stream_c))
281 vv = fold(covariance(wall_c, wall_c))
282 ww = fold(covariance(span_c, span_c))
283 uv = orientation * fold(covariance(stream_c, wall_c), -1.0)
284
285 nu, h = args.viscosity, args.half_height
286 if args.body_force is not None:
287 u_tau = math.sqrt(args.body_force * h)
288 source_note = f"sqrt(f*h) with f={args.body_force:.8e}"
289 elif args.wall_model_csv is not None:
290 start, end = float(window["effective_start"]), float(window["effective_end"])
291 u_tau, rows = friction_velocity_from_wall_model(args.wall_model_csv, start, end)
292 source_note = f"sqrt(<u_tau^2>) over {rows} wall_model.csv rows, t in [{start:g}, {end:g}]"
293 elif args.u_tau is not None:
294 u_tau = args.u_tau
295 source_note = "supplied with --u-tau"
296 else:
297 u_tau = math.sqrt(nu * abs(U[0]) / wall_distance[0])
298 source_note = "sqrt(nu*U/y) from the first cell (approximate; needs a resolved sublayer)"
299 if not u_tau > 0.0:
300 raise SystemExit("computed a non-positive friction velocity; check the inputs.")
301
302 with open(args.output, "w", encoding="utf-8") as handle:
303 handle.write(f"# u_tau = {u_tau:.10e} ({source_note})\n")
304 handle.write(f"# Re_tau = u_tau*h/nu = {u_tau * h / nu:.6f}\n")
305 handle.write(f"# nu = {nu:.10e}, h = {h:.10e}, window = {args.window} "
306 f"({window.get('state')}, {window.get('sample_count')} samples, "
307 f"represented time {float(window.get('represented_time', 'nan')):g})\n")
308 handle.write("y,y_plus,U_plus,u_rms_plus,v_rms_plus,w_rms_plus,"
309 "minus_uv_plus,U_plus_loglaw,U_plus_sublayer\n")
310 for index in range(half):
311 y_plus = wall_distance[index] * u_tau / nu
312 loglaw = (1.0 / 0.41) * math.log(y_plus) + 5.2 if y_plus > 0.0 else float("nan")
313 handle.write(
314 f"{wall_distance[index]:.10e},{y_plus:.10e},"
315 f"{U[index] / u_tau:.10e},"
316 f"{math.sqrt(max(uu[index], 0.0)) / u_tau:.10e},"
317 f"{math.sqrt(max(vv[index], 0.0)) / u_tau:.10e},"
318 f"{math.sqrt(max(ww[index], 0.0)) / u_tau:.10e},"
319 f"{-uv[index] / (u_tau ** 2):.10e},"
320 f"{loglaw:.10e},{y_plus:.10e}\n")
321
322 print(f"u_tau = {u_tau:.10e} ({source_note})")
323 print(f"Re_tau = {u_tau * h / nu:.4f}")
324 print(f"wrote {half} wall-normal stations to {args.output}")
325 return 0
326
327
int main(int argc, char **argv)
Entry point for the postprocessor executable.
Here is the call graph for this function:
Here is the caller graph for this function:

Variable Documentation

◆ REPO_ROOT

wall_normal_profile.REPO_ROOT = find_repo_root()

Definition at line 92 of file wall_normal_profile.py.

◆ SPECTRA_GEN

wall_normal_profile.SPECTRA_GEN = os.path.join(REPO_ROOT, "generators", "spectra.gen")

Definition at line 93 of file wall_normal_profile.py.

◆ AXIS_TO_KJI

dict wall_normal_profile.AXIS_TO_KJI = {"Xi": 2, "Eta": 1, "Zeta": 0}

Definition at line 96 of file wall_normal_profile.py.

◆ AXIS_TO_COMPONENT

dict wall_normal_profile.AXIS_TO_COMPONENT = {"Xi": 0, "Eta": 1, "Zeta": 2}

Definition at line 98 of file wall_normal_profile.py.

◆ M2_PAIRS

tuple wall_normal_profile.M2_PAIRS = ((0, 0), (0, 1), (0, 2), (1, 1), (1, 2), (2, 2))

Definition at line 100 of file wall_normal_profile.py.