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