Generate SqDRIFT Circuits

SqDRIFT is a variant of SQD that replaces the need to choose an ansatz from which to sample bitstrings with an ensemble of time-evolution circuits constructed directly from the target Hamiltonian. This is achieved by subsampling smaller time-evolution operators from said Hamiltonian based on its coefficients, which is known as the qDRIFT Trotterization method.

This tutorial shows how to generate an ensemble of such randomized circuits.

1. Hamiltonian Setup

For the purposes of this tutorial, we load the electronic structure Hamiltonian of N2 from an FCIDUMP file. Of course, there are also other means of constructing the FermionOperator. Be sure to check out its documentation as well as qiskit_fermions.operators.library.

>>> from qiskit_fermions.operators.library import FCIDump
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> fcidump = FCIDump.from_file("docs/tutorials/n2.fcidump")
>>> num_modes = 2 * fcidump.norb
>>> hamil = FermionOperator.from_fcidump(fcidump)
#include <qiskit_fermions.h>

QfFCIDump* fcidump = qf_fcidump_from_file("docs/tutorials/n2.fcidump");
QfFermionOperator* hamil = qf_ferm_op_from_fcidump(fcidump);
uint32_t num_modes = 2 * qf_fcidump_norb(fcidump);

2. Group Hamiltonian terms

In this step, we exploit the many symmetries that are present in the electronic structure Hamiltonian by grouping related terms with identical coefficients. While doing so changes the operator coefficient distribution which the qDRIFT protocol samples from, this does not affect its convergence guarantees. Crucially, the grouping of terms related by symmetry results in a favorable cancellation of Pauli terms resulting in an overall shorter circuit depth, when time-evolving a state under their action.

The qiskit_fermions.operators.grouping module provides convenience functions for grouping the terms of an operator. This is explained in more detail in this guide.

>>> from qiskit_fermions.operators.grouping import group_terms_by_electronic_structure
>>>
>>> exit_code = group_terms_by_electronic_structure(hamil, num_modes)
>>> assert exit_code is None
QfExitCode exit = qf_group_terms_by_electronic_structure(hamil, num_modes, false);

3. Prepare the Time-Evolution Circuit

In this step, we prepare the time evolution circuit of our Hamiltonian as the base circuit from which to draw samples. The qiskit_fermions.circuit.library provides us will all the required components to do so, in a way that fits naturally with Qiskit’s conventions.

>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>>
>>> time = 1.0  # you can choose a desired scaling factor here
>>> evo_gate = Evolution(num_modes, hamil, time)
>>>
>>> circ = FermionicCircuit(num_modes)
>>> circ.append(evo_gate, circ.modes)
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions.

Note

In this example, we neither initialize the fermionic modes with particles, nor measure their final state.

4. Transpile the circuit with QDrift Trotterization

The qiskit_fermions.transpiler module integrates directly with Qiskit’s transpilation pipeline, allowing the FermionicCircuit constructed above to be directly transpiled to a QuantumCircuit.

Here, we are using the jordan_wigner() fermion-to-qubit mapping to convert the Hamiltonian expressed in terms of fermions to Pauli strings. This can be done directly as part of the transpilation process through the use of the EvolutionSynthesis transpilation pass plugin. Here, we are using generate_preset_jw_pass_manager() as a short-hand for building a FermionicStagedPassManager which ensures the consistent use of the Jordan-Wigner encoding for all circuit instructions.

Crucially, we add the QDriftTrotterization transpilation pass to the optimization stage of the transpilation pipeline. This ensures that we do not use the time evolution of the entire Hamiltonian, a circuit whose depth would exceed the capabilities of currently available quantum computing hardware.

Instead, it will subsample a fixed number of groups of Hamiltonian terms for each circuit, every time we transpile the circuit. Through this, we can generate multiple circuit randomizations as required by the SqDRIFT algorithm by repeatedly running the transpilation pipeline.

This step also introduces the few parameters with which one can tweak the ensemble of circuits to generate:

  • the number of circuits to generate: num_sqdrift_randomizations

  • the length of each circuit in terms of excitation groups: num_groups

>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>> from qiskit_fermions.transpiler.passes import QDriftTrotterization
>>>
>>> num_groups = 10
>>> qdrift = QDriftTrotterization(num_groups, rng=42)
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> pm.optimization = FermionicPassManager([qdrift])
>>>
>>> num_sqdrift_randomizations = 10
>>> sqdrift_circuits = [
...     pm.run(circ) for _ in range(num_sqdrift_randomizations)
... ]
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.

Note

In the example above we have fixed the seed for the random number generator used inside of the QDriftTrotterization transpilation pass.

(Optional) Optimize the fermionic mode indexing

One can add an additional optimization step to the transpilation pipeline which minimizes the distance of the fermionic excitation spans by relabeling the fermionic mode indices. This optimization was introduced in the SqDRIFT paper and is implemented by build_excitation_span_minimization_model(). It can be easily inserted into the transpiler pipeline via the RelabelModes pass:

>>> from pyomo.environ import SolverFactory
>>> from qiskit_fermions.transpiler.passes import RelabelModes
>>>
>>> solver = SolverFactory("appsi_highs")
>>> solver.options["time_limit"] = 60
>>>
>>> qdrift = QDriftTrotterization(5, rng=42)
>>> relabel = RelabelModes(solver=solver)
>>>
>>> pm.optimization = FermionicPassManager([qdrift, relabel])
>>>
>>> relabeled_circ = pm.run(circ)
>>> assert "permutation" in relabeled_circ.metadata
// WARNING: This feature is not available via the C API.

Note

Using the automatic optimization inside RelabelModes (which leverages build_excitation_span_minimization_model()) requires the optional dependency managed by HAS_PYOMO.

Next steps

Now that we have successfully generated an ensemble of circuits, we must sample bitstrings from them. To do so, the circuits must be sent to hardware for execution. We will not cover this here, and instead refer to the Qiskit documentation for detailed guides on the various steps involved.

Once the bitstring samples have been obtained, these can be used in combination with the Hamiltonian coefficients to perform the SQD post-processing, a great guide for which is written up in the SQD addon tutorials.