FOANSS, i.e. Fadoobaba's Open Advanced Navier-Stokes ๐ฌ Solver is now capable to simulate flow around curved geometries. In this example, flow around a circular cylinder ⭕ is presented. The main challenge in handling the curved boundaries ๐ป in discrete world is application of the Neumann boundary condition for pressure. The method employed in FOANSS is to use polar ⭗ coordinate system for normal derivative calculation. The idea came while yours's truly was developing FOAMNE i.e. Fadoobaba's Open Advanced ๐จ Mechanics ๐ง Neural Engine. More details can be read here.
In the CFD code, ∂p/dn is replaced by ∂p/dx * cos(ฮธ) + ∂p/dy * sin(ฮธ). After some trickery, the pressure on the curved surface can be represented by equation 1. Within equation 1, p is the pressure at current time-step. i and j are nodes in the x and y axis, respectively. cos_theta and sin_theta represent radial angles of the polar coordinate system (r ฮธ).
((p[i + 1, j] + p[i - 1, j]) * cos_theta + (p[i, j + 1] + p[i, j - 1] * sin_theta)) / (2 * (cos_theta + sin_theta)) (1)
At first, flow around a circular cylinder at Reynolds number 100 is simulated. The accuracy of the results is measured using the Strouhal number. For Reynolds number 100, Strouhal number for flow around a circular cylinder is 0.16. The Strouhal number obtained from the code on a coarse mesh, i.e. only 50 cells per cylinder diameter, is 0.17. This is within 8% of the published data. The Strouhal number will never reach 0.16 as the mesh is cartesian and the cylinder surface is represented as a stairstep. Still, very good for teaching purposes! The results are shown in Fig. 1. It can be seen that the vortex shedding phenomenon is calculated relatively accurately. A remainder that you are reading a blog, not a Q1 journal ๐. The vorticity plot is shown in Fig. 2.
This code and blog post is not endorsed or approved by Dr. Barba, I just continue the open-source work of her.
#%% import libraries
import numpy as np
import matplotlib.pyplot as plt
#%% define spatial and temporal grids
l_square = 1 # length of square or diamater of circle
h = l_square / 50 # grid spacing
dt = 0.0001 # time step
L = 15 # domain length
D = 10 # domain depth
Nx = round(L / h) + 1 # grid points in x-axis
Ny = round(D / h) + 1 # grid points in y-axis
nu = 1 / 100 # kinematic viscosity
Uinf = 1 # free stream velocity / inlet velocity / lid velocity
cfl = dt * Uinf / h # cfl number
travel = 2 # times the disturbance travels entire length of computational domain
TT = travel * L / Uinf # total time
ns = int(TT / dt) # number of time steps
Re = round(l_square * Uinf / nu) # Reynolds number
rho = 1 # fluid density
#%% initialize flowfield
u = np.ones((Nx, Ny)) # x-velocity
v = np.zeros((Nx, Ny)) # y-velocity
p = np.zeros((Nx, Ny)) # pressure
x = np.linspace(0, L, Nx) # x-axis vector
y = np.linspace(0, D, Ny) # y-axis vector
XX, YY = np.meshgrid(x, y, indexing="ij") # grid for circle boundary
#%% create circle
x_c = 5 # circle center y
y_c = 5 # circle center y
radius = 0.5 # circle radius
mask = (XX - x_c)**2 + (YY - y_c)**2 <= radius**2 # circular region
tol = 0.125 * h # tolerance for boundary points
circle_boundary = np.abs(np.sqrt((XX - x_c)**2 + (YY - y_c)**2) - radius) <= tol # boundary mask
boundary_indices = np.argwhere(circle_boundary) # circle boundary
# compute radial distances and angles (polar coordinates)
r_values = []
cos_theta_values = []
sin_theta_values = []
for idx in boundary_indices:
i, j = idx
r = np.sqrt((XX[i, j] - x_c)**2 + (YY[i, j] - y_c)**2)
cos_theta = (XX[i, j] - x_c) / r
sin_theta = (YY[i, j] - y_c) / r
r_values.append(r)
cos_theta_values.append(cos_theta)
sin_theta_values.append(sin_theta)
# define quadrant masks (circle is divided in 4 parts) and corner points
quadrant1 = (XX > x_c) & (YY > y_c) & circle_boundary
quadrant2 = (XX > x_c) & (YY < y_c) & circle_boundary
quadrant3 = (XX < x_c) & (YY < y_c) & circle_boundary
quadrant4 = (XX < x_c) & (YY > y_c) & circle_boundary
top_pt= (XX == x_c) & (YY > y_c) & circle_boundary
bottom_pt = (XX == x_c) & (YY < y_c) & circle_boundary
left_pt = (XX < x_c) & (YY == y_c) & circle_boundary
right_pt = (XX > x_c) & (YY == y_c) & circle_boundary
#%% Solve 2D Navier-Stokes equations
for nt in range(ns):
pn = p.copy()
p[1:-1, 1:-1] = (pn[2:, 1:-1] + pn[:-2, 1:-1] + pn[1:-1, 2:] + pn[1:-1, :-2]) / 4 - h * rho / (8 * dt) * (u[2:, 1:-1] - u[:-2, 1:-1] + v[1:-1, 2:] - v[1:-1, :-2]) # pressure
# boundary conditions for pressure
p[0, :] = p[1, :] # dp/dx = 0 at x = 0
p[-1, :] = 0 # p = 0 at x = L
p[:, 0] = p[:, 1] # dp/dy = 0 at y = 0
p[:, -1] = p[:, -2] # dp/dy = 0 at y = D
p[mask] = 0 # circle
# apply Neumann boundary condition for pressure in polar coordinates
p[quadrant1] = (((p[i + 1, j] + p[i - 1, j]) * cos_theta) + ((p[i, j + 1] + p[i, j - 1]) * sin_theta)) / (2 * (cos_theta + sin_theta))
p[quadrant2] = (((p[i + 1, j] + p[i - 1, j]) * cos_theta) + ((p[i, j + 1] + p[i, j - 1]) * sin_theta)) / (2 * (cos_theta + sin_theta))
p[quadrant3] = (((p[i + 1, j] + p[i - 1, j]) * cos_theta) + ((p[i, j + 1] + p[i, j - 1]) * sin_theta)) / (2 * (cos_theta + sin_theta))
p[quadrant4] = (((p[i + 1, j] + p[i - 1, j]) * cos_theta) + ((p[i, j + 1] + p[i, j - 1]) * sin_theta)) / (2 * (cos_theta + sin_theta))
p[top_pt] = p[i, j + 1]
p[bottom_pt] = p[i, j - 1]
p[left_pt] = p[i - 1, j]
p[right_pt] = p[i + 1, j]
un = u.copy()
vn = v.copy()
u[1:-1, 1:-1] = (un[1:-1, 1:-1] - dt / (2 * h) * (un[1:-1, 1:-1] * (un[2:, 1:-1] - un[:-2, 1:-1]) + vn[1:-1, 1:-1] * (un[1:-1, 2:] - un[1:-1, :-2])) - dt / (2 * rho * h) * (p[2:, 1:-1] - p[:-2, 1:-1]) + nu * dt / h**2 * (un[2:, 1:-1] + un[:-2, 1:-1] + un[1:-1, 2:] + un[1:-1, :-2] - 4 * un[1:-1, 1:-1])) # x momentum
# boundary conditions for x-velocity
u[0, :] = Uinf # u = Uinf at x = 0
u[-1, :] = u[-2, :] # du/dx = 0 at x = L
u[:, 0] = Uinf # u = 0 at y = 0
u[:, -1] = Uinf # u = 0 at y = D
u[mask] = 0 # circle
u[circle_boundary] = 0 # circle
v[1:-1, 1:-1] = (vn[1:-1, 1:-1] - dt / (2 * h) * (un[1:-1, 1:-1] * (vn[2:, 1:-1] - vn[:-2, 1:-1]) + vn[1:-1, 1:-1] * (vn[1:-1, 2:] - vn[1:-1, :-2])) - dt / (2 * rho * h) * (p[1:-1, 2:] - p[1:-1, :-2]) + nu * dt / h**2 * (vn[2:, 1:-1] + vn[:-2, 1:-1] + vn[1:-1, 2:] + vn[1:-1, :-2] - 4 * vn[1:-1, 1:-1])) # y momentum
# boundary conditions for y-velocity
v[0, :] = 0 # v = 0 at x = 0
v[-1, :] = v[-2, :] # dv/dx = 0 at x = L
v[:, 0] = 0 # v = 0 at y = 0
v[:, -1] = 0 # v = 0 at y = D
v[mask] = 0 # circle
v[circle_boundary] = 0 # circle
#%% post-processing
velocity_magnitude = np.sqrt(u**2 + v**2) # velocity magnitude
X, Y = np.meshgrid(np.linspace(0, L, Nx), np.linspace(0, D, Ny)) # spatial grid
# visualize velocity vectors and pressure contours
plt.figure(dpi = 500)
plt.contourf(X, Y, u.T, 128, cmap = 'jet')
# plt.colorbar()
plt.gca().set_aspect('equal', adjustable='box')
plt.xticks([0, L])
plt.yticks([0, D])
plt.xlabel('x [m]')
plt.ylabel('y [m]')
plt.colorbar(orientation='vertical')
plt.show()
plt.figure(dpi = 500)
plt.contourf(X, Y, v.T, 128, cmap = 'jet')
# plt.colorbar()
plt.gca().set_aspect('equal', adjustable='box')
plt.xticks([0, L])
plt.yticks([0, D])
plt.xlabel('x [m]')
plt.ylabel('y [m]')
plt.colorbar(orientation='vertical')
plt.show()
plt.figure(dpi = 500)
plt.contourf(X, Y, p.T, 128, cmap = 'jet')
# plt.colorbar()
plt.gca().set_aspect('equal', adjustable='box')
plt.xticks([0, L])
plt.yticks([0, D])
plt.xlabel('x [m]')
plt.ylabel('y [m]')
plt.colorbar(orientation='vertical')
plt.show()
plt.figure(dpi = 500) # make a nice crisp image :)
plt.streamplot(X, Y, u.T, v.T, color = 'black', cmap = 'jet', density = 2, linewidth = 0.5,\
arrowstyle='->', arrowsize = 1) # plot streamlines
plt.gca().set_aspect('equal', adjustable = 'box')
plt.axis('off')
plt.show()
Thank you very much for reading! If you want to hire me as your new shinning post-doc or want to collaborate on the research, please reach out!