flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
packing2d.py
Go to the documentation of this file.
1"""2-D prototype, round 8: NEAR-CONTACT conjugate conduction.
2
3Effective conductivity of a periodic square array of cylinders (unit cell, cylinder radius R at the
4centre), macroscopic gradient dT/dx = -1, k_f = 1. Gap g = 1 - 2R from 0.2 down to 0.01.
5Unknowns are periodic: T(x) = theta - x. k_eff = total +x heat flux through a face column.
6
7Methods
8 P2F two-field probe-flux cut-cell FV (conj.py), probe 0.7h, round-4 fallback ladder
9 (renormalised valid weights -> 1.0h -> 1.5h). A probe landing inside ANOTHER solid image
10 is not detected (the current design's blind spot).
11 P2Fg P2F + gap-aware fluid probe: s_f = min(0.7h, 0.5 * normal distance to the next solid)
12 PH Peters' hybrid (his 3-D packing code construct_flux_ibm.m, transcribed to 2-D):
13 simple crossing face (1 crossing, 2 same-phase cells each side) -> directional
14 min-curvature quadratic, aperture-weighted branches;
15 close / very close crossing face -> appendix series-parallel
16 k_eff = [k_f + (k_s-k_f) a_s (1-n_x^2)] / [1 + (k_f/k_s - 1) l_s n_x^2]
17 (l_s = solid fraction of the centre line);
18 cut non-crossing face -> k_f + (k_s-k_f) a_s (1-n_x^2) (fluid centres; mirror for solid).
19Reference: Richardson extrapolation (p = 2) of the two finest resolutions, compared across methods.
20"""
21import numpy as np, scipy.sparse as sp, scipy.sparse.linalg as spla, sys
22from conj_peters import dir_face_both
23
24C0 = np.array([0.5, 0.5])
25IMG = [np.array([a, b], float) for a in (-1, 0, 1) for b in (-1, 0, 1)]
26
27
28def sdf(p, R): # > 0 in fluid
29 return min(np.hypot(*(p - C0 - s)) for s in IMG) - R
30
31
32def center_of(p):
33 return min((C0 + s for s in IMG), key=lambda c: np.hypot(*(p - c)))
34
35
36class Cell:
37 def __init__(self, n, R, off=(0.0, 0.0)):
38 self.n, self.R, self.h = n, R, 1.0 / n
39 h = self.h
40 self.x0 = off[0] * h; self.y0 = off[1] * h
41 self.xc = self.x0 + (np.arange(n) + 0.5) * h; self.yc = self.y0 + (np.arange(n) + 0.5) * h
42 self.xe = self.x0 + np.arange(n + 1) * h; self.ye = self.y0 + np.arange(n + 1) * h
43 X, Y = np.meshgrid(self.xc, self.yc, indexing="ij"); self.X, self.Y = X, Y
44 self.s = np.array([[sdf(np.array([X[i, j], Y[i, j]]), R) for j in range(n)] for i in range(n)])
45 self.solid = self.s < 0
46 def solid_len(c, lo, hi, axis):
47 tot = 0.0
48 for sft in IMG:
49 cc = C0 + sft; d = c - cc[axis]
50 if abs(d) >= R: continue
51 w = np.sqrt(R * R - d * d); m = cc[1 - axis]
52 tot += max(0.0, min(hi, m + w) - max(lo, m - w))
53 return min(tot, hi - lo)
54 self.asx = np.array([[solid_len(self.xe[i], self.ye[j], self.ye[j + 1], 0) / h for j in range(n)] for i in range(n + 1)])
55 self.asy = np.array([[solid_len(self.ye[j], self.xe[i], self.xe[i + 1], 1) / h for j in range(n + 1)] for i in range(n)])
56 g, w = np.polynomial.legendre.leggauss(12)
57 self.ks = self.solid.astype(float)
58 for i, j in zip(*np.nonzero(np.abs(self.s) < 0.75 * h)):
59 pts = [(X[i, j] + 0.5 * h * a, Y[i, j] + 0.5 * h * b, wa * wb * 0.25) for a, wa in zip(g, w) for b, wb in zip(g, w)]
60 self.ks[i, j] = sum(ww for (px, py, ww) in pts if sdf(np.array([px, py]), R) < 0)
61
62 def pos(self, c): # unwrapped centre of (possibly out-of-range) index c
63 return np.array([self.x0 + (c[0] + 0.5) * self.h, self.y0 + (c[1] + 0.5) * self.h])
64
65
66class System:
67 """rows of 'sum outward k dT/dn' with T = theta - x; theta periodic"""
68 def __init__(self, N):
69 self.r, self.c, self.v = [], [], []; self.rhs = np.zeros(N); self.N = N
70
71 def add(self, row, col, w, xcol): # contribution w * T(col) = w*theta(col) - w*x(col)
72 self.r.append(row); self.c.append(col); self.v.append(w); self.rhs[row] += w * xcol
73
74 def solve(self, pin=0):
75 A = sp.csr_matrix((self.v, (self.r, self.c)), shape=(self.N, self.N)).tolil()
76 rhs = self.rhs.copy(); A[pin, :] = 0; A[pin, pin] = 1; rhs[pin] = 0
77 return spla.spsolve(A.tocsc(), rhs)
78
79
80def k_PH(G, kf, ks):
81 n, h = G.n, G.h
82 kc = np.where(G.solid, ks, kf)
83 I = lambda c: (c[0] % n) * n + (c[1] % n)
84 S = System(n * n); col = []
85 for axis in (0, 1):
86 sh = np.array((1, 0) if axis == 0 else (0, 1))
87 for i in range(n):
88 for j in range(n):
89 P = (i, j); Q = (i + sh[0], j + sh[1])
90 Pw = (P[0] % n, P[1] % n); Qw = (Q[0] % n, Q[1] % n)
91 sP, sQ = G.solid[Pw], G.solid[Qw]
92 a_s = G.asx[i + 1, j] if axis == 0 else G.asy[i, j + 1]
93 pP = G.pos(P); fpt = pP + 0.5 * h * sh
94 nrm = fpt - center_of(fpt); nrm /= np.hypot(*nrm); nx2 = nrm[axis] ** 2
95 if min(abs(G.s[Pw]), abs(G.s[Qw])) > 1.5 * h:
96 ts = np.array([0.0, 1.0]); sv = np.array([G.s[Pw], G.s[Qw]])
97 else:
98 ts = np.linspace(0, 1, 129); sv = np.array([sdf(pP + t * h * sh, G.R) for t in ts])
99 cr = np.nonzero(sv[:-1] * sv[1:] < 0)[0]
100 if len(cr) == 0:
101 if 0 < a_s < 1:
102 kk = kf + (ks - kf) * a_s * (1 - nx2) if not sP else ks + (kf - ks) * (1 - a_s) * (1 - nx2)
103 else:
104 kk = kc[Pw]
105 st = [(P, -kk), (Q, kk)]
106 else:
107 Pm = (P[0] - sh[0], P[1] - sh[1]); Qp = (Q[0] + sh[0], Q[1] + sh[1])
108 simple = (len(cr) == 1 and G.solid[Pm[0] % n, Pm[1] % n] == sP and G.solid[Qp[0] % n, Qp[1] % n] == sQ)
109 if simple:
110 k_ = cr[0]; t0 = ts[k_] + (ts[k_ + 1] - ts[k_]) * sv[k_] / (sv[k_] - sv[k_ + 1])
111 gl, gr = dir_face_both(t0 - 0.5)
112 fP = a_s if sP else 1 - a_s
113 w = kc[Pw] * fP * gl + kc[Qw] * (1 - fP) * gr
114 st = [(Pm, w[0]), (P, w[1]), (Q, w[2]), (Qp, w[3])]
115 else:
116 ls = np.mean(sv < 0)
117 kk = (kf + (ks - kf) * a_s * (1 - nx2)) / (1 + (kf / ks - 1) * ls * nx2)
118 st = [(P, -kk), (Q, kk)]
119 for c, wv in st: # F = sum w T / h ; +F/h to P (outward east), -F/h to Q
120 xc = G.pos(c)[0]
121 S.add(I(P), I(c), wv / h**2, xc); S.add(I(Q), I(c), -wv / h**2, xc)
122 if axis == 0 and i == n - 1:
123 col.append(st)
124 th = S.solve()
125 T = lambda c: th[I(c)] - G.pos(c)[0]
126 return sum(-sum(wv * T(c) for c, wv in st) / h * h for st in col) # sum of -k dT/dx * h
127
128
129def bilin(G, ids_fn, p, valid):
130 n, h = G.n, G.h
131 fi = (p[0] - G.x0) / h - 0.5; fj = (p[1] - G.y0) / h - 0.5
132 i0, j0 = int(np.floor(fi)), int(np.floor(fj)); tx, ty = fi - i0, fj - j0
133 cand = [(i0, j0), (i0 + 1, j0), (i0, j0 + 1), (i0 + 1, j0 + 1)]
134 ww = [(1 - tx) * (1 - ty), tx * (1 - ty), (1 - tx) * ty, tx * ty]
135 ok = [valid[c[0] % n, c[1] % n] for c in cand]
136 ws = sum(w for w, o in zip(ww, ok) if o)
137 if ws < 0.5: return None, False
138 return [(c, w / ws) for c, w, o in zip(cand, ww, ok) if o], not all(ok)
139
140
141def k_P2F(G, kf, ks, gapaware=False, c=0.7):
142 n, h = G.n, G.h
143 kfr = 1 - G.ks
144 # a phase DOF exists where the phase has volume OR any open face aperture (consistency with the
145 # exact apertures; an independent volume quadrature can miss corner slivers)
146 def touched(ax_, ay_):
147 t = (ax_[1:, :] > 0) | (ax_[:-1, :] > 0) | (ay_[:, 1:] > 0) | (ay_[:, :-1] > 0)
148 return t
149 vf = (kfr > 1e-12) | touched(1 - G.asx, 1 - G.asy)
150 vs = (G.ks > 1e-12) | touched(G.asx, G.asy)
151 idf = -np.ones((n, n), int); idf[vf] = np.arange(vf.sum())
152 ids = -np.ones((n, n), int); ids[vs] = vf.sum() + np.arange(vs.sum())
153 N = vf.sum() + vs.sum()
154 S = System(N)
155 If = lambda cc: idf[cc[0] % n, cc[1] % n]; Is = lambda cc: ids[cc[0] % n, cc[1] % n]
156 col = []
157 for (Ix, D, ax_, ay_, valid) in ((If, kf, 1 - G.asx, 1 - G.asy, vf), (Is, ks, G.asx, G.asy, vs)):
158 for axis in (0, 1):
159 sh = (1, 0) if axis == 0 else (0, 1)
160 for i in range(n):
161 for j in range(n):
162 a = ax_[i + 1, j] if axis == 0 else ay_[i, j + 1]
163 P = (i, j); Q = (i + sh[0], j + sh[1])
164 if a <= 0 or Ix(P) < 0 or Ix(Q) < 0: continue
165 cf = D * a / h**2
166 for (cc, wv) in ((P, -cf), (Q, cf)):
167 xc = G.pos(cc)[0]
168 S.add(Ix(P), Ix(cc), wv, xc); S.add(Ix(Q), Ix(cc), -wv, xc)
169 if axis == 0 and i == n - 1:
170 col.append((Ix, D * a, P, Q))
171 Ax = -(G.asx[1:, :] - G.asx[:-1, :]) * h; Ay = -(G.asy[:, 1:] - G.asy[:, :-1]) * h
172 Aw = np.hypot(Ax, Ay)
173 stats = dict(fallback=0, inside_other=0)
174 for i, j in zip(*np.nonzero(Aw > 1e-12 * h)):
175 if idf[i, j] < 0 or ids[i, j] < 0: continue
176 p0 = G.pos((i, j)); cc = center_of(p0); nn = (p0 - cc) / np.hypot(*(p0 - cc)); xw = cc + G.R * nn
177 sf_max = c * h
178 if gapaware: # normal distance to the next solid along +n
179 ts = np.linspace(1e-4, 3 * h, 300)
180 hit = [t for t in ts if sdf(xw + t * nn, G.R) < 0 and np.hypot(*(xw + t * nn - cc)) > G.R + 1e-12]
181 if hit: sf_max = min(sf_max, 0.5 * hit[0])
182 pf = None
183 for sf in (sf_max, 1.0 * h, 1.5 * h):
184 pf, fb = bilin(G, If, xw + sf * nn, vf)
185 if pf is not None: break
186 if sdf(xw + sf * nn, G.R) < 0: stats["inside_other"] += 1
187 for ss in (c * h, 1.0 * h, 1.5 * h):
188 ps, fb2 = bilin(G, Is, xw - ss * nn, vs)
189 if ps is not None: break
190 stats["fallback"] += int(fb) + int(fb2)
191 gf, gs = kf / sf, ks / ss
192 g = gf * gs / (gf + gs) # q (solid -> fluid) = g (T_ps - T_pf); +q A_w into fluid
193 A = Aw[i, j] / h**2
194 # rows are sum_faces k grad T . n_out (gradient flux, = div k grad T). Through the facet the
195 # fluid cell's k grad T . n_out = -k dT/dn_f = q (q = heat flux solid -> fluid along n), and the
196 # solid cell's is -q. q = g (T_ps - T_pf).
197 for cc_, wv in pf:
198 x_ = G.pos(cc_)[0]
199 S.add(idf[i, j], If(cc_), -A * g * wv, x_); S.add(ids[i, j], If(cc_), +A * g * wv, x_)
200 for cc_, wv in ps:
201 x_ = G.pos(cc_)[0]
202 S.add(idf[i, j], Is(cc_), +A * g * wv, x_); S.add(ids[i, j], Is(cc_), -A * g * wv, x_)
203 th = S.solve(pin=0)
204 q = 0
205 for (Ix, ka, P, Q) in col:
206 TP = th[Ix(P)] - G.pos(P)[0]; TQ = th[Ix(Q)] - G.pos(Q)[0]
207 q += -ka * (TQ - TP) / h * h
208 return q, stats
209
210
211if __name__ == "__main__":
212 ks = float(sys.argv[1]) if len(sys.argv) > 1 else 100.0
213 gaps = [float(a) for a in sys.argv[2:]] or [0.2, 0.05, 0.02]
214 for gap in gaps:
215 R = 0.5 * (1 - gap)
216 res = {}
217 for n in (16, 32, 64, 128):
218 G = Cell(n, R, off=(0.0, 0.0))
219 res.setdefault("PH", []).append(k_PH(G, 1.0, ks))
220 q, st = k_P2F(G, 1.0, ks); res.setdefault("P2F", []).append(q); res.setdefault("P2F_st", []).append(st)
221 q, st = k_P2F(G, 1.0, ks, gapaware=True); res.setdefault("P2Fg", []).append(q); res.setdefault("P2Fg_st", []).append(st)
222 print(f"gap={gap} n={n:3d} (cells/gap {gap*n:5.2f}) PH={res['PH'][-1]:.6f} P2F={res['P2F'][-1]:.6f} "
223 f"P2Fg={res['P2Fg'][-1]:.6f} | P2F probe-in-other-solid {res['P2F_st'][-1]['inside_other']}", flush=True)
224 for m in ("PH", "P2F", "P2Fg"):
225 v = res[m]; rich = v[-1] + (v[-1] - v[-2]) / 3
226 print(f" {m:4s} Richardson(64,128) = {rich:.6f} successive diffs {np.diff(v)}")
__init__(self, n, R, off=(0.0, 0.0))
Definition packing2d.py:37
pos(self, c)
Definition packing2d.py:62
add(self, row, col, w, xcol)
Definition packing2d.py:71
__init__(self, N)
Definition packing2d.py:68
solve(self, pin=0)
Definition packing2d.py:74
bilin(G, ids_fn, p, valid)
Definition packing2d.py:129
k_PH(G, kf, ks)
Definition packing2d.py:80
center_of(p)
Definition packing2d.py:32
sdf(p, R)
Definition packing2d.py:28
k_P2F(G, kf, ks, gapaware=False, c=0.7)
Definition packing2d.py:141