Fermion-qubit mappings#

Physical observables are often written in terms of fermionic operators: \(c_i\), \(c_i^{\dagger}\) instead of a sum of Pauli strings. Here, \(c_i\), \(c_i^{\dagger}\) satisfy the anti-commutation relation:

\[\begin{equation} \{c_{i}, c_{j}^{\dagger}\} = \delta_{ij}, \end{equation}\]

and \(i\), \(j\) denote the label of degree of freedom the operator acts on. In this tutorial, we explain how to perform mapping from OpenFermion’s FermionOperator to QURI Parts Operator, 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-core, and quri-parts-openfermion. You can install them as follows:

[ ]:
!pip install "quri_parts[openfermion]"

Overview#

Here we set up a Fermi-Hubbard hamiltonian for demonstration:

[27]:
from openfermion import fermi_hubbard

n_site = 2
hamiltonian = fermi_hubbard(x_dimension=n_site, y_dimension=1, tunneling=1, coulomb=2)
print("Fermi-Hubbard Hamiltonian:")
print(hamiltonian)
Fermi-Hubbard Hamiltonian:
2.0 [0^ 0 1^ 1] +
-1.0 [0^ 2] +
-1.0 [1^ 3] +
-1.0 [2^ 0] +
2.0 [2^ 2 3^ 3] +
-1.0 [3^ 1]

In QURI Parts, we provide mapping objects that generates:

  • OpenFermion operator mapper: a function that maps

    • openfermion.ops.FermionOperator

    • openfermion.ops.InteractionOperator

    • openfermion.ops.MajoranaOperator

    to QURI Parts Operator.

  • state mapper: A function that maps the occupation number state:

    \[\begin{equation} | \Psi \rangle = c_i^{\dagger} c_j^{\dagger} \cdots c_k^{\dagger} | 00\cdots 0\rangle \end{equation}\]

    to a ComputationalBasisState.

  • inverse state mapper: A function that maps a ComputationalBasisState to the occupation number state.

We use Jordan-Wigner mapping as an example. The steps of obtaining the mappers in QURI Parts are:

  1. Create a mapping object.

  2. Retrieve the mappers by accessing corresponding properties.

We first create a mapping object that performs Jordan-Wigner mapping for a system with 4 spin orbitals.

[28]:
from quri_parts.openfermion.transforms import jordan_wigner
jw_mapping = jordan_wigner(n_spin_orbitals=2*n_site)

Map a Fermion operator to qubit operator#

[29]:
operator_mapper = jw_mapping.of_operator_mapper
qubit_hamiltonian = operator_mapper(hamiltonian)
print(qubit_hamiltonian)
(-0.5+0j)*Y0 Z1 Y2 + (-0.5+0j)*X0 Z1 X2 + (-0.5+0j)*Y1 Z2 Y3 + (-0.5+0j)*X1 Z2 X3 + (1+0j)*I + (-0.5+0j)*Z1 + (-0.5+0j)*Z0 + (0.5+0j)*Z0 Z1 + (-0.5+0j)*Z3 + (-0.5+0j)*Z2 + (0.5+0j)*Z2 Z3

Map an occupation state to a ComputationalBasisState#

Let’s look at what the occupation state \(|0, 3\rangle = c_0^{\dagger} c_3^{\dagger}|0000\rangle\) is mapped to under Jordan-Wigner mapping.

[30]:
state_mapper = jw_mapping.state_mapper

occ_state = [0, 3]
cb_state = state_mapper(occ_state)

print("Occupation state:", "|" + ", ".join(map(str, occ_state)) + ">")
print("ComputationalBasisState:", cb_state)
print("State preparation circuit:")
for g in cb_state.circuit.gates:
    print(g)
Occupation state: |0, 3>
ComputationalBasisState: ComputationalBasisState(qubit_count=4, bits=0b1001, 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=(3,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())

Map a ComputationalBasisState to an occupation state#

We look at what the computational basis state \(|1011\rangle\) is mapped to under Jordan-Wigner mapping.

[31]:
from quri_parts.core.state import quantum_state

inv_state_mapper = jw_mapping.inv_state_mapper

cb_state = quantum_state(n_qubits=2*n_site, bits=0b1011)
occ_state = inv_state_mapper(cb_state)

print("ComputationalBasisState:", cb_state)
print("Occupation state:", "|" + ", ".join(map(str, occ_state)) + ">")
ComputationalBasisState: ComputationalBasisState(qubit_count=4, bits=0b1011, phase=0π/2)
Occupation state: |0, 1, 3>

The Bravyi-Kitaev mapping and symmetry conserving Bravyi-Kitaev mapping can also be used in a similar way.

Interface#

Here, we introduce the two pillars of performing mappings in QURI Parts: OpenFermionQubitMapperFactory and OpenFermionQubitMapping.

OpenFermionQubitMapping#

An OpenFermionQubitMapping is a mapping object that holds the configuration of a state you want to transform or act operators onto. We will refer it as a “mapping object” for short throughout this tutorial. The configuration of a state includes:

  • \(N_s\): number of spin-orbitals

  • \(N_e\): number of electrons

  • \(s_z\): \(z\)-component of the state’s spin

Operator mapper, state mapper and inverse state mapper are retrieved as properties. The jw_mapping variable we created in the last section is an OpenFermionQubitMapping object. They are generated by OpenFermionQubitMapperFactory, which we will introduce in the next subsection.

OpenFermionQubitMapperFactory#

An OpenFermionQubitMapperFactory is an object for generating OpenFermionQubitMappings. In QURI Parts, we provide the following OpenFermionQubitMapperFactorys.

[32]:
# Jordan-Wigner
from quri_parts.openfermion.transforms import jordan_wigner

# Bravyi-Kitaev
from quri_parts.openfermion.transforms import bravyi_kitaev

# Symmetry conserving Bravyi-Kitaev
from quri_parts.openfermion.transforms import symmetry_conserving_bravyi_kitaev as scbk

An OpenFermionQubitMapperFactory can also generate mappers with the:

  • get_of_operator_mapper

  • get_state_mapper

  • get_inv_state_mapper

methods by passing in the state configuration. In addition, they hold information about how many qubits are required to perform the mapping for a system of given number of spin orbitals and vice versa. You may obtain the information with:

  • n_qubits_required: Number of qubits required to perform the mapping for a system of \(n\) spin orbitals.

  • n_spin_orbitals: Number of spin orbtials the system contains if the mapped computational basis state contains \(n\) qubits.

Jordan-Wigner Mapping#

Jordan-Wigner mapping can be performed with the jordan_wigner object in QURI Parts. We first look at the relation between the number of spin orbitals and the number of qubits when Jordan-Wigner mapping is under consideration.

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

n_spin_orbitals = 4
n_qubits_required = jordan_wigner.n_qubits_required(n_spin_orbitals=4)
print(
    f"{n_qubits_required} qubits are required to perform Jordan-Wigner mapping for a {n_spin_orbitals}-spin-orbital system."
)

n_qubits = 4
n_spin_orbitals = jordan_wigner.n_spin_orbitals(n_qubits=4)
print(
    f"{n_spin_orbitals} spin orbitals are present in a system if the Jordan-Wigner-mapped "
    f"computational basis state contains {n_qubits} qubits."
)
4 qubits are required to perform Jordan-Wigner mapping for a 4-spin-orbital system.
4 spin orbitals are present in a system if the Jordan-Wigner-mapped computational basis state contains 4 qubits.

Now, let’s generate a Jordan-Wigner mapping object. For Jordan-Wigner mapping, only the number of spin orbitals is required for creating the mapping object. n_fermions or sz are ignored automatically if they are passed in. Let’s create one for a system with 4 spin orbitals.

[34]:
n_spin_orbitals = 2 * n_site
jw_mapping = jordan_wigner(n_spin_orbitals)

Let’s retrieve mappers from it.

Operator mapper#

[35]:
jw_operator_mapper = jw_mapping.of_operator_mapper
qubit_hamiltonian = jw_operator_mapper(hamiltonian)
print(qubit_hamiltonian)
(-0.5+0j)*Y0 Z1 Y2 + (-0.5+0j)*X0 Z1 X2 + (-0.5+0j)*Y1 Z2 Y3 + (-0.5+0j)*X1 Z2 X3 + (1+0j)*I + (-0.5+0j)*Z1 + (-0.5+0j)*Z0 + (0.5+0j)*Z0 Z1 + (-0.5+0j)*Z3 + (-0.5+0j)*Z2 + (0.5+0j)*Z2 Z3

State mapper#

Let’s look at what the occupation state \(|0, 3\rangle = c_0^{\dagger} c_3^{\dagger}|0000\rangle\) is mapped to under Jordan-Wigner mapping.

[36]:
jw_state_mapper = jw_mapping.state_mapper

occ_state = [0, 3]
cb_state = jw_state_mapper(occ_state)

print("Occupation state:", "|" + ", ".join(map(str, occ_state)) + ">")
print("ComputationalBasisState:", cb_state)
print("State preparation circuit:")
for g in cb_state.circuit.gates:
    print(g)
Occupation state: |0, 3>
ComputationalBasisState: ComputationalBasisState(qubit_count=4, bits=0b1001, 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=(3,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())

Inverse state mapper#

We look at what the computational basis state \(|1011\rangle\) is mapped to under Jordan-Wigner mapping.

[37]:
from quri_parts.core.state import quantum_state

jw_inv_state_mapper = jw_mapping.inv_state_mapper

cb_state = quantum_state(n_qubits=2*n_site, bits=0b1011)
occ_state = jw_inv_state_mapper(cb_state)

print("ComputationalBasisState:", cb_state)
print("Occupation state:", "|" + ", ".join(map(str, occ_state)) + ">")
ComputationalBasisState: ComputationalBasisState(qubit_count=4, bits=0b1011, phase=0π/2)
Occupation state: |0, 1, 3>

Alternative way of obtaining the mappers#

As explained in the interface section, mappers can also be generated by the jordan_wigner object without creating the mapping object. They can be done with:

[38]:
jw_operator_mapper = jordan_wigner.get_of_operator_mapper(n_spin_orbitals=2*n_site)
jw_state_mapper = jordan_wigner.get_state_mapper(n_spin_orbitals=2*n_site)
jw_inv_state_mapper = jordan_wigner.get_inv_state_mapper(n_spin_orbitals=2*n_site)

Bravyi-Kitaev Mapping#

Bravyi-Kitaev mapping can be performed with the bravyi_kitaev object in QURI Parts. We first look at the relation between the number of spin orbitals and the number of qubits when Bravyi-Kitaev mapping is under consideration.

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

n_spin_orbitals = 4
n_qubits_required = bravyi_kitaev.n_qubits_required(n_spin_orbitals=4)
print(
    f"{n_qubits_required} qubits are required to perform Bravyi-Kitaev mapping for a {n_spin_orbitals}-spin-orbital system."
)

n_qubits = 4
n_spin_orbitals = bravyi_kitaev.n_spin_orbitals(n_qubits=4)
print(
    f"{n_spin_orbitals} spin orbitals are present in a system if the Bravyi-Kitaev-mapped "
    f"computational basis state contains {n_qubits} qubits."
)
4 qubits are required to perform Bravyi-Kitaev mapping for a 4-spin-orbital system.
4 spin orbitals are present in a system if the Bravyi-Kitaev-mapped computational basis state contains 4 qubits.

Now, let’s generate a Bravyi-Kitaev mapping object. For Bravyi-Kitaev mapping, only the number of spin orbitals is required for creating the mapping object. n_fermions or sz are ignored automatically if they are passed in. Let’s create one for a system with 4 spin orbitals.

[40]:
n_spin_orbitals = 2 * n_site
bk_mapping = bravyi_kitaev(n_spin_orbitals)

Let’s retrieve mappers from it.

Operator mapper#

[41]:
bk_operator_mapper = bk_mapping.of_operator_mapper
qubit_hamiltonian = bk_operator_mapper(hamiltonian)
print(qubit_hamiltonian)
(-0.5+0j)*X0 Y1 Y2 + (0.5+0j)*Y0 Y1 X2 + (0.5+0j)*Z0 X1 Z3 + (-0.5+0j)*X1 Z2 + (1+0j)*I + (-0.5+0j)*Z0 Z1 + (-0.5+0j)*Z0 + (0.5+0j)*Z1 + (-0.5+0j)*Z1 Z2 Z3 + (-0.5+0j)*Z2 + (0.5+0j)*Z1 Z3

State mapper#

Let’s look at what the occupation state \(|0, 3\rangle = c_0^{\dagger} c_3^{\dagger}|0000\rangle\) is mapped to under Bravyi-Kitaev mapping.

[42]:
bk_state_mapper = bk_mapping.state_mapper

occ_state = [0, 3]
cb_state = bk_state_mapper(occ_state)

print("Occupation state:", "|" + ", ".join(map(str, occ_state)) + ">")
print("ComputationalBasisState:", cb_state)
print("State preparation circuit:")
for g in cb_state.circuit.gates:
    print(g)
Occupation state: |0, 3>
ComputationalBasisState: ComputationalBasisState(qubit_count=4, 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=())

Inverse state mapper#

We look at what the computational basis state \(|0011\rangle\) is mapped to under Bravyi-Kitaev mapping.

[43]:
from quri_parts.core.state import quantum_state

bk_inv_state_mapper = bk_mapping.inv_state_mapper

cb_state = quantum_state(n_qubits=2*n_site, bits=0b11)
occ_state = bk_inv_state_mapper(cb_state)

print("ComputationalBasisState:", cb_state)
print("Occupation state:", "|" + ", ".join(map(str, occ_state)) + ">")
ComputationalBasisState: ComputationalBasisState(qubit_count=4, bits=0b11, phase=0π/2)
Occupation state: |0, 3>

Alternative way of obtaining the mappers#

As explained in the interface section, mappers can also be generated by the bravyi_kitaev object without creating the mapping object. They can be done with:

[44]:
bk_operator_mapper = bravyi_kitaev.get_of_operator_mapper(n_spin_orbitals=2*n_site)
bk_state_mapper = bravyi_kitaev.get_state_mapper(n_spin_orbitals=2*n_site)
bk_inv_state_mapper = bravyi_kitaev.get_inv_state_mapper(n_spin_orbitals=2*n_site)

Symmetry conserving Bravyi-Kitaev Mapping#

Symmetry conserving Bravyi-Kitaev (SCBK) mapping can be performed with the symmetry_conserving_bravyi_kitaev object in QURI Parts. We first look at the relation between the number of spin orbitals and the number of qubits when SCBK mapping is under consideration.

[45]:
from quri_parts.openfermion.transforms import symmetry_conserving_bravyi_kitaev as scbk

n_spin_orbitals = 4
n_qubits_required = scbk.n_qubits_required(n_spin_orbitals=n_spin_orbitals)
print(
    f"{n_qubits_required} qubits are required to perform SCBK mapping for a {n_spin_orbitals}-spin-orbital system."
)

n_qubits = 2
n_spin_orbitals = scbk.n_spin_orbitals(n_qubits=n_qubits)
print(
    f"{n_spin_orbitals} spin orbitals are present in a system if the SCBK-mapped "
    f"computational basis state contains {n_qubits} qubits."
)
2 qubits are required to perform SCBK mapping for a 4-spin-orbital system.
4 spin orbitals are present in a system if the SCBK-mapped computational basis state contains 2 qubits.

Now, let’s generate a SCBK mapping object. For SCBK mapping, we need to be particular careful about the configuration of the state. Thus, all n_spin_orbitals, n_fermions and sz are required to create a SCBK mapping object. Let’s create one for a system with 4 spin orbitals, 2 electrons and \(s_z = 0\).

[46]:
n_spin_orbitals = 2 * n_site
n_fermions = 2
sz = 0
scbk_mapping = scbk(n_spin_orbitals, n_fermions, sz)

Let’s retrieve mappers from it.

Operator mapper#

[47]:
scbk_operator_mapper = scbk_mapping.of_operator_mapper
qubit_hamiltonian = scbk_operator_mapper(hamiltonian)
print(qubit_hamiltonian)
-1.0*X0 + -1.0*X1 + 1.0*I + 1.0*Z0 Z1

State mapper#

Let’s look at what the occupation state \(|0, 3\rangle = c_0^{\dagger} c_3^{\dagger}|0000\rangle\) is mapped to under SCBK mapping.

[48]:
scbk_state_mapper = scbk_mapping.state_mapper

occ_state = [0, 3]
cb_state = scbk_state_mapper(occ_state)

print("Occupation state:", "|" + ", ".join(map(str, occ_state)) + ">")
print("ComputationalBasisState:", cb_state)
print("State preparation circuit:")
for g in cb_state.circuit.gates:
    print(g)
Occupation state: |0, 3>
ComputationalBasisState: ComputationalBasisState(qubit_count=2, bits=0b1, phase=0π/2)
State preparation circuit:
QuantumGate(name='X', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())

Inverse state mapper#

We look at what the computational basis state \(|01\rangle\) is mapped to under SCBK mapping.

[49]:
from quri_parts.core.state import quantum_state

scbk_inv_state_mapper = scbk_mapping.inv_state_mapper

n_spin_orbitals = 2*n_site
n_qubits = scbk.n_qubits_required(n_spin_orbitals)
cb_state = quantum_state(n_qubits=n_qubits, bits=0b01)
occ_state = scbk_inv_state_mapper(cb_state)

print("ComputationalBasisState:", cb_state)
print("Occupation state:", "|" + ", ".join(map(str, occ_state)) + ">")
ComputationalBasisState: ComputationalBasisState(qubit_count=2, bits=0b1, phase=0π/2)
Occupation state: |0, 3>

Alternative way of obtaining the mappers#

As explained in the interface section, mappers can also be generated by the scbk object without creating the mapping object. They can be done with:

[50]:
scbk_operator_mapper = scbk.get_of_operator_mapper(n_spin_orbitals=2*n_site, n_fermions=n_fermions, sz=sz)
scbk_state_mapper = scbk.get_state_mapper(n_spin_orbitals=2*n_site, n_fermions=n_fermions, sz=sz)
scbk_inv_state_mapper = scbk.get_inv_state_mapper(n_spin_orbitals=2*n_site, n_fermions=n_fermions, sz=sz)