62 """t[(c*3+k)*n + i]: exact crossing fraction from component-c staggered point i toward its
63 +k neighbour (NaN = no sign change). Line-sphere with nearest image; roots of
64 t^2 + 2 t d_k + |d|^2 - R^2 = 0 restricted to the sign-changing segment."""
67 out = np.full((3, 3, N, N, N), np.nan)
71 d2 = d[0] ** 2 + d[1] ** 2 + d[2] ** 2
72 s_here = np.sqrt(d2) - R
74 s_nb = np.roll(s_here, -1, axis=k)
75 cross = (s_here < 0) != (s_nb < 0)
76 disc = d[k] ** 2 - (d2 - R * R)
77 ok = cross & (disc > 0)
78 sq = np.sqrt(np.where(ok, disc, 1.0))
82 in1 = ok & (t1 >= -eps) & (t1 <= 1 + eps)
83 in2 = ok & (t2 >= -eps) & (t2 <= 1 + eps)
84 t = np.where(in1, t1, np.where(in2, t2, np.nan))
85 out[c, k] = np.clip(t, 0.0, 1.0)
86 miss = cross & ~np.isfinite(out[c, k])
88 tl = s_here / (s_here - s_nb)
89 out[c, k][miss] = np.clip(tl[miss], 0.0, 1.0)
91 return np.concatenate([out[c, k].ravel(order=
"F")
for c
in range(3)
for k
in range(3)])
95 """Exact fluid area fraction of every -a face (analytic chord integral of the disk cut,
96 1024-pt midpoint rule; faces are unit squares in grid units)."""
102 o = np.ones((N, N, N))
103 t1a, t2a = (a + 1) % 3, (a + 2) % 3
104 w =
nimg(np.arange(N, dtype=np.float64) - C, N)
106 lo = np.arange(N, dtype=np.float64)
107 for i
in np.nonzero(rho2 > 0)[0]:
108 rho = np.sqrt(rho2[i])
109 j1 = np.nonzero(np.abs(
nimg(lo + 0.5 - C, N)) < rho + 1.0)[0]
110 j2 = np.nonzero(np.abs(
nimg(lo + 0.5 - C, N)) < rho + 1.0)[0]
111 if len(j1) == 0
or len(j2) == 0:
113 J1, J2 = np.meshgrid(j1, j2, indexing=
"ij")
114 lo1 =
nimg(lo[J1.ravel()] - C, N)
115 lo2 =
nimg(lo[J2.ravel()] - C, N)
116 t = lo1[:,
None] + (np.arange(M)[
None, :] + 0.5) / M
117 half = np.sqrt(np.maximum(rho * rho - t * t, 0.0))
118 zlo = np.maximum(-half, lo2[:,
None])
119 zhi = np.minimum(half, lo2[:,
None] + 1.0)
120 solid = np.clip(zhi - zlo, 0.0,
None).mean(axis=1)
121 idx = [
None,
None,
None]
122 idx[a] = np.full(len(lo1), i)
123 idx[t1a] = J1.ravel()
124 idx[t2a] = J2.ravel()
125 o[idx[0], idx[1], idx[2]] -= solid
127 o = np.clip(np.moveaxis(o, [0, 1, 2], [a, t1a, t2a]), 0.0, 1.0)
135 o = np.where(
sdf_at(N, Xf, Yf, Zf) >= 0.0, o, 0.0)
141def run(N, scheme, warm_tol=1e-7, tail=40, max_steps=5000, dt=80.0):
144 lv = max(2, int(np.log2(N)) - 1)
145 s = flow.Solver(N, N, N)
149 s.set_body_force(F, 0, 0)
150 s.set_advection(
False)
151 s.set_velocity_solver_params(200)
152 s.set_pressure_multigrid(
True, levels=lv)
153 s.set_pressure_pcg(
True, 400, 1e-10)
155 if scheme ==
"cutcell":
157 s.set_openness_override(ox.ravel(order=
"F"), oy.ravel(order=
"F"), oz.ravel(order=
"F"))
159 mo, ro = {
"g22": (2, 2),
"g11": (1, 1),
"g12": (1, 2)}[scheme]
160 s.set_ghost_projection(
True, matrix_order=mo, rhs_order=ro)
161 s.set_solid(sdf, cutcell_pressure=
True, pressure_coarse=
"rediscretized")
162 prev, warm, um, t0 = 0.0,
None, [], time.time()
163 for it
in range(max_steps):
165 m = float(s.get_u().mean())
169 if it > 10
and abs(m - prev) < warm_tol * (abs(m) + 1e-30):
172 elif it - warm >= tail:
174 K = F * N**3 / (6 * np.pi * MU *
radius(N) * np.mean(um[-tail:]))
175 fields = dict(u=s.get_u(), v=s.get_v(), w=s.get_w(), p=s.get_p())
176 return dict(K=K, steps=it + 1, iters=s.last_pressure_iterations(),
177 div=s.max_open_divergence(), secs=time.time() - t0, **fields)
182 """Cubic-spline (periodic) sample of a reference field at the N-grid points of comp."""
184 g = np.arange(N, dtype=np.float64)
185 off = [0.5, 0.5, 0.5]
189 co = [
None,
None,
None]
190 roff = [0.5, 0.5, 0.5]
194 co[a] = (g + off[a]) * r - roff[a]
195 CX, CY, CZ = np.meshgrid(co[0], co[1], co[2], indexing=
"ij")
196 return map_coordinates(ref, [CX, CY, CZ], order=3, mode=
"grid-wrap", prefilter=
True)
200 """Relative L1/L2/Linf of velocity (3 comps pooled) and pressure vs the reference.
201 The solver works in grid units (dx = 1), so each N is a RESCALED physical problem:
202 u ~ F N^2/mu and p ~ F N. Nondimensionalize by bringing the coarse fields to the
203 reference scale (u * (refN/N)^2, p * refN/N) before differencing."""
206 fields = dict(u=fields[
"u"] * su, v=fields[
"v"] * su, w=fields[
"w"] * su,
211 num = {1: 0.0, 2: 0.0}
212 den = {1: 0.0, 2: 0.0}
216 for c, key
in enumerate(
"uvw"):
222 d = np.abs(fields[key] - rf)
224 num[2] += (d[m] ** 2).sum()
225 den[1] += np.abs(rf[m]).sum()
226 den[2] += (rf[m] ** 2).sum()
227 linf = max(linf, float(d[m].max()))
229 linf_int = max(linf_int, float(d[mi].max()))
231 uref = np.sqrt(den[2] / npts)
232 out[
"u"] = (num[1] / den[1], np.sqrt(num[2] / npts) / uref, linf / uref, linf_int / uref)
239 dp = fields[
"p"] - rf
241 rf0 = rf - rf[m].mean()
242 pref = np.sqrt((rf0[m] ** 2).mean())
244 out[
"p"] = (d[m].sum() / np.abs(rf0[m]).sum(), np.sqrt((d[m] ** 2).mean()) / pref,
245 float(d[m].max()) / pref, float(d[mi].max()) / pref
if mi.any()
else 0.0)
250 ap = argparse.ArgumentParser()
251 ap.add_argument(
"--quick", action=
"store_true")
252 ap.add_argument(
"--run-ref", action=
"store_true")
253 ap.add_argument(
"--ref-n", type=int, default=
None)
254 args = ap.parse_args()
255 refN = args.ref_n
or (96
if args.quick
else 192)
256 Ns = [16, 24, 32]
if args.quick
else [24, 32, 48, 64, 96]
257 ref_file = os.path.join(HERE, f
"zh_exact_ref_N{refN}.npz")
259 if args.run_ref
or not os.path.exists(ref_file):
260 print(f
"[ref] running cut-cell-exact reference at N={refN} ...", flush=
True)
261 r =
run(refN,
"cutcell")
262 np.savez_compressed(ref_file, u=r[
"u"], v=r[
"v"], w=r[
"w"], p=r[
"p"], K=r[
"K"])
263 print(f
"[ref] done: K={r['K']:.4f} err={100 * (r['K'] - KREF) / KREF:+.3f}% "
264 f
"steps={r['steps']} secs={r['secs']:.0f}", flush=
True)
265 z = np.load(ref_file)
266 refs = {k: z[k]
for k
in "uvwp"}
267 print(f
"[ref] N={refN}, K={float(z['K']):.4f} ({100 * (float(z['K']) - KREF) / KREF:+.3f}% "
268 f
"vs Z&H)", flush=
True)
270 schemes = [
"cutcell",
"g22",
"g11",
"g12"]
271 hdr = (f
"{'scheme':>8} {'N':>4} | {'K err%':>8} {'ord':>5} | "
272 f
"{'u L1':>8} {'u L2':>8} {'u Linf':>8} {'Linf>2h':>8} {'ordL2':>5} | "
273 f
"{'p L1':>8} {'p L2':>8} {'p Linf':>8} {'Linf>2h':>8} {'ordL2':>5} | it secs")
274 print(hdr, flush=
True)
275 for scheme
in schemes:
279 nm =
norms(r, refs, refN, N)
280 eK = 100 * (r[
"K"] - KREF) / KREF
281 oK = np.log(abs(prev[
"eK"]) / abs(eK)) / np.log(N / prev[
"N"])
if prev
else float(
"nan")
282 oU = (np.log(prev[
"u2"] / nm[
"u"][1]) / np.log(N / prev[
"N"]))
if prev
else float(
"nan")
283 oP = (np.log(prev[
"p2"] / nm[
"p"][1]) / np.log(N / prev[
"N"]))
if prev
else float(
"nan")
284 print(f
"{scheme:>8} {N:>4} | {eK:>+8.3f} {oK:>5.2f} | "
285 f
"{nm['u'][0]:>8.2e} {nm['u'][1]:>8.2e} {nm['u'][2]:>8.2e} {nm['u'][3]:>8.2e} "
287 f
"{nm['p'][0]:>8.2e} {nm['p'][1]:>8.2e} {nm['p'][2]:>8.2e} {nm['p'][3]:>8.2e} "
288 f
"{oP:>5.2f} | {r['iters']:>2d} {r['secs']:>5.0f}", flush=
True)
289 prev = dict(eK=eK, u2=nm[
"u"][1], p2=nm[
"p"][1], N=N)