flow 0.4.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
collocated_constraint_consistency.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""Is the COLLOCATED constraint operator consistent? A-priori, no solver, no time stepping.
3
4The permeability ladders show the collocated schemes converging to a fixed offset rather than to
5the staggered/continuum answer. If that offset is real the operator is ZEROTH order -- plain
6inconsistent -- so test the operator directly rather than inferring it from a global functional.
7
8Method. Take Stokes flow past a sphere: exactly solenoidal, exactly no-slip at r=R. For a
9solenoidal field the EXACT flux balance over the fluid part of any cell is zero,
10 sum_faces Int_{open part of face} u.n dA = 0
11(the wall fragment contributes nothing, u.n = 0 there). So for each near-wall fluid cell compute
12that sum with the scheme's MODEL flux in place of the exact one, and the residual IS the operator's
13consistency error -- no second discretisation, no periodicity, no solver in the loop.
14
15Model fluxes compared, all on the same aperture geometry:
16 exact Int over the open part, by k x k quadrature on the face (the reference)
17 stag alpha_f * h^2 * u_a(face centre) -- face value is a free unknown
18 col alpha_f * h^2 * 1/2 (u_i + u_j) -- interpolated from cells, solid cells
19 masked to 0 as the solver does
20 col_nm as col but WITHOUT the solid-cell mask (uses the analytic continuation) -- isolates
21 how much of the defect is the masking rather than the interpolation
22
23Reported per resolution: the L1 flux defect summed over near-wall cells, normalised by the
24through-flux u_inf * pi * R^2, i.e. directly comparable to a permeability error in %.
25
26 python tests/study/collocated_constraint_consistency.py [N ...]
27"""
28import sys
29
30import numpy as np
31
32R_SPH = 0.3102
33C0 = np.array([0.013, -0.007, 0.004])
34KQ = 8 # quadrature points per face direction
35
36
37def stokes_u(x, y, z, comp):
38 dx, dy, dz = x - C0[0], y - C0[1], z - C0[2]
39 r2 = dx * dx + dy * dy + dz * dz
40 r = np.sqrt(np.maximum(r2, 1e-300))
41 A = 3.0 * R_SPH / (4.0 * r)
42 B = R_SPH ** 3 / (4.0 * r ** 3)
43 if comp == 0:
44 return 1.0 - A * (1.0 + dx * dx / r2) - B * (1.0 - 3.0 * dx * dx / r2)
45 d1 = (dy, dz)[comp - 1]
46 return -(A - 3.0 * B) * dx * d1 / r2
47
48
49def sdf(x, y, z):
50 return np.sqrt((x - C0[0]) ** 2 + (y - C0[1]) ** 2 + (z - C0[2]) ** 2) - R_SPH
51
52
53def face_quadrature(fc, a, h):
54 """Exact open-area flux and the planar aperture for faces whose centres are `fc` (3, M)."""
55 t = [k for k in range(3) if k != a]
56 off = (np.arange(KQ) + 0.5) / KQ - 0.5
57 P, Q = np.meshgrid(off * h, off * h, indexing="ij")
58 pts = [fc[k][None, :] + 0.0 for k in range(3)]
59 pts[t[0]] = fc[t[0]][None, :] + P.ravel()[:, None]
60 pts[t[1]] = fc[t[1]][None, :] + Q.ravel()[:, None]
61 s = sdf(*pts)
62 ua = stokes_u(*pts, a)
63 openm = s >= 0.0
64 exact = (h * h / (KQ * KQ)) * np.where(openm, ua, 0.0).sum(axis=0)
65 alpha = openm.mean(axis=0) # exact area fraction from the same quadrature
66 return exact, alpha
67
68
69def run(N, band=3.0):
70 h = 1.0 / N
71 c = (np.arange(N) + 0.5) * h - 0.5
72 X, Y, Z = np.meshgrid(c, c, c, indexing="ij")
73 S = sdf(X, Y, Z)
74 uc = [np.where(S >= 0.0, stokes_u(X, Y, Z, k), 0.0) for k in range(3)] # masked, as the solver
75 uc_nm = [stokes_u(X, Y, Z, k) for k in range(3)] # unmasked
76
77 sel = (S >= 0.0) & (np.abs(S) < band * h) # near-wall FLUID cells
78 sel[:2, :, :] = sel[-2:, :, :] = False # keep the stencil inside the box
79 sel[:, :2, :] = sel[:, -2:, :] = False
80 sel[:, :, :2] = sel[:, :, -2:] = False
81 idx = np.array(np.nonzero(sel)) # (3, M)
82 M = idx.shape[1]
83 if M == 0:
84 return None
85
86 res = {k: np.zeros(M) for k in ("exact", "stag", "col", "col_nm")}
87 for a in range(3):
88 for side in (0, 1): # 0 = minus face of the cell, 1 = plus face
89 j = idx.copy()
90 j[a] += side # face index: minus face of cell i+side
91 fc = [c[j[k]] if k != a else c[j[a]] - 0.5 * h for k in range(3)]
92 fc = [np.asarray(v) for v in fc]
93 ex, al = face_quadrature(fc, a, h)
94 nb = idx.copy(); nb[a] += 2 * side - 1 # the cell on the other side of that face
95 ui = uc[a][tuple(idx)]
96 uj = uc[a][tuple(nb)]
97 ui_n = uc_nm[a][tuple(idx)]
98 uj_n = uc_nm[a][tuple(nb)]
99 uf_c = 0.5 * (ui + uj)
100 uf_n = 0.5 * (ui_n + uj_n)
101 uf_s = stokes_u(*fc, a)
102 sgn = 1.0 if side == 1 else -1.0
103 res["exact"] += sgn * ex
104 res["stag"] += sgn * al * h * h * uf_s
105 res["col"] += sgn * al * h * h * uf_c
106 res["col_nm"] += sgn * al * h * h * uf_n
107
108 Q = np.pi * R_SPH ** 2 # u_inf * projected area: the through-flux scale
109 out = dict(N=N, hR=h / R_SPH, ncell=M,
110 quad=float(np.abs(res["exact"]).sum() / Q))
111 for k in ("stag", "col", "col_nm"):
112 d = res[k] - res["exact"]
113 out[k] = float(np.abs(d).sum() / Q) # L1: no cancellation
114 out[k + "_net"] = float(d.sum() / Q) # SIGNED: what a flux/permeability feels
115 return out
116
117
118if __name__ == "__main__":
119 Ns = [int(x) for x in (sys.argv[1:] or [32, 48, 64, 96, 128])]
120 print("L1 flux defect over near-wall cells, normalised by u_inf*pi*R^2 (so read it as a "
121 "relative flux error, i.e. comparable to the permeability error in %).\n")
122 print(f"{'N':>5} {'h/R':>7} | {'stag L1':>9} {'ord':>6} {'stag NET':>10} {'ord':>6} | "
123 f"{'col L1':>9} {'ord':>6} {'col NET':>10} {'ord':>6}")
124 prev = None
125 for N in Ns:
126 r = run(N)
127 o = {}
128 if prev:
129 lr = np.log(N / prev["N"])
130 for k in ("stag", "col", "col_nm", "stag_net", "col_net", "col_nm_net"):
131 with np.errstate(all="ignore"):
132 o[k] = f"{np.log(abs(prev[k]/r[k]))/lr:+.2f}"
133 print(f"{r['N']:>5} {r['hR']:>7.4f} | {r['stag']:>9.3e} {o.get('stag',''):>6} "
134 f"{r['stag_net']:>+10.3e} {o.get('stag_net',''):>6} | {r['col']:>9.3e} "
135 f"{o.get('col',''):>6} {r['col_net']:>+10.3e} {o.get('col_net',''):>6}", flush=True)
136 prev = r
137 print("\n'quad' is the residual of the EXACT open-area balance -- the quadrature floor; every")
138 print("other column must be read against it. A column converging to zero is consistent; one")
139 print("that flattens above the quadrature floor is the zeroth-order signature.")