# Kapfer Krauth 2016

S. C. Kapfer, W. Krauth Cell-veto Monte Carlo algorithm for long-range systems Physical Review E 94, 031302(R) (2016)

# Paper

Abstract We present a rigorous efficient event-chain Monte Carlo algorithm for long-range interacting particle systems. Using a cell-veto scheme within the factorized Metropolis algorithm, we compute each single-particle move with a fixed number of operations. For slowly decaying potentials such as Coulomb interactions, screening line charges allow us to take into account periodic boundary conditions. We discuss the performance of the cell-veto Monte Carlo algorithm for general inverse-power-law potentials, and illustrate how it provides a new outlook on one of the prominent bottlenecks in large-scale atomistic Monte Carlo simulations.

# Illustration

The cell-veto algorithm is an application of the event-chain paradigm that was developed in 2009, with E. P. Bernard and D. B. Wilson, and much extended together with M. Michel and S. C. Kapfer, where we introduced the factorized Metropolis algorithm. In this algorithm, unlike in 99.9% of all simulation algorithms (Monte Carlo or Molecular dynamics), one does not compute the system energy in order to decide on a change of the physical system, but rather looks at all the interactions separately. So, if a particle a (the active particle) wants to move, it has to ask all its partners t_1, t_2, .... (the target particles). If there is only a single veto, the move is rejected. In the cell-veto algorithm (see the right side of the figure), the identification of the rejecting particle is preceeded by that of a veto cell. The advantage of this is that cell vetos can be identified immediately (in a constant number of operations, that is, in O(1)), and then instantly confirmed or infirmed on the particle level.

# Python implementation

The paper contains a Python demo implementation of the cell-veto algorithm (that is also available on github).

``` import math, random, sys
import numpy as np

def norm (x, y):
"""norm of a two-dimensional vector"""
return (x*x + y*y) ** 0.5

def dist (a, b):
"""periodic distance between two two-dimensional points a and b"""
delta_x = (a - b + 2.5) % 1.0 - 0.5
delta_y = (a - b + 2.5) % 1.0 - 0.5
return norm (delta_x, delta_y)

def random_exponential (rate):
"""sample an exponential random number with given rate parameter"""
return -math.log (random.uniform (0.0, 1.0)) / rate

def pair_event_rate (delta_x, delta_y):
"""compute the particle event rate for the 1/r potential in 2D (lattice-screened version)"""
q = 0.0
for ky in range (-k_max, k_max + 1):
for kx in range (-k_max, k_max + 1):
q += (delta_x + kx) / norm (delta_x + kx, delta_y + ky) ** 3
q += 1.0 / norm (delta_x + k_max + 0.5, delta_y + ky)
q -= 1.0 / norm (delta_x - k_max - 0.5, delta_y + ky)
return max (0.0, q)

def translated_cell (target_cell, active_cell):
"""translate target_cell with respect to active_cell"""
kt_y = target_cell // L
kt_x = target_cell  % L
ka_y = active_cell // L
ka_x = active_cell  % L
del_x = (kt_x + ka_x) % L
del_y = (kt_y + ka_y) % L
return del_x + L*del_y

def cell_containing (a):
"""return the index of the cell which contains the point a"""
k_x = int (a * L)
k_y = int (a * L)
return k_x + L*k_y

def walker_setup (pi):
"""compute the lookup table for Walker's algorithm"""
N_walker = len(pi)
walker_mean = sum(a for a in pi) / float(N_walker)
long_s = []
short_s = []
for p in pi:
if p > walker_mean:
long_s.append (p[:])
else:
short_s.append (p[:])
walker_table = []
for k in range(N_walker - 1):
e_plus = long_s.pop()
e_minus = short_s.pop()
walker_table.append((e_minus, e_minus, e_plus))
e_plus = e_plus - (walker_mean - e_minus)
if e_plus < walker_mean:
short_s.append(e_plus)
else:
long_s.append(e_plus)
if long_s != []:
walker_table.append((long_s, long_s, long_s))
else:
walker_table.append((short_s, short_s, short_s))
return N_walker, walker_mean, walker_table

def sample_cell_veto (active_cell):
"""determine the cell which raised the cell veto"""
# first sample the distance vector using Walker's algorithm
i = random.randint (0, N_walker - 1)
Upsilon = random.uniform (0.0, walker_mean)
if Upsilon < walker_table[i]:
veto_offset = walker_table[i]
else:
veto_offset = walker_table[i]
# translate with respect to active cell
veto_rate = Q_cell[veto_offset]
vetoing_cell = translated_cell (veto_offset, active_cell)
return vetoing_cell, veto_rate

N = 40
k_max = 3 # extension of periodic images.
chain_ell = 0.18  # displacement during one chain
L = 10  # number of cells along each dimension
density = N / 1.
cell_side = 1.0 / L
# precompute the cell-veto rates
cell_boundary = []
cb_discret = 10 # going around the boundary of a cell (naive)
for i in range (cb_discret):
x = i / float (cb_discret)
cell_boundary += [(x*cell_side, 0.0), (cell_side, x*cell_side),
(cell_side - x*cell_side, cell_side),
(0.0, cell_side - x*cell_side)]

excluded_cells = [ del_x + L*del_y for del_x in (0, 1, L-1) \
for del_y in (0, 1, L-1) ]
Q_cell = []

for del_y in xrange (L):
for del_x in xrange (L):
k = del_x + L*del_y
Q = 0.0
# "nearby" cells have no cell vetos
if k not in excluded_cells:
# scan the cell boundaries of both active and target cells
# to find the maximum of event rate
for delta_a in cell_boundary:
for delta_t in cell_boundary:
delta_x = del_x*cell_side + delta_t - delta_a
delta_y = del_y*cell_side + delta_t - delta_a
Q = max (Q, pair_event_rate (delta_x, delta_y))
Q_cell.append ([Q, k])

Q_tot = sum (a for a in Q_cell)
N_walker, walker_mean, walker_table = walker_setup (Q_cell)

# histogram for computing g(r)
hbins = 50
histo = np.zeros (hbins)
histo_binwid = .5 / hbins
hsamples = 0

# random initial configuration
particles = [ (random.uniform (0.0, 1.0), random.uniform (0.0, 1.0))
for _ in xrange (N) ]

for iter in xrange (250000):
if iter % 100 == 0:
print iter

# possibly exchange x and y coordinates for ergodicity
if random.randint(0,1) == 1:
particles = [ (y,x) for (x,y) in particles ]
# pick active particle for first move
active_particle = random.choice (particles)
particles.remove (active_particle)
active_cell = cell_containing (active_particle)
# put particles into cells
surplus = []
cell_occupant = [ None ] * L * L
for part in particles:
k = cell_containing (part)
if cell_occupant[k] is None:
cell_occupant[k] = part
else:
surplus.append (part)

# run one event chain
distance_to_go = chain_ell
while distance_to_go > 0.0:
planned_event_type = 'end-of-chain'
planned_displacement = distance_to_go
target_particle = None
target_cell = None

active_cell_limit = cell_side * (active_cell % L + 1)
if active_cell_limit - active_particle <= planned_displacement:
planned_event_type = 'active-cell-change'
planned_displacement = active_cell_limit - active_particle

delta_s = random_exponential (Q_tot)
while delta_s < planned_displacement:
vetoing_cell, veto_rate = sample_cell_veto (active_cell)
part = cell_occupant[vetoing_cell]
if part is not None:
Ratio = pair_event_rate (part - active_particle - delta_s, \
part - active_particle)           \
/ veto_rate
if random.uniform (0.0, 1.0) < Ratio:
planned_event_type = 'particle'
planned_displacement = delta_s
target_particle = part
target_cell = vetoing_cell
break
delta_s += random_exponential (Q_tot)

# compile the list of particles that need separate treatment
extra_particles = surplus[:]
for k in excluded_cells:
part = cell_occupant[translated_cell (k, active_cell)]
if part is not None:
extra_particles.append (part)

# naive version of the short-range code by discretization
delta_s = 0.0
short_range_step = 1e-3
while delta_s < planned_displacement:
for possible_target_particle in extra_particles:
# this supposes a constant event rate over the time interval
# [delta_s:delta_s+short_range_step]
q = pair_event_rate (possible_target_particle - active_particle - delta_s,
possible_target_particle - active_particle)
if q > 0.0:
event_time = random_exponential (q)
if event_time < short_range_step and delta_s + event_time < planned_displacement:
planned_event_type = 'particle'
planned_displacement = delta_s + event_time
target_particle = possible_target_particle
target_cell = cell_containing (target_particle)
break
delta_s += short_range_step

distance_to_go -= planned_displacement
new_x = active_particle + planned_displacement
active_particle = (new_x % 1.0, active_particle)

if planned_event_type == 'active-cell-change':
ac_x = (active_cell_limit + 0.5*cell_side) % 1.0
active_cell = cell_containing ([ac_x, active_particle])
active_particle = (active_cell % L * cell_side, active_particle)

elif planned_event_type == 'particle':
# remove newly active particle from store
if target_particle in surplus:
surplus.remove (target_particle)
else:
cell_occupant[target_cell] = None
# put the previously active particle in the store
if cell_occupant[active_cell] is not None:
surplus.append (active_particle)
else:
cell_occupant[active_cell] = active_particle
active_particle = target_particle
active_cell = cell_containing (active_particle)

# restore particles vector for x <-> y transfer
particles = [ active_particle ]
particles += [ part for part in cell_occupant if part is not None ]
particles += surplus
```
```    # form histogram for computing radial distribution function g(r)
for k in range (len (particles)):
for l in range (k):
ibin = int (dist (particles[k], particles[l]) / histo_binwid)
if ibin < len (histo):
histo[ibin] += 1
hsamples += 1

# compute g(r) from histogram
half_bin = .5 * histo_binwid
r = np.arange (0., hbins) * histo_binwid + half_bin
g_of_r = histo / density / hsamples * 2
g_of_r /= math.pi * ((r+half_bin)**2 - (r-half_bin)**2)
# save g(r)