Wavefunction

The Wavefunction class in QDK/Chemistry represents quantum mechanical wavefunctions for molecular systems. This class provides access to wavefunction coefficients, determinants, reduced density matrices (RDM), orbital entropies [BT15], and other quantum chemical properties.

Overview

A wavefunction in quantum chemistry describes the quantum state of a molecular system. In QDK/Chemistry, the Wavefunction class encapsulates various wavefunction types, from simple single-determinant Hartree-Fock wavefunctions to complex multi-reference wavefunctions.

The class uses a container-based design where different wavefunction types (Slater determinants, configuration interaction, coupled cluster, etc.) are implemented as specialized container classes, while the main Wavefunction class provides a unified interface.

Mathematical representation

Wavefunctions are represented as linear combinations of determinants:

\[|\Psi\rangle = \sum_I c_I |\Phi_I\rangle\]

where \(c_I\) are expansion coefficients and \(|\Phi_I\rangle\) are Slater determinants.

For post-Hartree-Fock methods like coupled cluster, the wavefunction is expressed in terms of cluster operators:

\[|\Psi_{CC}\rangle = e^{\hat{T}} |\Phi_0\rangle\]

where \(\hat{T} = \hat{T}_1 + \hat{T}_2 + ...\) is the cluster operator and \(|\Phi_0\rangle\) is the reference determinant.

Container types

QDK/Chemistry supports different wavefunction container types for various quantum chemistry methods:

Slater determinant container

Single-determinant wavefunctions (e.g., from Hartree-Fock calculations).

# Use helper function to get orbitals
orbitals = make_minimal_orbitals()

# Create a simple Slater determinant wavefunction for H2 ground state
# 2 electrons in bonding sigma orbital
det = Configuration.from_spin_half_string("20")

# Constructor takes single determinant and orbitals as input
sd_container = StateVectorContainer(det, orbitals)
sd_wavefunction = Wavefunction(sd_container)
  // Use helper function to get orbitals
  std::shared_ptr<Orbitals> orbitals = make_minimal_orbitals();
  // Create a simple Slater determinant wavefunction for H2 ground state
  // 2 electrons in bonding sigma orbital
  auto det = Configuration::from_spin_half_string("20");

  // Constructor takes single determinant and orbitals as input
  auto sd_container = std::make_unique<StateVectorContainer>(det, orbitals);
  Wavefunction sd_wavefunction(std::move(sd_container));

SCI wavefunction container

Sparse multi-determinant wavefunctions for Selected Configuration Interaction methods.

# Create an SCI wavefunction for H2
# SCI selects only the most important configurations/determinants from the full space
sci_dets = [
    Configuration.from_spin_half_string(
        "20"
    ),  # both electrons in bonding MO (ground state)
    Configuration.from_spin_half_string("du"),  # alpha in bonding, beta in antibonding
    Configuration.from_spin_half_string("ud"),  # beta in bonding, alpha in antibonding
]

# Coefficients for selected determinants
sci_coeffs = np.array([0.96, 0.15, 0.15])

# Create a SCI wavefunction: requires selected coefficients and determinants, as well
# as orbitals, in constructor
sci_container = StateVectorContainer(sci_coeffs, sci_dets, orbitals)
sci_wavefunction = Wavefunction(sci_container)
  // Create an SCI wavefunction for H2
  // SCI selects only the most important configurations/determinants from the
  // full space
  std::vector<Configuration> sci_dets = {
      Configuration::from_spin_half_string("20"),  // Ground state
      Configuration::from_spin_half_string("ud"),  // Mixed state
      Configuration::from_spin_half_string("du")   // Mixed state
  };

  // Coefficients for selected determinants
  Eigen::VectorXd sci_coeffs(3);
  sci_coeffs << 0.96, 0.15, 0.15;

  // Create a SCI wavefunction: requires selected coefficients and determinants,
  // as well as orbitals, in constructor
  auto sci_container =
      std::make_unique<StateVectorContainer>(sci_coeffs, sci_dets, orbitals);
  Wavefunction sci_wavefunction(std::move(sci_container));

CAS wavefunction container

A multi-determinant wavefunction from Complete Active Space methods (CASSCF/CASCI).

# Create a CAS wavefunction for H2
# CAS(2,2) = 2 electrons in 2 MOs (bonding and antibonding)
# All possible configurations:
cas_dets = [
    Configuration.from_spin_half_string(
        "20"
    ),  # both electrons in bonding MO (ground state)
    Configuration.from_spin_half_string("ud"),  # alpha in bonding, beta in antibonding
    Configuration.from_spin_half_string("du"),  # beta in bonding, alpha in antibonding
    Configuration.from_spin_half_string("02"),  # both electrons in antibonding
]

# Coefficients (normalized later by container)
cas_coeffs = np.array([0.95, 0.15, 0.15, 0.05])

# Create a CAS wavefunction: requires all coefficients and determinants,
# as well as orbitals, in constructor
cas_container = StateVectorContainer(cas_coeffs, cas_dets, orbitals)
cas_wavefunction = Wavefunction(cas_container)
  // Create a CAS wavefunction for H2
  // CAS(2,2) = 2 electrons in 2 MOs (bonding and antibonding)
  // All possible configurations:
  std::vector<Configuration> cas_dets = {
      Configuration::from_spin_half_string(
          "20"),  // Both electrons in bonding (ground state)
      Configuration::from_spin_half_string(
          "ud"),  // Alpha in bonding, beta in antibonding
      Configuration::from_spin_half_string(
          "du"),  // Beta in bonding, alpha in antibonding
      Configuration::from_spin_half_string(
          "02")  // Both electrons in antibonding
  };

  // Coefficients
  Eigen::VectorXd cas_coeffs(4);
  cas_coeffs << 0.95, 0.15, 0.15, 0.05;  // Normalized later by the container

  // Create a CAS wavefunction : requires all coefficients and determinants, as
  // well as orbitals, in constructor
  auto cas_container =
      std::make_unique<StateVectorContainer>(cas_coeffs, cas_dets, orbitals);
  Wavefunction cas_wavefunction(std::move(cas_container));

Amplitude wavefunction container

A single container, AmplitudeContainer, represents amplitude-based correlated wavefunctions such as MP2 and coupled cluster. It stores a reference wavefunction together with T1/T2 excitation amplitudes, and records which correlated method produced them via AmplitudeType (MP2, CCSD, or Unspecified). It is pure storage: the amplitudes are supplied by the producing algorithm and are not expanded into a determinant/coefficient representation, so determinant-, coefficient-, and RDM-based accessors raise.

The following example tags the amplitudes as MP2 (T1 is zero for MP2):

# Create an MP2 wavefunction for H2
# In practice the MP2 algorithm computes the amplitudes; here we store them
# directly in an AmplitudeContainer. T1 is zero for MP2.

# Use the Slater determinant as reference
orbitals = make_minimal_orbitals()
ref_det = Configuration.from_spin_half_string("20")
sd_container = StateVectorContainer(ref_det, orbitals)
ref_wavefunction = Wavefunction(sd_container)

# MP2 amplitudes (T1 is zero for MP2)
t1_mp2 = np.zeros(1)
t2_mp2 = np.array([0.1])
mp2_container = AmplitudeContainer(
    orbitals, ref_wavefunction, AmplitudeType.MollerPlesset, t1_mp2, t2_mp2
)
mp2_wavefunction = Wavefunction(mp2_container)
  // Create an MP2 wavefunction for H2
  // MP2 uses a reference wavefunction and Hamiltonian to compute amplitudes on
  // demand

  // Use the Slater determinant as reference
  auto orbitals_mp2 = make_minimal_orbitals();
  auto ref_det = Configuration::from_spin_half_string("20");
  auto sd_container_mp2 =
      std::make_unique<StateVectorContainer>(ref_det, orbitals_mp2);
  auto ref_wavefunction =
      std::make_shared<Wavefunction>(std::move(sd_container_mp2));

  // In practice the MP2 algorithm computes the amplitudes; here we store them
  // directly in an AmplitudeContainer. T1 is zero for MP2.
  Eigen::VectorXd t1_mp2 = Eigen::VectorXd::Zero(1);
  Eigen::VectorXd t2_mp2(1);
  t2_mp2 << 0.1;
  auto mp2_container = std::make_unique<AmplitudeContainer>(
      orbitals_mp2, ref_wavefunction, AmplitudeType::MollerPlesset, t1_mp2,
      t2_mp2);
  Wavefunction mp2_wavefunction(std::move(mp2_container));

The same container holds coupled cluster amplitudes when tagged as CCSD:

# Create a coupled cluster wavefunction for H2
# CC uses a reference wavefunction and pre-computed amplitudes

# Use the Slater determinant as reference
orbitals = make_minimal_orbitals()
ref_det = Configuration.from_spin_half_string("20")
sd_container = StateVectorContainer(ref_det, orbitals)
ref_wavefunction = Wavefunction(sd_container)

# Create example T1 and T2 amplitudes
# T1: occupied-virtual excitations (1 occ × 1 virt = 1 element for H2)
t1_amplitudes = np.array([0.05])

# T2: occupied-occupied to virtual-virtual excitations
# (1 occ pair × 1 virt pair = 1 element for H2)
t2_amplitudes = np.array([0.15])

# Create CC container: requires reference wavefunction, orbitals, and amplitudes
cc_container = AmplitudeContainer(
    orbitals,
    ref_wavefunction,
    AmplitudeType.CoupledCluster,
    t1_amplitudes,
    t2_amplitudes,
)
cc_wavefunction = Wavefunction(cc_container)
  // Create a coupled cluster wavefunction for H2
  // CC uses a reference wavefunction and pre-computed amplitudes

  // Use the Slater determinant as reference
  auto orbitals_cc = make_minimal_orbitals();
  auto ref_det_cc = Configuration::from_spin_half_string("20");
  auto sd_container_cc =
      std::make_unique<StateVectorContainer>(ref_det_cc, orbitals_cc);
  auto ref_wavefunction_cc =
      std::make_shared<Wavefunction>(std::move(sd_container_cc));

  // Create example T1 and T2 amplitudes
  // T1: occupied-virtual excitations (1 occ × 1 virt = 1 element for H2)
  Eigen::VectorXd t1_amplitudes(1);
  t1_amplitudes << 0.05;

  // T2: occupied-occupied to virtual-virtual excitations
  // (1 occ pair × 1 virt pair = 1 element for H2)
  Eigen::VectorXd t2_amplitudes(1);
  t2_amplitudes << 0.15;

  // Create CC container: requires reference wavefunction, orbitals, and
  // amplitudes
  auto cc_container = std::make_unique<AmplitudeContainer>(
      orbitals_cc, ref_wavefunction_cc, AmplitudeType::CoupledCluster,
      t1_amplitudes, t2_amplitudes);
  Wavefunction cc_wavefunction(std::move(cc_container));

Properties

The Wavefunction class provides access to various quantum chemical properties. Availability depends on the specific container type:

Property availability by container type

Property

Slater determinant

CAS

SCI

MP2

Coupled cluster

Coefficients

Determinants

Electron counts

Orbital occupations

1-RDMs (spin-dependent)

1-RDMs (spin-traced)

2-RDMs (spin-dependent)

✓*

✓*

2-RDMs (spin-traced)

✓*

✓*

Orbital entropies

✓*

✓*

T1/T2 amplitudes

Overlap calculations

Norm calculations

Legend:

  • ✓ Available and implemented

  • ✗ Not available (method not implemented)

  • ✓* Implemented and available only if 2-RDMs were provided during construction

Accessing wavefunction data

The Wavefunction class provides methods to access coefficients, determinants, and derived properties:

# Access coefficient(s) and determinant(s) - SD has only one
coeffs = sd_wavefunction.get_coefficients()
dets = sd_wavefunction.get_active_determinants()

# Get orbital information
orbitals_ref = sd_wavefunction.get_orbitals()

# Get electron counts
n_alpha, n_beta = sd_wavefunction.get_total_num_electrons()

# Get RDMs
rdm1_aa, rdm1_bb = sd_wavefunction.get_active_one_rdm_spin_dependent()
rdm1_total = sd_wavefunction.get_active_one_rdm_spin_traced()
rdm2_aaaa, rdm2_aabb, rdm2_bbbb = sd_wavefunction.get_active_two_rdm_spin_dependent()
rdm2_total = sd_wavefunction.get_active_two_rdm_spin_traced()

# Get single orbital entropies
entropies = sd_wavefunction.get_single_orbital_entropies()
  // Access coefficient(s) and determinant(s) - SD has only one
  auto coeffs = sd_wavefunction.get_coefficients();
  auto dets = sd_wavefunction.get_active_determinants();

  // Get orbital information
  auto orbitals_ref = sd_wavefunction.get_orbitals();

  // Get electron counts
  auto [n_alpha, n_beta] = sd_wavefunction.get_total_num_electrons();

  // Get RDMs
  auto [rdm1_aa, rdm1_bb] = sd_wavefunction.get_active_one_rdm_spin_dependent();
  auto rdm1_total = sd_wavefunction.get_active_one_rdm_spin_traced();
  auto [rdm2_aaaa, rdm2_aabb, rdm2_bbbb] =
      sd_wavefunction.get_active_two_rdm_spin_dependent();
  auto rdm2_total = sd_wavefunction.get_active_two_rdm_spin_traced();

  // Get single orbital entropies
  auto entropies = sd_wavefunction.get_single_orbital_entropies();

Accessing cluster amplitudes

For MP2 and coupled cluster wavefunctions, one can access T1 and T2 cluster amplitudes.

# Access T1 and T2 amplitudes from MP2 and CC containers

# For MP2 - amplitudes computed on demand
t2_abab_mp2, t2_aaaa_mp2, t2_bbbb_mp2 = (
    mp2_wavefunction.get_container().get_t2_amplitudes()
)

# For CC - amplitudes stored during construction
t1_aa, t1_bb = cc_wavefunction.get_container().get_t1_amplitudes()
t2_abab_cc, t2_aaaa_cc, t2_bbbb_cc = cc_wavefunction.get_container().get_t2_amplitudes()
  // Access T1 and T2 amplitudes from MP2 and CC containers

  // MP2
  // Get the container back from wfn
  const auto& mp2_container_ref =
      mp2_wavefunction.get_container<AmplitudeContainer>();
  auto [t2_abab_mp2, t2_aaaa_mp2, t2_bbbb_mp2] =
      mp2_container_ref.get_t2_amplitudes();

  // CC
  const auto& cc_container_ref =
      cc_wavefunction.get_container<AmplitudeContainer>();
  // Amplitudes are stored already from construction
  auto [t1_aa, t1_bb] = cc_container_ref.get_t1_amplitudes();
  auto [t2_abab_cc, t2_aaaa_cc, t2_bbbb_cc] =
      cc_container_ref.get_t2_amplitudes();

Further reading