Calculating the Mulliken population#
In Cedillo2012, we find a Mulliken charge of 0.20 on the carbon atom in CO. In this notebook, we will reproduce this result.
# Force the local gqcpy to be imported
import sys
sys.path.insert(0, '../../build/gqcpy/')
import gqcpy
import numpy as np
Let’s start by creating the molecule, and quantizing the Hamiltonian in the associated restricted spin-orbital basis of the AOs.
Restricted Mulliken populations#
molecule = gqcpy.Molecule.ReadXYZ("../../gqcp/tests/data/CO_mulliken.xyz" , 0) # '0': Create a neutral molecule.
N = molecule.numberOfElectrons()
spin_orbital_basis = gqcpy.RSpinOrbitalBasis_d(molecule, "STO-3G")
S = spin_orbital_basis.quantize(gqcpy.OverlapOperator())
fq_hamiltonian = gqcpy.FQMolecularHamiltonian(molecule)
hamiltonian = spin_orbital_basis.quantize(fq_hamiltonian)
Afterwards, we should solve the RHF SCF equations.
environment = gqcpy.RHFSCFEnvironment_d.WithCoreGuess(N, hamiltonian, S)
solver = gqcpy.RHFSCFSolver_d.DIIS()
objective = gqcpy.DiagonalRHFFockMatrixObjective_d(hamiltonian) # This objective makes sure that the RHF parameters represent the canonical RHF MOs.
rhf_parameters = gqcpy.RHF_d.optimize(objective, solver, environment).groundStateParameters()
D = rhf_parameters.calculateScalarBasis1DM() # The AO density matrix.
The Mulliken population is effectively the expectation value of the Mulliken operator and a density matrix, so we’ll have to set up the Mulliken operator.
Since we’re interested in determining the Mulliken charge on the carbon atom, we’ll prepare a Mulliken partitioning of the spin-orbital basis with basis functions centered on the carbon atom.
mulliken_domain = spin_orbital_basis.mullikenDomain(lambda shell: shell.nucleus().element() == "C")
Let’s also confirm that the supplied selector lambda function has selected the correct indices for the basis functions centered on the carbon atom.
print(mulliken_domain.domainIndices())
[0, 1, 2, 3, 4]
Theoretically, the Mulliken operator is the Mulliken-partitioned number/overlap operator. Since GQCP supports partitioning of any one-electron operator, we can use the .partitioned()
API.
P = mulliken_domain.projectionMatrix(spin_orbital_basis.expansion())
mulliken_operator = S.partitioned(P)
What remains is to calculate the expectation value of the Mulliken-partitioned number operator, which yields the Mulliken population. The Mulliken charge can then be calculated straightforwardly.
C_population = mulliken_operator.calculateExpectationValue(D)[0]
print("Mulliken population: ", C_population)
Mulliken population: 5.801066370434745
C_charge = 6 - C_population
print("Mulliken charge: ", C_charge)
Mulliken charge: 0.1989336295652553
Unrestricted Mulliken populations#
GQCP also offers the ability to calculate alpha- and beta- Mulliken populations. Since the previous example would be trivial, let’s use CO+ instead of CO.
molecule = gqcpy.Molecule.ReadXYZ("../../gqcp/tests/data/CO_mulliken.xyz" , +1) # '+1': Create CO+.
N_alpha = molecule.numberOfElectronPairs() + 1
N_beta = molecule.numberOfElectronPairs()
u_spin_orbital_basis = gqcpy.USpinOrbitalBasis_d(molecule, "STO-3G")
S_unrestricted = u_spin_orbital_basis.quantize(gqcpy.OverlapOperator())
fq_hamiltonian = gqcpy.FQMolecularHamiltonian(molecule)
hamiltonian = u_spin_orbital_basis.quantize(fq_hamiltonian) # We perform UHF calculations with the Hamiltonian in the basis of the restricted atomic spin-orbitals.
We solve the UHF SCF equations.
environment = gqcpy.UHFSCFEnvironment_d.WithCoreGuess(N_alpha, N_beta, hamiltonian, S_unrestricted)
solver = gqcpy.UHFSCFSolver_d.DIIS(threshold=1.0e-6, maximum_number_of_iterations=500)
uhf_parameters = gqcpy.UHF_d.optimize(solver, environment).groundStateParameters()
D_unrestricted = uhf_parameters.calculateScalarBasis1DM() # The AO density matrix.
We now analogously proceed by setting up the Mulliken partitioning scheme, and finally partition the number/overlap operator according to the obtained partitioning scheme.
u_mulliken_domain = u_spin_orbital_basis.mullikenDomain(lambda shell: shell.nucleus().element() == "C")
u_P = u_mulliken_domain.projectionMatrix(u_spin_orbital_basis.expansion())
u_mulliken_operator = S_unrestricted.partitioned(u_P)
Let’s first check the total Mulliken population and charge.
C_population_total = u_mulliken_operator.calculateExpectationValue(D_unrestricted)[0]
print("Mulliken population: ", C_population_total)
print("Mulliken charge: ", 6 - C_population_total)
Mulliken population: 5.202385763365194
Mulliken charge: 0.7976142366348062
Finally, let’s find out the Mulliken populations for the alpha and beta electrons, separately
C_population_alpha = u_mulliken_operator.alpha.calculateExpectationValue(D_unrestricted.alpha)[0]
C_population_beta = u_mulliken_operator.beta.calculateExpectationValue(D_unrestricted.beta)[0]
print("Mulliken population alpha: ", C_population_alpha)
print("Mulliken population beta: ", C_population_beta)
Mulliken population alpha: 3.5902074357563403
Mulliken population beta: 1.6121783276088537
Generalized Mulliken populations#
GQCP also allows for Mulliken analysis in case of complex orbital bases. The example below applies Mulliken population analysis on a complex GHF calculation.
molecule = gqcpy.Molecule.ReadXYZ("../../gqcp/tests/data/CO_mulliken.xyz" , 0) # '0': Create a neutral molecule.
N = molecule.numberOfElectrons()
spinor_basis = gqcpy.GSpinorBasis_cd(molecule, "STO-3G")
S_generalized = spinor_basis.quantize(gqcpy.OverlapOperator())
fq_hamiltonian = gqcpy.FQMolecularHamiltonian(molecule)
gsq_hamiltonian = spinor_basis.quantize(fq_hamiltonian)
We solve the GHF SCF equations.
ghf_environment = gqcpy.GHFSCFEnvironment_cd.WithComplexlyTransformedCoreGuess(N, gsq_hamiltonian, S_generalized)
ghf_solver = gqcpy.GHFSCFSolver_cd.Plain(threshold=1.0e-6, maximum_number_of_iterations=1500)
ghf_parameters = gqcpy.GHF_cd.optimize(ghf_solver, ghf_environment).groundStateParameters()
D_generalized = ghf_parameters.calculateScalarBasis1DM() # The AO density matrix.
We now analogously proceed by setting up the Mulliken partitioning scheme, and finally partition the number/overlap operator according to the obtained partitioning scheme.
g_mulliken_domain = spinor_basis.mullikenDomain(lambda shell: shell.nucleus().element() == "C")
g_P = g_mulliken_domain.projectionMatrix(spinor_basis.expansion())
g_mulliken_operator = S_generalized.partitioned(g_P)
Let’s first check the total Mulliken population and charge.
C_population_total_generalized = g_mulliken_operator.calculateExpectationValue(D_generalized)[0]
print("Mulliken population: ", C_population_total_generalized)
print("Mulliken charge: ", 6 - C_population_total_generalized)
Mulliken population: (5.8010659379343235+2.1089527784128929e-19j)
Mulliken charge: (0.19893406206567654-2.1089527784128929e-19j)