flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
jc.py
Go to the documentation of this file.
1"""2-D prototype, round 3: Johansen-Colella / Schwartz-Barad-Colella-Ligocki (2006) style cut-cell FV
2(the AMReX EB approach), with Robin eliminated at the wall. Disc gates as in hybrid.py.
3
4 values : cell-centred, every kappa>0 cell is an unknown, mass kappa
5 face flux : aperture * gradient at the open-face CENTROID, linearly interpolated between the
6 face's own two-point gradient and the parallel neighbouring face's (if open)
7 [toggle fc=False -> plain aperture two-point flux]
8 wall flux : quadratic in the wall-normal distance through u_Gamma, u(p1), u(p2); p1,p2 where
9 the normal ray from the wall centroid crosses the next two cell-centre lines of the
10 dominant normal axis; u(p) by 3-point quadratic interpolation along that line
11 [toggle wall='lin' -> 2-point (u(p1)-u_G)/d1]
12 Robin : D du/ds = k u_G (s into the fluid) -> u_G eliminated per wall:
13 du/ds = S - beta u_G, u_G = D S / (k + D beta); wall flux into cell = -k A_w u_G
14"""
15import numpy as np, scipy.sparse as sp, scipy.sparse.linalg as spla, sys
16from disc_bc import Geo, R
17from hybrid import mu_ref
18
19
20def lagr(xs, x):
21 w = np.ones(len(xs))
22 for a in range(len(xs)):
23 for b in range(len(xs)):
24 if a != b:
25 w[a] *= (x - xs[b]) / (xs[a] - xs[b])
26 return w
27
28
29REACH = [0]
30FB = [0]
31
32def build(G, k, fc=True, wall="quad"):
33 n, h = G.n, G.h
34 valid = G.kap > 1e-12
35 idx = -np.ones((n, n), int); idx[valid] = np.arange(valid.sum())
36 N = valid.sum()
37 rows, cols, vals = [], [], []
38 def add(r, c, v):
39 rows.append(r); cols.append(c); vals.append(v)
40 # ---- faces: open interval centroid offsets
41 def xface_cent(i, j): # x-face at xe[i], row j: open part centroid y offset in units of h
42 s = np.sqrt(max(R**2 - G.xe[i] ** 2, 0)); y0, y1 = G.ye[j], G.ye[j + 1]
43 lo, hi = max(y0, -s), min(y1, s)
44 return (0.5 * (lo + hi) - G.yc[j]) / h
45 def yface_cent(i, j):
46 s = np.sqrt(max(R**2 - G.ye[j] ** 2, 0)); x0, x1 = G.xe[i], G.xe[i + 1]
47 lo, hi = max(x0, -s), min(x1, s)
48 return (0.5 * (lo + hi) - G.xc[i]) / h
49 # flux through x-face (i-1,j)|(i,j) as a list of (cell, coef) giving (grad u . e_x) at centroid
50 def flux_x(i, j):
51 a = G.ax[i, j]
52 if a <= 0 or idx[i - 1, j] < 0 or idx[i, j] < 0:
53 return a, []
54 terms = [((i, j), 1 / h), ((i - 1, j), -1 / h)]
55 if fc and a < 1:
56 d = xface_cent(i, j)
57 jj = j + (1 if d > 0 else -1)
58 if 0 <= jj < n and G.ax[i, jj] > 0 and idx[i - 1, jj] >= 0 and idx[i, jj] >= 0:
59 f = abs(d)
60 terms = [(c, (1 - f) * v) for c, v in terms] + \
61 [((i, jj), f / h), ((i - 1, jj), -f / h)]
62 return a, terms
63 def flux_y(i, j):
64 a = G.ay[i, j]
65 if a <= 0 or idx[i, j - 1] < 0 or idx[i, j] < 0:
66 return a, []
67 terms = [((i, j), 1 / h), ((i, j - 1), -1 / h)]
68 if fc and a < 1:
69 d = yface_cent(i, j)
70 ii = i + (1 if d > 0 else -1)
71 if 0 <= ii < n and G.ay[ii, j] > 0 and idx[ii, j - 1] >= 0 and idx[ii, j] >= 0:
72 f = abs(d)
73 terms = [(c, (1 - f) * v) for c, v in terms] + \
74 [((ii, j), f / h), ((ii, j - 1), -f / h)]
75 return a, terms
76 # -L u * kappa h^2 = -(sum of outward fluxes) ; we assemble K = -L*(kappa h^2)/h^2 so K u = mu kappa u
77 for i, j in zip(*np.nonzero(valid)):
78 r = idx[i, j]
79 for (a, terms, sgn) in (flux_x(i + 1, j) + (+1,), flux_x(i, j) + (-1,),
80 flux_y(i, j + 1) + (+1,), flux_y(i, j) + (-1,)):
81 for (c, v) in terms:
82 add(r, idx[c], -sgn * a * h * v / h**2)
83 if G.Aw[i, j] <= 1e-14 * h or np.isinf(k) and False:
84 pass
85 if G.Aw[i, j] > 1e-14 * h and k > 0:
86 # wall centroid: radial projection of the fluid centroid (exact enough for a circle)
87 cx, cy = G.cx[i, j], G.cy[i, j]
88 rr = np.hypot(cx, cy); xw = np.array([cx, cy]) * R / rr
89 nn = -xw / R # into the fluid
90 ax_ = 0 if abs(nn[0]) >= abs(nn[1]) else 1
91 sg = 1 if nn[ax_] > 0 else -1
92 ci = (i, j)
93 pts = []
94 for m in (1, 2):
95 if ax_ == 0:
96 col = i + sg * m; s = (G.xc[col] - xw[0]) / nn[0]
97 py = xw[1] + s * nn[1]; jn = int(round((py - G.yc[0]) / h))
98 cand = [(col, jn - 1), (col, jn), (col, jn + 1)]; coords = [G.yc[q] for _, q in cand]; t = py
99 else:
100 row = j + sg * m; s = (G.yc[row] - xw[1]) / nn[1]
101 px = xw[0] + s * nn[0]; im = int(round((px - G.xc[0]) / h))
102 cand = [(im - 1, row), (im, row), (im + 1, row)]; coords = [G.xc[q] for q, _ in cand]; t = px
103 ok = all(idx[c] >= 0 for c in cand)
104 if not ok:
105 break
106 pts.append((s, cand, lagr(np.array(coords), t)))
107 if wall.startswith("probe"):
108 if wall[5:6] == "N": # normal-dependent support distance: sigma * 1/2 sum_a |n_a| (units of h)
109 c = float(wall[6:] or 1.1) * 0.5 * (abs(nn[0]) + abs(nn[1]))
110 else:
111 c = (max(0.3, (G.kap[i, j]**2 - 0.25) / (2 * G.kap[i, j])) if wall[5:] == "K"
112 else float(wall[5:] or 1.0))
113 sp_ = c * h
114 p = xw + sp_ * nn
115 fi = (p[0] - G.xc[0]) / h; fj = (p[1] - G.yc[0]) / h
116 i0, j0 = int(np.floor(fi)), int(np.floor(fj)); tx, ty = fi - i0, fj - j0
117 cand = [(i0, j0), (i0 + 1, j0), (i0, j0 + 1), (i0 + 1, j0 + 1)]
118 ww = [(1 - tx) * (1 - ty), tx * (1 - ty), (1 - tx) * ty, tx * ty]
119 okm = [idx[q] >= 0 for q in cand]
120 if not all(okm):
121 FB[0] += 1
122 wsum = sum(wv for o_, wv in zip(okm, ww) if o_)
123 if wsum < 0.25:
124 raise RuntimeError("probe hits invalid cells")
125 cand = [q for q, o_ in zip(cand, okm) if o_]
126 ww = [wv / wsum for wv, o_ in zip(ww, okm) if o_]
127 reach = max(max(abs(q[0] - i), abs(q[1] - j)) for q in cand)
128 REACH[0] = max(REACH[0], reach)
129 pts = [(sp_, cand, np.array(ww))]
130 use_quad = (wall == "quad" and len(pts) == 2)
131 if len(pts) == 0:
132 raise RuntimeError("no interpolation point")
133 # du/ds = sum_c S_c u_c - beta u_G
134 if use_quad:
135 (d1, c1, w1), (d2, c2, w2) = pts
136 den = d1 * d2 * (d2 - d1)
137 S = [(c, d2 * d2 * w / den) for c, w in zip(c1, w1)] + \
138 [(c, -d1 * d1 * w / den) for c, w in zip(c2, w2)]
139 beta = (d1 + d2) / (d1 * d2)
140 else:
141 (d1, c1, w1) = pts[0]
142 S = [(c, w / d1) for c, w in zip(c1, w1)]; beta = 1 / d1
143 # flux out of fluid through wall = D du/ds_out... wall flux INTO cell = -D du/ds (D=1) ;
144 # with Robin: D du/ds = k u_G -> u_G = S.u/(k + beta); wall term into cell = -k A_w u_G
145 fac = 1.0 if np.isinf(k) else k / (k + beta)
146 # inf: u_G=0 -> into-cell flux = -(S.u)
147 for c, v in S:
148 add(r, idx[c], fac * G.Aw[i, j] * v / h**2)
149 K = sp.csr_matrix((vals, (rows, cols)), shape=(N, N))
150 return K, G.kap[valid]
151
152
153def eig(K, M, which):
154 s = 1 / np.sqrt(M)
155 A = sp.diags(s) @ K @ sp.diags(s)
156 w = spla.eigs(A.tocsc(), k=3, sigma=-1e-3, which="LM", return_eigenvectors=False)
157 w = np.sort(w.real)
158 return w[which]
159
160
161if __name__ == "__main__":
162 rng = np.random.default_rng(1); offs = rng.random((3, 2))
163 NDs = [16, 32, 64, 128]
164 variants = {v: dict(fc=False, wall=v) for v in (sys.argv[1:] or ["probe0.7"])}
165 for Bi in (0.0, 0.1, 1.0, 10.0, 100.0, np.inf):
166 ref = mu_ref(Bi); which = 1 if Bi == 0 else 0
167 for tag, kw in variants.items():
168 line = f"Bi={Bi:6g} {tag:8s}"; prev = None
169 for ND in NDs:
170 e = []
171 for o in offs:
172 G = Geo(ND, o)
173 K, M = build(G, Bi, **kw)
174 e.append(eig(K, M, which) / ref - 1)
175 em = np.sqrt(np.mean(np.square(e)))
176 line += f" | {ND:3d}: {np.mean(e):+9.2e}" + (f" p={np.log(prev/em)/np.log(2):4.1f}" if prev else " ")
177 prev = em
178 print(line + f' reach={REACH[0]} fallbacks={FB[0]}', flush=True); REACH[0] = 0; FB[0] = 0
eig(K, M, which)
Definition jc.py:153
lagr(xs, x)
Definition jc.py:20