flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
mg_coarse_wall.py
Go to the documentation of this file.
1"""2-D prototype (architect, Amendment A1 to doc/scalar_ibm_design.md): which coarse construction of
2the lumped-probe surrogate's WALL term makes a sound V-cycle?
3
4Surrogate (steady, Lam = 1): S = aperture 5-point Laplacian + diagonal wall sink W_i = alpha_i/s0_i,
5alpha_i = |area vector|/h^2 (aperture-consistent), s0 = 1.1 * 1/2 (|n_x|+|n_y|) h (Dirichlet wall).
6Coarse constructions compared (same smoother, transfers and exact bottom, so only A_c differs):
7 redisc-sL : faces averaged + wall W_C = <alpha / s_L>, s_L = probe distance at the level's H (design ยง5.2)
8 avg-s0 : faces averaged + wall W_C = <alpha / s0> (= variational for piecewise-constant P)
9 rap : Galerkin R A P, R = 4-child average, P = bilinear (zero beyond Dirichlet box faces)
10Smoother: 4-colour Gauss-Seidel (colour = (i%2, j%2): race-free for 5- and 9-point), 2 pre (colours
110..3) + 2 post (3..0); bilinear prolongation, pinned cells re-zeroed; exact bottom at 4x4.
12Measure: V-cycle contraction rho(I - B S) by power iteration (mean projected out when singular).
13Results (n = 64 -> 128) are in doc/scalar_ibm_design.md, Amendment A1.
14"""
15import numpy as np, scipy.sparse as sp, scipy.sparse.linalg as spla, sys
16
17SNAP = 1e-3
18
19
20def edge_frac(pa, pb):
21 if pa > 0 and pb > 0:
22 return 1.0
23 if pa <= 0 and pb <= 0:
24 return 0.0
25 p, q = (pa, pb) if pa > 0 else (pb, pa)
26 return p / (p - q)
27
28
29def snap(a):
30 return 0.0 if a < SNAP else (1.0 if a > 1 - SNAP else a)
31
32
33class Geo:
34 """n x n cells on [0, L]^2; sdf(x, y) > 0 fluid; periodic or Dirichlet box."""
35 def __init__(self, n, L, sdf, periodic):
36 self.n, self.h, self.periodic = n, L / n, periodic
37 h = self.h
38 xe = np.arange(n + 1) * h
39 # ax[i, j]: x-face at xe[i] (i = 0..n), cell row j ; ay[i, j]: y-face at ye[j]
40 self.ax = np.zeros((n + 1, n)); self.ay = np.zeros((n, n + 1))
41 for i in range(n + 1):
42 for j in range(n):
43 self.ax[i, j] = snap(edge_frac(sdf(xe[i], xe[j]), sdf(xe[i], xe[j + 1])))
44 for i in range(n):
45 for j in range(n + 1):
46 self.ay[i, j] = snap(edge_frac(sdf(xe[i], xe[j]), sdf(xe[i + 1], xe[j])))
47 if periodic: # the periodic face is one face
48 self.ax[n, :] = self.ax[0, :]; self.ay[:, n] = self.ay[:, 0]
49 Ax = (self.ax[1:, :] - self.ax[:-1, :]) * h
50 Ay = (self.ay[:, 1:] - self.ay[:, :-1]) * h
51 A = np.hypot(Ax, Ay)
52 self.unknown = (self.ax[1:, :] + self.ax[:-1, :] + self.ay[:, 1:] + self.ay[:, :-1]) > 0
53 with np.errstate(invalid="ignore", divide="ignore"):
54 nx = np.where(A > 0, Ax / np.maximum(A, 1e-300), 0); ny = np.where(A > 0, Ay / np.maximum(A, 1e-300), 0)
55 self.alpha = np.where(self.unknown & (A > 1e-14 * h), A / h**2, 0.0)
56 self.supp = 0.5 * (np.abs(nx) + np.abs(ny)) # S(n)/h
57
58
59def level_op(ax, ay, unknown, h, periodic, W):
60 """5-point per-volume operator on an n x n level, identity rows at pinned cells."""
61 n = unknown.shape[0]
62 idx = lambda i, j: (i % n) * n + (j % n)
63 rows, cols, vals = [], [], []
64 diag = np.array(W, dtype=float).ravel().copy()
65 for i in range(n):
66 for j in range(n):
67 if not unknown[i, j]:
68 continue
69 r = idx(i, j)
70 for (a, ii, jj) in ((ax[i + 1, j], i + 1, j), (ax[i, j], i - 1, j), (ay[i, j + 1], i, j + 1), (ay[i, j], i, j - 1)):
71 if a <= 0:
72 continue
73 inside = 0 <= ii < n and 0 <= jj < n
74 if inside or periodic:
75 if unknown[ii % n, jj % n]:
76 diag[r] += a / h**2; rows.append(r); cols.append(idx(ii, jj)); vals.append(-a / h**2)
77 else: # Dirichlet box face (value 0 at the face)
78 diag[r] += 2 * a / h**2
79 for i in range(n):
80 for j in range(n):
81 if not unknown[i, j]:
82 diag[idx(i, j)] = 1.0
83 rows += list(range(n * n)); cols += list(range(n * n)); vals += list(diag)
84 return sp.csr_matrix((vals, (rows, cols)), shape=(n * n, n * n))
85
86
87def transfers(n, periodic):
88 """R: average of 4 children (nc^2 x n^2); P: cell-centred bilinear (n^2 x nc^2)."""
89 nc = n // 2
90 R = sp.lil_matrix((nc * nc, n * n)); P = sp.lil_matrix((n * n, nc * nc))
91 for I in range(nc):
92 for J in range(nc):
93 for di in (0, 1):
94 for dj in (0, 1):
95 R[I * nc + J, (2 * I + di) * n + 2 * J + dj] = 0.25
96 for i in range(n):
97 for j in range(n):
98 I, J = i // 2, j // 2
99 si = 1 if i % 2 else -1; sj = 1 if j % 2 else -1
100 for (II, JJ, w) in ((I, J, 9 / 16), (I + si, J, 3 / 16), (I, J + sj, 3 / 16), (I + si, J + sj, 1 / 16)):
101 if periodic:
102 P[i * n + j, (II % nc) * nc + JJ % nc] += w
103 elif 0 <= II < nc and 0 <= JJ < nc:
104 P[i * n + j, II * nc + JJ] += w
105 return R.tocsr(), P.tocsr()
106
107
108def build_hierarchy(G, mode, nmin=4):
109 n, h = G.n, G.h
110 levels = []
111 ax, ay, unk = G.ax, G.ay, G.unknown
112 W0 = G.alpha / np.maximum(1.1 * G.supp * h, 1e-300) * (G.alpha > 0)
113 A = level_op(ax, ay, unk, h, G.periodic, W0)
114 L = 0
115 while True:
116 levels.append(dict(A=A, unk=unk.ravel(), n=n))
117 if n // 2 < nmin:
118 break
119 R, P = transfers(n, G.periodic)
120 levels[-1].update(R=R, P=P)
121 nc, H = n // 2, h * 2 ** (L + 1)
122 unk_c = unk.reshape(nc, 2, nc, 2).any(axis=(1, 3))
123 if mode == "rap":
124 Ac = (R @ A @ P).tolil()
125 for k in np.nonzero(~unk_c.ravel())[0]:
126 Ac.rows[k] = [k]; Ac.data[k] = [1.0]
127 A = Ac.tocsr()
128 else:
129 axc = 0.5 * (ax[0::2, :][:, 0::2] + ax[0::2, :][:, 1::2])
130 ayc = 0.5 * (ay[:, 0::2][0::2, :] + ay[:, 0::2][1::2, :])
131 f = 2 ** (L + 1)
132 if mode == "redisc-sL":
133 s = 1.1 * G.supp * h * f
134 else:
135 s = 1.1 * G.supp * h
136 Wf = np.where(G.alpha > 0, G.alpha / np.maximum(s, 1e-300), 0.0)
137 Wc = Wf.reshape(nc, f, nc, f).mean(axis=(1, 3))
138 A = level_op(axc, ayc, unk_c, H, G.periodic, Wc)
139 ax, ay = axc, ayc
140 unk = unk_c; n = nc; L += 1
141 return levels
142
143
145 i, j = np.meshgrid(np.arange(n), np.arange(n), indexing="ij")
146 return [np.nonzero(((i % 2) * 2 + (j % 2)).ravel() == c)[0] for c in range(4)]
147
148
149def smooth(A, x, b, sets, order):
150 D = A.diagonal()
151 for c in order:
152 s = sets[c]
153 x[s] += (b[s] - A[s] @ x) / D[s]
154 return x
155
156
157def vcycle(levels, l, b, singular):
158 lv = levels[l]; A = lv["A"]; n = lv["n"]
159 if "R" not in lv:
160 if singular:
161 x = np.linalg.lstsq(A.toarray(), b, rcond=None)[0]
162 x[lv["unk"]] -= x[lv["unk"]].mean(); return x
163 return spla.spsolve(A.tocsc(), b)
164 sets = lv.setdefault("sets", colour_sets(n))
165 x = np.zeros_like(b)
166 for _ in range(2):
167 x = smooth(A, x, b, sets, (0, 1, 2, 3))
168 r = b - A @ x
169 rc = lv["R"] @ r
170 if singular:
171 u = levels[l + 1]["unk"]; rc[u] -= rc[u].mean(); rc[~u] = 0
172 ec = vcycle(levels, l + 1, rc, singular)
173 x += lv["P"] @ ec
174 x[~lv["unk"]] = 0
175 for _ in range(2):
176 x = smooth(A, x, b, sets, (3, 2, 1, 0))
177 return x
178
179
180def contraction(levels, singular, its=60):
181 A = levels[0]["A"]; u = levels[0]["unk"]
182 rng = np.random.default_rng(3)
183 e = rng.standard_normal(A.shape[0]); e[~u] = 0
184 hist = []
185 for _ in range(its):
186 if singular:
187 e[u] -= e[u].mean()
188 nrm = np.linalg.norm(e)
189 e = e - vcycle(levels, 0, A @ e, singular)
190 e[~u] = 0
191 if singular:
192 e[u] -= e[u].mean()
193 hist.append(np.linalg.norm(e) / nrm)
194 if not np.isfinite(hist[-1]) or hist[-1] > 1e6:
195 return float("inf")
196 return float(np.exp(np.mean(np.log(hist[-10:]))))
197
198
199def bicgstab_its(levels, singular):
200 A = levels[0]["A"]; u = levels[0]["unk"]
201 rng = np.random.default_rng(5)
202 b = np.where(u, 1 + 0.3 * rng.standard_normal(A.shape[0]), 0.0)
203 if singular:
204 b[u] -= b[u].mean()
205 M = spla.LinearOperator(A.shape, matvec=lambda r: vcycle(levels, 0, r.copy(), singular))
206 cnt = [0]
207 x, info = spla.bicgstab(A, b, M=M, rtol=1e-10, maxiter=300, callback=lambda _: cnt.__setitem__(0, cnt[0] + 1))
208 return cnt[0] if info == 0 else f"{cnt[0]}!"
209
210
211def disc_union(centres, R, L):
212 def f(x, y):
213 d = np.inf
214 for (cx, cy) in centres:
215 for ox in (-L, 0, L):
216 for oy in (-L, 0, L):
217 d = min(d, np.hypot(x - cx - ox, y - cy - oy) - R)
218 return d
219 return f
220
221
222if __name__ == "__main__":
223 n = int(sys.argv[1]) if len(sys.argv) > 1 else 128
224 L = 1.0
225 off = 0.37 / n # off-lattice placement
226 cases = {
227 "box+disc R=L/4 (G1-like)": (disc_union([(0.5 + off, 0.5 + off)], 0.25, L), False, False),
228 "periodic+disc R=L/8": (disc_union([(0.5 + off, 0.5 + off)], 0.125, L), True, False),
229 "periodic+disc R=L/32": (disc_union([(0.5 + off, 0.5 + off)], 1 / 32, L), True, False),
230 "periodic 4x4 dense (R=0.4 pitch)": (disc_union([((k + 0.5) / 4 + off, (m + 0.5) / 4 + off) for k in range(4) for m in range(4)], 0.1, L), True, False),
231 "box, no solid": (lambda x, y: 1.0, False, False),
232 }
233 print(f"n = {n}, h = 1/{n}; contraction rho(I - B S) [BiCGStab its to 1e-10]")
234 for name, (sdf, per, _) in cases.items():
235 G = Geo(n, L, sdf, per)
236 line = f"{name:34s}"
237 for mode in ("redisc-sL", "avg-s0", "rap"):
238 lv = build_hierarchy(G, mode)
239 rho = contraction(lv, False)
240 its = bicgstab_its(lv, False)
241 line += f" | {mode}: {rho:6.3f} [{its}]"
242 print(line, flush=True)
__init__(self, n, L, sdf, periodic)
smooth(A, x, b, sets, order)
level_op(ax, ay, unknown, h, periodic, W)
build_hierarchy(G, mode, nmin=4)
transfers(n, periodic)
disc_union(centres, R, L)
bicgstab_its(levels, singular)
vcycle(levels, l, b, singular)
contraction(levels, singular, its=60)
static double mean(const std::vector< double > &a)