Numerical Solver#
The soliton_solver implements a theory-agnostic numerical solver for 2D PDEs describing topological solitons. This document describes the finite-difference method, the arrested Newton flow minimization algorithm, and solver configuration.
Overview#
The numerical core solves the relaxation equation:
where \(E_h[\phi]\) is a discretized energy functional and \(\phi\) is the field configuration. The goal is to find local minima of \(E_h\).
Key features:
Second-order relaxation — Velocity helps escape shallow regions of the energy landscape
Flow arrest — Velocity resets if energy increases, preventing oscillations
Theory-agnostic — Core solver doesn’t know specific physics
GPU-accelerated — All kernels run on NVIDIA GPUs via Numba CUDA
Finite Difference Method#
Fields are discretized on a 2D Cartesian grid with uniform spacing. Spatial derivatives are computed via fourth-order central finite differences.
Grid setup#
The computational domain is discretized into \(n_x \times n_y\) lattice points:
Domain: \([0, L_x] \times [0, L_y]\) in physical space
Grid points: \(n_x \times n_y\) lattice sites
Spacing: \(\Delta x = L_x / n_x\), \(\Delta y = L_y / n_y\)
Halo: Additional boundary points for stencil computations
Parameters:
params = theory.params.default_params(
xlen=256, # Number of lattice points x
ylen=256, # Number of lattice points y
xsize=80.0, # Physical domain size x
ysize=80.0, # Physical domain size y
halo=2, # Halo width (default: 2)
)
rp = params.resolved()
print(f"Lattice spacing: dx={rp.lsx}, dy={rp.lsy}")
print(f"Grid volume: {rp.grid_volume}")
Halo width — Boundary region used by finite-difference stencils. For fourth-order stencils with ±2 offset, halo=2 is standard.
Stencils#
First derivatives#
Fourth-order central difference for \(\partial_x f\):
Implementation (from derivatives.py):
@cuda.jit(device=True)
def compute_derivative_first(d1fd1x, Field, a, x, y, p_i, p_f):
"""Compute first derivatives at (x, y) for field component a."""
xlen = p_i[0]
ylen = p_i[1]
halo = p_i[2]
lsx = p_f[2] # Lattice spacing
# Check interior (not in halo)
if x > halo - 1 and x < xlen - halo:
d1fd1x[idx_d1(0, a, x, y, p_i)] = (
(1.0/12.0) * Field[idx_field(a, x-2, y, p_i)]
- (2.0/3.0) * Field[idx_field(a, x-1, y, p_i)]
+ (2.0/3.0) * Field[idx_field(a, x+1, y, p_i)]
- (1.0/12.0) * Field[idx_field(a, x+2, y, p_i)]
) / lsx
else:
d1fd1x[idx_d1(0, a, x, y, p_i)] = 0.0
Accuracy — Fourth-order: error \(\mathcal{O}(\Delta x^4)\)
Second derivatives#
Fourth-order Laplacian \(\partial_{xx} f\):
Similarly for \(\partial_{yy}\) and mixed derivatives \(\partial_{xy}\) (via chain rule from diagonals).
Boundary conditions#
Fields satisfy Dirichlet boundary conditions on the computational domain. The halo region fixes boundary values.
For a field with halo=2:
Interior points:
2 ≤ x < nx - 2Boundary points:
0 ≤ x < 2ornx - 2 ≤ x < nx
Stability and accuracy#
Stability criterion — The Courant condition for explicit time-stepping:
where \(C\) is a theory-dependent constant (default Courant number ≈ 0.5).
Accuracy — Overall fourth-order in space, second-order in time (RK4).
Arrested Newton Flow Algorithm#
ANF is a second-order relaxation scheme combining fast convergence with stability.
Algorithm description#
The continuous ANF equation is:
This is a system of ODEs in velocity \(v = \dot{\phi}\) and position \(\phi\):
Arrest rule — If energy increases, velocity is reset:
Convergence properties#
Second-order dynamics — Velocity accelerates motion; useful for escaping shallow regions
Energy stable — Arrest prevents oscillations; energy is non-increasing (except at resets)
Faster than gradient descent — Particularly for multi-soliton configurations
Convergence criterion — Stop when \(\|\nabla E\|_\infty < \epsilon\) (convergence tolerance)
GPU implementation: RK4 integration#
One full ANF step consists of:
Compute gradient — \(\nabla E\) at all grid points via finite differences
RK4 step — Advance \((φ, v)\) using fourth-order Runge-Kutta
Arrest check — Reset velocity if energy increases
Convergence check — Test \(\|\nabla E\|_\infty\) against tolerance
RK4 stages#
The RK4 method for \(\dot{\mathbf{y}} = \mathbf{f}(t, \mathbf{y})\) is:
For the ANF system, \(\mathbf{y} = (\phi, v)\) and:
The implementation launches four gradient computation kernels (one per RK4 stage) and updates field and velocity buffers.
Solver Configuration#
The solver is configured via the Params and ResolvedParams classes.
Core parameters#
Grid and domain:
Params(
xlen=256, # Grid points in x
ylen=256, # Grid points in y
xsize=80.0, # Physical size in x
ysize=80.0, # Physical size in y
halo=2, # Halo width for stencils
)
Time stepping:
Params(
time_step=0.01, # Explicit time step (if set)
courant=0.5, # Courant number for auto time stepping
)
If time_step is None, it is computed from the Courant number:
Arrested Newton flow:
Params(
newtonflow=True, # Enable Newton flow (vs. gradient descent)
killkinen=True, # Reset velocity on energy increase
)
Constraints:
Params(
unit_magnetization=False, # Enforce unit norm on magnetization (some theories)
)
Resolved parameters#
After calling .resolved(), derived parameters are computed:
rp = params.resolved()
print(rp.xlen, rp.ylen) # Grid dimensions
print(rp.lsx, rp.lsy) # Lattice spacings
print(rp.grid_volume) # Cell area
print(rp.time_step) # Actual time step
print(rp.dim_grid) # Total grid points
print(rp.dim_fields) # Total field values
Example configuration#
from soliton_solver.theories import load_theory
theory = load_theory("Chiral magnet")
# Create base parameters
params = theory.params.default_params(
xlen=512, ylen=512, # Finer grid
xsize=20.0, ysize=20.0, # Larger domain
courant=0.3, # Conservative time step
newtonflow=True,
killkinen=True,
)
# Resolve and create simulation
sim = Simulation(params, theory)
sim.initialize({"mode": "ground"})
# Relax via arrested Newton flow
from soliton_solver.core.simulation import Simulation
energy = sim.observables_mod.compute_energy(
sim.Field, sim.d1fd1x, sim.en, sim.entmp,
sim.gridsum_partial, sim.p_i_d, sim.p_f_d,
sim.p_i_h, sim.p_f_h
)
# Advance one step
new_energy, err = sim.step(prev_energy=energy)
print(f"Energy: {new_energy}, Gradient norm: {err}")
Performance considerations#
Time step selection#
Too small — Slow convergence, many unnecessary steps
Too large — Instability, divergence, energy oscillations
The Courant condition provides automatic stability; manual override via time_step should be conservative.
Grid resolution#
Coarse grids — Fast but inaccurate; may miss fine soliton details
Fine grids — Accurate but slow; scales as \(O(n_x n_y)\) per step
Recommended: start coarse, refine around features of interest.
Convergence tolerance#
Typically \(10^{-4}\) to \(10^{-6}\) depending on application:
# Run until convergence
energy = initial_energy
for step in range(max_steps):
new_energy, err = sim.step(energy)
energy = new_energy
if err < 1e-4:
print(f"Converged in {step} steps")
break
if step % 100 == 0:
print(f"Step {step}: E={energy:.6e}, ||∇E||_∞={err:.6e}")
Memory usage#
Field arrays are stored in GPU device memory:
For multiple fields and buffers (Field, Velocity, derivatives, RK4 stages):
Example: 1024×1024 grid with 4 fields ≈ 80 MB on GPU.
Typical runtime#
A single RK4 step on NVIDIA A100:
512×512, 2–4 fields: ~1–2 ms
1024×1024, 4–8 fields: ~5–10 ms
Convergence typically requires 1,000–10,000 steps depending on configuration complexity.