Kapfer Krauth 2016

From Werner KRAUTH

Jump to: navigation, search

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.

Electronic version (from arXiv, including supplemental material in one pdf file)

Journal version (subscription required)

Supplemental material (subscription required)

Open-access repository of program(s) on github

Illustration

Here are pictures of three bosons on an permutation cycle.
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[0] - b[0] + 2.5) % 1.0 - 0.5
   delta_y = (a[1] - b[1] + 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[0] * L)
    k_y = int (a[1] * 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[0] for a in pi) / float(N_walker)
    long_s = []
    short_s = []
    for p in pi:
        if p[0] > 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[0], e_minus[1], e_plus[1]))
        e_plus[0] = e_plus[0] - (walker_mean - e_minus[0])
        if e_plus[0] < walker_mean:
            short_s.append(e_plus)
        else:
            long_s.append(e_plus)
    if long_s != []:
        walker_table.append((long_s[0][0], long_s[0][1], long_s[0][1]))
    else:
        walker_table.append((short_s[0][0], short_s[0][1], short_s[0][1]))
    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][0]:
        veto_offset = walker_table[i][1]
    else:
        veto_offset = walker_table[i][2]
    # translate with respect to active cell
    veto_rate = Q_cell[veto_offset][0]
    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[0] - delta_a[0]
                    delta_y = del_y*cell_side + delta_t[1] - delta_a[1]
                    Q = max (Q, pair_event_rate (delta_x, delta_y))
        Q_cell.append ([Q, k])

Q_tot = sum (a[0] 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[0] <= planned_displacement:
            planned_event_type = 'active-cell-change'
            planned_displacement = active_cell_limit - active_particle[0]

        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[0] - active_particle[0] - delta_s, \
                                         part[1] - active_particle[1])           \
                        / 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[0] - active_particle[0] - delta_s,
                                     possible_target_particle[1] - active_particle[1])
                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

        # advance active particle
        distance_to_go -= planned_displacement
        new_x = active_particle[0] + planned_displacement
        active_particle = (new_x % 1.0, active_particle[1])

        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[1]])
            active_particle = (active_cell % L * cell_side, active_particle[1])

        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)
np.savetxt ('cvmc-radial-distr-func.dat', zip (r, g_of_r))
Personal tools