matscipy.electrochemistry.steric_correction

Enforces minimum distances on coordinates within discrete distribtution.

Copyright 2020 IMTEK Simulation University of Freiburg

Authors:

Examples

Benchmark different scipy optimizers for the steric correction problem:

>>> # measures of box
>>> xsize = ysize = 5e-9 # nm, SI units
>>> zsize = 10e-9    # nm, SI units
>>>
>>> # get continuum distribution, z direction
>>> x = np.linspace(0, zsize, 2000)
>>> c = [0.1,0.1]
>>> z = [1,-1]
>>> u = 0.05
>>>
>>> phi = potential(x, c, z, u)
>>> C   = concentration(x, c, z, u)
>>> rho = charge_density(x, c, z, u)
>>>
>>> # create distribution functions
>>> distributions = [interpolate.interp1d(x,c) for c in C]
>>>
>>> # sample discrete coordinate set
>>> box = np.array([xsize, ysize, zsize])m
>>> sample_size = 100
>>>
>>> samples = [ continuous2discrete(
>>>     distribution=d, box=box, count=sample_size) for d in distributions ]
>>>
>>> # apply penalty for steric overlap
>>> x = np.vstack(samples)
>>>
>>> box = np.array([[0.,0.,0],box]) # needs lower corner
>>>
>>> n = x.shape[0]
>>> dim = x.shape[1]
>>>
>>> # benchmark methods
>>> mindsq, (p1,p2) = scipy_distance_based_closest_pair(x)
>>> pmin = np.min(x,axis=0)
>>> pmax = np.max(x,axis=0)
>>> mind = np.sqrt(mindsq)
>>> logger.info("Minimum pair-wise distance in sample: {}".format(mind))
>>> logger.info("First sample point in pair:    ({:8.4e},{:8.4e},{:8.4e})".format(*p1))
>>> logger.info("Second sample point in pair    ({:8.4e},{:8.4e},{:8.4e})".format(*p2))
>>> logger.info("Box lower boundary:            ({:8.4e},{:8.4e},{:8.4e})".format(*box[0]))
>>> logger.info("Minimum coordinates in sample: ({:8.4e},{:8.4e},{:8.4e})".format(*pmin))
>>> logger.info("Maximum coordinates in sample: ({:8.4e},{:8.4e},{:8.4e})".format(*pmax))
>>> logger.info("Box upper boundary:            ({:8.4e},{:8.4e},{:8.4e})".format(*box[1]))
>>>
>>> # stats: method, x, res, dt, mind, p1, p2 , pmin, pmax
>>> stats = [('initial',x,None,0,mind,p1,p2,pmin,pmax)]
>>>
>>> r = 4e-10 # 4 Angstrom steric radius
>>> logger.info("Steric radius: {:8.4e}".format(r))
>>>
>>> methods = [
>>>     'Powell',
>>>     'CG',
>>>     'BFGS',
>>>     'L-BFGS-B'
>>> ]
>>>
>>> for m in methods:
>>>     try:
>>>         logger.info("### {} ###".format(m))
>>>         t0 = time.perf_counter()
>>>         x1, res = apply_steric_correction(x,box=box,r=r,method=m)
>>>         t1 = time.perf_counter()
>>>         dt = t1 - t0
>>>         logger.info("{} s runtime".format(dt))
>>>
>>>         mindsq, (p1,p2) = scipy_distance_based_closest_pair(x1)
>>>         mind = np.sqrt(mindsq)
>>>         pmin = np.min(x1,axis=0)
>>>         pmax = np.max(x1,axis=0)
>>>
>>>         stats.append([m,x1,res,dt,mind,p1,p2,pmin,pmax])
>>>
>>>         logger.info("Minimum pair-wise distance in final configuration: {:8.4e}".format(mind))
>>>         logger.info("First sample point in pair:    ({:8.4e},{:8.4e},{:8.4e})".format(*p1))
>>>         logger.info("Second sample point in pair    ({:8.4e},{:8.4e},{:8.4e})".format(*p2))
>>>         logger.info("Box lower boundary:            ({:8.4e},{:8.4e},{:8.4e})".format(*box[0]))
>>>         logger.info("Minimum coordinates in sample: ({:8.4e},{:8.4e},{:8.4e})".format(*pmin))
>>>         logger.info("Maximum coordinates in sample: ({:8.4e},{:8.4e},{:8.4e})".format(*pmax))
>>>         logger.info("Box upper boundary:            ({:8.4e},{:8.4e},{:8.4e})".format(*box[1]))
>>>     except:
>>>         logger.warn("{} failed.".format(m))
>>>         continue
>>>
>>> stats_df = pd.DataFrame( [ {
>>>     'method':  s[0],
>>>     'runtime': s[3],
>>>     'mind':    s[4],
>>>     **{'p1{:d}'.format(i): c for i,c in enumerate(s[5]) },
>>>     **{'p2{:d}'.format(i): c for i,c in enumerate(s[6]) },
>>>     **{'pmin{:d}'.format(i): c for i,c in enumerate(s[7]) },
>>>     **{'pmax{:d}'.format(i): c for i,c in enumerate(s[8]) }
>>> } for s in stats] )
>>>
>>> print(stats_df.to_string(float_format='%8.6g'))
method  runtime        mind         p10         p11         p12         p20         p21         p22       pmin0       pmin1       pmin2       pmax0       pmax1       pmax2
0   initial        0 1.15674e-10 2.02188e-09 4.87564e-10 5.21835e-09 2.03505e-09 3.72691e-10 5.22171e-09 1.17135e-12 1.49124e-10 6.34126e-12 4.98407e-09 4.99037e-09 9.86069e-09
1    Powell  75.2704 8.02318e-10 4.23954e-09 3.36242e-09 8.80092e-09 4.31183e-09 2.56345e-09 8.81278e-09 4.01789e-10  4.0081e-10  4.2045e-10 4.59284e-09 4.54413e-09  9.5924e-09
2        CG  27.0756  7.9992e-10 3.39218e-09 4.00079e-09 8.27255e-09 3.86337e-09 4.27807e-09 7.68863e-09 4.00018e-10 4.00146e-10 4.00565e-10 4.59941e-09 4.59989e-09 9.59931e-09
3      BFGS  19.0255 7.99527e-10 1.82802e-09 3.54397e-09 9.69736e-10 2.41411e-09   3.936e-09 1.34664e-09 4.00514e-10 4.01874e-10  4.0002e-10 4.59695e-09 4.59998e-09 9.58155e-09
4  L-BFGS-B  11.7869 7.99675e-10 4.34395e-09 3.94096e-09 1.28996e-09 4.44064e-09 3.15999e-09 1.14778e-09 4.12146e-10 4.01506e-10 4.03583e-10     4.6e-09 4.59898e-09  9.5982e-09

Functions

apply_steric_correction(x[, box, r, method, ...])

Enforce steric constraints on coordinate distribution within box.

box_constraint(x[, box, r])

Constraint function.

box_constraint_with_gradient(x[, box, r])

Constraint function.

brute_force_closest_pair(x)

Find coordinate pair with minimum distance squared ||xi-xj||^2.

brute_force_target_function(x[, r, constraints])

Target function.

neigh_list_based_target_function(x[, r, ...])

Target function.

numpy_only_target_function(x[, r, constraints])

Target function.

planar_closest_pair(x)

Find coordinate pair with minimum distance ||xi-xj||.

recursive_closest_pair(x, y)

Find coordinate pair with minimum distance squared ||xi-xj||^2.

scipy_distance_based_closest_pair(x)

Find coordinate pair with minimum distance ||xi-xj||.

scipy_distance_based_target_function(x[, r, ...])

Target function.

Classes

DeferredMessage(msg, func, *args, **kwargs)

Lazy evaluation for log messages.

class matscipy.electrochemistry.steric_correction.DeferredMessage(msg, func, *args, **kwargs)

Bases: object

Lazy evaluation for log messages.

__init__(msg, func, *args, **kwargs)
matscipy.electrochemistry.steric_correction.brute_force_closest_pair(x)

Find coordinate pair with minimum distance squared ||xi-xj||^2.

Parameters:

x ((N,dim) ndarray) – coordinates

Returns:

float, (ndarray, ndarray)

Return type:

minimum distance squared and coodinates pair

Examples

Compare the performance of closest pair algorithms:

>>> from matscipy.electrochemistry.steric_distribution import scipy_distance_based_target_function
>>> from matscipy.electrochemistry.steric_distribution import numpy_only_target_function
>>> from matscipy.electrochemistry.steric_distribution import brute_force_target_function
>>> import itertools
>>> import pandas as pd
>>> import scipy.spatial.distance
>>> import timeit
>>>
>>> funcs = [
>>>         brute_force_closest_pair,
>>>         scipy_distance_based_closest_pair,
>>>         planar_closest_pair ]
>>> func_names = ['brute','scipy','planar']
>>> stats = []
>>> N = 1000
>>> dim = 3
>>> for k in range(5):
>>>     x = np.random.rand(N,dim)
>>>     lambdas = [ (lambda x=x,f=f: f(x)) for f in funcs ]
>>>     rets    = [ f() for f in lambdas ]
>>>     vals    = [ v[0] for v in rets ]
>>>     coords  = [ c for v in rets for p in v[1] for c in p ]
>>>     times   = [ timeit.timeit(f,number=1) for f in lambdas ]
>>>     diffs   = scipy.spatial.distance.pdist(
>>>         np.atleast_2d(vals).T,metric='euclidean')
>>>     stats.append((*vals,*diffs,*times,*coords))
>>>
>>> func_name_tuples = list(itertools.combinations(func_names,2))
>>> diff_names =  [ 'd_{:s}_{:s}'.format(f1,f2) for (f1,f2) in func_name_tuples ]
>>> perf_names =  [ 't_{:s}'.format(f) for f in func_names ]
>>> coord_names = [
>>>     'p{:d}{:s}_{:s}'.format(i,a,f) for f in func_names for i in (1,2) for a in ('x','y','z') ]
>>> float_fields = [*func_names,*diff_names,*perf_names,*coord_names]
>>> dtypes = [ (field, 'f4') for field in float_fields ]
>>> labeled_stats = np.array(stats,dtype=dtypes)
>>> stats_df = pd.DataFrame(labeled_stats)
>>> print(stats_df.T.to_string(float_format='%8.6g'))
                         0           1           2           3           4
brute          2.24089e-05 5.61002e-05 8.51047e-05 3.48424e-05 5.37235e-05
scipy          2.24089e-05 5.61002e-05 8.51047e-05 3.48424e-05 5.37235e-05
planar         2.24089e-05 5.61002e-05 8.51047e-05 3.48424e-05 5.37235e-05
d_brute_scipy            0           0           0           0           0
d_brute_planar           0           0           0           0           0
d_scipy_planar           0           0           0           0           0
t_brute            4.02697     3.85543      4.1414     3.90338     3.86993
t_scipy         0.00708364  0.00698962  0.00762594  0.00703242  0.00703579
t_planar           0.38302     0.39462    0.434342    0.407233    0.420773
p1x_brute         0.132014    0.331441    0.553405    0.534633    0.977582
p1y_brute         0.599688    0.186959     0.90897    0.575864    0.636278
p1z_brute          0.49631    0.993856    0.246418    0.853567    0.411793
p2x_brute         0.134631    0.333526     0.55322    0.534493    0.977561
p2y_brute         0.603598    0.179771    0.915063    0.576894    0.629313
p2z_brute         0.496833    0.994145    0.239493    0.859377    0.409509
p1x_scipy         0.132014    0.331441    0.553405    0.534633    0.977582
p1y_scipy         0.599688    0.186959     0.90897    0.575864    0.636278
p1z_scipy          0.49631    0.993856    0.246418    0.853567    0.411793
p2x_scipy         0.134631    0.333526     0.55322    0.534493    0.977561
p2y_scipy         0.603598    0.179771    0.915063    0.576894    0.629313
p2z_scipy         0.496833    0.994145    0.239493    0.859377    0.409509
p1x_planar        0.132014    0.331441     0.55322    0.534633    0.977561
p1y_planar        0.599688    0.186959    0.915063    0.575864    0.629313
p1z_planar         0.49631    0.993856    0.239493    0.853567    0.409509
p2x_planar        0.134631    0.333526    0.553405    0.534493    0.977582
p2y_planar        0.603598    0.179771     0.90897    0.576894    0.636278
p2z_planar        0.496833    0.994145    0.246418    0.859377    0.411793
matscipy.electrochemistry.steric_correction.recursive_closest_pair(x, y)

Find coordinate pair with minimum distance squared ||xi-xj||^2.

Find coordinate pair with minimum distance squared ||xi-xj||^2 with one point from x and the other point from y

Parameters:

x ((N,dim) ndarray) – coordinates

Returns:

float, (ndarray, ndarray)

Return type:

minimum distance squared and coordinate pair

matscipy.electrochemistry.steric_correction.planar_closest_pair(x)

Find coordinate pair with minimum distance ||xi-xj||.

ATTENTION: this implementation tackles the planar problem!

Parameters:

x ((N,dim) ndarray) – coordinates

Returns:

float, (ndarray, ndarray)

Return type:

minimum distance squared and coordinates pair

matscipy.electrochemistry.steric_correction.scipy_distance_based_closest_pair(x)

Find coordinate pair with minimum distance ||xi-xj||.

Parameters:

x ((N,dim) ndarray) – coordinates

Returns:

float, (ndarray, ndarray)

Return type:

minimum distance squared and coodinates pair

Examples

Handling condensed distance matrix indices:

>>> c = np.array([1, 2, 3, 4, 5, 6])
>>> print(c)
[1 2 3 4 5 6]
>>> d = scipy.spatial.distance.squareform(c)
>>> print(d)
[[0 1 2 3]
 [1 0 4 5]
 [2 4 0 6]
 [3 5 6 0]]
>>> I,J = np.tril_indices(d.shape[0],-1)
>>> print(I,J)
[1 2 2 3 3 3] [0 0 1 0 1 2]
>>> I,J = np.triu_indices(d.shape[0],1)
>>> print(I,J)
[0 0 0 1 1 2] [1 2 3 2 3 3]
>>> print(d[I])
[1 2 4 3 5 6]
matscipy.electrochemistry.steric_correction.brute_force_target_function(x, r=1.0, constraints=None)

Target function. Penalize dense packing for coordinates ||xi-xj||<ri+rj.

Parameters:
  • x ((N,dim) ndarray) – particle coordinates

  • r (float or (N,) ndarray, optional (default=1.0)) – steric radii of particles

Returns:

float

Return type:

target function value

Examples

Compare performance of target functions:

>>> from matscipy.electrochemistry.steric_distribution import scipy_distance_based_target_function
>>> from matscipy.electrochemistry.steric_distribution import numpy_only_target_function
>>> from matscipy.electrochemistry.steric_distribution import brute_force_target_function
>>> import itertools
>>> import pandas as pd
>>> import scipy.spatial.distance
>>> import timeit
>>>
>>> funcs = [
>>>         brute_force_target_function,
>>>         numpy_only_target_function,
>>>         scipy_distance_based_target_function ]
>>> func_names = ['brute','numpy','scipy']
>>>
>>> stats = []
>>> K = np.exp(np.log(10)*np.arange(-3,3))
>>> for k in K:
>>>     lambdas = [ (lambda x0=x0,k=k,f=f: f(x0*k)) for f in funcs ]
>>>     vals    = [ f() for f in lambdas ]
>>>     times   = [ timeit.timeit(f,number=1) for f in lambdas ]
>>>     diffs = scipy.spatial.distance.pdist(np.atleast_2d(vals).T,metric='euclidean')
>>>     stats.append((k,*vals,*diffs,*times))
>>>
>>> func_name_tuples = list(itertools.combinations(func_names,2))
>>> diff_names = [ 'd_{:s}_{:s}'.format(f1,f2) for (f1,f2) in func_name_tuples ]
>>> perf_names = [ 't_{:s}'.format(f) for f in func_names ]
>>> fields =  ['k',*func_names,*diff_names,*perf_names]
>>> dtypes = [ (field, '>f4') for field in fields ]
>>> labeled_stats = np.array(stats,dtype=dtypes)
>>> stats_df = pd.DataFrame(labeled_stats)
>>> print(stats_df.to_string(float_format='%8.6g'))
         k       brute       numpy       scipy  d_brute_numpy  d_brute_scipy  d_numpy_scipy  t_brute  t_numpy   t_scipy
0    0.001  3.1984e+07  3.1984e+07  3.1984e+07    5.58794e-08    6.70552e-08    1.11759e-08 0.212432 0.168858 0.0734278
1     0.01 3.19829e+07 3.19829e+07 3.19829e+07    9.31323e-08    7.82311e-08    1.49012e-08 0.212263  0.16846 0.0791856
2      0.1 3.18763e+07 3.18763e+07 3.18763e+07    7.45058e-09    1.86265e-08    1.11759e-08 0.201706 0.164867 0.0711544
3        1 2.27418e+07 2.27418e+07 2.27418e+07    3.72529e-08    4.84288e-08    1.11759e-08  0.20762 0.166005 0.0724238
4       10      199751      199751      199751    1.16415e-10    2.91038e-11    8.73115e-11 0.202635 0.161932 0.0772684
5      100     252.548     252.548     252.548    3.28555e-11              0    3.28555e-11 0.202512 0.161217 0.0726705
matscipy.electrochemistry.steric_correction.scipy_distance_based_target_function(x, r=1.0, constraints=None)

Target function. Penalize dense packing for coordinates ||xi-xj||<ri+rj.

Parameters:
  • x ((N,dim) ndarray) – particle coordinates

  • r (float or (N,) ndarray, optional (default=1.0)) – steric radii of particles

Returns:

float

Return type:

target function value

matscipy.electrochemistry.steric_correction.numpy_only_target_function(x, r=1.0, constraints=None)

Target function. Penalize dense packing for coordinates ||xi-xj||<ri+rj.

Parameters:
  • x ((N,dim) ndarray) – particle coordinates

  • r (float or (N,) ndarray, optional (default=1.0)) – steric radii of particles

Returns:

float

Return type:

target function value

matscipy.electrochemistry.steric_correction.neigh_list_based_target_function(x, r=1.0, constraints=None, Dij=None)

Target function. Penalize dense packing for coordinates ||xi-xj||<ri+rj.

Parameters:
  • x ((N,dim) ndarray) – particle coordinates

  • r (float or (N,) ndarray, optional (default=1.0)) – steric radii of particles

  • constraints (callable, returns (float, (N,dim) ndarray)) – constraint function value and gradient

  • Dij ((N, N) ndarray, optional (default=None)) – pairwise minimum allowed distance matrix, overrides r

Returns:

(float, (N,dim) ndarray)

f: float, target (or penalty) function, value evaluates to

sum_i sum_{j!=i} max(0,(r_i+r_j)^2-||xi-xj||^2)^2

without double-counting pairs, where r_i and r_j are the steric radii of coordinate points i and j

df: (N,dim) ndarray of float, the gradient, evaluates to

4*si sj ((r_i’+r_j)^2-||xi-xj||^2)^2)*(xik’-xjk’)*(kdi’j-kdi’i)

for entry (i’,k’), where i subscribes the coordinate point and k the spatial dimension. kd is Kronecker delta, si is sum over i

Return type:

target function value and gradient

matscipy.electrochemistry.steric_correction.box_constraint(x, box=array([[0., 0., 0.], [1., 1., 1.]]), r=0.0)

Constraint function. Confine coordinates within box.

Parameters:
  • x ((N,dim) ndarray) – coordinates

  • box ((2,dim) ndarray, optional (default: [[0.,0.,0.],[1.,1.,1.]])) – box corner coordinates

  • r (float or np.(N,) ndarray, optional (default=0)) – steric radii of particles

Returns:

float

Return type:

penalty, positive

matscipy.electrochemistry.steric_correction.box_constraint_with_gradient(x, box=array([[0., 0., 0.], [1., 1., 1.]]), r=0.0)

Constraint function. Confine coordinates within box.

Parameters:
  • x ((N,dim) ndarray) – coordinates

  • box ((2,dim) ndarray, optional (default: [[0.,0.,0.],[1.,1.,1.]])) – box corner coordinates

  • r (float or np.(N,) ndarray, optional (default=0)) – steric radii of particles

Returns:

(float, (N,dim) ndarray of float)

g(x), positive:

lik = box[0,k] - xik + ri > 0 for xik out of lower boundaries rik = xik + ri - box[1,k] > 0 for xik out of upper boundaries

where box[0] and box[1] are lower and upper corner of bounding box and k marks spatial dimension.

g(x) = sum_i sum_k ( max(0,lik) + max(0,rik) )^2

dg(x): gradient, entries accordingly evaluates to

dgdxik(x) = 2*( -max(0,lik) + max(0,rik) )

Return type:

penalty g(x) and its gradient dg(x)

matscipy.electrochemistry.steric_correction.apply_steric_correction(x, box=None, r=None, method='L-BFGS-B', options={'disp': True, 'eps': 1e-08, 'gtol': 1e-08, 'maxiter': 100}, target_function=<function neigh_list_based_target_function>, returns_gradient=True, closest_pair_function=<function scipy_distance_based_closest_pair>)

Enforce steric constraints on coordinate distribution within box.

Parameters:
  • x ((N,dim) ndarray) – Particle coordinates.

  • box ((2,dim) ndarray, optional (default: None)) – Box corner coordinates.

  • r (float or (N,) ndarray, optional (default=None)) – Steric radius of particles. Can be specified particle-wise.

  • options (dict, optional) – Forwarded to scipy minimzer. https://docs.scipy.org/doc/scipy/reference/optimize.minimize-bfgs.html (default: {‘gtol’:1.e-5,’maxiter’:10,’disp’:True,’eps’:1.e-8})

  • method (str) – scipy.optimize.minimize method

  • target_function (func, optional) – One of the target functions within this submodule, or function of same signature. (default: neigh_list_based_target_function)

  • returns_gradient (bool, optional (default: True)) – If True, then ‘target_function’ is expected to return a tuple (f, df) with f the actual target function value and df its (N,dim) gradient. This flag must be set for ‘neigh_list_based_target_function’.

  • closest_pair_function (func, optional) – One of the closest pair functions within this submodule, or function of same signature. (default: scipy_distance_based_closest_pair)

Returns:

  • float ((N,dim) ndarray) – Modified particle coordinates, meeting steric constraints.

  • scipy.optimize.OptimizeResult – Forwarded raw object as returned by scipy.optimize.minimize