flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
conj.py
Go to the documentation of this file.
1"""2-D prototype, round 4: CONJUGATE transport with the probe-flux cut-cell FV.
2
3Geometry: solid core r < a (diffusivity Ds, capacity gs), fluid annulus a < r < R (Df = 1, capacity
41), u = 0 at r = R (immersed Dirichlet, probe-flux). Interface at r = a:
5 flux continuity q = -Ds du_s/dr = -Df du_f/dr (q: radial, solid -> fluid)
6 q = hc (u_s/K - u_f) (hc = inf -> u_s = K u_f: partition coefficient K)
7Exact: slowest mode of g du/dt = div(D grad u) via a Bessel determinant.
8
9Discretization (two fields on one grid; every cell with fluid fraction > 0 has a fluid unknown,
10every cell with solid-core fraction > 0 has a solid unknown):
11 faces : aperture-weighted two-point fluxes, separately per phase (fluid apertures / core apertures)
12 facet : per cut cell, wall area A_w (aperture area vector), centroid x_w (radial projection),
13 normal n (into the fluid). Fluid probe x_w + c h n (bilinear over fluid unknowns), solid
14 probe x_w - c h n (bilinear over solid unknowns). Two interface equations
15 Df (u_pf - u_Gf)/s = Ds (u_Gs - u_ps)/s (flux continuity, q = -Df (u_pf-u_Gf)/s)
16 q = hc (u_Gs/K - u_Gf) or u_Gs = K u_Gf
17 are solved for (u_Gf, u_Gs) -> q is a linear combination of probe-stencil unknowns;
18 +q A_w enters the fluid cell, -q A_w the solid cell (exactly conservative).
19 The fluid facet and the solid facet of a cell are the same facet; cells cut by the core interface
20 carry both unknowns. Outer boundary: Dirichlet probe flux as in jc.py.
21"""
22import numpy as np, scipy.sparse as sp, scipy.sparse.linalg as spla, sys
23from scipy.optimize import brentq
24from scipy.special import j0, j1, y0, y1
25
26R = 1.0; A = 0.5
27
28
29def exact(Ds, K, hc, gs, nmax=1):
30 def det(mu):
31 wf = np.sqrt(mu); ws = np.sqrt(gs * mu / Ds)
32 # u_s = P J0(ws r); u_f = B J0(wf r) + C Y0(wf r)
33 M = np.zeros((3, 3))
34 M[0] = [0, j0(wf * R), y0(wf * R)] # u_f(R) = 0
35 M[1] = [-Ds * ws * j1(ws * A), wf * j1(wf * A), wf * y1(wf * A)] # Ds u_s' = Df u_f'
36 qs = [Ds * ws * j1(ws * A), 0, 0] # q = -Ds u_s'
37 if np.isinf(hc):
38 M[2] = [j0(ws * A), -K * j0(wf * A), -K * y0(wf * A)] # u_s = K u_f
39 else:
40 M[2] = [qs[0] - hc / K * j0(ws * A), hc * j0(wf * A), hc * y0(wf * A)]
41 return np.linalg.det(M)
42 mus = np.linspace(0.05, 60, 6000); d = [det(m) for m in mus]
43 for i in range(len(mus) - 1):
44 if d[i] * d[i + 1] < 0:
45 return brentq(det, mus[i], mus[i + 1], xtol=1e-13)
46
47
48class Geo2:
49 def __init__(self, ND, off):
50 h = 2 * R / ND; n = ND + 6; self.h, self.n = h, n
51 x0 = -n * h / 2 + off[0] * h; y0_ = -n * h / 2 + off[1] * h
52 self.xe = x0 + h * np.arange(n + 1); self.ye = y0_ + h * np.arange(n + 1)
53 self.xc = 0.5 * (self.xe[1:] + self.xe[:-1]); self.yc = 0.5 * (self.ye[1:] + self.ye[:-1])
54 def ap(edges_fixed, edges_var, rad, inside):
55 s = np.sqrt(np.maximum(rad**2 - edges_fixed**2, 0))[:, None]
56 lo, hi = edges_var[None, :-1], edges_var[None, 1:]
57 return np.clip(np.minimum(hi, s) - np.maximum(lo, -s), 0, h) / h
58 # disc apertures (inside rad) on x-faces [i, j] and y-faces [i, j]
59 dRx = ap(self.xe, self.ye, R, True); dAx = ap(self.xe, self.ye, A, True)
60 dRy = ap(self.ye, self.xe, R, True).T; dAy = ap(self.ye, self.xe, A, True).T
61 self.afx, self.afy = dRx - dAx, dRy - dAy # fluid annulus apertures
62 self.asx, self.asy = dAx, dAy # solid core apertures
63 g, w = np.polynomial.legendre.leggauss(32)
64 X, Y = np.meshgrid(self.xc, self.yc, indexing="ij")
65 r = np.hypot(X, Y)
66 self.kf = ((r > A) & (r < R)).astype(float); self.ks = (r < A).astype(float)
67 cut = (np.abs(r - A) < 1.5 * h) | (np.abs(r - R) < 1.5 * h)
68 for i, j in zip(*np.nonzero(cut)):
69 XX, YY = np.meshgrid(self.xc[i] + 0.5 * h * g, self.yc[j] + 0.5 * h * g, indexing="ij")
70 rr = np.hypot(XX, YY); W = np.outer(w, w) * 0.25
71 self.kf[i, j] = (W * ((rr > A) & (rr < R))).sum(); self.ks[i, j] = (W * (rr < A)).sum()
72 # facets: inner (core) and outer (R) wall areas from the aperture area vectors
73 def area(ax_, ay_):
74 Ax = -(ax_[1:, :] - ax_[:-1, :]) * h; Ay = -(ay_[:, 1:] - ay_[:, :-1]) * h
75 return np.hypot(Ax, Ay)
76 self.Ain = area(dAx, dAy); self.Aout = area(dRx, dRy)
77 self.X, self.Y = X, Y
78
79
80FALLBACK = [0]
81
82
83def bilin(G, idx, p):
84 h = G.h
85 fi = (p[0] - G.xc[0]) / h; fj = (p[1] - G.yc[0]) / h
86 i0, j0_ = int(np.floor(fi)), int(np.floor(fj)); tx, ty = fi - i0, fj - j0_
87 cand = [(i0, j0_), (i0 + 1, j0_), (i0, j0_ + 1), (i0 + 1, j0_ + 1)]
88 ww = [(1 - tx) * (1 - ty), tx * (1 - ty), (1 - tx) * ty, tx * ty]
89 ok = [idx[q] >= 0 for q in cand]
90 if all(ok):
91 return [(idx[q], wv) for q, wv in zip(cand, ww)]
92 wsum = sum(wv for o, wv in zip(ok, ww) if o)
93 if wsum < 0.5:
94 return None
95 FALLBACK[0] += 1
96 return [(idx[q], wv / wsum) for q, wv, o in zip(cand, ww, ok) if o]
97
98
99def build(G, Ds, K, hc, gs, c=0.7):
100 n, h = G.n, G.h
101 vf = G.kf > 1e-12; vs = G.ks > 1e-12
102 idf = -np.ones((n, n), int); idf[vf] = np.arange(vf.sum())
103 ids = -np.ones((n, n), int); ids[vs] = vf.sum() + np.arange(vs.sum())
104 N = vf.sum() + vs.sum()
105 rows, cols, vals = [], [], []
106 def add(r, cc, v): rows.append(r); cols.append(cc); vals.append(v)
107 # faces (two-point, aperture-weighted), K = -(div D grad) * h^2 / h^2
108 for (idx, D, ax_, ay_) in ((idf, 1.0, G.afx, G.afy), (ids, Ds, G.asx, G.asy)):
109 for (a, P, Q) in ((ax_[1:-1, :], idx[:-1, :], idx[1:, :]), (ay_[:, 1:-1], idx[:, :-1], idx[:, 1:])):
110 m = (a > 0) & (P >= 0) & (Q >= 0)
111 for aa, p, q in zip(a[m], P[m], Q[m]):
112 cf = D * aa / h**2
113 add(p, p, cf); add(p, q, -cf); add(q, q, cf); add(q, p, -cf)
114 s = c * h
115 # inner facets (conjugate)
116 for i, j in zip(*np.nonzero(G.Ain > 1e-12 * h)):
117 cx, cy = G.X[i, j], G.Y[i, j]
118 rr = np.hypot(cx, cy); xw = np.array([cx, cy]) * A / rr; nn = xw / A # into fluid (outward)
119 for sf in (s, 1.0 * h, 1.5 * h):
120 pf = bilin(G, idf, xw + sf * nn)
121 if pf is not None: break
122 for ss in (s, 1.0 * h, 1.5 * h):
123 ps = bilin(G, ids, xw - ss * nn)
124 if ps is not None: break
125 if pf is None or ps is None:
126 raise RuntimeError("probe failed")
127 gf, gs_ = 1.0 / sf, Ds / ss
128 # unknowns uGf, uGs: gf (uGf - upf) = gs (ups - uGs) [q = gf (uGf - upf)]
129 # hc=inf: uGs - K uGf = 0 ; else gf (uGf - upf) - hc (uGs/K - uGf) = 0
130 Mx = np.array([[gf, gs_], [-K, 1.0]]) if np.isinf(hc) else np.array([[gf, gs_], [gf + hc, -hc / K]])
131 # rhs = Bf * upf + Bs * ups
132 Bf = np.array([gf, 0.0]) if np.isinf(hc) else np.array([gf, gf])
133 Bs = np.array([gs_, 0.0])
134 Minv = np.linalg.inv(Mx)
135 cf_ = Minv @ Bf; cs_ = Minv @ Bs # uG = cf_*upf + cs_*ups
136 # q = gf (uGf - upf) = gf (cf_[0]-1) upf + gf cs_[0] ups
137 qf = gf * (cf_[0] - 1.0); qs = gf * cs_[0]
138 Aw = G.Ain[i, j]
139 rf = idf[i, j]; rs = ids[i, j]
140 for (col, wv) in pf:
141 if rf >= 0: add(rf, col, -Aw * qf * wv / h**2) # fluid gains +q A_w -> K gets -q
142 if rs >= 0: add(rs, col, +Aw * qf * wv / h**2)
143 for (col, wv) in ps:
144 if rf >= 0: add(rf, col, -Aw * qs * wv / h**2)
145 if rs >= 0: add(rs, col, +Aw * qs * wv / h**2)
146 # outer facets: Dirichlet 0 on the fluid side; flux out of fluid = D (u_p - 0)/s
147 for i, j in zip(*np.nonzero(G.Aout > 1e-12 * h)):
148 if idf[i, j] < 0: continue
149 cx, cy = G.X[i, j], G.Y[i, j]; rr = np.hypot(cx, cy)
150 xw = np.array([cx, cy]) * R / rr; nn = -xw / R
151 pf = bilin(G, idf, xw + s * nn)
152 if pf is None: raise RuntimeError("outer probe failed")
153 for col, wv in pf:
154 add(idf[i, j], col, G.Aout[i, j] * (1.0 / s) * wv / h**2)
155 Kmat = sp.csr_matrix((vals, (rows, cols)), shape=(N, N))
156 M = np.concatenate([G.kf[vf], gs * G.ks[vs]])
157 return Kmat, M
158
159
160def slowest(Kmat, M):
161 sc = 1 / np.sqrt(M)
162 Am = sp.diags(sc) @ Kmat @ sp.diags(sc)
163 w = spla.eigs(Am.tocsc(), k=2, sigma=-1e-3, which="LM", return_eigenvectors=False)
164 return np.sort(w.real)[0]
165
166
167if __name__ == "__main__":
168 rng = np.random.default_rng(1); offs = rng.random((3, 2))
169 NDs = [16, 32, 64, 128]
170 cases = [(1.0, 1.0, np.inf, 1.0), (10.0, 1.0, np.inf, 1.0), (0.1, 1.0, np.inf, 1.0),
171 (100.0, 1.0, np.inf, 0.5), (1.0, 3.0, np.inf, 1.0), (1.0, 1.0, 5.0, 1.0),
172 (10.0, 0.5, 2.0, 2.0)]
173 for (Ds, K, hc, gs) in cases:
174 ref = exact(Ds, K, hc, gs)
175 line = f"Ds={Ds:5g} K={K:3g} hc={hc:4g} gs={gs:3g} mu={ref:8.5f}"; prev = None
176 for ND in NDs:
177 e = []
178 for o in offs:
179 G = Geo2(ND, o)
180 Km, M = build(G, Ds, K, hc, gs)
181 e.append(slowest(Km, M) / ref - 1)
182 em = np.sqrt(np.mean(np.square(e)))
183 line += f" | {ND:3d}: {np.mean(e):+8.1e}" + (f" p={np.log(prev/em)/np.log(2):4.1f}" if prev else " ")
184 prev = em
185 print(line + f' fallbacks={FALLBACK[0]}', flush=True); FALLBACK[0] = 0
__init__(self, ND, off)
Definition conj.py:49
bilin(G, idx, p)
Definition conj.py:83
slowest(Kmat, M)
Definition conj.py:160
exact(Ds, K, hc, gs, nmax=1)
Definition conj.py:29