Source code for mpqp.core.instruction.measurement.expectation_value

"""Information about the state can be retrieved using the expectation value of
this state measured by an observable. This is done using the :class:`Observable`
class to define your observable, and a :class:`ExpectationMeasure` to perform
the measure."""

from __future__ import annotations

import copy
from numbers import Complex
from typing import TYPE_CHECKING, Optional
from warnings import warn

import numpy as np
from typeguard import typechecked

if TYPE_CHECKING:
    from sympy import Expr
    from qiskit.circuit import Parameter
    from qiskit.quantum_info import Operator
    from qat.core.wrappers.observable import Observable as QLMObservable
    from braket.circuits.observables import Hermitian
    from cirq.circuits.circuit import Circuit as Cirq_Circuit
    from cirq.ops.pauli_string import PauliString as CirqPauliString
    from cirq.ops.linear_combinations import PauliSum as CirqPauliSum

from mpqp.core.instruction.gates.native_gates import SWAP
from mpqp.core.instruction.measurement.measure import Measure
from mpqp.core.instruction.measurement.pauli_string import PauliString
from mpqp.core.languages import Language
from mpqp.tools.errors import NumberQubitsError
from mpqp.tools.generics import Matrix, one_lined_repr
from mpqp.tools.maths import is_hermitian


[docs]@typechecked class Observable: """Class defining an observable, used for evaluating expectation values. An observable can be defined by using a Hermitian matrix, or using a combination of operators in a specific basis Pauli. Args: observable : can be either a Hermitian matrix representing the observable or PauliString representing the observable. Raises: ValueError: If the input matrix is not Hermitian or does not have a square shape. NumberQubitsError: If the number of qubits in the input observable does not match the number of target qubits. Example: >>> from mpqp.core.instruction.measurement.pauli_string import I, X, Y, Z >>> matrix = np.array([[1, 0], [0, -1]]) >>> ps = 3 * I @ Z + 4 * X @ Y >>> obs = Observable(matrix) >>> obs2 = Observable(ps) """ def __init__(self, observable: Matrix | PauliString): self._matrix = None self._pauli_string = None if isinstance(observable, PauliString): self.nb_qubits = observable.nb_qubits self._pauli_string = observable.simplify() else: self.nb_qubits = int(np.log2(len(observable))) """Number of qubits of this observable.""" self._matrix = np.array(observable) basis_states = 2**self.nb_qubits if self.matrix.shape != (basis_states, basis_states): raise ValueError( f"The size of the matrix {self.matrix.shape} doesn't neatly fit on a" " quantum register. It should be a square matrix of size a power" " of two." ) if not is_hermitian(self.matrix): raise ValueError( "The matrix in parameter is not hermitian (cannot define an observable)" ) @property def matrix(self) -> Matrix: """The matrix representation of the observable.""" if self._matrix is None: self._matrix = self.pauli_string.to_matrix() matrix = copy.deepcopy(self._matrix).astype(np.complex64) return matrix @property def pauli_string(self) -> PauliString: """The PauliString representation of the observable.""" if self._pauli_string is None: self._pauli_string = PauliString.from_matrix(self.matrix) pauli_string = copy.deepcopy(self._pauli_string) return pauli_string @pauli_string.setter def pauli_string(self, pauli_string: PauliString): self._pauli_string = pauli_string self._matrix = None @matrix.setter def matrix(self, matrix: Matrix): self._matrix = matrix self._pauli_string = None def __repr__(self) -> str: return f"{type(self).__name__}({one_lined_repr(self.matrix)})" def __mult__(self, other: Expr | Complex) -> Observable: """3M-TODO""" ... def subs( self, values: dict[Expr | str, Complex], remove_symbolic: bool = False ) -> Observable: """3M-TODO""" ...
[docs] def to_other_language( self, language: Language, circuit: Optional[Cirq_Circuit] = None ) -> Operator | QLMObservable | Hermitian | CirqPauliSum | CirqPauliString: """Converts the observable to the representation of another quantum programming language. Args: language: The target programming language. circuit: The Cirq circuit associated with the observable (required for ``cirq``). Returns: Depends on the target language. Example: >>> obs = Observable(np.diag([0.7, -1, 1, 1])) >>> obs_qiskit = obs.to_other_language(Language.QISKIT) >>> print(obs_qiskit) Operator([[ 0.69999999+0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, -1. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 1. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 1. +0.j]], input_dims=(2, 2), output_dims=(2, 2)) """ if language == Language.QISKIT: from qiskit.quantum_info import Operator return Operator(self.matrix) elif language == Language.MY_QLM: from qat.core.wrappers.observable import Observable as QLMObservable return QLMObservable(self.nb_qubits, matrix=self.matrix) elif language == Language.BRAKET: from braket.circuits.observables import Hermitian return Hermitian(self.matrix) elif language == Language.CIRQ: if circuit is None: raise ValueError("Circuit must be specified for cirq_observable.") from cirq.ops.identity import I as Cirq_I from cirq.ops.pauli_gates import X as Cirq_X from cirq.ops.pauli_gates import Y as Cirq_Y from cirq.ops.pauli_gates import Z as Cirq_Z all_qubits = sorted( set( q for moment in circuit for op in moment.operations for q in op.qubits ) ) pauli_gate_map = {"I": Cirq_I, "X": Cirq_X, "Y": Cirq_Y, "Z": Cirq_Z} cirq_pauli_string = None for monomial in self.pauli_string.monomials: cirq_monomial = None for index, atom in enumerate(monomial.atoms): cirq_atom = pauli_gate_map[atom.label](all_qubits[index]) cirq_monomial = cirq_atom if cirq_monomial is None else cirq_monomial * cirq_atom # type: ignore cirq_monomial *= monomial.coef # type: ignore cirq_pauli_string = ( cirq_monomial if cirq_pauli_string is None else cirq_pauli_string + cirq_monomial ) return cirq_pauli_string else: raise ValueError(f"Unsupported language: {language}")
[docs]@typechecked class ExpectationMeasure(Measure): """This measure evaluates the expectation value of the output of the circuit measured by the observable given as input. If the ``targets`` are not sorted and contiguous, some additional swaps will be needed. This will affect the performance of your circuit if run on noisy hardware. The swaps added can be checked out in the ``pre_measure`` attribute of the :class:`ExpectationMeasure`. Args: targets: List of indices referring to the qubits on which the measure will be applied. observable: Observable used for the measure. shots: Number of shots to be performed. label: Label used to identify the measure. Example: >>> obs = Observable(np.diag([0.7, -1, 1, 1])) >>> c = QCircuit([H(0), CNOT(0,1), ExpectationMeasure([0,1], observable=obs, shots=10000)]) >>> run(c, ATOSDevice.MYQLM_PYLINALG).expectation_value # doctest: +SKIP 0.85918 Warns: UserWarning: If the ``targets`` are not sorted and contiguous, some additional swaps will be needed. This will change the performance of your circuit is run on noisy hardware. """ def __init__( self, targets: list[int], observable: Observable, shots: int = 0, label: Optional[str] = None, ): from mpqp.core.circuit import QCircuit super().__init__(targets, shots, label) self.observable = observable """See parameter description.""" # Raise an error if the number of target qubits does not match the size of the observable. if self.nb_qubits != observable.nb_qubits: raise NumberQubitsError( f"{self.nb_qubits}, the number of target qubit(s) doesn't match" f" {observable.nb_qubits}, the size of the observable" ) self.pre_measure = QCircuit(max(targets) + 1) """Circuit added before the expectation measurement to correctly swap target qubits when their are note ordered or contiguous.""" targets_is_ordered = all( [targets[i] > targets[i - 1] for i in range(1, len(targets))] ) tweaked_tgt = copy.copy(targets) if ( max(tweaked_tgt) - min(tweaked_tgt) + 1 != len(tweaked_tgt) or not targets_is_ordered ): warn( "Non contiguous or non sorted observable target will introduce " "additional CNOTs." ) for t_index, target in enumerate(tweaked_tgt): # sort the targets min_index = tweaked_tgt.index(min(tweaked_tgt[t_index:])) if t_index != min_index: self.pre_measure.add(SWAP(target, tweaked_tgt[min_index])) tweaked_tgt[t_index], tweaked_tgt[min_index] = ( tweaked_tgt[min_index], target, ) for t_index, target in enumerate(tweaked_tgt): # compact the targets if t_index == 0: continue if target != tweaked_tgt[t_index - 1] + 1: self.pre_measure.add(SWAP(target, tweaked_tgt[t_index - 1] + 1)) tweaked_tgt[t_index] = tweaked_tgt[t_index - 1] + 1 self.rearranged_targets = tweaked_tgt """Adjusted list of target qubits when they are not initially sorted and contiguous.""" def __repr__(self) -> str: return ( f"ExpectationMeasure({self.targets}, {self.observable}, shots={self.shots})" )
[docs] def to_other_language( self, language: Language = Language.QISKIT, qiskit_parameters: Optional[set["Parameter"]] = None, ) -> None: raise NotImplementedError( "This object should not be exported as is, because other SDKs have " "no equivalent. Instead, this object is used to store the " "appropriate data, and the data in later used in the needed " "locations." )