2dPointVortex is a dependency-light C++20 solver for two-dimensional point-vortex dynamics. It supports the infinite plane, a plane periodic in one direction, a square periodic box, and a circular disk; CPU/OpenMP, MPI, and NVIDIA CUDA executables use the same input format, integrators, output files, and checkpoints.
The solver uses direct all-pairs velocity sums: O(N^2) except for the doubly periodic
O(N^2 M) image sum. It is a clear numerical reference and a practical tool for small-to-medium
simulations; it is not a tree-code or FMM implementation.
The CPU example needs only CMake 3.20+ and a C++20 compiler. From the repository root:
cmake -S . -B build/cpu -DCMAKE_BUILD_TYPE=Release \
-DPOINT_VORTEX_MPI=OFF -DPOINT_VORTEX_CUDA=OFF
cmake --build build/cpu --parallel
ctest --test-dir build/cpu --output-on-failure
./build/cpu/point_vortex_cpu examples/quickstart.paramsThis advances a two-vortex infinite-plane case to time 0.1. It writes:
| File | Contents |
|---|---|
runs/quickstart/trajectory.csv |
Saved positions, circulations, and velocities |
runs/quickstart/diagnostics.csv |
Invariants and conservation drift |
runs/quickstart/checkpoints/ |
Restart checkpoints |
Every simulation has one managed run directory. Missing directories are created automatically.
Existing solver output is protected; use a new runDirectory for another experiment, or set
overwriteRun true to replace the managed output in that directory.
params.txt is a larger periodic example with 400 vortices, adaptive integration,
and dipole removal/reinjection.
- Four geometries:
infinite,periodic_x,periodic, anddisk - Fixed-step classical RK4 and adaptive Dormand-Prince 5(4) integration
- CPU serial/OpenMP, MPI, and CUDA velocity backends
- Text initial conditions, geometry-aware generator, CSV output, and restart checkpoints
- Optional close dipole removal, with reinjection in bounded periodic and disk domains
- CMake and Make builds, numerical tests, and analysis/movie tools
.
├── src/ Solver, kernels, integrators, I/O, and backend implementations
├── initial_conditions/ Initial-condition generator and its guide
├── examples/ Small parameter files, including the quick start
├── tests/ C++ unit/audit and Python integration/backend tests
├── scripts/
│ ├── vortices.ipynb Vortex-configuration plotting notebook
│ ├── diagnostics.ipynb Invariants, drift, and dipole-event notebook
│ ├── movie_vortices.py Vortex-configuration MP4/GIF renderer
│ ├── point_vortex_plotting.py Shared readers and plotting helpers
│ ├── analysis/ Jupyter notebook for diagnostics and configuration figures
│ └── movie/ Legacy streaming CSV-to-MP4 renderer
├── runs/ Generated managed run output (ignored by Git)
├── CMakeLists.txt Primary cross-platform build configuration
├── Makefile Lightweight alternative build workflow
└── params.txt Full periodic-run example
| Area | Files | Responsibility |
|---|---|---|
| Program driver | main.cpp, print.cpp |
Loads a run, schedules output, and reports diagnostics |
| Model and diagnostics | vortex.h, compute.cpp/.h |
Vortex storage, velocity kernels, Hamiltonian, and invariants |
| Time integration | timestep.cpp/.h |
RK4 and adaptive DOPRI5 stepping |
| Execution backends | backend_cpu.cpp, backend_mpi.cpp, backend_cuda.cu, backend_common.cpp, backend.h |
CPU/OpenMP, MPI, CUDA, and shared backend interface |
| Configuration and input | params.h, read.cpp/.h |
Parameter parsing, validation, and initial-condition loading |
| Events and restart | dipole.cpp/.h, checkpoint.cpp/.h |
Dipole handling and versioned checkpoint I/O |
| Benchmark | benchmark.cpp |
Standalone infinite-plane kernel benchmark |
The velocity kernel is deliberately separate from the timestepper, so a new geometry or faster kernel can be added without rewriting the integrators.
The basic build attempts optional MPI and CUDA targets when their toolchains are installed; otherwise it still builds the CPU executable.
cmake -S . -B build/release -DCMAKE_BUILD_TYPE=Release
cmake --build build/release --parallel
ctest --test-dir build/release --output-on-failureUse a CPU-only build when MPI or CUDA should not be detected:
cmake -S . -B build/cpu -DCMAKE_BUILD_TYPE=Release \
-DPOINT_VORTEX_MPI=OFF -DPOINT_VORTEX_CUDA=OFFUseful configuration options:
| Option | Default | Effect |
|---|---|---|
POINT_VORTEX_OPENMP |
ON |
Enable OpenMP when the compiler supports it |
POINT_VORTEX_MPI |
ON |
Build point_vortex_mpi when MPI is found |
POINT_VORTEX_CUDA |
ON |
Build point_vortex_cuda when CUDA is found |
POINT_VORTEX_CUDA_ARCHITECTURES |
empty | Optional CUDA target, e.g. -DPOINT_VORTEX_CUDA_ARCHITECTURES=89 |
make # CPU/OpenMP executable and initial-condition generator
make test # C++ numerical tests
make test-output # Python output/restart integration tests
make mpi # MPI executable
make cuda # CUDA executable
make benchmark # Kernel benchmarkMake outputs are under build/make/; CMake outputs are under the selected build directory.
They are alternative ways to build the same source tree.
All backends receive a parameter file as their optional first argument; with no argument, they
use params.txt.
./build/release/point_vortex_cpu run.params
mpirun -n 4 ./build/release/point_vortex_mpi run.params
./build/release/point_vortex_cuda run.paramsStart with the CPU backend when checking a new input. The CPU executable uses OpenMP when it was
compiled with support and the population is sufficiently large. Set numThreads in the parameter
file, or use OMP_NUM_THREADS; a positive numThreads takes precedence.
MPI distributes target-vortex calculations across ranks while retaining the complete source state on each rank. Only rank zero writes output. In a hybrid MPI/OpenMP run, choose ranks × threads to fit the available CPU cores:
OMP_NUM_THREADS=8 mpirun -n 2 ./build/release/point_vortex_mpi run.paramsThe CUDA backend requires an NVIDIA GPU, driver, and CUDA toolkit. It keeps vortex state and RK4/DOPRI5 stages on the GPU between output events, avoiding per-stage host/device transfers. The host synchronizes state only for output, diagnostics, checkpoints, and enabled dipole processing. This uses additional GPU memory for the integration-stage buffers.
Each non-empty line is key value; # starts a comment. Keys are case-sensitive. Paths cannot
contain whitespace. Invalid or unknown settings stop the run rather than being ignored.
# Minimal two-vortex run
N 2
initialCondition dipole
boundaryCondition infinite
integrator rk4
timeStep 0.001
endTime 0.1
outputTime 0.02
runDirectory runs/my-two-vortex-case
Most-used settings:
| Setting | Values / default | Purpose |
|---|---|---|
initialCondition |
ring |
random, ring, single, dipole, or file |
N |
100 |
Population for random/ring; ignored for fixed, file, and checkpoint input |
boundaryCondition |
infinite |
infinite, periodic_x, periodic, or disk |
integrator |
dopri5 |
rk4 or adaptive dopri5 |
timeStep, endTime |
0.001, 1.0 |
Initial/fixed step and final simulation time |
outputTime |
0.1 |
Trajectory interval; final state is always saved |
diagnosticsTime, checkpointTime |
outputTime |
Optional independent output intervals |
coreRadius |
0.0 |
Infinite-plane regularization radius |
numThreads |
0 |
OpenMP thread count; zero defers to the runtime |
initialConditionFile |
unset | Required when initialCondition file; contains x y circulation rows |
dipoleRemoval |
false |
Enable dipole-removal events |
dipoleRemovalDistance |
0.01 |
Lower separation cutoff |
dipoleRemovalInterval |
0.0 |
Zero checks every accepted step; positive values set an independent time interval |
dipoleRemovalUpper |
false |
Also remove closest-first matched dipoles above the upper distance |
dipoleRemovalUpperDistance |
1.0 |
Upper separation cutoff; must exceed dipoleRemovalDistance |
dipoleReinjection |
none |
none, independent, or paired; bounded domains only |
restartFile |
unset | Checkpoint to restore; overrides the initial condition |
runDirectory |
runs/default |
Self-contained output root for this simulation |
overwriteRun |
false |
Replace this directory's managed solver output |
For a plane periodic only in periodic_x and set boxLengthX; boxLengthX, boxLengthY
(currently equal), and optionally periodicImageLayers; total circulation must be zero. For a
disk, set diskRadius; every vortex must remain strictly inside it. absoluteTolerance,
relativeTolerance, minimumTimeStep, and maximumTimeStep control adaptive DOPRI5. See the
commented params.txt for every supported setting, including dipole removal and
reinjection.
Vortex
where the kernel boundaryCondition.
For boundaryCondition infinite, the implemented regularized Biot–Savart
equations are
Here
For boundaryCondition periodic_x, the
Summing the infinite row of periodic images with
No image truncation or zero-total-circulation constraint is needed, so this
kernel costs
which is defined up to the usual circulation-dependent additive constant.
For boundaryCondition periodic, the current implementation requires
The truncated Weiss–McWilliams image sum used by the code is
The singular
For boundaryCondition disk, let
With
The second sum includes the vortex's own opposite-sign image and enforces an impermeable circular wall. The implementation evaluates this term in an algebraically equivalent form that stays finite when a source is at the disk center.
| Symbol | Parameter key | Meaning |
|---|---|---|
N |
Built-in initial vortex count | |
| third initial-condition column | Circulation of vortex |
|
coreRadius |
Infinite-plane regularization radius | |
boxLengthX, boxLengthY
|
Periodic lengths; periodic_x uses only |
|
periodicImageLayers |
Doubly periodic image-sum truncation | |
diskRadius |
Circular-domain radius | |
timeStep, endTime
|
Initial/fixed step and final time | |
| tolerances |
absoluteTolerance, relativeTolerance
|
Adaptive DOPRI5 error controls |
There is no continuous forcing or viscous damping term in these ODEs.
dipoleRemoval, dipoleRemovalDistance, and dipoleReinjection instead
define discrete population events. dipoleRemovalInterval selects their
schedule: zero checks after every accepted timestep, while a positive value
uses an independent physical-time interval. The integrator lands exactly on
scheduled removal times. These events can change circulation moments and the
Hamiltonian; they are recorded in the diagnostics and should not be interpreted
as part of the conservative point-vortex equations.
When dipoleRemovalUpper is enabled, the closest-first matching also removes
opposite-sign pairs farther apart than dipoleRemovalUpperDistance. This models
large-scale dissipation when like-signed vortices have clustered into separated
regions. The same dipoleReinjection setting applies to lower- and upper-cutoff
events. Diagnostics report their combined count in removed_pairs and the
upper-cutoff subset in removed_upper_pairs.
Without removal events, circulation and Hamiltonian are reported for every
geometry. The diagnostics additionally treat both linear impulses as relevant
for infinite, periodic_x, and periodic, and angular impulse as relevant
for infinite and disk. CSV files retain every invariant column so their
schema remains identical across geometries; console and notebook displays
select the geometry-appropriate subset.
Built-in initial conditions are geometry-aware. random samples inside the periodic box or disk
(and uses finite sampling extents in unbounded directions), while ring, single, and dipole
use geometry-appropriate scales. Every generated state is checked against the selected domain.
Provide a plain text file with one x y circulation row per vortex (whitespace or commas are
accepted), then select file and set initialConditionFile:
# initial.dat
-1.0, 0.0, 1.0
1.0, 0.0, -1.0
initialCondition file
initialConditionFile initial.dat
File-loaded states are validated as well: periodic coordinates must lie in the fundamental box, disk coordinates must lie strictly inside the circle, and fully periodic states must have zero total circulation. Invalid input stops before the run begins.
Or build and use the generator:
cmake --build build/release --target point_vortex_initial --parallel
./build/release/point_vortex_initial \
--geometry periodic --case random --count 400 --seed 20261376 \
--box-length 2 --min-separation 0.01 --output runs/periodic_n400/initial_n400.datThe generator supports all four geometries and the single, pair, dipole, ring, and
random cases, records geometry
metadata, and refuses incompatible solver settings. Full options and examples are in
initial_conditions/README.md.
Every simulation writes to a self-contained run directory. runDirectory defaults to
runs/default; give each experiment a descriptive directory:
runDirectory runs/periodic_n400
The solver creates this fixed layout, which keeps each experiment's outputs together.
runs/periodic_n400/
├── trajectory.csv
├── diagnostics.csv
├── checkpoints/
├── resolved_parameters.txt
└── segments/
└── segment_00000001/resolved_parameters.txt
resolved_parameters.txt records the validated settings, resolved output paths, selected
backend, and available runtime details (OpenMP threads, MPI ranks, or CUDA device). Every fresh
run, restart, or branch gets a new numbered segment record, retaining the provenance of the
invocation even when the top-level record is updated. A run directory that already contains solver
output is rejected by default. Set overwriteRun true only when intentionally replacing its
trajectory, diagnostics, checkpoints, and provenance records; unrelated files in that directory
are not removed.
Every backend writes the same portable formats:
| Output | Location | Notes |
|---|---|---|
| Trajectory | runDirectory/trajectory.csv |
time,frame,index,x,y,circulation,u,v rows |
| Diagnostics | runDirectory/diagnostics.csv |
Invariants, drift, and dipole-event counts |
| Checkpoints | runDirectory/checkpoints/checkpoint_*.dat |
Versioned restart state |
Trajectory, diagnostics, and checkpoint intervals are simulation time, not wall-clock time. The solver always saves the initial and final states.
To branch from a checkpoint, create a new parameter file with a new run directory:
restartFile runs/periodic_n400/checkpoints/checkpoint_00000005.dat
endTime 2.0
runDirectory runs/periodic_n400_branch
The geometry, integrator, core radius, and dipole settings must match the checkpoint. The
restart begins new CSV files; it does not append to the source trajectory. frame is a
monotonically increasing output-event identifier stored in trajectory, diagnostics, and
checkpoints. It lets analysis join streams reliably when their independent schedules coincide;
gaps in an individual CSV mean that stream was not scheduled at that event.
The plotting notebooks read a complete run directory, infer its geometry and box from
resolved_parameters.txt, and write figures beneath that run by default. Open the notebooks and
edit their clearly marked Configuration cells:
jupyter lab scripts/vortices.ipynb scripts/diagnostics.ipynb
python3 scripts/movie_vortices.py --run-dir runs/periodic_n400 \
--output runs/periodic_n400/figures/vortices.mp4The configuration notebook can override the geometry, the singly periodic length, square or
rectangular periodic display box, disk radius, and explicit viewing limits. The movie exposes
equivalent command-line options.
Frame selection, GIF/MP4 output, diagnostics ranges, smoothing, dependencies, and more examples
are documented in scripts/README.md. The original analysis notebook and
movie entry point remain under scripts/analysis/ and scripts/movie/ for compatibility.
Run the CMake test suite after changes to solver code:
ctest --test-dir build/release --output-on-failureOptional backend comparisons require the corresponding executable/toolchain:
python3 tests/backend_consistency.py \
./build/release/point_vortex_cpu ./build/release/point_vortex_cuda
python3 tests/backend_consistency.py \
./build/release/point_vortex_cpu mpirun -n 2 ./build/release/point_vortex_mpitests/tests.cpp covers core numerical behavior; tests/audit_tests.cpp targets numerical edge
cases; tests/output_integration.py covers output and restart workflows. The analysis and movie
smoke tests can be run with python3 tests/tooling_tests.py build/release when their Python
dependencies are installed.
- Velocity evaluation is direct
O(N^2)(O(N^2 M)for the doubly periodic image sum); MPI still replicates the source arrays on each rank. - CUDA keeps velocity evaluation and RK4/DOPRI5 integration on the device; diagnostics and file I/O remain host-side, and the stage buffers increase GPU-memory use.
- Doubly periodic dynamics currently requires a square, zero-net-circulation domain;
periodic_xhas neither restriction on the unbounded direction nor a neutrality requirement. - Dipole reinjection is not defined for the unbounded
infiniteandperiodic_xgeometries. - Core regularization is available only for the infinite plane.
- Singular encounters, disk-boundary violations, and non-finite states stop the run.
Copyright (c) 2022–2026 Jason Laurie. This project is distributed under the BSD 3-Clause License. Third-party dependencies remain subject to their own license terms.
If this software contributes to research or a publication, please cite it
using the metadata in CITATION.cff.