Why analphipy#
analphipy is a python package for common analysis on pair potentials. It’s features include
Simple interface for defining a pair potential
Methods to create ‘cut’, ‘linear force shifted’, and ‘table’ potentials
Calculation of common metrics, like the second virial coefficient, and Noro-Frenkel effective parameters
This package is used actively by the author, so new metrics and potentials will be added over time. If you have a suggestion, please let the author know!
Useful references#
Please see the following for a primer on the type of analysis provided in analphipy.
A simple example#
Let’s take a look at the properties of a Lennard-Jones (LJ), which has a length scale \(\sigma\) and energy scale \(\epsilon\). It is typical to consider units of length in terms of \(\sigma\) and energy in terms of \(\epsilon\), which is the same as setting \(\sigma=1\) and \(\epsilon=1\). We create a LJ potential object using the following:
# the passed function should only be a function of r, so use partial
from functools import partial
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import analphipy
p = analphipy.potential.LennardJones(sig=1.0, eps=1.0)
p.r_min
1.122462048309373
p.to_measures()
<analphipy.measures.Measures at 0x7f9b630174d0>
This defines an LJ potential. This contains things like the potential energy function LennardJones.phi(), the derivative function LennardJones.dphidr(), the location of the minimum in the potential Analytic.r_min, and the value at the minimum Analytic.phi_min.
For example:
%matplotlib inline
r = np.linspace(0.9, 2.5, 100)
fig, axes = plt.subplots(2)
axes[0].set_title("phi")
axes[0].plot(r, p.phi(r))
axes[0].plot(p.r_min, p.phi_min, marker="o")
axes[1].set_title("dphidr")
axes[1].plot(r, p.dphidr(r))
axes[1].plot(p.r_min, p.dphidr(p.r_min), marker="o")
fig.tight_layout()
A common question is, is the pair potential of interest like some other, perhaps simpler, potential? For example,
Can given potential be mapped to some ‘effective’ hard-sphere or square-well potential? There is a deep history to
this question. In brief, what is commonly done (and what analphipy is designed to do) is
Determine an effective (temperature dependent) hard-sphere diameter
Determine an effective square-well well depth
Determine an effective square-well attractive range.
Please look at the above references for further information. To perform the analysis on our LJ potential, we first need to create a analphipy.norofrenkel.NoroFrenkelPair object. This is conveniently done with the following method.
nf = p.to_nf()
Now, the object nf can be used to calculate effective metrics. For example, to calculate the effective hard-sphere diameter at an inverse temperature \(\beta = 1/(k_{\rm B} T)\), where \(k_{\rm B}\) is Boltzmann’s constant, use the following:
nf.sig(beta=1.0)
1.0156054202252172
betas = np.linspace(0.2, 2.0)
plt.plot(betas, [nf.sig(beta) for beta in betas])
[<matplotlib.lines.Line2D at 0x7f9b54a3e3c0>]
Likewise, the effective attractive range lambda can be found using
plt.plot(betas, [nf.lam(beta) for beta in betas])
[<matplotlib.lines.Line2D at 0x7f9b5479f0e0>]
Since it is common want to calculate such metrics over a spectrum of values of \(\beta\), there is a utility method table(). This creates a dictionary of results, which can be easily converted to a pandas DataFrame
# create a dictionary of values
betas = np.linspace(0.2, 2.0, 4)
table = nf.table(betas=betas, props=["sig", "eps", "lam"])
table
{'beta': array([0.2, 0.8, 1.4, 2. ]),
'sig': [0.9468474615554747,
1.0072387682627164,
1.0274539879239069,
1.0389955732798468],
'eps': [-1.0, -1.0, -1.0, -1.0],
'lam': [1.6162246932413373,
1.4699861479698415,
1.3923506242906025,
1.333901317192492]}
pd.DataFrame(table)
| beta | sig | eps | lam | |
|---|---|---|---|---|
| 0 | 0.2 | 0.946847 | -1.0 | 1.616225 |
| 1 | 0.8 | 1.007239 | -1.0 | 1.469986 |
| 2 | 1.4 | 1.027454 | -1.0 | 1.392351 |
| 3 | 2.0 | 1.038996 | -1.0 | 1.333901 |
There are several more metrics, and potential energy functions included in the modules analphipy.measures and analphipy.norofrenkel. Please look at the api reference for
further information!
Defining your own potential#
If you’d like to define your own potential energy function, there are two routes. The easiest is the define a callable
potential energy function, and use analphipy.potential.Generic. For example, you can create a LJ potential using:
def my_lj_func(r, sig, eps):
# function should always return an array
r = np.asarray(r)
x = sig / r
return 4.0 * eps * (x**12 - x**6)
g = analphipy.potential.Generic(phi_func=partial(my_lj_func, sig=1.0, eps=1.0))
g
Generic(r_min=None, phi_min=None, segments=None, phi_func=functools.partial(<function my_lj_func at 0x7f9b54540ca0>, sig=1.0, eps=1.0), dphidr_func=None)
r = np.linspace(0.5, 2.5, 5)
np.testing.assert_allclose(g.phi(r), p.phi(r))
Note that additional info is not required, but you can explicitly pass it if so desired. For example, include a form for \(d\phi(r)/dr\)
def my_lj_deriv_func(r, sig, eps):
r = np.asarray(r)
x = sig / r
return -48 * eps * (x**12 - 0.5 * x**6) / r
sig = 1.0
eps = 1.0
g = analphipy.potential.Generic(
phi_func=partial(my_lj_func, sig=sig, eps=eps),
dphidr_func=partial(my_lj_deriv_func, sig=1.0, eps=1.0),
# infinite integration bounds
segments=[0.0, np.inf],
)
np.testing.assert_allclose(g.dphidr(r), p.dphidr(r))
Note that to investigate the Noro-Frenkel metrics, the values of r_min and segments must be set. The latter is the integration bounds including any discontinuities. This was done analytically in analphipy.potential.LennardJones, but is not set in analphipy.potential.Generic. You can pass a value directly during the creation, or set the value numerically. For example, we could use:
# NBVAL_RAISES_EXCEPTION
# this will raise an error because we don't have a minimum
g.to_nf()
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[17], line 3
1 # NBVAL_RAISES_EXCEPTION
2 # this will raise an error because we don't have a minimum
----> 3 g.to_nf()
File ~/work/analphipy/analphipy/src/analphipy/base_potential.py:300, in PhiAbstract.to_nf(self, **kws)
298 if self.r_min is None:
299 msg = "must set `self.r_min` to use NoroFrenkel"
--> 300 raise ValueError(msg)
302 for k in ("phi", "segments", "r_min", "phi_min"):
303 if k not in kws:
ValueError: must set `self.r_min` to use NoroFrenkel
# instead set the minimum numerically with the following
g_with_min = g.assign_min_numeric(
r0=1.0, # guess for location of minimum
bounds=[0.5, 1.5], # bounds for search
)
g_with_min.to_nf().lam(beta=1.5)
1.3815918477142934
p.to_nf().lam(beta=1.5)
1.3815918477142932
Cut potential#
The classes analphipy.potential module provides a simple means to ‘cut’ the potential. To perform a simple cut, use the method analphipy.base_potential.PhiBase.cut().
p_cut = p.cut(2.5)
p_cut.phi(2.5)
array(0.)
p.phi(2.5)
-0.016316891136000003
r = np.linspace(0.9, 2.5)
plt.plot(r, p.phi(r))
plt.plot(r, p_cut.phi(r))
[<matplotlib.lines.Line2D at 0x7f9b54371d30>]
To perform a cut with linear force shift, use the method analphipy.base_potential.PhiBase.lfs().
p_lfs = p.lfs(rcut=2.5)
p_lfs.phi(2.5)
array(0.)
p_lfs.dphidr(2.5)
array(0.)
p_cut.dphidr(2.5)
array(0.03899948)
p.dphidr(2.5)
0.03899947745280001
Note that to use the lfs method, dphidr of the class must be specified.
Please take a look at the api documentation for more information!