flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
tracers.py
Go to the documentation of this file.
1"""2-D prototype: dispersion estimators from Brownian tracer trajectories, against an exact
2Eulerian reference (Brenner closure solved spectrally).
3
4Flow: periodic cellular flow with mean through-flow on [0, 2pi)^2,
5 psi = U0*y + A*sin(x)*sin(y) -> u = U0 + A sin x cos y, v = -A cos x sin y
6A > U0 gives closed recirculation cells (an analogue of hold-up regions in a bed).
7
8Reference: B periodic, mean zero, u.grad B - D lap B = <u> - u (one per direction)
9 D*_aa = D <|e_a + grad B_a|^2> (no walls -> <grad B> = 0)
10
11Estimators compared on the same trajectories:
12 MSD : late-time slope 1/2 dVar(x)/dt over [t1, t2], error by batch means over particles
13 GK : D + int_0^T <u'(0) u'(tau)> dtau (velocity autocorrelation only) -- misses the
14 velocity-noise cross-covariance when u_x depends on x
15 FPT : first passage over n periods: D = L^2 Var(tau) / (2 <tau>^3) (inverse Gaussian)
16"""
17import numpy as np
18import scipy.sparse.linalg as spla
19import sys, time
20
21TWO_PI = 2 * np.pi
22
23
24def closure_reference(U0, A, D, N=96):
25 k = np.fft.fftfreq(N, 1.0 / N)
26 KX, KY = np.meshgrid(k, k, indexing="ij")
27 K2 = KX**2 + KY**2
28 x = TWO_PI * np.arange(N) / N
29 X, Y = np.meshgrid(x, x, indexing="ij")
30 u = U0 + A * np.sin(X) * np.cos(Y)
31 v = -A * np.cos(X) * np.sin(Y)
32
33 def L(bvec):
34 b = bvec.reshape(N, N)
35 bh = np.fft.fft2(b)
36 bx = np.real(np.fft.ifft2(1j * KX * bh)); by = np.real(np.fft.ifft2(1j * KY * bh))
37 lap = np.real(np.fft.ifft2(-K2 * bh))
38 r = u * bx + v * by - D * lap
39 return (r - r.mean()).ravel() + b.mean() # pin the mean
40
41 def Pinv(r):
42 rh = np.fft.fft2(r.reshape(N, N))
43 with np.errstate(divide="ignore", invalid="ignore"):
44 bh = np.where(K2 > 0, rh / (D * K2), rh)
45 return np.real(np.fft.ifft2(bh)).ravel()
46
47 op = spla.LinearOperator((N * N, N * N), matvec=L)
48 P = spla.LinearOperator((N * N, N * N), matvec=Pinv)
49 out = []
50 for comp, rhs in (("xx", u.mean() - u), ("yy", v.mean() - v)):
51 b, info = spla.gmres(op, rhs.ravel(), M=P, rtol=1e-12, restart=200, maxiter=50)
52 assert info == 0
53 bh = np.fft.fft2(b.reshape(N, N))
54 bx = np.real(np.fft.ifft2(1j * KX * bh)); by = np.real(np.fft.ifft2(1j * KY * bh))
55 if comp == "xx":
56 out.append(D * np.mean((1 + bx) ** 2 + by**2))
57 else:
58 out.append(D * np.mean(bx**2 + (1 + by) ** 2))
59 return out, u.mean()
60
61
62def vel(x, y, U0, A):
63 return U0 + A * np.sin(x) * np.cos(y), -A * np.cos(x) * np.sin(y)
64
65
66def simulate(U0, A, D, Np, dt, T, nper_fpt=(1, 4, 16), seed=0, nsave=400):
67 rng = np.random.default_rng(seed)
68 x = rng.random(Np) * TWO_PI; y = rng.random(Np) * TWO_PI # uniform = invariant measure
69 x0 = x.copy()
70 nsteps = int(round(T / dt))
71 every = max(1, nsteps // nsave)
72 ts, X, Y = [], [], []
73 U = [] # u' samples for GK (saved at the same cadence)
74 fpt = {n: np.full(Np, np.nan) for n in nper_fpt}
75 s = np.sqrt(2 * D * dt)
76 for it in range(nsteps):
77 if it % every == 0:
78 ts.append(it * dt); X.append(x - x0); Y.append(y.copy())
79 U.append(vel(x, y, U0, A)[0])
80 u1, v1 = vel(x, y, U0, A)
81 u2, v2 = vel(x + u1 * dt, y + v1 * dt, U0, A) # Heun drift
82 x = x + 0.5 * (u1 + u2) * dt + s * rng.standard_normal(Np)
83 y = y + 0.5 * (v1 + v2) * dt + s * rng.standard_normal(Np)
84 t = (it + 1) * dt
85 for n in nper_fpt:
86 hit = np.isnan(fpt[n]) & (x - x0 >= n * TWO_PI)
87 fpt[n][hit] = t
88 return np.array(ts), np.array(X), np.array(U), fpt
89
90
91def est_msd(ts, X, t1, t2, nb=20):
92 i1, i2 = np.searchsorted(ts, t1), np.searchsorted(ts, t2)
93 def one(Xs):
94 v = Xs.var(axis=1)
95 # least-squares slope of Var over [t1, t2]
96 tt = ts[i1:i2 + 1]; vv = v[i1:i2 + 1]
97 return 0.5 * np.polyfit(tt, vv, 1)[0]
98 full = one(X)
99 b = [one(X[:, k::nb]) for k in range(nb)]
100 return full, np.std(b, ddof=1) / np.sqrt(nb)
101
102
103def est_gk(ts, U, D, Umean, tmax, nb=20):
104 up = U - Umean
105 dt = ts[1] - ts[0]
106 nl = int(tmax / dt)
107 def one(u):
108 # time-origin averaged autocorrelation (stationary: uniform start = invariant measure)
109 C = np.array([np.mean(u[: len(u) - l] * u[l:]) for l in range(nl)])
110 return D + dt * (C.sum() - 0.5 * C[0])
111 full = one(up)
112 b = [one(up[:, k::nb]) for k in range(nb)]
113 return full, np.std(b, ddof=1) / np.sqrt(nb)
114
115
116def est_fpt(tau, L, nb=20):
117 def one(t):
118 t = t[~np.isnan(t)]
119 return L**2 * t.var() / (2 * t.mean() ** 3), np.isnan(t).mean()
120 full, _ = one(tau)
121 miss = np.isnan(tau).mean()
122 b = [one(tau[k::nb])[0] for k in range(nb)]
123 return full, np.std(b, ddof=1) / np.sqrt(nb), miss
124
125
126if __name__ == "__main__":
127 U0, A = 1.0, float(sys.argv[1]) if len(sys.argv) > 1 else 2.0
128 D = float(sys.argv[2]) if len(sys.argv) > 2 else 0.1
129 Np = int(sys.argv[3]) if len(sys.argv) > 3 else 20000
130 T = float(sys.argv[4]) if len(sys.argv) > 4 else 600.0
131 dt = 0.01
132 (Dxx, Dyy), Um = closure_reference(U0, A, D)
133 tD = TWO_PI**2 / D
134 print(f"U0={U0} A={A} D={D} Pe=U0*L/D={U0*TWO_PI/D:.0f} t_D=L^2/D={tD:.0f} T={T}")
135 print(f"reference (spectral closure): D*_xx = {Dxx:.6f} D*_yy = {Dyy:.6f} <u> = {Um:.6f}")
136 t0 = time.time()
137 ts, X, U, fpt = simulate(U0, A, D, Np, dt, T)
138 print(f"simulated {Np} tracers x {int(T/dt)} steps in {time.time()-t0:.0f} s")
139 # running estimates: apparent D(t) = Var/2t and the local slope -> preasymptotic diagnostics
140 v = X.var(axis=1)
141 for frac in (0.05, 0.1, 0.25, 0.5, 1.0):
142 i = min(len(ts) - 1, int(frac * (len(ts) - 1)))
143 j = max(1, i // 2)
144 print(f" t={ts[i]:7.1f} ({ts[i]/tD:5.2f} t_D): Var/2t = {v[i]/(2*ts[i]):.4f} "
145 f"slope[t/2,t] = {0.5*(v[i]-v[j])/(ts[i]-ts[j]):.4f}")
146 for (t1, t2) in ((0.25 * T, 0.5 * T), (0.5 * T, T)):
147 m, e = est_msd(ts, X, t1, t2)
148 print(f"MSD slope [{t1:.0f},{t2:.0f}] : {m:.4f} +- {e:.4f} rel.err {m/Dxx-1:+.3%}")
149 for tm in (0.05 * T, 0.2 * T):
150 g, e = est_gk(ts, U, D, Um, tm)
151 print(f"GK (int to {tm:.0f}) : {g:.4f} +- {e:.4f} rel.err {g/Dxx-1:+.3%}")
152 for n, tau in fpt.items():
153 f, e, miss = est_fpt(tau, n * TWO_PI)
154 print(f"FPT over {n:2d} periods : {f:.4f} +- {e:.4f} rel.err {f/Dxx-1:+.3%} (unarrived {miss:.1%})")
closure_reference(U0, A, D, N=96)
Definition tracers.py:24
est_msd(ts, X, t1, t2, nb=20)
Definition tracers.py:91
simulate(U0, A, D, Np, dt, T, nper_fpt=(1, 4, 16), seed=0, nsave=400)
Definition tracers.py:66
vel(x, y, U0, A)
Definition tracers.py:62
est_gk(ts, U, D, Umean, tmax, nb=20)
Definition tracers.py:103
est_fpt(tau, L, nb=20)
Definition tracers.py:116
static double mean(const std::vector< double > &a)