Flow Past Multiple Obstacles¶
A steady Navier-Stokes example that highlights TensorMesh’s
mesh-generation capabilities as much as its FEM machinery. The
script examples/fluid/flow_obstacles/flow_obstacles.py solves
incompressible flow at \(\mathrm{Re} = 150\) through a
\(3 \times 1\) channel containing six circular obstacles of
varying size and position. The mesh is generated programmatically
via MeshGen using CSG (constructive solid
geometry): start with a rectangle, subtract six discs.
Problem¶
with the channel \((0, 3) \times (0, 1)\) minus six discs at
Boundary conditions:
inlet (\(x = 0\)): parabolic profile, \(u_x(y) = 4\,y\,(1 - y)\),
walls (\(y = 0\), \(y = 1\)) and obstacle surfaces: no-slip,
outlet (\(x = 3\)): \(p = 0\).
At \(\mathrm{Re} = 150\) the flow is steady (no shedding), the wake of each obstacle interacts with the next, and the solution shows characteristic narrow jets between the closely- spaced cylinders.
Mesh generation via CSG¶
The most interesting part of this script is the mesh-generation
block — it is short, self-contained, and demonstrates the
add_… / remove_… style of CSG that
MeshGen exposes:
from tensormesh import MeshGen
gen = MeshGen(chara_length=1.0/n_grid)
gen.add_rectangle(0, 0, 3.0, 1.0)
for ox, oy, orad in [
(0.5, 0.5, 0.10), (1.0, 0.3, 0.08), (1.0, 0.7, 0.08),
(1.5, 0.5, 0.12), (2.0, 0.4, 0.07), (2.0, 0.6, 0.07),
]:
gen.remove_circle(ox, oy, orad)
mesh = gen.gen().double()
Under the hood, MeshGen defers to Gmsh’s OpenCASCADE backend
for the boolean operations and triangulation, then converts back
into a Mesh with its internal element
ordering. See Meshes for the full
MeshGen API.
Solver¶
Identical to Lid-Driven Cavity — same custom NavierStokesAssembler,
same SUPG/PSPG stabilization, same Picard linearization. The only
real differences are:
the boundary masks are richer (inlet, walls, obstacle surfaces, outlet),
the inlet velocity is non-zero (parabolic), so the
Condenseris built withdirichlet_valuecontaining the prescribed inlet profile,the outlet has no velocity BC; only the pressure is pinned to zero.
is_inlet = points[:, 0] < 1e-6
is_outlet = points[:, 0] > 3.0 - 1e-6
is_wall = (points[:, 1] < 1e-6) | (points[:, 1] > 1.0 - 1e-6)
# …assemble u_mask / u_val accordingly, then Picard-iterate as in cavity…
Output¶
flow_obstacles.png is the final figure: speed magnitude and
pressure contour overlaid on the obstacle silhouettes. Useful
sanity checks on the picture:
a high-speed jet between the two upper-row obstacles at \(x \approx 1.0\),
a broad recirculation zone behind the largest obstacle at \((1.5, 0.5)\),
near-uniform pressure drop across the channel.
Fig. 47 Output of flow_obstacles.py at Re = 150. Left: speed
magnitude — high-velocity jets squeeze between obstacle
pairs and broaden into wakes downstream. Right: pressure —
stagnation upstream of each obstacle, low-pressure pockets
in the wakes, and a near-uniform streamwise pressure drop
across the channel.¶
Running it¶
cd examples/fluid/flow_obstacles
python flow_obstacles.py # writes flow_obstacles.png
The Picard loop converges in roughly 20 iterations at the default mesh resolution.
What’s next¶
Lid-Driven Cavity — the simpler steady benchmark.
Cylinder Flow (Vortex Shedding) — the transient version of “flow past a body”, where shedding takes over.
Meshes — full
MeshGenAPI including 3D primitives and hybrid meshes.