flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
fresh_cell_gate.py
Go to the documentation of this file.
1"""FRESH-CELL GATE: does a body that physically MOVES through the grid spike the drag?
2
3A sphere oscillating at vanishing amplitude has an exact answer (Stokes 1851) and, in this solver,
4a second reference that is exact by construction: the LINEARISED run, where the boundary condition
5oscillates but the geometry never moves. The difference between the two is everything the motion
6itself introduces -- which in a sharp-interface IBM is the textbook source of spurious force
7oscillations (Lee, Kim, Choi & Yang 2011; Seo & Mittal 2011): cells emerging from the body inherit
8a solid state rather than a fluid one, and the stencil changes abruptly as the interface crosses a
9face.
10
11TWO METRICS, deliberately:
12 * RMS |F^{n+1} - 2F^n + F^{n-1}| / |F1| -- the Seo & Mittal (2011) Eq. (12) second-difference
13 measure, the one the literature actually uses (Martins et al. 2017 Eq. 54 adopts it verbatim).
14 Comparable in FORM to published numbers; the absolute value is not, since we normalise by the
15 fundamental amplitude rather than their C_PD.
16 * the residual after fitting 5 harmonics of the driving frequency -- flat spectral gain, no
17 explicit dt dependence of its own, and physically interpretable as "the part of the force that
18 is not at the driving frequency". Kept because the 2-delta operator's own (omega*dt)^2 rolloff
19 makes its temporal exponent metric-dependent rather than physical.
20
21THREE LEGS: refine dt at fixed grid (the artefact should NOT improve -- it is spatial); refine h at
22fixed physics; sweep the amplitude (hence the number of cell crossings per cycle). Each run is
23repeated with set_fresh_cell_seed off and on.
24"""
25
26import os, time, numpy as np
27from peclet import flow as sdflow
28RHO, MU = 1.0, 0.1; NU = MU/RHO
29DR, NPER, FITP, NH = 1.0, 3, 2, 5
30KN_R, KI_I, KI_R = 16, 2, 17
31
32def run(N, R, SPP, AR, seed, moving=True):
33 delta=DR*R; om=2*NU/delta**2; A=AR*R; U0=A*om
34 T=2*np.pi/om; dt=T/SPP; x0=0.5*N
35 ni=np.array([1,-1,-1],dtype=np.int32); nr=np.zeros(KN_R); nr[0]=R; nr[14]=1.0; nr[15]=1.0
36 ii=np.zeros((1,KI_I),dtype=np.int32); ii[0]=(0,-1)
37 ir=np.zeros((1,KI_R)); ir[0,0:3]=(x0,0.5*N,0.5*N); ir[0,6]=1.0; ir[0,7]=1.0
38 s=sdflow.Solver(N,N,N); s.set_rho(RHO); s.set_mu(MU); s.set_dt(dt); s.set_advection(False)
39 s.set_velocity_solver_params(100); s.set_pressure_solver_params(25)
40 s.set_pressure_multigrid(True, levels=4)
41 s.set_scene(ni,nr,ii.ravel(),ir.ravel(),periodic=True)
42 s.set_fresh_cell_seed(bool(seed))
43 s.set_instance_motion(0, lin_vel=[U0,0.0,0.0]); s.set_solid_from_scene(True)
44 ts,Fs=[],[]; t0=time.time()
45 for it in range(SPP*NPER):
46 t=(it+1)*dt; u=U0*np.cos(om*t)
47 if moving:
48 s.set_instance_transform(0,[x0+A*np.sin(om*t),0.5*N,0.5*N])
49 s.set_instance_motion(0, lin_vel=[u,0.0,0.0]); s.rebuild_geometry()
50 else:
51 s.set_instance_motion(0, lin_vel=[u,0.0,0.0]); s.refresh_wall_velocity()
52 s.step(); ts.append(t); Fs.append(float(np.asarray(s.hydro_force_torque_reaction())[0][0][0]))
53 ts=np.array(ts); Fs=np.array(Fs); m=ts>=(NPER-FITP)*T
54 t,F=ts[m],Fs[m]
55 cols=[np.ones_like(t)]
56 for k in range(1,NH+1): cols += [np.cos(k*om*t), np.sin(k*om*t)]
57 Ab=np.column_stack(cols); c,*_=np.linalg.lstsq(Ab,F,rcond=None)
58 res=F-Ab@c; F1=np.hypot(c[1],c[2])
59 # Seo & Mittal (2011) Eq. 12 metric: RMS of |F^{n+1} - 2F^n + F^{n-1}|, normalised the same
60 # way the force is (here by the fundamental amplitude, so it is dimensionless and comparable
61 # ACROSS OUR OWN runs; the absolute value is not comparable to their C_PD normalisation).
62 d2 = np.abs(F[2:] - 2*F[1:-1] + F[:-2])
63 return dict(F1=F1, p2p=(res.max()-res.min())/F1, rms=res.std()/F1,
64 d2=float(np.sqrt((d2**2).mean()))/F1, wall=time.time()-t0, A=A, U0=U0)
65
66def line(tag, r, ref=None):
67 e = "" if ref is None else " dF1 %+6.2f%%" % (100*(r['F1']/ref-1))
68 print(" %-26s |F1|=%.5e p2p=%.3e rms=%.3e RMS(F2d)/|F1|=%.3e (%3.0fs)%s"
69 % (tag, r['F1'], r['p2p'], r['rms'], r['d2'], r['wall'], e))
70
71which = os.environ.get("LEG", "dt")
72if which == "dt":
73 print("TIME-STEP leg: N=96, R=9.6 h, A/R=0.25 (A=2.40 h) -- grid crossings FIXED")
74 st = run(96, 9.6, 200, 0.25, 0, moving=False)
75 line("static (linearised) ref", st)
76 for spp in (100, 200, 400):
77 for sd in (0, 1):
78 line("SPP=%3d seed=%d" % (spp, sd), run(96, 9.6, spp, 0.25, sd), st['F1'])
79elif which == "h":
80 print("GRID leg: R/L=0.1, delta/R=1, A/R=0.25, SPP=200")
81 for N, R in ((64, 6.4), (96, 9.6), (128, 12.8)):
82 st = run(N, R, 200, 0.25, 0, moving=False)
83 line("N=%3d static ref" % N, st)
84 for sd in (0, 1):
85 line("N=%3d moving seed=%d" % (N, sd), run(N, R, 200, 0.25, sd), st['F1'])
86else:
87 print("AMPLITUDE leg: N=96, R=9.6 h, SPP=200")
88 for AR in (0.05, 0.10, 0.25, 0.50, 1.00):
89 st = run(96, 9.6, 200, AR, 0, moving=False)
90 for sd in (0, 1):
91 line("A/R=%.2f seed=%d" % (AR, sd), run(96, 9.6, 200, AR, sd), st['F1'])
line(tag, r, ref=None)
run(N, R, SPP, AR, seed, moving=True)
static double mean(const std::vector< double > &a)