flow
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
study_avg_velocity_spheres.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Grid-convergence of Stokes permeability through an SC sphere array (Zick & Homsy ground truth),
3comparing how the SUPERFICIAL VELOCITY U_sup = <u_x> is computed across discretizations:
4
5 * staggered — sdflow.Solver: u lives on faces (divergence-free), <u_x> over the face field.
6 * collocated/cell — sdflow.SolverColocated: <u_x> over the CELL-centered velocity (the default).
7 * collocated/face — sdflow.SolverColocated: <u_x> over the projected, divergence-free MAC FACE field
8 (get_uf). Same solve as collocated/cell — only the averaging location differs.
9 * amr/uniform — transport-core tpx_amr.Flow (collocated cut-cell) on a UNIFORM octree (lmax=0).
10
11Motivation: the staggered solver is ~1% more accurate per grid than collocated on permeability. The cell
12average is biased by the openness-aware central-difference pressure correction (projectCorrectCenter) at cut
13cells, which has a non-zero mean there; the face field's correction is a plain gradient (zero periodic mean),
14so its mean is the clean momentum-balance superficial velocity. This script measures whether averaging the
15divergence-free face field recovers the staggered accuracy.
16
17K = f N^3 / (6 pi mu R U_sup) (Z&H drag); err% vs the Z&H table. Run from sdflow/ with the OpenMP build:
18 SDFLOW_BUILD=build_omp PYTHONPATH=build_omp:../transport-core/python/build python scripts/study_avg_velocity_spheres.py
19"""
20import os
21import sys
22import time
23
24import numpy as np
25
26_here = os.path.dirname(__file__)
27sys.path.insert(0, os.path.abspath(os.path.join(_here, "..", os.environ.get("SDFLOW_BUILD", "build_omp"))))
28sys.path.insert(0, os.path.abspath(os.path.join(_here, "..", "..", "transport-core", "python", "build")))
29
30from peclet import flow as sdflow # noqa: E402
31
32ZH_PHI = [0.000125, 0.001, 0.008, 0.027, 0.064, 0.125, 0.216, 0.343, 0.45, 0.5236]
33ZH_K = [1.096, 1.212, 1.525, 2.008, 2.810, 4.292, 7.442, 15.4, 28.1, 42.1]
34
35
36def zh_ref(phi):
37 return float(np.interp(phi, ZH_PHI, ZH_K))
38
39
40def sphere_radius(phi, N):
41 return (phi * 3.0 / (4.0 * np.pi)) ** (1.0 / 3.0) * N
42
43
44def sc_sdf(N, phi):
45 """Single SC sphere centered in the N^3 cube; suite sign (<0 solid)."""
46 R = sphere_radius(phi, N)
47 g = np.arange(N)
48 X, Y, Z = np.meshgrid(g, g, g, indexing="ij")
49 c = N / 2.0
50 d = np.sqrt((X - c) ** 2 + (Y - c) ** 2 + (Z - c) ** 2) - R
51 return np.asfortranarray(d), R, c
52
53
54def drag_K(umean, R, N, f, mu):
55 return f * N ** 3 / (6.0 * np.pi * mu * R * umean)
56
57
58def run_sdflow(SolverCls, N, phi, mu=0.1, f=1e-3, dt=60.0, max_steps=600, tol=1e-6):
59 """Returns (U_cell, U_face): the cell-mean and the divergence-free face-mean of u_x at steady state."""
60 sdf, R, _ = sc_sdf(N, phi)
61 lv = max(2, int(np.log2(N)) - 1)
62 s = SolverCls(N, N, N)
63 s.set_rho(1.0); s.set_mu(mu); s.set_dt(dt); s.set_body_force(f, 0, 0); s.set_advection(False)
64 s.set_velocity_solver_params(200)
65 s.set_pressure_multigrid(True, levels=lv)
66 s.set_pressure_pcg(True, max_iter=200, rtol=1e-8)
67 s.set_solid(sdf, cutcell_pressure=True)
68 prev = 0.0
69 for it in range(max_steps):
70 s.step()
71 if it % 5 == 4:
72 m = float(s.get_u().mean())
73 if it > 10 and abs(m - prev) < tol * (abs(m) + 1e-30):
74 break
75 prev = m
76 u, uf = s.get_u(), s.get_uf()
77 if os.environ.get("STUDY_DIAG") and SolverCls is sdflow.SolverColocated:
78 # prove get_uf() is the genuinely different face field, not an alias of the cell field
79 print(f" [diag N={N}] max|uf-u_cell| = {np.abs(uf - u).max():.3e} "
80 f"(field differs pointwise; means: cell {float(u.mean()):.8e} face {float(uf.mean()):.8e})",
81 flush=True)
82 return float(u.mean()), float(uf.mean())
83
84
85def run_amr(N, phi, mu=0.1, f=1e-3, dt=60.0, steps=100, mom=120, pres=6, psw=2):
86 import tpx_amr
87 _, R, c = sc_sdf(N, phi)
88 oct = tpx_amr.Octree([N, N, N], 0, [0.0, 0.0, 0.0], 1.0)
89 fl = tpx_amr.Flow(oct, 1.0, mu, dt)
90 fl.set_body_force(f, 0, 0); fl.set_advection(False)
91 fl.set_solid(lambda x, y, z: ((x - c) ** 2 + (y - c) ** 2 + (z - c) ** 2) ** 0.5 - R)
92 for _ in range(steps):
93 fl.step(mom, pres, psw)
94 return float(fl.velocity(0).mean())
95
96
97def main():
98 phi = float(sys.argv[1]) if len(sys.argv) > 1 else 0.125
99 Ns = [int(x) for x in sys.argv[2].split(",")] if len(sys.argv) > 2 else [16, 24, 32, 48]
100 amr_max = int(os.environ.get("AMR_MAX_N", "32")) # AMR host path is serial; cap its N
101 kref = zh_ref(phi)
102 mu, f = 0.1, 1e-3
103
104 print(f"=== Stokes permeability through an SC sphere (phi={phi}, Z&H K_ref={kref:.4f}) ===")
105 print(f" U_sup averaging: staggered(face) | collocated(cell) | collocated(FACE) | amr/uniform(cell)")
106 print(f"{'N':>4} | {'K_stag':>8} {'e%':>6} | {'K_co_cell':>9} {'e%':>6} | {'K_co_FACE':>9} {'e%':>6}"
107 f" | {'K_amr':>8} {'e%':>6} | {'face-cell':>9}")
108 for N in Ns:
109 t0 = time.time()
110 u_stag, _ = run_sdflow(sdflow.Solver, N, phi)
111 uc, uf = run_sdflow(sdflow.SolverColocated, N, phi)
112 Ks = drag_K(u_stag, sphere_radius(phi, N), N, f, mu)
113 Kc = drag_K(uc, sphere_radius(phi, N), N, f, mu)
114 Kf = drag_K(uf, sphere_radius(phi, N), N, f, mu)
115 es = 100 * (Ks - kref) / kref
116 ec = 100 * (Kc - kref) / kref
117 ef = 100 * (Kf - kref) / kref
118 fc = (uf - uc) / uc # relative shift in U_sup from face vs cell averaging
119 if N <= amr_max:
120 ua = run_amr(N, phi)
121 Ka = drag_K(ua, sphere_radius(phi, N), N, f, mu)
122 ea = 100 * (Ka - kref) / kref
123 amr = f"{Ka:8.4f} {ea:+6.2f}"
124 else:
125 amr = f"{'--':>8} {'--':>6}"
126 print(f"{N:4d} | {Ks:8.4f} {es:+6.2f} | {Kc:9.4f} {ec:+6.2f} | {Kf:9.4f} {ef:+6.2f}"
127 f" | {amr} | {fc:+.2e} [{time.time()-t0:.0f}s]", flush=True)
128
129
130if __name__ == "__main__":
131 main()
run_amr(N, phi, mu=0.1, f=1e-3, dt=60.0, steps=100, mom=120, pres=6, psw=2)
run_sdflow(SolverCls, N, phi, mu=0.1, f=1e-3, dt=60.0, max_steps=600, tol=1e-6)