Examples
Concrete, runnable examples that exercise the major code paths.
Open-BC Coulomb, 5 charges in 3D
using MultilevelSummation
using StaticArrays
using Random
Random.seed!(42)
positions = [SVector{3,Float64}((rand(3) .* 2)...) for _ in 1:5]
charges = randn(5) ./ sqrt(5)
cell = zero(SMatrix{3,3,Float64}) # unused for open BC
periodic = (false, false, false)
# Baseline: direct O(N²) Coulomb
using MultilevelSummation.Reference: naive_energy
K = Coulomb()
U_naive = naive_energy(positions, charges, cell, periodic, K)
# MSM with a fairly small a — splitting error is the dominant approximation here.
calc = MSMCalculator(HardyC2Cubic(2.0, 3), CubicC1(), 0.2)
U_msm = msm_energy(positions, charges, cell, periodic, calc)
(U_naive, U_msm, abs(U_msm - U_naive) / abs(U_naive))(-0.08508664602342171, -0.07634694482575935, 0.10271530970037993)Convergence in a
Holding h fixed and growing a should reduce the splitting error monotonically.
using MultilevelSummation, StaticArrays, Random
using MultilevelSummation.Reference: naive_energy
Random.seed!(0xC0FFEE)
positions = [SVector{3,Float64}((rand(3) .* 2)...) for _ in 1:8]
charges = randn(8); charges .-= sum(charges)/length(charges)
cell, periodic = zero(SMatrix{3,3,Float64}), (false, false, false)
U_naive = naive_energy(positions, charges, cell, periodic, Coulomb())
for a in (0.8, 1.6, 3.2, 6.4)
calc = MSMCalculator(HardyC2Cubic(a, 3), CubicC1(), 0.2)
U = msm_energy(positions, charges, cell, periodic, calc)
@show a, abs(U - U_naive) / abs(U_naive)
end(a, abs(U - U_naive) / abs(U_naive)) = (0.8, 0.07483594153438516)
(a, abs(U - U_naive) / abs(U_naive)) = (1.6, 0.013746410333744958)
(a, abs(U - U_naive) / abs(U_naive)) = (3.2, 0.002401370669336198)
(a, abs(U - U_naive) / abs(U_naive)) = (6.4, 0.0005538276368636638)Periodic 3D Coulomb
using MultilevelSummation, StaticArrays, Random
Random.seed!(7)
h, L = 0.5, 4 # 8 fine points per axis ⇒ top is 1×1×1
cell_L = h * 8
cell = SMatrix{3,3,Float64}(cell_L * one(SMatrix{3,3,Float64}))
periodic = (true, true, true)
N = 8
positions = [SVector{3,Float64}((rand(3) .* cell_L)...) for _ in 1:N]
charges = randn(N); charges .-= sum(charges)/N
calc = MSMCalculator(HardyC2Cubic(2.0, L), CubicC1(), h)
U, F = msm_energy_forces(positions, charges, cell, periodic, calc)
(U, sum(F)) # ΣF ≈ 0 expected(-1.772514777870421, [0.029026986702012814, -0.03124331469721546, 0.01615475477658279])Through the AtomsCalculators interface
using MultilevelSummation, AtomsBase, AtomsCalculators, Unitful, StaticArrays
L = 4.0u"Å"
cell_vec = (SVector(L, 0u"Å", 0u"Å"),
SVector(0u"Å", L, 0u"Å"),
SVector(0u"Å", 0u"Å", L))
sys = periodic_system([
Atom(:Na, SVector(1.0u"Å", 2.0u"Å", 0.5u"Å"); charge = +1.0),
Atom(:Cl, SVector(2.5u"Å", 1.5u"Å", 3.0u"Å"); charge = -1.0),
], cell_vec)
calc = MSMCalculator(HardyC2Cubic(2.0, 4), CubicC1(), 0.5)
ef = AtomsCalculators.energy_forces(sys, calc)
(ef.energy, ef.forces[1])(-0.5859876127899326, [0.0707274850556901, -0.03910912379936127, -0.0698562289975313])Mixed BC: periodic in xy, open in z
using MultilevelSummation, StaticArrays, Random
Random.seed!(11)
# 4×4 periodic in xy, open in z
L_xy = 4.0
cell = SMatrix{3,3,Float64}([L_xy 0 0; 0 L_xy 0; 0 0 1.0])
periodic = (true, true, false)
positions = [SVector(2.0, 1.5, 0.3), SVector(0.5, 3.2, -0.4)]
charges = [1.0, -1.0]
calc = MSMCalculator(HardyC2Cubic(2.0, 3), CubicC1(), 0.5)
msm_energy(positions, charges, cell, periodic, calc)-0.5355393303057003Naive Ewald reference
A naive 3D Ewald reference lives in the public MultilevelSummation.Reference submodule. Used in tests for periodic-Coulomb accuracy checks and as the reference inside MultilevelSummation.Tune.sweep:
using MultilevelSummation.Reference: ewald_energy, ewald_energy_forces
U = ewald_energy(positions, charges, cell; α=0.7, R_cut=10.0, k_cut=12.0)
U, F = ewald_energy_forces(positions, charges, cell; α=0.7, R_cut=10.0, k_cut=12.0)For auto-tuned parameters use the Tune wrapper:
using MultilevelSummation.Tune
U = Tune.ewald_reference(positions, charges, cell; tol = 1e-9)Both perform the standard erfc / reciprocal-Gaussian split and self- validate via α-invariance in test/test_ewald.jl.
Programmatic hyperparameter sweep
using MultilevelSummation
using MultilevelSummation.Tune
using StaticArrays, Random
Random.seed!(0xABCD)
L = 8.0; cell = SMatrix{3,3,Float64}(L * one(SMatrix{3,3,Float64}))
periodic = (true, true, true)
positions = [SVector{3,Float64}((rand(3) .* L)...) for _ in 1:32]
charges = randn(32); charges .-= sum(charges) / length(charges)
U_ref = Tune.ewald_reference(positions, charges, cell)
results = Tune.sweep(positions, charges, cell, periodic;
reference = U_ref,
h_values = (0.5, 1.0),
a_values = (1.0, 2.0),
L_strategy = :all)
length(results), Tune.recommend(results; max_rel_err = 0.1)(14, SweepResult(h=1, a=2, L=3, n_grid=8, energy=-5.105100e+00, rel_err=4.811e-02, t_msm=0.0012s))The shipped tuning/tune_NaCl.jl is a thin caller of this same API on realistic NaCl supercells — see tuning/README.md for how to run it.