Quick Start
This notebook provides a quick introduction to using MolGrid for molecular grid generation and integration. We’ll walk through the basic workflow of creating molecules, generating grids, and calculating electron density with PySCF.
Creating a Molecule
First, let’s create a simple water molecule using the Molecule class. The water molecule has an oxygen atom at the origin and two hydrogen atoms at specific positions.
from molgrid import Atom, Molecule
# Create atoms with their coordinates (in angstroms)
oxygen = Atom('O', [0.0, 0.0, 0.0])
hydrogen1 = Atom('H', [0.0, -0.757, 0.587])
hydrogen2 = Atom('H', [0.0, 0.757, 0.587])
# Create molecule from the atoms
water = Molecule([oxygen, hydrogen1, hydrogen2])
print(f"Created molecule: {water}")
print(f"Number of atoms: {len(water)}")
# Let's also print the atom information
print("\nAtom details:")
for i, atom in enumerate(water):
print(f" Atom {i}: {atom.symbol} at {atom.coordinate}")
Created molecule: <molgrid.molecule.Molecule object at 0x11a84f390>
Number of atoms: 3
Atom details:
Atom 0: O at [0. 0. 0.]
Atom 1: H at [ 0. -0.757 0.587]
Atom 2: H at [0. 0.757 0.587]
Creating an Atomic Grid
Now let’s create an atomic grid for the oxygen atom. Atomic grids are constructed using radial shells and angular points. The radial shells determine how many concentric spheres we have, and the angular points determine how many points on each sphere.
from molgrid import AtomicGrid
# Create atomic grid for oxygen
# nshells=10: 10 radial shells
# nangpts=110: 110 angular points per shell (using Lebedev quadrature)
oxygen_grid = AtomicGrid(oxygen, nshells=75, nangpts=302)
print(f"Oxygen atomic grid:")
print(f" Number of points: {len(oxygen_grid)}")
print(f" Coordinates shape: {oxygen_grid.coords.shape}")
print(f" Weights shape: {oxygen_grid.weights.shape}")
print(f" Total weight: {oxygen_grid.weights.sum():.6f}")
# Let's look at the first few points
print("\nFirst 5 grid points and weights:")
for i in range(5):
print(f" Point {i}: {oxygen_grid.coords[i]}, weight: {oxygen_grid.weights[i]:.6f}")
Oxygen atomic grid:
Number of points: 22650
Coordinates shape: (22650, 3)
Weights shape: (22650,)
Total weight: 59199645676.309372
First 5 grid points and weights:
Point 0: [1326.73399783 0. 0. ], weight: 50173399.244666
Point 1: [-1326.73399783 0. 0. ], weight: 50173399.244666
Point 2: [ 0. 1326.73399783 0. ], weight: 50173399.244666
Point 3: [ 0. -1326.73399783 0. ], weight: 50173399.244666
Point 4: [ 0. 0. 1326.73399783], weight: 50173399.244666
Creating a Molecular Grid
A molecular grid combines atomic grids from all atoms in the molecule. We use Becke partitioning to assign weights to each grid point based on its proximity to different atoms. This prevents double-counting of electron density.
from molgrid import MolecularGrid
# Create molecular grid with Becke partition
# partition_method='becke': Use Becke partitioning for grid partition
mol_grid = MolecularGrid(water, nshells=75, nangpts=302, partition_method='becke')
print(f"Molecular grid:")
print(f" Number of points: {len(mol_grid)}")
print(f" Coordinates shape: {mol_grid.coords.shape}")
print(f" Weights shape: {mol_grid.weights.shape}")
print(f" Total weight: {mol_grid.weights.sum():.6f}")
# Compare with unpartitioned grid
mol_grid_no_prune = MolecularGrid(water, nshells=75, nangpts=302, partition_method=None)
print(f"\nWithout partitioning: {len(mol_grid_no_prune)} points")
print(f" Total weight: {mol_grid_no_prune.weights.sum():.6f}")
Molecular grid:
Number of points: 67950
Coordinates shape: (67950, 3)
Weights shape: (67950,)
Total weight: 28788884690.918945
Without partitioning: 67950 points
Total weight: 82701356864.936829
Total weight: 82701356864.936829
Iterating Through Atomic Grids
You can iterate through the atomic grids that make up the molecular grid. This is useful for analyzing contributions from individual atoms.
print("Atomic grids in pruned molecular grid:")
total_points = 0
for i, atomic_grid in enumerate(mol_grid):
points = len(atomic_grid)
total_points += points
print(f" Atom {i} ({atomic_grid.atom.symbol}): {points} points, weight sum: {atomic_grid.weights.sum():.6f}")
print(f"\nTotal points from atomic grids: {total_points}")
print(f"Matches molecular grid total: {total_points == len(mol_grid)}")
Atomic grids in pruned molecular grid:
Atom 0 (O): 22650 points, weight sum: 21244363291.203724
Atom 1 (H): 22650 points, weight sum: 3772260699.857614
Atom 2 (H): 22650 points, weight sum: 3772260699.857613
Total points from atomic grids: 67950
Matches molecular grid total: True
Calculating Electron Density
Now let’s calculate the electron density using PySCF. We’ll use the molecular grid we created to integrate the electron density and get the total number of electrons in the molecule.
import numpy as np
from pyscf import gto, dft
from pyscf.dft import numint
# Create PySCF molecule (same geometry as our MolGrid molecule)
mol = gto.M(atom='O 0 0 0; H 0 -0.757 0.587; H 0 0.757 0.587', basis='6-31g', verbose=0)
# Run DFT calculation to get the electron density
mf = dft.RKS(mol)
mf.kernel()
rdm1 = mf.make_rdm1() # Get the 1-electron reduced density matrix
# Evaluate atomic orbitals on our grid
ao = numint.eval_ao(mol, mol_grid.coords, deriv=0)
# Calculate electron density at each grid point
rho = numint.eval_rho(mol, ao, rdm1, xctype='LDA')
# Integrate electron density over the entire grid
number_of_electrons = np.sum(rho * mol_grid.weights)
print(f"Electron density calculation results:")
print(f" Calculated number of electrons: {number_of_electrons:.6f}")
print(f" Expected number of electrons: 10.000000")
print(f" Error: {abs(number_of_electrons - 10.0):.6f}")
# Let's also calculate electron density for each atomic grid
print("\nElectron distribution by atom:")
total_electrons = 0
for i, atomic_grid in enumerate(mol_grid):
# Evaluate AO on atomic grid
ao_atom = numint.eval_ao(mol, atomic_grid.coords, deriv=0)
# Calculate density on atomic grid
rho_atom = numint.eval_rho(mol, ao_atom, rdm1, xctype='LDA')
# Integrate electron density on atomic grid
atom_electrons = np.sum(rho_atom * atomic_grid.weights)
total_electrons += atom_electrons
print(f" Atom {i} ({atomic_grid.atom.symbol}): {atom_electrons:.6f} electrons")
print(f"\nTotal electrons from atomic grids: {total_electrons:.6f}")
print(f"Matches molecular total: {abs(total_electrons - number_of_electrons) < 1e-10}")
Electron density calculation results:
Calculated number of electrons: 10.000001
Expected number of electrons: 10.000000
Error: 0.000001
Electron distribution by atom:
Atom 0 (O): 6.820235 electrons
Atom 1 (H): 1.589883 electrons
Atom 2 (H): 1.589883 electrons
Total electrons from atomic grids: 10.000001
Matches molecular total: True