flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
conj_peters.py
Go to the documentation of this file.
1"""2-D prototype, round 7: Frank Peters' directional conjugate IBM (~/Codes/conjugate/
2IBM_conjugated_transport.tex) against the other conjugate candidates, on HIS test problem.
3
4Test (tex sec. Results): steady -div(k grad T) = 0, disc r < R = 1 with k2, k1 = 1 outside, in a
5square of side 5 with Dirichlet values from the exact infinite-domain solution
6 inside : T = 2 k1/(k1+k2) x ; outside: T = x + (k1-k2)/(k1+k2) R^2 x / r^2.
7Errors at cell centres (L1, L2, Linf as in the tex), k2 = 0.01, 0.5, 2, 100.
8
9Methods (all finite-volume on cell-centred unknowns; one cell = the phase of its centre unless noted)
10 OF-h one field, harmonic mean of the two cell conductivities on a face ("one field")
11 OF-a one field, face conductivity = aperture-weighted arithmetic mean a k1 + (1-a) k2
12 GFM one field, series coefficient across the crossing (Liu-Fedkiw-Kang 2000):
13 k_f = 1/((1/2+xi)/k_P + (1/2-xi)/k_Q) on faces whose centre line crosses the interface
14 DIR Peters' directional scheme: on a crossing line, quadratics through (-3/2, -1/2, xi) and
15 (xi, 1/2, 3/2) sharing T_xi; T_xi minimises T''(xi-)^2 + T''(xi+)^2 (eq. 12); the face
16 flux is k(side of x=0) T'(0) (eq. 13/14). Non-crossing faces use the phase k.
17 Fallback (no second same-phase cell on a side): GFM coefficient.
18 DIR-B DIR + the appendix "second approach" effective conductivity on faces that are CUT but whose
19 centre line does not cross (k_eff with a_s, l_s = 0, n_x).
20 P2F two-field probe-flux cut-cell FV (conj.py): fluid and solid unknowns in cut cells, probe
21 0.7h each side, 2x2 elimination per facet (error read from the DOF of the centre's phase).
22"""
23import numpy as np, scipy.sparse as sp, scipy.sparse.linalg as spla, sys
24
25R = 1.0
26L = 5.0
27
28
29def exact(x, y, k1, k2):
30 r2 = x * x + y * y
31 return np.where(r2 <= R * R, 2 * k1 / (k1 + k2) * x,
32 x + (k1 - k2) / (k1 + k2) * R * R * x / np.maximum(r2, 1e-300))
33
34
35class Box:
36 def __init__(self, ND, off):
37 h = 2 * R / ND; n = int(round(L / h)); self.h, self.n = h, n
38 x0 = -L / 2 + off[0] * h; y0 = -L / 2 + off[1] * h # shift the disc off the grid
39 self.xe = x0 + h * np.arange(n + 1); self.ye = y0 + h * np.arange(n + 1)
40 self.xc = 0.5 * (self.xe[1:] + self.xe[:-1]); self.yc = 0.5 * (self.ye[1:] + self.ye[:-1])
41 self.X, self.Y = np.meshgrid(self.xc, self.yc, indexing="ij")
42 self.solid = np.hypot(self.X, self.Y) < R
43 # solid (disc) apertures of x-faces [i, j] (face at xe[i], row j) and y-faces [i, j]
44 s = np.sqrt(np.maximum(R**2 - self.xe**2, 0))[:, None]
45 self.asx = np.clip(np.minimum(self.ye[None, 1:], s) - np.maximum(self.ye[None, :-1], -s), 0, h) / h
46 s = np.sqrt(np.maximum(R**2 - self.ye**2, 0))[None, :]
47 self.asy = np.clip(np.minimum(self.xe[1:, None], s) - np.maximum(self.xe[:-1, None], -s), 0, h) / h
48
49
50def lagr_d(nodes, x, order):
51 """weights w with p^(order)(x) = sum w_j f_j for the Lagrange polynomial through nodes."""
52 nodes = np.asarray(nodes, float); m = len(nodes)
53 V = np.vander(nodes, m, increasing=True) # p(t) = sum c_k t^k, V c = f
54 Vi = np.linalg.inv(V)
55 if order == 1:
56 row = np.array([k * x ** (k - 1) if k >= 1 else 0.0 for k in range(m)])
57 else:
58 row = np.array([k * (k - 1) * x ** (k - 2) if k >= 2 else 0.0 for k in range(m)])
59 return row @ Vi
60
61
62def dir_face(xi):
63 """Peters' face derivative T'(0) = sum w_j T_j over (T_-3/2, T_-1/2, T_1/2, T_3/2), unit spacing."""
64 left = [-1.5, -0.5, xi]; right = [xi, 0.5, 1.5]
65 a = lagr_d(left, xi, 2); b = lagr_d(right, xi, 2) # T''(xi-) = a.(T-3/2, T-1/2, Txi) ...
66 # minimise (a0 Tm3 + a1 Tm1 + a2 Txi)^2 + (b0 Txi + b1 Tp1 + b2 Tp3)^2 -> Txi = c . (Tm3,Tm1,Tp1,Tp3)
67 den = a[2] ** 2 + b[0] ** 2
68 cxi = -np.array([a[2] * a[0], a[2] * a[1], b[0] * b[1], b[0] * b[2]]) / den
69 if xi >= 0: # face x = 0 lies on the left branch
70 d = lagr_d(left, 0.0, 1)
71 return np.array([d[0], d[1], 0.0, 0.0]) + d[2] * cxi
72 d = lagr_d(right, 0.0, 1)
73 return np.array([0.0, 0.0, d[1], d[2]]) + d[0] * cxi
74
75
76def dir_face_both(xi):
77 """(left-branch row, right-branch row) of T'(0) over (T-3/2, T-1/2, T1/2, T3/2), shared min-curvature T_xi."""
78 left = [-1.5, -0.5, xi]; right = [xi, 0.5, 1.5]
79 a = lagr_d(left, xi, 2); b = lagr_d(right, xi, 2)
80 den = a[2] ** 2 + b[0] ** 2
81 cxi = -np.array([a[2] * a[0], a[2] * a[1], b[0] * b[1], b[0] * b[2]]) / den
82 dl = lagr_d(left, 0.0, 1); dr = lagr_d(right, 0.0, 1)
83 return (np.array([dl[0], dl[1], 0.0, 0.0]) + dl[2] * cxi,
84 np.array([0.0, 0.0, dr[1], dr[2]]) + dr[0] * cxi)
85
86
87def dir_face_flux(xi, kl, kr):
88 """as dir_face_both, but T_xi from FLUX continuity kl T'_l(xi) = kr T'_r(xi) (k-aware)."""
89 left = [-1.5, -0.5, xi]; right = [xi, 0.5, 1.5]
90 a = lagr_d(left, xi, 1); b = lagr_d(right, xi, 1)
91 # kl (a0 Tm3 + a1 Tm1 + a2 Txi) = kr (b0 Txi + b1 Tp1 + b2 Tp3)
92 den = kl * a[2] - kr * b[0]
93 cxi = np.array([-kl * a[0], -kl * a[1], kr * b[1], kr * b[2]]) / den
94 dl = lagr_d(left, 0.0, 1); dr = lagr_d(right, 0.0, 1)
95 return (np.array([dl[0], dl[1], 0.0, 0.0]) + dl[2] * cxi,
96 np.array([0.0, 0.0, dr[1], dr[2]]) + dr[0] * cxi)
97
98
100 """the tex's closed form (eq. after 'with f_A ...'), to cross-check dir_face."""
101 fA, fB, fC, fD = 0.5 - xi, 1.5 - xi, 0.5 + xi, 1.5 + xi
102 W = fA**2 * fB**2 + fC**2 * fD**2
103 txi = np.array([-fA**2 * fB**2 * fC, fA**2 * fB**2 * fD, fC**2 * fD**2 * fB, -fC**2 * fD**2 * fA]) / W
104 if xi >= 0:
105 return np.array([fA / fD, -fB / fC, 0, 0]) + 2 / (fC * fD) * txi
106 return np.array([0, 0, fD / fA, -fC / fB]) - 2 / (fA * fB) * txi
107
108
109def crossing(G, P, Q, axis):
110 """xi in (-1/2, 1/2): crossing of the segment P->Q (unit spacing, face at 0) with the circle."""
111 p = np.array([G.X[P], G.Y[P]]); q = np.array([G.X[Q], G.Y[Q]]); d = q - p
112 a = d @ d; b = 2 * p @ d; c = p @ p - R * R
113 disc = np.sqrt(max(b * b - 4 * a * c, 0))
114 ts = [t for t in ((-b - disc) / (2 * a), (-b + disc) / (2 * a)) if 0 <= t <= 1]
115 return ts[0] - 0.5 if ts else None
116
117
118def solve_one_field(G, k1, k2, method):
119 n, h = G.n, G.h
120 kc = np.where(G.solid, k2, k1)
121 idx = np.arange(n * n).reshape(n, n)
122 rows, cols, vals = [], [], []
123 rhs = np.zeros(n * n)
124 def add(r, c, v): rows.append(r); cols.append(c); vals.append(v)
125 nfb = 0
126 for axis in (0, 1):
127 for i in range(n):
128 for j in range(n):
129 P = (i, j); Q = (i + 1, j) if axis == 0 else (i, j + 1)
130 if Q[axis] >= n: continue
131 a_s = G.asx[i + 1, j] if axis == 0 else G.asy[i, j + 1]
132 kP, kQ = kc[P], kc[Q]
133 cross = G.solid[P] != G.solid[Q]
134 stencil = None # list of (cell, weight) for k * dT/dx * h (unit-spacing derivative)
135 if method == "OF-h":
136 kf = 2 * kP * kQ / (kP + kQ); stencil = [(P, -kf), (Q, kf)]
137 elif method in ("DIRq", "DIRq-side"):
138 kf = (1 - a_s) * k1 + a_s * k2 if method == "DIRq" else kP
139 stencil = [(P, -kf), (Q, kf)]
140 if cross:
141 xi = crossing(G, P, Q, axis)
142 sh = (1, 0) if axis == 0 else (0, 1)
143 Pm = (P[0] - sh[0], P[1] - sh[1]); Qp = (Q[0] + sh[0], Q[1] + sh[1])
144 gl, gr = dir_face_flux(xi, kP, kQ)
145 if method == "DIRq":
146 fP = a_s if G.solid[P] else 1 - a_s
147 w = kP * fP * gl + kQ * (1 - fP) * gr
148 else:
149 w = kP * gl if xi >= 0 else kQ * gr
150 stencil = [(Pm, w[0]), (P, w[1]), (Q, w[2]), (Qp, w[3])]
151 elif method == "DIRc":
152 kf = (1 - a_s) * k1 + a_s * k2
153 stencil = [(P, -kf), (Q, kf)]
154 if cross:
155 xi = crossing(G, P, Q, axis)
156 sh = (1, 0) if axis == 0 else (0, 1)
157 Pm = (P[0] - sh[0], P[1] - sh[1]); Qp = (Q[0] + sh[0], Q[1] + sh[1])
158 gl, gr = dir_face_both(xi)
159 fP = a_s if G.solid[P] else 1 - a_s # fraction of the face in P's phase
160 w = kP * fP * gl + kQ * (1 - fP) * gr
161 stencil = [(Pm, w[0]), (P, w[1]), (Q, w[2]), (Qp, w[3])]
162 elif method == "OF-a":
163 kf = (1 - a_s) * k1 + a_s * k2; stencil = [(P, -kf), (Q, kf)]
164 elif not cross:
165 kf = kP
166 if method == "DIR-B" and 0 < a_s < 1:
167 # appendix 'second approach', reference phase = the centre-line phase, l = 0
168 kr, ko = kP, (k1 if kP == k2 else k2) # line phase, other phase
169 ao = a_s if kP == k1 else 1 - a_s # fraction of the face in the other phase
170 # normal of the interface near the face centre
171 fx = G.xe[i + 1] if axis == 0 else G.xc[i]; fy = G.yc[j] if axis == 0 else G.ye[j + 1]
172 nrm = np.array([fx, fy]) / np.hypot(fx, fy); nx2 = nrm[axis] ** 2
173 S = ao / ko + (1 - ao) / kr
174 kf = (1 / kr * nx2 + S * (1 - nx2)) / ((1 / kr) ** 2 * nx2 + S / (ao * ko + (1 - ao) * kr) * (1 - nx2))
175 stencil = [(P, -kf), (Q, kf)]
176 else:
177 xi = crossing(G, P, Q, axis)
178 sh = (1, 0) if axis == 0 else (0, 1)
179 Pm = (P[0] - sh[0], P[1] - sh[1]); Qp = (Q[0] + sh[0], Q[1] + sh[1])
180 ok = (method in ("DIR", "DIR-B") and min(Pm) >= 0 and max(Qp) < n and
181 G.solid[Pm] == G.solid[P] and G.solid[Qp] == G.solid[Q])
182 if ok:
183 w = dir_face(xi)
184 kf = kP if xi >= 0 else kQ
185 stencil = [(Pm, kf * w[0]), (P, kf * w[1]), (Q, kf * w[2]), (Qp, kf * w[3])]
186 else:
187 if method in ("DIR", "DIR-B"): nfb += 1
188 kf = 1 / ((0.5 + xi) / kP + (0.5 - xi) / kQ); stencil = [(P, -kf), (Q, kf)]
189 # flux F = sum w T (into Q from P is -F... F is k dT/dx: flux in +x = -F)
190 # cell P loses -F (outflow +x is -F) : div contribution for P: -(F)/h^2 ... assemble -div(k grad T)
191 for c, w in stencil:
192 add(idx[P], idx[c], -w / h**2 * -1) # P: -( -F_face_out ) ... see below
193 add(idx[Q], idx[c], -w / h**2)
194 # sign bookkeeping: -div(k grad T) at P = -(F_east - F_west)/h^2 with F = k dT/dx.
195 # The east face of P contributes -F/h^2 to P and the west face of Q contributes +F/h^2 to Q.
196 A = sp.csr_matrix((vals, (rows, cols)), shape=(n * n, n * n))
197 A = -A # flip: the loop above added +F to P and -F to Q
198 # outer Dirichlet faces: flux k1 (Tb - TP)/(h/2) through each boundary face
199 diag = np.zeros(n * n)
200 for (P, bx, by) in ([((0, j), G.xe[0], G.yc[j]) for j in range(n)] + [((n - 1, j), G.xe[n], G.yc[j]) for j in range(n)] +
201 [((i, 0), G.xc[i], G.ye[0]) for i in range(n)] + [((i, n - 1), G.xc[i], G.ye[n]) for i in range(n)]):
202 c = 2 * k1 / h**2
203 diag[idx[P]] += c; rhs[idx[P]] += c * exact(bx, by, k1, k2)
204 A = A + sp.diags(diag)
205 T = spla.spsolve(A.tocsc(), rhs)
206 return T.reshape(n, n), nfb
207
208
209def norms(G, T, k1, k2):
210 e = (T - exact(G.X, G.Y, k1, k2)).ravel()
211 return np.mean(np.abs(e)), np.sqrt(np.mean(e * e)), np.abs(e).max()
212
213
214
215# ---------------------------------------------------------------------------------------------
216# P2F: two-field probe-flux cut-cell FV on the same Box test (K = 1, no contact resistance)
217def solve_p2f(G, k1, k2, c=0.7):
218 from conj import bilin
219 n, h = G.n, G.h
220 gq, wq = np.polynomial.legendre.leggauss(24)
221 ks = G.solid.astype(float)
222 r = np.hypot(G.X, G.Y)
223 cut = np.abs(r - R) < 1.5 * h
224 for i, j in zip(*np.nonzero(cut)):
225 XX, YY = np.meshgrid(G.xc[i] + 0.5 * h * gq, G.yc[j] + 0.5 * h * gq, indexing="ij")
226 ks[i, j] = (np.outer(wq, wq) * 0.25 * (np.hypot(XX, YY) < R)).sum()
227 kf = 1 - ks
228 vf, vs = kf > 1e-12, ks > 1e-12
229 idf = -np.ones((n, n), int); idf[vf] = np.arange(vf.sum())
230 ids = -np.ones((n, n), int); ids[vs] = vf.sum() + np.arange(vs.sum())
231 N = vf.sum() + vs.sum()
232 rows, cols, vals = [], [], []
233 rhs = np.zeros(N)
234 def add(r_, c_, v): rows.append(r_); cols.append(c_); vals.append(v)
235 afx, afy = 1 - G.asx, 1 - G.asy
236 for (idx, D, ax_, ay_) in ((idf, k1, afx, afy), (ids, k2, G.asx, G.asy)):
237 for (a, P, Q) in ((ax_[1:-1, :], idx[:-1, :], idx[1:, :]), (ay_[:, 1:-1], idx[:, :-1], idx[:, 1:])):
238 m = (a > 0) & (P >= 0) & (Q >= 0)
239 for aa, p, q in zip(a[m], P[m], Q[m]):
240 cf = D * aa / h**2
241 add(p, p, cf); add(p, q, -cf); add(q, q, cf); add(q, p, -cf)
242 Ax = -(G.asx[1:, :] - G.asx[:-1, :]) * h; Ay = -(G.asy[:, 1:] - G.asy[:, :-1]) * h
243 Aw = np.hypot(Ax, Ay)
244 s = c * h
245 for i, j in zip(*np.nonzero(Aw > 1e-12 * h)):
246 if idf[i, j] < 0 or ids[i, j] < 0: continue
247 xw = np.array([G.X[i, j], G.Y[i, j]]); xw = xw * R / np.hypot(*xw); nn = xw / R
248 for sf in (s, 1.0 * h, 1.5 * h):
249 pf = bilin(G, idf, xw + sf * nn)
250 if pf is not None: break
251 for ss in (s, 1.0 * h, 1.5 * h):
252 ps = bilin(G, ids, xw - ss * nn)
253 if ps is not None: break
254 gf, gs_ = k1 / sf, k2 / ss
255 # continuity uGs = uGf; flux continuity gf (uG - upf) = gs (ups - uG) -> uG = (gf upf + gs ups)/(gf+gs)
256 # q (solid -> fluid, along n) = gf (uG - upf) = gf gs (ups - upf)/(gf + gs)
257 g = gf * gs_ / (gf + gs_)
258 for (col, wv) in pf:
259 add(idf[i, j], col, +Aw[i, j] * g * wv / h**2); add(ids[i, j], col, -Aw[i, j] * g * wv / h**2)
260 for (col, wv) in ps:
261 add(idf[i, j], col, -Aw[i, j] * g * wv / h**2); add(ids[i, j], col, +Aw[i, j] * g * wv / h**2)
262 # outer Dirichlet (fluid only near the box)
263 for (P, bx, by) in ([((0, j), G.xe[0], G.yc[j]) for j in range(n)] + [((n - 1, j), G.xe[n], G.yc[j]) for j in range(n)] +
264 [((i, 0), G.xc[i], G.ye[0]) for i in range(n)] + [((i, n - 1), G.xc[i], G.ye[n]) for i in range(n)]):
265 cc = 2 * k1 / h**2
266 add(idf[P], idf[P], cc); rhs[idf[P]] += cc * exact(bx, by, k1, k2)
267 A = sp.csr_matrix((vals, (rows, cols)), shape=(N, N))
268 u = spla.spsolve(A.tocsc(), rhs)
269 T = np.where(G.solid, u[np.maximum(ids, 0)], u[np.maximum(idf, 0)])
270 return T, 0
271
272
273
274if __name__ == "__main__":
275 for xi in (-0.49, -0.2, 0.0, 0.3, 0.49):
276 assert np.allclose(dir_face(xi), dir_face_closed(xi), atol=1e-10), xi
277 print("closed-form face weights (tex) == generic Lagrange derivation: OK")
278 methods = sys.argv[1:] or ["OF-h", "OF-a", "GFM", "DIR", "DIR-B"]
279 rng = np.random.default_rng(3); offs = rng.random((2, 2))
280 if "centred" in methods:
281 methods = [m for m in methods if m != "centred"]; offs = np.zeros((1, 2))
282 NDs = [16, 32, 64, 128]
283 for k2 in (0.01, 0.5, 2.0, 100.0):
284 for m in methods:
285 line = f"k2={k2:6g} {m:6s}"; prev = None; fb = 0
286 for ND in NDs:
287 ee = []
288 for o in offs:
289 G = Box(ND, o)
290 T, nfb = (solve_p2f(G, 1.0, k2) if m == "P2F" else solve_one_field(G, 1.0, k2, m)); fb += nfb
291 ee.append(norms(G, T, 1.0, k2))
292 ee = np.mean(ee, axis=0)
293 line += f" | {ND:3d}: L1 {ee[0]:.1e} Linf {ee[2]:.1e}" + (f" p={np.log(prev/ee[0])/np.log(2):4.1f}" if prev is not None else " ")
294 prev = ee[0]
295 print(line + f" fallbacks={fb}", flush=True)
__init__(self, ND, off)
solve_p2f(G, k1, k2, c=0.7)
dir_face_flux(xi, kl, kr)
dir_face_closed(xi)
lagr_d(nodes, x, order)
solve_one_field(G, k1, k2, method)
exact(x, y, k1, k2)
norms(G, T, k1, k2)
crossing(G, P, Q, axis)