Kelvin–Helmholtz instability of the tanh mixing layer (Michalke 1964), inviscid and viscous

Family: shear · Script: examples/shear/kelvin_helmholtz.py

Obtained

max ω_i = 0.189702 at α = 0.44492 (L = 20, N = 384); viscous → inviscid in O(1/Re)

Reference

Michalke 1964, JFM 19, Table 1: 0.1897 at α = 0.4446

Base flow U(y) = tanh(y) (lengths in units of the half-thickness, velocities in units of the
half velocity difference). A normal mode v(y) exp(i alpha (x - c t)) of the cross-stream velocity
obeys the Rayleigh equation (inviscid) or the Orr–Sommerfeld equation (viscous):

    (U - c)(v'' - alpha^2 v) - U'' v = (1 / (i alpha Re)) (v'''' - 2 alpha^2 v'' + alpha^4 v)

with the temporal growth rate omega_i = alpha Im(c). Michalke (1964, J. Fluid Mech. 19, 543-556,
Table 1 / Fig. 3) finds the maximum omega_i = 0.1897 at alpha = 0.4446 (c_r = 0 by symmetry).

The unbounded layer is truncated to y in [-L, L] with v = 0 (and v' = 0 when viscous) at y = +-L.
The eigenfunction decays like exp(-alpha |y|), so the truncation error is ~exp(-2 alpha L); both
L and N are explicit and their convergence is printed.

Run:  python examples/shear/kelvin_helmholtz.py

Run it: python examples/shear/kelvin_helmholtz.py (in the conatus environment).

 1"""Kelvin–Helmholtz instability of the tanh mixing layer (Michalke 1964), inviscid and viscous.
 2
 3Base flow U(y) = tanh(y) (lengths in units of the half-thickness, velocities in units of the
 4half velocity difference). A normal mode v(y) exp(i alpha (x - c t)) of the cross-stream velocity
 5obeys the Rayleigh equation (inviscid) or the Orr–Sommerfeld equation (viscous):
 6
 7    (U - c)(v'' - alpha^2 v) - U'' v = (1 / (i alpha Re)) (v'''' - 2 alpha^2 v'' + alpha^4 v)
 8
 9with the temporal growth rate omega_i = alpha Im(c). Michalke (1964, J. Fluid Mech. 19, 543-556,
10Table 1 / Fig. 3) finds the maximum omega_i = 0.1897 at alpha = 0.4446 (c_r = 0 by symmetry).
11
12The unbounded layer is truncated to y in [-L, L] with v = 0 (and v' = 0 when viscous) at y = +-L.
13The eigenfunction decays like exp(-alpha |y|), so the truncation error is ~exp(-2 alpha L); both
14L and N are explicit and their convergence is printed.
15
16Run:  python examples/shear/kelvin_helmholtz.py
17"""
18import numpy as np
19import scipy.optimize as so
20
21from conatus.expert import EVP, Parameter, ParametricEVP, channel, diff, interp, verify
22
23MICHALKE = {"alpha": 0.4446, "omega_i": 0.1897}      # Michalke (1964), Table 1
24
25
26def mixing_layer(N: int, L: float, viscous: bool):
27    """Rayleigh (viscous=False) or Orr–Sommerfeld (viscous=True) pencil in alpha (and Re)."""
28    ch = channel(N, base=None, interval=(-L, L))                # Chebyshev in y on [-L, L]
29    y, v, U = ch.y, ch.field("v"), ch.field("U")
30    c, alpha, Re = Parameter("c", dtype=complex), Parameter("alpha", 0.44), Parameter("Re", 1e3)
31
32    def L2(f):                                                  # d^2/dy^2 - alpha^2
33        return diff(diff(f, y), y) - alpha ** 2 * f
34
35    Upp = diff(diff(U, y), y)                                   # U'' from the NCC, not by hand
36    # (U - c) L2 v - U'' v [- L2 L2 v / (i alpha Re)] = 0; the eigenvalue c multiplies L2 v
37    rayleigh = U * L2(v) - c * L2(v) - Upp * v
38    pb = EVP([v], eigenvalue=c, name="Orr-Sommerfeld (tanh)" if viscous else "Rayleigh (tanh)")
39    if viscous:
40        pb.add_equation((rayleigh - L2(L2(v)) / (1j * alpha * Re), 0))
41        pb.add_bc(interp(v, y, -L), interp(v, y, L), interp(diff(v, y), y, -L), interp(diff(v, y), y, L))
42        params = [alpha, Re]
43    else:
44        pb.add_equation((rayleigh, 0))
45        pb.add_bc(interp(v, y, -L), interp(v, y, L))
46        params = [alpha]
47    # U'' is computed from the sampled tanh: on [-15, 15] at N = 192 its Chebyshev tail is 1.1e-5
48    # (the interpolation error of tanh, 1e-10, amplified by d^2/dy^2; largest at |y| = L where
49    # U'' ~ 0 and the mode ~ exp(-alpha L)); accepted explicitly up to 2e-5. The gallery
50    # test checks the growth rate against Michalke, and __main__ its convergence in N and L.
51    return ParametricEVP(pb, params, ncc={U: np.tanh}, ncc_tail_tol=2e-5), pb
52
53
54def most_unstable(P: ParametricEVP, **fixed):
55    """max over alpha of omega_i = alpha Im(c): full QZ once, then the KH mode is tracked in alpha."""
56    # KH mode = largest Im(c) among |c| < 2 (|U| <= 1 bounds physical phase speeds, Howard's
57    # semicircle); the viscous tau-spurious modes have |c| ~ 1e3 and are excluded (count logged)
58    seed = P.least_stable(growth="imag", select=lambda s: abs(s) < 2, alpha=0.44, **fixed)
59
60    def minus_omega_i(a):
61        m = P.track(seed.sigma, seed.x, left=False, alpha=a, **fixed)
62        return -a * m.sigma.imag
63
64    opt = so.minimize_scalar(minus_omega_i, bounds=(0.40, 0.49), method="bounded",
65                             options={"xatol": 1e-7})
66    mode = P.track(seed.sigma, seed.x, alpha=float(opt.x), **fixed)
67    return float(opt.x), mode
68
69
70if __name__ == "__main__":
71    print("Inviscid (Rayleigh), convergence in N and L:")
72    for L, N in [(10, 192), (15, 192), (15, 256), (20, 384)]:
73        P, pb = mixing_layer(N, L, viscous=False)
74        a, m = most_unstable(P)
75        print(f"  L={L:2d} N={N:3d}: alpha_max = {a:.6f}, omega_i = {a * m.sigma.imag:.8f}, "
76              f"c = {m.sigma:.2e}, residual {m.residual:.1e}")
77    print(f"  Michalke (1964): alpha_max = {MICHALKE['alpha']}, omega_i = {MICHALKE['omega_i']}")
78    # Michalke's 4 printed digits, at the largest L and N (L = 20, N = 384)
79    assert abs(a * m.sigma.imag - MICHALKE["omega_i"]) < 5e-5 and abs(a - MICHALKE["alpha"]) < 5e-4, (a, m.sigma)
80    print(pb.explain())
81    print(verify(P, m).explain())
82    print("Viscous (Orr–Sommerfeld), L=15, N=256: omega_i -> inviscid value as Re -> infinity")
83    for Re in (1e2, 1e3, 1e4):
84        P, _ = mixing_layer(256, 15, viscous=True)
85        a, m = most_unstable(P, Re=Re)
86        print(f"  Re = {Re:7.0f}: alpha_max = {a:.6f}, omega_i = {a * m.sigma.imag:.8f}, residual {m.residual:.1e}")