flow
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
two_layer_couette.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""Two-layer plane Couette — validates variable viscosity (Phase 4) INCLUDING the
3incremental-rotational pressure scheme (large-dt / steady-Stokes capability) at strong contrast.
4
5Bottom wall fixed, top wall moving at U; two viscosity layers (mu1 lower, mu2 upper, 10x jump).
6Analytic steady profile: piecewise linear with interface velocity u_i/U = mu2/(mu1+mu2). The
7harmonic face-viscosity mean reproduces it to ~1e-5; the arithmetic mean shows O(1%) error at the
8jump (why 'harmonic' matters).
9
10Rotational pressure under variable viscosity: the constant-mu Timmermans term -mu*div(u*) is only
11valid for HOMOGENEOUS viscosity (Deteix & Yakoubi, Appl. Math. Lett. 79 (2018) 111-117; arXiv:
121902.05643 for the full shear-rate-projection). The default here ('min') uses the constant
13coefficient chi*mu_min - stable at any contrast by domination, exact fallback to the constant-mu
14scheme for uniform mu, and it KEEPS the incremental predictor: dt=100 converges in ~200 steps below.
15'full' (pointwise mu(i)) is demonstrated to diverge at 10x contrast - mild-contrast use only.
16
17Run: PYTHONPATH=<build> python two_layer_couette.py
18"""
19import numpy as np
20import peclet.flow as F
21
22
23def couette(harmonic, rot=None, chi=1.0, dt=20.0, N=32, steps=4000, tol=1e-9):
24 nz = 4
25 mu1, mu2, U = 1.0, 0.1, 1.0
26 s = F.Solver(N, N, nz)
27 s.set_rho(1.0); s.set_mu(mu1); s.set_dt(dt)
28 s.set_domain_bc(2, 1, 0.0, 0.0, 0.0) # -y fixed wall
29 s.set_domain_bc(3, 2, U, 0.0, 0.0) # +y moving wall
30 s.set_pressure_geometry(np.asfortranarray(np.full((N, N, nz), 10.0)))
31 y = np.arange(N)
32 muy = np.where(y < N // 2, mu1, mu2).astype(np.float64)
33 s.add_field("mu")
34 s.set_field("mu", np.asfortranarray(np.repeat(muy[None, :, None], N, 0).repeat(nz, 2)))
35 s.set_property_mode("variable", harmonic)
36 if rot is not None:
37 s.set_variable_rotational(rot, chi)
38 prev, conv = None, -1
39 for it in range(steps):
40 s.step()
41 if np.isnan(s.get_u()).any():
42 return float("nan"), -(it + 1) # diverged at step it
43 if it % 100 == 99:
44 u = s.get_u(); um = np.abs(u).max()
45 if prev is not None and np.abs(u - prev).max() / (um + 1e-30) < tol:
46 conv = it + 1
47 break
48 prev = u.copy()
49 u = s.get_u()[N // 2, :, 0]
50 ui = U * mu2 / (mu1 + mu2)
51 yc = (y + 0.5) / N
52 exact = np.where(yc < 0.5, ui * yc / 0.5, ui + (U - ui) * (yc - 0.5) / 0.5)
53 return np.max(np.abs(u - exact)) / U, conv
54
55
56if __name__ == "__main__":
57 e, c = couette(True) # default: incremental + rotational('min')
58 print(f"harmonic, incr+rot(min), dt=20 : err {e*100:.4f}% conv@{c}")
59 assert c > 0 and e < 0.005
60
61 e2, c2 = couette(True, dt=100.0, steps=3000) # LARGE dt — the steady-Stokes capability
62 print(f"harmonic, incr+rot(min), dt=100: err {e2*100:.4f}% conv@{c2}")
63 assert c2 > 0 and e2 < 0.005
64
65 ea, ca = couette(False) # arithmetic mean: O(1%) at the jump
66 print(f"arithmetic, incr+rot(min) : err {ea*100:.3f}% conv@{ca} (harmonic matters)")
67 assert ca > 0 and ea > e
68
69 ef, cf = couette(True, rot="full", steps=800) # pointwise rotational at 10x: diverges
70 print(f"harmonic, rot('full') 10x : {'DIVERGED@'+str(-cf) if cf < 0 else 'stable'} "
71 f"(documented mild-contrast-only mode)")
72 print("PASS")
couette(harmonic, rot=None, chi=1.0, dt=20.0, N=32, steps=4000, tol=1e-9)