flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_cell_force_placement.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""Placement of the per-cell body force and drag coefficient in the momentum equation.
3
4The per-cell fields ``force_x/y/z`` (enable_cell_force; the CFD-DEM drag feedback, Boussinesq) and
5``drag_beta`` (enable_drag) live at CELL CENTRES. The velocity unknown they act on does not always:
6
7 * staggered (MAC): u_c(i) is the face between cells i - e_c and i, so the force it feels is the
8 second-order face interpolant 0.5*(f(i - e_c) + f(i)) -- and the implicit drag coefficient on
9 its diagonal is the same face mean of beta, so the drag target beta*u_p and the diagonal agree;
10 * collocated: u_c(i) is the cell centre, so it feels the cell value f(i).
11
12The placement follows the GRID, never the density model (the constant- and variable-density RHS
13builders used to disagree). Three checks, each on both density models:
14
15 1. placement -- a Taylor-Green force f = A (sin kx cos ky, -cos kx sin ky, 0) sampled at the cell
16 centres. Its face mean on the MAC grid (and its cell values under the collocated ABC face
17 interpolation) is EXACTLY discretely solenoidal, so the steady Stokes state is p = 0 and
18 u = F / (mu * lam_h), lam_h = (8/h^2) sin^2(kh/2) -- the force actually placed is recovered
19 pointwise as mu * lam_h * u and compared with the two candidates (cell value, face mean);
20 2. convergence -- the same steady state against the continuum u = f(x_u) / (2 mu k^2) at the
21 velocity's own location, N = 16, 32, 64: second order (a half-cell shift is first order);
22 3. drag consistency -- beta = 2 + sin(kx) cos(ky), force_x = beta * U0 (a uniform particle velocity):
23 the exact steady state is u = U0 everywhere iff the RHS target and the diagonal carry the SAME
24 beta at the velocity's location.
25
26Run: OMP_NUM_THREADS=4 PYTHONPATH=<build> python tests/python/test_cell_force_placement.py
27Exit 0 pass, 1 fail, 77 skipped (no module).
28"""
29import sys
30
31import numpy as np
32
33try:
34 import peclet.flow as pf
35except ImportError:
36 print("SKIP: peclet.flow not importable")
37 sys.exit(77)
38
39NZ = 4
40MU = 1.0
41
42
43def make(cls, N, variable, dt, drag=False):
44 s = cls(N, N, NZ)
45 s.set_rho(1.0)
46 s.set_mu(MU)
47 s.set_dt(dt)
48 s.set_advection(False)
49 s.set_pressure_pcg(True, 400, 1e-13)
50 s.diagnostics.set_velocity_solver_params(400, 1e-12, 2)
51 s.set_pressure_geometry(np.asfortranarray(np.full((N, N, NZ), 10.0)))
52 if variable:
53 s.diagnostics.set_density_mode("variable") # uniform rho field: reduces to constant rho
54 s.enable_cell_force()
55 if drag:
56 s.enable_drag()
57 return s
58
59
60def set_cell(s, name, a):
61 s.set_field(name, np.asfortranarray(np.ascontiguousarray(a, dtype=np.float64)))
62 s.diagnostics.exchange_field(name)
63
64
65def run_steady(s, tol=1e-13, max_steps=2000):
66 prev = None
67 for _ in range(max_steps):
68 s.step()
69 u = np.array(s.get_u())
70 if prev is not None and np.max(np.abs(u - prev)) <= tol * max(np.max(np.abs(u)), 1e-300):
71 break
72 prev = u
73 return np.array(s.get_u()), np.array(s.get_v())
74
75
76def tg_force(N, A):
77 k = 2.0 * np.pi / N
78 xc = np.arange(N) + 0.5 # cell centres (h = 1)
79 X, Y = np.meshgrid(xc, xc, indexing="ij")
80 fx = A * np.sin(k * X) * np.cos(k * Y)
81 fy = -A * np.cos(k * X) * np.sin(k * Y)
82 rep = lambda a: np.repeat(a[:, :, None], NZ, axis=2) # noqa: E731
83 return k, rep(fx), rep(fy)
84
85
86def tg_case(cls, N, variable, A=1e-3):
87 """Steady Stokes under the Taylor-Green force. Returns (u, v, k, fx, fy)."""
88 k, fx, fy = tg_force(N, A)
89 lam = 2.0 * k * k
90 s = make(cls, N, variable, dt=10.0 / (MU * lam))
91 set_cell(s, "force_x", fx)
92 set_cell(s, "force_y", fy)
93 set_cell(s, "force_z", np.zeros_like(fx))
94 u, v = run_steady(s)
95 return u, v, k, fx, fy
96
97
98def placement(cls, variable, N=16):
99 """Relative misfit of the recovered placed force against (cell value, face mean)."""
100 u, v, k, fx, fy = tg_case(cls, N, variable)
101 lam_h = 8.0 * np.sin(0.5 * k) ** 2
102 rec_x, rec_y = MU * lam_h * u, MU * lam_h * v
103 face_x = 0.5 * (fx + np.roll(fx, 1, axis=0))
104 face_y = 0.5 * (fy + np.roll(fy, 1, axis=1))
105 scale = np.max(np.abs(fx))
106
107 def mis(cx, cy):
108 return max(np.max(np.abs(rec_x - cx)), np.max(np.abs(rec_y - cy))) / scale
109
110 return mis(fx, fy), mis(face_x, face_y)
111
112
113def tg_error(cls, N, variable, A=1e-3):
114 """Max error of the steady velocity vs the continuum solution at the velocity's location."""
115 u, v, k, _, _ = tg_case(cls, N, variable, A)
116 i = np.arange(N, dtype=np.float64)
117 off = 0.0 if cls is pf.Solver else 0.5 # MAC face x = i on the staggered grid; centre i + 1/2
118 xu, yc = np.meshgrid(i + off, i + 0.5, indexing="ij") # u: (x_u, y_c)
119 xc, yv = np.meshgrid(i + 0.5, i + off, indexing="ij") # v: (x_c, y_v)
120 amp = A / (MU * 2.0 * k * k)
121 ue = amp * np.sin(k * xu) * np.cos(k * yc)
122 ve = -amp * np.cos(k * xc) * np.sin(k * yv)
123 return max(np.max(np.abs(u[:, :, 0] - ue)), np.max(np.abs(v[:, :, 0] - ve))) / amp
124
125
126def drag_case(cls, variable, N=16, U0=0.1):
127 k = 2.0 * np.pi / N
128 xc = np.arange(N) + 0.5
129 X, Y = np.meshgrid(xc, xc, indexing="ij")
130 # varies along BOTH axes: a mismatch varying along x alone is a pure gradient the projection
131 # absorbs into p, and would pass even with the target and the diagonal disagreeing
132 beta = np.repeat((2.0 + np.sin(k * X) * np.cos(k * Y))[:, :, None], NZ, 2)
133 s = make(cls, N, variable, dt=1.0, drag=True)
134 set_cell(s, "drag_beta", beta)
135 set_cell(s, "force_x", beta * U0)
136 set_cell(s, "force_y", np.zeros_like(beta))
137 set_cell(s, "force_z", np.zeros_like(beta))
138 u, v = run_steady(s)
139 return max(np.max(np.abs(u - U0)), np.max(np.abs(v))) / U0
140
141
142# SolverColocated + variable density is rung V8 (doc/collocated_varrho_forces.md): the per-cell force
143# enters its implicit predictor at the cell value (times the reconstruction's weight sum, 1 in the
144# interior), exactly like the constant-density collocated path. Gated like every other combination
145# since the WO-T face-acceleration form (which placed 1/4(1,2,1) f and diverged here) was retired.
146SKIP = set()
147
148
149def cases():
150 for gname, cls in (("staggered", pf.Solver), ("collocated", pf.SolverColocated)):
151 for mname, var in (("const-rho", False), ("var-rho", True)):
152 if (gname, var) in SKIP:
153 print(f" {gname:10s} {mname:9s} not gated (rung V8 face-acceleration path)")
154 continue
155 yield gname, cls, mname, var
156
157
158def main():
159 ok = True
160
161 print("1. placement: misfit of the recovered force vs (cell value, face mean), N = 16")
162 for gname, cls, mname, var in cases():
163 m_cell, m_face = placement(cls, var)
164 want = m_face if cls is pf.Solver else m_cell
165 good = want < 1e-6
166 ok &= good
167 print(f" {gname:10s} {mname:9s} cell {m_cell:.3e} face {m_face:.3e} "
168 f"{'ok' if good else 'FAIL'}")
169
170 print("2. convergence of the steady Taylor-Green velocity (max rel. error, observed order)")
171 for gname, cls, mname, var in cases():
172 e = [tg_error(cls, N, var) for N in (16, 32, 64)]
173 p = [np.log2(e[j] / e[j + 1]) for j in range(2)]
174 good = p[-1] > 1.8
175 ok &= good
176 print(f" {gname:10s} {mname:9s} " + " ".join(f"{x:.3e}" for x in e)
177 + f" order {p[0]:.2f} {p[1]:.2f} {'ok' if good else 'FAIL'}")
178
179 print("3. drag consistency: max |u - U0| / U0 with beta(x, y) and force = beta * U0")
180 for gname, cls, mname, var in cases():
181 try:
182 d = drag_case(cls, var)
183 except RuntimeError as exc: # a configuration the grid refuses is reported, not failed
184 print(f" {gname:10s} {mname:9s} refused: {exc}")
185 continue
186 good = d < 1e-8
187 ok &= good
188 print(f" {gname:10s} {mname:9s} {d:.3e} {'ok' if good else 'FAIL'}")
189
190 print("PASS" if ok else "FAIL")
191 return 0 if ok else 1
192
193
195 assert main() == 0
196
197
198if __name__ == "__main__":
199 sys.exit(main())
make(cls, N, variable, dt, drag=False)
run_steady(s, tol=1e-13, max_steps=2000)
drag_case(cls, variable, N=16, U0=0.1)
tg_error(cls, N, variable, A=1e-3)
tg_case(cls, N, variable, A=1e-3)