3.1. Working with Orbitals in PyBEST
The (molecular) orbitals in PyBEST contain information on the AO/MO coefficient
matrix, the orbital occupation numbers, and the orbital energies. Internally,
the orbitals
are an instance of the DenseOrbital
class.
Specifically, the AO/MO coefficients, occupation numbers, and energies are
attributes of the DenseOrbital
class.
In the following, some simple orbital manipulations are explained that might be
useful for users.
3.1.1. Creating an empty set of orbitals
Prior to any wave function calculation, the orbitals need to be created as an
instance of the DenseOrbital
class
# Create an empty set of orbitals
orb_a = lf.create_orbital()
where lf
is an instance of the LinalgFactory
class, where all linear algebra and tensor contraction operations are defined.
3.1.2. Printing orbital information
The AO/MO coefficient matrix, the orbital occupation numbers, and the orbital
energies can be accessed as follows (assuming orb
to be an instance of
DenseOrbital
)
# Print AO/MO coefficients
print(orb.coeffs)
# Print occupations
print(orb.occupations)
# Print orbital energies
print(orb.energies)
The AO/MO coefficients can also be printed in a more readable format using the
print_ao_mo_coeffs()
function
# Print all AO/MO coefficients in a more readable format
print_ao_mo_coeffs(gobasis, orb)
where gobasis
is an instance of the GOBasis
class. If only a subset of
orbitals is required, this can be achieved using optional arguments
# Print AO/MO coefficients in a more readable format for orbitals
# no. 4 to no. 31 (including)
print_ao_mo_coeffs(gobasis, orb, begin=4, end=31)
where begin
and end
are the first and last index, respectively, of the
orbital to be printed. Note that the orbitals are indexed using Fortran
indexing, that is, beginning with 1. The order of the AOs is according to the list of atoms in the
geometry input. In the output, each column corresponds to one molecular orbital.
Note
In all electronic structure methods that optimize the orbitals, the current orbitals are always overwritten with the most recent ones.
To save a copy of the current orbitals during the execution of the PyBEST code,
use the copy()
method of
DenseOrbital
,
# Create a true copy of the current orbitals
orb_output = orb.copy()
3.1.3. Writing orbitals to a checkpoint file
In PyBEST, all electronic structure methods save their own copy of the molecular
orbitals and dump them in their method-dependent checkpoint files.
In case, you want to write the molecular orbitals to a separate file at any
stage of the program (represented in PyBEST’s internal format), you can use
the IOData
container (see also
Input/Output Operations: the IOData Container for more details on how to use the container),
# Dump orbitals for restart
mol = IOData(olp=olp, orb_a=orb)
mol.to_file('checkpoint.h5')
where the overlap matrix is stored in olp
and the orbitals in orb_a
.
The molecular orbitals (for restricted orbitals or alpha spin)
are always associated with orb_a
, while the
overlap matrix is associated with olp
. In case of unrestricted orbitals,
PyBEST uses the attribute name orb_b
. If the user chooses a different
naming convention, PyBEST might run into unexpected errors (for instance,
during restarts). See also Input/Output Operations: the IOData Container for more information on how
to use the IOData container and all default variable names.
Note
To use previously dumped orbitals for restart purposes, the overlap matrix has to be written to file as well.
3.1.4. Reading orbitals from a checkpoint file
Most electronic structure methods in PyBEST feature a restart option, which reads and projects some previously stored orbitals automatically. The documentation below is mentioned for reasons of completeness to allow the user to manually read in orbitals from disk.
3.1.4.1. PyBEST’s internal format
Similar to the recipe of dumping molecular orbitals to file, they can be read
from disk using the IOData
container functionality.
First, some previously stored PyBEST checkpoint file that contains the molecular
orbitals and overlap integrals has to be read in,
# Read some PyBEST checkpoint file and store the corresponding data in 'old'
old = IOData.from_file('checkpoint.h5')
The molecular orbitals and overlap matrix can be accessed from old
as
old.orb_a
and old.olp
, respectively.
If the molecular geometry of the restart orbitals differs from the current
molecular geometry, the molecular orbitals have to be projected against the
current molecular orbitals orb
and current overlap matrix olp
,
# Project orbitals read from file against current orbitals
project_orbitals(old.olp, olp, old.orb_a, orb)
The project_orbitals()
function overwrites
orb
and olp
with the projected restart orbitals and overlap matrix,
respectively.
Note
If the restart orbitals are obtained for a different molecular geometry,
the project_orbitals()
function
has to be used, otherwise all electronic structure calculations will be
corrupted.
3.1.4.2. External file format
Orbital information can also be imported from external programs. Supported
file formats are .mkl
and .molden
. Similar to the procedure mentioned
above (PyBEST’s internal format), the IOData
container is used to read in some external file. The file extension is handled
automatically.
# Load molden file file
mol = IOData.from_file('water.molden')
# Print the total number of molecular orbitals
print(mol.orb_a.nfn)
Similarly, if the orbitals are not projected onto a new basis set, the
Hamiltonian has to be recomputed for the external basis set stored as
mol.gobasis
.
The mol instance has the following attributes
- mol.gobasis:
A
Basis
instance. It contains all basis set information.- mol.orb_a:
A
DenseOrbital
instance containing the molecular orbitals for alpha spin- mol.orb_b:
A
DenseOrbital
instance containing the molecular orbitals for beta spin (if present)- mol.coordinates:
A np.array containing the xyz coordinates
- mol.atom:
A list of strings with the element names
3.1.5. Rotating orbitals
The molecular orbitals can be perturbed by various rotations. The user has the choice between (a series) of 2-by-2 rotations or a random rotation of all molecular orbitals.
3.1.5.1. Givens rotation (2-by-2 rotations)
Two molecular orbitals can be rotated using the
rotate_2orbitals()
method.
Depending on the angle of rotation, the selected orbitals are either mixed or
swapped. By default, the function rotates the HOMO and LUMO orbitals by 45 degrees.
# Mix HOMO and LUMO orbitals
orb.rotate_2orbitals()
If a different pair of orbitals is to be rotated, the corresponding orbital indices can be specified as optional arguments,
# Rotate 7th and 12th orbital by 60 deg; note zero-based indexing
orb.rotate_2orbitals(60*deg, 6, 11)
Keep in mind that PyBEST uses radians as unit for angles, that is,
60*deg == 60*np.pi/180
.
Since rotate_2orbitals()
is a class
method (defined in DenseOrbital
),
all quantities use Python-based indexing, starting with 0.
The rotate_2orbitals()
method can
be applied in sequence if several orbitals are to be rotated with each other,
# Perform series of rotations: 7/12 by 60 degree, 8/14 by 20 degree, 9/26
# by 80 degree
orb.rotate_2orbitals(60*deg, 6, 11)
orb.rotate_2orbitals(20*deg, 7, 13)
orb.rotate_2orbitals(80*deg, 8, 25)
Note that an angle of 90 degrees is equivalent to swapping the corresponding
orbitals. This functionality can also be achieved using the
swap_orbitals()
method
(see Swapping orbitals below).
3.1.5.2. Random orbital rotations
All molecular orbitals can be perturbed simultaneously using the
rotate_random()
method.
This function method performs a random unitary rotation to all
molecular orbitals.
# perform random unitary rotation of orbitals orb
orb.rotate_random()
Note
All orbital rotation operations leave the occupation numbers and orbital energies unchanged. Only the MO coefficients are perturbed. The AOs remain always untouched.
3.1.6. Swapping orbitals
Two molecular orbitals can be swapped using the
swap_orbitals()
method.
The list of orbital swaps is passed as a 2-dimensional numpy array, where each
row corresponds to one transposition of orbitals. Since
swap_orbitals()
is again a class
method, the indexing starts with 0.
# Swap some orbitals; note zero-based indexing
swaps = np.array([[0, 6], [5, 9]])
orb.swap_orbitals(swaps)
By default, the orbital occupations and orbital energies are swapped as well. In some cases (like in SCF calculations), the swapping of orbital occupations and/or orbital energies should be suppressed to provide a perturbed guess for the molecular orbitals. These features can be switched on using optional arguments,
# Swap only AO/MO coeffients and leave occupations and energies unchanged;
# note zero-based indexing
swaps = np.array([[0, 6], [5, 9]])
orb.swap_orbitals(swaps, skip_occs=True, skip_energies=True)
3.1.7. Orbital localization
The current version of PyBEST supports only Pipek-Mezey localization.
For more details on Pipek-Mezey localization please refer to Localization of molecular orbitals.
Due to reasons of completeness, we will only briefly explain the
syntax on how to perform a localization of molecular orbitals.
In the Pipek-Mezey localization algorithm, the occupied and virtual block of the
(canoncial Hartree-Fock) orbitals are localized using Mulliken projectors.
In order to perform a localization, the Mulliken projectors need to be determined.
To do so, an instance of the Basis
class
(here gobasis
) and of the LinalgFactory
class (here lf
) are required.
###############################################################################
## Define Mulliken projectors #################################################
###############################################################################
mulliken = get_mulliken_operators(gobasis)
###############################################################################
## Pipek-Mezey localizaton ####################################################
###############################################################################
loc = PipekMezey(lf, occ_model, mulliken)
As soon as the Mulliken projectors are calculated, they can be passed to the
PipekMezey
class. The occupied and
virtual blocks can be localized individually by two separate function calls,
###############################################################################
## occupied block #############################################################
###############################################################################
loc(hf_output, "occ")
###############################################################################
## virtual block ##############################################################
###############################################################################
loc(hf_output, "virt")
Note
The orbitals have to passed as an IOData
container, here hf_results
. The corresponding
attribute orb_a
will be overwritten in the localization procedure.
Frozen core orbitals are also supported during the localization of the occupied
orbitals. To define a frozen core, the ncore
argument has to be defined during
the initialization of the PipekMezey
class
###############################################################################
## Pipek-Mezey localizaton with one frozen core orbital #######################
###############################################################################
loc = PipekMezey(lf, occ_model, mulliken, ncore=1)
The actual localization is invoked as in the case of no frozen core orbitals.
3.1.7.1. Example Python scripts
The following, we list a complete example script for a restricted Hartree-Fock calculation of the water molecule followed by orbital localization.
from pybest import context
from pybest.gbasis import get_gobasis
from pybest.gbasis.gobasis import (
compute_eri,
compute_kinetic,
compute_nuclear,
compute_nuclear_repulsion,
compute_overlap,
)
from pybest.io.iodata import IOData
from pybest.linalg.dense import DenseLinalgFactory
from pybest.localization import PipekMezey
from pybest.part.mulliken import get_mulliken_operators
from pybest.scf import AufbauOccModel
from pybest.wrappers.hf import RHF
###############################################################################
## Set up molecule, define basis set ##########################################
###############################################################################
# Use the XYZ file from PyBEST's test data directory.
fn_xyz = context.get_fn("test/water.xyz")
gobasis = get_gobasis("cc-pvdz", fn_xyz)
###############################################################################
## Define Occupation model, expansion coefficients and overlap ################
###############################################################################
lf = DenseLinalgFactory(gobasis.nbasis)
occ_model = AufbauOccModel(5)
orb_a = lf.create_orbital(gobasis.nbasis)
olp = compute_overlap(gobasis)
###############################################################################
## Construct Hamiltonian ######################################################
###############################################################################
kin = compute_kinetic(gobasis)
na = compute_nuclear(gobasis)
er = compute_eri(gobasis)
external = compute_nuclear_repulsion(gobasis)
###############################################################################
## Do a Hartree-Fock calculation ##############################################
###############################################################################
hf = RHF(lf, occ_model)
hf_output = hf(kin, na, er, external, olp, orb_a)
3.1.8. Generating molden files
PyBEST uses libint2 to dump molden files. As all I/O operations, the generation
of molden files is handled by the IOData
container
class. The corresponding class instance has to contain an instance of the
orbitals, for which the molden file has to be generated, as well as an instance
of the Basis
class. The latter contains
all necessary information to dump the molden file.
###############################################################################
## Define Mulliken projectors #################################################
###############################################################################
mulliken = get_mulliken_operators(gobasis)
###############################################################################
## Pipek-Mezey localizaton ####################################################
###############################################################################
loc = PipekMezey(lf, occ_model, mulliken)
###############################################################################
## occupied block #############################################################
###############################################################################
loc(hf_output, "occ")
###############################################################################
## virtual block ##############################################################
###############################################################################
loc(hf_output, "virt")
###############################################################################
## dump Molden file; hf_ already contains the orb_a attribute #################
###############################################################################
hf_output.gobasis = gobasis
hf_output.to_file("water.molden")
Specifying the file ending molden
tells the
to_file()
method to dump a molden file to
disk.
In the case of unrestricted orbitals, both alpha and beta orbitals have to be
passed to the IOData
container.
We can also construct an IOData
container
from scratch that contains all required information,
molden = IOData(gobasis=gobasis,
orb_a=orb_a,
orb_b=orb_b
)
molden.to_file("water.molden")