How to build Qiskit circuits for Trotterized Hamiltonian simulation

This guide provides some examples of building and transpiling Qiskit circuits for Trotterized time evolution of various Hamiltonians.

[1]:
import numpy as np
import pyscf
from qiskit.circuit import QuantumCircuit, QuantumRegister
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

import ffsim

Molecular Hamiltonian in the double-factorized representation

[2]:
# Build N2 molecule
mol = pyscf.gto.Mole()
mol.build(
    atom=[["N", (0, 0, 0)], ["N", (1.0, 0, 0)]],
    basis="sto-6g",
    symmetry="Dooh",
)

# Define active space
n_frozen = pyscf.data.elements.chemcore(mol)
active_space = range(n_frozen, mol.nao_nr())

# Get molecular data and Hamiltonian
scf = pyscf.scf.RHF(mol).run()
mol_data = ffsim.MolecularData.from_scf(scf, active_space=active_space)
norb, nelec = mol_data.norb, mol_data.nelec
mol_hamiltonian = mol_data.hamiltonian
print(f"norb = {norb}")
print(f"nelec = {nelec}")

# Get Hamiltonian in double-factorized representation
df_hamiltonian = ffsim.DoubleFactorizedHamiltonian.from_molecular_hamiltonian(
    mol_hamiltonian
)

# Construct circuit
# For the initial state, take the Hartree-Fock state
qubits = QuantumRegister(2 * norb, name="q")
circuit = QuantumCircuit(qubits)
circuit.append(ffsim.qiskit.PrepareHartreeFockJW(norb, nelec), qubits)
circuit.append(
    ffsim.qiskit.SimulateTrotterDoubleFactorizedJW(
        df_hamiltonian, time=1.0, n_steps=1, order=0
    ),
    qubits,
)
circuit.measure_all()

# Create a generic backend and transpile the circuit to it
backend = GenericBackendV2(2 * norb, basis_gates=["cp", "xx_plus_yy", "p", "x"])
pass_manager = generate_preset_pass_manager(optimization_level=3, backend=backend)
# Set the pre-initialization stage of the pass manager with passes suggested by ffsim
pass_manager.pre_init = ffsim.qiskit.PRE_INIT
transpiled = pass_manager.run(circuit)

transpiled.count_ops()
converged SCF energy = -108.464957764796
norb = 8
nelec = (5, 5)
[2]:
OrderedDict([('xx_plus_yy', 1946),
             ('cp', 1506),
             ('p', 462),
             ('measure', 16),
             ('x', 10),
             ('barrier', 1)])

Fermi-Hubbard model with split-operator method

[3]:
# Build Hubbard model and set number of electrons
norb_x = 4
norb_y = 4
norb = norb_x * norb_y
nelec = (2, 2)
n_alpha, n_beta = nelec
op = ffsim.fermi_hubbard_2d(
    norb_x=norb_x,
    norb_y=norb_y,
    tunneling=1.0,
    interaction=4.0,
    chemical_potential=0.0,
    periodic_x=True,
    periodic_y=False,
)

# Convert the FermionOperator to a DiagonalCoulombHamiltonian
dc_hamiltonian = ffsim.DiagonalCoulombHamiltonian.from_fermion_operator(op)

# Construct circuit
# For the initial state, take the ground state of the one-body part of the Hamiltonian
qubits = QuantumRegister(2 * norb, name="q")
circuit = QuantumCircuit(qubits)
eigs, orbital_rotation = np.linalg.eigh(dc_hamiltonian.one_body_tensor)
circuit.append(
    ffsim.qiskit.PrepareSlaterDeterminantJW(
        norb,
        occupied_orbitals=(range(n_alpha), range(n_beta)),
        orbital_rotation=orbital_rotation,
    ),
    qubits,
)
circuit.append(
    ffsim.qiskit.SimulateTrotterDiagCoulombSplitOpJW(
        dc_hamiltonian, time=1.0, n_steps=1, order=0
    ),
    qubits,
)
circuit.measure_all()

# Create a generic backend and transpile the circuit to it
backend = GenericBackendV2(2 * norb, basis_gates=["cp", "xx_plus_yy", "p", "x"])
pass_manager = generate_preset_pass_manager(optimization_level=3, backend=backend)
# Set the pre-initialization stage of the pass manager with passes suggested by ffsim
pass_manager.pre_init = ffsim.qiskit.PRE_INIT
transpiled = pass_manager.run(circuit)

transpiled.count_ops()
[3]:
OrderedDict([('xx_plus_yy', 514),
             ('p', 54),
             ('measure', 32),
             ('cp', 16),
             ('x', 4),
             ('barrier', 1)])