123 for d, wrap
in enumerate((
True,
False,
True)):
124 sp, dp = src_pos[d], dst_pos[d]
127 spx = np.concatenate([sp - 1.0, sp, sp + 1.0])
128 idx = np.clip(np.searchsorted(spx, dp) - 1, 0, len(spx) - 2)
129 w = (dp - spx[idx]) / (spx[idx + 1] - spx[idx])
130 i0, i1 = idx % n, (idx + 1) % n
132 idx = np.clip(np.searchsorted(sp, dp) - 1, 0, n - 2)
133 w = np.clip((dp - sp[idx]) / (sp[idx + 1] - sp[idx]), 0.0, 1.0)
134 i0, i1 = idx, idx + 1
135 sl = [
slice(
None)] * 3
136 sl0, sl1 = list(sl), list(sl)
137 sl0[d], sl1[d] = i0, i1
140 out = out[tuple(sl0)] * (1.0 - w.reshape(shape)) + out[tuple(sl1)] * w.reshape(shape)
141 return np.asfortranarray(out)
145 nxt, nyt, nzt = 192, 160, 96
146 ux =
tbf_read(os.path.join(TBF,
"ux"), (nxt + 1, nyt, nzt))
147 uy =
tbf_read(os.path.join(TBF,
"uy"), (nxt, nyt + 1, nzt))
148 uz =
tbf_read(os.path.join(TBF,
"uz"), (nxt, nyt, nzt + 1))
149 fx = np.arange(nxt + 1) / nxt
150 cx = (np.arange(nxt) + 0.5) / nxt
151 fy = np.arange(nyt + 1) / nyt
152 cy = (np.arange(nyt) + 0.5) / nyt
153 fz = np.arange(nzt + 1) / nzt
154 cz = (np.arange(nzt) + 0.5) / nzt
155 dfx = np.arange(NX) / NX
156 dcx = (np.arange(NX) + 0.5) / NX
157 dfy = np.arange(NY) / NY
158 dcy = (np.arange(NY) + 0.5) / NY
159 dfz = np.arange(NZ) / NZ
160 dcz = (np.arange(NZ) + 0.5) / NZ
164 return (np.asfortranarray(u * S), np.asfortranarray(v * S), np.asfortranarray(w * S))
168 y = (np.arange(NY) + 0.5) / NY * LY
169 prof = 1.5 * 0.6113 * (1.0 - (y - 1.0) ** 2)
170 rng = np.random.default_rng(7)
171 u = np.broadcast_to(prof[
None, :,
None], (NX, NY, NZ)).copy()
172 u += 0.15 * 0.6113 * rng.standard_normal((NX, NY, NZ))
173 v = 0.05 * 0.6113 * rng.standard_normal((NX, NY, NZ))
174 w = 0.05 * 0.6113 * rng.standard_normal((NX, NY, NZ))
178 return (np.asfortranarray(u * S), np.asfortranarray(v * S), np.asfortranarray(w * S))
183 ax = (np.arange(nx)[:,
None] + (np.arange(sub)[
None, :] + 0.5) / sub).ravel()
184 ay = (np.arange(ny)[:,
None] + (np.arange(sub)[
None, :] + 0.5) / sub).ravel()
185 X, Y = ax[:,
None], ay[
None, :]
186 half = np.sqrt(np.maximum(R * R - (X - c[0]) ** 2 - (Y - c[1]) ** 2, 0.0))
187 z0, z1 = c[2] - half, c[2] + half
188 C = np.zeros((nx, ny, nz))
190 seg = np.maximum(np.minimum(z1, k + 1) - np.maximum(z0, k), 0.0)
191 C[:, :, k] = seg.reshape(nx, sub, ny, sub).
mean(axis=(1, 3))
192 return np.asfortranarray(C)
348 resume = os.path.exists(CKPT)
350 print(
"TBFsolver channel_18 on the peclet BLOCK VoF container — WO-W3 (statistically steady)")
352 print(f
" grid {NX} x {NY} x {NZ} isotropic (TBFsolver: 192 x 160 x 96 anisotropic)")
353 print(f
" box {LXC:.4f} x {LY:.4f} x {LZC:.4f} h (case: {LX:.4f} x {LY:.4f} x {LZ:.4f} h)")
354 print(f
" Delta+ = {RET/(NY/2):.2f}, D = {2*Rc:.1f} cells, 18 bubbles, "
355 f
"u_tau = {UTAU:.6f}, tau_w = {TAUW:.6e}")
356 print(f
" target {TARGET} eddy turnovers; statistics start at t u_tau/h = {STATS_START}; "
357 f
"sample every {SAMPLE} steps; wall budget {WALL:.0f} s")
358 print(f
" checkpoint {CKPT} ({'RESUME' if resume else 'fresh start'}), out {OUT}")
360 s = pf.Solver(NX, NY, NZ)
363 s.set_domain_bc(
"-y",
"wall", (0, 0, 0))
364 s.set_domain_bc(
"+y",
"wall", (0, 0, 0))
365 s.set_pressure_geometry(np.full((NX, NY, NZ), 10.0, order=
"F"))
366 s.set_pressure_chebyshev(
True, 800, 1e-10)
368 step0, t, acc = 0, 0.0,
new_acc()
369 alpha0, pres =
None,
None
371 step0, t, acc, blocks, (u, v, w), alpha0, pres =
load_ckpt(CKPT)
372 print(f
" resumed at step {step0}, t u_tau/h = {t*UTAU:.4f}, "
373 f
"{int(acc['_n'])} samples already accumulated, {len(blocks)} markers")
375 if os.path.isdir(TBF):
377 print(f
" initial condition: TBFsolver's own snapshot, resampled ({TBF})")
380 print(
" initial condition: a perturbed parabolic profile (snapshot not found)")
382 for c, a
in enumerate((u, v, w)):
384 print(f
" bulk velocity: U_b = {float(u.mean())/S:.4f}")
388 C = np.zeros((NX, NY, NZ), order=
"F")
392 cx = (i + 0.5) * NX / NBX
394 cz = (k + 0.5) * NZ / NBZ
397 lo = [int(math.floor(q - Rc)) - 1
for q
in (cx, cy, cz)]
398 hi = [int(math.ceil(q + Rc)) + 1
for q
in (cx, cy, cz)]
399 boxes.append([lo[0], lo[1], lo[2], hi[0], hi[1], hi[2]])
400 s.set_vof(np.asfortranarray(C))
401 alpha0 = float(C.sum()) / (NX * NY * NZ)
403 C = np.zeros((NX, NY, NZ), order=
"F")
405 rho_av = RHO_L + (RHO_G - RHO_L) * alpha0
406 f0 = S * (TAUW + (RHO_L - rho_av) * GCH)
407 f1 = S * (RHO_G - RHO_L) * GCH
408 s.set_property_model(
"rho",
"linear",
"C", [RHO_L, RHO_G - RHO_L])
409 s.set_property_model(
"mu",
"linear",
"C", [S * S * MU, 0.0])
410 s.set_property_model(
"force_x",
"linear",
"C", [f0, f1])
411 s.set_surface_tension(S ** 3 * SIGMA)
413 s.enable_vof_blocks_from_field(boxes)
415 s.enable_vof_blocks_from_colors([b
for b, _
in blocks], [c
for _, c
in blocks])
416 s.enable_vof_block_csf()
417 st0 = s.diagnostics.vof_block_stats()
418 vol0 = [b[
"volume"]
for b
in st0]
419 print(f
" void fraction {100*alpha0:.3f} % (case: 1.492 %), <rho> = {rho_av:.6f}; "
420 f
"{len(st0)} markers, V in [{min(vol0):.3f}, {max(vol0):.3f}] "
421 f
"(sphere seed {4/3*math.pi*Rc**3:.3f})")
425 s.set_field(
"p", pres)
426 print(
" accumulated pressure restored from the checkpoint")
432 s.diagnostics.set_vof_step_parity(step0)
434 seedV = 4.0 / 3.0 * math.pi * Rc ** 3
437 print(f
" --steps 0: wrote {CKPT}.roundtrip.npz and stopped")
439 maxit, maxdiv, dt = 0, 0.0, 0.0
444 while stepped < NSTEP:
446 L = s.vof_step_limits()
447 dt = 0.2 / max(np.abs(s.get_u()).max(), np.abs(s.get_v()).max(),
448 np.abs(s.get_w()).max(), 1e-30)
453 for nm
in (
"cfl_dt",
"capillary_dt"):
454 if not (L[nm] > 0.0
and math.isfinite(L[nm])):
455 with open(CKPT +
".failed",
"w")
as f:
456 f.write(json.dumps({
"step": i,
"t": t,
"turnovers": t * UTAU,
457 "limit": nm,
"value": L[nm],
458 "vof": s.diagnostics.vof_diagnostics()}, default=float) +
"\n")
460 f
" *** step {i}: {nm} = {L[nm]} at t u_tau/h = {t*UTAU:.4f} — the state "
461 f
"is already broken (the colour has left [0,1]); the last checkpoint is "
463 dt = min(dt, DTSAFE * L[
"cfl_dt"], DTSAFE * L[
"capillary_dt"])
468 except Exception
as exc:
473 with open(CKPT +
".failed",
"w")
as f:
474 f.write(json.dumps({
"step": i,
"t": t,
"turnovers": t * UTAU,
"dt": dt,
475 "error": repr(exc)}) +
"\n")
476 print(f
"\n *** step {i} FAILED at t u_tau/h = {t*UTAU:.4f}, dt = {dt:.3e}: {exc}")
477 print(f
" the last checkpoint ({CKPT}) is untouched; restart from it with a smaller "
478 f
"dt safety factor. Chain stopped.")
482 it = s.diagnostics.last_pressure_iterations()
483 maxit = max(maxit, it)
484 maxdiv = max(maxdiv, s.max_open_divergence())
485 if t * UTAU >= STATS_START
and (i % SAMPLE == 0):
487 if i % PRINTEVERY == 0:
488 el = time.time() - wall0
489 D = s.diagnostics.vof_diagnostics()
490 cmx = max(float(D.get(
"max", 0.0)), float(D.get(
"max_fluid", 0.0)))
491 umx = max(np.abs(s.get_u()).max(), np.abs(s.get_v()).max(), np.abs(s.get_w()).max())
492 st = s.diagnostics.vof_block_stats()
493 cen = np.array([b[
"centroid"]
for b
in st])
494 box = np.array([NX, NY, NZ], dtype=float)
496 for a
in range(len(cen)):
497 for b2
in range(a + 1, len(cen)):
499 q[0] -= box[0] * round(q[0] / box[0])
500 q[2] -= box[2] * round(q[2] / box[2])
501 dmin = min(dmin, float(np.hypot(np.hypot(q[0], q[1]), q[2])))
502 print(f
" step {i:7d} t u_tau/h = {t*UTAU:8.4f} dt = {dt:.3e} "
503 f
"U_b = {s.get_u().mean()/S:.4f} press {it:3d}/800 maxC {cmx:.6f} "
504 f
"max|u| {umx:8.2f} dmin {dmin:6.2f} "
505 f
"{1000*el/max(stepped,1):.0f} ms/step ({el:.0f} s)", flush=
True)
510 if not (cmx <= 1.0 + 1e-6):
511 with open(CKPT +
".failed",
"w")
as f:
512 f.write(json.dumps({
"step": i,
"t": t,
"turnovers": t * UTAU,
513 "max_C": cmx,
"dt": dt}, default=float) +
"\n")
514 raise SystemExit(f
" *** step {i}: max C = {cmx} > 1 — Weymouth-Yue boundedness "
515 f
"lost. Reduce --dt-safety and restart from the checkpoint.")
516 done_t = t * UTAU >= TARGET
517 done_w = (time.time() - wall0) > WALL
518 if done_t
or done_w
or (i % CKEVERY == 0)
or stepped >= NSTEP:
521 stop =
"turnovers" if done_t
else "wall"
524 el = time.time() - wall0
525 st = s.diagnostics.vof_block_stats()
526 vols = [b[
"volume"]
for b
in st]
527 capped = maxit >= 800
529 "stop": stop,
"step": i,
"steps_this_chunk": stepped,
"t": t,
530 "turnovers": t * UTAU,
"wall_s": el,
"ms_per_step": 1000 * el / max(stepped, 1),
531 "max_pressure_iterations": maxit,
"pressure_cap": 800,
"capped": bool(capped),
532 "max_open_divergence": maxdiv,
"markers": len(st),
533 "volume_min": min(vols),
"volume_max": max(vols),
"volume_seed": seedV,
534 "volume_rel_err": max(abs(v / seedV - 1.0)
for v
in vols),
535 "area_total": sum(b[
"area"]
for b
in st),
536 "samples": acc[
"_n"],
"grid": [NX, NY, NZ],
538 print(
"\n " + json.dumps(rec))
539 with open(LOGJ,
"a")
as f:
540 f.write(json.dumps(rec) +
"\n")
542 print(
" *** PRESSURE CAPPED -> THIS CHUNK IS INVALID (rule 3b) ***")
546 np.savez(OUT, **{k: np.asarray(v)
for k, v
in pr.items()},
547 turnovers=np.float64(t * UTAU), step=np.int64(i),
548 grid=np.asarray([NX, NY, NZ]), utau=np.float64(UTAU))
549 print(f
" profiles written to {OUT} ({int(acc['_n'])} samples over "
550 f
"t u_tau/h {pr['window_turnovers'][0]:.3f} .. {pr['window_turnovers'][1]:.3f})")
551 print(f
" u_tau: imposed {UTAU:.6f}, wall gradient {pr['utau_wall']:.6f} "
552 f
"({100*(pr['utau_wall']/UTAU-1):+.1f} %; walls {pr['utau_lo']:.6f} / "
553 f
"{pr['utau_hi']:.6f})")
554 print(
"\n y/h y+ <u>/u_tau <u>_liq/u_tau alpha u'/u_tau v'/u_tau "
555 "w'/u_tau -<u'v'>/u_tau^2")
557 print(f
" {pr['y'][j]:7.4f} {pr['yplus'][j]:8.2f} {pr['u'][j]/UTAU:11.4f} "
558 f
"{pr['u_liq'][j]/UTAU:13.4f} {pr['alpha'][j]:9.5f} "
559 f
"{pr['urms'][j]/UTAU:9.4f} {pr['vrms'][j]/UTAU:9.4f} "
560 f
"{pr['wrms'][j]/UTAU:9.4f} {-pr['uv'][j]/UTAU**2:15.5f}")
562 print(
" no statistics yet (still inside the discarded transient)")
563 print(f
" chunk ended on '{stop}' at step {i}, t u_tau/h = {t*UTAU:.4f}")
564 if stop ==
"turnovers":
566 with open(CKPT +
".done",
"w")
as f:
567 f.write(json.dumps(rec) +
"\n")
568 print(f
" TARGET REACHED -> {CKPT}.done written")