Note

Run interactively in jupyter notebook.

Fermionic Tweezer Hardware

Experimental system

In this tutorial, we give a concrete example of how the fermionic setting introduced in the first tutorial can be used to describe a realistic cold atomic experimental setup as a backend in Qiskit-cold-atom. We call this setting the “fermionic tweezer hardware”.

In this architecture, individual fermionic atoms are trapped and cooled in optical microtraps or “tweezers” [1]. The trapped fermionic atoms come in two different atomic states related to the nuclear spin which we thus refer to as spin-up (\(\uparrow\)) and spin-down (\(\downarrow\)). The system is cooled down to a point where only the lowest spatial mode of each tweezer can be occupied by at most one \(\uparrow\)- and one \(\downarrow\)-atom. The occupations of the individual tweezers are the carriers of quantum information in this system.

The tweezers are laid out in a linear geometry. Schematically, we can picture the fermionic register with a given number of \(\uparrow\)-particles (red) and \(\downarrow\)-particles (blue) distributed over an array of wells:

0416bc2777a24aed94c557535d795c15

Each tweezer can be initialized with a desired number of \(\uparrow\)- and \(\downarrow-\) particles as a state preparation step.

Circuit description

Initialization

In the circuit description, the initialization of the tweezers with atoms is carried out by the LoadFermions instruction. In the fermionic setting each individual fermionic mode is assigned one wire in the circuit. In our case, each spatial mode supports two spin modes. The circuit will thus consist of \(2L\) wires, where the first \(L\) wires are by convention taken to represent the spin-\(\uparrow\) modes, assigning the remaining wires to the spin- \(\downarrow\) modes.

The ColdAtomProvider includes the backend to simulate the fermionic tweezer hardware. This backend (like all fermionic backends) comes with an initialize_circuit method that creates an empty circuit initialized with the given atomic occupations. The backend is defined for a system consisting of \(L=4\) tweezers. For example, the schematic state drawn above can be initialized like this:

[1]:
from qiskit_cold_atom.providers import ColdAtomProvider

provider = ColdAtomProvider()
backend = provider.get_backend("fermionic_tweezer_simulator")

# give initial occupations separated by spin species
qc = backend.initialize_circuit([[1, 0, 0, 1], [0, 0, 1, 1]])

qc.draw(output='mpl')
[1]:
../_images/tutorials_03_fermionic_tweezer_hardware_2_0.png

Readout:

A measurement on this system will yield the occupations per spin species of the individual tweezer modes [2]. Therefore, similar to qubit circuits, each wire gets projected to 0 (mode not occupied) or 1 (mode occupied) upon measurement of the circuit.

[2]:
qc.measure_all()

print("measured counts: ", backend.run(qc, shots=10).result().get_counts())
print("measured counts: ", backend.run(qc, shots=10).result().get_memory())
measured counts:  {'10010011': 10}
measured counts:  ['10010011', '10010011', '10010011', '10010011', '10010011', '10010011', '10010011', '10010011', '10010011', '10010011']

Gates and dynamics

As laid out in the introductory notebook, within Qiskit-cold-atom, the unitary gates available in the hardware are described on the basis of their generating Hamiltonian. Here, the physical dynamics of the 1-D tweezer array are governed by a Fermi-Hubbard Hamiltonian in second quantization:

\[H_{\text{FH}}(\boldsymbol{J},U,\boldsymbol{\mu}) = \underbrace{\sum_{i=1,\sigma}^{L-1} -J_i (f^\dagger_{i,\sigma} f_{i+1,\sigma} + f^\dagger_{i+1,\sigma} f_{i,\sigma} )}_{\text{Tunneling/Hopping}} + \underbrace{U \sum_{i=1}^{L} n_{i,\uparrow}n_{i,\downarrow}}_{\text{interaction}} + \underbrace{\sum_{i=1,\sigma}^{L} \mu_i n_{i,\sigma}}_{\text{potential offset}}\]

Here, \(f_{i,\sigma}, f^\dagger_{i,\sigma}\) are annihilation/creation operators for atoms in tweezers at site \(i\) with spin \(\sigma\) and \(n_{i,\sigma} = f^\dagger_{i,\sigma} f_{i,\sigma}\) is the number operator.

The dynamics depend on the parameters \(\{ \boldsymbol{J}, U, \boldsymbol{\mu} \}\) that determine the strength of the different contributions:

  • The first term describes hopping between the site \(i\) and its neighboring site \(i+1\) (for both spin species) through the tunnel effect. The parameter \(J_i\) determines the strength of this hopping and can be tuned by adjusting the potential barrier between these two wells.

  • The second term describes an interaction between two atoms when they occupy the same site. It is controlled globally by the parameter \(U\) set by an external magnetic field exploiting a Feshbach resonance.

  • The third term describes local offsets in the potential which can locally imprint a phase by tuning \(\mu_i\).

Here, we assume that the parameters \(\{ J_i, U, \mu_i \}\) are tuned individually and can be switched on and off quasi-instantaneously. We can thus use this Hamiltonian to define a set of unitary quantum gates that can be implemented on the hardware. By choosing specific values \(\{ J_i, U, \mu_i \}\), the unitary of the gate is defined as \(U_{\text{FH}} = e^{-i H_{\text{FH}}(J_i, U, \mu_i)}\).

Let’s look at a first example circuit where a global “Fermi-Hubbard-gate” is applied:

[3]:
from qiskit_cold_atom.fermions.fermion_gate_library import FermiHubbard

qc = backend.initialize_circuit([[1, 0, 0, 1], [0, 0, 1, 1]])

all_modes=range(8)

qc.append(FermiHubbard(num_modes=8, j=[0.5, 1., -1.], u=5., mu=[0., -1., 1., 0.]), qargs=all_modes)

# alternatively append the FH gate directly:
# qc.fhubbard(j=[0.5, 1., -1.], u=5., mu=[0., -1., 1., 0.], modes=all_modes)

qc.draw(output='mpl')
[3]:
../_images/tutorials_03_fermionic_tweezer_hardware_6_0.png

This Fermi-Hubbard gate always acts globally on the entire fermionic register. The dynamics can be broken apart to separate the different interactions in the hardware by having only one term of the Hamiltonian active at a given time to create three individual gates:

  • a “hopping” gate with unitary \(U_{\text{hop}}(J_i) = U_{\text{FH}}( J, U=0, \mu=0)\). This hopping gate is a local gate that turns on the hopping of particles to neighbouring wells which is the crucial tool to create superposition in the occupation number basis.

  • an “interaction” gate with unitary \(U_{\text{int}}(U) = U_{\text{FH}}( U, J=0, \mu=0)\). This interaction gate is always a global gate acting on the entire register. It turns on the interaction of particles which affects only the states where two particles of opposite spin occupy the same well. It is thus diagonal in the occupation number basis and imprints conditional phases on the states with double occupations.

  • a “local phase” gate with unitary \(U_{\text{ext}}(\mu) = U_{\text{FH}}( \mu, J=0, U=0)\). This gate acts locally on individual tweezers. For each index \(i\) in the tweezer array, the parameter \(\mu_i\) creates an external offset potential. This operation is also diagonal in the occupation number basis and imprints local phases.

These gates are available as FermionicGates from the fermion_gate_library. Let’s use them to build up a small circuit:

[4]:
from qiskit_cold_atom.fermions.fermion_gate_library import Hop, Interaction, Phase

qc = backend.initialize_circuit([[0, 1, 1, 0], [0, 1, 0, 1]])

hop_gate = Hop(num_modes=4, j=[0.5])
interaction_gate = Interaction(num_modes=8, u=2.)
phase_gate = Phase(num_modes=2, mu=[1.])

qc.append(hop_gate, qargs=[0, 1, 4, 5])
qc.append(interaction_gate, qargs=all_modes)
qc.append(phase_gate, qargs=[2, 6])

# equivalently, we can build the same circuit with a shortcut notation:
# qc.fhop([0.5], [0, 1, 4, 5])
# qc.fint(2., all_modes)
# qc.fphase([1.], [2, 6])

qc.draw(output= "mpl")
[4]:
../_images/tutorials_03_fermionic_tweezer_hardware_8_0.png

As detailed in the introduction notebook, these gates have their generating Hamiltonians defined as a FermionicOp:

[5]:
print("hopping generator: \n", hop_gate.generator)
print("\n interaction generator: \n",interaction_gate.generator)
print("\n phase generator: \n", phase_gate.generator)
hopping generator:
 Fermionic Operator
register length=4, number terms=4
  (-0.5+0j) * ( +_0 -_1 )
+ (0.5+0j) * ( -_0 +_1 )
+ (-0.5+0j) * ( +_2 -_3 )
+ (0.5+0j) * ( -_2 +_3 )

 interaction generator:
 Fermionic Operator
register length=8, number terms=4
  (2+0j) * ( +_0 -_0 +_4 -_4 )
+ (2+0j) * ( +_1 -_1 +_5 -_5 )
+ (2+0j) * ( +_2 -_2 +_6 -_6 )
+ (2+0j) * ( +_3 -_3 +_7 -_7 )

 phase generator:
 Fermionic Operator
register length=2, number terms=2
  (1+0j) * ( +_0 -_0 )
+ (1+0j) * ( +_1 -_1 )

Let’s run this circuit on the backend:

[6]:
qc.measure_all()

job = backend.run(qc, shots=100)

print("counts: ", job.result().get_counts())

print("time taken: ", job.result().time_taken)
counts:  {'10100101': 19, '01101001': 21, '01100101': 55, '10101001': 5}
time taken:  0.05585169792175293

Note that in the measured result bitstrings, the number of 1s is always constant. This is of course a direct result of the fact that all dynamics conserve the total number of atoms initialized in the circuit.

This is also reflected in the basis states over which the simulation is performed:

[7]:
print(backend.get_basis(qc))

 0.  |0, 0, 0, 0, 1, 1, 1, 1>
 1.  |0, 0, 0, 1, 0, 1, 1, 1>
 2.  |0, 0, 0, 1, 1, 0, 1, 1>
 3.  |0, 0, 0, 1, 1, 1, 0, 1>
 4.  |0, 0, 0, 1, 1, 1, 1, 0>
 .
 .
 .
 65.  |1, 1, 1, 0, 0, 0, 0, 1>
 66.  |1, 1, 1, 0, 0, 0, 1, 0>
 67.  |1, 1, 1, 0, 0, 1, 0, 0>
 68.  |1, 1, 1, 0, 1, 0, 0, 0>
 69.  |1, 1, 1, 1, 0, 0, 0, 0>

However, this circuit not only conserves the total atom number, but also the atom number per spin species. This can be leveraged to reduce the dimension of the basis of the simulation dramatically. When running the circuit, by passing num_species = 2 as a keyword argument, the solver checks for conservation of each species. In our example, this almost halves the dimension of the basis. This also works for any number of different fermionic species.

[8]:
job_efficient = backend.run(qc, shots=100, num_species=2)

print("counts: ", job_efficient.result().get_counts())

print("time taken: ",job_efficient.result().time_taken)

print("basis: ", backend.get_basis(qc, num_species=2))
counts:  {'10100101': 18, '01101001': 18, '01100101': 57, '10101001': 7}
time taken:  0.03786921501159668
basis:
 0.  |0, 0, 1, 1>|0, 0, 1, 1>
 1.  |0, 0, 1, 1>|0, 1, 0, 1>
 2.  |0, 0, 1, 1>|0, 1, 1, 0>
 3.  |0, 0, 1, 1>|1, 0, 0, 1>
 4.  |0, 0, 1, 1>|1, 0, 1, 0>
 .
 .
 .
 31.  |1, 1, 0, 0>|0, 1, 0, 1>
 32.  |1, 1, 0, 0>|0, 1, 1, 0>
 33.  |1, 1, 0, 0>|1, 0, 0, 1>
 34.  |1, 1, 0, 0>|1, 0, 1, 0>
 35.  |1, 1, 0, 0>|1, 1, 0, 0>

Spin-changing gates:

In addition to the gates introduced above that stem from the internal Fermi-Hubbard dynamics, resonant laser pulses can be used to locally manipulate the spin state of the atoms in an individual tweezer. In this way, \(x\) - \(y\) and \(z-\)rotations of the two level system defined by the spin-\(\uparrow\) and spin-\(\downarrow\) state at a single site can be performed. These gates can break the spin-conservation symmetry we have observed in the circuits above:

[9]:
from qiskit_cold_atom.fermions.fermion_gate_library import FRXGate, FRYGate, FRZGate
import numpy as np

qc = backend.initialize_circuit([[0, 1, 1, 0], [0, 1, 0, 1]])


# flip the spin of the atom at site 3
qc.append(FRXGate(np.pi), [2, 6])

# equivalently use shortcut
# qc.frx(np.pi, [2, 6])

qc.measure_all()

print("counts: ", backend.run(qc, num_species=2).result().get_counts())

print("basis dimension :", backend.get_basis(qc).dimension)

counts:  {'01100101': 1000}
basis dimension : 70

Note that the wire indices of the gates always need to specify the indices of both spin modes corresponding to a single spatial mode.

Description as a backend

The framework describing the fermionic tweezer hardware is implemented as a dedicated backend.

We could have also run the circuits above on the generic FermionSimulator backend. However, by defining a specific FermionicTweezerSimulator backend, we have a direct way of describing the accepted gates, size and other settings of this specific system in the backend’s configuration.

In the backend configuration, we can explicitly state which instructions the backend accepts and in what way these instructions can be applied to the fermionic register. The latter is achieved through coupling maps which define the accepted wiring for all gates. For reference, let’s take a look at the entire configuration of the FermionicTweezerSimulator below:

[10]:
from pprint import pprint

tweezer_configuration = backend.configuration().to_dict()

pprint(tweezer_configuration)
{'backend_name': 'fermionic_tweezer_simulator',
 'backend_version': '0.0.1',
 'basis_gates': ['fhop',
                 'fint',
                 'fphase',
                 'fhubbard',
                 'frx',
                 'fry',
                 'frz',
                 'load'],
 'cold_atom_type': 'fermion',
 'conditional': False,
 'coupling_map': None,
 'description': 'Mock backend of a fermionic tweezer hardware. The first half '
                'of wires in a circuit denote the occupations of the spin-up '
                'fermions and the last half of wires denote the spin-down '
                'fermions',
 'dynamic_reprate_enabled': False,
 'gates': [{'coupling_map': [[0, 1, 4, 5],
                             [1, 2, 5, 6],
                             [2, 3, 6, 7],
                             [0, 1, 2, 4, 5, 6],
                             [1, 2, 3, 5, 6, 7],
                             [0, 1, 2, 3, 4, 5, 6, 7]],
            'description': 'hopping of atoms to neighboring tweezers',
            'name': 'fhop',
            'parameters': ['j_i'],
            'qasm_def': '{}'},
           {'coupling_map': [[0, 1, 2, 3, 4, 5, 6, 7]],
            'description': 'on-site interaction of atoms of opposite spin '
                           'state',
            'name': 'fint',
            'parameters': ['u'],
            'qasm_def': '{}'},
           {'coupling_map': [[0, 4], [1, 5], [2, 6], [3, 7]],
            'description': 'Applying a local phase to tweezers through an '
                           'external potential',
            'name': 'fphase',
            'parameters': ['mu_i'],
            'qasm_def': '{}'},
           {'coupling_map': [[0, 4], [1, 5], [2, 6], [3, 7]],
            'description': 'x-rotation between the spin-up and spin-down state '
                           'at one tweezer site',
            'name': 'frx',
            'parameters': ['phi'],
            'qasm_def': '{}'},
           {'coupling_map': [[0, 4], [1, 5], [2, 6], [3, 7]],
            'description': 'y-rotation between the spin-up and spin-down state '
                           'at one tweezer site',
            'name': 'fry',
            'parameters': ['phi'],
            'qasm_def': '{}'},
           {'coupling_map': [[0, 4], [1, 5], [2, 6], [3, 7]],
            'description': 'z-rotation between the spin-up and spin-down state '
                           'at one tweezer site',
            'name': 'frz',
            'parameters': ['phi'],
            'qasm_def': '{}'}],
 'local': True,
 'max_experiments': 10,
 'max_shots': 1000000.0,
 'memory': True,
 'n_qubits': 8,
 'num_species': 2,
 'open_pulse': False,
 'simulator': True,
 'supported_instructions': ['load',
                            'measure',
                            'barrier',
                            'fhop',
                            'fint',
                            'fphase',
                            'fhubbard',
                            'frx',
                            'fry',
                            'frz']}

Note how each gate has a custom coupling map which defines the allowed wire indices with which the gate can be added to the circuit.

Apart from that, the FermionicTweezerSimulator inherits all functionality of the generic FermionSimulator such as retrieving the statevector and the unitary.

Outlook

The fermionic tweezer simulator backend can be used as a tool to study what kind of quantum computations can be performed in a realistic experimental hardware based on cold fermions. As an example application, we study the time-evolution of the FH-dynamics native to the system (see tutorial 6).

The supported functionality for fermionic circuits in Qiskit-cold-atom can in principle be utilized to describe a larger class of fermionic setups, i.e. other trap- or lattice-based architectures that can deterministically prepare, manipulate and measure individual atoms. Such experiments might have different underlying Hamiltonian dynamics, number of spin species, available gates and coupling maps.

This tutorial should be seen as just one example of how this framework can be used to build a backend for one specific experimental setting.

Interested users are encouraged to explore this possibility of describing suitable fermionic gates and backend configurations of other setups.

References

[1] Murmann, Simon et al. Two Fermions in a double well: Exploring a fundamental building block of the Hubbard model Physical Review Letters 114, 080402, 2015

[2] Bergschneider, Andrea et al. Experimental Characterization of Two-Particle Entanglement through Position and Momentum Correlations, Nature Physics, 15 604-4, 2019

[11]:
import qiskit.tools.jupyter
%qiskit_version_table
%qiskit_copyright

Version Information

Qiskit SoftwareVersion
qiskit-terra0.23.1
qiskit-aer0.11.2
qiskit-nature0.5.2
System information
Python version3.9.16
Python compilerMSC v.1916 64 bit (AMD64)
Python buildmain, Jan 11 2023 16:16:36
OSWindows
CPUs8
Memory (Gb)63.724937438964844
Wed Feb 22 16:58:34 2023 W. Europe Standard Time

This code is a part of Qiskit

© Copyright IBM 2017, 2023.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.

Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.

[ ]: