Polar coordinatesΒΆ
The biharmonic equation in polar coordinates is written explicitly as:
We adopt a Fourier series to expand the stream function in the azimuthal direction which is periodic:
Since the azimuthal derivatives are exchangeable with the rest of the operators, we obtain
or
due to the orthogonality of the trigonometric functions. Note that this can be written differently:
Here we perform a change-of-variable following Wood and Porter, J. Appl. Eng. Math. (6), 2019:
By using
we notice
and thus we obtain
whose characteristic equation is
whose four roots are \(\lambda = \pm k, 2 \pm k\).
At \(k = 0\), we have \(\lambda = 0, 0, 2, 2\) (two multiple roots) and
At \(k = \pm 1\), we have \(\lambda = - 1, 1, 1, 3\) (one multiple root) and thus
For the other \(k\) which is not \(0\) nor \(\pm 1\), we have \(\lambda = \pm k, 2 \pm k\) (no multiple roots) and thus
To summarize, the general solution leads to
The wall-normal derivative is
The coefficients are determined by incorporating the boundary conditions imposed on the walls; namely, at \(\vx = \vx_i > 0\) and \(\vx = \vx_o > \vx_i\), we enforce
which are, by performing forward transform:
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(k * ys + phase)
return uy_m, uy_p
def get_f(k: int, x: float) -> float:
if 0 == k:
raise RuntimeError("This code should not be reachable")
elif 1 == k:
return [
np.power(x, -1),
x,
x * np.log(x),
np.power(x, 3),
]
else:
return [
np.power(x, 0 - k),
np.power(x, 0 + k),
np.power(x, 2 - k),
np.power(x, 2 + k),
]
def get_dfdx(k: int, x: float) -> float:
if 0 == k:
raise RuntimeError("This code should not be reachable")
elif 1 == k:
return [
- np.power(x, -2),
1,
np.log(x) + 1,
3 * np.power(x, 2),
]
else:
return [
(0 - k) * np.power(x, 0 - k - 1),
(0 + k) * np.power(x, 0 + k - 1),
(2 - k) * np.power(x, 2 - k - 1),
(2 + k) * np.power(x, 2 + k - 1),
]
The results are displayed below.

