flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
hybrid.py
Go to the documentation of this file.
1"""2-D prototype, round 2: symmetric compact schemes for Neumann / Robin / Dirichlet on the disc.
2
3Gates (D = R = 1): Robin eigenvalue x J1(x) = Bi J0(x) -> mu = x^2 (Bi=0 Neumann j'11^2 is
4the SECOND eigenvalue; Bi=inf is Dirichlet j01^2).
5
6Schemes (all symmetric, <= 5-point rows):
7 P Papac-Gibou-Ratsch 2010: aperture FV, kappa mass, every kappa>0 cell, wall term k*A_w*u_P
8 RF aperture FV, kappa mass, wall flux A_w u_P / (d_centroid + 1/k) (round 1)
9 H* "aperture + link": unknowns = centre-fluid cells; slivers (kappa>0, centre in solid) merged
10 into their fluid-centred face neighbours (mass shared by aperture); fluid-fluid faces
11 aperture-weighted; each axis link P->S crossing the wall carries the Gibou distance theta
12 and a film over the wall area it represents, A_link = h |n_a|:
13 G_link = 1 / (theta/D + 1/(k |n_a|)) (per unit depth, flux = G (u_P - u_s))
14 H-k : mass = kappa_P + merged sliver shares
15 H-1 : mass = 1 (point values, Gibou)
16 Hf-1: same as H-1 but fluid-fluid faces fully open (pure Gibou + Robin links)
17"""
18import numpy as np
19from scipy.optimize import brentq
20from scipy.special import j0, j1, jn_zeros, jnp_zeros
21from disc_bc import Geo, assemble_fv, smallest_eig
22
23
24def mu_ref(Bi):
25 if Bi == 0:
26 return jnp_zeros(1, 1)[0] ** 2
27 if np.isinf(Bi):
28 return jn_zeros(0, 1)[0] ** 2
29 return brentq(lambda x: x * j1(x) - Bi * j0(x), 1e-9, 2.404825557695773) ** 2
30
31
32def hybrid(G, k, mass, full_faces=False):
33 h, n = G.h, G.n
34 sc = G.sdf > 0
35 X, Y = np.meshgrid(G.xc, G.yc, indexing="ij")
36 rr = np.hypot(X, Y)
37 nrm = (X / rr, Y / rr)
38 # fluid-fluid faces
39 ax = np.zeros_like(G.ax); ay = np.zeros_like(G.ay)
40 ax[1:-1, :] = np.where(sc[1:, :] & sc[:-1, :], 1.0 if full_faces else G.ax[1:-1, :], 0)
41 ay[:, 1:-1] = np.where(sc[:, 1:] & sc[:, :-1], 1.0 if full_faces else G.ay[:, 1:-1], 0)
42 wallc = np.zeros((n, n))
43 M = np.where(sc, G.kap, 0.0)
44 for i, j in zip(*np.nonzero(sc)):
45 for (di, dj, a_face, na) in ((1, 0, G.ax[i + 1, j], nrm[0]), (-1, 0, G.ax[i, j], nrm[0]),
46 (0, 1, G.ay[i, j + 1], nrm[1]), (0, -1, G.ay[i, j], nrm[1])):
47 if sc[i + di, j + dj]:
48 continue
49 th = max(G.sdf[i, j] / (G.sdf[i, j] - G.sdf[i + di, j + dj]), 1e-3)
50 if k > 0:
51 film = 0.0 if np.isinf(k) else 1.0 / (k * h * max(abs(na[i, j]), 1e-12))
52 wallc[i, j] += 1.0 / (th + film) / h**2
53 # merge slivers: each sliver's kappa shared among fluid-centred face neighbours by aperture
54 sl = (G.kap > 1e-12) & ~sc
55 for i, j in zip(*np.nonzero(sl)):
56 nb = [(i + 1, j, G.ax[i + 1, j]), (i - 1, j, G.ax[i, j]),
57 (i, j + 1, G.ay[i, j + 1]), (i, j - 1, G.ay[i, j])]
58 w = np.array([a if sc[p, q] else 0.0 for p, q, a in nb])
59 if w.sum() == 0: # no fluid-centred face neighbour: give it to the nearest by distance
60 w = np.array([1.0 if sc[p, q] else 0.0 for p, q, a in nb])
61 if w.sum() == 0:
62 continue
63 w /= w.sum()
64 for (p, q, _), ww in zip(nb, w):
65 M[p, q] += ww * G.kap[i, j]
66 K, _ = assemble_fv(G, ax, ay, sc, wall_coef=wallc)
67 Md = M[sc] if mass == "kappa" else np.ones(sc.sum())
68 return K, Md
69
70
71def run(ND, off, Bi):
72 G = Geo(ND, off); h = G.h
73 cells = G.kap > 1e-12
74 which = 1 if Bi == 0 else 0
75 out = {}
76 if not np.isinf(Bi):
77 K, _ = assemble_fv(G, G.ax, G.ay, cells, wall_coef=np.where(cells, Bi * G.Aw / h**2, 0))
78 out["P"] = smallest_eig(K, G.kap[cells], k=2)[which]
79 if Bi > 0:
80 wc = np.where(cells, G.Aw / (h * h * (np.maximum(G.dwall, 1e-3 * h) + 1 / Bi)), 0)
81 K, _ = assemble_fv(G, G.ax, G.ay, cells, wall_coef=wc)
82 out["RF"] = smallest_eig(K, G.kap[cells], k=2)[which]
83 for tag, mass, full in (("H-k", "kappa", False), ("H-1", "one", False), ("Hf-1", "one", True)):
84 K, M = hybrid(G, Bi, mass, full)
85 out[tag] = smallest_eig(K, M, k=2)[which]
86 return out
87
88
89if __name__ == "__main__":
90 rng = np.random.default_rng(1); offs = rng.random((3, 2))
91 NDs = [16, 32, 64, 128]
92 for Bi in (0.0, 0.1, 1.0, 10.0, 100.0, np.inf):
93 ref = mu_ref(Bi)
94 res = {}
95 for ND in NDs:
96 for o in offs:
97 for t, v in run(ND, o, Bi).items():
98 res.setdefault((t, ND), []).append(v / ref - 1)
99 for t in sorted({t for t, _ in res}):
100 line = f"Bi={Bi:6g} {t:5s}"; prev = None
101 for ND in NDs:
102 e = np.array(res[(t, ND)])
103 em = np.sqrt(np.mean(e**2))
104 line += f" | {ND:3d}: {np.mean(e):+9.2e}" + (f" p={np.log(prev/em)/np.log(2):4.1f}" if prev else " ")
105 prev = em
106 print(line, flush=True)
run(ND, off, Bi)
Definition hybrid.py:71