flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
vof_capillary_references.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Exact VISCOUS references for the two capillary-oscillation benchmarks of the VoF campaign
3(`tests/study/vof_surface_tension.py` gates `wave` and `lamb`), replacing the inviscid formulas
4those gates were first compared against. Derived 2026-09-02 (VOF_NEXT_SESSION.md, Item 2).
5
61. Planar capillary wave on the interface between two semi-infinite fluids of EQUAL density rho
7 and kinematic viscosity nu (the `wave` gate's configuration). Normal modes ~ exp(s t):
8
9 s^2 + w0^2 (1 - k / sqrt(k^2 + s/nu)) = 0, w0^2 = sigma k^3 / (2 rho), Re sqrt > 0
10
11 (even vertical-velocity solution A e^{-k|z|} + B e^{-m|z|}, m^2 = k^2 + s/nu; continuity of
12 w, w', w''; normal-stress jump mu [w'''] = -sigma k^4 w(0)/s). First order in
13 eps = k sqrt(nu/w0): s = i w0 - (1 + i) w0 eps/(2 sqrt 2), i.e. the frequency deficit equals
14 the damping rate over w0. NOTE the damping is the O(sqrt(nu)) interfacial boundary-layer
15 rate, 3-4x larger than the free-surface 2 nu k^2 at the gate's parameters. Walls at +-H
16 enter only through w0^2 -> sigma k^3/(2 rho coth(kH)) (0.4 % at kH = pi).
17 Measured (WO-P, nvidia-cuda): omega agrees to -0.03 / -0.23 / +0.52 % at
18 (32 cells/lambda, nu 0.005) / (64, 0.005) / (32, 0.02).
19
202. Mode-n shape oscillation of a drop in a host of EQUAL rho and nu (the `lamb` gate). Unsteady
21 Stokes normal modes: potential part (r^n inside, r^-(n+1) outside) + poloidal vortical part
22 (j_n(q r) inside, h_n^(1)(q r) outside, q^2 = -s/nu, Im q > 0), matched at r = R by
23 continuity of u_r, u_theta, tau_rtheta and the normal-stress jump sigma (n-1)(n+2) eta/R^2
24 with s eta = u_r. 4x4 determinant, root near Lamb's i w0. First order:
25 d omega/w0 = -0.885 sqrt(nu/(w0 R^2)) for n = 2.
26 Measured: the simulation captures only part of this shift (its damping is 25-43 % below
27 -Re s) because the interfacial boundary layer sqrt(nu/w0) is 0.1-0.45 cells at R = 8; and
28 a further ~4 % INVISCID, resolution-independent deficit remains (Item 2 addendum).
29
30Usage: python tests/study/vof_capillary_references.py # prints both tables
31 from vof_capillary_references import wave_mode, drop_mode # in a page or a gate
32Requires mpmath for the drop (pip install mpmath); the wave needs only numpy/scipy.
33"""
34import math
35
36import numpy as np
37
38
39# --------------------------------------------------------------------------- planar wave
40def wave_mode(k, nu, sigma=1.0, rho=1.0, H=None):
41 """Complex growth rate s (Re s < 0 decay, Im s frequency) of the two-fluid capillary wave."""
42 from scipy.optimize import fsolve
43
44 w0sq = sigma * k ** 3 / (2 * rho)
45 if H is not None:
46 w0sq *= math.tanh(k * H)
47 w0 = math.sqrt(w0sq)
48
49 def f(v):
50 s = complex(v[0], v[1])
51 m = np.sqrt(k * k + s / nu)
52 if m.real < 0:
53 m = -m
54 r = s * s + w0sq * (1.0 - k / m)
55 return [r.real, r.imag]
56
57 eps = k * math.sqrt(nu / w0)
58 v = fsolve(f, [-w0 * eps / (2 * math.sqrt(2)), w0 * (1 - eps / (2 * math.sqrt(2)))], xtol=1e-12)
59 return complex(v[0], v[1]), w0
60
61
62# --------------------------------------------------------------------------- drop modes
63def drop_mode(n, R, nu, sigma=1.0, rho=1.0, dps=50):
64 """Complex growth rate s of mode n of a drop in an identical fluid; returns (s, w0_Lamb)."""
65 import mpmath as mp
66
67 mp.mp.dps = dps
68 mu = rho * nu
69
70 def jn(z):
71 return mp.sqrt(mp.pi / (2 * z)) * mp.besselj(n + mp.mpf(1) / 2, z)
72
73 def hn(z): # spherical Hankel of the first kind, via mpmath's hankel1 (no j + i y cancellation)
74 return mp.sqrt(mp.pi / (2 * z)) * mp.hankel1(n + mp.mpf(1) / 2, z)
75
76 def det(s):
77 q = mp.sqrt(-s / nu)
78 if mp.im(q) < 0:
79 q = -q # the outer vortical solution must decay
80 Rm = mp.mpf(R)
81 jR, hR = jn(q * Rm), hn(q * Rm) # column scaling (root-neutral): keeps the Hankel
82 basis = [(lambda r: (r / Rm) ** n, lambda r: mp.mpf(0), 1), # column from underflowing
83 (lambda r: mp.mpf(0), lambda r: jn(q * r) / jR, 1),
84 (lambda r: (Rm / r) ** (n + 1), lambda r: mp.mpf(0), -1),
85 (lambda r: mp.mpf(0), lambda r: hn(q * r) / hR, -1)]
86 M = mp.matrix(4, 4)
87 for j, (phi, f, sg) in enumerate(basis):
88 U = lambda x, phi=phi, f=f: mp.diff(phi, x) + n * (n + 1) * f(x) / x
89 V = lambda x, phi=phi, f=f: (phi(x) + mp.diff(lambda y: y * f(y), x)) / x
90 T = mu * (Rm * mp.diff(lambda y: V(y) / y, Rm) + U(Rm) / Rm)
91 N = rho * s * phi(Rm) + 2 * mu * mp.diff(U, Rm)
92 M[0, j] = sg * U(Rm)
93 M[1, j] = sg * V(Rm)
94 M[2, j] = sg * T
95 M[3, j] = -sg * N - (sigma * (n - 1) * (n + 2) / (s * Rm ** 2)) * U(Rm) * (1 if sg == 1 else 0)
96 return mp.det(M)
97
98 w0 = mp.sqrt(n * (n - 1) * (n + 1) * (n + 2) * sigma / (R ** 3 * (2 * n + 1) * rho))
99 eps = mp.sqrt(nu / (w0 * R * R))
100 s0 = w0 * mp.mpc(-0.885 * eps, 1 - 0.885 * eps)
101 d0 = det(s0)
102 s = mp.findroot(lambda s: det(s) / d0, s0, tol=1e-24, maxsteps=200)
103 return complex(s), float(w0)
104
105
106if __name__ == "__main__":
107 print("Planar two-fluid capillary wave (equal rho, nu), sigma = rho = 1, cell units")
108 print(f"{'nx':>4} {'nu':>7} {'w0':>9} {'Im s':>9} {'dw/w0':>8} {'-Re s':>10} {'2 nu k^2':>10} WO-P measured omega / gamma")
109 for nx, nu, mw, mg in [(32, 0.005, 0.06017, 1.087e-3), (64, 0.005, 0.02130, 4.628e-4), (32, 0.02, 0.05928, 2.746e-3)]:
110 k = 2 * math.pi / nx
111 s, w0 = wave_mode(k, nu, H=nx / 2.0)
112 print(f"{nx:>4} {nu:>7.3f} {w0:>9.5f} {s.imag:>9.5f} {100*(s.imag/w0-1):>7.2f}% {-s.real:>10.3e} {2*nu*k*k:>10.3e}"
113 f" {mw:.5f} ({100*(mw/s.imag-1):+.2f}%) / {mg:.3e} ({100*(mg/(-s.real)-1):+.1f}%)")
114 print("\nMode-2 drop in an identical fluid, R = 8, sigma = rho = 1")
115 print(f"{'nu':>8} {'eps':>7} {'w0':>9} {'Im s':>9} {'dw/w0':>8} {'-Re s':>10} 32^3 measured omega / gamma")
116 meas = {0.02: (0.09010, 3.983e-3), 0.01: (0.09061, 2.300e-3), 0.005: (0.09072, 1.474e-3),
117 0.0025: (0.09075, 1.011e-3), 0.00125: (0.09066, 7.108e-4)}
118 try:
119 for nu in [0.02, 0.01, 0.005, 0.0025, 0.00125]:
120 s, w0 = drop_mode(2, 8.0, nu)
121 m = meas[nu]
122 eps = math.sqrt(nu / (w0 * 64))
123 print(f"{nu:>8g} {eps:>7.4f} {w0:>9.5f} {s.imag:>9.5f} {100*(s.imag/w0-1):>7.2f}% {-s.real:>10.3e}"
124 f" {m[0]:.5f} ({100*(m[0]/s.imag-1):+.2f}%) / {m[1]:.3e} ({100*(m[1]/(-s.real)-1):+.1f}%)")
125 except ImportError:
126 print(" (mpmath not installed)")
static constexpr double U