Source code for qiskit_cold_atom.fermions.base_fermion_backend
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# 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.
"""Module for cold-atom fermion backends."""
from abc import ABC
from typing import Union, List, Optional
import numpy as np
from qiskit.providers import BackendV1 as Backend
from qiskit import QuantumCircuit
from qiskit_nature.operators.second_quantization import FermionicOp
from qiskit_cold_atom.fermions.fermion_gate_library import LoadFermions
from qiskit_cold_atom.fermions.fermion_circuit_solver import FermionCircuitSolver
from qiskit_cold_atom.fermions.fermionic_state import FermionicState
from qiskit_cold_atom.exceptions import QiskitColdAtomError
[docs]class BaseFermionBackend(Backend, ABC):
"""Abstract base class for fermionic tweezer backends."""
[docs] def initialize_circuit(self, occupations: Union[List[int], List[List[int]]]):
"""
Initialize a fermionic quantum circuit with the given occupations.
Args:
occupations: List of occupation numbers. When ``List[int]`` is given, the occupations
correspond to the number of indistinguishable fermionic particles in each mode,
e.g. ``[0, 1, 1, 0]`` implies that sites one and two are occupied by a fermion.
When ``List[List[int]]`` is given, the occupations describe the number of particles in
fermionic modes with different (distinguishable) species of fermions. Each
inner list gives the occupations of one fermionic species.
Returns:
circuit: Qiskit QuantumCircuit with a quantum register for each fermionic species
initialized with the ``load`` instructions corresponding to the given occupations
Raises:
QiskitColdAtomError: If occupations do not match the backend
"""
try:
backend_size = self.configuration().to_dict()["n_qubits"]
except NameError as name_error:
raise QiskitColdAtomError(
f"Number of tweezers not specified for {self.name()}"
) from name_error
initial_state = FermionicState(occupations)
n_wires = initial_state.sites * initial_state.num_species
if n_wires > backend_size:
raise QiskitColdAtomError(
f"{self.name()} supports up to {backend_size} sites, {n_wires} were given"
)
# if num_species is specified by the backend, the wires describe different atomic species
# and the circuit must exactly match the expected wire count of the backend.
if "num_species" in self.configuration().to_dict().keys():
num_species = self.configuration().num_species
if num_species > 1 and n_wires < self.configuration().num_qubits:
raise QiskitColdAtomError(
f"{self.name()} requires circuits with exactly "
f"{self.configuration().num_qubits} wires, but an initial occupation of size "
f"{n_wires} was given."
)
from qiskit.circuit import QuantumRegister
if initial_state.num_species > 1:
registers = []
for i in range(initial_state.num_species):
registers.append(QuantumRegister(initial_state.sites, f"spin_{i}"))
circuit = QuantumCircuit(*registers)
else:
circuit = QuantumCircuit(QuantumRegister(initial_state.sites, "fer_mode"))
for i, occupation_list in enumerate(initial_state.occupations):
for j, occ in enumerate(occupation_list):
if occ:
circuit.append(LoadFermions(), qargs=[i * initial_state.sites + j])
return circuit
[docs] def measure_observable_expectation(
self,
circuits: Union[QuantumCircuit, List[QuantumCircuit]],
observable: FermionicOp,
shots: int,
seed: Optional[int] = None,
num_species: int = 1,
get_variance: bool = False,
):
"""Measure the expectation value of an observable in a state prepared by a given quantum circuit
that uses fermionic gates. Measurements are added to the entire register if they are not yet
applied in the circuit.
Args:
circuits: QuantumCircuit applying gates with fermionic generators
observable: A FermionicOp describing an observable of which the expectation value is sampled
shots: Number of measurement shots taken in case the circuit has measure instructions
seed: seed for the random number generator of the measurement simulation
num_species: number of different fermionic species described by the circuits
get_variance: If True, also returns an estimate of the variance of the observable
Raises:
QiskitColdAtomError: if the observable is non-diagonal
Returns:
observable_ev: List of the measured expectation values of the observables in given circuits
variance: List of the estimated variances of of the observables (if get_variance is True)
"""
if isinstance(circuits, QuantumCircuit):
circuits = [circuits]
observable_evs = [0] * len(circuits)
observable_vars = [0] * len(circuits)
for idx, circuit in enumerate(circuits):
# check whether the observable is diagonal in the computational basis.
solver = FermionCircuitSolver(num_species=2)
solver.preprocess_circuit(circuit)
observable_mat = solver.operator_to_mat(observable)
if list(observable_mat.nonzero()[0]) != list(observable_mat.nonzero()[1]):
raise QiskitColdAtomError(
"Measuring general observables that are non-diagonal in the "
"computational basis is not yet implemented for "
"fermionic backends. This requires non-trivial basis "
"transformations that are in general difficult to find and "
"depend on the backend's native gate set."
)
circuit.remove_final_measurements()
circuit.measure_all()
# pylint: disable=unexpected-keyword-arg
job = self.run(circuit, shots=shots, seed=seed, num_species=num_species)
counts = job.result().get_counts()
for bitstring in counts:
# Extract the index of the measured count-bitstring in the fermionic basis.
# In contrast to qubits, this is not trivial and requires an additional step.
ind = solver.basis.get_index_of_measurement(bitstring)
# contribution to the operator estimate of this outcome
p = counts[bitstring] / shots
observable_evs[idx] += p * observable_mat[ind, ind].real
if get_variance:
# contribution to the variance of the operator
observable_vars[idx] += (
np.sqrt(p * (1 - p) / shots) * observable_mat[ind, ind]
) ** 2
if get_variance:
return observable_evs, observable_vars
else:
return observable_evs
[docs] def draw(self, qc: QuantumCircuit, **draw_options):
"""Modified circuit drawer to better display atomic mixture quantum circuits.
Note that in the future this method may be modified and tailored to fermionic quantum circuits.
Args:
qc: The quantum circuit to draw.
draw_options: Key word arguments for the drawing of circuits.
"""
qc.draw(**draw_options)