Womersley flow in a plane channel: exact profile (LBVP and IVP) and Floquet stability (Hill)

Family: oscillatory · Script: examples/oscillatory/womersley.py

Obtained

profile to 9e-16 (N = 24); RK443 order 2.85, SBDF2 1.95; Floquet ε = 0 = Orszag to 1e-9, O(ε²) deviation

Reference

Womersley 1955; Orszag 1971, JFM 50, Table 3

1. Oscillating pressure gradient. Lengths in half-widths h, time in 1/omega, velocity in P/omega
   (P: amplitude of -dp/dx / rho), Womersley number Wo = h sqrt(omega / nu):

       u_t - u_yy / Wo^2 = cos t,        u(-1) = u(1) = 0.

   The periodic solution is u = Re[U(y) e^{it}] with U = -i (1 - cosh(k y) / cosh k), k = sqrt(i) Wo
   (the plane-channel form of Womersley 1955, J. Physiol. 127, 553-563, eq. for the pipe with
   replaced by cosh). The harmonic U is an LBVP (i U - U'' / Wo^2 = 1) solved to round-off; the IVP
   started from the exact state is compared after one period (temporal order of the scheme).

2. Pulsatile Poiseuille flow (Orr–Sommerfeld units: centreline velocity 1, Re = U_c h / nu):
   U(y, t) = 1 - y^2 + eps Re[W(y) e^{i omega t}], W the same Womersley profile normalized to
   W(0) = 1. Floquet modes v = e^{mu t} sum_n v_n(y) e^{i n omega t} of

       (d/dt + i alpha U) (v'' - alpha^2 v) - i alpha U'' v - (v'''' - 2 alpha^2 v'' + alpha^4 v) / Re = 0

   come from the general Hill operator (floquet.hill_from_plan) built on the same IR problem as the
   steady Orr–Sommerfeld EVP. Checks: eps = 0 gives Orszag's eigenvalue (1971, J. Fluid Mech. 50,
   689-703, Table 3: c = 0.23752649 + 0.00373967 i at Re = 1e4, alpha = 1), and the shift
   mu(eps) - mu(0) is O(eps^2) (the harmonics +-1 only act at second order). A published Floquet
   value for this flow (von Kerczek 1982; Pier & Schmid 2017) is not reproduced yet.

Run:  python examples/oscillatory/womersley.py

Run it: python examples/oscillatory/womersley.py (in the conatus environment).

  1"""Womersley flow in a plane channel: exact profile (LBVP and IVP) and Floquet stability (Hill).
  2
  31. Oscillating pressure gradient. Lengths in half-widths h, time in 1/omega, velocity in P/omega
  4   (P: amplitude of -dp/dx / rho), Womersley number Wo = h sqrt(omega / nu):
  5
  6       u_t - u_yy / Wo^2 = cos t,        u(-1) = u(1) = 0.
  7
  8   The periodic solution is u = Re[U(y) e^{it}] with U = -i (1 - cosh(k y) / cosh k), k = sqrt(i) Wo
  9   (the plane-channel form of Womersley 1955, J. Physiol. 127, 553-563, eq. for the pipe with
 10   replaced by cosh). The harmonic U is an LBVP (i U - U'' / Wo^2 = 1) solved to round-off; the IVP
 11   started from the exact state is compared after one period (temporal order of the scheme).
 12
 132. Pulsatile Poiseuille flow (Orr–Sommerfeld units: centreline velocity 1, Re = U_c h / nu):
 14   U(y, t) = 1 - y^2 + eps Re[W(y) e^{i omega t}], W the same Womersley profile normalized to
 15   W(0) = 1. Floquet modes v = e^{mu t} sum_n v_n(y) e^{i n omega t} of
 16
 17       (d/dt + i alpha U) (v'' - alpha^2 v) - i alpha U'' v - (v'''' - 2 alpha^2 v'' + alpha^4 v) / Re = 0
 18
 19   come from the general Hill operator (floquet.hill_from_plan) built on the same IR problem as the
 20   steady Orr–Sommerfeld EVP. Checks: eps = 0 gives Orszag's eigenvalue (1971, J. Fluid Mech. 50,
 21   689-703, Table 3: c = 0.23752649 + 0.00373967 i at Re = 1e4, alpha = 1), and the shift
 22   mu(eps) - mu(0) is O(eps^2) (the harmonics +-1 only act at second order). A published Floquet
 23   value for this flow (von Kerczek 1982; Pier & Schmid 2017) is not reproduced yet.
 24
 25Run:  python examples/oscillatory/womersley.py
 26"""
 27import numpy as np
 28
 29from conatus.expert import (EVP, IVP, LBVP, IVPSolver, Parameter, channel, cos, diff, dt,
 30                            discretize, grid, hill_from_plan, interp, secant_root)
 31
 32ORSZAG_C = 0.23752649 + 0.00373967j          # Orszag (1971), Re = 1e4, alpha = 1
 33
 34
 35def womersley_exact(Y, t, Wo):
 36    """u(y, t) = Re[-i (1 - cosh(k y)/cosh k) e^{it}], k = sqrt(i) Wo."""
 37    k = np.sqrt(1j) * Wo
 38    return (-1j * (1 - np.cosh(k * Y) / np.cosh(k)) * np.exp(1j * t)).real
 39
 40
 41def harmonic_lbvp(N: int, Wo: float):
 42    """i U - U'' / Wo^2 = 1, U(+-1) = 0 (complex LBVP); returns (y grid, U, max residual)."""
 43    ch = channel(N, base=None)
 44    y, U = ch.y, ch.field("U", dtype=complex)
 45    pb = LBVP([U], name="Womersley harmonic")
 46    pb.add_equation((1j * U - diff(diff(U, y), y) / Wo ** 2, 1))
 47    pb.add_bc(interp(U, y, -1), interp(U, y, 1))
 48    sol = discretize(pb).solve()
 49    return grid(ch.basis, dealias=False), sol.grid("U"), sol.max_residual
 50
 51
 52def oscillating_channel(N: int, Wo: float):
 53    """IVP u_t - u_yy / Wo^2 = cos t with no-slip walls; t enters the RHS as a parameter."""
 54    ch = channel(N, base=None)
 55    y, u, t = ch.y, ch.field("u"), Parameter("t", 0.0)
 56    pb = IVP([u], name="oscillating channel")
 57    pb.add_equation((dt(u) - diff(diff(u, y), y) / Wo ** 2, cos(t)))
 58    pb.add_bc(interp(u, y, -1), interp(u, y, 1))
 59    return discretize(pb), u, grid(ch.basis, dealias=False)
 60
 61
 62def one_period_error(N: int, Wo: float, scheme: str, steps: int) -> float:
 63    plan, u, Y = oscillating_channel(N, Wo)
 64    s = IVPSolver(plan, scheme, 2 * np.pi / steps, time_parameter="t")
 65    s.set_state({u: lambda y: womersley_exact(y, 0.0, Wo)})
 66    s.run(steps)
 67    return float(np.abs(s.grid("u") - womersley_exact(Y, s.t, Wo)).max())
 68
 69
 70def pulsatile_hill(N: int, Re: float, alpha: float, omega: float, eps: float, Nh: int):
 71    """Hill operator of Orr–Sommerfeld around U = 1 - y^2 + eps Re[W e^{i omega t}]."""
 72    ch = channel(N, base=None)
 73    y, v, U = ch.y, ch.field("v"), ch.field("U")
 74    s = Parameter("s", dtype=complex)                           # growth rate (EVP convention)
 75
 76    def L2(f):
 77        return diff(diff(f, y), y) - alpha ** 2 * f
 78
 79    pb = EVP([v], eigenvalue=s, name="Orr-Sommerfeld (growth rate)")
 80    pb.add_equation((s * L2(v) + 1j * alpha * U * L2(v) - 1j * alpha * diff(diff(U, y), y) * v
 81                     - L2(L2(v)) / Re, 0))
 82    pb.add_bc(interp(v, y, -1), interp(v, y, 1), interp(diff(v, y), y, -1), interp(diff(v, y), y, 1))
 83    k = np.sqrt(1j * omega * Re)                                # Wo = sqrt(omega Re) in these units
 84    W = lambda Y: (1 - np.cosh(k * Y) / np.cosh(k)) / (1 - 1 / np.cosh(k))   # noqa: E731, W(0) = 1
 85    harmonics = {0: lambda Y: 1 - Y ** 2 + 0j,                  # steady Poiseuille part
 86                 1: lambda Y: 0.5 * eps * W(Y), -1: lambda Y: 0.5 * eps * np.conj(W(Y))}
 87    return hill_from_plan(lambda ncc: discretize(pb, ncc=ncc), {U: harmonics}, Nh=Nh,
 88                          omega=omega, label=f"pulsatile Poiseuille eps={eps}")
 89
 90
 91def floquet_exponent(eps: float, *, N=64, Re=1e4, alpha=1.0, omega=0.1, Nh=8):
 92    """Floquet exponent continued from the steady Orr–Sommerfeld mode (seed mu = -i alpha c)."""
 93    H = pulsatile_hill(N, Re, alpha, omega, eps, Nh)
 94    root = secant_root(H, -1j * alpha * ORSZAG_C, tol=1e-12, max_iterations=60)  # raises if unconverged
 95    return root.mu, H
 96
 97
 98if __name__ == "__main__":
 99    Wo = 4.0
100    print(f"1. Womersley harmonic (LBVP), Wo = {Wo}:")
101    for N in (16, 24, 32):
102        Y, U, res = harmonic_lbvp(N, Wo)
103        ex = -1j * (1 - np.cosh(np.sqrt(1j) * Wo * Y) / np.cosh(np.sqrt(1j) * Wo))
104        print(f"   N={N}: max|U - U_exact| = {np.abs(U - ex).max():.1e}, residual {res:.1e}")
105    assert np.abs(U - ex).max() < 5e-15 and res < 1e-14         # Womersley (1955), closed form
106    print("   IVP over one period from the exact state (N = 32): error vs steps")
107    for scheme in ("RK443", "SBDF2"):
108        errs = [one_period_error(32, Wo, scheme, n) for n in (100, 200, 400)]
109        orders = np.log2(np.array(errs[:-1]) / errs[1:])
110        print(f"   {scheme}: " + ", ".join(f"{e:.2e}" for e in errs) + f"  observed orders {np.round(orders, 2)}")
111        assert orders[-1] > {"RK443": 2.6, "SBDF2": 1.75}[scheme], (scheme, orders)   # 2.85, 1.95 observed
112    print("2. Pulsatile Poiseuille, Floquet (Re = 1e4, alpha = 1, omega = 0.1):")
113    mu0, H = floquet_exponent(0.0)
114    print(f"   eps = 0: mu = {mu0:.10f}, c = i mu / alpha = {1j * mu0:.8f} (Orszag {ORSZAG_C})")
115    assert abs(1j * mu0 - ORSZAG_C) < 1e-8, mu0                   # Orszag (1971), his 8 digits
116    for eps in (0.0125, 0.025, 0.05):
117        mu, _ = floquet_exponent(eps)
118        print(f"   eps = {eps}: mu = {mu:.10f}, |mu - mu0| / eps^2 = {abs(mu - mu0) / eps ** 2:.4e}")
119    for Nh in (4, 8, 12):
120        print(f"   eps = 0.05, Nh = {Nh}: mu = {floquet_exponent(0.05, Nh=Nh)[0]:.12f}")
121    for N in (48, 64, 80):
122        print(f"   eps = 0.05, N = {N}: mu = {floquet_exponent(0.05, N=N)[0]:.12f}")
123    print(H.explain())