flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_mirror_symmetry.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""Wall-normal mirror symmetry of the MAC step, and the placement of the variable viscosity.
3
4A bubble column between two walls (y = 0 and y = NY, x/z periodic) collected its bubbles at the
5LOW wall -- and so did the same column with every bubble mirrored to the other side. The cause
6was the variable-viscosity momentum operator: it averaged mu across the faces of CELL i, but the
7staggered unknown u_c(i) lives on the -c face of cell i, so every component saw mu shifted half a
8cell towards +c -- first-order accurate, and in a walled direction a mirror asymmetry (VarFaceProps,
9src/face_props.hpp). Two checks:
10
11 1. mirror pair -- solver A and its y-mirror B (cell fields reflected, v negated onto the mirrored
12 face), same fixed dt, A(y) against mirror(B)(y) after a few steps. Round-off only (~1e-14);
13 the shifted viscosity gave 1e-3..1e-2. Variants: single phase with a transported scalar
14 driving rho, the body force and (or not) mu, arithmetic and harmonic viscosity means; the
15 structured VoF with CSF; the block container with its per-marker CSF. The blob touches the
16 LOW wall (the mirror touches the high one) and straddles the periodic x seam.
17 2. placement -- steady variable-viscosity Stokes, u = A (sin kx cos ky, -cos kx sin ky, 0),
18 mu = 1 + sin(kx + 0.3) cos(ky + 0.7) / 2, forced by -div(mu grad u_c): second order in h
19 (the shifted viscosity was first order: 1.39 then 1.05).
20
21Run: OMP_NUM_THREADS=4 PYTHONPATH=<build> python tests/python/test_mirror_symmetry.py
22Exit 0 pass, 1 fail, 77 skipped (no module).
23"""
24import sys
25
26import numpy as np
27
28try:
29 import peclet.flow as pf
30except ImportError:
31 print("SKIP: peclet.flow not importable")
32 sys.exit(77)
33
34NX, NY, NZ = 24, 20, 12
35DT, NSTEPS = 0.05, 3
36R = 3.6
37
38
39def mir_c(f):
40 """Mirror of a cell-centred (and x/z-face) field: row j <-> NY-1-j."""
41 return np.asfortranarray(f[:, ::-1, :])
42
43
44def mir_v(v):
45 """Mirror of the y-face field: get_v's index j is the face j - 1/2 (index 0 the low wall face),
46 so the face j maps to NY - j and changes sign."""
47 out = np.zeros_like(v)
48 out[:, 1:, :] = -v[:, :0:-1, :]
49 return np.asfortranarray(out)
50
51
52def sphere(c, r, sub=6):
53 g = (np.arange(sub) + 0.5) / sub
54 frac = np.zeros((NX, NY, NZ))
55 for gx in g:
56 for gy in g:
57 for gz in g:
58 X, Y, Z = np.meshgrid(np.arange(NX) + gx, np.arange(NY) + gy, np.arange(NZ) + gz,
59 indexing="ij")
60 dx = X - c[0]
61 dx -= NX * np.round(dx / NX) # periodic in x: the blob straddles the seam
62 frac += (dx * dx + (Y - c[1]) ** 2 + (Z - c[2]) ** 2) < r * r
63 return np.asfortranarray(frac / sub ** 3)
64
65
66def build(mode, mirror, variable_mu=True, harmonic=False):
67 cy = 2.0 if mode == "scalar" else 3.0 # scalar: straddles the wall; markers: 0.6 cell inside
68 c = (0.4, NY - cy if mirror else cy, 6.1)
69 s = pf.Solver(NX, NY, NZ)
70 s.set_rho(1.0)
71 s.set_mu(0.05)
72 s.set_domain_bc("-y", "wall", (0, 0, 0))
73 s.set_domain_bc("+y", "wall", (0, 0, 0))
74 s.set_pressure_geometry(np.full((NX, NY, NZ), 10.0, order="F"))
75 if mode == "scalar":
76 s.add_scalar("T", 0.0, "koren")
77 s.set_field("T", sphere(c, R))
78 s.set_property_model("rho", "linear", "T", [1.0, -0.5])
79 if variable_mu:
80 s.set_property_model("mu", "linear", "T", [0.05, -0.04])
81 s.set_property_model("force_x", "linear", "T", [0.0, -0.02])
82 else:
83 s.enable_vof()
84 s.set_vof(sphere(c, R) if mode == "vof" else np.zeros((NX, NY, NZ), order="F"))
85 s.set_property_model("rho", "linear", "C", [1.0, -0.9])
86 s.set_property_model("mu", "linear", "C", [0.05, -0.04])
87 s.set_surface_tension(0.02)
88 if mode == "blocks":
89 s.enable_vof_blocks([(c[0], c[1], c[2], R)])
90 s.enable_vof_block_csf()
91 s.set_property_model("force_x", "linear", "C", [0.0, -0.02])
92 if harmonic:
93 s.diagnostics.set_property_mode("variable", True)
94 s.set_pressure_pcg(True, 800, 1e-12)
95 s.set_superficial_velocity(True, "x", 0.0)
96 rng = np.random.default_rng(1)
97 u, v, w = (np.asfortranarray(0.05 * rng.standard_normal((NX, NY, NZ))) for _ in range(3))
98 v[:, 0, :] = 0.0 # the low wall face
99 if mirror:
100 u, v, w = mir_c(u), mir_v(v), mir_c(w)
101 for k, a in enumerate((u, v, w)):
102 s.set_velocity(k, a)
103 return s
104
105
106def mirror_asymmetry(mode, **kw):
107 A, B = build(mode, False, **kw), build(mode, True, **kw)
108 for _ in range(NSTEPS):
109 for s in (A, B):
110 s.set_dt(DT)
111 s.step()
112
113 def rel(a, b):
114 return float(np.abs(a - b).max() / np.abs(a).max())
115
116 vel = max(rel(A.get_u(), mir_c(B.get_u())), rel(A.get_v(), mir_v(B.get_v())),
117 rel(A.get_w(), mir_c(B.get_w())))
118 return vel, rel(A.get_p(), mir_c(B.get_p()))
119
120
121def steady_varmu_error(N, A=1e-3, nz=4):
122 k = 2.0 * np.pi / N
123 xc = np.arange(N) + 0.5
124 X, Y = np.meshgrid(xc, xc, indexing="ij")
125 mu = 1 + 0.5 * np.sin(k * X + 0.3) * np.cos(k * Y + 0.7)
126 mux = 0.5 * k * np.cos(k * X + 0.3) * np.cos(k * Y + 0.7)
127 muy = -0.5 * k * np.sin(k * X + 0.3) * np.sin(k * Y + 0.7)
128 ux, uy = A * np.sin(k * X) * np.cos(k * Y), -A * np.cos(k * X) * np.sin(k * Y)
129 fx = -(-2 * k * k * mu * ux + A * k * (mux * np.cos(k * X) * np.cos(k * Y)
130 - muy * np.sin(k * X) * np.sin(k * Y)))
131 fy = -(-2 * k * k * mu * uy + A * k * (mux * np.sin(k * X) * np.sin(k * Y)
132 - muy * np.cos(k * X) * np.cos(k * Y)))
133 rep = lambda a: np.asfortranarray(np.repeat(a[:, :, None], nz, axis=2)) # noqa: E731
134 s = pf.Solver(N, N, nz)
135 s.set_rho(1.0)
136 s.set_mu(1.0)
137 s.set_dt(2.0)
138 s.set_advection(False)
139 s.set_pressure_pcg(True, 400, 1e-13)
140 s.diagnostics.set_velocity_solver_params(400, 1e-12, 2)
141 s.set_pressure_geometry(np.asfortranarray(np.full((N, N, nz), 10.0)))
142 s.enable_cell_force()
143 s.diagnostics.set_property_mode("variable", False)
144 # plain incremental pressure: its fixed point is the steady Stokes solution exactly (the
145 # rotational correction with a variable mu is not -- see set_variable_rotational)
146 s.diagnostics.set_variable_rotational("off", 1.0)
147 s.set_field("mu", rep(mu))
148 for nm, a in (("force_x", rep(fx)), ("force_y", rep(fy)), ("force_z", rep(0 * fx))):
149 s.set_field(nm, a)
150 s.diagnostics.exchange_field(nm)
151 prev = None
152 for _ in range(20000):
153 s.step()
154 u = np.array(s.get_u())
155 if prev is not None and np.max(np.abs(u - prev)) <= 1e-13 * np.max(np.abs(u)):
156 break
157 prev = u
158 u, v = np.array(s.get_u())[:, :, 0], np.array(s.get_v())[:, :, 0]
159 i = np.arange(N, dtype=np.float64)
160 xu, yc = np.meshgrid(i, i + 0.5, indexing="ij") # u on the -x face: (x_i, y_c)
161 xc2, yv = np.meshgrid(i + 0.5, i, indexing="ij") # v on the -y face: (x_c, y_j)
162 ue = A * np.sin(k * xu) * np.cos(k * yc)
163 ve = -A * np.cos(k * xc2) * np.sin(k * yv)
164 return max(np.abs(u - ue).max(), np.abs(v - ve).max()) / A
165
166
167def main():
168 ok = True
169 cases = [("scalar, constant mu", "scalar", dict(variable_mu=False)),
170 ("scalar, mu(T) arithmetic", "scalar", {}),
171 ("scalar, mu(T) harmonic", "scalar", dict(harmonic=True)),
172 ("structured VoF + CSF", "vof", {}),
173 ("block VoF + block CSF", "blocks", {})]
174 for name, mode, kw in cases:
175 vel, p = mirror_asymmetry(mode, **kw)
176 good = vel < 1e-10 and p < 1e-8 # p carries the 1e-12 PCG tolerance, not round-off
177 ok &= good
178 print(f"mirror {name:28s} velocity {vel:.2e} pressure {p:.2e} "
179 f"{'ok' if good else 'FAIL'}")
180 e16, e32 = steady_varmu_error(16), steady_varmu_error(32)
181 order = np.log2(e16 / e32)
182 good = order > 1.8
183 ok &= good
184 print(f"placement variable-mu steady Stokes error N=16 {e16:.3e} N=32 {e32:.3e} "
185 f"order {order:.2f} (> 1.8) {'ok' if good else 'FAIL'}")
186 print("PASS" if ok else "FAIL")
187 return 0 if ok else 1
188
189
190if __name__ == "__main__":
191 sys.exit(main())
steady_varmu_error(N, A=1e-3, nz=4)