flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
cutwall_freefall_trace.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):
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 / 2 + 1.0, NY / 2 + 1.0, NX / 2 + 1.0])
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(True)
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
113r = run_case("E1", 8)
114v = -np.asarray(r["v"]); g = np.asarray(r["gap"]); t = np.asarray(r["t"])
115print("nstep=%d dt=%.3f wall=%.0fs peak=%.4f (exp %.3f) final gap=%.3f" % (r["nstep"], r["dt"], r["wall"], v.max(), r["ratio_exp"], g[-1]))
116for i in range(0, len(v), max(1, len(v)//20)):
117 print("k=%5d gap/D=%7.3f -v/u*=%8.4f" % (i, g[i], v[i]))