Source code for qiskit_cold_atom.fermions.fermion_circuit_solver

# 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 to simulate fermionic circuits."""

from typing import List, Tuple, Optional
import numpy as np
from scipy.sparse import csc_matrix

from qiskit import QuantumCircuit
from qiskit_nature.operators.second_quantization import FermionicOp

from qiskit_cold_atom.base_circuit_solver import BaseCircuitSolver
from qiskit_cold_atom.exceptions import QiskitColdAtomError
from qiskit_cold_atom.fermions.fermionic_state import FermionicState
from qiskit_cold_atom.fermions.fermionic_basis import FermionicBasis
from qiskit_cold_atom.fermions.fermion_gate_library import FermionicGate


[docs]class FermionCircuitSolver(BaseCircuitSolver): """ Numerically simulate fermionic systems by exactly computing the time evolution under unitary operations generated by fermionic Hamiltonians. """ def __init__( self, shots: Optional[int] = None, seed: Optional[int] = None, num_species: int = 1, ): """ Args: shots: amount of shots for the measurement simulation; if not None, measurements are performed seed: seed for the RNG for the measurement simulation num_species: number of different fermionic species, defaults to 1 for a single type of (spinless) fermions, 2 for spin-1/2 fermions etc. If > 1, the solver will check for conservation of the particle number per fermionic species in order to reduce the Hilbert space dimension of the simulation """ self._basis = None self.num_species = num_species super().__init__(shots=shots, seed=seed) @property def basis(self) -> FermionicBasis: """ Return the basis of fermionic occupation number states. This basis is updated via the setter whenever a new circuit is passed to __call__. """ return self._basis @basis.setter def basis(self, basis: FermionicBasis): """ Set the basis of the simulation and check its dimensions. Args: basis: The new basis. Raises: QiskitColdAtomError: If the dimension of the basis is too large. """ if basis.dimension > self.max_dimension: raise QiskitColdAtomError( f"Dimension {basis.dimension} exceeds the maximum " f"allowed dimension {self.max_dimension}." ) self._basis = basis
[docs] def preprocess_circuit(self, circuit: QuantumCircuit): """ Pre-processing fermionic circuits includes setting up the basis for the simulation by extracting the size, particle number and spin conservation from the circuit. Args: circuit: A fermionic quantum circuit for which to setup a basis. """ initial_occupations = FermionicState.initial_state(circuit, self.num_species) _, spin_conservation = self._check_conservations(circuit) self.basis = FermionicBasis.from_state(initial_occupations, spin_conservation) self._dim = self.basis.dimension
[docs] def get_initial_state(self, circuit: QuantumCircuit) -> csc_matrix: """ Return the initial state of the quantum circuit as a sparse column vector. Args: circuit: The circuit for which to extract the initial_state. Returns: The initial state of the circuit as a sparse matrix. """ init_state = FermionicState.initial_state(circuit, self.num_species) initial_occs = init_state.occupations_flat initial_index = self.basis.get_occupations().index(initial_occs) initial_state = csc_matrix( ([1 + 0j], ([initial_index], [0])), shape=(self.basis.dimension, 1), dtype=complex, ) return initial_state
def _embed_operator( self, operator: FermionicOp, num_wires: int, qargs: List[int] ) -> FermionicOp: """ Turn a FermionicOp operator that acts on the wires given in qargs into an operator that acts on the entire state space of the circuit by padding with identities "I" on the remaining wires Args: operator: FermionicOp describing the generating Hamiltonian of a gate num_wires: The total number of wires in which the operator should be embedded into qargs: The wire indices the gate acts on Returns: FermionicOp, an operator acting on the entire quantum register of the Circuit Raises: QiskitColdAtomError: - If the given operator is not a FermionicOp - If the size of the operator does not match the given qargs """ if not isinstance(operator, FermionicOp): raise QiskitColdAtomError( f"Expected FermionicOp; got {type(operator).__name__} instead." ) if operator.register_length != len(qargs): raise QiskitColdAtomError( f"length of gate labels {operator.register_length} does not match " f"qargs {qargs} of the gates" ) embedded_op_list = [] for partial_label, factor in operator.to_list(display_format="dense"): full_label = ["I"] * num_wires for i, individual_label in enumerate(list(partial_label)): full_label[qargs[i]] = individual_label embedded_op_list.append(("".join(full_label), factor)) return FermionicOp(embedded_op_list, display_format="dense") def _check_conservations(self, circuit: QuantumCircuit) -> Tuple[bool, bool]: """ Check if the fermionic operators defined in the circuit conserve the total particle number (i.e. there are as many creation operators as annihilation operators) and the particle number per spin species (e.g. there are as many up/down creation operators as there are up/down annihilation operators). Args: circuit: A quantum circuit with fermionic gates Returns: particle_conservation: True if the particle number is conserved in the circuit spin_conservation: True if the particle number is conserved for each spin species Raises: QiskitColdAtomError: - If an operator in the circuit is not a FermionicOp. - If the length of the fermionic operators does not match the system size. - If the circuit has a number of wires that is not a multiple of the number of fermionic species. """ particle_conservation = True spin_conservation = True for fermionic_op in self.to_operators(circuit): if not isinstance(fermionic_op, FermionicOp): raise QiskitColdAtomError("operators need to be given as FermionicOp") for term in fermionic_op.to_list(): opstring = term[0] if len(opstring) != circuit.num_qubits: raise QiskitColdAtomError( f"Expected length {circuit.num_qubits} for fermionic operator; " f"received {len(opstring)}." ) num_creators = opstring.count("+") num_annihilators = opstring.count("-") if num_creators != num_annihilators: return False, False if self.num_species > 1: if circuit.num_qubits % self.num_species != 0: raise QiskitColdAtomError( f"The number of wires in the circuit {circuit.num_qubits} is not a " f"multiple of the {self.num_species} fermionic species number." ) sites = circuit.num_qubits // self.num_species # check if the particle number is conserved for each spin species for i in range(self.num_species): ops = opstring[i * sites : (i + 1) * sites] num_creators = ops.count("+") num_annihilators = ops.count("-") if num_creators != num_annihilators: spin_conservation = False break return particle_conservation, spin_conservation
[docs] def operator_to_mat(self, operator: FermionicOp) -> csc_matrix: """Convert the fermionic operator to a sparse matrix. Args: operator: fermionic operator of which to compute the matrix representation Returns: scipy.sparse matrix of the Hamiltonian """ return FermionicGate.operator_to_mat(operator, self.num_species, self._basis)
[docs] def draw_shots(self, measurement_distribution: List[float]) -> List[str]: """ Helper function to draw counts from a given distribution of measurement outcomes. Args: measurement_distribution: List of probabilities of the individual measurement outcomes Returns: a list of individual measurement results, e.g. ["011000", "100010", ...] The outcome of each shot is denoted by a binary string of the occupations of the individual modes in little endian convention Raises: QiskitColdAtomError: - If the length of the given probabilities does not match the expected Hilbert space dimension. - If the number of shots self.shots has not been specified. """ meas_dim = len(measurement_distribution) if meas_dim != self.dim: raise QiskitColdAtomError( f"Dimension of the measurement probabilities {meas_dim} does not " f"match the dimension expected by the solver, {self.dim}" ) if self.shots is None: raise QiskitColdAtomError( "The number of shots has to be set before drawing measurements" ) # list all possible outcomes as strings '001011', reversing the order of the wires # to comply with Qiskit's ordering convention outcome_strings = ["".join(map(str, k)) for k in self.basis.get_occupations()] # Draw measurements: meas_results = np.random.choice(outcome_strings, self.shots, p=measurement_distribution) return meas_results.tolist()