Kármán vortex street
Follow a two-dimensional incompressible Navier–Stokes simulation as vortices form behind a circular cylinder and travel downstream. The image and video show Eqiora output from the linked workflow.
Google Colab
Section titled “Google Colab”Open the notebook and choose Runtime → Run all in a CPU runtime.
It installs Eqiora 0.1.3, then builds the simulation in the notebook: geometry,
equations, mesh, initialization, time stepping, plots and load measurements.
Each stage includes editable code and an explanation; no example script is imported.
This notebook targets the published 0.1.3 API, including its State-owned load readings.
The current development workflow below uses Result-owned load observations.
Local Python
Section titled “Local Python”The video was produced with 0.1.3. The source and snippets below follow the
current development API. From a repository checkout, install the declared
build toolchain and run the example against that same source:
mise installuv sync --extra gmsh --extra matplotlibuv run python examples/python/karman_vortex_street.py --vorticity-png wake.pngThe Python source includes geometry, model compilation, initialization, time integration and observations. The stages below explain that same workflow.
Stage 1 Define the channel and cylinder
The script builds the channel and
subtracts a circle of diameter centered at
. The Geometry names the fluid,
inlet, outlet, walls, and cylinder selections used throughout the run.
graph = eqiora.geometry.GeometryGraph()rectangle = graph.rectangle(x_bounds=(0.0, 2.2), y_bounds=(0.0, 0.41))circle = graph.circle(center=(0.2, 0.2), radius=0.05)fluid = graph.subtract(rectangle, circle)geometry = graph.build(fluid, named_topology={...})Stage 2 Set the flow problem
The .eqi source defines transient incompressible Navier–Stokes equations,
no-slip walls and cylinder, a parabolic inlet, and a zero-traction outlet. The physical
setup uses density 1 kg/m³, dynamic viscosity 0.001 Pa·s, and maximum inlet
speed 1.5 m/s. With mean inlet speed 1 m/s and cylinder diameter 0.1 m,
the Reynolds number is .
The outlet condition is authored in the same .eqi source:
, where is the outward normal.
Stage 3 Generate the mesh
Gmsh generates an unstructured triangular mesh with a target element size of
. The named geometry stays attached, so boundary conditions
and observations still refer to inlet, cylinder, and the other selections.
request = eqiora.meshing.GmshMesher( maximum_boundary_error=1e-4, maximum_target_size=0.02, minimum_mean_ratio=1e-5, maximum_boundary_facets=50,)mesh = eqiora.meshing.generate(eqiora.meshing.resolve(geometry, request))Compare meshes using a fixed physical observation window, then vary the time step separately. Pressure, force and frequency can respond differently to these changes; a smoother image alone does not measure their accuracy.
Stage 4 Advance the wake
Numerical method
Section titled “Numerical method”The resolved plan combines MINI/P1 elements, Backward Euler time stepping, Newton iteration, a sparse linear solver, and incompressible-flow scaling. Starting from a steady Stokes field, it advances 700 steps of 0.01 s, then samples another 200 steps through 9 s. Saving every second step gives a 0.02 s interval between displayed states. Video playback speed changes how quickly you watch them, not their physical times.
plan = eqiora.resolve( model, mesh=mesh, spatial=eqiora.fem.MiniP1(), temporal=eqiora.time.BackwardEuler(0.01), solve=eqiora.solve.Newton(linear=linear), scaling=eqiora.fluid.IncompressibleScaling(...),)result = eqiora.run(plan, state=state, steps=steps, output_steps=output_steps)The geometry and Reynolds number follow the FeatFlow DFG 2D-2 specification. For numerical comparisons, also inspect the boundary laws and measurement window: this model uses full symmetric Newtonian outlet traction and begins from a Stokes field. The reference uses a different outlet expression and initialization procedure, as well as different spatial and temporal methods. The cylinder-wake lesson explains how these choices affect a comparison.
Stage 5 Inspect vorticity and loads
Cell-average curl exposes the rotating structures convected behind the cylinder. The Result reconstructs signed cylinder force for the selected output; State provides pressure samples at the front and rear of the obstacle.
accepted = result.trajectory.states[-1]vorticity = accepted.curl(plan.capability.velocity)cylinder_force = result.boundary_force( geometry.selection("cylinder"), output_index=len(result.trajectory.states) - 1,)front_pressure = accepted.sample(plan.capability.pressure, at=(0.15, 0.2))rear_pressure = accepted.sample(plan.capability.pressure, at=(0.25, 0.2))
Stage 6 Follow the vortex street

Cell-average vorticity from selected states. Motion is disabled when the browser requests reduced motion; one representative state is shown instead.
Oppositely rotating structures form behind the obstacle. Their alternating signed vorticity travels downstream through the channel as the transient run advances.
Next steps
Section titled “Next steps”Cylinder wakes derives the momentum terms and explains how to read force histories. The Python execution guide covers run control, diagnostics, and arrays.