Build Qiskit circuits for the LUCJ ansatz¶
This guide provides some examples of building and transpiling Qiskit circuits to implement the LUCJ ansatz.
[1]:
import warnings
import pyscf
import pyscf.cc
from qiskit.circuit import QuantumCircuit, QuantumRegister
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.transpiler import CouplingMap
import ffsim
from ffsim.qiskit import generate_lucj_pass_manager
warnings.formatwarning = lambda msg, *args, **kwargs: f"Warning: {msg}\n"
LUCJ circuit for a closed-shell molecule¶
For a closed-shell system, use the spin-balanced LUCJ ansatz. This example creates an LUCJ circuit for a nitrogen molecule in the 6-31g basis and transpiled it for generic backend with square connectivity.
[2]:
# Build N2 molecule
mol = pyscf.gto.Mole()
mol.build(
atom=[["N", (0, 0, 0)], ["N", (1.0, 0, 0)]],
basis="6-31g",
symmetry="Dooh",
)
# Define active space
n_frozen = 2
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 CCSD t2 amplitudes for initializing the ansatz
ccsd = pyscf.cc.CCSD(
scf, frozen=[i for i in range(mol.nao_nr()) if i not in active_space]
).run()
# Use 2 ansatz layers
n_reps = 2
# Define interactions
pairs_aa = [(p, p + 1) for p in range(norb - 1)]
pairs_ab = (
None # Use generate_lucj_pass_manager() to find alpha-beta interactions later
)
# Create a generic backend with square connectivity
cmap_square = CouplingMap.from_grid(num_rows=12, num_columns=10)
backend = GenericBackendV2(
num_qubits=cmap_square.size(),
basis_gates=["cp", "xx_plus_yy", "p", "x", "swap"],
coupling_map=cmap_square,
)
# Generate a LUCJ-specific configured Qiskit preset pass manager
# and get pairs_ab, implementable on a square coupling map.
# The pass manager is confifgured with pre-init passes suggested by
# ffsim (ffsim.qiskit.PRE_INIT) and an error-aware LUCJ-friendly layout
pass_manager, pairs_ab = generate_lucj_pass_manager(
backend=backend,
norb=norb,
connectivity="square",
interaction_pairs=(pairs_aa, pairs_ab),
optimization_level=3,
)
# Use the backend implementable pairs_ab to construct the ucj_op
ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(
ccsd.t2,
t1=ccsd.t1,
n_reps=n_reps,
interaction_pairs=(pairs_aa, pairs_ab),
# Setting optimize=True enables the "compressed" factorization.
# Additionally, you may want to set the multi_stage_start or multi_stage_step
# arguments (or both) to obtain a better result at increased computational cost.
# See the API documentation for details.
optimize=True,
options=dict(maxiter=100),
)
# Construct circuit
qubits = QuantumRegister(2 * norb)
circuit = QuantumCircuit(qubits)
circuit.append(ffsim.qiskit.PrepareHartreeFockJW(norb, nelec), qubits)
circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)
circuit.measure_all()
# Transpile the circuit with configured pass manager
transpiled = pass_manager.run(circuit)
transpiled.count_ops()
WARN: Unable to to identify input symmetry using original axes.
Different symmetry axes will be used.
converged SCF energy = -108.835236570774
norb = 16
nelec = (5, 5)
E(CCSD) = -109.0398256929733 E_corr = -0.2045891221988307
Warning: Backend cannot accommodate pairs_ab=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10), (11, 11), (12, 12), (13, 13), (14, 14), (15, 15)].
Removing interaction (15, 15) from the end.
Warning: Backend cannot accommodate pairs_ab=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10), (11, 11), (12, 12), (13, 13), (14, 14)].
Removing interaction (14, 14) from the end.
Warning: Backend cannot accommodate pairs_ab=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10), (11, 11), (12, 12), (13, 13)].
Removing interaction (13, 13) from the end.
Warning: Backend cannot accommodate pairs_ab=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10), (11, 11), (12, 12)].
Removing interaction (12, 12) from the end.
[2]:
OrderedDict([('xx_plus_yy', 590),
('cp', 84),
('p', 64),
('measure', 32),
('x', 10),
('barrier', 1)])
As we initially set pairs_ab=None, the generate_lucj_pass_manager() starts with a default one. For square lattice, it is [(0, 0), (1, 1), (2, 2), ..., (norb-1, norb-1)]. However, if the hardware connectivity does not accommodate all starting pairs, then pairs are dropped from the end of the list until a valid layout is found, and a warning is raised. In the example above, last four pairs, from (15, 15) to (12, 12), are dropped with a warning message showing the dropped pairs.
Note that you can use a different pairs_ab during the ucj_op construction instead of the one returned by generate_lucj_pass_manager, but doing so may incur a higher SWAP overhead.
LUCJ circuit for an open-shell molecule¶
For an open-shell system, use the spin-unbalanced LUCJ ansatz. This example creates an LUCJ circuit for a hydroxyl radical in the 6-31g basis.
[3]:
# Build HO molecule
mol = pyscf.gto.Mole()
mol.build(
atom=[["H", (0, 0, 0)], ["O", (0, 0, 1.1)]],
basis="6-31g",
spin=1,
symmetry="Coov",
)
# Get molecular data and Hamiltonian
scf = pyscf.scf.ROHF(mol).run()
mol_data = ffsim.MolecularData.from_scf(scf)
norb, nelec = mol_data.norb, mol_data.nelec
mol_hamiltonian = mol_data.hamiltonian
print(f"norb = {norb}")
print(f"nelec = {nelec}")
# Get CCSD t2 amplitudes for initializing the ansatz
ccsd = pyscf.cc.CCSD(scf).run()
# Use 4 layers from opposite-spin amplitudes and 2 layers from same-spin amplitudes
n_reps = (4, 2)
# Use interactions implementable on a square lattice
pairs_aa = [(p, p + 1) for p in range(norb - 1)]
pairs_ab = [(p, p) for p in range(norb)] # Update it later; can be ``None`` also
pairs_bb = [(p, p + 1) for p in range(norb - 1)] # Can be ``None`` as well
# Generate the configured preset pass manager and implementable pairs_ab
pass_manager, pairs_ab = generate_lucj_pass_manager(
backend=backend,
norb=norb,
connectivity="square",
interaction_pairs=(pairs_aa, pairs_ab, pairs_bb),
optimization_level=3,
)
# Use the backend implementable pairs_ab to construct the ucj_op
ucj_op = ffsim.UCJOpSpinUnbalanced.from_t_amplitudes(
ccsd.t2,
t1=ccsd.t1,
n_reps=n_reps,
interaction_pairs=(pairs_aa, pairs_ab, pairs_bb),
# Setting optimize=True enables the "compressed" factorization.
# Additionally, you may want to set the multi_stage_start or multi_stage_step
# arguments (or both) to obtain a better result at increased computational cost.
# See the API documentation for details.
optimize=True,
options=dict(maxiter=100),
)
# Construct circuit
qubits = QuantumRegister(2 * norb)
circuit = QuantumCircuit(qubits)
circuit.append(ffsim.qiskit.PrepareHartreeFockJW(norb, nelec), qubits)
circuit.append(ffsim.qiskit.UCJOpSpinUnbalancedJW(ucj_op), qubits)
circuit.measure_all()
# Transpile the circuit with configured pass manager
transpiled = pass_manager.run(circuit)
transpiled.count_ops()
SCF not converged.
SCF energy = -75.3484557060049
norb = 11
nelec = (5, 4)
WARN: RCCSD method does not support ROHF method. ROHF object is converted to UHF object and UCCSD method is called.
E(UCCSD) = -75.45619739145242 E_corr = -0.1077416854475011
[3]:
OrderedDict([('xx_plus_yy', 715),
('p', 132),
('cp', 128),
('measure', 22),
('x', 9),
('barrier', 1)])
As norb=11 in this example, pairs_ab = [(0, 0), ..., (10, 10)], which the backend can accommodate. Therefore, this example does not raise warnings about dropped pairs.