flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
advect.py
Go to the documentation of this file.
1"""2-D prototype, round 6: explicit advection with kappa storage on cut cells (small-cell problem).
2
3Flow: solid-body rotation psi = r^2/2 in the annulus a < r < R (a = 0.4, R = 1), both circles
4immersed. Face fluxes are EXACT integrals of the stream function over the OPEN part of each face,
5so the discrete aperture-weighted divergence vanishes in every cut cell (as for flow's projected
6MAC flux). A Gaussian blob next to the outer wall is carried once round (T = 2 pi); the exact
7answer is the initial field. dt = CFL * h / max|u| with the FULL-cell CFL 0.5.
8
9Schemes (first-order upwind fluxes throughout, to isolate the storage / small-cell question):
10 unit : (c^{n+1}-c^n) h^2/dt = -sum F c_up (flow today: conserves sum c, not sum kappa c)
11 kappa : kappa h^2 (c^{n+1}-c^n)/dt = -sum F c_up (explicit; unstable for small kappa?)
12 mb : kappa storage; faces touching a cell with kappa < 1/2 use IMPLICIT upwind values, all
13 other faces explicit (May & Berger-style explicit-implicit split; conservative because
14 each face flux is shared by its two cells); one sparse solve per step
15 impl : kappa storage, all faces implicit (reference for stability)
16Reported: max/min of c (overshoot = instability or max-principle violation), relative drift of
17the physical mass sum kappa c, L1 error vs the exact (initial) field over the fluid volume.
18"""
19import numpy as np, scipy.sparse as sp, scipy.sparse.linalg as spla, sys
20
21R, A = 1.0, 0.4
22
23
24class Ann:
25 def __init__(self, ND, off):
26 h = 2 * R / ND; n = ND + 4; self.h, self.n = h, n
27 x0 = -n * h / 2 + off[0] * h; y0 = -n * h / 2 + off[1] * h
28 self.xe = x0 + h * np.arange(n + 1); self.ye = y0 + h * np.arange(n + 1)
29 self.xc = 0.5 * (self.xe[1:] + self.xe[:-1]); self.yc = 0.5 * (self.ye[1:] + self.ye[:-1])
30 psi = lambda x, y: 0.5 * (x * x + y * y)
31 def open_intervals(c, lo, hi): # fluid set a<r<R on the line (coordinate c fixed), within [lo,hi]
32 sR = np.sqrt(max(R**2 - c * c, 0)); sA = np.sqrt(max(A**2 - c * c, 0))
33 ivs = [(-sR, -sA), (sA, sR)] if sA > 0 else [(-sR, sR)]
34 out = []
35 for (p, q) in ivs:
36 l, u = max(p, lo), min(q, hi)
37 if u > l: out.append((l, u))
38 return out
39 # Fx[i, j]: flux in +x through x-face at xe[i], row j ; Fy[i, j]: +y through y-face ye[j], column i
40 self.Fx = np.zeros((n + 1, n)); self.ax = np.zeros((n + 1, n))
41 for i in range(n + 1):
42 for j in range(n):
43 for (l, u) in open_intervals(self.xe[i], self.ye[j], self.ye[j + 1]):
44 self.Fx[i, j] += psi(self.xe[i], u) - psi(self.xe[i], l)
45 self.ax[i, j] += (u - l) / h
46 self.Fy = np.zeros((n, n + 1)); self.ay = np.zeros((n, n + 1))
47 for i in range(n):
48 for j in range(n + 1):
49 for (l, u) in open_intervals(self.ye[j], self.xe[i], self.xe[i + 1]):
50 self.Fy[i, j] += -(psi(u, self.ye[j]) - psi(l, self.ye[j]))
51 self.ay[i, j] += (u - l) / h
52 g, w = np.polynomial.legendre.leggauss(24)
53 X, Y = np.meshgrid(self.xc, self.yc, indexing="ij"); r = np.hypot(X, Y)
54 self.kap = ((r > A) & (r < R)).astype(float)
55 cut = (np.abs(r - A) < 1.5 * h) | (np.abs(r - R) < 1.5 * h)
56 for i, j in zip(*np.nonzero(cut)):
57 XX, YY = np.meshgrid(self.xc[i] + 0.5 * h * g, self.yc[j] + 0.5 * h * g, indexing="ij")
58 rr = np.hypot(XX, YY)
59 self.kap[i, j] = (np.outer(w, w) * 0.25 * ((rr > A) & (rr < R))).sum()
60 self.X, self.Y = X, Y
61 div = (self.Fx[1:, :] - self.Fx[:-1, :]) + (self.Fy[:, 1:] - self.Fy[:, :-1])
62 self.maxdiv = np.abs(div).max()
63
64
65def upwind_matrix(G, valid, idx, implicit_face):
66 """Matrix U with (U c)_i = sum over faces of F_out * c_upwind (net outflow), restricted to faces
67 selected by implicit_face(face) True/False; returns (U_sel) as sparse."""
68 n = G.n; rows, cols, vals = [], [], []
69 for (F, sh) in ((G.Fx, 0), (G.Fy, 1)):
70 if sh == 0:
71 Fi = F[1:-1, :]; P = idx[:-1, :]; Q = idx[1:, :]; Pm = np.s_[:-1, :]; Qm = np.s_[1:, :]
72 else:
73 Fi = F[:, 1:-1]; P = idx[:, :-1]; Q = idx[:, 1:]
74 m = (P >= 0) & (Q >= 0) & (Fi != 0)
75 for f, p, q, sel in zip(Fi[m], P[m], Q[m], implicit_face(sh)[m]):
76 if not sel: continue
77 up = p if f > 0 else q
78 # outflow from p: +f c_up ; inflow to q: -f c_up
79 rows += [p, q]; cols += [up, up]; vals += [f, -f]
80 N = valid.sum()
81 return sp.csr_matrix((vals, (rows, cols)), shape=(N, N))
82
83
84def run(ND, scheme, cfl=0.5, off=(0.37, 0.71)):
85 G = Ann(ND, off); h = G.h
86 valid = G.kap > 1e-12
87 idx = -np.ones((G.n, G.n), int); idx[valid] = np.arange(valid.sum())
88 kap = G.kap[valid]
89 small = np.zeros((G.n, G.n), bool); small[valid] = kap < 0.5
90 def faces_touching_small(sh):
91 if sh == 0: return small[:-1, :] | small[1:, :]
92 return small[:, :-1] | small[:, 1:]
93 allf = lambda sh: np.ones((G.n - 1, G.n) if sh == 0 else (G.n, G.n - 1), bool)
94 nof = lambda sh: np.zeros((G.n - 1, G.n) if sh == 0 else (G.n, G.n - 1), bool)
95 c0 = np.exp(-((G.X - 0.7) ** 2 + G.Y**2) / (2 * 0.08**2))[valid]
96 dt = cfl * h / 1.0
97 nsteps = int(np.ceil(2 * np.pi / dt)); dt = 2 * np.pi / nsteps
98 M = kap if scheme != "unit" else np.ones_like(kap)
99 if scheme in ("unit", "kappa"):
100 Uex = upwind_matrix(G, valid, idx, allf); Uim = None
101 elif scheme == "mb":
102 Uim = upwind_matrix(G, valid, idx, faces_touching_small)
103 Uex = upwind_matrix(G, valid, idx, lambda sh: ~faces_touching_small(sh))
104 else:
105 Uim = upwind_matrix(G, valid, idx, allf); Uex = upwind_matrix(G, valid, idx, nof)
106 Mh = M * h * h
107 lu = spla.splu((sp.diags(Mh / dt) + Uim).tocsc()) if Uim is not None else None
108 c = c0.copy(); mass0 = (kap * c).sum()
109 cmax = 1.0
110 for _ in range(nsteps):
111 rhs = Mh / dt * c - Uex @ c
112 c = lu.solve(rhs) if lu is not None else rhs / (Mh / dt)
113 cmax = max(cmax, np.abs(c).max())
114 if not np.isfinite(cmax) or cmax > 1e6:
115 return dict(status="BLOWUP", maxdiv=G.maxdiv, minkap=kap.min())
116 err = (kap * np.abs(c - c0)).sum() / (kap * c0).sum()
117 return dict(status="ok", cmax=c.max(), cmin=c.min(), overshoot=cmax - 1,
118 mass=(kap * c).sum() / mass0 - 1, L1=err, maxdiv=G.maxdiv, minkap=kap.min(), steps=nsteps)
119
120
121if __name__ == "__main__":
122 for ND in (32, 64, 128):
123 for scheme in ("unit", "kappa", "mb", "impl"):
124 o = run(ND, scheme)
125 if o["status"] != "ok":
126 print(f"ND={ND:3d} {scheme:6s} BLOW-UP (min kappa {o['minkap']:.1e}, max|div| {o['maxdiv']:.1e})", flush=True)
127 continue
128 print(f"ND={ND:3d} {scheme:6s} L1={o['L1']:.3e} mass drift={o['mass']:+.2e} "
129 f"max over run={1+o['overshoot']:.4f} min={o['cmin']:+.1e} | min kappa {o['minkap']:.1e} "
130 f"max|div| {o['maxdiv']:.1e} steps {o['steps']}", flush=True)
__init__(self, ND, off)
Definition advect.py:25
upwind_matrix(G, valid, idx, implicit_face)
Definition advect.py:65
run(ND, scheme, cfl=0.5, off=(0.37, 0.71))
Definition advect.py:84