(Iterative) Hirshfeld method

In this tutorial, we will introduce how to use the horton_part API method to execute (iterative) Hirshfeld partitioning methods.

First, we import the required libraries.

[1]:
import logging

import numpy as np
from gbasis.evals.eval import evaluate_basis
from gbasis.wrappers import from_iodata
from grid import AtomGrid, ExpRTransform, UniformInteger
from iodata import load_one
from setup import prepare_grid_and_dens, print_results

from horton_part import HirshfeldIWPart, HirshfeldWPart, ProAtomDB, ProAtomRecord
from horton_part.scripts.generate_cube import (
    _setup_cube_grid,
    prepare_input_cube,
    to_cube,
)

np.set_printoptions(precision=3, linewidth=np.inf, suppress=True)

In (iterative) Hirshfeld methods, a database of atomic radial density profiles is required. Therefore, different single-atom DFT or other ab initio calculations should be executed. Here, we use the Gaussian package. The checkpoint files, in fchk format, are generated using Gaussian tools as well.

Next, the radial atomic density is obtained by computing the spherical average of the atomic density. This process can be applied using the gbasis and grid packages.

[2]:
def prepare_record(filename):
    """Prepare molecular grid and density."""
    mol = load_one(filename)

    # Specify the integration grid
    rtf = ExpRTransform(5e-4, 2e1, 120 - 1)
    uniform_grid = UniformInteger(120)
    rgrid = rtf.transform_1d_grid(uniform_grid)

    # Get the spin-summed density matrix
    one_rdm = mol.one_rdms.get("post_scf", mol.one_rdms.get("scf"))
    basis = from_iodata(mol)
    assert len(mol.atnums) == 1
    grid = AtomGrid.from_preset(atnum=mol.atnums[0], preset="fine", rgrid=rgrid)

    basis_grid = evaluate_basis(basis, grid.points)
    rho = np.einsum("ab,bp,ap->p", one_rdm, basis_grid, basis_grid, optimize=True)
    spline = grid.spherical_average(rho)
    radial_rho = spline(rgrid.points)

    record = ProAtomRecord(mol.atnums[0], mol.charge, mol.energy, rgrid, radial_rho)
    return record

In this tutorial, we focus on the water molecule and therefore only need data for hydrogen and oxygen atoms. It should be noted that data for both cations and anions are also used.

[3]:
h_1 = prepare_record("./data/atoms/001__h_001_q+00/mult02/atom.fchk")
h_2 = prepare_record("./data/atoms/001__h_002_q-01/mult01/atom.fchk")
o_7 = prepare_record("./data/atoms/008__o_007_q+01/mult04/atom.fchk")
o_8 = prepare_record("./data/atoms/008__o_008_q+00/mult03/atom.fchk")
o_9 = prepare_record("./data/atoms/008__o_009_q-01/mult02/atom.fchk")

records = [h_1, h_2, o_7, o_8, o_9]
db = ProAtomDB(records)

Next, we prepare the molecular grid and densities as described in the Basic Usage section. It should be noted that the radial grids for atoms in the water molecule should match the atomic radial grids used for the corresponding single atoms in the atomic database calculations.

[4]:
def prepare_argument_dict(mol, grid, rho, db):
    """Prepare basic input arguments for all AIM methods."""
    kwargs = {
        "coordinates": mol.atcoords,
        "numbers": np.asarray(mol.atnums, np.int64),
        "pseudo_numbers": mol.atnums,
        "grid": grid,
        "moldens": rho,
        "lmax": 3,
        "proatomdb": db,
    }
    return kwargs

Hirshfeld method

The Hirshfeld method can be executed using the HirshfeldWPart class.

[5]:
mol, grid, rho = prepare_grid_and_dens("data/h2o.fchk")
h = HirshfeldWPart(**prepare_argument_dict(mol, grid, rho, db))
h.do_all()
print_results(h)
The number of electrons: 10.000003764139395
Coordinates of the atoms
 [[ 0.     0.     0.224]
 [-0.     1.457 -0.896]
 [-0.    -1.457 -0.896]]
Atomic numbers of the atom
 [8 1 1]

================================================================================
Information of integral grids.
--------------------------------------------------------------------------------
Grid size of molecular grid: 18460
************************************ Atom 0 ************************************
   |-- Radial grid size: 120
   |-- Angular grid sizes:
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
          6 6 6 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18
          18 18 18 18 18 18 26 26 26 26 38 38 38 38 38 50 50 50 50 50
          86 86 110 110 110 110 170 194 194 434 590 590 434 434 434 302 302 302 194 194
          170 110 110 110 110 110 110 110 110 110 110 110 86 50 50 18 18 18 18 18

************************************ Atom 1 ************************************
   |-- Radial grid size: 120
   |-- Angular grid sizes:
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 18 18 18 18 18
          18 18 18 18 18 18 18 18 18 26 26 26 38 38 38 38 38 38 38 50
          50 50 50 50 50 86 86 86 110 110 110 170 170 170 194 194 194 194 170 170
          170 110 110 110 110 110 110 110 110 110 110 86 86 50 50 50 18 18 18 18

************************************ Atom 2 ************************************
   |-- Radial grid size: 120
   |-- Angular grid sizes:
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 18 18 18 18 18
          18 18 18 18 18 18 18 18 18 26 26 26 38 38 38 38 38 38 38 50
          50 50 50 50 50 86 86 86 110 110 110 170 170 170 194 194 194 194 170 170
          170 110 110 110 110 110 110 110 110 110 110 86 86 50 50 50 18 18 18 18

--------------------------------------------------------------------------------
================================================================================

Performing a density-based AIM analysis with a wavefunction as input.
  Molecular grid    : MolGrid
  Using local grids : True
Initialized: HirshfeldWPart
  Scheme       : Hirshfeld
  Proatomic DB : <horton_part.core.proatomdb.ProAtomDB object at 0x104443c10>
Computing atomic populations.
Computing atomic charges.
Computing density decomposition for atom 0
Computing density decomposition for atom 1
Computing density decomposition for atom 2
Computing cartesian and pure AIM multipoles and radial AIM moments.
Computing atomic dispersion coefficients.
Storing proatom density spline for atom 0.
Storing proatom density spline for atom 1.
Storing proatom density spline for atom 2.
charges:
[-0.305  0.153  0.153]
cartesian multipoles:
[[-0.305 -0.    -0.    -0.253 -4.416 -0.    -0.    -3.647 -0.    -4.093 -0.    -0.    -0.637 -0.    -0.    -0.    -0.    -0.247 -0.    -1.401]
 [ 0.153 -0.     0.167 -0.131 -0.758 -0.    -0.    -0.643  0.035 -0.689 -0.     0.206 -0.2   -0.    -0.    -0.     0.468 -0.127  0.164 -0.454]
 [ 0.153  0.    -0.167 -0.131 -0.758  0.     0.    -0.643 -0.035 -0.689 -0.    -0.206 -0.2   -0.    -0.    -0.    -0.468 -0.127 -0.164 -0.454]]

Iterative Hirshfeld method

The iterative Hirshfeld method can be executed using HirshfeldIWpart class.

[6]:
hi = HirshfeldIWPart(**prepare_argument_dict(mol, grid, rho, db))
hi.do_all()
print_results(hi)

================================================================================
Information of integral grids.
--------------------------------------------------------------------------------
Grid size of molecular grid: 18460
************************************ Atom 0 ************************************
   |-- Radial grid size: 120
   |-- Angular grid sizes:
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
          6 6 6 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18
          18 18 18 18 18 18 26 26 26 26 38 38 38 38 38 50 50 50 50 50
          86 86 110 110 110 110 170 194 194 434 590 590 434 434 434 302 302 302 194 194
          170 110 110 110 110 110 110 110 110 110 110 110 86 50 50 18 18 18 18 18

************************************ Atom 1 ************************************
   |-- Radial grid size: 120
   |-- Angular grid sizes:
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 18 18 18 18 18
          18 18 18 18 18 18 18 18 18 26 26 26 38 38 38 38 38 38 38 50
          50 50 50 50 50 86 86 86 110 110 110 170 170 170 194 194 194 194 170 170
          170 110 110 110 110 110 110 110 110 110 110 86 86 50 50 50 18 18 18 18

************************************ Atom 2 ************************************
   |-- Radial grid size: 120
   |-- Angular grid sizes:
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
          6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 18 18 18 18 18
          18 18 18 18 18 18 18 18 18 26 26 26 38 38 38 38 38 38 38 50
          50 50 50 50 50 86 86 86 110 110 110 170 170 170 194 194 194 194 170 170
          170 110 110 110 110 110 110 110 110 110 110 86 86 50 50 50 18 18 18 18

--------------------------------------------------------------------------------
================================================================================

Performing a density-based AIM analysis with a wavefunction as input.
  Molecular grid    : MolGrid
  Using local grids : True
Initialized: HirshfeldIWPart
  Scheme                : Hirshfeld-I
  Convergence threshold : 1.0e-06
  Maximum iterations    : 500
  Proatomic DB          : <horton_part.core.proatomdb.ProAtomDB object at 0x104443c10>
Iteration       Change      Entropy
        1   4.91894e-02   2.00489e-01
        2   3.09349e-02   1.80862e-01
        3   1.95877e-02   1.90830e-01
        4   1.26979e-02   2.03872e-01
        5   8.40308e-03   2.14964e-01
        6   5.64913e-03   2.23432e-01
        7   3.84155e-03   2.29631e-01
        8   2.63387e-03   2.34078e-01
        9   1.81637e-03   2.37237e-01
       10   1.25775e-03   2.39468e-01
       11   8.73435e-04   2.41038e-01
       12   6.07777e-04   2.42140e-01
       13   4.23518e-04   2.42912e-01
       14   2.95413e-04   2.43453e-01
       15   2.06199e-04   2.43832e-01
       16   1.43998e-04   2.44098e-01
       17   1.00594e-04   2.44283e-01
       18   7.02895e-05   2.44413e-01
       19   4.91226e-05   2.44503e-01
       20   3.43338e-05   2.44567e-01
       21   2.39993e-05   2.44611e-01
       22   1.67764e-05   2.44642e-01
       23   1.17278e-05   2.44664e-01
       24   8.19875e-06   2.44679e-01
       25   5.73174e-06   2.44690e-01
       26   4.00710e-06   2.44697e-01
       27   2.80143e-06   2.44702e-01
       28   1.95853e-06   2.44706e-01
       29   1.36925e-06   2.44709e-01
       30   9.57279e-07   2.44710e-01

Computing atomic populations.
Computing atomic charges.
Computing density decomposition for atom 0
Computing density decomposition for atom 1
Computing density decomposition for atom 2
Computing cartesian and pure AIM multipoles and radial AIM moments.
Computing atomic dispersion coefficients.
Storing proatom density spline for atom 0.
Storing proatom density spline for atom 1.
Storing proatom density spline for atom 2.
charges:
[-0.862  0.431  0.431]
cartesian multipoles:
[[-0.862  0.    -0.     0.189 -5.147 -0.    -0.    -4.812 -0.    -5.012 -0.    -0.    -0.105  0.     0.     0.     0.     0.58  -0.     0.174]
 [ 0.431 -0.     0.077 -0.04  -0.392 -0.    -0.    -0.389  0.02  -0.375 -0.     0.088 -0.057  0.     0.    -0.     0.264 -0.039  0.083 -0.136]
 [ 0.431  0.    -0.077 -0.04  -0.392  0.     0.    -0.389 -0.02  -0.375  0.    -0.088 -0.057  0.    -0.     0.    -0.264 -0.039 -0.083 -0.136]]

Export AIM density to a cube file

First, a new cube grid must be constructed and the molecular density evaluated at the grid points. Next, the AIM density is recalculated on this cube grid using the AIM weight functions obtained from the partitioning method (e.g., h or hi).

[7]:
def update_pro(part, iatom, proatmdens, grid):
    """Update propars.

    Parameters
    ----------
    iatom : int
        The index of the atom for which the pro-atom densities are to be updated.
    proatdens : 1D np.ndarray
        The array representing the pro-atom density. This array is updated with the new density values
        for the specified atom.
    grid : Grid
        A `Grid` object, e.g., Molecular grid or a 3D cubic grid.
    """
    work = np.zeros((grid.size,))
    part.eval_proatom(iatom, work, grid)
    proatmdens += work
    proatmdens += 1e-100


def compute_aimdens(part, grid, moldens):
    """See ``Compute AIM density.

    Parameters
    ----------
    part : HirshfeldWPart or HirshfeldIWPart.
        A partitioning method.
    grid : Grid
        A `Grid` object, e.g., Molecular grid or a 3D cubic grid.
    moldens : 1D np.ndarray
        Molecular density on `grid` points.
    """
    proatmdens = np.zeros((part.natom, grid.size))
    for iatom in range(part.natom):
        update_pro(part, iatom, proatmdens[iatom, :], grid)
    promoldens = np.sum(proatmdens, axis=0)

    at_weights = np.zeros((part.natom, grid.size))
    # Compute the atomic weights by taking the ratios: pro-atom-dens/pro-molecule-dens.
    for iatom in range(part.natom):
        at_weights[iatom, :] = proatmdens[iatom, :] / promoldens
    np.clip(at_weights, 0, 1, out=at_weights)
    aimdens = at_weights * moldens[None, :]
    return proatmdens, promoldens, at_weights, aimdens
[8]:
logger = logging.getLogger("__main__")
# A denser grid points should be applied to reproduce correct population.
cube_grid = _setup_cube_grid(mol.atnums, mol.atcoords, logger, spacing=0.2)
cube_grid, data = prepare_input_cube(mol, 1000, False, False, logger, cube_grid)
cube_moldens = data["density"]
proatmdens, promoldens, at_weights, aimdens = compute_aimdens(
    hi, cube_grid, cube_moldens
)

sum_pop = 0.0
for iatom in range(hi.natom):
    pop = cube_grid.integrate(aimdens[iatom, :])
    print(f"The population of atom {iatom} using cube_grid is : {pop:.3f}")
    sum_pop += pop
    # export AIM density of each atom in water to a cube file
    to_cube(
        f"h2o_aimrho_{iatom}.cube",
        mol.atnums,
        mol.atcorenums,
        mol.atcoords,
        cube_grid,
        aimdens[iatom, :],
    )
    # export pro-atom density to a cube file
    to_cube(
        f"h2o_prorho_{iatom}.cube",
        mol.atnums,
        mol.atcorenums,
        mol.atcoords,
        cube_grid,
        proatmdens[iatom, :],
    )

# Note: the pupulation of sum of all atoms is not equal to 10 due to a coarse cube grid used.
print(f"The total population using cube_grid is : {sum_pop:.3f}")
print(
    f"The total population using partitioning grid is : {hi.grid.integrate(hi._moldens):.3f}"
)
# export molecular density of water to cube file
to_cube(
    "h2o_molrho.cube",
    mol.atnums,
    mol.atcorenums,
    mol.atcoords,
    cube_grid,
    cube_moldens,
)
# export pro-molecular density of water to cube file
to_cube(
    "h2o_promolrho.cube",
    mol.atnums,
    mol.atcorenums,
    mol.atcoords,
    cube_grid,
    cube_moldens,
)
The population of atom 0 using cube_grid is : 8.404
The population of atom 1 using cube_grid is : 0.540
The population of atom 2 using cube_grid is : 0.540
The total population using cube_grid is : 9.484
The total population using partitioning grid is : 10.000

By plotting these cube files with a visualization program such as VESTA (using an isosurface value of 0.05), one obtains the following:

image1

image2

Molecular density

Pro-molecular density

image3

image4

AIM density (O)

Pro-atom density (O)

image5

image6

AIM density (H)

Pro-atom density (H)

[9]:
import glob

# remvoe all cube files
import os

for f in glob.glob("*.cube"):
    os.remove(f)
[ ]: