Fast McKean-Vlasov particle simulation. Rust core, Python API.
mvkit simulates systems of interacting particles whose dynamics depend on the empirical distribution of the population, i.e. mean-field SDEs of McKean-Vlasov type:
When mvkit lets you simulate such systems efficiently from Python while keeping the heavy lifting in Rust.
Early alpha (v0.1). The current scope:
- Generic mean-field SDE trait (
MeanFieldSDE) with state-dependent diagonal diffusion - Euler-Maruyama and Milstein integrators with Rayon-parallel particle updates. Selected via
scheme="euler"(default) orscheme="milstein"on everysimulate_*entry point. On constant-diffusion models, Milstein reduces to Euler exactly because the diffusion derivative is zero; the two schemes produce bit-exact identical trajectories there. - Built-in Cucker-Smale flocking model (any spatial dimension)
- Built-in linear-quadratic McKean-Vlasov model with closed-form Gaussian moments, used as a quantitative weak-order benchmark for the integrator
- Built-in Kuramoto model of coupled phase oscillators with O(N) drift via the order-parameter trick
- Built-in mean-field Cox-Ingersoll-Ross model with square-root diffusion: the first model with non-trivial state-dependent diffusion, used to exercise the Milstein correction term
- New
mvkit.mfgsub-module: scalar and vector linear-quadratic Mean Field Game solvers (solve_lq_mfg,solve_lq_mfg_vector) via Picard iteration, with closed-form (matrix) Riccati and (matrix) Lyapunov covariance benchmarks. The vector solver handles arbitrary state dimension with full Q, R, Sigma matrices and recovers cross-coordinate correlations from non-diagonal Sigma. -
Brownian-increment hook on every
simulate_*function (increments=keyword) plusmvkit.brownianhelpers (generate_increments,coarsen), enabling pathwise (strong) error tests by driving coarse and fine simulations from the same Brownian path. Recovers the textbook strong orders (Euler 1/2, Milstein 1) on multiplicative-noise CIR. - Standalone HJB grid solver (
mvkit.mfg.solve_hjb): backward Hamilton-Jacobi-Bellman on a 1D periodic grid with Engquist-Osher upwind for the quadratic Hamiltonian and implicit-explicit time stepping. First step toward a generic non-LQ MFG solver. Validated quantitatively via the Hopf-Cole closed form ($u = -\sigma^2 \log v$ linearizes to backward heat) plus monotonicity and self-convergence tests. - Standalone Fokker-Planck grid solver (
mvkit.mfg.solve_fokker_planck): forward FP on the same periodic grid with conservative-form upwind convection and implicit central diffusion. Mass-preserving by construction (errors of order$10^{-13}$ over thousands of steps). Validated against the closed-form translate-and-decay of a Fourier-mode initial under constant drift; convergence rate matches the first-order theoretical prediction. -
Generic grid-based MFG solver (
mvkit.mfg.solve_mfg) on top of the HJB and FP grids: a user-definedMFGProblem(running cost$F(t, x, m)$ , terminal cost$g(x, m_T)$ , initial density,$\sigma$ ,$T$ , spatial domain) is solved via Picard or Fictitious Play outer iteration. Periodic and Neumann (no-flux) boundary conditions are both supported: periodic identifies endpoints (natural for ring/torus geometries); Neumann enforces$\partial_x u = 0$ on the value function and$J = 0$ on the FP flux (natural for problems on a closed interval, conserves mass exactly). Validated against the closed-form LQ-MFG in both the symmetric (periodic-friendly) and asymmetric (Neumann-only) settings. Seeexamples/mfg_grid_demo.pyfor a non-LQ congestion problem. -
2D grid-based MFG solver (
mvkit.mfg.solve_mfg_2d) extending the 1D pipeline to scalar state in$\mathbb{R}^2$ : per-axis Engquist-Osher upwind for the Hamiltonian$H(\nabla u) = \tfrac{1}{2}|\nabla u|^2 - F$ , conservative-form upwind FP, 2D discrete Laplacian via Kronecker sum factored once with sparse LU. Periodic and Neumann BC, isotropic or anisotropic per-axis diffusion viasigma=(sigma_x, sigma_y). Validated against the closed-form vector LQ-MFG (solve_lq_mfg_vectorat$d = 2$ ) in both isotropic and anisotropic settings: the equilibrium Lyapunov covariance trajectory is recovered at first order under joint refinement. - Reproducible seeded RNG (Xoshiro256++)
- PyO3 bindings, abi3 wheels for Python 3.9+
Roadmap (non-binding) for v0.2 and beyond: tamed schemes for super-linear drift, kernel-based interactions via FFT, non-quadratic Hamiltonians for the 2D grid solver,
git clone https://github.com/TriGo06/MVKit
cd mvkit
pip install maturin
maturin develop --releaseOnce a release is published:
pip install mvkitimport numpy as np
from mvkit import simulate_cucker_smale
rng = np.random.default_rng(0)
n, d = 500, 2
x0 = np.empty((n, 2 * d))
x0[:, :d] = rng.normal(scale=2.0, size=(n, d)) # initial positions
x0[:, d:] = rng.normal(scale=1.5, size=(n, d)) # initial velocities
history = simulate_cucker_smale(
x0,
t_final=10.0,
n_steps=2000,
spatial_dim=d,
beta=0.4,
sigma=0.05,
record_every=20,
seed=42,
)
print(history.shape) # (101, 500, 4)See examples/cucker_smale_demo.py for a runnable visualization.
Each particle has scalar state with dynamics
where
Per particle the state is
with kernel
Each particle has a scalar phase
where
with
The drift is implemented in
Reference: Kuramoto, Y. (1975). Self-entrainment of a population of coupled non-linear oscillators. International Symposium on Mathematical Problems in Theoretical Physics.
See examples/kuramoto_demo.py for a runnable visualization showing phase trajectories and
Each particle has scalar state
where
The truncation
In the limit
A note on Milstein's improvement. Milstein has strong order 1 (vs Euler's strong order 1/2), but on weak error of smooth functionals of
References: Cox, J. C., Ingersoll, J. E., and Ross, S. A. (1985). A theory of the term structure of interest rates. Econometrica 53, 385-407. McKean-Vlasov extensions are standard, see Carmona and Delarue (2018).
For a McKean-Vlasov SDE with i.i.d. initial conditions, Sznitman's classical result (1991) says the empirical measure
mvkit.poc provides a small utility that takes any 1D mean-field simulator wrapped as (n_particles, seed) -> 1D array of terminal-time states, sweeps N, computes the exact 1D Wasserstein-2 distance to a reference inverse CDF on each run, and fits the log-log slope of the median against
import numpy as np
from scipy.stats import norm
from mvkit import simulate_linear_quadratic
from mvkit.poc import estimate_propagation_of_chaos_rate
a, b, sigma, T = -0.5, 1.0, 0.5, 1.0
m_0, v_0 = 0.0, 1.0
m_T = m_0 * np.exp((a + b) * T)
v_T = v_0 * np.exp(2 * a * T) + sigma**2 * (np.exp(2 * a * T) - 1) / (2 * a)
def simulator(n, seed):
rng = np.random.default_rng(seed)
x0 = rng.normal(m_0, np.sqrt(v_0), size=(n, 1))
h = simulate_linear_quadratic(x0, T, 1000, a=a, b=b, sigma=sigma, seed=seed)
return h[-1, :, 0]
result = estimate_propagation_of_chaos_rate(
simulator=simulator,
reference_inv_cdf=norm(loc=m_T, scale=np.sqrt(v_T)).ppf,
n_values=[100, 300, 1000, 3000, 10000],
n_seeds=16,
)
print(result.fitted_slope) # should be ~ -0.5See examples/poc_rate_lq.py for a runnable two-panel figure showing the log-log fit and the empirical-vs-analytical CDF overlay at the largest
Scope. Currently 1D only (LinearQuadratic, Kuramoto via the order parameter, MeanFieldCIR). Cucker-Smale state is 4D, which requires sliced or projected Wasserstein and is on the v0.2 roadmap.
Every simulate_* function accepts an optional increments keyword: a 3D array of standard normals of shape (n_steps, N, dim) that the integrator uses in place of internal sampling. With it you can drive two simulations at different n_steps from the same Brownian path, which makes pathwise (strong) error well-defined. The mvkit.brownian module ships two helpers:
-
generate_increments(n_steps, n_particles, dim, seed)for drawing a fresh fine grid. -
coarsen(fine_increments, factor)that aggregates onto a coarser grid via the variance-preserving bridge relation $Z^{\text{coarse}}k = \tfrac{1}{\sqrt f}\sum{j=0}^{f-1} Z^{\text{fine}}_{f k + j}$.
Combined, they reproduce the textbook strong orders on multiplicative-noise SDEs:
import numpy as np
from mvkit import simulate_mean_field_cir
from mvkit.brownian import generate_increments, coarsen
N, T, n_fine = 5000, 1.0, 4096
x0 = np.full((N, 1), 0.04)
Z_fine = generate_increments(n_steps=n_fine, n_particles=N, dim=1, seed=0)
ref = simulate_mean_field_cir(x0, T, n_fine, kappa=1.0, theta=0.04, b=0.0,
sigma=0.2, increments=Z_fine, scheme="milstein")
X_ref = ref[-1, :, 0]
errs, dts = [], []
for n_steps in [32, 64, 128, 256, 512, 1024]:
Z_n = coarsen(Z_fine, factor=n_fine // n_steps)
h = simulate_mean_field_cir(x0, T, n_steps, kappa=1.0, theta=0.04, b=0.0,
sigma=0.2, increments=Z_n, scheme="euler")
errs.append(np.sqrt(np.mean((h[-1, :, 0] - X_ref) ** 2)))
dts.append(T / n_steps)
slope, _ = np.polyfit(np.log(dts), np.log(errs), 1)
print(slope) # ~ 0.55, the Euler strong order on multiplicative noiseSee examples/strong_error_demo.py for the two-panel figure showing Euler vs Milstein on CIR.
A Mean Field Game (Lasry and Lions, 2007) is a Cournot-Nash equilibrium for a continuum of identical agents: each agent chooses a control to minimize a personal cost that depends on the population's distribution, and at equilibrium the distribution generated by every agent's optimal response coincides with the input distribution. Numerically, finding an equilibrium amounts to solving a coupled forward-backward system: a Hamilton-Jacobi-Bellman PDE for the value function
mvkit.mfg is a new sub-module dedicated to numerical MFG. The first release ships the scalar linear-quadratic case, where the HJB ansatz method keyword:
-
method="picard"(default):$m^{(k+1)} = \mathrm{BR}(m^{(k)})$ . Geometric convergence when the BR map is a contraction (LQ-MFG with moderate$\int_0^T P$ ). Typically 5 to 10 iterations to$10^{-4}$ . -
method="fictitious_play"(also exposed assolve_lq_mfg_fictitious_play):$m^{(k+1)} = \mathrm{BR}(\bar m^{(k)})$ where$\bar m^{(k)}$ is the historical average of past iterates. Cardaliaguet and Hadikhanloo (2017) prove$O(1/k)$ convergence under MFG monotonicity, without requiring strict contraction. Slower than Picard on LQ ($\sim$ 30 to 50 iterations) but the natural choice once monotonicity is the only structure available.
A damping_burn_in parameter on the FP solver runs leading Picard steps before starting to accumulate the historical average, which speeds up the tail when the initial guess is far from the equilibrium.
| Use | Function | Algorithm |
|---|---|---|
| Cost is exactly LQ ( |
mvkit.mfg.solve_lq_mfg |
Particle Picard (or Fictitious Play) on the mean trajectory; analytical Riccati for |
| Cost is non-LQ, scalar 1D state | mvkit.mfg.solve_mfg |
Grid HJB and Fokker-Planck with Picard / FP outer iteration |
The two solvers agree on LQ within first-order discretization error of the grid solver; we use that as a regression check for the grid pipeline. Use solve_lq_mfg for benchmarks and quick scalar LQ studies (no spatial discretization error in the value function), and solve_mfg whenever the cost is non-LQ.
import numpy as np
from mvkit.mfg import solve_lq_mfg
sol = solve_lq_mfg(
q=1.0, q_T=0.5, sigma=0.5, T=1.0,
mu_0_mean=1.5, mu_0_var=1.0,
n_particles=10_000, n_grid=200, seed=0,
)
print("converged:", sol.converged, "n_iterations:", sol.n_iterations)
print("max |m - m_0|:", np.max(np.abs(sol.m - 1.5))) # equilibrium mean is constant
# Compare empirical terminal variance to the analytical V(T).
V_T_emp = sol.x_trajectory[-1].var()
print(f"V(T) empirical = {V_T_emp:.4f}, analytical = {sol.V[-1]:.4f}")The closed-form construction follows Carmona and Delarue (2018), Probabilistic Theory of Mean Field Games with Applications I, Section 3.5. The Fictitious Play scheme follows Cardaliaguet and Hadikhanloo (2017). See examples/mfg_lq_demo.py for a three-panel figure (examples/mfg_lq_picard_vs_fp.py for a side-by-side log-y convergence trace of both methods.
Roadmap. The non-LQ MFG pipeline is end-to-end in 1D and 2D, with periodic + Neumann BC, and is validated against closed-form LQ-MFG references in each dimension. The 1D HJB solver accepts an arbitrary convex Hamiltonian: solve_hjb and MFGProblem take a Hamiltonian, with power_hamiltonian(q) covering the H(p) = |p|^q / q family. The generalized Engquist-Osher scheme stays explicit, so no per-cell nonlinear solve is needed. Next: non-quadratic Hamiltonians for the 2D grid solver (where the Engquist-Osher form is no longer separable),
Criterion benchmarks track Euler-Maruyama throughput in particle-steps per second. Run them from the workspace root:
cargo bench --bench integratorsEach suite reports throughput via Throughput::Elements(N * n_steps) so Criterion prints the unit directly. HTML reports land under target/criterion/ and are gitignored alongside the rest of target/. Indicative numbers on an Apple-silicon laptop, single process:
- Linear-quadratic (cheap drift): ~14 M particle-steps/s at N=1000, ~96 M at N=10000, ~316 M at N=100000. The sub-linear region at small N is dominated by sequential noise sampling and parallel-launch overhead; once N is large enough to amortize that, the integrator scales near-linearly with the rayon pool.
- Cucker-Smale (O(N^2) pairwise drift): ~940 K at N=100, ~460 K at N=500, ~130 K at N=2000. The drift cost dominates once N grows; an FFT-convolution path for translation-invariant kernels is on the roadmap.
The benchmarks are not part of CI by default; they are too noisy on shared GitHub runners. A manual workflow at .github/workflows/bench.yml (triggered via the Actions tab) runs them on ubuntu-latest and uploads the HTML report as an artifact.
mvkit/
├── crates/
│ ├── mvkit-core/ # pure Rust: traits, integrators, models
│ └── mvkit-py/ # PyO3 bindings, no business logic
├── python/mvkit/ # Python package, wraps _core
│ ├── poc.py # propagation-of-chaos rate utility
│ ├── brownian.py # increment helpers for strong-error tests
│ └── mfg/ # Mean Field Games sub-module (LQ for now)
├── tests/ # pytest test suite
└── examples/ # runnable demos
The split between mvkit-core and mvkit-py keeps the FFI surface thin and lets the core crate be reused from pure Rust. The Python wrapper layer adds input validation and ergonomic defaults without touching the Rust ABI.
PRs welcome. Before submitting, please run:
cargo test --workspace
cargo clippy --workspace -- -D warnings
cargo fmt --all -- --check
maturin develop
pytest- Cucker, F. and Smale, S. (2007). Emergent behavior in flocks. IEEE Trans. Automatic Control.
- Sznitman, A.-S. (1991). Topics in propagation of chaos. Ecole d'Eté de Probabilités de Saint-Flour XIX.
- Carmona, R. and Delarue, F. (2018). Probabilistic Theory of Mean Field Games with Applications I & II. Springer.
- Lasry, J.-M. and Lions, P.-L. (2007). Mean field games. Japanese Journal of Mathematics 2, 229-260.
- Cardaliaguet, P. and Hadikhanloo, S. (2017). Learning in mean field games: the fictitious play. ESAIM: Control, Optimisation and Calculus of Variations 23, 569-591.
Dual-licensed under either of:
- MIT license (LICENSE-MIT)
- Apache License 2.0 (LICENSE-APACHE)
at your option.