Fermion-qubit mappings#

In this tutorial, we explain how to perform mapping from fermionic operators to qubit operators with QURI Parts, where we provide 3 types of mapping

  1. Jordan-Wigner mapping

  2. Bravyi-Kitaev mapping

  3. Symmetry-conserving Bravyi-Kitaev mapping

Prerequisite#

QURI Parts modules used in this tutorial: quri-parts-chem, quri-parts-pyscf, and quri-parts-openfermion. You can install them as follows:

[1]:
!pip install "quri_parts[chem]"
!pip install "quri_parts[pyscf]"
!pip install "quri_parts[openfermion]"

Getting the Electron integrals and the Hamiltonian#

We first prepare a fermionic Hamiltonian for later demonstration. For generating the molecular Hamiltonian, please refer to the Hamiltonian generation tutorial.

[2]:
from pyscf import gto, scf

from quri_parts.pyscf.mol import get_spin_mo_integrals_from_mole
from quri_parts.openfermion.mol import get_fermionic_hamiltonian

h2_atom_list = [['H', [0, 0, 0]], ['H',  [0, 0, 2]]]
h2_mol = gto.M(atom=h2_atom_list, verbose = 0)
h2_mf = scf.RHF(h2_mol).run()

_, mo_eint_set = get_spin_mo_integrals_from_mole(h2_mol, h2_mf.mo_coeff)

fermionic_hamiltonian = get_fermionic_hamiltonian(mo_eint_set)

Convert Openfermion Operator to Quri-Parts Operator#

Here, we demonstrate how to convert OpenFermion’s operator to QURI Parts Operator. For example, given the fermionic Hamiltonian we obtained above, we use OpenFermion’s jordan_wigner function to map it into a QubitOperator and then convert it into QURI Parts Operator with the operator_from_openfermion_op function we provide.

[3]:
from quri_parts.openfermion.operator import operator_from_openfermion_op
from openfermion import jordan_wigner as of_jordan_wigner

qubit_operator = of_jordan_wigner(fermionic_hamiltonian)
qp_operator = operator_from_openfermion_op(qubit_operator)

print(qp_operator)
(-0.5339363487727398+0j)*I + (0.06727930458983411+0j)*Z0 + (0.06727930458983411+0j)*Z1 + (0.006651295687574416+0j)*Z2 + (0.006651295687574416+0j)*Z3 + (0.1273657031065746+0j)*Z0 Z1 + (0.06501569581211997+0j)*Z0 Z2 + (0.12980031453238416+0j)*Z0 Z3 + (0.12980031453238416+0j)*Z1 Z2 + (0.06501569581211997+0j)*Z1 Z3 + (0.13366602988233997+0j)*Z2 Z3 + -0.06478461872026421*X0 X1 Y2 Y3 + 0.06478461872026421*X0 Y1 Y2 X3 + 0.06478461872026421*Y0 X1 X2 Y3 + -0.06478461872026421*Y0 Y1 X2 X3

This method of obtaining the Operator is a bit cumbersome and we are difficult to obtain mapped states with it. Thus, we introduce several OpenFermionQubitMapping objects below showing how to map OpenFermion’s operators and occupation states into QURI Parts Operator and ComputationBasisState.

Jordan-Wigner Mapping#

QURI Parts provides Jordan-Wigner mapping that can convert OpenFermion’s operators to QURI Parts Operator and fermionic states into a ComputationalBasisState.

The jordan_wigner.get_of_operator_mapper returns a function that maps

  • openfermion.ops.FermionOperator

  • openfermion.ops.InteractionOperator

  • openfermion.ops.MajoranaOperator

to QURI Parts Operator. Here, we use the hamiltonian we defined above, which is an InteractionOperator object, to demonstrate how to obtain the Jordan-Wigner Hamiltonian written in terms of QURI Parts Operators.

[4]:
from quri_parts.openfermion.transforms import jordan_wigner

# Obtaining the operator mapper
operator_mapper = jordan_wigner.get_of_operator_mapper()

# Map the Hamiltonian into quri-parts Operators with Jordan-Wigner mapping
jordan_wigner_hamiltonian = operator_mapper(fermionic_hamiltonian)

for op, coeff in jordan_wigner_hamiltonian.items():
    print(coeff.round(10), op)
(-0.5339363488+0j) I
(0.0672793046+0j) Z0
(0.0672793046+0j) Z1
(0.0066512957+0j) Z2
(0.0066512957+0j) Z3
(0.1273657031+0j) Z0 Z1
(0.0650156958+0j) Z0 Z2
(0.1298003145+0j) Z0 Z3
(0.1298003145+0j) Z1 Z2
(0.0650156958+0j) Z1 Z3
(0.1336660299+0j) Z2 Z3
-0.0647846187 X0 X1 Y2 Y3
0.0647846187 X0 Y1 Y2 X3
0.0647846187 Y0 X1 X2 Y3
-0.0647846187 Y0 Y1 X2 X3

State Mapping#

The state vector describing the occupation number can also be mapped to computational basis state using the jordan_wigner object.

In this case, we may construct a state mapper function from the jordan_wigner object with the get_state_mapper method where we need to supply it with the number of spin orbitals:

[5]:
jw_state_mapper = jordan_wigner.get_state_mapper(n_spin_orbitals=4)

With the state mapper at hand, we may pass in the labels of the occupied spin orbitals to obtain a Jordan-Wigner-mapped ComputationalBasisState object.

For example, to generate the qubit state for

\[| \Psi \rangle = c_0^{\dagger} c_1^{\dagger} c_2^{\dagger} | 00\cdots 0\rangle\]

where the indices on the ladder operators are the spin-orbital indices, we can do:

[6]:
occupation_spin_orbitals = [0, 1, 2]
jw_state = jw_state_mapper(occupation_spin_orbitals)
jw_state_preparation_circuit = jw_state.circuit  # The circuit that prepares the specified state

print('State:')
print(jw_state, '\n')

print('State preparation circuit')
for gate in jw_state_preparation_circuit.gates:
    print(gate)
State:
ComputationalBasisState(qubit_count=4, bits=0b111, phase=0π/2)

State preparation circuit
QuantumGate(name='X', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())
QuantumGate(name='X', target_indices=(1,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())
QuantumGate(name='X', target_indices=(2,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())

Bravyi-Kitaev mapping#

quri-parts also provides Bravyi-Kitaev mapping, the interface is the similar tojordan_wigner’s. However, note that you need to pass in the number of spin orbitals to the .get_of_operator_mapper method in order to perform the BK mapping:

[7]:
from quri_parts.openfermion.transforms import bravyi_kitaev

# Obtaining the operator mapper
bk_operator_mapper = bravyi_kitaev.get_of_operator_mapper(n_spin_orbitals=4)

# Map the Hamiltonian into quri-parts Operators with Bravyi-Kitaev mapping
bravyi_kitaev_hamiltonian = bk_operator_mapper(fermionic_hamiltonian)

for op, coeff in bravyi_kitaev_hamiltonian.items():
    print(coeff.round(10), op)
(0.0672793046+0j) Z0
(-0.5339363488+0j) I
(0.0672793046+0j) Z0 Z1
(0.0066512957+0j) Z2
(0.0066512957+0j) Z1 Z2 Z3
(0.0647846187+0j) Y0 Z1 Y2
(0.0647846187+0j) X0 Z1 X2
(0.0647846187+0j) X0 Z1 X2 Z3
(0.0647846187+0j) Y0 Z1 Y2 Z3
(0.1273657031+0j) Z1
(0.0650156958+0j) Z0 Z2
(0.1298003145+0j) Z0 Z1 Z2
(0.1298003145+0j) Z0 Z1 Z2 Z3
(0.0650156958+0j) Z0 Z2 Z3
(0.1336660299+0j) Z1 Z3
[8]:
bk_state_mapper = bravyi_kitaev.get_state_mapper(n_spin_orbitals=4)

occupation_spin_orbitals = [0, 1, 2]
bk_state_mapper_state = bk_state_mapper(occupation_spin_orbitals)
bk_state_preparation_circuit = bk_state_mapper_state.circuit

print('State:')
print(bk_state_mapper_state, '\n')

print('State preparation circuit:')
for gate in bk_state_preparation_circuit.gates:
    print(gate)
State:
ComputationalBasisState(qubit_count=4, bits=0b1101, phase=0π/2)

State preparation circuit:
QuantumGate(name='X', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())
QuantumGate(name='X', target_indices=(2,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())
QuantumGate(name='X', target_indices=(3,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())

Symmetry Conserving Bravyi-Kitaev mapping#

QURI Parts also provides symmetry-conserving Bravyi-Kitaev (SCBK) mapping, the interface is the similar tojordan_wigner’s. However, note that you need to pass in the number of spin orbitals as well as the number of electrons in order to perform SCBK mapping to an operator. In addition, also note that the number of qubits for SCBK is less than the number of spin orbitals as we show below:

[9]:
import numpy as np

from quri_parts.openfermion.transforms import symmetry_conserving_bravyi_kitaev

# Obtaining the operator mapper
sym_conserving_bk_operator_mapper = symmetry_conserving_bravyi_kitaev.get_of_operator_mapper(n_fermions=2, n_spin_orbitals=4)

print(
    f'Number of qubits using SCBK: {symmetry_conserving_bravyi_kitaev.n_qubits_required(n_spin_orbitals=4)}'
)

# Map the Hamiltonian into quri-parts Operators with Jordan-Wigner mapping
sym_conserving_bravyi_kitaev_hamiltonian = sym_conserving_bk_operator_mapper(fermionic_hamiltonian)

for op, coeff in sym_conserving_bravyi_kitaev_hamiltonian.items():
    if (c := np.round(coeff, 10)):
        print(c, op)
Number of qubits using SCBK: 2
-0.6639677404 I
0.0606280089 Z0
0.0606280089 Z1
0.0014311039 Z0 Z1
0.2591384749 X0 X1
[10]:
scbk_state_mapper = symmetry_conserving_bravyi_kitaev.get_state_mapper(n_fermions=2, n_spin_orbitals=4)

occupation_spin_orbitals = [0, 1]
scbk_state_mapper_state = scbk_state_mapper(occupation_spin_orbitals)
scbk_state_preparation_circuit = scbk_state_mapper_state.circuit

print('State:')
print(scbk_state_mapper_state, '\n')

print('State preparation circuit:')
for gate in scbk_state_preparation_circuit.gates:
    print(gate)
State:
ComputationalBasisState(qubit_count=2, bits=0b11, phase=0π/2)

State preparation circuit:
QuantumGate(name='X', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())
QuantumGate(name='X', target_indices=(1,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())