flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
cutwall_periodic_control.py
Go to the documentation of this file.
1import importlib.util, os, subprocess, sys
2_local = os.environ.get("PECLET_LOCAL_BUILD")
3if _local:
4 for p in _local.split(os.pathsep):
5 sys.path.insert(0, p)
6elif importlib.util.find_spec("peclet") is None:
7 subprocess.run([sys.executable, "-m", "pip", "install", "-q", "peclet"], check=True)
8
9import time
10import numpy as np
11import matplotlib.pyplot as plt
12from peclet import flow as sdflow
13from peclet import dem as pdem
14from peclet import geom
15
16plt.rcParams.update({"figure.dpi": 130, "font.size": 9, "axes.axisbelow": True,
17 "figure.facecolor": "white", "savefig.bbox": "tight"})
18
19# The four experiments, from the paper's Table I (the printed viscosity-header unit is an
20# erratum; values are Pa s). u_inf is NOT measured: it is the unbounded terminal velocity from
21# the Abraham correlation, which is how the paper defines Re. u_max/u_inf are the measured
22# ratios from Table II.
23CASES = { # rho_f mu u_inf ratio Re
24 "E1": (970., 0.373, 0.03829, 0.947, 1.5),
25 "E2": (965., 0.212, 0.05992, 0.953, 4.1),
26 "E3": (962., 0.113, 0.09062, 0.959, 11.6),
27 "E4": (960., 0.058, 0.12839, 0.955, 31.9)}
28RHO_P_SI, D_SI, G_SI = 1120., 0.015, 9.81
29
30USTAR, WALL = 0.02, 4.3 # u_inf in cell units; tank wall thickness (off-lattice: 4.3)
31KI_I, KI_R = 2, 17
32
33def run_case(case, DH, sweeps=60, gpu_dt=None, advect=True):
34 rho_f, mu_si, u_inf, ratio_exp, Re = CASES[case]
35 # Grid: round UP to multiples of 8 so the 4-level multigrid can actually coarsen (a 62-wide
36 # grid has one factor of two); the extra padding goes into the wall thickness.
37 NX = int(np.ceil((100 / 15 * DH + 2 * WALL) / 8) * 8)
38 NY = int(np.ceil((160 / 15 * DH + 2 * WALL) / 8) * 8)
39 WX = (NX - 100 / 15 * DH) / 2
40 WY = (NY - 160 / 15 * DH) / 2
41 NU = USTAR * DH / Re
42 GSTAR = G_SI * D_SI / u_inf ** 2 * USTAR ** 2 / DH
43 RATIO = RHO_P_SI / rho_f
44 # dt: a fraction of the particle response time, and small enough that the sphere moves a
45 # fraction of a cell per rebuild
46 tau = RATIO * DH * Re / (18 * USTAR)
47 dt = gpu_dt if gpu_dt else min(max(tau / 12, 1.0), 0.35 / USTAR / 4)
48 b = geom.SceneBuilder()
49 slab = b.add_leaf("box", [NX * 0.7, NY * 0.7, NX * 0.7])
50 cavity = b.add_leaf("box", [(NX - 2 * WX) / 2, (NY - 2 * WY) / 2, (NX - 2 * WX) / 2])
51 tank = b.add_difference(slab, cavity)
52 sph = b.add_leaf("sphere", [DH / 2])
53 ni, nr, _, _ = b.encode()
54 OFF = 0.3 # shift the tank so no wall sits exactly on a grid plane
55 x0 = 0.5 * NX + OFF
56 y0 = WY + OFF + 8.5 * DH
57 ii = np.zeros((2, KI_I), dtype=np.int32); ir = np.zeros((2, KI_R))
58 ii[0] = (tank, -1); ir[0, 0:3] = (0.5 * NX + OFF, 0.5 * NY + OFF, 0.5 * NX + OFF)
59 ir[0, 6] = 1.0; ir[0, 7] = 1.0
60 ii[1] = (sph, -1); ir[1, 0:3] = (x0, y0, x0); ir[1, 6] = 1.0; ir[1, 7] = 1.0
61 s = sdflow.Solver(NX, NY, NX)
62 s.set_rho(1.0); s.set_mu(NU); s.set_dt(dt); s.set_advection(advect)
63 s.set_velocity_solver_params(sweeps); s.set_pressure_solver_params(20)
64 s.set_pressure_multigrid(True, levels=4)
65 s.set_scene(np.asarray(ni, np.int32), np.asarray(nr, float), ii.ravel(), ir.ravel(),
66 periodic=True)
67 s.set_solid_from_scene(True)
68
69 m = RATIO * (np.pi / 6) * DH ** 3
70 Vp = (np.pi / 6) * DH ** 3
71 # Virtual-mass stabilization for the explicit coupling: at rho_p/rho_f = 1.15 the resolved
72 # hydrodynamic force contains an added-mass part evaluated one step late, which rings — and
73 # near the floor the added mass grows and the ringing diverges. Integrating with m + ma and
74 # adding the lagged ma*a back keeps every steady and smooth trajectory EXACT (m dv/dt = F - Fg
75 # when a is converged) while cutting the loop gain on the fluctuation.
76 ma = 2.0 * Vp
77 d = pdem.Simulation(8)
78 d.set_gravity(0.0, 0.0, 0.0)
79 d.set_sphere_shape(1.0)
80 d.set_positions(np.array([[x0, y0, x0]], dtype=np.float32))
81 d.set_inv_mass(np.array([1.0 / (m + ma)], dtype=np.float32))
82 d.set_inv_inertia(np.array([[0, 0, 0]], dtype=np.float32)) # translation-only, like the paper
83 Fg = (RATIO - 1.0) * Vp * GSTAR # buoyant weight (the fluid carries no hydrostatic field)
84 # Hard bound: 2.2x the time to fall the full release height at 0.8 u*. The physical stops
85 # below (near-floor gap, deep post-peak deceleration) fire first; this cannot hang.
86 kmax = int(2.2 * (8.5 * DH / (0.8 * USTAR)) / dt)
87 t0 = time.time(); tr = []; vpk = 0.0; a_prev = np.zeros(3)
88 for k in range(kmax):
89 p = np.asarray(d.get_positions())[0].astype(float)
90 v = np.asarray(d.get_velocities())[0].astype(float)
91 s.set_instance_transform(1, p.tolist())
92 s.set_instance_motion(1, lin_vel=v.tolist())
93 s.rebuild_geometry(); s.step()
94 F = np.asarray(s.hydro_force_torque_reaction())[0][1].astype(float)
95 F[1] -= Fg
96 F += ma * a_prev
97 d.set_external_forces(np.array([F], dtype=np.float32))
98 for _ in range(10):
99 d.step(dt / 10)
100 a_prev = (np.asarray(d.get_velocities())[0].astype(float) - v) / dt
101 tr.append(((k + 1) * dt, p[1] - (WY + OFF) - DH / 2, v[1])) # gap = bottom apex to floor
102 vpk = max(vpk, -v[1])
103 if tr[-1][1] < 0.5 * DH:
104 break # sub-cell gap: lubrication unresolved, dem would take over
105 if vpk > 0.3 * USTAR and -v[1] < 0.45 * vpk:
106 break # well past the peak: the approach to rest is asymptotic
107 tr = np.array(tr)
108 return dict(t=tr[:, 0], gap=tr[:, 1] / DH, v=tr[:, 2] / USTAR, case=case, DH=DH,
109 ratio_exp=ratio_exp, Re=Re, u_inf=u_inf, wall=time.time() - t0,
110 nstep=len(tr), dt=dt)
111
112
113
114def run_unbounded(case, DH, N=96, advect=True, nstep=4000):
115 rho_f, mu_si, u_inf, ratio_exp, Re = CASES[case]
116 NU = USTAR * DH / Re
117 GSTAR = G_SI * D_SI / u_inf ** 2 * USTAR ** 2 / DH
118 RATIO = RHO_P_SI / rho_f
119 tau = RATIO * DH * Re / (18 * USTAR)
120 dt = min(max(tau / 12, 1.0), 0.35 / USTAR / 4)
121 b = geom.SceneBuilder()
122 sph = b.add_leaf("sphere", [DH / 2])
123 ni, nr, _, _ = b.encode()
124 x0 = 0.5 * N + 0.3
125 ii = np.zeros((1, KI_I), dtype=np.int32); ir = np.zeros((1, KI_R))
126 ii[0] = (sph, -1); ir[0, 0:3] = (x0, x0, x0); ir[0, 6] = 1.0; ir[0, 7] = 1.0
127 s = sdflow.Solver(N, N, N)
128 s.set_rho(1.0); s.set_mu(NU); s.set_dt(dt); s.set_advection(advect)
129 s.set_velocity_solver_params(60); s.set_pressure_solver_params(20)
130 s.set_pressure_multigrid(True, levels=4)
131 s.set_scene(np.asarray(ni, np.int32), np.asarray(nr, float), ii.ravel(), ir.ravel(),
132 periodic=True)
133 s.set_solid_from_scene(True)
134 _Fg = (RATIO - 1.0) * (np.pi / 6) * DH ** 3 * GSTAR
135 s.set_body_force(0.0, _Fg / (N ** 3), 0.0) # back-pressure: zero net force on the suspension
136 m = RATIO * (np.pi / 6) * DH ** 3
137 Vp = (np.pi / 6) * DH ** 3
138 ma = 2.0 * Vp
139 d = pdem.Simulation(8)
140 d.set_gravity(0.0, 0.0, 0.0); d.set_sphere_shape(1.0)
141 d.set_positions(np.array([[x0, x0, x0]], dtype=np.float32))
142 d.set_inv_mass(np.array([1.0 / (m + ma)], dtype=np.float32))
143 d.set_inv_inertia(np.array([[0, 0, 0]], dtype=np.float32))
144 Fg = (RATIO - 1.0) * Vp * GSTAR
145 a_prev = np.zeros(3); vs = []
146 for k in range(nstep):
147 p = np.asarray(d.get_positions())[0].astype(float)
148 v = np.asarray(d.get_velocities())[0].astype(float)
149 s.set_instance_transform(0, p.tolist())
150 s.set_instance_motion(0, lin_vel=v.tolist())
151 s.rebuild_geometry(); s.step()
152 F = np.asarray(s.hydro_force_torque_reaction())[0][0].astype(float)
153 F[1] -= Fg
154 F += ma * a_prev
155 d.set_external_forces(np.array([F], dtype=np.float32))
156 for _ in range(10):
157 d.step(dt / 10)
158 a_prev = (np.asarray(d.get_velocities())[0].astype(float) - v) / dt
159 vs.append(-v[1] / USTAR)
160 if k > 400 and abs(vs[-1] - vs[-200]) < 5e-4 * abs(vs[-1]):
161 break
162 c = (np.pi / 6) * DH ** 3 / N ** 3
163 hasi = 1.0 - 1.7601 * c ** (1 / 3)
164 print("%s unbounded N=%d: plateau=%.4f Hasimoto-corrected expectation=%.4f steps=%d"
165 % (case, N, vs[-1], hasi, len(vs)))
166 return vs
167
168vs = run_unbounded("E1", 8)
run_unbounded(case, DH, N=96, advect=True, nstep=4000)