Stratified Kelvin–Helmholtz: Taylor–Goldstein equation for the tanh/tanh layer (Hazel 1972)¶
Family: shear · Script: examples/shear/taylor_goldstein.py
Obtained |
whole line (tanh map, L = 2/α) + shifted complex path: J_n(1/2) = 1/4 to 2.5e-12 (N = 192) and J_n(0.3) = 0.21 to 3.8e-9 by Newton on c_i(J) = 0, without extrapolation, independent of L and δ; J = 0.3: every mode damped |
Reference |
Hazel 1972, JFM 51 (R = 1); Miles 1961 / Howard 1961: J_c = 1/4 |
Base state (half-thickness and half velocity difference as units): U(y) = tanh(y) and buoyancy
frequency N^2(y) = J sech^2(y) (density profile tanh(y), same thickness as the shear layer: Hazel's
R = 1). The local Richardson number Ri(y) = N^2 / U'^2 = J cosh^2(y) is smallest at the centre, so J
is the minimum Richardson number. Linearized inviscid Boussinesq equations for a normal mode
exp(i alpha (x - c t)), written for the cross-stream velocity v and the buoyancy b:
(U - c)(v'' - alpha^2 v) - U'' v - i alpha b = 0 (vorticity)
i alpha (U - c) b + N^2 v = 0 (buoyancy: b_t + U b_x + N^2 v = 0)
Eliminating b gives the Taylor–Goldstein equation (U - c)(v'' - a^2 v) - U'' v + N^2 v / (U - c) = 0;
the pair above keeps the eigenvalue c linear. Miles (1961) and Howard (1961): no instability when
Ri >= 1/4 everywhere. For R = 1 the neutral curve is J = alpha (1 - alpha) (Hazel 1972, J. Fluid
Mech. 51, 39-61, R = 1 case; also Drazin & Reid 2004, section 44), whose maximum J = 1/4 at
alpha = 1/2 makes the Miles–Howard bound sharp.
Numerics (the whole line, no truncation, no extrapolation):
- y in (-inf, inf) through the tanh map y = L artanh(xi) (MappedChebyshev, ir/maps.py): the metric
dxi/dy = (1 - xi^2)/L is a polynomial, so the operators stay banded. The far field e^{-alpha|y|}
becomes (1 -+ xi)^{alpha L / 2}; L = 2 / alpha makes it analytic (declared choice; convergence in
L is shown by the study).
- Near neutrality c_i -> 0 and the critical layer U = c reaches the real axis: on the real line the
mode becomes singular and the discretized continuous spectrum (U(y) = c) mixes with it.
The path is therefore shifted into the complex plane, y(xi) - i delta (``contour``):
the unstable mode's critical point atanh(c) lies above the path, so its eigenvalue is unchanged
(Cauchy), while the continuous spectrum moves below the real axis. The mode is then followed
analytically *through* c_i = 0 and the neutral J is a root of c_i(J), found by Newton
(critical()), converged in N, in L and in delta: no extrapolation.
- Admissible modes (declared select): Howard's semicircle |c| <= 1, and |c_r| < 1/2 to stay away from
the ends U = +-1 of the continuous spectrum.
Run: python examples/shear/taylor_goldstein.py (quick, ~1 min)
python examples/shear/taylor_goldstein.py --study (N up to 192, L and delta: via Slurm)
Run it: python examples/shear/taylor_goldstein.py (in the conatus environment).
1"""Stratified Kelvin–Helmholtz: Taylor–Goldstein equation for the tanh/tanh layer (Hazel 1972).
2
3Base state (half-thickness and half velocity difference as units): U(y) = tanh(y) and buoyancy
4frequency N^2(y) = J sech^2(y) (density profile tanh(y), same thickness as the shear layer: Hazel's
5R = 1). The local Richardson number Ri(y) = N^2 / U'^2 = J cosh^2(y) is smallest at the centre, so J
6is the minimum Richardson number. Linearized inviscid Boussinesq equations for a normal mode
7exp(i alpha (x - c t)), written for the cross-stream velocity v and the buoyancy b:
8
9 (U - c)(v'' - alpha^2 v) - U'' v - i alpha b = 0 (vorticity)
10 i alpha (U - c) b + N^2 v = 0 (buoyancy: b_t + U b_x + N^2 v = 0)
11
12Eliminating b gives the Taylor–Goldstein equation (U - c)(v'' - a^2 v) - U'' v + N^2 v / (U - c) = 0;
13the pair above keeps the eigenvalue c linear. Miles (1961) and Howard (1961): no instability when
14Ri >= 1/4 everywhere. For R = 1 the neutral curve is J = alpha (1 - alpha) (Hazel 1972, J. Fluid
15Mech. 51, 39-61, R = 1 case; also Drazin & Reid 2004, section 44), whose maximum J = 1/4 at
16alpha = 1/2 makes the Miles–Howard bound sharp.
17
18Numerics (the whole line, no truncation, no extrapolation):
19- y in (-inf, inf) through the tanh map y = L artanh(xi) (MappedChebyshev, ir/maps.py): the metric
20 dxi/dy = (1 - xi^2)/L is a polynomial, so the operators stay banded. The far field e^{-alpha|y|}
21 becomes (1 -+ xi)^{alpha L / 2}; L = 2 / alpha makes it analytic (declared choice; convergence in
22 L is shown by the study).
23- Near neutrality c_i -> 0 and the critical layer U = c reaches the real axis: on the real line the
24 mode becomes singular and the discretized continuous spectrum (U(y) = c) mixes with it.
25 The path is therefore shifted into the complex plane, y(xi) - i delta (``contour``):
26 the unstable mode's critical point atanh(c) lies above the path, so its eigenvalue is unchanged
27 (Cauchy), while the continuous spectrum moves below the real axis. The mode is then followed
28 analytically *through* c_i = 0 and the neutral J is a root of c_i(J), found by Newton
29 (critical()), converged in N, in L and in delta: no extrapolation.
30- Admissible modes (declared select): Howard's semicircle |c| <= 1, and |c_r| < 1/2 to stay away from
31 the ends U = +-1 of the continuous spectrum.
32
33Run: python examples/shear/taylor_goldstein.py (quick, ~1 min)
34 python examples/shear/taylor_goldstein.py --study (N up to 192, L and delta: via Slurm)
35"""
36import sys
37
38import numpy as np
39
40from conatus.expert import (EVP, CartesianCoordinates, Domain, Field, MappedChebyshev, Parameter,
41 ParametricEVP, TanhMap, critical, diff, interp)
42
43
44def admissible(c):
45 """Howard's semicircle (an unstable mode has |c| <= max|U| = 1), and away from the ends of the
46 continuous spectrum U = +-1 (reached only at y = +-inf, where the shifted path meets the real
47 axis: discretized-continuum modes accumulate there with |c_i| -> 0 as N grows). The unstable
48 modes of this symmetric layer are stationary (c_r = 0)."""
49 return (np.abs(c) < 1) & (np.abs(c.real) < 0.5)
50
51
52def taylor_goldstein(N: int, alpha: float, *, L: float | None = None, contour: float = 0.4):
53 """Pencil in (J, alpha) on the whole line (tanh map, scale L = 2/alpha unless given)."""
54 L = 2 / alpha if L is None else L
55 cs = CartesianCoordinates("y")
56 y = cs["y"]
57 D = Domain(cs, [MappedChebyshev(y, N, TanhMap(L, contour=contour), dealias=1)])
58 v, b, U, S = Field("v", D), Field("b", D), Field("U", D), Field("S", D) # S = sech^2 y
59 c = Parameter("c", dtype=complex)
60 J, a = Parameter("J", 0.2), Parameter("alpha", alpha)
61
62 def L2(f):
63 return diff(diff(f, y), y) - a ** 2 * f
64
65 pb = EVP([v, b], eigenvalue=c, name="Taylor-Goldstein (tanh/tanh, R = 1)")
66 pb.add_equation(((U - c) * L2(v) - diff(diff(U, y), y) * v - 1j * a * b, 0))
67 pb.add_equation((1j * a * (U - c) * b + J * S * v, 0))
68 pb.add_bc(interp(v, y, -np.inf), interp(v, y, np.inf), into=0) # decay: taus in vorticity eq.
69 # 1 - tanh^2 rather than 1/cosh^2: cosh overflows on the mapped (complex) grid (refused)
70 # tanh and sech^2 on the mapped line are resolved to 2e-8 (alpha = 0.5) .. 2e-6 (alpha = 0.3,
71 # L = 6.7) at N = 96, and U'' (computed from the sampled tanh, so its interpolation error
72 # amplified) to 4e-5: accepted explicitly (the gallery tests check convergence in N and L:
73 # c_i drifts by 1.2e-7 from N = 96 to 128)
74 return ParametricEVP(pb, [J, a], ncc={U: np.tanh, S: lambda Y: 1 - np.tanh(Y) ** 2},
75 ncc_tail_tol=5e-5)
76
77
78def growth(P, J: float, alpha: float):
79 """Most unstable admissible mode at (J, alpha): full QZ, then tracked (residual reported)."""
80 return P.least_stable(growth="imag", select=admissible, J=J, alpha=alpha)
81
82
83def neutral_J(P, alpha: float, start: float = 0.15):
84 """Neutral J at alpha: Newton on c_i(J) = 0 along the continued mode (critical())."""
85 return critical(P, "J", bracket=(0.02, 0.35), start=start, growth="imag", select=admissible,
86 values={"alpha": alpha}, xtol=1e-13)
87
88
89if __name__ == "__main__":
90 study = "--study" in sys.argv
91 Ns = (64, 96, 128, 160, 192) if study else (64, 96)
92 print(taylor_goldstein(Ns[0], 0.5).problem.explain()) # N = 48 under-resolves dy(dy(U))
93 print("c_i(J) at alpha = 1/2 (tanh map L = 4, contour 0.4), drift between the last two N:")
94 for J in (0.0, 0.1, 0.2, 0.24):
95 row = [growth(taylor_goldstein(N, 0.5), J, 0.5) for N in Ns[-2:]]
96 print(f" J = {J:.2f}: c = {row[-1].sigma:.10f} (residual {row[-1].residual:.0e}), "
97 f"drift {abs(row[-1].sigma - row[-2].sigma):.1e}")
98 for alpha in (0.5, 0.3) if study else (0.5,):
99 print(f"Neutral J at alpha = {alpha} (Hazel: alpha (1 - alpha) = {alpha * (1 - alpha)}):")
100 for N in Ns:
101 cp = neutral_J(taylor_goldstein(N, alpha), alpha)
102 print(f" N = {N:3d}: J_n = {cp.value:.13f}, error {cp.value - alpha * (1 - alpha):+.1e}, "
103 f"c = {cp.mode.sigma:.1e}, residual {cp.mode.residual:.0e}", flush=True)
104 if alpha == 0.5: # Miles (1961), Howard (1961): J_c = 1/4
105 assert abs(cp.value - 0.25) < (2e-7 if N >= 96 else 1e-5), (N, cp.value)
106 if study:
107 print("Map scale L and contour delta at alpha = 1/2, N = 160 (path independence):")
108 for L, delta in ((4.0, 0.3), (4.0, 0.5), (8.0, 0.4), (3.0, 0.4)):
109 cp = neutral_J(taylor_goldstein(160, 0.5, L=L, contour=delta), 0.5)
110 print(f" L = {L}, delta = {delta}: J_n = {cp.value:.13f}, error {cp.value - 0.25:+.1e}")
111 print(cp.explain())