135 """One Raschig ring (hollow cylinder): annulus [r_in,r_out] intersect the slab |axial| <= H/2."""
136 ax = np.asarray(axis, float)
137 ax = ax / np.linalg.norm(ax)
141 z = dx * ax[0] + dy * ax[1] + dz * ax[2]
142 rx, ry, rz = dx - z * ax[0], dy - z * ax[1], dz - z * ax[2]
143 rho = np.sqrt(rx * rx + ry * ry + rz * rz)
144 return np.maximum(np.maximum(r_in - rho, rho - r_out), np.abs(z) - 0.5 * H)
148 """The regression suite's `hollow_rings` bed: 3 Raschig rings, fixed positions/orientations."""
149 rO, rI, H = 0.22 * N, 0.12 * N, 0.34 * N
150 rings = [((0.30 * N, 0.32 * N, 0.30 * N), (1, 0, 0)),
151 ((0.70 * N, 0.68 * N, 0.55 * N), (0, 1, 0)),
152 ((0.45 * N, 0.50 * N, 0.78 * N), (0, 0, 1))]
154 sdf = np.full((N, N, N), 1e30)
155 for c, axis
in rings:
204def run_one(geom, shape, edge, ratio, driver, case, N, quiet=True, bottom=None, levels=None):
205 from peclet
import flow
as F
208 levels = levels
or max(2, int(np.floor(np.log2(N))) - 1)
210 s = F.Solver(N, N, N)
214 s.set_advection(
False)
215 s.diagnostics.set_velocity_solver_params(VEL_SWEEPS)
217 s.set_domain_bc(
"-z",
"wall", (0, 0, 0))
218 s.set_domain_bc(
"+z",
"wall", (0, 0, 0))
220 s.set_domain_bc(
"-x",
"wall", (0, 0, 0))
221 s.set_domain_bc(
"+x",
"wall", (0, 0, 0))
222 s.set_domain_bc(
"-z",
"wall", (0, 0, 0))
223 s.set_domain_bc(
"+z",
"inflow", (ULID, 0.0, 0.0))
225 s.set_body_force((FBODY, 0.0, 0.0))
227 raise ValueError(case)
228 s.set_pressure_multigrid(
True, levels=levels)
230 s.set_pressure_bottom(bottom)
233 s.set_pressure_geometry(np.asfortranarray(np.full((N, N, N), 10.0)))
235 s.set_solid(np.asfortranarray(sdf), cutcell_pressure=
True)
240 s.set_body_force((0.0, 0.0, -GRAV))
243 s.set_field(
"rho", np.asfortranarray(
rho_field(shape, edge, ratio, N)))
244 s.diagnostics.set_density_mode(
"variable")
246 s.set_property_model(
"force_z",
"linear",
"rho", [0.0, -GRAV])
252 s.set_pressure_pcg(
True, MAXIT, RTOL)
253 s.set_pressure_chebyshev(
True, MAXIT, RTOL)
254 elif driver ==
"pcg":
255 s.set_pressure_chebyshev(
False, MAXIT, RTOL)
256 s.set_pressure_pcg(
True, MAXIT, RTOL)
257 elif driver ==
"fcg":
260 s.set_pressure_fcg(
True, MAXIT, RTOL)
262 raise ValueError(driver)
264 iters, divs, t_step, t_proj = [], [], [], []
265 t_wall0 = time.perf_counter()
266 for _
in range(STEPS):
268 iters.append(int(s.diagnostics.last_pressure_iterations()))
269 tm = s.diagnostics.last_step_timers()
270 t_step.append(float(tm[
"step"]))
271 t_proj.append(float(tm[
"projection"]))
272 divs.append(float(s.max_open_divergence()))
273 wall = time.perf_counter() - t_wall0
274 umax = max(float(np.abs(s.get_u()).max()), float(np.abs(s.get_v()).max()),
275 float(np.abs(s.get_w()).max()))
281 it = np.asarray(iters, float)
282 uscale = max(umax, {
"lid": ULID,
"hydro": GRAV * DT,
"per": FBODY * DT}[case])
283 div_rel = divs[-1] / uscale
285 geom=geom, shape=shape, edge=edge, ratio=ratio, driver=driver, case=case, N=N,
286 levels=levels, bottom=bottom
or "auto", steps=STEPS, maxit=MAXIT, rtol=RTOL,
288 it_median=float(np.median(it)), it_max=int(it.max()), it_min=int(it.min()),
289 it_total=int(it.sum()),
290 capped_steps=int((it >= MAXIT).sum()),
291 converged=bool((it < MAXIT).all()),
292 div_final=divs[-1], div_max=float(max(divs)), div_rel=float(div_rel),
293 healthy=bool((it < MAXIT).all()
and np.isfinite(umax)
and div_rel < 1e-8),
295 t_step_median=float(np.median(t_step)), t_proj_median=float(np.median(t_proj)),
296 t_proj_min=float(np.min(t_proj)), t_proj_total=float(np.sum(t_proj)),
300 print(f
" {geom:5s} {shape:4s} {edge:6s} r={ratio:<7.0e} {driver:4s} {case:6s} "
301 f
"it med {rec['it_median']:6.1f} max {rec['it_max']:4d} "
302 f
"{'CAPPED' if not rec['converged'] else ('ok ' if rec['healthy'] else 'BADDIV')} "
303 f
"div/u {rec['div_rel']:.1e} proj {rec['t_proj_min']*1e3:7.1f} ms "
304 f
"(med {rec['t_proj_median']*1e3:7.1f})", flush=
True)
405 """Quantify what rung S2 (bound amortization) would actually buy.
407 Under varRho the coefficient operator is rebuilt every step and `chebBoundsSet_` is
408 invalidated with it (`flow_ibm.hpp` project()), so `estimateEigenvalues(..., iters=15, ...)`
409 runs EVERY step -- two 15-step power loops, each applying M^{-1}A once, i.e. **30 extra
410 V-cycles per step** on top of the ~15-25 the solve itself needs. Constant density estimates
411 the bounds once (step 1) and reuses them, so the A/B below isolates the cost.
413 from peclet
import flow
as F
415 def run(varrho, driver):
417 levels = max(2, int(np.floor(np.log2(N))) - 1)
418 s = F.Solver(N, N, N)
422 s.set_advection(
False)
423 s.diagnostics.set_velocity_solver_params(VEL_SWEEPS)
424 s.set_domain_bc(
"-x",
"wall", (0, 0, 0))
425 s.set_domain_bc(
"+x",
"wall", (0, 0, 0))
426 s.set_domain_bc(
"-z",
"wall", (0, 0, 0))
427 s.set_domain_bc(
"+z",
"inflow", (ULID, 0.0, 0.0))
428 s.set_pressure_multigrid(
True, levels=levels)
430 s.set_pressure_geometry(np.asfortranarray(np.full((N, N, N), 10.0)))
432 s.set_solid(np.asfortranarray(sdf), cutcell_pressure=
True)
435 s.set_field(
"rho", np.asfortranarray(
rho_field(
"slab",
"sharp", ratio, N)))
436 s.diagnostics.set_density_mode(
"variable")
438 s.set_pressure_chebyshev(
True, MAXIT, RTOL)
440 s.set_pressure_chebyshev(
False, MAXIT, RTOL)
441 s.set_pressure_pcg(
True, MAXIT, RTOL)
443 for _
in range(STEPS):
445 tp.append(float(s.diagnostics.last_step_timers()[
"projection"]))
446 it.append(int(s.diagnostics.last_pressure_iterations()))
447 return np.asarray(tp), np.asarray(it)
450 for varrho
in (
False,
True):
451 for drv
in (
"cheb",
"pcg"):
452 tp, it =
run(varrho, drv)
453 k = f
"{'varrho' if varrho else 'constrho'}_{drv}"
454 out[k] = dict(t_first=float(tp[0]), t_median=float(np.median(tp[1:])),
455 it_first=int(it[0]), it_median=float(np.median(it[1:])))
456 c = out[
"constrho_cheb"]
457 est = c[
"t_first"] - c[
"t_median"]
458 v = out[
"varrho_cheb"]
459 print(
"\n--- Chebyshev bound-estimation overhead (rung S2's target) ---")
460 for k, r
in out.items():
461 print(f
" {k:14s} step1 {r['t_first']*1e3:7.1f} ms ({r['it_first']:3d} it) "
462 f
"steady {r['t_median']*1e3:7.1f} ms ({r['it_median']:5.1f} it)")
463 print(f
" isolated bound estimate (const-rho step1 - steady): {est*1e3:.1f} ms")
464 print(f
" varRho Chebyshev pays it EVERY step: {est/max(v['t_median'],1e-12)*100:.0f} % of "
465 f
"its steady projection time; amortizing it (S2) would leave "
466 f
"{(v['t_median']-est)*1e3:.1f} ms")
467 out[
"estimate_cost_s"] = est
486 ap = argparse.ArgumentParser()
487 ap.add_argument(
"--out", default=os.path.join(os.path.dirname(os.path.abspath(__file__)),
488 "vardensity_solver_probe.json"))
489 ap.add_argument(
"--md", default=
None, help=
"also write the markdown table to this path")
490 ap.add_argument(
"--n", type=int, default=N_DEFAULT)
491 ap.add_argument(
"--geoms", default=
"box,cyl,rings")
492 ap.add_argument(
"--shapes", default=
"const," +
",".join(SHAPES))
493 ap.add_argument(
"--edges", default=
",".join(EDGES))
494 ap.add_argument(
"--ratios", default=
",".join(f
"{r:.0e}" for r
in RATIOS))
495 ap.add_argument(
"--drivers", default=
",".join(DRIVERS_DEFAULT),
496 help=
"cheb, pcg, fcg (comma-separated); default cheb,pcg = WO-B's battery")
497 ap.add_argument(
"--cases", default=
",".join(CASES))
498 ap.add_argument(
"--pack", action=
"store_true",
499 help=
"append the data/packing_ring.vti sub-sweep at N=64 (shapes slab,blob)")
500 ap.add_argument(
"--trace", action=
"store_true")
501 ap.add_argument(
"--cheb-overhead", action=
"store_true",
502 help=
"also measure the per-step Chebyshev bound-estimation cost (rung S2)")
503 ap.add_argument(
"--overhead-geoms", default=
"box,rings",
504 help=
"geometries for --cheb-overhead (independent of --geoms)")
505 ap.add_argument(
"--tag", default=
"", help=
"label stored in the JSON (e.g. the backend)")
506 ap.add_argument(
"--bottom", default=
None,
507 help=
"set_pressure_bottom mode ('auto'/'smoother'/'agglomerated'); "
508 "default = the shipped 'auto'")
509 ap.add_argument(
"--levels", type=int, default=
None,
510 help=
"pressure MG depth (default floor(log2 N) - 1)")
511 args = ap.parse_args()
513 geoms = [g
for g
in args.geoms.split(
",")
if g]
514 shapes = [s
for s
in args.shapes.split(
",")
if s]
515 edges = [e
for e
in args.edges.split(
",")
if e]
516 ratios = [float(r)
for r
in args.ratios.split(
",")
if r]
517 drivers = [d
for d
in args.drivers.split(
",")
if d]
518 cases = [c
for c
in args.cases.split(
",")
if c]
520 def expand(gs, shs, n):
526 ee = [edges[0]]
if sh ==
"const" else edges
527 rr = [1.0]
if sh ==
"const" else ratios
528 cc = [c
for c
in cases
530 if not (c ==
"per" and g ==
"box" and sh
in (
"const",
"slab"))]
531 out += [(g, sh, e, ra, d, ca, n)
532 for e
in ee
for ra
in rr
for d
in drivers
for ca
in cc]
535 plan = expand(geoms, shapes, args.n)
537 plan += expand([
"pack"], [s
for s
in (
"const",
"slab",
"blob")
if s
in shapes], N_PACK)
539 print(f
"{len(plan)} configurations x {STEPS} steps", flush=
True)
541 t0 = time.perf_counter()
542 for i, (g, sh, e, ra, d, ca, n)
in enumerate(plan):
543 print(f
"[{i+1}/{len(plan)}]", end=
" ", flush=
True)
544 records.append(
run_one(g, sh, e, ra, d, ca, n, quiet=
False,
545 bottom=args.bottom, levels=args.levels))
546 wall = time.perf_counter() - t0
549 if args.cheb_overhead:
551 for g
in args.overhead_geoms.split(
",")
if g]
556 print(f
"\ntotal wall {wall:.1f} s")
558 meta = dict(tag=args.tag, steps=STEPS, maxit=MAXIT, rtol=RTOL, mu=MU, dt=DT,
559 g=GRAV, u_lid=ULID, vel_sweeps=VEL_SWEEPS, wall_s=wall,
560 python=sys.version.split()[0], host=platform.node(),
561 argv=
" ".join(sys.argv[1:]))
562 if os.path.exists(args.out):
563 with open(args.out)
as f:
567 blob.setdefault(
"runs", []).append({
"meta": meta,
"records": records,
568 "cheb_overhead": overhead})
569 with open(args.out,
"w")
as f:
570 json.dump(blob, f, indent=1)
571 print(f
"wrote {args.out} ({len(blob['runs'])} run(s))")
573 with open(args.md,
"w")
as f:
574 f.write(md +
"\n" +
summarize(records) +
"\n")
577 trace_pcg(
"box",
"slab",
"sharp", 1e3,
"hydro", args.n)