diff --git a/examples/notebooks/3_Expectation_value_of_observables.ipynb b/examples/notebooks/3_Expectation_value_of_observables.ipynb index f6d697ce..f9c9b9a1 100644 --- a/examples/notebooks/3_Expectation_value_of_observables.ipynb +++ b/examples/notebooks/3_Expectation_value_of_observables.ipynb @@ -554,6 +554,26 @@ ")\n", "print(results)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad9fed49", + "metadata": {}, + "outputs": [], + "source": [ + "#TODO: show examples with multi observable expectation value\n", + "\n", + "#TODO: maybe show also the set of commuting pauli string when those observables are decomposed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6f5f27c", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/examples/scripts/aws_execution_trials.py b/examples/scripts/aws_execution_trials.py index 0c885924..4e0caceb 100644 --- a/examples/scripts/aws_execution_trials.py +++ b/examples/scripts/aws_execution_trials.py @@ -42,7 +42,8 @@ circuit.add(SWAP(2, 0)) circuit.add(CNOT(0, 2)) circuit.add(Ry(3.14 / 2, 2)) -circuit.add(S(1)) +circuit.add(S(0)) +circuit.add(S_dagger(1)) circuit.add(H(3)) circuit.add(CNOT(1, 2)) circuit.add(Rx(3.14, 1)) diff --git a/examples/scripts/circuit_to_qasm.py b/examples/scripts/circuit_to_qasm.py index ff6d33ee..4c060a83 100644 --- a/examples/scripts/circuit_to_qasm.py +++ b/examples/scripts/circuit_to_qasm.py @@ -15,7 +15,8 @@ circuit.add(SWAP(2, 0)) circuit.add(CNOT(0, 2)) circuit.add(Ry(3.14 / 2, 2)) -circuit.add(S(1)) +circuit.add(S(0)) +circuit.add(S_dagger(1)) circuit.add(H(3)) circuit.add(CNOT(1, 2)) circuit.add(Rx(3.14, 1)) diff --git a/examples/scripts/native_gates_examples.py b/examples/scripts/native_gates_examples.py index 4e1241c1..373a3723 100644 --- a/examples/scripts/native_gates_examples.py +++ b/examples/scripts/native_gates_examples.py @@ -9,7 +9,7 @@ # Declaration of the circuit with the right size circuit = QCircuit(3, label="Test native gates") # Constructing the circuit by adding gates and measurements -circuit.add([H(0), X(1), Y(2), Z(0), S(1), T(0)]) +circuit.add([H(0), X(1), Y(2), Z(0), S(0), S_dagger(1), T(0)]) circuit.add([Rx(1.2324, 2), Ry(-2.43, 0), Rz(1.04, 1), Rk(-1, 1), P(-323, 2)]) circuit.add(U(1.2, 2.3, 3.4, 2)) circuit.add(SWAP(2, 0)) diff --git a/examples/scripts/observable_job.py b/examples/scripts/observable_job.py index c7370758..ec5a0d90 100644 --- a/examples/scripts/observable_job.py +++ b/examples/scripts/observable_job.py @@ -71,3 +71,5 @@ ], ) print(results) + +# TODO: show examples of multi observable run diff --git a/mpqp/all.py b/mpqp/all.py index dff08cbe..6628ffc2 100644 --- a/mpqp/all.py +++ b/mpqp/all.py @@ -49,6 +49,7 @@ Ry, Rz, S, + S_dagger, T, U, UnitaryMatrix, diff --git a/mpqp/core/circuit.py b/mpqp/core/circuit.py index 39b65aea..d9354d18 100644 --- a/mpqp/core/circuit.py +++ b/mpqp/core/circuit.py @@ -760,29 +760,29 @@ def inverse(self) -> QCircuit: The inverse circuit. Examples: - >>> c1 = QCircuit([S(0), CZ(0,1), H(1), Ry(4.56, 1)]) + >>> c1 = QCircuit([T(0), CZ(0,1), H(1), Ry(4.56, 1)]) >>> print(c1) # doctest: +NORMALIZE_WHITESPACE ┌───┐ - q_0: ┤ S ├─■────────────────── + q_0: ┤ T ├─■────────────────── └───┘ │ ┌───┐┌──────────┐ q_1: ──────■─┤ H ├┤ Ry(4.56) ├ └───┘└──────────┘ >>> print(c1.inverse()) # doctest: +NORMALIZE_WHITESPACE ┌────┐ - q_0: ───────────────────■─┤ S† ├ + q_0: ───────────────────■─┤ T† ├ ┌───────────┐┌───┐ │ └────┘ q_1: ┤ Ry(-4.56) ├┤ H ├─■─────── └───────────┘└───┘ - >>> c2 = QCircuit([S(0), CRk(2, 0, 1), Barrier(), H(1), Ry(4.56, 1)]) + >>> c2 = QCircuit([T(0), CRk(2, 0, 1), Barrier(), H(1), Ry(4.56, 1)]) >>> print(c2) # doctest: +NORMALIZE_WHITESPACE ┌───┐ ░ - q_0: ┤ S ├─■────────░────────────────── + q_0: ┤ T ├─■────────░────────────────── └───┘ │P(π/2) ░ ┌───┐┌──────────┐ q_1: ──────■────────░─┤ H ├┤ Ry(4.56) ├ ░ └───┘└──────────┘ >>> print(c2.inverse()) # doctest: +NORMALIZE_WHITESPACE ░ ┌────┐ - q_0: ───────────────────░──■────────┤ S† ├ + q_0: ───────────────────░──■────────┤ T† ├ ┌───────────┐┌───┐ ░ │P(-π/2) └────┘ q_1: ┤ Ry(-4.56) ├┤ H ├─░──■────────────── └───────────┘└───┘ ░ @@ -906,7 +906,7 @@ def measurements(self) -> list[Measure]: ... ]) >>> circuit.measurements # doctest: +NORMALIZE_WHITESPACE [BasisMeasure(shots=1000), - ExpectationMeasure(Observable(array([[1.+0.j, 0.+0.j], [0.+0.j, 1.+0.j]], dtype=complex64)), [1], shots=1000)] + ExpectationMeasure([Observable(array([[1.+0.j, 0.+0.j], [0.+0.j, 1.+0.j]], dtype=complex64))], [1], shots=1000)] """ return [inst for inst in self.instructions if isinstance(inst, Measure)] diff --git a/mpqp/core/instruction/gates/__init__.py b/mpqp/core/instruction/gates/__init__.py index 4c2464f9..2033bba1 100644 --- a/mpqp/core/instruction/gates/__init__.py +++ b/mpqp/core/instruction/gates/__init__.py @@ -20,6 +20,7 @@ Ry, Rz, S, + S_dagger, T, U, X, diff --git a/mpqp/core/instruction/gates/native_gates.py b/mpqp/core/instruction/gates/native_gates.py index af193b5d..10c8c2ea 100644 --- a/mpqp/core/instruction/gates/native_gates.py +++ b/mpqp/core/instruction/gates/native_gates.py @@ -378,8 +378,10 @@ class OneQubitNoParamGate(SingleQubitGate, NoParameterGate, SimpleClassReprABC): target: Index referring to the qubits on which the gate will be applied. """ - def __init__(self, target: int): - SingleQubitGate.__init__(self, target, type(self).__name__) + def __init__(self, target: int, label: Optional[str] = None): + SingleQubitGate.__init__( + self, target, type(self).__name__ if label is None else label + ) class Id(OneQubitNoParamGate, InvolutionGate): @@ -767,6 +769,56 @@ def __init__(self, target: int): super().__init__(target) self.matrix = np.array([[1, 0], [0, 1j]]) + def inverse(self) -> Gate: + return S_dagger(self.targets[0]) + + +class S_dagger(OneQubitNoParamGate): + r"""One qubit S adjoint gate. It's equivalent to ``P(-pi/2)``. + + `\begin{pmatrix}1&0\\0&-i\end{pmatrix}` + + Args: + target: Index referring to the qubit on which the gate will be applied. + + Example: + >>> pprint(S_dagger(0).to_matrix()) + [[1, 0 ], + [0, -1j]] + + """ + + @classproperty + def braket_gate(cls): + from braket.circuits import gates + + return gates.Si + + @classproperty + def qiskit_gate(cls): + from qiskit.circuit.library import SdgGate + + return SdgGate + + @classproperty + def cirq_gate(cls): + from cirq.ops.common_gates import ZPowGate + + return ZPowGate(exponent=-1 / 2) + + qlm_aqasm_keyword = "DAG(S)" + qiskit_string = "sdg" + + def __init__(self, target: int): + super().__init__(target, "S†") + self.matrix = np.array([[1, 0], [0, -1j]]) + + def __repr__(self): + return f"{type(self).__name__}({self.targets[0]})" + + def inverse(self) -> Gate: + return S(self.targets[0]) + class T(OneQubitNoParamGate): r"""One qubit T gate. It is also referred to as the `\pi/4` gate because it diff --git a/mpqp/core/instruction/measurement/expectation_value.py b/mpqp/core/instruction/measurement/expectation_value.py index b2819219..d4151cac 100644 --- a/mpqp/core/instruction/measurement/expectation_value.py +++ b/mpqp/core/instruction/measurement/expectation_value.py @@ -1,36 +1,40 @@ """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` +this state measured by one or several observables. 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, Union +from numbers import Real +from typing import TYPE_CHECKING, Literal, Optional, Union from warnings import warn import numpy as np +import numpy.typing as npt from typeguard import typechecked -if TYPE_CHECKING: - from sympy import Expr - from qiskit.circuit import Parameter - from qiskit.quantum_info import SparsePauliOp - from qat.core.wrappers.observable import Observable as QLMObservable - from braket.circuits.observables import Hermitian - from cirq.circuits.circuit import Circuit as CirqCircuit - 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.instruction.measurement.pauli_string import ( + PauliString, + PauliStringMonomial, +) from mpqp.core.languages import Language from mpqp.tools.display import one_lined_repr from mpqp.tools.errors import NumberQubitsError from mpqp.tools.generics import Matrix -from mpqp.tools.maths import is_hermitian +from mpqp.tools.maths import is_diagonal, is_hermitian, is_power_of_two + +if TYPE_CHECKING: + from braket.circuits.observables import Hermitian + from cirq.circuits.circuit import Circuit as CirqCircuit + from cirq.ops.linear_combinations import PauliSum as CirqPauliSum + from cirq.ops.pauli_string import PauliString as CirqPauliString + from qat.core.wrappers.observable import Observable as QLMObservable + from qiskit.circuit import Parameter + from qiskit.quantum_info import SparsePauliOp + from sympy import Expr @typechecked @@ -42,7 +46,8 @@ class Observable: Args: observable : can be either a Hermitian matrix representing the - observable or PauliString representing the observable. + observable, or PauliString representing the observable, or a list + of diagonal elements of the matrix when the observable is diagonal. Raises: ValueError: If the input matrix is not Hermitian or does not have a @@ -66,36 +71,65 @@ class Observable: """ - def __init__(self, observable: Matrix | PauliString): + def __init__(self, observable: Matrix | PauliString | list[Real]): self._matrix = None self._pauli_string = None + self._is_diagonal = None + self._diag_elements = 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))) + size_1 = len(observable) + + if not is_power_of_two(size_1): + raise ValueError("The size of the observable is not a power of two.") + + self.nb_qubits = int(np.log2(size_1)) """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 isinstance(observable, list): + shape = observable.shape - if not is_hermitian(self.matrix): - raise ValueError( - "The matrix in parameter is not hermitian (cannot define an observable)" - ) + if len(shape) > 2: + raise ValueError( + f"The dimension of the observable matrix {len(shape)} does not correspond " + f"to the one of a matrix (2) or a list (1)." + ) + + if len(shape) == 2: + if shape != (size_1, size_1): + raise ValueError( + f"The size of the 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(observable): + raise ValueError( + "The matrix in parameter is not hermitian (cannot define an observable)." + ) + + self._matrix = np.array(observable) + + else: + self._is_diagonal = True + self._diag_elements = observable.real + + # correspond to isinstance(observable, list) + else: + self._is_diagonal = True + self._diag_elements = np.array(observable) @property def matrix(self) -> Matrix: """The matrix representation of the observable.""" if self._matrix is None: - self._matrix = self.pauli_string.to_matrix() + if self.is_diagonal and self._diag_elements is not None: + self._matrix = np.diag(np.array(self._diag_elements, dtype=np.float64)) + else: + self._matrix = self.pauli_string.to_matrix() matrix = copy.deepcopy(self._matrix).astype(np.complex64) return matrix @@ -103,29 +137,121 @@ def matrix(self) -> Matrix: 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) + if self.is_diagonal: + self._pauli_string = PauliString.from_diagonal_elements( + self.diagonal_elements + ) + else: + self._pauli_string = PauliString.from_matrix(self.matrix) pauli_string = copy.deepcopy(self._pauli_string) return pauli_string + @property + def diagonal_elements(self) -> npt.NDArray[np.float64]: + """The diagonal elements of the matrix representing the observable (diagonal or not).""" + if self._diag_elements is None: + self._diag_elements = np.diagonal(self.matrix) + return copy.deepcopy(np.array(self._diag_elements, dtype=np.float64)).real + @matrix.setter def matrix(self, matrix: Matrix): + shape = matrix.shape + if len(shape) != 2 or shape[0] != shape[1]: + raise ValueError(f"The matrix must be square, but got shape {shape}.") + + size_1 = shape[0] + if not is_power_of_two(size_1): + raise ValueError(f"Matrix dimension {size_1} must be a power of two.") + + if not is_hermitian(matrix): + raise ValueError( + "The matrix is not hermitian (cannot define an observable)." + ) + self._matrix = matrix self._pauli_string = None + self._diag_elements = None + self._is_diagonal = None @pauli_string.setter def pauli_string(self, pauli_string: PauliString): self._pauli_string = pauli_string self._matrix = None + self._diag_elements = None + self._is_diagonal = None + + @diagonal_elements.setter + def diagonal_elements(self, diag_elements: list[Real] | npt.NDArray[np.float64]): + if not is_power_of_two(len(diag_elements)): + raise ValueError( + "The size of the diagonal elements of the matrix is not a power of two." + ) + + self._diag_elements = diag_elements + self._is_diagonal = True + self._pauli_string = None + self._matrix = None + + @property + def is_diagonal(self) -> bool: + """Returns True if this observable is diagonal. + + Examples: + >>> Observable(np.array([[3, 0], [0, 8]])).is_diagonal + True + >>> Observable(np.array([[3, -1], [-1, 8]])).is_diagonal + False + >>> Observable(np.diag([-1,8,4,5])).is_diagonal + True + >>> Observable([0, 5, 6, 9, 7, 4, 3, 6]).is_diagonal + True + >>> Observable(np.array([7, 4, 3, 6])).is_diagonal + True + >>> from mpqp.measures import I, X, Y, Z + >>> Observable(I @ Z - 3 * Z @ Z + 2* Z @ I).is_diagonal + True + >>> Observable(I @ X - 3* Z @ Z + 2 * Y @ I).is_diagonal + False + + """ + if self._is_diagonal is None: + # We first check if the pauli string is already known, we use it for efficiency + if self._pauli_string is not None: + self._is_diagonal = self._pauli_string.is_diagonal() + # If not we check if the matrix is already known, + elif self._matrix is not None: + self._is_diagonal = is_diagonal(self._matrix) + # If only the diagonal elements are known, it means the observable is diagonal + elif self._diag_elements is not None: + self._is_diagonal = True + # Otherwise, the observable is empty, we return False by convention + else: + return False + + return self._is_diagonal def __repr__(self) -> str: return f"{type(self).__name__}({one_lined_repr(self.matrix)})" - def __mult__(self, other: Expr | Complex) -> Observable: + def __mult__(self, other: Expr | Real) -> Observable: """3M-TODO""" ... + def commutes_with(self, obs: Observable): + """3M-TODO""" + # Naive version, just computing AB - BA, and compare to 0 matrix. + # TODO : distinguer si on a l'observable ou le pauli string + # TODO: traitement spécifique si observable diagonal ? + + if self.is_diagonal: + if obs.is_diagonal: + return True + # TODO: check if self is multiple of identity + + return ~np.any(self.matrix.dot(obs.matrix) - obs.matrix.dot(self.matrix)) + def subs( - self, values: dict[Expr | str, Complex], remove_symbolic: bool = False + self, values: dict[Expr | str, Real], remove_symbolic: bool = False ) -> Observable: """3M-TODO""" ... @@ -173,7 +299,7 @@ def to_other_language( @typechecked class ExpectationMeasure(Measure): """This measure evaluates the expectation value of the output of the circuit - measured by the observable given as input. + measured by the observable(s) 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 @@ -200,19 +326,29 @@ class ExpectationMeasure(Measure): """ - # TODO: problem here with the doc generation, the arguments are messed up - def __init__( self, - observable: Observable, + observable: Union[Observable, list[Observable]], targets: Optional[list[int]] = None, shots: int = 0, label: Optional[str] = None, ): super().__init__(targets, shots, label) - self.observable = observable + self.observables: list[Observable] """See parameter description.""" + + if isinstance(observable, Observable): + self.observables = [observable] + else: + if not all( + observable[0].nb_qubits == obs.nb_qubits for obs in observable[1:] + ): + raise ValueError( + "All observables in ExpectationMeasure must have the same size. Sizes: " + + str([o.nb_qubits for o in observable]) + ) + self.observables = observable self._check_targets_order() def _check_targets_order(self): @@ -223,10 +359,10 @@ def _check_targets_order(self): self.pre_measure = QCircuit(0) return - if self.nb_qubits != self.observable.nb_qubits: + if self.nb_qubits != self.observables[0].nb_qubits: raise NumberQubitsError( f"Target size {self.nb_qubits} doesn't match observable size " - f"{self.observable.nb_qubits}." + f"{self.observables[0].nb_qubits}." ) self.pre_measure = QCircuit(max(self.targets) + 1) @@ -261,6 +397,27 @@ def _check_targets_order(self): """Adjusted list of target qubits when they are not initially sorted and contiguous.""" + def get_pauli_grouping( + self, + method: Literal[ + "full_greedy" + ] = "full_greedy", # , "full_clique", "qubit_wise_clique" + ) -> list[list[PauliStringMonomial]]: + """Decompose the observables and regroup the pauli measurements + by commutativity relation using different strategies. + + Args: + method: The grouping method to use. + - "full_clique": Finds the largest possible commuting groups (cliques). + - "full_greedy": Uses a greedy algorithm to iteratively build commuting groups. + - "qubit_wise_clique": Groups Pauli strings based on qubit-wise commutativity. + + Returns: + A list of list, where each list contains Pauli strings that can be measured simultaneously. + 3M-TODO + """ + ... + def __repr__(self) -> str: targets = ( f", {self.targets}" @@ -269,7 +426,7 @@ def __repr__(self) -> str: ) shots = "" if self.shots == 0 else f", shots={self.shots}" label = "" if self.label is None else f", label={self.label}" - return f"ExpectationMeasure({self.observable}{targets}{shots}{label})" + return f"ExpectationMeasure({self.observables}{targets}{shots}{label})" def to_other_language( self, diff --git a/mpqp/core/instruction/measurement/pauli_string.py b/mpqp/core/instruction/measurement/pauli_string.py index a676f4e2..ab370d08 100644 --- a/mpqp/core/instruction/measurement/pauli_string.py +++ b/mpqp/core/instruction/measurement/pauli_string.py @@ -17,8 +17,9 @@ from sympy import Expr from typeguard import typechecked +from mpqp.core.instruction.gates.native_gates import NativeGate, S_dagger, H from mpqp.core.languages import Language -from mpqp.tools import format_element +from mpqp.tools import format_element, NumberQubitsError from mpqp.tools.generics import Matrix from mpqp.tools.maths import atol, is_power_of_two, rtol @@ -330,6 +331,20 @@ def to_matrix(self) -> Matrix: start=np.zeros((size, size), dtype=np.complex64), ) + def commutes_with(self, other: PauliString) -> bool: + """ + 3M-TODO: Determine EFFICIENTLY if this pauli string is commuting with the one in parameter. + Args: + other: + + Returns: + + Examples: + >>> from mpqp.measures import I, X, Y, Z + + """ + pass + @staticmethod def from_matrix( matrix: Matrix, method: Literal["ptdr", "trace"] = "ptdr" @@ -796,6 +811,23 @@ def __hash__(self): ) return hash(monomials_as_tuples) + def is_diagonal(self) -> bool: + """Checks whether this pauli string has a diagonal representation, by checking if only ``I`` and ``Z`` Pauli + operators appears in the monomials of the string. + + Returns: + True if the observable represented by pauli string is diagonal. + + Examples: + >>> from mpqp.measures import I, X, Y, Z + >>> (I @ X + Z @ Y - Y @ X).is_diagonal() + False + >>> (I @ Z @ I - 2* Z @ Z @ Z + I @ I @ I).is_diagonal() + True + + """ + return all([all([a != X and a != Y for a in m.atoms]) for m in self.monomials]) + @typechecked class PauliStringMonomial(PauliString): @@ -810,7 +842,7 @@ class PauliStringMonomial(PauliString): def __init__(self, coef: Coef = 1, atoms: Optional[list["PauliStringAtom"]] = None): self.coef = coef """Coefficient of the monomial.""" - self.atoms = [] if atoms is None else atoms + self.atoms: list["PauliStringAtom"] = [] if atoms is None else atoms """The list of atoms in the monomial.""" @property @@ -821,6 +853,11 @@ def nb_qubits(self) -> int: def monomials(self) -> list["PauliStringMonomial"]: return [PauliStringMonomial(self.coef, self.atoms)] + @property + def eigen_values(self) -> npt.NDArray[np.complex64]: + """Return the eigen values associated with this Pauli monomial, without taking into account the coefficient.""" + return reduce(np.kron, [a.eigen_values for a in self.atoms]) + def __str__(self): if isinstance(self.coef, Expr): coef = f'({str(self.coef)})' @@ -912,6 +949,38 @@ def __hash__(self): atoms_as_tuples = tuple((atom.label for atom in self.atoms)) return hash(atoms_as_tuples) + def commutes_with(self, other: PauliStringMonomial) -> bool: + """Checks wether this Pauli monomial commutes (full commutativity) with the Pauli monomial in parameter. This + is done by checking that the number of anti-commuting atoms is even. + + Args: + pm: The Pauli monomial for which we want to check commutativity with this monomial. + + Returns: + True if this Pauli monomial commutes with the one in parameter. + + Examples: + >>> from mpqp.measures import I, X, Y, Z + >>> (I @ X @ Y).commutes_with(Z @ Y @ X) + True + >>> (X @ Z @ Z).commutes_with(Y @ Z @ I) + False + >>> (X @ Z @ Z).commutes_with(X @ Z @ Z) + True + + """ + if self.nb_qubits != other.nb_qubits: + raise NumberQubitsError( + f"The number of qubits of this Pauli monomial {self.nb_qubits} is not matching " + f"the one of the monomial in parameter {other.nb_qubits}" + ) + + return ( + sum(1 for a, b in zip(self.atoms, other.atoms) if not a.commutes_with(b)) + % 2 + == 0 + ) + def subs( self, values: dict[Expr | str, Real], remove_symbolic: bool = True ) -> PauliStringMonomial: @@ -1004,6 +1073,11 @@ class PauliStringAtom(PauliStringMonomial): Args: label: The label representing the Pauli operator. matrix: The matrix representation of the Pauli operator. + eig_values: + eig_vectors: + basis_change: + + TODO: finish comments Raises: RuntimeError: New atoms cannot be created, you should use the available @@ -1016,10 +1090,20 @@ class PauliStringAtom(PauliStringMonomial): __is_mutable = True - def __init__(self, label: str, matrix: npt.NDArray[np.complex64]): + def __init__( + self, + label: str, + matrix: npt.NDArray[np.complex64], + eig_values: list[int], + eig_vectors: npt.NDArray[np.complex64], + basis_change: list[type[NativeGate]], + ): if _allow_atom_creation: self.label = label self.matrix = matrix + self._eig_vals = np.array(eig_values) + self._eig_vecs = np.array(eig_vectors) + self._basis_change = basis_change self.__is_mutable = False else: raise RuntimeError( @@ -1046,6 +1130,10 @@ def coef(self): def monomials(self): return [PauliStringMonomial(self.coef, [a for a in self.atoms])] + @property + def eigen_values(self) -> npt.NDArray[np.complex64]: + return self._eig_vals + def __setattr__(self, name: str, value: Any): if not self.__is_mutable: raise AttributeError("This object is immutable") @@ -1096,9 +1184,6 @@ def __matmul__(self, other: PauliString): res @= other return res - def to_matrix(self) -> npt.NDArray[np.complex64]: - return self.matrix - def __eq__(self, other: object) -> bool: if isinstance(other, PauliStringAtom): return self.label == other.label @@ -1108,6 +1193,34 @@ def __eq__(self, other: object) -> bool: def __hash__(self): return hash(self.label) + def to_matrix(self) -> npt.NDArray[np.complex64]: + return self.matrix + + def commutes_with(self, other: PauliStringAtom) -> bool: + """Determines whether this single-qubit Pauli operator commutes with the one in parameter. In this case, this + can be done by simply checking if one of the atoms are identity, or if they are the same. + + Args: + other: The single-qubit Pauli operator with which want to check the commutativity + + Returns: + True if the atoms commute, False otherwise. + + Examples: + >>> from mpqp.measures import I, X, Y, Z + >>> X.commutes_with(X) + True + >>> X.commutes_with(Y) + False + >>> X.commutes_with(I) + True + >>> Y.commutes_with(Z) + False + >>> I.commutes_with(Z) + True + """ + return other == I or self == I or self == other + def to_other_language( self, language: Language, @@ -1157,23 +1270,37 @@ def to_other_language( _allow_atom_creation = True -I = PauliStringAtom("I", np.eye(2, dtype=np.complex64)) +I = PauliStringAtom( + "I", np.eye(2, dtype=np.complex64), [1, 1], np.array([[1, 0], [0, 1]]), [] +) r"""Pauli-I atom representing the identity operator in a Pauli monomial or string. Matrix representation: `\begin{pmatrix}1&0\\0&1\end{pmatrix}` """ -X = PauliStringAtom("X", 1 - np.eye(2, dtype=np.complex64)) +X = PauliStringAtom( + "X", + 1 - np.eye(2, dtype=np.complex64), + [1, -1], + (1 / np.sqrt(2)) * np.array([[1, 1], [1, -1]]), + [H], +) r"""Pauli-X atom representing the X operator in a Pauli monomial or string. Matrix representation: `\begin{pmatrix}0&1\\1&0\end{pmatrix}` """ -Y = PauliStringAtom("Y", np.fliplr(np.diag([1j, -1j]))) +Y = PauliStringAtom( + "Y", + np.fliplr(np.diag([1j, -1j])), + [1, -1], + (1 / np.sqrt(2)) * np.array([[1, 1j], [1, -1j]]), + [S_dagger, H], +) r"""Pauli-Y atom representing the Y operator in a Pauli monomial or string. Matrix representation: `\begin{pmatrix}0&-i\\i&0\end{pmatrix}` """ -Z = PauliStringAtom("Z", np.diag([1, -1])) +Z = PauliStringAtom("Z", np.diag([1, -1]), [1, -1], np.array([[1, 0], [0, 1]]), []) r"""Pauli-Z atom representing the Z operator in a Pauli monomial or string. Matrix representation: `\begin{pmatrix}1&0\\0&-1\end{pmatrix}` diff --git a/mpqp/execution/devices.py b/mpqp/execution/devices.py index b0bc9eae..b748f880 100644 --- a/mpqp/execution/devices.py +++ b/mpqp/execution/devices.py @@ -453,7 +453,7 @@ class AZUREDevice(AvailableDevice): QUANTINUUM_SIM_H1_1E = "quantinuum.sim.h1-1e" RIGETTI_SIM_QVM = "rigetti.sim.qvm" - RIGETTI_SIM_QPU_ANKAA_2 = "rigetti.qpu.ankaa-2" + RIGETTI_SIM_QPU_ANKAA_3 = "rigetti.qpu.ankaa-3" MICROSOFT_ESTIMATOR = "microsoft.estimator" diff --git a/mpqp/execution/providers/atos.py b/mpqp/execution/providers/atos.py index 9b4f868d..c990f375 100644 --- a/mpqp/execution/providers/atos.py +++ b/mpqp/execution/providers/atos.py @@ -248,9 +248,10 @@ def generate_observable_job(myqlm_circuit: "Circuit", job: Job) -> "JobQLM": Returns: A myQLM Job for retrieving the expectation value of the observable. """ + # TODO: [multi-obs] update this to take into account the case when we have list of Observables if TYPE_CHECKING: assert job.measure is not None and isinstance(job.measure, ExpectationMeasure) - qlm_obs = job.measure.observable.to_other_language(Language.MY_QLM) + qlm_obs = job.measure.observables[0].to_other_language(Language.MY_QLM) myqlm_job = myqlm_circuit.to_job( job_type="OBS", observable=qlm_obs, @@ -713,19 +714,24 @@ def run_myQLM(job: Job) -> Result: myqlm_job = generate_sample_job(myqlm_circuit, job) elif job.job_type == JobType.OBSERVABLE: + # TODO: update this to take into account the case when we have list of Observables myqlm_job = generate_observable_job(myqlm_circuit, job) else: raise ValueError(f"Job type {job.job_type} not handled") job.status = JobStatus.RUNNING - myqlm_result = qpu.submit(myqlm_job) + myqlm_result = qpu.submit( + myqlm_job + ) # TODO: update this to take into account the case when we have list of Observables # retrieving the results - result = extract_result(myqlm_result, job, job.device) + result = extract_result( + myqlm_result, job, job.device + ) # TODO: update this to take into account the case when we have list of Observables job.status = JobStatus.DONE - return result + return result # TODO: update this to take into account the case when we have list of Observables @typechecked @@ -763,11 +769,13 @@ def submit_QLM(job: Job) -> tuple[str, "AsyncResult"]: myqlm_job = generate_sample_job(myqlm_circuit, job) elif job.job_type == JobType.OBSERVABLE: + # TODO: update this to take into account the case when we have list of Observables myqlm_job = generate_observable_job(myqlm_circuit, job) else: raise ValueError(f"Job type {job.job_type} not handled") + # TODO: update this to take into account the case when we have list of Observables job.status = JobStatus.RUNNING async_result = qpu.submit(myqlm_job) job_id = async_result.get_info().id @@ -800,6 +808,7 @@ def run_QLM(job: Job) -> Result: "this function. Use `run` instead." ) + # TODO: update this to take into account the case when we have list of Observables _, async_result = submit_QLM(job) qlm_result = async_result.join() diff --git a/mpqp/execution/providers/aws.py b/mpqp/execution/providers/aws.py index 494e47c6..148b9029 100644 --- a/mpqp/execution/providers/aws.py +++ b/mpqp/execution/providers/aws.py @@ -103,6 +103,8 @@ def run_braket(job: Job) -> Result: This function is not meant to be used directly, please use :func:`~mpqp.execution.runner.run` instead. """ + # TODO : [multi-obs] update this to take into account the case when we have list of Observables + # TODO : [multi-obs] check if Braket allows for a list of several observables if not isinstance(job.device, AWSDevice): raise ValueError( "`job` must correspond to an `AWSDevice`, but corresponds to a " @@ -183,9 +185,10 @@ def submit_job_braket(job: Job) -> tuple[str, "QuantumTask"]: task = device.run(braket_circuit, shots=job.measure.shots, inputs=None) elif job.job_type == JobType.OBSERVABLE: + # TODO : [multi-obs] update this to take into account the case when we have list of Observables if TYPE_CHECKING: assert isinstance(job.measure, ExpectationMeasure) - herm_op = job.measure.observable.to_other_language(Language.BRAKET) + herm_op = job.measure.observables[0].to_other_language(Language.BRAKET) braket_circuit.expectation( # pyright: ignore[reportAttributeAccessIssue] observable=herm_op, target=job.measure.targets ) @@ -196,7 +199,10 @@ def submit_job_braket(job: Job) -> tuple[str, "QuantumTask"]: else: raise NotImplementedError(f"Job of type {job.job_type} not handled.") - return task.id, task + return ( + task.id, + task, + ) # TODO : [multi-obs] update this to take into account the case when we have list of Observables @typechecked diff --git a/mpqp/execution/providers/azure.py b/mpqp/execution/providers/azure.py index 856b20e0..392508d9 100644 --- a/mpqp/execution/providers/azure.py +++ b/mpqp/execution/providers/azure.py @@ -17,7 +17,7 @@ ) from mpqp.execution.devices import AZUREDevice from mpqp.execution.job import Job, JobStatus, JobType -from mpqp.execution.result import Result, Sample +from mpqp.execution.result import Result, Sample, BatchResult @typechecked @@ -76,7 +76,7 @@ def extract_result( result: "MicrosoftEstimatorResult | QiskitResult", job: Optional[Job], device: AZUREDevice, -) -> Result: +) -> Result | BatchResult: """Extract the result from Azure or Qiskit result objects and convert it into our format. Args: diff --git a/mpqp/execution/providers/google.py b/mpqp/execution/providers/google.py index 04b3d45a..4885aa5c 100644 --- a/mpqp/execution/providers/google.py +++ b/mpqp/execution/providers/google.py @@ -162,8 +162,9 @@ def run_local(job: Job) -> Result: if TYPE_CHECKING: assert isinstance(job.measure, ExpectationMeasure) - - cirq_obs = job.measure.observable.to_other_language( + # TODO: update this to take into account the case when we have list of Observables + # TODO: check if Cirq allows for a list of observable when computing expectation values (apparently yes) + cirq_obs = job.measure.observables[0].to_other_language( language=Language.CIRQ, circuit=cirq_circuit ) if TYPE_CHECKING: @@ -178,6 +179,7 @@ def run_local(job: Job) -> Result: ) else: return extract_result_OBSERVABLE_shot_noise( + # TODO: here precise the 'grouper' argument of measure_observable to precise the pauli grouping strategy measure_observables( cirq_circuit, observables=( # pyright: ignore[reportArgumentType] @@ -250,7 +252,8 @@ def run_local_processor(job: Job) -> Result: if TYPE_CHECKING: assert isinstance(job.measure, ExpectationMeasure) - cirq_obs = job.measure.observable.to_other_language( + # TODO: update this to take into account the case when we have list of Observables + cirq_obs = job.measure.observables[0].to_other_language( language=Language.CIRQ, circuit=cirq_circuit ) if TYPE_CHECKING: @@ -262,7 +265,10 @@ def run_local_processor(job: Job) -> Result: ) return extract_result_OBSERVABLE_processors( simulator.get_sampler(job.device.value).sample_expectation_values( - cirq_circuit, observables=cirq_obs, num_samples=job.measure.shots + # TODO: update for multi-observable runs + cirq_circuit, + observables=cirq_obs, + num_samples=job.measure.shots, ), job, ) @@ -349,6 +355,7 @@ def extract_result_OBSERVABLE_processors( NotImplementedError: If the job does not contain a measurement (i.e., ``job.measure`` is ``None``). """ + # TODO: update for multi-observable runs if job.measure is None: raise NotImplementedError("job.measure is None") mean = 0 @@ -375,12 +382,13 @@ def extract_result_OBSERVABLE_ideal( but this might result in slightly unexpected results. Args: - result : The result of the simulation. - job : The original job. + results: The result of the simulation. + job: The original job. Returns: The formatted result. """ + # TODO: update for multi-observable runs if job.measure is None: raise NotImplementedError("job.measure is None") return Result(job, sum(map(lambda r: r.real, results)), 0, job.measure.shots) @@ -393,12 +401,13 @@ def extract_result_OBSERVABLE_shot_noise( """Extracts the result from an observable-based job. Args: - result : The result of the simulation. - job : The original job. + results: The result of the simulation. + job: The original job. Returns: The formatted result. """ + # TODO: update for multi-observable runs if job.measure is None: raise NotImplementedError("job.measure is None") pauli_mono = PauliString.from_other_language( diff --git a/mpqp/execution/providers/ibm.py b/mpqp/execution/providers/ibm.py index 09208e2e..cf2e8346 100644 --- a/mpqp/execution/providers/ibm.py +++ b/mpqp/execution/providers/ibm.py @@ -12,7 +12,9 @@ from mpqp.core.instruction.gates import TOF, CRk, Gate, Id, P, Rk, Rx, Ry, Rz, T, U from mpqp.core.instruction.gates.native_gates import NativeGate from mpqp.core.instruction.measurement import BasisMeasure -from mpqp.core.instruction.measurement.expectation_value import ExpectationMeasure +from mpqp.core.instruction.measurement.expectation_value import ( + ExpectationMeasure, +) from mpqp.core.languages import Language from mpqp.execution.connection.ibm_connection import ( get_backend, @@ -20,7 +22,7 @@ ) from mpqp.execution.devices import AZUREDevice, IBMDevice from mpqp.execution.job import Job, JobStatus, JobType -from mpqp.execution.result import Result, Sample, StateVector +from mpqp.execution.result import BatchResult, Result, Sample, StateVector from mpqp.noise import DimensionalNoiseModel from mpqp.tools.errors import DeviceJobIncompatibleError, IBMRemoteExecutionError @@ -42,7 +44,7 @@ @typechecked -def run_ibm(job: Job) -> Result: +def run_ibm(job: Job) -> BatchResult | Result: """Executes the job on the right IBM Q device precised in the job in parameter. @@ -62,7 +64,7 @@ def run_ibm(job: Job) -> Result: @typechecked def compute_expectation_value( ibm_circuit: QuantumCircuit, job: Job, simulator: Optional["AerSimulator"] -) -> Result: +) -> BatchResult | Result: """Configures observable job and run it locally, and returns the corresponding Result. @@ -93,11 +95,15 @@ def compute_expectation_value( "Cannot compute expectation value if measure used in job is not of " "type ExpectationMeasure" ) + nb_shots = job.measure.shots - qiskit_observable = job.measure.observable.to_other_language(Language.QISKIT) + + qiskit_observables = [ + obs.to_other_language(Language.QISKIT) for obs in job.measure.observables + ] if TYPE_CHECKING: - assert isinstance(qiskit_observable, SparsePauliOp) + assert all(isinstance(obs, SparsePauliOp) for obs in qiskit_observables) if isinstance(job.device, IBMSimulatedDevice): from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager @@ -107,10 +113,10 @@ def compute_expectation_value( pm = generate_preset_pass_manager(optimization_level=0, backend=backend) ibm_circuit = pm.run(ibm_circuit) - qiskit_observable = qiskit_observable.apply_layout(ibm_circuit.layout) - + qiskit_observables = [ + obs.apply_layout(ibm_circuit.layout) for obs in qiskit_observables + ] options = {"default_shots": nb_shots} - estimator = Runtime_Estimator(mode=backend, options=options) else: @@ -125,13 +131,9 @@ def compute_expectation_value( } estimator = Estimator(options=options) - # 3M-TODO: implement the possibility to compute several expectation values at - # the same time when the circuit is the same apparently the estimator.run() - # can take several circuits and observables at the same time, because - # putting them all together will increase the performance - job.status = JobStatus.RUNNING - job_expectation = estimator.run([(ibm_circuit, qiskit_observable)]) + # circuits_and_observables = [(ibm_circuit, obs) for obs in qiskit_observables] + job_expectation = estimator.run([(ibm_circuit, qiskit_observables)]) estimator_result = job_expectation.result() if TYPE_CHECKING: @@ -536,11 +538,18 @@ def submit_remote_ibm(job: Job) -> tuple[str, "RuntimeJobV2"]: if TYPE_CHECKING: assert isinstance(meas, ExpectationMeasure) estimator = Runtime_Estimator(mode=session) - qiskit_observable = meas.observable.to_other_language(Language.QISKIT) + qiskit_observables = [ + obs.to_other_language(Language.QISKIT) for obs in meas.observables + ] if TYPE_CHECKING: - assert isinstance(qiskit_observable, SparsePauliOp) + assert all(isinstance(obs, SparsePauliOp) for obs in qiskit_observables) - qiskit_observable = qiskit_observable.apply_layout(qiskit_circ.layout) + qiskit_observables = [ + obs.apply_layout( # pyright: ignore[reportAttributeAccessIssue] + qiskit_circ.layout + ) + for obs in qiskit_observables + ] # We have to disable all the twirling options and set manually the number of circuits and shots per circuits twirling = getattr(estimator.options, "twirling", None) @@ -552,7 +561,8 @@ def submit_remote_ibm(job: Job) -> tuple[str, "RuntimeJobV2"]: setattr(estimator.options, "default_shots", meas.shots) - ibm_job = estimator.run([(qiskit_circ, qiskit_observable)]) + ibm_job = estimator.run([(qiskit_circ, obs) for obs in qiskit_observables]) + elif job.job_type == JobType.SAMPLE: if TYPE_CHECKING: assert isinstance(meas, BasisMeasure) @@ -569,7 +579,7 @@ def submit_remote_ibm(job: Job) -> tuple[str, "RuntimeJobV2"]: @typechecked -def run_remote_ibm(job: Job) -> Result: +def run_remote_ibm(job: Job) -> BatchResult | Result: """Submits the job on the right IBM remote device, precised in the job in parameter, and waits until the job is completed. @@ -587,6 +597,7 @@ def run_remote_ibm(job: Job) -> Result: ibm_result = remote_job.result() if TYPE_CHECKING: assert isinstance(job.device, IBMDevice) + return extract_result(ibm_result, job, job.device) @@ -595,7 +606,7 @@ def extract_result( result: "QiskitResult | EstimatorResult | PrimitiveResult[PubResult | SamplerPubResult]", job: Optional[Job], device: "IBMDevice | IBMSimulatedDevice | AZUREDevice", -) -> Result: +) -> BatchResult | Result: """Parses a result from ``IBM`` execution (remote or local) in a ``MPQP`` :class:`~mpqp.execution.result.Result`. @@ -614,24 +625,28 @@ def extract_result( # If this is a PubResult from primitives V2 if isinstance(result, PrimitiveResult): - res_data = result[0].data + all_results = [] # res_data is a DataBin, which means all typechecking is out of the # windows for this specific object + res_data = result[0].data - # If we are in observable mode if hasattr(res_data, "evs"): if job is None: job = Job(JobType.OBSERVABLE, QCircuit(0), device) - mean = float(res_data.evs) # pyright: ignore[reportAttributeAccessIssue] - error = float(res_data.stds) # pyright: ignore[reportAttributeAccessIssue] - shots = ( - job.measure.shots - if job.device.is_simulator() and job.measure is not None - else result[0].metadata["shots"] - ) - return Result(job, mean, error, shots) - # If we are in sample mode + exp_values = res_data.evs # pyright: ignore[reportAttributeAccessIssue] + for i in range(len(exp_values)): + mean = float(exp_values[i]) + error = float( + res_data.stds[i] # pyright: ignore[reportAttributeAccessIssue] + ) + shots = ( + job.measure.shots + if job.device.is_simulator() and job.measure is not None + else result[0].metadata["shots"] + ) + all_results.append(Result(job, mean, error, shots)) + else: if job is None: shots = ( @@ -650,16 +665,21 @@ def extract_result( assert job.measure is not None counts = ( - res_data.c.get_counts() # pyright: ignore[reportAttributeAccessIssue] - ) + res_data.c.get_counts() + ) # pyright: ignore[reportAttributeAccessIssue] data = [ Sample( - bin_str=item, count=counts[item], nb_qubits=job.circuit.nb_qubits + bin_str=item, + count=counts[item], + nb_qubits=job.circuit.nb_qubits, ) for item in counts ] + # Since we don't handle multiple sampling jobs, we know the first result is the only one return Result(job, data, None, job.measure.shots) + return BatchResult(all_results) if len(all_results) > 1 else all_results[0] + else: if job is not None and ( @@ -671,15 +691,23 @@ def extract_result( ) if isinstance(result, EstimatorResult): - if job is None: - job = Job(JobType.OBSERVABLE, QCircuit(0), device) - shots = result.metadata[0]["shots"] if "shots" in result.metadata[0] else 0 - variance = ( - result.metadata[0]["variance"] - if "variance" in result.metadata[0] - else None - ) - return Result(job, result.values[0], variance, shots) + all_results = [] + + for i in range(len(result.values)): + if job is None: + job = Job(JobType.OBSERVABLE, QCircuit(0), device) + + shots = ( + result.metadata[i]["shots"] if "shots" in result.metadata[i] else 0 + ) + variance = ( + result.metadata[i]["variance"] + if "variance" in result.metadata[i] + else None + ) + all_results.append(Result(job, result.values[i], variance, shots)) + + return BatchResult(all_results) if len(all_results) > 1 else all_results[0] elif isinstance( result, QiskitResult @@ -738,7 +766,7 @@ def extract_result( @typechecked -def get_result_from_ibm_job_id(job_id: str) -> Result: +def get_result_from_ibm_job_id(job_id: str) -> Result | BatchResult: """Retrieves from IBM remote platform and parse the result of the job_id given in parameter. If the job is still running, we wait (blocking) until it is ``DONE``. @@ -747,7 +775,7 @@ def get_result_from_ibm_job_id(job_id: str) -> Result: job_id: Id of the remote IBM job. Returns: - The result converted to our format. + The result (or batch of result) converted to our format. """ from qiskit.providers import BackendV1, BackendV2 @@ -780,6 +808,16 @@ def get_result_from_ibm_job_id(job_id: str) -> Result: def extract_samples(job: Job, result: QiskitResult) -> list[Sample]: + """Extracts measurement samples from the execution results. + + Args: + job: ``MPQP`` job used to generate the run. Enables a more complete result. + result: Result returned by IBM after running of the job. + + Returns: + A list of sample objects representing measurement outcomes. + + """ counts = result.get_counts(0) job_data = result.data() return [ diff --git a/mpqp/execution/remote_handler.py b/mpqp/execution/remote_handler.py index 1411b4c2..2dbbc4bb 100644 --- a/mpqp/execution/remote_handler.py +++ b/mpqp/execution/remote_handler.py @@ -6,34 +6,32 @@ from typing import Optional -from typeguard import typechecked - -from mpqp.execution import Result +from mpqp.execution import BatchResult, Result from mpqp.execution.connection.aws_connection import get_all_task_ids as aws_ids +from mpqp.execution.connection.azure_connection import get_all_job_ids as azure_ids from mpqp.execution.connection.google_connection import get_all_job_ids as cirq_ids from mpqp.execution.connection.ibm_connection import get_all_job_ids as ibm_ids from mpqp.execution.connection.qlm_connection import get_all_job_ids as qlm_ids -from mpqp.execution.connection.google_connection import get_all_job_ids as cirq_ids -from mpqp.execution.connection.azure_connection import get_all_job_ids as azure_ids from mpqp.execution.devices import ( ATOSDevice, AvailableDevice, AWSDevice, - GOOGLEDevice, AZUREDevice, + GOOGLEDevice, IBMDevice, ) from mpqp.execution.job import Job from mpqp.execution.providers.atos import get_result_from_qlm_job_id from mpqp.execution.providers.aws import get_result_from_aws_task_arn -from mpqp.execution.providers.ibm import get_result_from_ibm_job_id from mpqp.execution.providers.azure import get_result_from_azure_job_id +from mpqp.execution.providers.ibm import get_result_from_ibm_job_id +from typeguard import typechecked @typechecked def get_remote_result( job_data: str | Job, device: Optional[AvailableDevice] = None -) -> Result: +) -> Result | BatchResult: """Retrieve and parse a remote the result from a job_id and device. If the job is still running, it will wait until it is done. @@ -44,7 +42,7 @@ def get_remote_result( ``job_data`` is the identifier of the job. Returns: - The ``Result`` of the desired remote job. + The result(s) associated with the desired remote job in parameter. Examples: >>> print(get_remote_result('Job141933', ATOSDevice.QLM_LINALG)) diff --git a/mpqp/execution/result.py b/mpqp/execution/result.py index f42e6824..b1de0320 100644 --- a/mpqp/execution/result.py +++ b/mpqp/execution/result.py @@ -557,7 +557,7 @@ class BatchResult: """ - def __init__(self, results: list[Result]): + def __init__(self, results: list[Result | BatchResult]): self.results = results """See parameter description.""" diff --git a/mpqp/execution/runner.py b/mpqp/execution/runner.py index cb8f173b..b1bda227 100644 --- a/mpqp/execution/runner.py +++ b/mpqp/execution/runner.py @@ -23,9 +23,6 @@ from typing import Iterable, Optional import numpy as np -from sympy import Expr -from typeguard import typechecked - from mpqp.core.circuit import QCircuit from mpqp.core.instruction.breakpoint import Breakpoint from mpqp.core.instruction.measurement.basis_measure import BasisMeasure @@ -52,6 +49,8 @@ from mpqp.tools.display import state_vector_ket_shape from mpqp.tools.errors import DeviceJobIncompatibleError, RemoteExecutionError from mpqp.tools.generics import OneOrMany, find_index, flatten +from sympy import Expr +from typeguard import typechecked @typechecked @@ -76,8 +75,14 @@ def adjust_measure(measure: ExpectationMeasure, circuit: QCircuit): """ Id_before = np.eye(2 ** measure.rearranged_targets[0]) Id_after = np.eye(2 ** (circuit.nb_qubits - measure.rearranged_targets[-1] - 1)) + + tweaked_observables = [ + Observable(np.kron(np.kron(Id_before, obs.matrix), Id_after)) + for obs in measure.observables + ] + tweaked_measure = ExpectationMeasure( - Observable(np.kron(np.kron(Id_before, measure.observable.matrix), Id_after)), + tweaked_observables, list(range(circuit.nb_qubits)), measure.shots, ) @@ -143,7 +148,7 @@ def _run_single( device: AvailableDevice, values: dict[Expr | str, Complex], display_breakpoints: bool = True, -) -> Result: +) -> Result | BatchResult: """Runs the circuit on the ``backend``. If the circuit depends on variables, the ``values`` given in parameters are used to do the substitution. @@ -355,6 +360,9 @@ def submit( if isinstance(device, IBMDevice): job_id, _ = submit_remote_ibm(job) + # TODO : [multi-obs] we need to set if we return several ids, or if we return the workload id + # we may need to update how we parse the result in get results with the list of obs case (cauz the ibm result + # will only correspond to pauli monomials elif isinstance(device, ATOSDevice): job_id, _ = submit_QLM(job) elif isinstance(device, AWSDevice): diff --git a/mpqp/execution/vqa/vqa.py b/mpqp/execution/vqa/vqa.py index 16b9ac3b..08b8a857 100644 --- a/mpqp/execution/vqa/vqa.py +++ b/mpqp/execution/vqa/vqa.py @@ -1,18 +1,18 @@ from __future__ import annotations -from typing import Any, Callable, Collection, Optional, TypeVar, Union +from typing import TYPE_CHECKING, Any, Callable, Collection, Optional, TypeVar, Union import numpy as np import numpy.typing as npt -from scipy.optimize import OptimizeResult -from scipy.optimize import minimize as scipy_minimize -from sympy import Expr -from typeguard import typechecked - from mpqp.core.circuit import QCircuit +from mpqp.execution import Result from mpqp.execution.devices import AvailableDevice from mpqp.execution.runner import _run_single # pyright: ignore[reportPrivateUsage] from mpqp.execution.vqa.optimizer import Optimizer +from scipy.optimize import OptimizeResult +from scipy.optimize import minimize as scipy_minimize +from sympy import Expr +from typeguard import typechecked T1 = TypeVar("T1") T2 = TypeVar("T2") @@ -257,11 +257,14 @@ def _minimize_local_circ( def eval_circ(params: OptimizerInput): # pyright is bad with abstract numeric types: # "float" is incompatible with "Complex" - return _run_single( + result = _run_single( circ, device, _maps(variables, params), # pyright: ignore[reportArgumentType] - ).expectation_value + ) + if TYPE_CHECKING: + assert isinstance(result, Result) + return result.expectation_value return _minimize_local_func( eval_circ, method, init_params, len(variables), optimizer_options diff --git a/mpqp/qasm/lexer_utils.py b/mpqp/qasm/lexer_utils.py index 7209f7fa..b1dd35b1 100644 --- a/mpqp/qasm/lexer_utils.py +++ b/mpqp/qasm/lexer_utils.py @@ -118,6 +118,7 @@ def t_error(t): # pyright: ignore[reportMissingParameterType] "y": Y, "z": Z, "s": S, + "sdg": S_dagger, "id": Id, "t": T, } diff --git a/mpqp/qasm/qasm_to_myqlm.py b/mpqp/qasm/qasm_to_myqlm.py index 0d5ae2f4..2964a1c7 100644 --- a/mpqp/qasm/qasm_to_myqlm.py +++ b/mpqp/qasm/qasm_to_myqlm.py @@ -4,15 +4,15 @@ call the function :func:`qasm2_to_myqlm_Circuit` to generate the circuit from the qasm code.""" +import re from typing import TYPE_CHECKING +from mpqp.qasm.open_qasm_2_and_3 import open_qasm_hard_includes from typeguard import typechecked if TYPE_CHECKING: from qat.core.wrappers.circuit import Circuit -from mpqp.qasm.open_qasm_2_and_3 import open_qasm_hard_includes - @typechecked def qasm2_to_myqlm_Circuit(qasm_str: str) -> "Circuit": @@ -45,5 +45,10 @@ def qasm2_to_myqlm_Circuit(qasm_str: str) -> "Circuit": from qat.interop.openqasm import OqasmParser parser = OqasmParser(gates={"p": "PH", "u": "U"}) # requires myqlm-interop-1.9.3 - circuit = parser.compile(open_qasm_hard_includes(qasm_str, set())) + + # We replace 'sdg' for S_dagger gate by 'u1(pi/2)', because of problem on myqlm side + pattern = re.compile(r'(? bool: Returns: ``True`` if the matrix in parameter is Unitary. - Example: + Examples: >>> is_unitary(np.array([[1,1],[1,-1]])) False >>> is_unitary(np.array([[1,1],[1,-1]])/np.sqrt(2)) @@ -132,6 +132,38 @@ def is_unitary(matrix: Matrix) -> bool: ) +@typechecked +def is_diagonal(matrix: Matrix): + """Checks whether the square matrix in parameter is diagonal. + + Args: + matrix: Matrix for which we want to know if it is diagonal. + + Returns: + ``True`` if the matrix in parameter is diagonal. + + Examples: + >>> is_diagonal(np.diag([1, 4, 2, 3])) + True + >>> is_diagonal(np.array([[1, 1], [1, -1]])) + False + """ + # Reference: https://stackoverflow.com/questions/43884189/check-if-a-large-matrix-is-diagonal-matrix-in-python + # Author: Daniel F, 10th May 2017 + + i, j = matrix.shape + if i != j: + raise ValueError( + "The input matrix is not square. Dimensions = (" + + str(i) + + ", " + + str(j) + + ")." + ) + test = matrix.reshape(-1)[:-1].reshape(i - 1, j + 1) + return not np.any(test[:, 1:]) + + @typechecked def closest_unitary(matrix: Matrix) -> Matrix: """Calculate the unitary matrix that is closest with respect to the operator @@ -273,7 +305,7 @@ def rand_clifford_matrix( """Generate a random Clifford matrix. Args: - size: Size (number of columns) of the square matrix to generate. + nb_qubits: Qubits of the clifford operator to be generated. seed: Seed used to initialize the random number generation. Returns: @@ -310,7 +342,6 @@ def rand_unitary_2x2_matrix( """Generate a random one-qubit unitary matrix. Args: - size: Size (number of columns) of the square matrix to generate. seed: Used for the random number generation. If unspecified, a new generator will be used. If a ``Generator`` is provided, it will be used to generate any random number needed. Finally if an ``int`` is diff --git a/mpqp/tools/obs_decomposition.py b/mpqp/tools/obs_decomposition.py index 601c0c95..89b74afb 100644 --- a/mpqp/tools/obs_decomposition.py +++ b/mpqp/tools/obs_decomposition.py @@ -307,7 +307,7 @@ def get_monomial(self) -> PauliStringMonomial: def compute_coefficients_diagonal_case( m: list[bool], current_node: DiagPauliNode, - diag_elements: npt.NDArray[np.float64], + diag_elements: npt.NDArray[np.float32], monomial_list: list[PauliStringMonomial], ): """Computes coefficients for the current node in the pauli tree based on the @@ -356,7 +356,7 @@ def update_tree_diagonal_case(current_node: DiagPauliNode, m: list[bool]): def generate_and_explore_node_diagonal_case( m: list[bool], current_node: DiagPauliNode, - diag_elements: npt.NDArray[np.float64], + diag_elements: npt.NDArray[np.float32], n: int, monomials: list[PauliStringMonomial], progression: Optional[list[int]] = None, @@ -480,7 +480,7 @@ def generate_hadamard(n: int) -> npt.NDArray[np.int8]: def compute_coefficients_walsh( - H_matrix: npt.NDArray[np.int8], diagonal_elements: npt.NDArray[np.float64] + H_matrix: npt.NDArray[np.int8], diagonal_elements: npt.NDArray[np.float32] ) -> list[float]: """Computes the coefficients using the Walsh-Hadamard transform. @@ -535,7 +535,7 @@ def decompose_diagonal_observable_walsh_hadamard( if TYPE_CHECKING: assert isinstance(m, PauliStringMonomial) if c != 0.0: - m.coef = c + m.coef = c.real final_monomials.append(m) return PauliString(final_monomials) diff --git a/mpqp/tools/pauli_grouping.py b/mpqp/tools/pauli_grouping.py new file mode 100644 index 00000000..c65f967e --- /dev/null +++ b/mpqp/tools/pauli_grouping.py @@ -0,0 +1,34 @@ +from mpqp.core.instruction.measurement.pauli_string import PauliStringMonomial + + +def full_commutation_pauli_grouping_greedy(monomials: set[PauliStringMonomial]): + """ + TODO: comment + Args: + monomials: + + Returns: + + """ + groups = [] + + for m in monomials: + added = False + for group in groups: + if all(m.commutes_with(m_g) for m_g in group): + group.append(m) + added = True + break + + if not added: + groups.append([m]) + + return groups + + +def full_commutation_pauli_grouping_ibm_clique(monomials: set[PauliStringMonomial]): + pass + + +def qubit_wise_commutation_pauli_grouping(monomials: set[PauliStringMonomial]): + pass diff --git a/mpqp/tools/theoretical_simulation.py b/mpqp/tools/theoretical_simulation.py index d15ca69f..b87ff8ee 100644 --- a/mpqp/tools/theoretical_simulation.py +++ b/mpqp/tools/theoretical_simulation.py @@ -32,7 +32,7 @@ from typeguard import typechecked from mpqp import QCircuit -from mpqp.execution import AvailableDevice, AWSDevice +from mpqp.execution import AvailableDevice, AWSDevice, Result from mpqp.execution.runner import _run_single # pyright: ignore[reportPrivateUsage] from mpqp.measures import BasisMeasure @@ -211,8 +211,9 @@ def exp_id_dist( noisy_circuit = circuit.without_measurements() noisy_circuit.add(BasisMeasure(shots=shots)) - mpqp_counts = _run_single(noisy_circuit, device, {}).counts - + result = _run_single(noisy_circuit, device, {}) + assert isinstance(result, Result) + mpqp_counts = result.counts return float(jensenshannon(mpqp_counts, noisy_probs * sum(mpqp_counts))) diff --git a/tests/core/instruction/gates/test_custom_gate.py b/tests/core/instruction/gates/test_custom_gate.py index 99d57da0..2b150e27 100644 --- a/tests/core/instruction/gates/test_custom_gate.py +++ b/tests/core/instruction/gates/test_custom_gate.py @@ -12,6 +12,7 @@ AWSDevice, GOOGLEDevice, IBMDevice, + Result, ) from mpqp.execution.runner import _run_single # pyright: ignore[reportPrivateUsage] from mpqp.gates import * @@ -62,6 +63,7 @@ def test_random_orthogonal_matrix(circ_size: int, device: AvailableDevice): result = _run_single(c, device, {}) # we reduce the precision because of approximation errors coming from CustomGate usage + assert isinstance(result, Result) assert matrix_eq(result.amplitudes, exp_state_vector, 1e-5, 1e-5) @@ -107,7 +109,9 @@ def test_custom_gate_with_native_gates(device: AvailableDevice): result2 = _run_single(c2, device, {}) # we reduce the precision because of approximation errors coming from CustomGate usage - assert matrix_eq(result1.amplitudes, result2.amplitudes, 1e-5, 1e-5) + assert isinstance(result1, Result) + assert isinstance(result2, Result) + assert matrix_eq(result1.amplitudes, result2.amplitudes, 1e-4, 1e-4) @pytest.mark.parametrize( @@ -138,4 +142,6 @@ def test_custom_gate_with_random_circuit(circ_size: int, device: AvailableDevice result2 = _run_single(custom_gate_circ, device, {}) # we reduce the precision because of approximation errors coming from CustomGate usage + assert isinstance(result1, Result) + assert isinstance(result2, Result) assert matrix_eq(result1.amplitudes, result2.amplitudes, 1e-4, 1e-4) diff --git a/tests/core/instruction/measurement/test_basis.py b/tests/core/instruction/measurement/test_basis.py index 4063c416..ca0d67bf 100644 --- a/tests/core/instruction/measurement/test_basis.py +++ b/tests/core/instruction/measurement/test_basis.py @@ -6,7 +6,7 @@ import pytest from mpqp import QCircuit -from mpqp.execution import ATOSDevice, AWSDevice, GOOGLEDevice, IBMDevice +from mpqp.execution import ATOSDevice, AWSDevice, GOOGLEDevice, IBMDevice, Result from mpqp.execution.devices import AvailableDevice from mpqp.execution.runner import _run_single # pyright: ignore[reportPrivateUsage] from mpqp.execution.runner import run @@ -281,7 +281,7 @@ def test_run_with_custom_basis_probas( circuit: QCircuit, expected_probabilities: npt.NDArray[np.complex64] ): res = _run_single(circuit, IBMDevice.AER_SIMULATOR, {}) - + assert isinstance(res, Result) assert matrix_eq(expected_probabilities, res.probabilities.astype(np.complex64)) @@ -318,7 +318,7 @@ def test_valid_run_custom_basis_state_vector_one_qubit( device, {}, ) - + assert isinstance(result, Result) assert matrix_eq(vectors[expected_vector_index], result.amplitudes) diff --git a/tests/core/test_circuit.py b/tests/core/test_circuit.py index 87435876..a1d75769 100644 --- a/tests/core/test_circuit.py +++ b/tests/core/test_circuit.py @@ -191,7 +191,7 @@ def test_count(circuit: QCircuit, filter: tuple[type[Gate]], count: int): ] ), "[BasisMeasure([0, 1], shots=1000), ExpectationMeasure(" - "Observable(array([[1.+0.j, 0.+0.j], [0.+0.j, 1.+0.j]], dtype=complex64)), [1], shots=1000)]", + "[Observable(array([[1.+0.j, 0.+0.j], [0.+0.j, 1.+0.j]], dtype=complex64))], [1], shots=1000)]", ) ], ) diff --git a/tests/examples/test_demonstrations.py b/tests/examples/test_demonstrations.py index d3f8c4ce..d9a304c4 100644 --- a/tests/examples/test_demonstrations.py +++ b/tests/examples/test_demonstrations.py @@ -38,7 +38,8 @@ def test_sample_demo(): circuit.add(CZ(2, 1)) circuit.add([SWAP(2, 0), CNOT(0, 2)]) circuit.add(Ry(3.14 / 2, 2)) - circuit.add(S(1)) + circuit.add(S(0)) + circuit.add(S_dagger(1)) circuit.add(H(3)) circuit.add(CNOT(1, 2)) circuit.add(Rx(3.14, 1)) @@ -80,7 +81,7 @@ def test_sample_demo_aer_stabilizers(): circuit.add(Z(2)) circuit.add(CZ(2, 1)) circuit.add([SWAP(2, 0), CNOT(0, 2)]) - circuit.add(S(1)) + circuit.add(S_dagger(1)) circuit.add(H(3)) circuit.add(CNOT(1, 2)) circuit.add(CNOT(3, 0)) @@ -112,7 +113,7 @@ def test_statevector_demo(): SWAP(2, 0), CNOT(0, 2), Ry(1.7, 2), - S(1), + S_dagger(1), H(3), CNOT(1, 2), Rx(3.14, 1), @@ -162,14 +163,14 @@ def test_statevector_demo_stab(): circuit = QCircuit(4) # Constructing the circuit by adding gates - circuit.add(S(0)) + circuit.add(S_dagger(0)) circuit.add(CNOT(0, 1)) circuit.add(X(0)) circuit.add(H(1)) circuit.add(Z(2)) circuit.add(CZ(2, 1)) circuit.add([SWAP(2, 0), CNOT(0, 2)]) - circuit.add(S(1)) + circuit.add(S_dagger(1)) circuit.add(H(3)) circuit.add(CNOT(3, 0)) @@ -252,7 +253,7 @@ def test_aws_mpqp_executions(): circuit.add(SWAP(2, 0)) circuit.add(CNOT(0, 2)) circuit.add(Ry(3.14 / 2, 2)) - circuit.add(S(1)) + circuit.add(S_dagger(1)) circuit.add(H(3)) circuit.add(CNOT(1, 2)) circuit.add(Rx(3.14, 1)) @@ -310,7 +311,7 @@ def test_all_native_gates(): # Declaration of the circuit with the right size circuit = QCircuit(3, label="Test native gates") # Constructing the circuit by adding gates and measurements - circuit.add([H(0), X(1), Y(2), Z(0), S(1), T(0)]) + circuit.add([H(0), X(1), Y(2), Z(0), S_dagger(1), T(0)]) circuit.add([Rx(1.2324, 2), Ry(-2.43, 0), Rz(1.04, 1), Rk(-1, 1), P(-323, 2)]) circuit.add(U(1.2, 2.3, 3.4, 2)) circuit.add(SWAP(2, 0)) diff --git a/tests/execution/providers/test_atos.py b/tests/execution/providers/test_atos.py index f1a9e2b9..cd17bbf6 100644 --- a/tests/execution/providers/test_atos.py +++ b/tests/execution/providers/test_atos.py @@ -24,7 +24,7 @@ T(0), CNOT(0, 1), Ry(np.pi / 2, 2), - S(1), + S_dagger(1), CZ(2, 1), SWAP(2, 0), BasisMeasure(list(range(3)), shots=2000), @@ -36,7 +36,7 @@ T(0), CNOT(0, 1), Ry(np.pi / 2, 2), - S(1), + S_dagger(1), CZ(2, 1), SWAP(2, 0), BasisMeasure(list(range(3)), shots=0), diff --git a/tests/execution/providers/test_azure.py b/tests/execution/providers/test_azure.py index 30bb889b..9cef311d 100644 --- a/tests/execution/providers/test_azure.py +++ b/tests/execution/providers/test_azure.py @@ -21,7 +21,7 @@ AZUREDevice.QUANTINUUM_SIM_H1_1, AZUREDevice.QUANTINUUM_SIM_H1_1E, AZUREDevice.QUANTINUUM_SIM_H1_1SC, - AZUREDevice.RIGETTI_SIM_QPU_ANKAA_2, + AZUREDevice.RIGETTI_SIM_QPU_ANKAA_3, AZUREDevice.RIGETTI_SIM_QVM, ] diff --git a/tests/execution/test_multi_observable.py b/tests/execution/test_multi_observable.py new file mode 100644 index 00000000..c7cf915c --- /dev/null +++ b/tests/execution/test_multi_observable.py @@ -0,0 +1,66 @@ +import numpy as np +import pytest +from mpqp import QCircuit +from mpqp.core.instruction import ExpectationMeasure, Observable +from mpqp.execution import AvailableDevice, run, IBMDevice, Result, BatchResult +from mpqp.gates import * + + +def list_circuits(): + return [ + QCircuit([H(0), CNOT(0, 1)]), + QCircuit([H(0), X(1)]), + # TODO add random circuit + ] + + +def list_observables(): + return [ + [ + Observable(np.ones((4, 4)).astype(np.complex64)), + Observable(np.diag([1, 2, -3, 4])), + # TODO add random observable ? + ] + ] + + +def list_devices(): + return [IBMDevice.AER_SIMULATOR] + + +@pytest.mark.parametrize( + "circuit, observables, device", + [ + (i, j, k) + for i in list_circuits() + for j in list_observables() + for k in list_devices() + ], +) +def test_sequential_versus_multi( + circuit: QCircuit, observables: list[Observable], device: AvailableDevice +): + seq_results = [ + run( + circuit + + QCircuit([ExpectationMeasure(obs, shots=0)], nb_qubits=circuit.nb_qubits), + device, + ) + for obs in observables + ] + + multi_result = run( + circuit + + QCircuit( + [ExpectationMeasure(observables, shots=0)], nb_qubits=circuit.nb_qubits + ), + device, + ) + + assert isinstance(multi_result, BatchResult) + assert len(seq_results) == len(multi_result.results) + + for r1, r2 in zip(seq_results, multi_result.results): + assert isinstance(r1, Result) + assert isinstance(r2, Result) + assert r1.expectation_value == r2.expectation_value diff --git a/tests/execution/test_runner.py b/tests/execution/test_runner.py index f2c38f4a..3798063f 100644 --- a/tests/execution/test_runner.py +++ b/tests/execution/test_runner.py @@ -33,10 +33,11 @@ def test_adjust_measure( measure = ExpectationMeasure(Observable(obs_matrix), measure_targets) adjusted_observable_matrix = np.kron( np.kron( - np.eye(2**nb_ids_before, dtype=np.complex64), measure.observable.matrix + np.eye(2**nb_ids_before, dtype=np.complex64), measure.observables[0].matrix ), np.eye(2**nb_ids_after), ) assert matrix_eq( - adjust_measure(measure, circuit).observable.matrix, adjusted_observable_matrix + adjust_measure(measure, circuit).observables[0].matrix, + adjusted_observable_matrix, ) diff --git a/tests/execution/test_simulated_devices.py b/tests/execution/test_simulated_devices.py index 3a89ec94..315b9376 100644 --- a/tests/execution/test_simulated_devices.py +++ b/tests/execution/test_simulated_devices.py @@ -19,7 +19,8 @@ def circuits(): X(1), Y(2), Z(0), - S(1), + S(0), + S_dagger(1), T(0), Rx(1.2324, 2), Ry(-2.43, 0), diff --git a/tests/execution/test_validity.py b/tests/execution/test_validity.py index 63b563fa..3a2b9292 100644 --- a/tests/execution/test_validity.py +++ b/tests/execution/test_validity.py @@ -4,13 +4,11 @@ import numpy as np import numpy.typing as npt import pytest - from mpqp import QCircuit from mpqp.core.instruction.barrier import Barrier from mpqp.core.instruction.breakpoint import Breakpoint from mpqp.core.instruction.gates.native_gates import NATIVE_GATES from mpqp.core.instruction.instruction import Instruction -from mpqp.core.instruction.measurement import BasisMeasure from mpqp.core.instruction.measurement.measure import Measure from mpqp.core.instruction.measurement.pauli_string import I as Ip from mpqp.core.instruction.measurement.pauli_string import PauliString @@ -131,6 +129,7 @@ def test_state_vector_result_HEA_ansatz( batch = run(hae_3_qubit_circuit(*parameters), state_vector_devices) assert isinstance(batch, BatchResult) for result in batch: + assert isinstance(result, Result) assert matrix_eq(result.amplitudes, expected_vector) @@ -201,6 +200,7 @@ def test_state_vector_various_native_gates(gates: list[Gate], expected_vector: M batch = run(QCircuit(gates), state_vector_devices) assert isinstance(batch, BatchResult) for result in batch: + assert isinstance(result, Result) if isinstance(result.device, GOOGLEDevice): # TODO : Cirq needs atol 1 as some results differ by 0.1 assert matrix_eq(result.amplitudes, expected_vector, atol=1) @@ -251,6 +251,7 @@ def test_sample_basis_state_in_samples(gates: list[Gate], basis_states: list[str assert isinstance(batch, BatchResult) nb_states = len(basis_states) for result in batch: + assert isinstance(result, Result) assert len(result.samples) == nb_states @@ -275,6 +276,7 @@ def test_sample_counts_in_trust_interval(instructions: list[Gate]): batch = run(c, sampling_devices) assert isinstance(batch, BatchResult) for result in batch: + assert isinstance(result, Result) print(result) print("expected_counts: " + str(expected_counts)) counts = result.counts @@ -324,6 +326,7 @@ def test_observable_ideal_case( batch = run(c, sampling_devices) assert isinstance(batch, BatchResult) for result in batch: + assert isinstance(result, Result) assert abs(result.expectation_value - expected_value) < ( atol + rtol * abs(expected_value) ) diff --git a/tests/execution/test_vqa.py b/tests/execution/test_vqa.py index 5f3ca1b0..959e44fb 100644 --- a/tests/execution/test_vqa.py +++ b/tests/execution/test_vqa.py @@ -83,7 +83,7 @@ def run(): ), ATOSDevice.MYQLM_PYLINALG, {theta: params[0]}, - ).expectation_value + ).expectation_value # pyright: ignore[reportAttributeAccessIssue] ** 2 ), 1, diff --git a/tests/noise/test_noisy_execution.py b/tests/noise/test_noisy_execution.py index 724b999e..742b21e4 100644 --- a/tests/noise/test_noisy_execution.py +++ b/tests/noise/test_noisy_execution.py @@ -59,7 +59,8 @@ def circuit(): X(1), Y(2), Z(0), - S(1), + S(0), + S_dagger(1), T(0), Rx(1.2324, 2), Ry(-2.43, 0), @@ -136,7 +137,9 @@ def test_all_native_gates_local_noise( circuit.add( [ BasisMeasure([0, 1, 2], shots=1023), - Depolarizing(0.23, [0, 2], gates=[H, X, Y, Z, S, T, Rx, Ry, Rz, Rk, P, U]), + Depolarizing( + 0.23, [0, 2], gates=[H, X, Y, Z, S, S_dagger, T, Rx, Ry, Rz, Rk, P, U] + ), Depolarizing(0.23, [0, 1], dimension=2, gates=[SWAP, CNOT, CZ]), BitFlip(0.2, [0, 2]), BitFlip(0.1, [0, 1], gates=[CNOT, H]), diff --git a/tests/qasm/test_mpqp_to_qasm.py b/tests/qasm/test_mpqp_to_qasm.py index 5f5244d3..be624728 100644 --- a/tests/qasm/test_mpqp_to_qasm.py +++ b/tests/qasm/test_mpqp_to_qasm.py @@ -58,6 +58,7 @@ ( [ S(0), + S_dagger(1), X(0), Y(0), Z(0), @@ -79,6 +80,7 @@ qreg q[3]; creg c[3]; s q[0]; +sdg q[1]; x q[0]; y q[0]; z q[0]; @@ -170,6 +172,7 @@ ( [ S(0), + S_dagger(1), X(0), Y(0), Z(0), @@ -189,6 +192,7 @@ include "qelib1.inc"; qreg q[3]; s q[0]; +sdg q[1]; x q[0]; y q[0]; z q[0]; @@ -326,6 +330,7 @@ def test_mpqp_to_qasm_custom_gate(instructions: list[Instruction]): ( [ S(0), + S_dagger(1), X(0), Y(0), Z(0), @@ -347,6 +352,7 @@ def test_mpqp_to_qasm_custom_gate(instructions: list[Instruction]): qreg q[3]; creg c[3]; s q[0]; +sdg q[1]; x q[0]; y q[0]; z q[0];