Skip to content

Commit 89b38d4

Browse files
authored
Merge pull request #131 from theochem/add_laplacian
Laplacian method for Species class
2 parents b742618 + 78e16d0 commit 89b38d4

File tree

8 files changed

+165
-44
lines changed

8 files changed

+165
-44
lines changed

atomdb/datasets/numeric/run.py

+66-6
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,28 @@
3131

3232
import atomdb
3333

34-
from atomdb.utils import MULTIPLICITIES
34+
from atomdb.utils import MULTIPLICITIES, DEFAULT_DATAPATH
3535

3636
from atomdb.periodic import Element
3737

3838

39-
def load_numerical_hf_data():
40-
"""Load data from desnity.out file into a `SpeciesTable`."""
39+
def load_numerical_hf_data(data_path):
40+
"""Load data from desnity.out file into a `SpeciesTable`.
41+
42+
Parameters
43+
----------
44+
data_path : str
45+
Path to the directory containing a folder named `raw` where the desnity.out file is stored.
46+
47+
Returns
48+
-------
49+
species : dict
50+
Dictionary of atomic species containing the information from the numeric Hartree-Fock calculation.
51+
This is energy components, grid, density, gradient, and laplacian values.
52+
53+
"""
54+
# set the path to the raw data
55+
data_path = os.path.join(data_path, "numeric", "raw")
4156

4257
from io import StringIO
4358

@@ -56,7 +71,7 @@ def helper_data():
5671
return data[:, 0], data[:, 1], data[:, 2], data[:, 3]
5772

5873
species = {}
59-
with open(os.path.join(os.path.dirname(__file__), "raw/density.out"), "r") as f:
74+
with open(os.path.join(data_path, "density.out"), "r") as f:
6075
line = f.readline()
6176
while line:
6277
if line.startswith(" 1st line is atomic no"):
@@ -99,6 +114,48 @@ def helper_data():
99114
return species
100115

101116

117+
def eval_radial_dd_density(gradient, laplacian, points, err='ignore', tol=1e-10):
118+
"""Helper function to compute the radial second derivative of the density.
119+
120+
From a set of radial points :math:`r`, the gradient of the density, :math:`df/dr`, and the
121+
Laplacian of the density, :math:`\nabla^2 f`, the radial second derivative of the density is
122+
computed as:
123+
124+
.. math::
125+
d/dr (df/dr) = \nabla^2 f - 2/r * df/dr
126+
127+
Parameters
128+
----------
129+
gradient : np.ndarray
130+
Gradient of the density.
131+
laplacian : np.ndarray
132+
Laplacian of the density.
133+
points : np.ndarray
134+
Radial points where the density gradient and Laplacian are evaluated.
135+
err : str, optional
136+
Error handling for division by zero.
137+
tol : float, optional
138+
Tolerance for the points close to zero.
139+
140+
Returns
141+
-------
142+
d2dens : np.ndarray
143+
Radial second derivative of the density.
144+
145+
Notes
146+
-----
147+
When the points are close to zero, the radial second derivative of the density tends to infinity.
148+
In this case, this function returns zero.
149+
150+
"""
151+
# Handle the case when the points are close to zero
152+
with np.errstate(divide=err):
153+
# Compute the radial second derivative of the density
154+
d2dens = laplacian - 2 * gradient / points
155+
d2dens = np.where(points < tol, 0.0, d2dens)
156+
return d2dens
157+
158+
102159
DOCSTRING = """Numeric Dataset
103160
104161
Load data from desnity.out file into a `SpeciesTable`.
@@ -134,7 +191,7 @@ def run(elem, charge, mult, nexc, dataset, datapath):
134191
f"Expected multiplicity is {expected_mult}."
135192
)
136193

137-
species_table = load_numerical_hf_data()
194+
species_table = load_numerical_hf_data(datapath)
138195
data = species_table[(atnum, nelec)]
139196

140197
#
@@ -165,6 +222,9 @@ def run(elem, charge, mult, nexc, dataset, datapath):
165222
lapl_tot = data["laplacian"]
166223
ked_tot = None
167224

225+
# Compute the second derivative of the density
226+
dd_dens_tot = eval_radial_dd_density(d_dens_tot, lapl_tot, points)
227+
168228
# Return Species instance
169229
fields = dict(
170230
elem=elem,
@@ -183,7 +243,7 @@ def run(elem, charge, mult, nexc, dataset, datapath):
183243
rs=points,
184244
dens_tot=dens_tot,
185245
d_dens_tot=d_dens_tot,
186-
dd_dens_tot=lapl_tot,
246+
dd_dens_tot=dd_dens_tot,
187247
ked_tot=ked_tot,
188248
)
189249
return atomdb.Species(dataset, fields)

atomdb/species.py

+51-5
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ def d_dens_func(self):
666666
@spline
667667
def dd_dens_func(self):
668668
r"""
669-
Return a cubic spline of the electronic density Laplacian.
669+
Return a cubic spline of the second derivative of the electronic density.
670670
671671
Parameters
672672
----------
@@ -683,14 +683,60 @@ def dd_dens_func(self):
683683
684684
Returns
685685
-------
686-
DensitySpline
687-
A DensitySpline instance for the density and its derivatives.
688-
Given a set of radial points, it can evaluate densities and
689-
derivatives up to order 2.
686+
Callable[[np.ndarray(N,), int] -> np.ndarray(N,)]
687+
a callable function evaluating the second derivative of the density given a set of radial
688+
points (1-D array).
690689
691690
"""
692691
pass
693692

693+
def dd_dens_lapl_func(self, spin="t", index=None, log=False):
694+
r"""
695+
Return the function for the electronic density Laplacian.
696+
697+
.. math::
698+
699+
\nabla^2 \rho(\mathbf{r}) = \frac{d^2 \rho(r)}{dr^2} + \frac{2}{r} \frac{d \rho(r)}{dr}
700+
701+
Parameters
702+
----------
703+
spin : str, default="t"
704+
Type of occupied spin orbitals.
705+
Can be either "t" (for alpha + beta), "a" (for alpha),
706+
"b" (for beta), or "m" (for alpha - beta).
707+
index : sequence of int, optional
708+
Sequence of integers representing the spin orbitals.
709+
These are indexed from 0 to the number of basis functions.
710+
By default, all orbitals of the given spin(s) are included.
711+
log : bool, default=False
712+
Whether the logarithm of the density is used for interpolation.
713+
714+
Returns
715+
-------
716+
Callable[np.ndarray(N,) -> np.ndarray(N,)]
717+
a callable function evaluating the Laplacian of the density given a set of radial
718+
points (1-D array).
719+
720+
Notes
721+
-----
722+
When this function is evaluated at a point close to zero, the Laplacian becomes undefined.
723+
In this case, this function returns zero.
724+
725+
"""
726+
# Obtain cubic spline functions for the first and second derivatives of the density
727+
d_dens_sp_spline = self.d_dens_func(spin=spin, index=index, log=log)
728+
dd_dens_spline = self.dd_dens_func(spin=spin, index=index, log=log)
729+
730+
# Define the Laplacian function
731+
def densityspline_like_func(rs):
732+
# Avoid division by zero and handle small values of r
733+
with np.errstate(divide='ignore'):
734+
laplacian = dd_dens_spline(rs) + 2 * d_dens_sp_spline(rs) / rs
735+
laplacian = np.where(rs < 1e-10, 0.0, laplacian)
736+
return laplacian
737+
738+
return densityspline_like_func
739+
694740
@spline
695741
def ked_func(self):
696742
r"""
19 Bytes
Binary file not shown.
19 Bytes
Binary file not shown.
19 Bytes
Binary file not shown.
19 Bytes
Binary file not shown.
19 Bytes
Binary file not shown.

atomdb/test/test_numeric.py

+48-33
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,13 @@ def test_numerical_hf_data_h():
6464
assert np.allclose(sp._data.dens_tot[-2:], [0.0, 0.0], atol=1e-10)
6565

6666
# evaluate radial density gradient (first derivative of density spline)
67-
dens = sp.dens_func(spin="t", log=True)
68-
gradient = dens(sp._data.rs, deriv=1)
67+
gradient = sp.d_dens_func(log=False)(sp._data.rs)
6968

70-
# load gradient reference values from numerical HF raw files
71-
fname = "001_q000_m02_numeric_gradient.npy"
72-
ref_grad = np.load(f"{TEST_DATAPATH}/numeric/db/{fname}")
73-
74-
# check interpolated gradient values against reference
75-
# close to the nuclei the spline derivative does not describe the gradient well
76-
assert np.allclose(gradient[:2], ref_grad[:2], atol=1e-3)
77-
# away from the nuclei the spline derivative describes the gradient well
78-
assert np.allclose(gradient[-2:], ref_grad[-2:], atol=1e-10)
69+
# check interpolated gradient values against reference values from numerical HF raw files
70+
# close to the nuclei
71+
assert np.allclose(gradient[:2], [-0.636619761671399, -0.613581739284137], atol=1e-10)
72+
# away from the nuclei
73+
assert np.allclose(gradient[-2:], [0.0, 0.0], atol=1e-10)
7974

8075

8176
def test_numerical_hf_data_h_anion():
@@ -116,18 +111,12 @@ def test_numerical_hf_data_h_anion():
116111
assert np.allclose(sp._data.dens_tot[-20:], 0.0, atol=1e-10)
117112

118113
# evaluate radial density gradient (first derivative of density spline)
119-
dens = sp.dens_func(spin="t", log=True)
120-
gradient = dens(sp._data.rs, deriv=1)
121-
122-
# load gradient reference values from numerical HF raw files
123-
fname = "001_q-01_m01_numeric_gradient.npy"
124-
ref_grad = np.load(f"{TEST_DATAPATH}/numeric/db/{fname}")
114+
gradient = sp.d_dens_func(log=False)(sp._data.rs)
125115

126-
# check interpolated gradient values against reference
127-
# close to the nuclei the spline derivative does not describe the gradient well
128-
assert np.allclose(gradient[:2], ref_grad[:2], atol=1e-3)
129-
# away from the nuclei the spline derivative describes the gradient well
130-
assert np.allclose(gradient[-2:], ref_grad[-2:], atol=1e-10)
116+
# check interpolated gradient values against reference values from numerical HF raw files
117+
assert np.allclose(gradient[:2], [-0.618386750431843, -0.594311093621533], atol=1e-10)
118+
assert np.allclose(gradient[20:22], [-0.543476018733641, -0.538979599233911], atol=1e-10)
119+
assert np.allclose(gradient[-20:], [0.0] * 20, atol=1e-10)
131120

132121

133122
@pytest.mark.parametrize(
@@ -163,7 +152,11 @@ def test_numerical_hf_atomic_density(atom, mult, npoints, nelec):
163152
assert all(sp._data.dens_tot >= 0.0)
164153

165154
# check the density integrates to the correct number of electrons
166-
assert_almost_equal(4 * np.pi * np.trapz(grid**2 * dens, grid), nelec, decimal=2)
155+
if hasattr(np, "trapezoid"):
156+
int_density = 4.0 * np.pi * np.trapezoid(grid**2 * dens, grid)
157+
else:
158+
int_density = 4.0 * np.pi * np.trapz(grid**2 * dens, grid)
159+
assert_almost_equal(int_density, nelec, decimal=2)
167160

168161
# get density spline and check its values
169162
spline = sp.dens_func(spin="t", log=True)
@@ -195,26 +188,48 @@ def test_numerical_hf_density_gradient(atom, charge, mult):
195188
assert np.allclose(gradient[indx_radii], ref_grad[indx_radii], atol=1e-3)
196189

197190

198-
@pytest.mark.xfail(reason="High errors in spline derivative of order 2")
191+
@pytest.mark.xfail(reason="High errors in spline derivative of order 2 at intermediate distances")
199192
@pytest.mark.parametrize(
200-
"atom, charge, mult", [("H", 0, 2), ("H", -1, 1), ("Be", 0, 1), ("Cl", 0, 3), ("Ne", 0, 1)]
193+
"atom, charge, mult", [("H", 0, 2), ("H", -1, 1), ("Be", 0, 1), ("Cl", 0, 2), ("Ne", 0, 1)]
201194
)
202-
def test_numerical_hf_density_laplacian(atom, charge, mult):
195+
def test_numerical_hf_dd_density(atom, charge, mult):
203196
# load atomic and density data
204197
sp = load(atom, charge, mult, dataset="numeric", datapath=TEST_DATAPATH)
205198

206-
# evaluate density and laplacian (second derivative of density spline) on the radial grid
207-
grid = sp._data.rs
208-
spline = sp.dens_func(spin="t", log=False)
209-
lapl = spline(grid, deriv=2)
199+
# evaluate the second derivative of the density on the radial grid
200+
dd_dens = sp.dd_dens_func(log=False)(sp._data.rs)
210201

211202
# check shape of arrays
212-
assert lapl.shape == grid.shape
203+
assert dd_dens.shape == sp._data.rs.shape
204+
205+
# check interpolated density derivative values against reference values
206+
# far away from the nuclei, the second derivative of the density is close to zero
207+
assert np.allclose(dd_dens[-10:], [0.0] * 10, atol=1e-10)
208+
# for r=0, the second derivative of the density is set to zero
209+
assert np.allclose(dd_dens[0], [0.0], atol=1e-10)
210+
211+
# WARNING: The values of the second order derivative of the density at intermediate r distances
212+
# are not tested. Comparisong agains deriv=2 of the density spline:
213+
# ref_dd_dens = sp.dens_func(log=True)(sp._data.rs, deriv=2)
214+
# results in high errors, rendering this test case as unreliable.
215+
216+
217+
@pytest.mark.parametrize(
218+
"atom, charge, mult", [("H", 0, 2), ("H", -1, 1), ("Be", 0, 1), ("Cl", 0, 2), ("Ne", 0, 1)]
219+
)
220+
def test_numerical_hf_density_laplacian(atom, charge, mult):
221+
# load atomic and density data
222+
sp = load(atom, charge, mult, dataset="numeric", datapath=TEST_DATAPATH)
223+
224+
# evaluate the Laplacian of the density on the radial grid
225+
laplacian_dens = sp.dd_dens_lapl_func(log=False)(sp._data.rs)
213226

214227
# load reference values from numerical HF raw files
215228
id = f"{str(sp.atnum).zfill(3)}_q{str(charge).zfill(3)}_m{mult:02d}"
216229
fname = f"{id}_numeric_laplacian.npy"
217230
ref_lapl = np.load(f"{TEST_DATAPATH}/numeric/db/{fname}")
218231

219-
# interpolated laplacian values against reference
220-
assert np.allclose(lapl, ref_lapl, atol=1e-6)
232+
# check interpolated Laplacian of density values against reference values
233+
assert np.allclose(laplacian_dens, ref_lapl, atol=1e-10)
234+
# for r=0, the Laplacian function in not well defined and is set to zero
235+
assert np.allclose(laplacian_dens[0], [0.0], atol=1e-10)

0 commit comments

Comments
 (0)