flow 0.4.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
pressure_h_independence.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""Is the cut-cell pressure multigrid resolution-independent on a wall-bounded domain?
3
4STATUS: NOT YET TRUSTWORTHY -- do not quote numbers from this script for the wall-bounded case.
5It works as intended with periodic boundaries and for modes that vary only in the periodic
6directions, but ANY wall-normal variation in the prescribed field makes the projection stall (no
7residual reduction at all, even at rtol = 0.1), so no h-independence conclusion can be drawn yet.
8See "What is known" at the bottom of this docstring before using it.
9
10A weak-scaling ladder that refines a DNS confounds two things: the discrete operator getting harder
11to solve, and the turbulence itself changing as it becomes better resolved. This isolates the first.
12
13The method is the textbook h-independence test. Fix a CONTINUOUS problem -- one velocity field
14defined as a function of the physical coordinates, whose divergence is therefore a fixed function --
15and sample it on grids of increasing resolution over the SAME physical box. Solve the pressure
16Poisson equation once at each resolution, from a zero initial guess, to a fixed relative tolerance.
17A multigrid whose convergence rate is resolution-independent takes the same number of iterations at
18every resolution; a rising count is the solver, not the physics.
19
20The solve is driven through the ordinary solver path with advection off and mu = 0, so the predictor
21leaves the prescribed field untouched (u* = u) and the projection sees exactly the divergence we
22prescribed.
23
24 python scripts/pressure_h_independence.py --n 32,48,64,96
25 python scripts/pressure_h_independence.py --n 32,48,64,96 --rhs broadband
26 python scripts/pressure_h_independence.py --n 32,48,64 --bottom smoother # contrast
27
28Options worth sweeping: --bottom (auto/smoother) shows whether the coarse level is implicated,
29--levels caps the hierarchy, --periodic-y removes the walls to separate the wall treatment from the
30rest of the operator.
31
32Needs PYTHONPATH pointing at a flow build; single rank, no MPI required.
33
34What is known, all measured at 192x32x64 with mu = 0, advection off, cold start:
35
36 phi = cos(2pi x) no walls or walls 7 iterations, |u| after ~ 6e-11
37 phi = cos(2pi x) cos(2pi z) walls 39 iterations
38 phi = cos(2pi x) cos(2pi y) PERIODIC y 5 iterations
39 phi = cos(2pi x) cos(2pi y) WALLS 500 (the cap; no progress at rtol 0.1)
40 random noise walls 10 iterations
41 Reichardt-like channel profile walls 5 iterations
42
43So the harness itself is sound -- it projects a pure gradient field to 1e-11 -- and the solver is
44plainly fine on real channel states (the production DNS converges in 4-6 iterations and reproduces
45the MKM statistics). The stall is specific to a prescribed field that varies in the wall-normal
46direction, and the most likely explanation is that the test field is not an admissible state rather
47than a solver defect: a curl-free (pure gradient) field cannot satisfy both no-penetration and the
48wall conditions simultaneously, so forcing its wall-normal component to zero at the wall face leaves
49a component the Neumann projection cannot remove.
50
51NEXT STEP (untried): drop the pure-gradient construction, which is what forces that conflict. Use a
52general field that is NOT curl-free -- wall-normal component vanishing at both walls, tangential
53components arbitrary -- and check it converges before trusting any refinement sweep. Alternatively,
54sidestep prescribed fields entirely: take one converged DNS field, interpolate it to several
55resolutions, and solve once at each. That uses only the validated path, at the cost of a
56band-limited field on the finer grids.
57"""
58import argparse
59
60import numpy as np
61
62
63def velocity(nx, ny, nz, kind):
64 """A prescribed field built as the DISCRETE gradient of a potential.
65
66 Taking finite differences of a sampled potential -- rather than sampling an analytic velocity --
67 makes the discrete divergence exactly the discrete Laplacian of that potential, so the singular
68 all-Neumann pressure problem is compatible to machine precision by construction. Sampling an
69 analytic velocity instead leaves a small incompatible component, and the solve then stalls at a
70 residual floor and never reaches its tolerance (it looks like divergence, and is not).
71
72 The potential uses cos(m*pi*y), whose y-derivative vanishes at both walls, so the wall-normal
73 velocity is zero there as the wall boundary condition requires. Wavelengths are fixed fractions
74 of the box, so every resolution samples the SAME continuous problem -- the point of the test.
75 """
76 tp = 2.0 * np.pi
77 x = (np.arange(nx) + 0.5)[:, None, None] / nx
78 y = (np.arange(ny) + 0.5)[None, :, None] / ny
79 z = (np.arange(nz) + 0.5)[None, None, :] / nz
80 if kind == "smooth":
81 modes = [(1, 1, 1)]
82 elif kind == "broadband":
83 # fixed physical wavelengths down to 1/16 of the box, amplitude ~1/k: the rougher spectrum a
84 # turbulent divergence field carries, still one resolution-independent continuous function.
85 modes = [(m, m, m) for m in (1, 2, 4, 8, 16)]
86 else:
87 raise SystemExit(f"unknown --rhs {kind!r} (smooth|broadband)")
88 phi = np.zeros((nx, ny, nz))
89 for kx, my, kz in modes:
90 phi += (1.0 / kx) * np.cos(tp * kx * x) * np.cos(my * np.pi * y) * np.cos(tp * kz * z)
91
92 u = np.roll(phi, -1, axis=0) - phi # periodic x
93 w = np.roll(phi, -1, axis=2) - phi # periodic z
94 v = np.zeros_like(phi)
95 v[:, :-1, :] = phi[:, 1:, :] - phi[:, :-1, :] # interior y faces
96 v[:, -1, :] = 0.0 # +y wall face; -y wall is a ghost, also 0
97 return (np.asfortranarray(u), np.asfortranarray(v), np.asfortranarray(w))
98
99
100def main():
101 ap = argparse.ArgumentParser()
102 ap.add_argument("--n", default="32,48,64", help="wall-normal cell counts (the box is refined)")
103 ap.add_argument("--aspect", default="6,1,2", help="box shape as x,y,z multiples of the y extent")
104 ap.add_argument("--rhs", default="smooth", help="smooth | broadband")
105 ap.add_argument("--levels", type=int, default=10)
106 ap.add_argument("--bottom", default="auto", help="auto | smoother | agglomerated")
107 ap.add_argument("--rtol", type=float, default=1e-10, help="tight, so counts resolve clearly")
108 ap.add_argument("--periodic-y", action="store_true", help="drop the walls (contrast case)")
109 args = ap.parse_args()
110 ax, ay, az = (float(v) for v in args.aspect.split(","))
111
112 from peclet import flow
113
114 print(f"box {ax:g}:{ay:g}:{az:g} rhs={args.rhs} bottom={args.bottom} "
115 f"levels<={args.levels} rtol={args.rtol:g} "
116 f"{'PERIODIC y (no walls)' if args.periodic_y else 'walls on -y/+y'}")
117 print(f"{'grid':>18} {'Mcells':>8} {'h (rel)':>8} {'pressure iterations':>20}")
118 first = None
119 for n in (int(v) for v in args.n.split(",")):
120 nx, ny, nz = int(round(ax / ay * n)), n, int(round(az / ay * n))
121 s = flow.Solver(nx, ny, nz)
122 s.set_rho(1.0)
123 s.set_mu(0.0) # predictor becomes (rho/dt) I => u* = u exactly
124 s.set_dt(1.0)
125 s.set_advection(False)
126 s.set_incremental_pressure(False)
127 s.set_pressure_warmstart(False) # cold start: the count is not an initial-guess artefact
128 s.set_pressure_multigrid(True, args.levels)
129 s.set_pressure_pcg(True, 500, args.rtol)
130 s.set_pressure_bottom(args.bottom)
131 if not args.periodic_y:
132 s.set_domain_bc(2, 1)
133 s.set_domain_bc(3, 1)
134 s.set_pressure_geometry(np.asfortranarray(np.full((nx, ny, nz), 1e30)))
135 s.set_state(*velocity(nx, ny, nz, args.rhs))
136 s.step()
137 it = s.last_pressure_iterations()
138 first = first or n
139 print(f"{f'{nx}x{ny}x{nz}':>18} {nx * ny * nz / 1e6:8.2f} {first / n:8.3f} {it:20.1f}")
140 del s
141 print("\nA resolution-independent multigrid holds the count flat as h shrinks.")
142
143
144if __name__ == "__main__":
145 main()