Scale SQD chemistry workflows with Dice solverΒΆ

For information on how to install and use qiskit-addon-dice-solver, visit the docs.

For more details on the SQD code used in this example, check out tutorial 1.

[1]:
%%capture

import os

import numpy as np
from pyscf import ao2mo, tools
from qiskit_addon_dice_solver import solve_fermion
from qiskit_addon_sqd.configuration_recovery import recover_configurations
from qiskit_addon_sqd.counts import counts_to_arrays, generate_counts_uniform
from qiskit_addon_sqd.fermion import (
    flip_orbital_occupancies,
)
from qiskit_addon_sqd.subsampling import postselect_and_subsample

# Specify molecule properties
num_orbitals = 16
num_elec_a = num_elec_b = 5
open_shell = False
spin_sq = 0

# Read in molecule from disk
active_space_path = os.path.abspath(os.path.join("..", "molecules", "n2_fci.txt"))
mf_as = tools.fcidump.to_scf(active_space_path)
hcore = mf_as.get_hcore()
eri = ao2mo.restore(1, mf_as._eri, num_orbitals)
nuclear_repulsion_energy = mf_as.mol.energy_nuc()

# Create a seed to control randomness throughout this workflow
rng = np.random.default_rng(24)

# Generate random samples
counts_dict = generate_counts_uniform(10_000, num_orbitals * 2, rand_seed=rng)

# Convert counts into bitstring and probability arrays
bitstring_matrix_full, probs_arr_full = counts_to_arrays(counts_dict)

# SQD options
iterations = 5

# Eigenstate solver options
n_batches = 5
samples_per_batch = 300

# Self-consistent configuration recovery loop
occupancies_bitwise = None  # orbital i corresponds to column i in bitstring matrix
e_hist = np.zeros((iterations, n_batches))
for i in range(iterations):
    # On the first iteration, we have no orbital occupancy information from the
    # solver, so we just post-select from the full bitstring set based on hamming weight.
    if occupancies_bitwise is None:
        bs_mat_tmp = bitstring_matrix_full
        probs_arr_tmp = probs_arr_full

    # In following iterations, we use both the occupancy info and the target hamming
    # weight to refine bitstrings.
    else:
        bs_mat_tmp, probs_arr_tmp = recover_configurations(
            bitstring_matrix_full,
            probs_arr_full,
            occupancies_bitwise,
            num_elec_a,
            num_elec_b,
            rand_seed=rng,
        )

    # Throw out samples with incorrect hamming weight and create batches of subsamples.
    batches = postselect_and_subsample(
        bs_mat_tmp,
        probs_arr_tmp,
        hamming_right=num_elec_a,
        hamming_left=num_elec_b,
        samples_per_batch=samples_per_batch,
        num_batches=n_batches,
        rand_seed=rng,
    )
    # Run eigenstate solvers in a loop. This loop should be parallelized for larger problems.
    int_e = np.zeros(n_batches)
    int_occs = np.zeros((n_batches, 2 * num_orbitals))
    for j, batch in enumerate(batches):
        energy_sci, wf_mags, avg_occs = solve_fermion(
            batch,
            hcore,
            eri,
            mpirun_options=["-n", "8"],
        )
        int_e[j] = energy_sci + nuclear_repulsion_energy
        int_occs[j, :num_orbitals] = avg_occs[0]
        int_occs[j, num_orbitals:] = avg_occs[1]

    # Combine batch results
    avg_occupancy = np.mean(int_occs, axis=0)
    # The occupancies from the solver should be flipped to match the bits in the bitstring matrix.
    occupancies_bitwise = flip_orbital_occupancies(avg_occupancy)

    # Track optimization history
    e_hist[i, :] = int_e
[2]:
print("Exact energy: -109.10288938")
print(f"Estimated energy: {np.min(e_hist[-1])}")
Exact energy: -109.10288938
Estimated energy: -109.03013363477585