Source code for saoovqe.circuits

"""Module for fast manipulation with often-used circuits.

Module comprising classes implementing an orthogonal set of circuits
representing parts of initial to-be-optimized state vectors.
"""

from abc import ABC, abstractmethod
from typing import Optional, Callable, Union

import numpy as np
import qiskit_nature
from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.primitives import BaseEstimatorV1, BaseEstimatorV2, BackendEstimatorV2, BackendEstimator
from qiskit.providers import BackendV1, BackendV2
from qiskit.quantum_info import Statevector
from qiskit_ibm_runtime import EstimatorV2
from qiskit_nature.second_q.circuit.library import HartreeFock
from qiskit_nature.second_q.mappers import QubitMapper

from .logger_config import log
from .problem import ProblemSet

###########
# Settings
###########
qiskit_nature.settings.dict_aux_operators = True


# TODO does this class belong into circuits.py?
[docs] class OperatorEvaluatorBase(ABC): """ Base class for evaluator of operators' expectation values. """ def __init__(self, operator, estimator: Union[BackendEstimator, BackendEstimatorV2, EstimatorV2]) -> None: """ Constructor :param operator: Operator whose expectation values are to be evaluated. :param estimator: BaseEstimator instance providing measurement implementation. """ self._operator = operator self._estimator = estimator
[docs] @abstractmethod def get_evaluation_func(self, circuit: QuantumCircuit) -> Callable: """ Returns an evaluation function, which returns expectation values for provided parameters. :param circuit: Quantum Circuit :return: Function returning expectation values """
# TODO does this class belong into circuits.py?
[docs] class HermitianOperatorEvaluator(OperatorEvaluatorBase): """ Expectation value evaluator for Hermitian operators. """
[docs] def get_evaluation_func(self, circuit: QuantumCircuit) -> Callable: """ Obtain evaluation function (i.e. a function returning expectation values when provided parameters). :param circuit: Quantum circuit representing a parametrized state vector w.r.t. which the expectation values are computed. :return: Evaluation function """ if isinstance(self._estimator, EstimatorV2): circuit = transpile(circuit, backend=self._estimator.session._backend) def evaluation_func(circ_params: list[float] | np.ndarray) -> float | complex: """ Function returning expectation values for the chosen operator and a state vector circuit w.r.t. provided circuit parameters. :param circ_params: Parameters for the circuit (representing a state vector) :return: Expectation value """ param_binding = None if isinstance(circ_params, dict): param_binding = circ_params else: param_binding = { p: circ_params[i] for i, p in enumerate(circuit.parameters) } # TODO Simplify, when migrating to Qiskit v1.2 # t=self._estimator.run([(circuit, self._operator, param_binding)]) # # print('asdf') # # print(t) # # print(t.result()) # # print(t.result()[0]) # # print(t.result()[0].data) # # print(t.result()[0].data.evs) # # exit(-1) return ( self._estimator.run(circuit.assign_parameters(param_binding), self._operator).result().values[0] if isinstance(self._estimator, BaseEstimatorV1) else self._estimator.run([(circuit, self._operator, param_binding)]) ) return evaluation_func
@property def operator(self): """ Operator whose expectation values are being evaluated. """ return self._operator @property def estimator(self): """ Estimator providing implementation of a measurement. """ return self._estimator
# class FastOperatorEvaluator: # """ # Simple statevector evaluator of operators' expectation values. # """ # # def __init__(self, operator, mapper) -> None: # self._mapper = mapper # self._qubit_operator = operator # self._mat_operator = self._qubit_operator.to_matrix() # # def get_evaluation_func(self, circuit: QuantumCircuit) -> Callable: # """ # Obtain evaluation function (i.e. a function returning expectation # values when provided parameters). # # :param circuit: Quantum circuit representing a parametrized state # vector w.r.t. which the expectation values # are computed. # # :return: Evaluation function # """ # # def evaluation_func(circ_params: list[float] | np.ndarray) -> float: # """ # Function returning expectation values for the chosen operator # and a state vector circuit w.r.t. provided # circuit parameters. # # :param circ_params: Parameters for the circuit (representing a # state vector) # # :return: Expectation value # """ # sv = Statevector(circuit.assign_parameters(circ_params)).data # # return (sv.conj() @ self._mat_operator @ sv).real # # return evaluation_func
[docs] class OrthogonalCircuitSet: """ Implementation of a circuit set representing parts of orthogonal state vectors. """ # TODO make more general, not only for initial circuits def __init__( self, n_states: int, n_spatial_orbs: int, n_particles: tuple[int, int], qubit_mapper: Optional[QubitMapper] = None, ): """ Creates an instance of OrthogonalCircuitSet. :param n_states: Number of states/ :param n_spatial_orbs: Number of spatial molecular orbitals. :param n_particles: Number of particles in the system in the format (no. alpha particles, no. beta particles). :param qubit_mapper: Qubit mapper encoding operators to quantum circuits. """ self._n_states = n_states self._n_spatial_orbs = n_spatial_orbs self._n_particles = n_particles self._qubit_mapper = qubit_mapper if n_states > 3: raise ValueError( "No more than three states in the ensemble are supported for now" ) # Ground state circuit |HF> # Paper notation: |PhiA> self._circuits = [self._ground_state_circuit()] self._n_qubits = self._circuits[0].num_qubits if n_states > 1: self._circuits.append(self._singly_excited_singlet_circuit()) if n_states > 2: self._circuits.append(self._doubly_excited_singlet_circuit()) log.info("Circuits representing an orthogonal basis were created.")
[docs] @classmethod def from_problem_set(cls, n_states: int, problem: ProblemSet): """ Alternative constructor for making instance of :class:`OrthogonalCircuitSet` from :class:`problem.ProblemSet` instance. :param n_states: Number of quantum states. :param problem: :class:`problem.ProblemSet` instance :return Instance of :class:`OrthogonalCircuitSet` created w.r.t. "problem" parameter """ return cls( n_states, problem.as_problem.num_spatial_orbitals, problem.as_problem.num_particles, qubit_mapper=problem.fermionic_mapper, )
def __len__(self): return len(self._circuits) def __getitem__(self, idx): return self._circuits[idx] @property def n_states(self): """ Number of quantum states """ return self._n_states @property def n_qubits(self): """ Number of qubits """ return self._n_qubits @property def n_particles(self): """ Number of particles """ return self._n_particles @property def circuits(self): """ Constructed quantum circuits """ return self._circuits @property def qubit_converter(self): """ Qubit mapper for encoding of operators to quantum circuits """ return self._qubit_mapper
[docs] def get_new_rotation_circuit(self): r""" Obtain :math:`|\psi_0\rangle = \cos(\varphi)|\psi_A\rangle + \sin( \varphi)|\psi_B\rangle` by applying the rotating circuit to :math:`|0000\rangle` This DOES NOT require any knowledge of initial :math:`|\psi_A\rangle` or :math:`|\psi_B\rangle` vectors """ circuit = QuantumCircuit(self._n_qubits) self.add_resolution_rotation_circuit(circuit) return circuit
def _ground_state_circuit(self): # First state is the HF state. Note that the orbitals are sorted # with first spin alpha and then spin beta. # Ex. 4 qubits: |0101> (in Qiskit order) circuit = HartreeFock( num_spatial_orbitals=self._n_spatial_orbs, num_particles=self._n_particles, qubit_mapper=self._qubit_mapper, ) return circuit
[docs] def transpile(self, backend: Union[BackendV1, BackendV2]): """ Transpiles the circuits in-place w.r.t. the provided backend. :param backend: The backend w.r.t. whose gate set the circuits are going to be transpiled. """ self._circuits = [transpile(circ, backend=backend) for circ in self._circuits]
[docs] def add_resolution_rotation_circuit( self, circuit: QuantumCircuit, rotation_angle: Optional[float] = None ): r""" Creates the singly-excited singlet excitation (superposition of one spin-up and one spin-down excitation) see Fig. 1 in Ref. https://doi.org/10.1021/acs.jctc.1c00995 If applied to :math:`|0000\rangle` with :math:`\varphi=0`, transforms the state to :math:`|0101\rangle = |\text{HF}\rangle`. """ (n_alpha, n_beta) = self.n_particles # set all N-2 electrons in the lowest alpha- and beta-occupied # spin-orbitals for i in range(n_alpha - 1): circuit.x(i) for i in range(n_beta - 1): circuit.x(self._n_qubits // 2 + i) # Adjust the rotation angle logic if rotation_angle is None: circuit.ry(2 * Parameter("RotAngle"), n_alpha - 1) else: circuit.ry(2 * rotation_angle, n_alpha - 1) # Adjust the rest of the operations circuit.x(self.n_qubits // 2 + n_beta - 1) circuit.ch(n_alpha - 1, self.n_qubits // 2 + n_beta) circuit.cx(self.n_qubits // 2 + n_beta, self.n_qubits // 2 + n_beta - 1) circuit.cx(self.n_qubits // 2 + n_beta, n_alpha - 1) circuit.cx(n_alpha - 1, n_alpha) circuit.x(n_alpha - 1)
def _singly_excited_singlet_circuit(self): circuit = QuantumCircuit(self._n_qubits) self.add_resolution_rotation_circuit(circuit, rotation_angle=np.pi / 2) return circuit def _doubly_excited_singlet_circuit(self): circuit = QuantumCircuit(self._n_qubits) n_alpha, n_beta = self._n_particles # TODO check & fix, if necessary! # Creates the doubly-excited singlet excitation (spin-up and # spin-down excitations) circuit.x(n_alpha + 1) circuit.x(self._n_qubits // 2 + n_beta) return circuit