37 """fluid fraction of an edge from its end-node SDF values (> 0 fluid), snapped at 1e-3."""
38 out = np.where((pa > 0) & (pb > 0), 1.0, 0.0)
39 mixed = (pa > 0) != (pb > 0)
40 p = np.where(pa > 0, pa, pb); q = np.where(pa > 0, pb, pa)
41 with np.errstate(invalid=
"ignore", divide=
"ignore"):
42 fr = np.where(mixed, p / (p - q), 0.0)
43 out = np.where(mixed, fr, out)
44 return np.where(out < SNAP, 0.0, np.where(out > 1 - SNAP, 1.0, out))
118def level_op(n, h, tx, ty, px, mx, py, my, unk, W, omega=None):
119 """per-volume operator; tx/ty diffusion coefficients (already / h^2), px/mx (py/my) advective
120 coefficients of the LOW faces: p = flow toward +axis (low cell -> high cell), m = toward -axis.
121 omega: lumped extra diagonal (the 'sym' baseline). Identity rows on pinned cells."""
122 idx = np.arange(n * n).reshape(n, n)
123 gx = unk & np.roll(unk, 1, 0); gy = unk & np.roll(unk, 1, 1)
124 tx = np.where(gx, tx, 0); ty = np.where(gy, ty, 0)
125 px = np.where(gx, px, 0); mx = np.where(gx, mx, 0); py = np.where(gy, py, 0); my = np.where(gy, my, 0)
126 up =
lambda a, ax_: np.roll(a, -1, ax_)
127 diag = W + tx + up(tx, 0) + ty + up(ty, 1) + mx + up(px, 0) + my + up(py, 1)
128 if omega
is not None:
130 rows, cols, vals = [], [], []
131 for (coef, sh)
in ((tx + px, (1, 0)), (up(tx, 0) + up(mx, 0), (-1, 0)),
132 (ty + py, (0, 1)), (up(ty, 1) + up(my, 1), (0, -1))):
133 nb = np.roll(idx, sh, axis=(0, 1))
134 m = unk & (coef != 0)
135 rows.append(idx[m]); cols.append(nb[m]); vals.append(-coef[m])
136 diag = np.where(unk, diag, 1.0)
137 rows.append(idx.ravel()); cols.append(idx.ravel()); vals.append(diag.ravel())
138 r = np.concatenate(rows); c = np.concatenate(cols); v = np.concatenate(vals)
139 return sp.csr_matrix((v, (r, c)), shape=(n * n, n * n))
144 I, J = np.meshgrid(np.arange(n), np.arange(n), indexing=
"ij")
145 fine = (I * n + J).ravel()
146 R = sp.csr_matrix((np.full(n * n, 0.25), (((I // 2) * nc + J // 2).ravel(), fine)), shape=(nc * nc, n * n))
148 return R, (4 * R).T.tocsr()
149 si = np.where(I % 2, 1, -1); sj = np.where(J % 2, 1, -1)
150 rows, cols, vals = [], [], []
151 for (dI, dJ, w)
in ((0, 0, 9 / 16), (1, 0, 3 / 16), (0, 1, 3 / 16), (1, 1, 1 / 16)):
152 II = (I // 2 + dI * si) % nc; JJ = (J // 2 + dJ * sj) % nc
153 rows.append(fine); cols.append((II * nc + JJ).ravel()); vals.append(np.full(n * n, w))
154 P = sp.csr_matrix((np.concatenate(vals), (np.concatenate(rows), np.concatenate(cols))), shape=(n * n, nc * nc))
165 ax, ay, unk = C.ax, C.ay, C.unknown
167 px, mx, py, my = np.maximum(fx, 0), np.maximum(-fx, 0), np.maximum(fy, 0), np.maximum(-fy, 0)
169 A =
level_op(n, h, ax / h**2, ay / h**2, px, mx, py, my, unk, W0)
171 if mode
in (
"sym",
"l0only"):
173 om = (mx + np.roll(px, -1, 0) + my + np.roll(py, -1, 1)) * unk
174 S =
level_op(n, h, ax / h**2, ay / h**2, Z, Z, Z, Z, unk, W0, omega=om)
179 levels.append(dict(A=S, unk=unk.ravel(), n=n))
183 levels[-1].update(R=R, P=P)
184 nc = n // 2; H = hh * 2
185 unk_c = unk.reshape(nc, 2, nc, 2).any(axis=(1, 3))
186 axc = 0.5 * (ax[0::2, 0::2] + ax[0::2, 1::2]); ayc = 0.5 * (ay[0::2, 0::2] + ay[1::2, 0::2])
188 Wc = W0.reshape(nc, f, nc, f).
mean(axis=(1, 3))
189 if mode
in (
"sym",
"l0only"):
190 om = om.reshape(nc, 2, nc, 2).
mean(axis=(1, 3))
191 Z = np.zeros((nc, nc))
192 S =
level_op(nc, H, axc / H**2, ayc / H**2, Z, Z, Z, Z, unk_c, Wc, omega=om)
195 px = 0.25 * (px[0::2, 0::2] + px[0::2, 1::2]); mx = 0.25 * (mx[0::2, 0::2] + mx[0::2, 1::2])
196 py = 0.25 * (py[0::2, 0::2] + py[1::2, 0::2]); my = 0.25 * (my[0::2, 0::2] + my[1::2, 0::2])
198 fx = 0.25 * (fx[0::2, 0::2] + fx[0::2, 1::2]); fy = 0.25 * (fy[0::2, 0::2] + fy[1::2, 0::2])
199 px, mx, py, my = np.maximum(fx, 0), np.maximum(-fx, 0), np.maximum(fy, 0), np.maximum(-fy, 0)
200 S =
level_op(nc, H, axc / H**2, ayc / H**2, px, mx, py, my, unk_c, Wc)
201 ax, ay, unk, n, hh, L = axc, ayc, unk_c, nc, H, L + 1
292def krylov_its(A, levels, singular, nu, method="bicgstab", maxit=300):
294 rng = np.random.default_rng(5)
295 b = np.where(u, 1 + 0.3 * rng.standard_normal(A.shape[0]), 0.0)
298 M = spla.LinearOperator(A.shape, matvec=
precond(levels, singular, nu))
300 cb =
lambda _: cnt.__setitem__(0, cnt[0] + 1)
301 if method ==
"bicgstab":
302 x, info = spla.bicgstab(A, b, M=M, rtol=1e-10, atol=0.0, maxiter=maxit, callback=cb)
304 x, info = spla.gmres(A, b, M=M, rtol=1e-10, atol=0.0, restart=maxit, maxiter=1, callback=cb, callback_type=
"pr_norm")
305 rel = np.linalg.norm(b - A @ x) / np.linalg.norm(b)
306 return cnt[0]
if (info == 0
and rel < 1e-8)
else f
"{cnt[0]}!({rel:.0e})"