Cartesian coordinatesΒΆ
The biharmonic equation in Cartesian coordinates is written explicitly as:
The domain is wall-bounded in the \(x\) direction. The boundary conditions on the walls are
where \(f_{\pm} \left( \vy \right)\) represents the prescribed wall velocities.
Using the Fourier series, \(\psi\) reads
where \(I\) is the imaginary unit and \(k \equiv 2 \pi k^\prime / \ly\) with \(k^\prime \in \mathbb{Z}\) being the wave number.
Substituting this relation into the biharmonic equation yields:
Due to the orthogonality of the trigonometric functions, we obtain
Since the characteristic equation of this differential equation:
has the roots:
the homogeneous solution leads to
and its wall-normal derivative:
The coefficients \(A_k, B_k, C_k, D_k\) are determined from the boundary conditions:
or equivalently:
The second relation is simply the Fourier transform of the prescribed wall velocities with the sign flipped, while the first relation states that
for \(k \ne 0\), while \(k = 0\) imposes no condition, as we only enforce Neumann conditions with respect to \(\psi\).
The implementation is as follows.
def compute_stream_function(xs: np.ndarray, ys: np.ndarray, bc: [np.ndarray, np.ndarray]) -> np.ndarray:
# convert wall velocities to frequency domain
uy_m = - np.fft.rfft(bc[0])
uy_p = - np.fft.rfft(bc[1])
# compute stream function in frequency domain
psi_freq = np.zeros((ny // 2 + 1, nx), dtype="complex128")
mat = np.zeros((4, 4), dtype="float64")
vec = np.zeros((4), dtype="complex128")
# zero-th mode cannot be determined: just assign zero
psi_freq[0, :] = 0
for k in range(1, ny // 2 + 1):
# for each wavenumber, compute coefficients
mat[0] = get_f(k, xlim[0])
mat[1] = get_f(k, xlim[1])
mat[2] = get_dfdx(k, xlim[0])
mat[3] = get_dfdx(k, xlim[1])
vec[0] = 0
vec[1] = 0
vec[2] = uy_m[k]
vec[3] = uy_p[k]
coefs = np.linalg.solve(mat, vec)
# for each wall-normal position, compute stream function
for i, x in enumerate(xs):
psi_freq[k, i] = np.dot(coefs, get_f(k, x))
# convert frequency domain to physical domain
psi = np.zeros((ny, nx), dtype="float64")
for i, x in enumerate(xs):
psi[:, i] = np.fft.irfft(psi_freq[:, i])
return psi
def compute_boundary_condition(ys: np.ndarray) -> [np.ndarray, np.ndarray]:
uy_m = np.zeros(ys.shape)
uy_p = np.zeros(ys.shape)
for k in range(1, 8):
amp = np.power(float(k), -1)
phase = 2 * np.pi * np.random.random_sample()
uy_m += amp * np.sin(2 * np.pi * (k * ys / (ylim[1] - ylim[0]) + phase))
return uy_m, uy_p
def get_f(k: float, x: float) -> float:
return [
1 * np.exp(+ k * x),
x * np.exp(+ k * x),
1 * np.exp(- k * x),
x * np.exp(- k * x),
]
def get_dfdx(k: float, x: float) -> float:
return [
(0 + k * 1) * np.exp(+ k * x),
(1 + k * x) * np.exp(+ k * x),
(0 - k * 1) * np.exp(- k * x),
(1 - k * x) * np.exp(- k * x),
]
The computed stream function under
is shown below:

