diff --git a/.gitignore b/.gitignore index 70c9dd85b..0df620dc3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ __pycache__/ .mypy_cache/ .pytest_cache/ .ipynb_checkpoints/ +venv/ # IDE .vscode/ diff --git a/docs/gates.rst b/docs/gates.rst index fbd4d4875..e6c3627de 100644 --- a/docs/gates.rst +++ b/docs/gates.rst @@ -59,3 +59,13 @@ Controlled Custom Gates ----------------------- .. automodule:: mpqp.core.instruction.gates.custom_controlled_gate + +Gate Decomposition +------------------ + +When a target language or device does not support a +:class:`~mpqp.core.instruction.gates.native_gates.ComposedGate` natively, MPQP +recursively resolves it into gates supported by the target gate set. Gates that +are already supported are preserved without decomposition. + +.. automodule:: mpqp.core.instruction.gates.gate_decomposition diff --git a/examples/notebooks/4c_Quantum_Phase_Estimation.ipynb b/examples/notebooks/4c_Quantum_Phase_Estimation.ipynb index 3a59bc46a..01a8ae349 100644 --- a/examples/notebooks/4c_Quantum_Phase_Estimation.ipynb +++ b/examples/notebooks/4c_Quantum_Phase_Estimation.ipynb @@ -26,7 +26,7 @@ " IBMDevice,\n", " run,\n", ")\n", - "from mpqp.qasm.qasm_to_mpqp import qasm2_parse\n", + "from mpqp.translation.qasm.qasm_to_mpqp import qasm2_parse\n", "from mpqp.tools.maths import normalize" ] }, diff --git a/examples/scripts/aws_execution_trials.py b/examples/scripts/aws_execution_trials.py index 735c4b4e5..018e6e966 100644 --- a/examples/scripts/aws_execution_trials.py +++ b/examples/scripts/aws_execution_trials.py @@ -7,7 +7,7 @@ from mpqp.execution.devices import ATOSDevice, AWSDevice from mpqp.gates import * from mpqp.measures import BasisMeasure -from mpqp.qasm.qasm_to_braket import qasm3_to_braket_Circuit +from mpqp.translation.qasm.qasm_to_braket import qasm3_to_braket_Circuit device = LocalSimulator() diff --git a/examples/scripts/open_qasm_conversions.py b/examples/scripts/open_qasm_conversions.py index 92fbadb44..cd9a3d8b5 100644 --- a/examples/scripts/open_qasm_conversions.py +++ b/examples/scripts/open_qasm_conversions.py @@ -1,6 +1,6 @@ """Examples of OpenQASM conversion from 2.0 to 3.0""" -from mpqp.qasm import open_qasm_2_to_3, remove_user_gates +from mpqp.translation.qasm import open_qasm_2_to_3, remove_user_gates print("-------------------------") print("-------------------------") diff --git a/mpqp/__init__.py b/mpqp/__init__.py index fe95468eb..aa3c410f0 100644 --- a/mpqp/__init__.py +++ b/mpqp/__init__.py @@ -37,6 +37,7 @@ CNOT, CP, CZ, + PRX, SWAP, TOF, ControlledGate, @@ -53,8 +54,11 @@ Rk, Rk_dagger, Rx, + Rxx, Ry, + Ryy, Rz, + Rzz, S, S_dagger, T, diff --git a/mpqp/core/circuit.py b/mpqp/core/circuit.py index 6d1827184..ced24dbd2 100644 --- a/mpqp/core/circuit.py +++ b/mpqp/core/circuit.py @@ -47,7 +47,6 @@ from mpqp.core.instruction.gates import ControlledGate, Gate from mpqp.core.instruction.gates.custom_controlled_gate import CustomControlledGate from mpqp.core.instruction.gates.custom_gate import CustomGate -from mpqp.core.instruction.gates.native_gates import NativeGate from mpqp.core.instruction.gates.parametrized_gate import ParametrizedGate from mpqp.core.instruction.measurement import BasisMeasure, Measure from mpqp.core.instruction.measurement.expectation_value import ExpectationMeasure @@ -58,6 +57,7 @@ InstructionParsingError, NonReversibleWarning, NumberQubitsError, + UnsupportedGateError, ) from mpqp.tools.generics import OneOrMany from mpqp.tools.maths import matrix_eq @@ -946,7 +946,7 @@ def initializer(cls, state: npt.NDArray[np.complex128]) -> QCircuit: qiskit_circuit.append( StatePreparation(Statevector(normalize(state))), range(size) ) - circ, phase = replace_custom_gate(qiskit_circuit[0], size, list(range(size))) + circ, phase = replace_custom_gate(qiskit_circuit, size, list(range(size))) cls = QCircuit.from_other_language(circ.reverse_bits()) cls.input_g_phase = phase return cls @@ -1109,7 +1109,6 @@ def to_other_language( language: Literal[Language.QASM2, Language.QASM3], skip_pre_measure: bool = False, skip_measurements: bool = False, - authorized_gates: Optional[set[type[NativeGate]]] = None, printing: bool = False, ) -> str: ... @@ -1119,7 +1118,6 @@ def to_other_language( language: Literal[Language.CIRQ], skip_pre_measure: bool = False, skip_measurements: bool = False, - authorized_gates: Optional[set[type[NativeGate]]] = None, printing: bool = False, ) -> cirq_Circuit: ... @@ -1129,7 +1127,6 @@ def to_other_language( language: Literal[Language.BRAKET], skip_pre_measure: bool = False, skip_measurements: bool = False, - authorized_gates: Optional[set[type[NativeGate]]] = None, printing: bool = False, ) -> braket_Circuit: ... @overload @@ -1138,7 +1135,6 @@ def to_other_language( language: Literal[Language.MY_QLM], skip_pre_measure: bool = False, skip_measurements: bool = False, - authorized_gates: Optional[set[type[NativeGate]]] = None, printing: bool = False, ) -> myQLM_Circuit: ... @@ -1148,7 +1144,6 @@ def to_other_language( language: Literal[Language.QISKIT], skip_pre_measure: bool = False, skip_measurements: bool = False, - authorized_gates: Optional[set[type[NativeGate]]] = None, printing: bool = False, ) -> QuantumCircuit: ... @@ -1158,7 +1153,6 @@ def to_other_language( language: Language, skip_pre_measure: bool = False, skip_measurements: bool = False, - authorized_gates: Optional[set[type[NativeGate]]] = None, printing: bool = False, ) -> QuantumCircuit | myQLM_Circuit | braket_Circuit | cirq_Circuit | str: ... @@ -1167,7 +1161,6 @@ def to_other_language( language: Language = Language.QISKIT, skip_pre_measure: bool = False, skip_measurements: bool = False, - authorized_gates: Optional[set[type[NativeGate]]] = None, printing: bool = False, ) -> QuantumCircuit | myQLM_Circuit | braket_Circuit | cirq_Circuit | str: """Transforms this circuit into the corresponding circuit in the language @@ -1241,26 +1234,17 @@ def to_other_language( circuits. """ - if authorized_gates is None: - authorized_gates = set() self._generated_g_phase = 0 if language == Language.QISKIT: from mpqp.translation.qiskit import mpqp_to_qiskit - return mpqp_to_qiskit( - self, - skip_pre_measure, - skip_measurements, - printing, - authorized_gates=authorized_gates, - ) + return mpqp_to_qiskit(self, skip_pre_measure, skip_measurements, printing) elif language == Language.MY_QLM: qasm2_code = self.to_other_language( Language.QASM2, skip_pre_measure=skip_pre_measure, skip_measurements=True, - authorized_gates=authorized_gates, ) from mpqp.translation.qasm.qasm_to_myqlm import qasm2_to_myqlm_Circuit @@ -1270,16 +1254,12 @@ def to_other_language( elif language == Language.BRAKET: from mpqp.translation.braket import mpqp_to_braket - return mpqp_to_braket( - self, skip_pre_measure, authorized_gates=authorized_gates - ) + return mpqp_to_braket(self, skip_pre_measure) elif language == Language.CIRQ: - from mpqp.translation import mpqp_to_cirq + from mpqp.translation.cirq import mpqp_to_cirq - return mpqp_to_cirq( - self, skip_pre_measure, skip_measurements, authorized_gates - ) + return mpqp_to_cirq(self, skip_pre_measure, skip_measurements) elif language == Language.QASM2: from mpqp.translation.qasm.mpqp_to_qasm import mpqp_to_qasm2 @@ -1415,16 +1395,32 @@ def to_other_device( skip_measurements = False + # Checks if all the gates or its direct decomposition are available on the device. + from copy import deepcopy + + from mpqp.core.instruction.gates.gate_decomposition import ( + resolve_instructions, + ) + + translated_circuit = deepcopy(self) + native_gates = device.compatible_gates() + + if native_gates: + translated_circuit.instructions = resolve_instructions( + translated_circuit.instructions, + native_gates, + ) + unsupported_gates = [ + gate + for gate in translated_circuit.gates + if type(gate) not in native_gates + ] + if unsupported_gates: + raise UnsupportedGateError(unsupported_gates[0], native_gates) + if isinstance(device, (IBMDevice, StaticIBMSimulatedDevice)): if job_type == JobType.STATE_VECTOR: skip_measurements = True - compatible_gates = list(device.compatible_gates()) - if len(compatible_gates) != 0: - if any(type(i) not in compatible_gates for i in self.gates): - raise ValueError( - f"Gates {', '.join(map(str, compatible_gates))} " - f"are the only ones available on {device}." - ) if ( isinstance(device, StaticIBMSimulatedDevice) and device.value().num_qubits < self.nb_qubits @@ -1433,10 +1429,10 @@ def to_other_device( f"Number of qubits of the circuit ({self.nb_qubits}) is higher " f"than the one of the IBMSimulatedDevice ({device.value().num_qubits})." ) - qiskit_circuit = self.to_other_language( - Language.QISKIT, - skip_pre_measure, - skip_measurements, + from mpqp.translation.qiskit import mpqp_to_qiskit + + qiskit_circuit = mpqp_to_qiskit( + translated_circuit, skip_pre_measure, skip_measurements ) if TYPE_CHECKING: assert isinstance(qiskit_circuit, QuantumCircuit) @@ -1612,11 +1608,12 @@ def to_other_device( if job_type == JobType.STATE_VECTOR: skip_measurements = True - aws_circuit = self.to_other_language( + aws_circuit = translated_circuit.to_other_language( Language.BRAKET, skip_pre_measure, skip_measurements, ) + return aws_circuit elif isinstance(device, ATOSDevice): circuit = self.to_other_language( @@ -1736,23 +1733,26 @@ def from_other_language( ) if InstalledProviders.QISKIT in _INSTALLED_MPQP_PROVIDERS: - from mpqp.translation.qiskit import qiskit_to_mpqp from qiskit import QuantumCircuit + from mpqp.translation.qiskit import qiskit_to_mpqp + if isinstance(qcircuit, QuantumCircuit): return qiskit_to_mpqp(qcircuit) if InstalledProviders.CIRQ in _INSTALLED_MPQP_PROVIDERS: - from mpqp.translation import cirq_to_mpqp from cirq.circuits.circuit import Circuit as cirq_Circuit from cirq.circuits.moment import Moment + from mpqp.translation import cirq_to_mpqp + if isinstance(qcircuit, Moment | cirq_Circuit): return cirq_to_mpqp(qcircuit) if InstalledProviders.BRAKET in _INSTALLED_MPQP_PROVIDERS: - from mpqp.translation.braket import braket_to_mpqp from braket.circuits import Circuit as braket_Circuit + from mpqp.translation.braket import braket_to_mpqp + if isinstance(qcircuit, braket_Circuit): return braket_to_mpqp(qcircuit) diff --git a/mpqp/core/instruction/__init__.py b/mpqp/core/instruction/__init__.py index 48ec18805..8a073b506 100644 --- a/mpqp/core/instruction/__init__.py +++ b/mpqp/core/instruction/__init__.py @@ -1,15 +1,15 @@ # pyright: reportUnusedImport=false -from .instruction import Instruction from .barrier import Barrier -from .gates import * from .breakpoint import Breakpoint +from .gates import * +from .instruction import Instruction from .measurement import ( Basis, - ComputationalBasis, - HadamardBasis, - VariableSizeBasis, BasisMeasure, + ComputationalBasis, ExpectationMeasure, - Observable, + HadamardBasis, Measure, + Observable, + VariableSizeBasis, ) diff --git a/mpqp/core/instruction/breakpoint.py b/mpqp/core/instruction/breakpoint.py index 08c9cb3fa..74b01da8f 100644 --- a/mpqp/core/instruction/breakpoint.py +++ b/mpqp/core/instruction/breakpoint.py @@ -6,9 +6,10 @@ truncated up to the breakpoint.""" from __future__ import annotations + from typing import TYPE_CHECKING, Optional -from mpqp.core.instruction import Instruction +from mpqp.core.instruction.instruction import Instruction from mpqp.core.languages import Language if TYPE_CHECKING: diff --git a/mpqp/core/instruction/gates/__init__.py b/mpqp/core/instruction/gates/__init__.py index a9ca3a52f..5d276ef31 100644 --- a/mpqp/core/instruction/gates/__init__.py +++ b/mpqp/core/instruction/gates/__init__.py @@ -1,25 +1,30 @@ # pyright: reportUnusedImport=false from .controlled_gate import ControlledGate -from .custom_gate import CustomGate, UnitaryMatrix from .custom_controlled_gate import CustomControlledGate +from .custom_gate import CustomGate, UnitaryMatrix from .gate import Gate from .gate_definition import GateDefinition from .native_gates import ( CNOT, + CP, CZ, + PRX, SWAP, TOF, + ComposedGate, CRk, CRk_dagger, H, Id, P, - CP, Rk, Rk_dagger, Rx, + Rxx, Ry, + Ryy, Rz, + Rzz, S, S_dagger, T, diff --git a/mpqp/core/instruction/gates/custom_controlled_gate.py b/mpqp/core/instruction/gates/custom_controlled_gate.py index a732711b0..f3cba7bb0 100644 --- a/mpqp/core/instruction/gates/custom_controlled_gate.py +++ b/mpqp/core/instruction/gates/custom_controlled_gate.py @@ -73,10 +73,8 @@ def inverse(self) -> "CustomControlledGate": return CustomControlledGate(self.controls, self.non_controlled_gate.inverse()) def to_custom_gate(self) -> CustomGate: - "returns the CustomGate equivalent of this gate." - import numpy as np - - targets = list(np.sort(self.targets + self.controls)) + "Returns the CustomGate equivalent of this gate." + targets = sorted(self.targets + self.controls) return CustomGate(self.to_matrix(), targets) @@ -84,8 +82,26 @@ def to_other_language( self, language: Language = Language.QISKIT, qiskit_parameters: Optional[set["Parameter"]] = None, + printing: bool = False, ) -> Any: + if isinstance(self.non_controlled_gate, CustomGate): + if language == Language.QISKIT and printing: + from qiskit.circuit import Gate as QiskitGate + + gate = self.non_controlled_gate.to_other_language( + language, + qiskit_parameters, + printing=True, + ) + if not isinstance(gate, QiskitGate): + raise TypeError( + "Expected CustomGate translation to return a Qiskit Gate." + ) + return gate.control(len(self.controls)) + return self.to_custom_gate().to_other_language(language) + if language == Language.QISKIT: + from qiskit.quantum_info import Operator gate = self.non_controlled_gate.to_other_language(Language.QISKIT) @@ -93,13 +109,26 @@ def to_other_language( gate = gate.to_instruction() gate = gate.control(len(self.controls)) return gate - elif language == Language.QASM2: - if isinstance(self.non_controlled_gate, CustomGate): - targets = self.targets + self.controls - targets.sort() - gate = CustomGate(self.to_matrix(), targets) - return gate.to_other_language(Language.QASM2) + elif language == Language.CIRQ: + + from cirq import ControlledGate as cirqControlledGate + + return cirqControlledGate( + sub_gate=self.non_controlled_gate.to_other_language(Language.CIRQ), + num_controls=len(self.controls), + ) + + elif language == Language.BRAKET: + from braket.circuits import Instruction as BraketInstruction + + return BraketInstruction( + operator=self.non_controlled_gate.to_other_language(language).operator, + target=self.targets, + control=self.controls, + ) + + elif language == Language.QASM2: from qiskit import QuantumCircuit, qasm2 diff --git a/mpqp/core/instruction/gates/custom_gate.py b/mpqp/core/instruction/gates/custom_gate.py index 0e43e6c65..7251a0af2 100644 --- a/mpqp/core/instruction/gates/custom_gate.py +++ b/mpqp/core/instruction/gates/custom_gate.py @@ -183,9 +183,7 @@ def to_other_language( self.label, ) - circuit, gphase = replace_custom_gate( - qiskit_circ.data[0], nb_qubits, self.targets - ) + circuit, gphase = replace_custom_gate(qiskit_circ, nb_qubits, self.targets) qasm_str = qasm2.dumps(circuit) qasm_lines = qasm_str.splitlines() @@ -229,10 +227,10 @@ def decompose(self) -> "QCircuit": if any( self.targets[i + 1] < self.targets[i] for i in range(len(self.targets) - 1) ): + import warnings from copy import deepcopy from mpqp.tools import rearrange_matrix - import warnings warnings.warn( "In order to decompose a CustomGate with non ordered targets, the matrix gets copied and ordered according to the targets provided." diff --git a/mpqp/core/instruction/gates/gate.py b/mpqp/core/instruction/gates/gate.py index 2b9fda8d8..3d2dd8cf2 100644 --- a/mpqp/core/instruction/gates/gate.py +++ b/mpqp/core/instruction/gates/gate.py @@ -210,12 +210,17 @@ def to_dict(self) -> dict[str, int | str | list[str] | float | None]: import braket # pyright: ignore[reportUnusedImport] except ImportError: continue + + try: + value = getattr(self, attr_name) + except NotImplementedError: + continue + if ( attr_name not in {'_abc_impl'} and not attr_name.startswith("__") - and not callable(getattr(self, attr_name)) + and not callable(value) ): - value = getattr(self, attr_name) if isinstance(value, np.ndarray): value = value.tolist() result[attr_name] = value diff --git a/mpqp/core/instruction/gates/gate_decomposition.py b/mpqp/core/instruction/gates/gate_decomposition.py new file mode 100644 index 000000000..fbaf57360 --- /dev/null +++ b/mpqp/core/instruction/gates/gate_decomposition.py @@ -0,0 +1,173 @@ +from dataclasses import dataclass + +from mpqp.core.instruction.gates.custom_controlled_gate import CustomControlledGate +from mpqp.core.instruction.gates.custom_gate import CustomGate +from mpqp.core.instruction.gates.gate import Gate +from mpqp.core.instruction.gates.native_gates import ComposedGate +from mpqp.core.instruction.instruction import Instruction +from mpqp.tools.errors import UnsupportedGateError + + +@dataclass(frozen=True) +class GateResolution: + """ + Result of resolving a gate against a target gate set. + + Args: + source: Original gate passed to the resolution process. + gates: Gates resulting from the resolution. This contains either the + original gate when it is directly supported, or its resolved + decomposition. + decomposed: Whether the source gate was decomposed during the resolution. + """ + + source: Gate + gates: tuple[Gate, ...] + decomposed: bool + + +def resolve_instructions( + instructions: list[Instruction], + gate_set: set[type[Gate]], +) -> list[Instruction]: + """ + Resolve the composed gates contained in a sequence of instructions. + + Gates are processed using the function :func:`resolve_gate`. Instructions that + are not gates are preserved unchanged and in their original order. + + Args: + instructions: Instructions to resolve. + gate_set: Gate types directly supported by the target language or + provider. + + Returns: + A list containing the resolved gates and the unchanged non-gate + instructions. + + Example: + >>> from mpqp.gates import CNOT, Rz, Rzz + >>> resolved = resolve_instructions([Rzz(1.0, 0, 1)], {CNOT, Rz}) + >>> [type(gate).__name__ for gate in resolved] + ['CNOT', 'Rz', 'CNOT'] + """ + + resolved: list[Instruction] = [] + + for instruction in instructions: + if isinstance(instruction, Gate): + resolved.extend(resolve_gate(instruction, gate_set)) + else: + resolved.append(instruction) + + return resolved + + +def resolve_composed_gate( + gate: Gate, + gate_set: set[type[Gate]], + resolving: frozenset[type[Gate]] = frozenset(), +) -> GateResolution: + """ + Resolve a gate recursively against a target gate set. + + If the gate type is directly supported, the gate is returned unchanged. + Otherwise, the gate is recursively decomposed until all resulting gates + belong to ``gate_set``. + + Args: + gate: Gate to resolve. + gate_set: Gate types directly supported by the target language or + provider. + resolving: Gate types currently being resolved. This internal state is + used to detect cyclic decompositions. + + Returns: + A :class:`GateResolution` containing the original gate, the resolved + gates and whether a decomposition occurred. + + Raises: + UnsupportedGateError: If the gate, or one of the gates produced by its + decomposition, cannot be represented using ``gate_set``. + ValueError: If the decomposition is empty, directly contains its source + gate, or contains a cycle. + + Example: + >>> from mpqp.gates import PRX, Rx, Rz + >>> result = resolve_composed_gate(PRX(1.0, 0.5, 0), {Rx, Rz}) + >>> [type(gate).__name__ for gate in result.gates] + ['Rz', 'Rx', 'Rz'] + """ + gate_type = type(gate) + + if gate_type in gate_set: + return GateResolution(gate, (gate,), decomposed=False) + + if not isinstance(gate, ComposedGate): + raise UnsupportedGateError(gate, gate_set) + + if gate_type in resolving: + raise ValueError(f"Cyclic decomposition detected for {gate_type.__name__}.") + + decomposition = gate.decompose() + + if not decomposition or any(child is gate for child in decomposition): + raise ValueError(f"{gate_type.__name__} returned an invalid decomposition.") + + resolved: list[Gate] = [] + missing: set[type[Gate]] = set() + + for child in decomposition: + if type(child) in gate_set: + resolved.append(child) + elif isinstance(child, ComposedGate): + result = resolve_composed_gate( + child, + gate_set, + resolving | {gate_type}, + ) + resolved.extend(result.gates) + else: + missing.add(type(child)) + + if missing: + raise UnsupportedGateError(gate, gate_set, missing) + + return GateResolution(gate, tuple(resolved), decomposed=True) + + +def resolve_gate( + gate: Gate, + gate_set: set[type[Gate]], +) -> tuple[Gate, ...]: + """Resolve a gate into gates supported by a target gate set. + + A custom controlled gate wrapping a custom gate is first converted into an + equivalent custom gate. Non-composed gates are returned unchanged. Composed + gates are resolved recursively using :func:`resolve_composed_gate`. + + Args: + gate: Gate to resolve. + gate_set: Gate types directly supported by the target language or + provider. + + Returns: + A tuple containing the original gate, its converted custom gate, or the + gates resulting from its recursive decomposition. + + Example: + >>> from mpqp.gates import CNOT, Rz, Rzz + >>> resolved = resolve_gate(Rzz(1.0, 0, 1), {CNOT, Rz}) + >>> [type(gate).__name__ for gate in resolved] + ['CNOT', 'Rz', 'CNOT'] + """ + if isinstance(gate, CustomControlledGate) and isinstance( + gate.non_controlled_gate, + CustomGate, + ): + return (gate.to_custom_gate(),) + + if not isinstance(gate, ComposedGate): + return (gate,) + + return resolve_composed_gate(gate, gate_set).gates diff --git a/mpqp/core/instruction/gates/native_gates.py b/mpqp/core/instruction/gates/native_gates.py index 7ea32428f..760e0dca0 100644 --- a/mpqp/core/instruction/gates/native_gates.py +++ b/mpqp/core/instruction/gates/native_gates.py @@ -33,7 +33,14 @@ from mpqp.core.instruction.gates.parametrized_gate import ParametrizedGate from mpqp.core.languages import Language from mpqp.tools.generics import Matrix, SimpleClassReprABC, classproperty -from mpqp.tools.maths import cos, exp, sin +from mpqp.tools.maths import ( + cos, + exp, + rotation_denominator, + sin, + symbolic_divide, + symbolic_product, +) # pylance doesn't handle well Expr, so a lot of "type:ignore" will happen in # this file :/ @@ -86,15 +93,15 @@ def _qiskit_parameter_adder( def _sympy_to_braket_param(val: Expr | float) -> "float | FreeParameter": - from sympy import Expr from braket.circuits import FreeParameter + from sympy import Expr if isinstance(val, Expr): if val.free_symbols: return FreeParameter(str(val)) # note: Braket won't parse expressions else: try: - return float(val.evalf()) + return float(val.evalf()) # pyright: ignore[reportArgumentType] except Exception as e: raise ValueError(f"Failed to evaluate sympy expression '{val}': {e}") else: @@ -170,7 +177,7 @@ def qiskit_gate( | PhaseGate | CPhaseGate ]: - pass + raise NotImplementedError @classproperty @abstractmethod @@ -194,7 +201,7 @@ def braket_gate( | gates.PhaseShift | gates.CPhaseShift ]: - pass + raise NotImplementedError @classproperty @abstractmethod @@ -202,7 +209,7 @@ def cirq_gate( cls, ) -> type[Gate]: """Returns the corresponding ``cirq`` class for this gate.""" - pass + raise NotImplementedError class RotationGate(NativeGate, ParametrizedGate, SimpleClassReprABC): @@ -217,11 +224,17 @@ class RotationGate(NativeGate, ParametrizedGate, SimpleClassReprABC): target: Index referring to the qubits on which the gate will be applied. """ - def __init__(self, theta: Expr | float, target: int): - self.parameters = [theta] + def __init__( + self, theta: list[Expr | float] | Expr | float, targets: list[int] | int + ): + if not isinstance(theta, list): + theta = [theta] + self.parameters = theta definition = UnitaryMatrix(self.to_canonical_matrix()) + if isinstance(targets, int): + targets = [targets] ParametrizedGate.__init__( - self, definition, [target], [self.theta], type(self).__name__.capitalize() + self, definition, targets, self.parameters, type(self).__name__.capitalize() ) @property @@ -229,7 +242,7 @@ def theta(self): """Rotation angle (in radians).""" return self.parameters[0] - def __repr__(self): + def __repr__(self) -> str: return f"{type(self).__name__}({self.theta}, {self.targets[0]})" def to_other_language( @@ -247,9 +260,10 @@ def to_other_language( qiskit_parameters = set() return self.qiskit_gate(_qiskit_parameter_adder(theta, qiskit_parameters)) elif language == Language.BRAKET: - from braket.circuits import Instruction from copy import deepcopy + from braket.circuits import Instruction + connection = deepcopy(self.targets) if isinstance(self, ControlledGate): connection += self.controls @@ -327,7 +341,7 @@ def qiskit_gate( | IGate ]: """Returns the corresponding ``qiskit`` class for this gate.""" - pass + raise NotImplementedError @classproperty @abstractmethod @@ -347,7 +361,7 @@ def braket_gate( | gates.I ]: """Returns the corresponding ``braket`` class for this gate.""" - pass + raise NotImplementedError """Corresponding ``qiskit``'s gate class.""" matrix: npt.NDArray[np.complex128] @@ -361,12 +375,14 @@ def to_other_language( if language == Language.QISKIT: return self.qiskit_gate() elif language == Language.BRAKET: - from braket.circuits import Instruction from copy import deepcopy + from braket.circuits import Instruction + connection = deepcopy(self.targets) if isinstance(self, ControlledGate): connection += self.controls + return Instruction(operator=self.braket_gate(), target=connection) elif language == Language.CIRQ: return self.cirq_gate @@ -400,6 +416,18 @@ def __init__(self, target: int, label: Optional[str] = None): ) +class ComposedGate(NativeGate, SimpleClassReprABC): + """Class describing gates that are composed of simpler native gates.""" + + def __init__(self, targets: list[int], label: Optional[str] = None): + NativeGate.__init__(self, targets, label) + + @abstractmethod + def decompose(self) -> list[Gate]: + """Method used to return the decomposed version of a ComposedGate.""" + raise NotImplementedError + + class Id(OneQubitNoParamGate, InvolutionGate): r"""One qubit identity gate. @@ -658,9 +686,7 @@ def qiskit_gate(cls): @classproperty def cirq_gate(cls): - from cirq.ops.common_gates import ZPowGate - - return lambda theta: ZPowGate(exponent=theta / np.pi) + raise NotImplementedError qlm_aqasm_keyword = "PH" qiskit_string = "p" @@ -668,13 +694,27 @@ def cirq_gate(cls): def __init__(self, theta: Expr | float, target: int): super().__init__(theta, target) + def to_other_language( + self, + language: Language = Language.QISKIT, + qiskit_parameters: Optional[set["Parameter"]] = None, + ): + if language == Language.CIRQ: + from cirq import MatrixGate + + return MatrixGate( + matrix=self.to_matrix(), name=self.label, unitary_check=False + ) + else: + return super().to_other_language(language, qiskit_parameters) + def to_canonical_matrix(self) -> Matrix: return np.array( [ [1, 0], [ 0, - exp(self.parameters[0] * 1j), + exp(symbolic_product(self.parameters[0], 1j)), ], ] ) @@ -714,7 +754,9 @@ def cirq_gate(cls): from cirq.ops.common_gates import ZPowGate from cirq.ops.controlled_gate import ControlledGate as CirqControlledGate - return lambda theta: CirqControlledGate(ZPowGate(exponent=theta / np.pi)) + return lambda theta: CirqControlledGate( + ZPowGate(exponent=symbolic_divide(theta, np.pi)) + ) # TODO: this is a special case, see if it needs to be generalized qlm_aqasm_keyword = "CNOT;PH" @@ -727,7 +769,7 @@ def __init__(self, theta: Expr | float, control: int, target: int): ParametrizedGate.__init__(self, definition, [target], [theta], "CP") def to_canonical_matrix(self): - e = exp(self.theta * 1j) + e = exp(symbolic_product(self.theta, 1j)) return np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, e]]) def __repr__(self) -> str: @@ -879,7 +921,9 @@ def __init__(self, target: int): def to_canonical_matrix(self): from sympy import pi - return np.array([[1, 0], [0, exp((pi / 4) * 1j)]]) + return np.array( + [[1, 0], [0, exp(symbolic_product(symbolic_divide(pi, 4), 1j))]] + ) class SWAP(InvolutionGate, NoParameterGate): @@ -1041,7 +1085,13 @@ def __repr__(self) -> str: def _decompose_(self, qubits: tuple[Qid, ...]): q = qubits[0] return [ - GlobalPhaseGate(np.exp(1j * (self.lmda + self.phi) / 2)).on(), + GlobalPhaseGate( + exp( + symbolic_product( + 1j, symbolic_divide(self.lmda + self.phi, 2) + ) + ) + ).on(), cirq_rz(self.lmda).on(q), cirq_ry(self.theta).on(q), cirq_rz(self.phi).on(q), @@ -1128,15 +1178,15 @@ def to_other_language( def to_canonical_matrix(self): c, s, eg, ep = ( - cos(self.theta / 2), - sin(self.theta / 2), - exp(self.gamma * 1j), - exp(self.phi * 1j), + cos(symbolic_divide(self.theta, 2)), + sin(symbolic_divide(self.theta, 2)), + exp(symbolic_product(self.gamma, 1j)), + exp(symbolic_product(self.phi, 1j)), ) return np.array( [ - [c, -eg * s], - [ep * s, eg * ep * c], + [c, -eg * s], # pyright: ignore[reportOperatorIssue] + [ep * s, eg * ep * c], # pyright: ignore[reportOperatorIssue] ] ) @@ -1185,9 +1235,10 @@ def __init__(self, theta: Expr | float, target: int): super().__init__(theta, target) def to_canonical_matrix(self): - c = cos(self.parameters[0] / 2) - s = sin(self.parameters[0] / 2) - return np.array([[c, -1j * s], [-1j * s, c]]) + c = cos(symbolic_divide(self.parameters[0], 2)) + s = sin(symbolic_divide(self.parameters[0], 2)) + imaginary_sine = symbolic_product(-1j, s) + return np.array([[c, imaginary_sine], [imaginary_sine, c]]) class Ry(RotationGate, SingleQubitGate): @@ -1231,8 +1282,8 @@ def __init__(self, theta: Expr | float, target: int): super().__init__(theta, target) def to_canonical_matrix(self): - c = cos(self.parameters[0] / 2) - s = sin(self.parameters[0] / 2) + c = cos(symbolic_divide(self.parameters[0], 2)) + s = sin(symbolic_divide(self.parameters[0], 2)) return np.array([[c, -s], [s, c]]) @@ -1277,8 +1328,442 @@ def __init__(self, theta: Expr | float, target: int): super().__init__(theta, target) def to_canonical_matrix(self): - e = exp(-1j * self.parameters[0] / 2) - return np.array([[e, 0], [0, 1 / e]]) + e = exp(symbolic_product(-0.5j, self.parameters[0])) + return np.array([[e, 0], [0, 1 / e]]) # pyright: ignore[reportOperatorIssue] + + +class Rxx(RotationGate, ComposedGate): + r"""Two-qubit XX rotation gate. + + Rxx(φ) = `e^{-iφ X ⊗ X / 2}` + + Equivalent to a rotation of angle φ generated by the X ⊗ X interaction + betweeen the two qubits. + + `\begin{pmatrix} + \cos(\phi/2)&0&0&-i\sin(\phi/2)\\ + 0&\cos(\phi/2)&-i\sin(\phi/2)&0\\ + 0&-i\sin(\phi/2)&\cos(\phi/2)&0\\ + -i\sin(\phi/2)&0&0&\cos(\phi/2) + \end{pmatrix}` + + Args: + phi: Rotation angle. + a: Index of the first qubit. + b: Index of the second qubit. + + Example: + >>> pprint(Rxx(np.pi, 0, 1).to_matrix()) + [[0 , 0 , 0 , -1j], + [0 , 0 , -1j, 0 ], + [0 , -1j, 0 , 0 ], + [-1j, 0 , 0 , 0 ]] + + """ + + @classproperty + def braket_gate(cls): + from braket.circuits import gates + + return gates.XX + + @classproperty + def qiskit_gate(cls): + from qiskit.circuit.library import RXXGate + + return RXXGate + + @classproperty + def cirq_gate(cls): + from cirq import XXPowGate + + return XXPowGate + + qlm_aqasm_keyword = "RXX" + qiskit_string = "rxx" + nb_qubits = ( # pyright: ignore[reportAssignmentType,reportIncompatibleMethodOverride] + 2 + ) + + def __init__(self, phi: Expr | float, a: int, b: int): + super().__init__(phi, [a, b]) + + def __repr__(self): + return ( + f"{type(self).__name__}({self.theta}, {self.targets[0]}, {self.targets[1]})" + ) + + def to_canonical_matrix(self): + phi = self.parameters[0] + c = cos(symbolic_divide(phi, 2)) + s = sin(symbolic_divide(phi, 2)) + + return np.array( + [ + [c, 0, 0, symbolic_product(-1j, s)], + [0, c, symbolic_product(-1j, s), 0], + [0, symbolic_product(-1j, s), c, 0], + [symbolic_product(-1j, s), 0, 0, c], + ], + ) + + def decompose(self) -> list[Gate]: + return [ + CNOT(self.targets[0], self.targets[1]), + Rx(self.parameters[0], self.targets[0]), + CNOT(self.targets[0], self.targets[1]), + ] + + def inverse(self) -> Gate: + return self.__class__(-self.parameters[0], self.targets[0], self.targets[1]) + + def to_other_language( + self, + language: Language = Language.QISKIT, + qiskit_parameters: Optional[set["Parameter"]] | None = None, + ): + if language == Language.CIRQ: + import numpy as np + + return self.cirq_gate( + exponent=symbolic_divide(self.parameters[0], np.pi), global_shift=-0.5 + ) + return super().to_other_language(language, qiskit_parameters) + + +class Ryy(RotationGate, ComposedGate): + r"""Two-qubit YY rotation gate. + + Ryy(φ) = `e^{-iφ Y ⊗ Y / 2}` + + Equivalent to a rotation of angle φ generated by the Y ⊗ Y interaction + betweeen the two qubits. + + `\begin{pmatrix} + \cos(\phi/2)&0&0&i\sin(\phi/2)\\ + 0&\cos(\phi/2)&-i\sin(\phi/2)&0\\ + 0&-i\sin(\phi/2)&\cos(\phi/2)&0\\ + i\sin(\phi/2)&0&0&\cos(\phi/2) + \end{pmatrix}` + + Args: + phi: Rotation angle. + a: Index of the first qubit. + b: Index of the second qubit. + + Example: + >>> pprint(Ryy(np.pi, 0, 1).to_matrix()) + [[0 , 0 , 0 , 1j], + [0 , 0 , -1j, 0 ], + [0 , -1j, 0 , 0 ], + [1j, 0 , 0 , 0 ]] + + """ + + @classproperty + def braket_gate(cls): + from braket.circuits import gates + + return gates.YY + + @classproperty + def qiskit_gate(cls): + from qiskit.circuit.library import RYYGate + + return RYYGate + + @classproperty + def cirq_gate(cls): + from cirq import YYPowGate + + return YYPowGate + + qlm_aqasm_keyword = "RYY" + qiskit_string = "ryy" + nb_qubits = ( # pyright: ignore[reportAssignmentType,reportIncompatibleMethodOverride] + 2 + ) + + def __init__(self, phi: Expr | float, a: int, b: int): + super().__init__(phi, [a, b]) + + def __repr__(self): + return ( + f"{type(self).__name__}({self.theta}, {self.targets[0]}, {self.targets[1]})" + ) + + def to_canonical_matrix(self): + phi = self.parameters[0] + c = cos(symbolic_divide(phi, 2)) + s = sin(symbolic_divide(phi, 2)) + + return np.array( + [ + [c, 0, 0, symbolic_product(1j, s)], + [0, c, symbolic_product(-1j, s), 0], + [0, symbolic_product(-1j, s), c, 0], + [symbolic_product(1j, s), 0, 0, c], + ], + ) + + def decompose(self) -> list[Gate]: + return [ + Rx(np.pi / 2, self.targets[0]), + Rx(np.pi / 2, self.targets[1]), + CNOT(self.targets[0], self.targets[1]), + Rz(self.parameters[0], self.targets[1]), + CNOT(self.targets[0], self.targets[1]), + Rx(-np.pi / 2, self.targets[0]), + Rx(-np.pi / 2, self.targets[1]), + ] + + def inverse(self) -> Gate: + return self.__class__(-self.parameters[0], self.targets[0], self.targets[1]) + + def to_other_language( + self, + language: Language = Language.QISKIT, + qiskit_parameters: Optional[set["Parameter"]] | None = None, + ): + if language == Language.CIRQ: + + return self.cirq_gate( + exponent=symbolic_divide(self.parameters[0], np.pi), global_shift=-0.5 + ) + return super().to_other_language(language, qiskit_parameters) + + +class Rzz(RotationGate, ComposedGate): + r"""Two-qubit ZZ rotation gate. + + Rzz(φ) = `e^{-iφ Z ⊗ Z / 2}` + + Equivalent to a rotation of angle φ generated by the Z ⊗ Z interaction + between the two qubits. + + `\begin{pmatrix} + e^{-i\phi/2} & 0 & 0 & 0 \\ + 0 & e^{i\phi/2} & 0 & 0 \\ + 0 & 0 & e^{i\phi/2} & 0 \\ + 0 & 0 & 0 & e^{-i\phi/2} + \end{pmatrix}` + + Args: + phi: Rotation angle. + a: Index of the first qubit. + b: Index of the second qubit. + + Example: + >>> pprint(Rzz(-np.pi, 0, 1).to_matrix()) + [[1j, 0 , 0 , 0 ], + [0 , -1j, 0 , 0 ], + [0 , 0 , -1j, 0 ], + [0 , 0 , 0 , 1j]] + + """ + + @classproperty + def braket_gate(cls): + from braket.circuits import gates + + return gates.ZZ + + @classproperty + def qiskit_gate(cls): + from qiskit.circuit.library import RZZGate + + return RZZGate + + @classproperty + def cirq_gate(cls): + from cirq import ZZPowGate + + return ZZPowGate + + qlm_aqasm_keyword = "RZZ" + qiskit_string = "rzz" + nb_qubits = ( # pyright: ignore[reportAssignmentType,reportIncompatibleMethodOverride] + 2 + ) + + def __init__(self, phi: Expr | float, a: int, b: int): + super().__init__(phi, [a, b]) + + def __repr__(self): + return ( + f"{type(self).__name__}({self.theta}, {self.targets[0]}, {self.targets[1]})" + ) + + def to_canonical_matrix(self): + phi = self.parameters[0] + e_minus = exp(symbolic_product(-0.5j, phi)) + e_plus = exp(symbolic_product(0.5j, phi)) + + return np.array( + [ + [e_minus, 0, 0, 0], + [0, e_plus, 0, 0], + [0, 0, e_plus, 0], + [0, 0, 0, e_minus], + ], + ) + + def decompose(self) -> list[Gate]: + return [ + CNOT(self.targets[0], self.targets[1]), + Rz(self.parameters[0], self.targets[1]), + CNOT(self.targets[0], self.targets[1]), + ] + + def inverse(self) -> Gate: + return self.__class__(-self.parameters[0], self.targets[0], self.targets[1]) + + def to_other_language( + self, + language: Language = Language.QISKIT, + qiskit_parameters: Optional[set["Parameter"]] | None = None, + ): + if language == Language.CIRQ: + import numpy as np + + return self.cirq_gate( + exponent=symbolic_divide(self.parameters[0], np.pi), global_shift=-0.5 + ) + + return super().to_other_language(language, qiskit_parameters) + + +class PRX(RotationGate, SingleQubitGate, ComposedGate): + r"""Parametrized rotated-X gate. + + PRX(θ, φ) = Rz(φ) Rx(θ) Rz(-φ) + + Equivalent to a rotation of angle θ around the axis + (cos φ, sin φ, 0) in the XY plane. + + `\begin{pmatrix} + \cos(θ/2)&-i e^{-iφ}\sin(θ/2)\\ + -i e^{iφ}\sin(θ/2)&\cos(θ/2) + \end{pmatrix}` + + Args: + theta: Rotation angle. + phi: Axis angle in the XY plane. + target: Target qubit. + + Example: + >>> pprint(PRX(np.pi, 0, 0).to_matrix()) + [[0 , -1j], + [-1j, 0 ]] + + """ + + qlm_aqasm_keyword = "PRX" + qiskit_string = "prx" + + @classproperty + def qiskit_gate(cls): + from qiskit.circuit.library import RGate + + return RGate + + @classproperty + def cirq_gate(cls): + from cirq import PhasedXPowGate + + return PhasedXPowGate + + @classproperty + def braket_gate(cls): + from braket.circuits import gates + + return gates.PRx + + def __init__(self, theta: Expr | float, phi: Expr | float, target: int): + self.targets = [target] + super().__init__([theta, phi], self.targets) + + def to_canonical_matrix(self): + theta, phi = self.parameters + c = cos(symbolic_divide(theta, 2)) + s = sin(symbolic_divide(theta, 2)) + e_minus = exp(symbolic_product(-1j, phi)) + e_plus = exp(symbolic_product(1j, phi)) + + return np.array( + [ + [c, symbolic_product(-1j, e_minus, s)], + [symbolic_product(-1j, e_plus, s), c], + ], + ) + + def to_matrix(self, desired_gate_size: int = 0): + return super().to_matrix(desired_gate_size) + + def decompose(self) -> list[Gate]: + return [ + Rz(-self.parameters[1], self.targets[0]), + Rx(self.parameters[0], self.targets[0]), + Rz(self.parameters[1], self.targets[0]), + ] + + def inverse(self) -> Gate: + return self.__class__(-self.parameters[0], self.parameters[1], self.targets[0]) + + def __repr__(self): + return f"PRX({self.parameters[0]}, {self.parameters[1]}, {self.targets[0]})" + + def to_other_language( + self, + language: Language = Language.QISKIT, + qiskit_parameters: Optional[set["Parameter"]] = None, + ): + + theta, phi = self.parameters[0], self.parameters[1] + try: + theta = float(theta) + except: + pass + try: + phi = float(phi) + except: + pass + + if language == Language.QISKIT: + if qiskit_parameters is None: + qiskit_parameters = set() + + return self.qiskit_gate( + _qiskit_parameter_adder(theta, qiskit_parameters), + _qiskit_parameter_adder(phi, qiskit_parameters), + ) + elif language == Language.BRAKET: + from braket.circuits import Instruction + + connection = self.targets + if isinstance(self, ControlledGate): + connection += self.controls + return Instruction( + operator=self.braket_gate( + _sympy_to_braket_param(theta), _sympy_to_braket_param(phi) + ), + target=connection, + ) + elif language == Language.CIRQ: + return self.cirq_gate( + phase_exponent=symbolic_divide(self.parameters[1], np.pi), + exponent=symbolic_divide(self.parameters[0], np.pi), + ) + elif language == Language.QASM2: + target = self.targets[0] + + return ( + f"rz({-self.parameters[1]}) q[{target}];\n" + f"rx({self.parameters[0]}) q[{target}];\n" + f"rz({self.parameters[1]}) q[{target}];" + ) + else: + raise NotImplementedError(f"Error: {language} is not supported") class Rk(RotationGate, SingleQubitGate): @@ -1317,7 +1802,7 @@ def qiskit_gate(cls): def cirq_gate(cls): from cirq.ops.common_gates import ZPowGate - return lambda theta: ZPowGate(exponent=theta / np.pi) + return lambda theta: ZPowGate(exponent=symbolic_divide(theta, np.pi)) qlm_aqasm_keyword = "PH" qiskit_string = "p" @@ -1334,7 +1819,7 @@ def theta(self) -> Expr | float: from sympy import pi p = np.pi if isinstance(self.k, Integral) else pi - return p / 2 ** (self.k - 1) + return symbolic_divide(p, rotation_denominator(self.k)) @property def k(self) -> Expr | int: @@ -1342,7 +1827,7 @@ def k(self) -> Expr | int: return self.parameters[0] def to_canonical_matrix(self): - e = exp(self.theta * 1j) + e = exp(symbolic_product(self.theta, 1j)) return np.array([[1, 0], [0, e]]) def __repr__(self): @@ -1401,9 +1886,7 @@ def qiskit_gate(cls): @classproperty def cirq_gate(cls): - from cirq.ops.common_gates import ZPowGate - - return lambda theta: ZPowGate(exponent=theta / np.pi) + raise NotImplementedError qlm_aqasm_keyword = "PH" qiskit_string = "p" @@ -1421,7 +1904,7 @@ def theta(self) -> Expr | float: # TODO study the relevance of having pi from sympy p = np.pi if isinstance(self.k, Integral) else pi - return -(p / 2 ** (self.k - 1)) + return -symbolic_divide(p, rotation_denominator(self.k)) @property def k(self) -> Expr | float: @@ -1429,7 +1912,7 @@ def k(self) -> Expr | float: return self.parameters[0] def to_canonical_matrix(self): - e = exp(self.theta * 1j) + e = exp(symbolic_product(self.theta, 1j)) return np.array([[1, 0], [0, e]]) def to_other_language( @@ -1437,7 +1920,13 @@ def to_other_language( language: Language = Language.QISKIT, qiskit_parameters: Optional[set["Parameter"]] = None, ): - if language == Language.QASM2: + if language == Language.CIRQ: + from cirq import MatrixGate + + return MatrixGate( + matrix=self.to_matrix(), name=self.label, unitary_check=False + ) + elif language == Language.QASM2: from mpqp.translation.qasm.mpqp_to_qasm import float_to_qasm_str instruction_str = self.qasm2_gate @@ -1606,7 +2095,9 @@ def cirq_gate(cls): from cirq.ops.common_gates import ZPowGate from cirq.ops.controlled_gate import ControlledGate as CirqControlledGate - return lambda theta: CirqControlledGate(ZPowGate(exponent=theta / np.pi)) + return lambda theta: CirqControlledGate( + ZPowGate(exponent=symbolic_divide(theta, np.pi)) + ) # TODO: this is a special case, see if it needs to be generalized qlm_aqasm_keyword = "CNOT;PH" @@ -1625,7 +2116,7 @@ def theta(self) -> Expr | float: from sympy import pi p = np.pi if isinstance(self.k, Integral) else pi - return p / 2 ** (self.k - 1) + return symbolic_divide(p, rotation_denominator(self.k)) @property def k(self) -> Expr | float: @@ -1633,7 +2124,7 @@ def k(self) -> Expr | float: return self.parameters[0] def to_canonical_matrix(self): - e = exp(self.theta * 1j) + e = exp(symbolic_product(self.theta, 1j)) return np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, e]]) def to_other_language( @@ -1704,7 +2195,9 @@ def cirq_gate(cls): from cirq.ops.common_gates import ZPowGate from cirq.ops.controlled_gate import ControlledGate as CirqControlledGate - return lambda theta: CirqControlledGate(ZPowGate(exponent=theta / np.pi)) + return lambda theta: CirqControlledGate( + ZPowGate(exponent=symbolic_divide(theta, np.pi)) + ) # TODO: this is a special case, see if it needs to be generalized qlm_aqasm_keyword = "CNOT;PH" @@ -1723,7 +2216,7 @@ def theta(self) -> Expr | float: from sympy import pi p = np.pi if isinstance(self.k, Integral) else pi - return -(p / 2 ** (self.k - 1)) + return -symbolic_divide(p, rotation_denominator(self.k)) @property def k(self) -> Expr | int: @@ -1731,7 +2224,7 @@ def k(self) -> Expr | int: return self.parameters[0] def to_canonical_matrix(self): - e = exp(self.theta * 1j) + e = exp(symbolic_product(self.theta, 1j)) return np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, e]]) def __repr__(self) -> str: diff --git a/mpqp/core/instruction/gates/parametrized_gate.py b/mpqp/core/instruction/gates/parametrized_gate.py index 4961019e5..fff8eaf6c 100644 --- a/mpqp/core/instruction/gates/parametrized_gate.py +++ b/mpqp/core/instruction/gates/parametrized_gate.py @@ -53,6 +53,13 @@ def __init__( self.parameters = parameters """See parameter description.""" + from sympy import Expr + + self.symbols = [] + for param in parameters: + if isinstance(param, Expr): + self.symbols.extend(param.free_symbols) + def subs(self, values: dict[Expr | str, Complex]) -> ParametrizedGate: from sympy import Expr diff --git a/mpqp/core/instruction/measurement/expectation_value.py b/mpqp/core/instruction/measurement/expectation_value.py index 39cd5e216..d1c5c04d1 100644 --- a/mpqp/core/instruction/measurement/expectation_value.py +++ b/mpqp/core/instruction/measurement/expectation_value.py @@ -8,11 +8,14 @@ class to define your observable, and a :class:`ExpectationMeasure` to perform import copy from numbers import Real from typing import TYPE_CHECKING, Literal, Optional, Union, overload +from warnings import warn import numpy as np import numpy.typing as npt from typing_extensions import Never +from mpqp.core.instruction.gates.gate import Gate +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 ( CommutingTypes, @@ -22,6 +25,7 @@ class to define your observable, and a :class:`ExpectationMeasure` to perform ) 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_diagonal, is_hermitian, is_power_of_two @@ -465,6 +469,7 @@ def __init__( if targets is None: self.targets = list(range(self.observables[0].nb_qubits)) + self._check_targets_order() @property def nb_observables(self) -> int: @@ -474,6 +479,56 @@ def nb_observables(self) -> int: def observables_labels(self) -> list[str]: return [o.label for o in self.observables if o.label is not None] + def _check_targets_order(self): + """Ensures target qubits are ordered and contiguous, rearranging them if + necessary (private).""" + + if len(self.targets) == 0: + self._pre_measure: list[Gate] = [] + return + + if self.nb_qubits != self.observables[0].nb_qubits: + raise NumberQubitsError( + f"Target size {self.nb_qubits} doesn't match observable size " + f"{self.observables[0].nb_qubits}." + ) + + self._pre_measure: list[Gate] = [] + """List of Gates added before the expectation measurement to correctly swap + target qubits when their are not ordered or contiguous.""" + targets_is_ordered = all( + [self.targets[i] > self.targets[i - 1] for i in range(1, len(self.targets))] + ) + tweaked_tgt = copy.copy(self.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 CNOT/SWAP gates." + ) + + 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.append(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.append(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.""" + + @property + def pre_measure(self) -> list[Gate]: + return self._pre_measure + def get_pauli_grouping(self) -> list[list[PauliStringMonomial]]: """Return the grouped monomials of the Pauli string of the observable. The grouping is done according to the grouping method of the expectation diff --git a/mpqp/core/instruction/measurement/measure.py b/mpqp/core/instruction/measurement/measure.py index 8594e14cd..305bfa2a2 100644 --- a/mpqp/core/instruction/measurement/measure.py +++ b/mpqp/core/instruction/measurement/measure.py @@ -13,7 +13,7 @@ from abc import ABC from typing import Optional -from mpqp.core.instruction import Instruction +from mpqp.core.instruction.instruction import Instruction from mpqp.core.instruction.gates import Gate diff --git a/mpqp/core/instruction/measurement/pauli_string.py b/mpqp/core/instruction/measurement/pauli_string.py index 94a40c4cb..9e0c8ec7e 100644 --- a/mpqp/core/instruction/measurement/pauli_string.py +++ b/mpqp/core/instruction/measurement/pauli_string.py @@ -731,9 +731,11 @@ def from_other_language( # TODO: better typing using overloads "Cannot parse non-homogeneous types when `pauli` is a `list`." ) from mpqp.environment.var_cache import ( - InstalledProviders, _INSTALLED_MPQP_PROVIDERS, # pyright: ignore[reportPrivateUsage] ) + from mpqp.environment.var_cache import ( + InstalledProviders, + ) if InstalledProviders.QISKIT in _INSTALLED_MPQP_PROVIDERS: from qiskit.quantum_info import SparsePauliOp diff --git a/mpqp/execution/__init__.py b/mpqp/execution/__init__.py index 561e0cae4..81440650e 100644 --- a/mpqp/execution/__init__.py +++ b/mpqp/execution/__init__.py @@ -3,14 +3,14 @@ ATOSDevice, AvailableDevice, AWSDevice, + AZUREDevice, GOOGLEDevice, IBMDevice, - AZUREDevice, ) -from .simulated_devices import IBMSimulatedDevice from .job import Job, JobStatus, JobType from .result import BatchResult, Result, Sample, StateVector from .runner import adjust_measure, run, submit +from .simulated_devices import IBMSimulatedDevice # This import has to be done after the loading of result to work, `pass` is a # trick to avoid isort to move this line above diff --git a/mpqp/execution/connection/aws_connection.py b/mpqp/execution/connection/aws_connection.py index 9d3b739df..79eb0b6bb 100644 --- a/mpqp/execution/connection/aws_connection.py +++ b/mpqp/execution/connection/aws_connection.py @@ -203,8 +203,8 @@ def configure_account_iam() -> tuple[str, list[Any]]: def delete_aws_braket_account() -> tuple[str, list[Any]]: """Deletes the locally stored AWS Braket configuration.""" - from pathlib import Path import configparser + from pathlib import Path decision = input( colored( diff --git a/mpqp/execution/devices.py b/mpqp/execution/devices.py index f0da0a7d1..23d33a845 100644 --- a/mpqp/execution/devices.py +++ b/mpqp/execution/devices.py @@ -28,8 +28,30 @@ from abc import abstractmethod from enum import Enum, auto +from typing_extensions import override + from mpqp.core.instruction.gates import Gate -from mpqp.core.instruction.gates.native_gates import * +from mpqp.core.instruction.gates.native_gates import ( + CNOT, + CZ, + PRX, + SWAP, + TOF, + H, + Id, + Rx, + Rxx, + Ry, + Ryy, + Rz, + Rzz, + S, + S_dagger, + T, + X, + Y, + Z, +) from mpqp.environment.env_manager import get_env_variable @@ -120,6 +142,10 @@ class IBMDevice(AvailableDevice): AER_SIMULATOR_EXTENDED_STABILIZER = "extended_stabilizer" AER_SIMULATOR_MATRIX_PRODUCT_STATE = "matrix_product_state" + IBM_SHERBROOKE = "ibm_sherbrooke" + IBM_BRISBANE = "ibm_brisbane" + IBM_KYIV = "ibm_kyiv" + IBM_RENSSELAER = "ibm_rensselaer" IBM_KAWASAKI = "ibm_kawasaki" IBM_QUEBEC = "ibm_quebec" @@ -136,6 +162,10 @@ class IBMDevice(AvailableDevice): IBM_MIAMI = "ibm_miami" IBM_BERLIN = "ibm_berlin" + IBM_TORINO = "ibm_torino" + IBM_NAZCA = "ibm_nazca" + IBM_STRASBOURG = "ibm_strasbourg" + IBM_CLEVELAND = "ibm_cleveland" IBM_PEEKSKILL = "ibm_peekskill" @@ -191,6 +221,9 @@ def supports_observable_ideal(self) -> bool: } def compatible_gates(self, native_set: bool = False) -> set[type[Gate]]: + """List of native gate set of IBM's chips. + Pulled from this link: https://quantum.cloud.ibm.com/computers + """ if self == IBMDevice.AER_SIMULATOR_STABILIZER: warnings.warn( UserWarning( @@ -207,7 +240,7 @@ def compatible_gates(self, native_set: bool = False) -> set[type[Gate]]: return {Rx, Ry, Rz, X, Y, Z, H, CNOT, CZ, S, S_dagger, SWAP} else: compatibilities: dict[IBMDeviceFamily, set[type[Gate]]] = { - IBMDeviceFamily.HERON: {CZ, Id, Rx, Rz, X}, # add Rzz + IBMDeviceFamily.HERON: {CZ, Id, Rx, Rz, X, Rzz}, IBMDeviceFamily.NIGHTHAWK: {CZ, Id, Rx, Rz, X}, } family = { @@ -395,6 +428,74 @@ def get_region(self) -> str: else: return get_env_variable("AWS_DEFAULT_REGION") + @override + def compatible_gates(self, native_set: bool = False) -> set[type[Gate]]: + """List of compatible gates with the devices that can be found in MPQP. + Lists pulled from here: https://docs.aws.amazon.com/braket/latest/developerguide/braket-submit-tasks.html#braket-qpu-partner-iqm + """ + if self == AWSDevice.IQM_GARNET or self == AWSDevice.IQM_EMERALD: + if native_set: # authorized: cz, prx + return set([CZ, PRX]) + else: + """authorized gates from doc: + "ccnot", "cnot", + "cphaseshift", "cphaseshift00", "cphaseshift01", "cphaseshift10", "phaseshift" + "cswap", "swap", "iswap", "pswap", + "ecr", "cy", "cz", "xy", "xx", "yy", "zz", "h", "i", "rx", "ry", "rz", "s", "si", "t", "ti", "v", "vi", "x", "y", "z" + """ + return set( + [ + TOF, + CNOT, + SWAP, + PRX, + CZ, + H, + Id, + Rx, + Rxx, + Ry, + Ryy, + Rz, + Rzz, + S, + T, + X, + Y, + Z, + ] + ) + + elif self == AWSDevice.RIGETTI_ANKAA_3: + if native_set: # 'rx', 'rz', 'iswap' + return {Rz, Rx} + # TODO: add (ISWAP) to the set + else: + """ + 'cz', 'xy', 'ccnot', 'cnot', + 'cphaseshift', 'cphaseshift00', 'cphaseshift01', 'cphaseshift10', + 'cswap', 'h', 'i', 'iswap', 'phaseshift', 'pswap', + 'rx', 'ry', 'rz', 's', 'si', 'swap', 't', 'ti', 'x', 'y', 'z' + """ + authorized = [ + TOF, + CNOT, + CZ, + H, + Id, + Rx, + Ry, + Rz, + S, + T, + X, + Y, + Z, + ] + return set(authorized) + + return set() + @staticmethod def from_arn(arn: str): """Returns the right AWSDevice from the arn given in parameter. diff --git a/mpqp/execution/providers/__init__.py b/mpqp/execution/providers/__init__.py index ef5606d2a..bab6fd9a8 100644 --- a/mpqp/execution/providers/__init__.py +++ b/mpqp/execution/providers/__init__.py @@ -1,5 +1,5 @@ # pyright: reportUnusedImport=false from .atos import run_atos, run_myQLM, run_QLM from .aws import run_braket, submit_job_braket -from .ibm import run_aer, run_ibm, run_remote_ibm from .google import run_google, run_local +from .ibm import run_aer, run_ibm, run_remote_ibm diff --git a/mpqp/execution/providers/aws.py b/mpqp/execution/providers/aws.py index 9ce9810af..4aeb352dc 100644 --- a/mpqp/execution/providers/aws.py +++ b/mpqp/execution/providers/aws.py @@ -214,7 +214,11 @@ def run_braket_observable(job: Job): cirq = deepcopy(transpiled_circuit + pre_measure) cirq.state_vector() # pyright: ignore[reportAttributeAccessIssue] - local_result = device.run(cirq, shots=0, inputs=None).result() + local_result = device.run( + cirq, + shots=0, + inputs=None, # disable_qubit_rewiring=True + ).result() assert isinstance(local_result, GateModelQuantumTaskResult) values = local_result.values[0] @@ -226,10 +230,12 @@ def run_braket_observable(job: Job): transpiled_circuit + pre_measure, shots=job.measure.shots, inputs=None, + # disable_qubit_rewiring=True, ) result = local_result.result() assert isinstance(result, GateModelQuantumTaskResult) - length = 2**job.measure.nb_qubits + a = len(list(result.measurement_probabilities.keys())[0]) + length = 2**a sorted_values: list[float] = [] for i in range(length): binary_state = f"{bin(i)[2:].zfill(len(bin(length))- 3)}" @@ -281,8 +287,12 @@ def run_braket_observable(job: Job): observable=braket_obs, target=job.measure.targets ) job.status = JobStatus.RUNNING + # TODO: handle disable_qubit_rewiring, linked to verbatim box but crashes when not in use. local_result = device.run( - copy, shots=job.measure.shots, inputs=None + copy, + shots=job.measure.shots, + inputs=None, + # disable_qubit_rewiring=True, ).result() assert isinstance(local_result, GateModelQuantumTaskResult) results.update({f"observable_{i}": local_result.values[0].real}) @@ -317,6 +327,7 @@ def run_braket_observable(job: Job): program_set, shots=program_set.total_executables * job.measure.shots, inputs=None, + # disable_qubit_rewiring=True, ).result() assert isinstance(local_result, ProgramSetQuantumTaskResult) for res in local_result: @@ -405,7 +416,11 @@ def safe_retrieve_samples(self): # pyright: ignore[reportMissingParameterType] if TYPE_CHECKING: assert isinstance(device, AWSDevice) - task = device.run(braket_circuit, shots=0, inputs=None) + task = device.run( + braket_circuit, + shots=0, + inputs=None, # disable_qubit_rewiring=True + ) elif job.job_type == JobType.SAMPLE: if TYPE_CHECKING: @@ -413,7 +428,12 @@ def safe_retrieve_samples(self): # pyright: ignore[reportMissingParameterType] job.status = JobStatus.RUNNING if TYPE_CHECKING: assert isinstance(device, AWSDevice) - task = device.run(braket_circuit, shots=job.measure.shots, inputs=None) + task = device.run( + braket_circuit, + shots=job.measure.shots, + inputs=None, + # disable_qubit_rewiring=True, + ) elif job.job_type == JobType.OBSERVABLE: # TODO : [multi-obs] update this to take into account the case when we have list of Observables @@ -431,7 +451,13 @@ def safe_retrieve_samples(self): # pyright: ignore[reportMissingParameterType] if TYPE_CHECKING: assert isinstance(device, AWSDevice) - task = device.run(braket_circuit, shots=job.measure.shots, inputs=None) + + task = device.run( + braket_circuit, + shots=job.measure.shots, + inputs=None, + # disable_qubit_rewiring=True, + ) else: raise NotImplementedError(f"Job of type {job.job_type} not handled.") diff --git a/mpqp/execution/providers/ibm.py b/mpqp/execution/providers/ibm.py index 5506b3e6b..8e29f6d14 100644 --- a/mpqp/execution/providers/ibm.py +++ b/mpqp/execution/providers/ibm.py @@ -100,9 +100,7 @@ def compute_expectation_value( "Cannot compute expectation value if measure used in job is not of " "type ExpectationMeasure" ) - nb_shots = job.measure.shots - qiskit_observables: list[SparsePauliOp] = [] for obs in job.measure.observables: if obs.pre_transpiled is None: @@ -112,7 +110,9 @@ def compute_expectation_value( if TYPE_CHECKING: assert isinstance(translated, SparsePauliOp) qiskit_observables.append(translated) - + qiskit_observables = [ + obs.apply_layout(ibm_circuit.layout) for obs in qiskit_observables + ] if isinstance(job.device, StaticIBMSimulatedDevice) or nb_shots != 0: from qiskit_ibm_runtime import EstimatorV2 as Runtime_Estimator @@ -124,10 +124,6 @@ def compute_expectation_value( if TYPE_CHECKING: assert isinstance(ibm_circuit, QuantumCircuit) - - 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) @@ -630,7 +626,6 @@ def extract_result( # res_data is a DataBin, which means all typechecking is out of the # windows for this specific object res_data = result[0].data - if hasattr(res_data, "evs"): if job is None: job = Job(JobType.OBSERVABLE, QCircuit(0), device) diff --git a/mpqp/execution/result.py b/mpqp/execution/result.py index 62bf8440a..7531b16c6 100644 --- a/mpqp/execution/result.py +++ b/mpqp/execution/result.py @@ -409,8 +409,11 @@ def expectation_values(self) -> Union[float, dict[str, float]]: def amplitudes(self) -> npt.NDArray[np.complex128]: """Get the amplitudes of the state of this result""" if self.job.job_type != JobType.STATE_VECTOR: + from mpqp.tools.errors import result_error_message + raise ResultAttributeError( - "Cannot get amplitudes if the job was not of type STATE_VECTOR" + "Cannot get amplitudes if the job was not of type STATE_VECTOR\n" + + result_error_message(self.job.job_type) ) if TYPE_CHECKING: assert self._state_vector is not None @@ -420,8 +423,11 @@ def amplitudes(self) -> npt.NDArray[np.complex128]: def state_vector(self) -> StateVector: """Get the state vector of the state associated with this result""" if self.job.job_type != JobType.STATE_VECTOR: + from mpqp.tools.errors import result_error_message + raise ResultAttributeError( - "Cannot get state vector if the job was not of type STATE_VECTOR" + "Cannot get state vector if the job was not of type STATE_VECTOR\n" + + result_error_message(self.job.job_type) ) if TYPE_CHECKING: assert self._state_vector is not None @@ -431,8 +437,11 @@ def state_vector(self) -> StateVector: def samples(self) -> list[Sample]: """Get the list of samples of the result""" if self.job.job_type != JobType.SAMPLE: + from mpqp.tools.errors import result_error_message + raise ResultAttributeError( - "Cannot get samples if the job was not of type SAMPLE" + "Cannot get samples if the job was not of type SAMPLE\n" + + result_error_message(self.job.job_type) ) if TYPE_CHECKING: assert self._samples is not None @@ -442,9 +451,12 @@ def samples(self) -> list[Sample]: def probabilities(self) -> npt.NDArray[np.float64]: """Get the list of probabilities associated with this result""" if self.job.job_type not in (JobType.SAMPLE, JobType.STATE_VECTOR): + from mpqp.tools.errors import result_error_message + raise ResultAttributeError( "Cannot get probabilities if the job was not of" - " type SAMPLE or STATE_VECTOR" + " type SAMPLE or STATE_VECTOR\n" + + result_error_message(self.job.job_type) ) if TYPE_CHECKING: assert self._probabilities is not None @@ -454,8 +466,11 @@ def probabilities(self) -> npt.NDArray[np.float64]: def counts(self) -> list[int]: """Get the list of counts for each sample of the experiment""" if self.job.job_type != JobType.SAMPLE: + from mpqp.tools.errors import result_error_message + raise ResultAttributeError( - "Cannot get counts if the job was not of type SAMPLE" + "Cannot get counts if the job was not of type SAMPLE\n" + + result_error_message(self.job.job_type) ) if TYPE_CHECKING: diff --git a/mpqp/execution/simulated_devices.py b/mpqp/execution/simulated_devices.py index bf5b93768..ac8a028f5 100644 --- a/mpqp/execution/simulated_devices.py +++ b/mpqp/execution/simulated_devices.py @@ -66,9 +66,11 @@ def to_noise_model(self) -> "Qiskit_NoiseModel": @staticmethod def get_ibm_fake_providers() -> list[tuple[str, type["FakeBackendV2"]]]: from mpqp.environment.var_cache import ( - InstalledProviders, _INSTALLED_MPQP_PROVIDERS, # pyright: ignore[reportPrivateUsage] ) + from mpqp.environment.var_cache import ( + InstalledProviders, + ) if InstalledProviders.QISKIT_IBM_RUNTIME in _INSTALLED_MPQP_PROVIDERS: from qiskit_ibm_runtime import fake_provider diff --git a/mpqp/execution/vqa/qaoa.py b/mpqp/execution/vqa/qaoa.py index 941d221c8..94de78e83 100644 --- a/mpqp/execution/vqa/qaoa.py +++ b/mpqp/execution/vqa/qaoa.py @@ -31,6 +31,7 @@ if TYPE_CHECKING: from networkx import Graph + from mpqp.tools.maths import Matrix diff --git a/mpqp/local_storage/load.py b/mpqp/local_storage/load.py index a554e8633..5c72e1394 100644 --- a/mpqp/local_storage/load.py +++ b/mpqp/local_storage/load.py @@ -30,11 +30,9 @@ def jobs_local_storage_to_mpqp(jobs: Optional[list[DictDB] | DictDB]) -> list[Jo """ if jobs is None: return [] - from numpy import ( - array, # pyright: ignore[reportUnusedImport] - complex64, # pyright: ignore[reportUnusedImport] - complex128, # pyright: ignore[reportUnusedImport] - ) + from numpy import array # pyright: ignore[reportUnusedImport] + from numpy import complex64 # pyright: ignore[reportUnusedImport] + from numpy import complex128 # pyright: ignore[reportUnusedImport] jobs_mpqp = [] if isinstance(jobs, dict): diff --git a/mpqp/tools/__init__.py b/mpqp/tools/__init__.py index c39376150..c0c3be100 100644 --- a/mpqp/tools/__init__.py +++ b/mpqp/tools/__init__.py @@ -1,6 +1,6 @@ # pyright: reportUnusedImport=false from .choice_tree import AnswerNode, QuestionNode, run_choice_tree +from .display import * from .errors import * from .generics import * from .maths import * -from .display import * diff --git a/mpqp/tools/choice_tree.py b/mpqp/tools/choice_tree.py index b085f7e62..2ff8e8f8c 100644 --- a/mpqp/tools/choice_tree.py +++ b/mpqp/tools/choice_tree.py @@ -9,6 +9,7 @@ """ from __future__ import annotations + from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, TypeVar diff --git a/mpqp/tools/circuit.py b/mpqp/tools/circuit.py index ef2479f7d..514dbbdb0 100644 --- a/mpqp/tools/circuit.py +++ b/mpqp/tools/circuit.py @@ -10,10 +10,11 @@ from mpqp.core.instruction.gates.gate import Gate, SingleQubitGate from mpqp.core.instruction.gates.native_gates import ( NATIVE_GATES, + PRX, TOF, CRk, - P, OneQubitNoParamGate, + P, Rk, RotationGate, Rx, @@ -34,8 +35,8 @@ from mpqp.tools.maths import closest_unitary if TYPE_CHECKING: - from qiskit.circuit import QuantumCircuit from qiskit._accelerate.circuit import CircuitInstruction + from qiskit.circuit import QuantumCircuit def random_circuit( @@ -116,9 +117,9 @@ def statevector_from_random_circuit( The statevector with the specified number of qubits Examples: - >>> print(statevector_from_random_circuit(2, seed=123)) # doctest: +NORMALIZE_WHITESPACE - [0.70710678+0.j 0. -0.j 0.26893257-0.65396886j - 0. -0.j ] + >>> expected = np.array([0.4364437 + 0.13832902j, 0, 0.21760065 + 0.861993j, 0]) + >>> np.allclose(statevector_from_random_circuit(2, seed=123), expected) + True """ from mpqp.execution import IBMDevice, Result, run @@ -186,6 +187,12 @@ def random_gate( ) elif issubclass(gate_class, Rk): return Rk(int(rng.integers(1, 10)), target) + elif issubclass(gate_class, PRX): + return gate_class( + np.round(rng.uniform(0, 2 * np.pi), 5), + np.round(rng.uniform(0, 2 * np.pi), 5), + target, + ) elif issubclass(gate_class, RotationGate): if TYPE_CHECKING: assert issubclass(gate_class, (Rx, Ry, Rz, P)) @@ -328,8 +335,8 @@ def replace_custom_gate( correct the statevector if need be. """ from qiskit import QuantumCircuit, transpile - from qiskit.exceptions import QiskitError from qiskit.circuit.library import UnitaryGate + from qiskit.exceptions import QiskitError if not isinstance(custom_unitary, QuantumCircuit): transpilation_circuit = QuantumCircuit(nb_qubits) diff --git a/mpqp/tools/errors.py b/mpqp/tools/errors.py index 54a14154b..75620351e 100644 --- a/mpqp/tools/errors.py +++ b/mpqp/tools/errors.py @@ -2,6 +2,14 @@ clearer errors. When relevant, we also append the trace of the error raised by a provider's SDK.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from mpqp.core.instruction.gates.gate import Gate + from mpqp.execution.result import JobType + class InstructionParsingError(ValueError): """Raised when an QASM instruction encountered by the parser is malformed.""" @@ -77,3 +85,37 @@ class AdditionalGateNoiseWarning(UserWarning): class NonReversibleWarning(UserWarning): """Warning for nonreversible instruction used in inverse function.""" + + +class UnsupportedGateError(ValueError): + def __init__( + self, + gate: Gate, + gate_set: set[type[Gate]], + missing_gates: set[type[Gate]] | None = None, + ): + missing = missing_gates or {type(gate)} + missing_names = ", ".join(sorted(g.__name__ for g in missing)) + available_names = ", ".join(sorted(g.__name__ for g in gate_set)) + + super().__init__( + f"{type(gate).__name__} cannot be represented with the target " + f"gate set. Missing gates: {missing_names}. " + f"Available gates: {available_names}." + ) + + +def result_error_message(type: JobType) -> str: + """Function to give more precision upon errors when getting data from results.""" + from mpqp.execution.result import JobType + + if type == JobType.OBSERVABLE: + msg = "Since your job is of type OBSERVABLE you have access to the following data:\n- expectation_values" + elif type == JobType.SAMPLE: + msg = "Since your job is of type SAMPLE you have access to the following data:\n-counts \n-probabilities" + elif type == JobType.STATE_VECTOR: + msg = "Since your job is of type STATE_VECTOR you have access to the following data:\n-counts \n-probabilities" + return ( + msg + + "\nNote: The type of the job in MPQP is dependant of the type of measurement done in the circuit." + ) diff --git a/mpqp/tools/maths.py b/mpqp/tools/maths.py index e46225082..1070cf69d 100644 --- a/mpqp/tools/maths.py +++ b/mpqp/tools/maths.py @@ -6,7 +6,7 @@ import math from functools import reduce from numbers import Complex, Real -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any, Optional, Union, cast import numpy as np import numpy.typing as npt @@ -393,6 +393,11 @@ def rand_product_local_unitaries( nb_qubits: Number of qubits on which the product of unitaries will act. seed: Seed used to initialize the random number generation. + 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 + provided, it will be used to initialize a new generator. + Returns: A tensor product of random unitary matrices. @@ -417,13 +422,17 @@ def rand_product_local_unitaries( ) # pyright: ignore[reportReturnType] -def rand_unitary_matrix(size: int) -> Matrix: - """Generate a random Unitary matrix sampled from the group U(N), calling the - associated `scipy` function. +def rand_unitary_matrix(size: int, seed: Optional[int] = None) -> Matrix: + """Generate a random Unitary matrix sampled from the group U(N), calling the associated `scipy` function. 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 + provided, it will be used to initialize a new generator. + Returns: A random unitary matrix with complex coefficients. @@ -435,7 +444,10 @@ def rand_unitary_matrix(size: int) -> Matrix: """ from scipy.stats import unitary_group - return np.asarray(unitary_group.rvs(size), dtype=np.complex128) + return np.asarray( + unitary_group.rvs(size, random_state=np.random.default_rng(seed)), + dtype=np.complex128, + ) def rand_hermitian_matrix( @@ -549,3 +561,98 @@ def rearrange_matrix(m: Matrix, targets: list[int], do_copy: bool = True) -> Mat m[...] = rearranged_matrix return m + + +def symbolic_product(*factors: Expr | complex) -> Expr | complex: + """Multiplies numeric or symbolic factors while preserving their + arithmetic type. + + If at least one factor is a SymPy expression, all factors are converted to + SymPy objects before the multiplication. Otherwise, the multiplication is + performed using Python complex numbers. + + Args: + factors: Numeric or symbolic factors to multiply. + + Returns: + The product as a SymPy expression if any factor is symbolic, or as a + Python complex number otherwise. + + Examples: + >>> symbolic_product(2, 3j) + 6j + >>> x = symbols("x") + >>> symbolic_product(2, x) + 2*x + + """ + from sympy import Expr, prod, sympify + + if any(isinstance(factor, Expr) for factor in factors): + return cast(Expr, prod(sympify(factor) for factor in factors)) + + result = 1 + 0j + for factor in factors: + result *= cast(complex, factor) + return result + + +def symbolic_divide(dividend: Expr | float, divisor: Expr | float) -> Expr | float: + """Divides numeric or symbolic values while preserving their arithmetic + type. + + If either operand is a SymPy expression, both operands are converted to + SymPy objects before the division. Otherwise, regular Python division is + used. + + Args: + dividend: Value to divide. + divisor: Value by which the ``dividend`` is divided. + + Returns: + The quotient as a SymPy expression if either operand is symbolic, or as + a floating-point number otherwise. + + Examples: + >>> symbolic_divide(3.0, 2.0) + 1.5 + >>> x = symbols("x") + >>> symbolic_divide(x, 2) + x/2 + + """ + from sympy import Expr, sympify + + if isinstance(dividend, Expr) or isinstance(divisor, Expr): + return sympify(dividend) / sympify(divisor) + + return dividend / divisor + + +def rotation_denominator(k: Expr | float) -> Expr | float: + """Computes the denominator of the angle of an :class:`Rk` gate. + + The denominator is defined as :math:`2^{k-1}`. A symbolic ``k`` produces a + SymPy expression, while a numeric ``k`` produces a floating-point number. + + Args: + k: Numeric or symbolic index of the rotation gate. + + Returns: + The value :math:`2^{k-1}` with the same symbolic or numeric nature as + ``k``. + + Examples: + >>> rotation_denominator(4) + 8.0 + >>> k = symbols("k") + >>> rotation_denominator(k) + 2**(k - 1) + + """ + from sympy import Expr, Integer + + if isinstance(k, Expr): + return Integer(2) ** (k - Integer(1)) + + return 2.0 ** (k - 1.0) diff --git a/mpqp/tools/obs_decomposition.py b/mpqp/tools/obs_decomposition.py index e8273f54b..bbc71ce79 100644 --- a/mpqp/tools/obs_decomposition.py +++ b/mpqp/tools/obs_decomposition.py @@ -8,10 +8,10 @@ import numpy.typing as npt from mpqp.core.instruction.measurement.pauli_string import ( - pI, PauliString, PauliStringAtom, PauliStringMonomial, + pI, pX, pY, pZ, diff --git a/mpqp/tools/unitary_decomposition.py b/mpqp/tools/unitary_decomposition.py index c0b8d8811..02b754436 100644 --- a/mpqp/tools/unitary_decomposition.py +++ b/mpqp/tools/unitary_decomposition.py @@ -162,15 +162,15 @@ def _decompose( else: # 2 qubits or more length = len(U) U12, MuxRy, V12 = cossin(U, p=length // 2, q=length // 2, separate=False) - # Extracts the rotations of the multiplexed Ry for later decomposition thetas = [] for i in range(MuxRy.shape[0] // 2): thetas.append(np.arccos(MuxRy[i][i])) - thetas = np.array(thetas) + thetas = np.array(thetas, dtype=np.float64) assert isinstance(U12, np.ndarray) assert isinstance(V12, np.ndarray) + Vu, MuxRzu, Wu = _unitary_SVD(U12) Vv, MuxRzv, Wv = _unitary_SVD(V12) @@ -203,7 +203,7 @@ def _decompose( circuit, targets, position, - Ry, + Ry, # pyright: ignore[reportArgumentType] ) circuit = _decompose(Wu, circuit, targets, position + 1) @@ -233,7 +233,6 @@ def _optimize_circuit(circuit: QCircuit) -> QCircuit: break j += 1 i += 1 - return circuit @@ -262,8 +261,13 @@ def quantum_shannon_decomposition( .. [1] Mikko Möttönen, Juha J. Vartiainen, Ville Bergholm, and Martti M. Salomaa. 2004. Quantum circuits for general multi-qubit gates. American Physical Society (APS) : 93-13. Examples: + >>> from mpqp.tools.maths import matrix_eq >>> U = np.array([[1,0],[0,1]]) - >>> circuit = quantum_shannon_decomposition(U) + >>> circuit = quantum_shannon_decomposition(U, [0]) + >>> print(matrix_eq(U, circuit.to_matrix())) + True + >>> U = np.fft.fft(np.eye(4)) / 2 + >>> circuit = quantum_shannon_decomposition(U, [0, 1]) >>> print(matrix_eq(U, circuit.to_matrix())) True """ diff --git a/mpqp/translation/__init__.py b/mpqp/translation/__init__.py index 9cf1a6ed7..1934d806b 100644 --- a/mpqp/translation/__init__.py +++ b/mpqp/translation/__init__.py @@ -1,4 +1,4 @@ -from .qasm import * from .braket import * from .cirq import * +from .qasm import * from .qiskit import * diff --git a/mpqp/translation/braket.py b/mpqp/translation/braket.py index 79e1fe841..1f4ec14a1 100644 --- a/mpqp/translation/braket.py +++ b/mpqp/translation/braket.py @@ -1,26 +1,29 @@ from typing import TYPE_CHECKING +from mpqp.core.instruction.gates.gate import Gate +from mpqp.core.instruction.gates.gate_decomposition import resolve_gate from mpqp.environment.var_cache import ( _INSTALLED_MPQP_PROVIDERS, # pyright: ignore[reportPrivateUsage] +) +from mpqp.environment.var_cache import ( InstalledProviders, ) if InstalledProviders.BRAKET in _INSTALLED_MPQP_PROVIDERS: - if TYPE_CHECKING: - from braket.circuits import Circuit as braket_Circuit - from mpqp.core.instruction.gates.native_gates import NativeGate - from mpqp.core.circuit import QCircuit + from braket.circuits import Circuit as braket_Circuit + + from mpqp.core.circuit import QCircuit - def braket_to_mpqp(qcircuit: "braket_Circuit") -> "QCircuit": + def braket_to_mpqp(qcircuit: braket_Circuit) -> QCircuit: + from braket.circuits import Circuit as braket_Circuit from braket.circuits.serialization import IRType from braket.ir.openqasm.program_v1 import Program + from mpqp.core.languages import Language from mpqp.translation.qasm.open_qasm_2_and_3 import open_qasm_3_to_2 from mpqp.translation.qasm.qasm_to_braket import braket_noise_to_mpqp from mpqp.translation.qasm.qasm_to_mpqp import qasm2_parse - from braket.circuits import Circuit as braket_Circuit - from mpqp.core.languages import Language assert isinstance(qcircuit, braket_Circuit) remove_measure = True @@ -46,12 +49,26 @@ def braket_to_mpqp(qcircuit: "braket_Circuit") -> "QCircuit": qc.add(noises) return qc + def get_braket_gate_set() -> set[type[Gate]]: + """Return gates directly representable by Braket.""" + from mpqp.gates import CNOT, PRX, Rx, Rxx, Ry, Ryy, Rz, Rzz + + return { + Rx, + Ry, + Rz, + PRX, + Rxx, + Ryy, + Rzz, + CNOT, + } + def mpqp_to_braket( - circuit: "QCircuit", + circuit: QCircuit, skip_pre_measure: bool = False, skip_measurements: bool = False, - authorized_gates: set[type["NativeGate"]] | None = None, - ) -> "braket_Circuit": + ) -> braket_Circuit: """Translate a MPQP circuit to a Braket equivalent. Note: @@ -61,7 +78,6 @@ def mpqp_to_braket( circuit: The original MPQP circuit to be translated. skip_pre_measure: If set at True will translate the circuit without its pre-measurement circuit (see QCircuit.to_other_language for more information). skip_measurements: If set at True will translate the circuit without any measurement. - authorized_gates: The set of gates allowed on the circuit, if the circuit contains any other gates it raises a ValueError. Examples: >>> circuit = QCircuit([H(0), CNOT(0, 1), BasisMeasure()]) @@ -86,7 +102,14 @@ def mpqp_to_braket( └───┘ T : │ 0 │ 1 │ """ - from mpqp.execution.providers.aws import apply_noise_to_braket_circuit + from mpqp.core.circuit import QCircuit + from mpqp.core.instruction import ( + Barrier, + BasisMeasure, + Breakpoint, + ControlledGate, + Measure, + ) from mpqp.core.instruction.gates.custom_controlled_gate import ( CustomControlledGate, ) @@ -94,26 +117,16 @@ def mpqp_to_braket( from mpqp.core.instruction.gates.gate import Gate from mpqp.core.instruction.gates.native_gates import CRk from mpqp.core.languages import Language - from mpqp.core.instruction import ( - Measure, - Breakpoint, - Barrier, - ControlledGate, - BasisMeasure, - ) - from mpqp.core.circuit import QCircuit + from mpqp.execution.providers.aws import apply_noise_to_braket_circuit - if authorized_gates is None: - authorized_gates = set() if len(circuit.noises) != 0: if any(isinstance(instr, CRk) for instr in circuit.instructions): raise NotImplementedError( "Cannot simulate noisy circuit with CRk gate due to " "an error on AWS Braket side." ) - from braket.circuits import Circuit as BracketCircuit - braket_circuit = BracketCircuit() + braket_circuit = braket_Circuit() # If the number of qubits are defined by the user, we ensure that every qubits are used. # Otherwise the circuit can remain non continuous. @@ -126,9 +139,10 @@ def mpqp_to_braket( ) ) if len(used_qubits) != circuit.nb_qubits: - from mpqp.gates import Id from copy import deepcopy + from mpqp.gates import Id + circuit = QCircuit( [ Id(qubit) @@ -154,13 +168,14 @@ def mpqp_to_braket( braket_circuit.measure(targets) continue if isinstance(instruction, Gate): - from mpqp.translation.utils import verify_convert_instructions - - instr = verify_convert_instructions(instruction, authorized_gates) + instructions = resolve_gate( + instruction, + get_braket_gate_set(), + ) else: - instr = [instruction] + instructions = (instruction,) - for instruction in instr: + for instruction in instructions: braket_instr = instruction.to_other_language(Language.BRAKET) try: targets = [target for target in instruction.targets] diff --git a/mpqp/translation/cirq.py b/mpqp/translation/cirq.py index 0ca8c8c55..a89124bd2 100644 --- a/mpqp/translation/cirq.py +++ b/mpqp/translation/cirq.py @@ -1,20 +1,22 @@ from typing import TYPE_CHECKING +from mpqp.core.instruction.gates.gate import Gate +from mpqp.core.instruction.gates.gate_decomposition import resolve_gate from mpqp.environment.var_cache import ( _INSTALLED_MPQP_PROVIDERS, # pyright: ignore[reportPrivateUsage] +) +from mpqp.environment.var_cache import ( InstalledProviders, ) -from mpqp.translation.utils import verify_convert_instructions if InstalledProviders.CIRQ in _INSTALLED_MPQP_PROVIDERS: - if TYPE_CHECKING: - from mpqp.core.circuit import QCircuit - from mpqp.core.instruction.gates.native_gates import NativeGate - from cirq.circuits.circuit import Circuit as cirq_Circuit - from cirq.circuits.moment import Moment + from cirq.circuits.circuit import Circuit as cirq_Circuit + from cirq.circuits.moment import Moment + + from mpqp.core.circuit import QCircuit - def cirq_to_mpqp(qcircuit: "cirq_Circuit | Moment") -> "QCircuit": + def cirq_to_mpqp(qcircuit: cirq_Circuit | Moment) -> QCircuit: """Translate a cirq Circuit to a MPQP QCircuit. Note: If the provided qcircuit is a Moment it will be translated to a fully fledged cirq circuit. @@ -22,12 +24,14 @@ def cirq_to_mpqp(qcircuit: "cirq_Circuit | Moment") -> "QCircuit": Args: qcircuit: Any cirq Circuit, or for simpler circuits could be a sole Moment. """ + from copy import deepcopy + + from cirq import MatrixGate, ops from cirq.circuits.circuit import Circuit as cirq_Circuit from cirq.circuits.moment import Moment - from cirq import ops, MatrixGate + from mpqp import QCircuit from mpqp.gates import CustomGate - from copy import deepcopy qcircuit = deepcopy(qcircuit) from mpqp.translation.qasm.qasm_to_mpqp import ( @@ -81,12 +85,17 @@ def cirq_to_mpqp(qcircuit: "cirq_Circuit | Moment") -> "QCircuit": c.input_g_phase = gphase return c + def get_cirq_gate_set() -> set[type[Gate]]: + """Return gates directly representable by Cirq.""" + from mpqp.gates import CNOT, Rx, Ry, Rz, PRX, Rzz, Rxx, Ryy + + return {Rx, Ry, Rz, CNOT, PRX, Rzz, Rxx, Ryy} + def mpqp_to_cirq( - circuit: "QCircuit", + circuit: QCircuit, skip_pre_measure: bool = False, skip_measurements: bool = False, - authorized_gates: set[type["NativeGate"]] | None = None, - ) -> "cirq_Circuit": + ) -> cirq_Circuit: """Translate a MPQP circuit to a Cirq equivalent. Note: @@ -96,7 +105,6 @@ def mpqp_to_cirq( circuit: The original MPQP circuit to be translated. skip_pre_measure: If set at True will translate the circuit without its pre-measurement circuit (see QCircuit.to_other_language for more information). skip_measurements: If set at True will translate the circuit without any measurement. - authorized_gates: The set of gates allowed on the circuit, if the circuit contains any other gates it raises a ValueError. Examples: >>> circuit = QCircuit([H(0), CNOT(0,1), BasisMeasure()]) @@ -115,20 +123,19 @@ def mpqp_to_cirq( from cirq.circuits.circuit import Circuit as CirqCircuit from cirq.ops.identity import I from cirq.ops.named_qubit import NamedQubit + from mpqp.core.instruction import ( - Measure, - Breakpoint, - CustomGate, Barrier, + Breakpoint, ControlledGate, CustomControlledGate, + CustomGate, ExpectationMeasure, + Measure, ) - from mpqp.core.languages import Language from mpqp.core.instruction.gates.gate import Gate + from mpqp.core.languages import Language - if authorized_gates is None: - authorized_gates = set() cirq_qubits = [NamedQubit(f"q_{i}") for i in range(circuit.nb_qubits)] cirq_circuit = CirqCircuit() @@ -140,12 +147,13 @@ def mpqp_to_cirq( if isinstance(instruction, Measure): for pre_measure in instruction.pre_measure: if isinstance(pre_measure, (CustomGate, CustomControlledGate)): - instr = verify_convert_instructions( - pre_measure, authorized_gates + resolved_pre_measure = resolve_gate( + pre_measure, + get_cirq_gate_set(), ) - qasm2_code, gphase = pre_measure.to_other_language( - Language.QASM2 - ) # pyright: ignore[reportGeneralTypeIssues] + qasm2_code, gphase = resolved_pre_measure[ + 0 + ].to_other_language(Language.QASM2) if TYPE_CHECKING: assert isinstance(qasm2_code, str) from mpqp.translation.qasm.qasm_to_cirq import ( @@ -172,17 +180,20 @@ def mpqp_to_cirq( cirq_circuit.append(cirq_pre_measure.on(*targets)) if isinstance(instruction, Gate): - instr = verify_convert_instructions(instruction, authorized_gates) + instructions = resolve_gate( + instruction, + get_cirq_gate_set(), + ) else: - instr = [instruction] + instructions = (instruction,) - for gate in instr: + for gate in instructions: if isinstance(gate, (ExpectationMeasure, Barrier, Breakpoint)): continue elif isinstance(gate, CustomGate): from cirq.ops.raw_types import Gate as CirqGate - custom_gate = instr[0] + custom_gate = instructions[0] targets = [] for target in custom_gate.targets: @@ -222,8 +233,8 @@ def mpqp_to_cirq( ) if circuit.input_g_phase != 0: - from cirq import GlobalPhaseGate import numpy as np + from cirq import GlobalPhaseGate cirq_circuit.insert( 0, GlobalPhaseGate(np.exp(1j * circuit.input_g_phase)).on() @@ -236,9 +247,10 @@ def mpqp_to_cirq( from cirq import Gate as cirqGate if TYPE_CHECKING: - from mpqp.tools.generics import Matrix from cirq import Qid + from mpqp.tools.generics import Matrix + class cirqCustomGate(cirqGate): def __init__( self, diff --git a/mpqp/translation/qasm/__init__.py b/mpqp/translation/qasm/__init__.py index ca9147e79..22df6016c 100644 --- a/mpqp/translation/qasm/__init__.py +++ b/mpqp/translation/qasm/__init__.py @@ -4,5 +4,5 @@ from .qasm_to_braket import * from .qasm_to_cirq import * from .qasm_to_mpqp import * -from .qasm_to_qiskit import * from .qasm_to_myqlm import * +from .qasm_to_qiskit import * diff --git a/mpqp/translation/qasm/lexer_utils.py b/mpqp/translation/qasm/lexer_utils.py index 000c6d9f9..c54822ad6 100644 --- a/mpqp/translation/qasm/lexer_utils.py +++ b/mpqp/translation/qasm/lexer_utils.py @@ -135,10 +135,12 @@ def t_error(t): # pyright: ignore[reportMissingParameterType] "rx": Rx, "ry": Ry, "rz": Rz, + "prx": PRX, } two_qubits_parametrized_gate_qasm = { "cp": CP, + "rzz": Rzz, } diff --git a/mpqp/translation/qasm/mpqp_to_qasm.py b/mpqp/translation/qasm/mpqp_to_qasm.py index ec66fea26..7c5d92a27 100644 --- a/mpqp/translation/qasm/mpqp_to_qasm.py +++ b/mpqp/translation/qasm/mpqp_to_qasm.py @@ -6,6 +6,7 @@ import numpy as np from mpqp.core.instruction.gates.custom_controlled_gate import CustomControlledGate +from mpqp.core.instruction.gates.native_gates import ComposedGate if TYPE_CHECKING: from mpqp.core.circuit import QCircuit @@ -152,62 +153,66 @@ def mpqp_to_qasm2( gphase += phase if skip_measurements and isinstance(instruction, Measure): continue - if simplify: - if isinstance(instruction, (SingleQubitGate, BasisMeasure)): - if previous is None: - previous = instruction - elif type(instruction) != type(previous) or ( - isinstance(instruction, ParametrizedGate) - and instruction.parameters - != previous.parameters # pyright: ignore[reportAttributeAccessIssue] - ): - if isinstance(previous, BasisMeasure): - qasm_measure += _simplify_instruction_to_qasm( - previous, targets, c_targets - ) - else: - qasm_str += _simplify_instruction_to_qasm( - previous, targets, c_targets - ) - targets = {i: 0 for i in range(qcircuit.nb_qubits)} - c_targets = {i: 0 for i in range(qcircuit.nb_qubits)} - previous = instruction - - for target in instruction.targets: - targets[target] += 1 - if isinstance(instruction, BasisMeasure): - if instruction.c_targets is not None: - for c_target in instruction.c_targets: - c_targets[c_target] += 1 + instructions = [instruction] + if isinstance(instruction, ComposedGate): + instructions = instruction.decompose() + for instruction in instructions: + if simplify: + if isinstance(instruction, (SingleQubitGate, BasisMeasure)): + if previous is None: + previous = instruction + elif type(instruction) != type(previous) or ( + isinstance(instruction, ParametrizedGate) + and instruction.parameters + != previous.parameters # pyright: ignore[reportAttributeAccessIssue] + ): + if isinstance(previous, BasisMeasure): + qasm_measure += _simplify_instruction_to_qasm( + previous, targets, c_targets + ) + else: + qasm_str += _simplify_instruction_to_qasm( + previous, targets, c_targets + ) + targets = {i: 0 for i in range(qcircuit.nb_qubits)} + c_targets = {i: 0 for i in range(qcircuit.nb_qubits)} + previous = instruction + + for target in instruction.targets: + targets[target] += 1 + if isinstance(instruction, BasisMeasure): + if instruction.c_targets is not None: + for c_target in instruction.c_targets: + c_targets[c_target] += 1 + else: + for i in range(len(instruction.targets)): + c_targets[i] += 1 + else: + if previous: + if isinstance(previous, BasisMeasure): + qasm_measure += _simplify_instruction_to_qasm( + previous, targets, c_targets + ) + else: + qasm_str += _simplify_instruction_to_qasm( + previous, targets, c_targets + ) + previous = None + targets = {i: 0 for i in range(qcircuit.nb_qubits)} + c_targets = {i: 0 for i in range(qcircuit.nb_qubits)} + qasm, phase = _instruction_to_qasm2(instruction) + if isinstance(instruction, BasisMeasure): + qasm_measure += qasm else: - for i in range(len(instruction.targets)): - c_targets[i] += 1 + qasm_str += qasm + gphase += phase else: - if previous: - if isinstance(previous, BasisMeasure): - qasm_measure += _simplify_instruction_to_qasm( - previous, targets, c_targets - ) - else: - qasm_str += _simplify_instruction_to_qasm( - previous, targets, c_targets - ) - previous = None - targets = {i: 0 for i in range(qcircuit.nb_qubits)} - c_targets = {i: 0 for i in range(qcircuit.nb_qubits)} qasm, phase = _instruction_to_qasm2(instruction) if isinstance(instruction, BasisMeasure): qasm_measure += qasm else: qasm_str += qasm gphase += phase - else: - qasm, phase = _instruction_to_qasm2(instruction) - if isinstance(instruction, BasisMeasure): - qasm_measure += qasm - else: - qasm_str += qasm - gphase += phase if previous: qasm_str += _simplify_instruction_to_qasm(previous, targets, c_targets) diff --git a/mpqp/translation/qasm/myqlm_to_mpqp.py b/mpqp/translation/qasm/myqlm_to_mpqp.py index 950faca53..50a09f49f 100644 --- a/mpqp/translation/qasm/myqlm_to_mpqp.py +++ b/mpqp/translation/qasm/myqlm_to_mpqp.py @@ -73,12 +73,12 @@ def from_myqlm_to_mpqp(circuit: my_QLM_Circuit) -> QCircuit: q_1: ─────┤ X ├ └───┘ """ + from mpqp.core.instruction.gates.custom_controlled_gate import CustomControlledGate from mpqp.core.instruction.gates.native_gates import ( NoParameterGate, OneQubitNoParamGate, RotationGate, ) - from mpqp.core.instruction.gates.custom_controlled_gate import CustomControlledGate qc = QCircuit(circuit.nbqbits) diff --git a/mpqp/translation/qasm/open_qasm_2_and_3.py b/mpqp/translation/qasm/open_qasm_2_and_3.py index cce00eaa8..4bcc67fb0 100644 --- a/mpqp/translation/qasm/open_qasm_2_and_3.py +++ b/mpqp/translation/qasm/open_qasm_2_and_3.py @@ -48,6 +48,7 @@ """ from __future__ import annotations + import os import re from enum import Enum, auto @@ -135,6 +136,7 @@ class Instr(Enum): "cry", "cp", "cu", + "rzz", ] std_gates_3 = [ "u1", @@ -166,6 +168,7 @@ class Instr(Enum): "cphase", "phase", "sx", + "rzz", ] std_gates_3_to_2_map = { "U": "u", @@ -192,6 +195,7 @@ class Instr(Enum): "rxx", "ryy", "rzz", + "prx", ] std_braket_gates = [ "i", @@ -214,6 +218,8 @@ class Instr(Enum): "gpi", "gpi2", "ms", + "prx", + "rzz", ] @@ -1279,7 +1285,6 @@ def open_qasm_3_to_2( if language == Language.QISKIT or language == Language.BRAKET: code = _replace_header(code) code = remove_user_gates(code) - instructions = parse_openqasm_3_file(code) included_instructions = set() @@ -1288,7 +1293,6 @@ def open_qasm_3_to_2( defined_gates.update(std_qiskit_gates) elif language == Language.BRAKET: defined_gates.update(std_braket_gates) - for instr in instructions: i_code, h_code, gphase = convert_instruction_3_to_2( instr, diff --git a/mpqp/translation/qasm/qasm_to_braket.py b/mpqp/translation/qasm/qasm_to_braket.py index b0b14adbb..98c24bb72 100644 --- a/mpqp/translation/qasm/qasm_to_braket.py +++ b/mpqp/translation/qasm/qasm_to_braket.py @@ -28,6 +28,7 @@ """ from __future__ import annotations + import io import warnings from logging import StreamHandler, getLogger @@ -39,8 +40,8 @@ from mpqp.core.instruction.gates.custom_gate import CustomGate from mpqp.noise import NoiseModel -from mpqp.translation.qasm.open_qasm_2_and_3 import open_qasm_hard_includes from mpqp.tools.errors import UnsupportedBraketFeaturesWarning +from mpqp.translation.qasm.open_qasm_2_and_3 import open_qasm_hard_includes def qasm3_to_braket_Program(qasm3_str: str) -> "Program": @@ -228,9 +229,9 @@ def braket_custom_gates_to_mpqp(qasm3_code: str) -> CustomGate: import numpy as np if "braket unitary" in qasm3_code: - matrix = np.array( - ast.literal_eval(qasm3_code[qasm3_code.find('[') : qasm3_code.rfind(')')]) - ) + matrix_str = qasm3_code[qasm3_code.find('[') : qasm3_code.rfind(')')] + matrix_str = matrix_str.replace("im", "j") + matrix = np.array(ast.literal_eval(matrix_str)) indices = [int(i) for i in re.findall(r"q\[(\d+)\]", qasm3_code)] return CustomGate(matrix, indices) diff --git a/mpqp/translation/qasm/qasm_to_cirq.py b/mpqp/translation/qasm/qasm_to_cirq.py index 8acdc707e..f6f2b75df 100644 --- a/mpqp/translation/qasm/qasm_to_cirq.py +++ b/mpqp/translation/qasm/qasm_to_cirq.py @@ -14,6 +14,7 @@ """ from __future__ import annotations + from typing import TYPE_CHECKING if TYPE_CHECKING: diff --git a/mpqp/translation/qasm/qasm_to_mpqp.py b/mpqp/translation/qasm/qasm_to_mpqp.py index 8a69c255a..aab029f22 100644 --- a/mpqp/translation/qasm/qasm_to_mpqp.py +++ b/mpqp/translation/qasm/qasm_to_mpqp.py @@ -75,9 +75,7 @@ def qasm2_parse(input_string: str) -> QCircuit: input_string = remove_user_gates(input_string, skip_qelib1=True) input_string, gphase = remove_include_and_comment(input_string) - tokens = lex_openqasm(input_string) - if ( tokens[0].type != 'OPENQASM' and tokens[1].type != 'REALN' @@ -238,6 +236,8 @@ def _Gate_two_qubits_parametrized( raise SyntaxError(f"Gate_one_parametrized: {idx} {tokens[idx]}") idx += 1 parameter, idx = _eval_expr(tokens, idx) + if len(parameter) == 1: + parameter = parameter[0] if ( check_Id(tokens, idx) or tokens[idx + 4].type != 'COMMA' @@ -311,9 +311,16 @@ def _eval_expr(tokens: list[LexToken], idx: int) -> tuple[Any, int]: expr = "" open_paren = 0 - while tokens[idx].type != 'COMMA' and ( - tokens[idx].type != 'RPAREN' or open_paren > 0 - ): + index = 0 + parameters = [] + while tokens[idx].type != 'RPAREN' or open_paren > 0: + + if tokens[idx].type == 'COMMA': + parameters.append(expr) + expr = "" + index += 1 + idx += 1 + continue if tokens[idx].type == 'LPAREN': open_paren += 1 expr += "(" @@ -339,7 +346,8 @@ def _eval_expr(tokens: list[LexToken], idx: int) -> tuple[Any, int]: else: expr += str(tokens[idx].value) idx += 1 - return eval(expr), idx + 1 + parameters.append(expr) + return [eval(param) for param in parameters], idx + 1 def _Gate_one_parametrized( @@ -349,13 +357,19 @@ def _Gate_one_parametrized( raise SyntaxError(f"Gate_one_parametrized: {idx} {tokens[idx]}") idx += 1 parameter, idx = _eval_expr(tokens, idx) - if check_Id(tokens, idx): raise SyntaxError( f'Gate_two_qubits: {" ".join(token.value for token in tokens[idx : idx + 3])}' ) target = tokens[idx + 2].value - circuit.add(one_parametrized_gate_qasm[gate_str](parameter, target)) + if one_parametrized_gate_qasm[gate_str] == PRX: + circuit.add(PRX(parameter[0], parameter[1], target)) + else: + circuit.add( + one_parametrized_gate_qasm[gate_str]( + parameter[0], target + ) # pyright: ignore[reportCallIssue] + ) return idx + 5 @@ -365,16 +379,14 @@ def _Gate_U(circuit: QCircuit, gate_str: str, tokens: list[LexToken], idx: int) idx += 1 theta, phi, lbda = 0, 0, 0 + parameters, idx = _eval_expr(tokens, idx) if gate_str == 'u1': - theta, idx = _eval_expr(tokens, idx) + (lbda,) = parameters elif gate_str == 'u2': - theta, idx = _eval_expr(tokens, idx) - phi, idx = _eval_expr(tokens, idx) + phi, lbda = parameters + theta = np.pi / 2 elif gate_str == 'u3' or gate_str == 'u' or gate_str == 'U': - theta, idx = _eval_expr(tokens, idx) - phi, idx = _eval_expr(tokens, idx) - lbda, idx = _eval_expr(tokens, idx) - + theta, phi, lbda = parameters if check_Id(tokens, idx): raise SyntaxError( f'GateU: {" ".join(str(token.value) for token in tokens[idx : idx + 4])}' @@ -412,14 +424,15 @@ def _TokenCustom(circuit: QCircuit, tokens: list[LexToken], idx: int) -> int: def parse_qasm2_gates(code: str) -> tuple[str, float]: + import re + from mpqp.translation.qasm.open_qasm_2_and_3 import ( - qasm_code, - remove_user_gates, Instr, parse_gphase_instruction, + qasm_code, remove_include_and_comment, + remove_user_gates, ) - import re code, gphase = remove_include_and_comment(code) diff --git a/mpqp/translation/qasm/qasm_to_myqlm.py b/mpqp/translation/qasm/qasm_to_myqlm.py index 8947687a5..801d524c6 100644 --- a/mpqp/translation/qasm/qasm_to_myqlm.py +++ b/mpqp/translation/qasm/qasm_to_myqlm.py @@ -5,6 +5,7 @@ the qasm code.""" from __future__ import annotations + import re from typing import TYPE_CHECKING diff --git a/mpqp/translation/qasm/qasm_to_qiskit.py b/mpqp/translation/qasm/qasm_to_qiskit.py index 6c4343e82..ee92662f4 100644 --- a/mpqp/translation/qasm/qasm_to_qiskit.py +++ b/mpqp/translation/qasm/qasm_to_qiskit.py @@ -7,6 +7,7 @@ """ from __future__ import annotations + from typing import TYPE_CHECKING if TYPE_CHECKING: diff --git a/mpqp/translation/qiskit.py b/mpqp/translation/qiskit.py index 3dbc6f4d5..edd0f0e8f 100644 --- a/mpqp/translation/qiskit.py +++ b/mpqp/translation/qiskit.py @@ -1,14 +1,19 @@ from typing import TYPE_CHECKING + +from mpqp.core.instruction.gates.gate import Gate +from mpqp.core.instruction.gates.gate_decomposition import resolve_gate +from mpqp.core.instruction.gates.native_gates import U from mpqp.environment.var_cache import ( _INSTALLED_MPQP_PROVIDERS, # pyright: ignore[reportPrivateUsage] +) +from mpqp.environment.var_cache import ( InstalledProviders, ) if InstalledProviders.QISKIT in _INSTALLED_MPQP_PROVIDERS: - if TYPE_CHECKING: - from mpqp.core.instruction.gates.native_gates import NativeGate - from mpqp.core.circuit import QCircuit - from qiskit import QuantumCircuit + from qiskit import QuantumCircuit + + from mpqp.core.circuit import QCircuit def qiskit_to_mpqp(qcircuit: "QuantumCircuit"): """Translate a qiskit QuantumCircuit into a MPQP QCircuit. @@ -20,6 +25,7 @@ def qiskit_to_mpqp(qcircuit: "QuantumCircuit"): qcircuit: Any Qiskit quantum circuit. """ from qiskit import qasm3 + from mpqp.core.languages import Language from mpqp.translation.qasm import open_qasm_3_to_2 from mpqp.translation.qasm.qasm_to_mpqp import qasm2_parse @@ -30,13 +36,17 @@ def qiskit_to_mpqp(qcircuit: "QuantumCircuit"): qc = qasm2_parse(qasm2_code) return qc + def get_qiskit_gate_set() -> set[type[Gate]]: + from mpqp.gates import CNOT, PRX, Rx, Rxx, Ry, Ryy, Rz, Rzz + + return {Rx, Ry, Rz, PRX, Rxx, Ryy, Rzz, U, CNOT} + def mpqp_to_qiskit( - circuit: "QCircuit", + circuit: QCircuit, skip_pre_measure: bool = False, skip_measurements: bool = False, printing: bool = False, - authorized_gates: set[type["NativeGate"]] | None = None, - ) -> "QuantumCircuit": + ) -> QuantumCircuit: """Translate a MPQP circuit to a Qiskit equivalent. Note: @@ -46,7 +56,6 @@ def mpqp_to_qiskit( circuit: The original MPQP circuit to be translated. skip_pre_measure: If set at True will translate the circuit without its pre-measurement circuit (see QCircuit.to_other_language for more information). skip_measurements: If set at True will translate the circuit without any measurement. - authorized_gates: The set of gates allowed on the circuit, if the circuit contains any other gates it raises a ValueError. Examples: >>> circuit = QCircuit([H(0), CNOT(0, 1), BasisMeasure()]) @@ -71,27 +80,26 @@ def mpqp_to_qiskit( from qiskit.circuit import Operation, QuantumCircuit from qiskit.circuit.quantumcircuit import CircuitInstruction from qiskit.quantum_info import Operator - from mpqp.core.instruction.gates.gate import Gate - from mpqp.core.instruction.gates.custom_controlled_gate import ( - CustomControlledGate, - ) - from mpqp.core.languages import Language from mpqp.core.instruction import ( - Measure, - Breakpoint, - CustomGate, Barrier, - ControlledGate, BasisMeasure, + Breakpoint, + ControlledGate, + CustomGate, ExpectationMeasure, + Measure, + ) + from mpqp.core.instruction.gates.custom_controlled_gate import ( + CustomControlledGate, ) + from mpqp.core.instruction.gates.gate import Gate + from mpqp.core.languages import Language # to avoid defining twice the same parameter, we keep trace of the # added parameters, and we use those instead of new ones when they # are used more than once - if authorized_gates is None: - authorized_gates = set() + qiskit_parameters = set() if circuit.nb_cbits == 0: new_circ = QuantumCircuit(circuit.nb_qubits) @@ -105,17 +113,29 @@ def mpqp_to_qiskit( if isinstance(instruction, (Measure, Breakpoint)): continue options = ( - {"printing": printing} if isinstance(instruction, CustomGate) else {} + {"printing": printing} + if isinstance(instruction, (CustomGate, CustomControlledGate)) + else {} ) - if isinstance(instruction, Gate): - from mpqp.translation.utils import verify_convert_instructions + if ( + printing + and isinstance(instruction, CustomControlledGate) + and isinstance(instruction.non_controlled_gate, CustomGate) + ): + instr = [instruction] - instr = verify_convert_instructions( - instruction, authorized_gates, printing + elif isinstance(instruction, Gate): + qiskit_gate_set = get_qiskit_gate_set() + instr = list( + resolve_gate( + instruction, + qiskit_gate_set, + ) ) else: instr = [instruction] + for instruction in instr: qiskit_inst = instruction.to_other_language( Language.QISKIT, qiskit_parameters, **options diff --git a/mpqp/translation/utils.py b/mpqp/translation/utils.py deleted file mode 100644 index a978797ac..000000000 --- a/mpqp/translation/utils.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from mpqp.core.instruction.gates.gate import Gate - from mpqp.core.instruction.gates.native_gates import NativeGate - - -def verify_convert_instructions( - gate: "Gate", authorized_gates: set[type["NativeGate"]], printing: bool = False -) -> list["Gate"]: - """Function used to verify if the instruction is contained in the gate set. - If the gate is a ComposedGate and not explicitly in the authorized_gates set it will check if it's decomposition is present in the gate set. - """ - from mpqp.core.instruction.gates.custom_controlled_gate import CustomControlledGate - from mpqp.core.instruction.gates.custom_gate import CustomGate - - if len(authorized_gates) != 0: - if type(gate) not in authorized_gates: - raise ValueError( - f"The gate {type(gate)} are not in the set of authorized gates: f{authorized_gates}" - ) - else: - return [gate] - if ( - isinstance(gate, CustomControlledGate) - and isinstance(gate.non_controlled_gate, CustomGate) - and not printing - ): - # If the CustomControlledGate contains itself a custom gate it's better to return a bigger custom gate for compatibilities issues. - # If the non_controlled_gate is a NativeGate it shouldn't pose a problem. - # TODO: check how to decompose a ComposeGate that is inside a CCG, (example: a C-PRX) - return [gate.to_custom_gate()] - - return [gate] diff --git a/requirements-dev.txt b/requirements-dev.txt index c03d3cce2..241a7942c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,3 +11,4 @@ nbsphinx pyright==1.1.408 boto3-stubs black==26.3.1 +isort diff --git a/requirements_providers/myqlm.txt b/requirements_providers/myqlm.txt index eb3cff6fa..6a718756f 100644 --- a/requirements_providers/myqlm.txt +++ b/requirements_providers/myqlm.txt @@ -1,3 +1,2 @@ myqlm==1.12.4 -myqlm-interop -setuptools<82 \ No newline at end of file +myqlm-interop \ No newline at end of file diff --git a/tests/core/instruction/gates/test_composed_gate.py b/tests/core/instruction/gates/test_composed_gate.py new file mode 100644 index 000000000..70ae312c9 --- /dev/null +++ b/tests/core/instruction/gates/test_composed_gate.py @@ -0,0 +1,210 @@ +import numpy as np +import pytest + +from mpqp.core.circuit import QCircuit +from mpqp.core.instruction.gates.gate_decomposition import ( + resolve_composed_gate, + resolve_gate, +) +from mpqp.core.languages import Language +from mpqp.gates import * +from mpqp.tools.errors import UnsupportedGateError +from mpqp.tools.maths import matrix_eq + +COMPOSED_GATES = [ + Rxx(np.pi / 2, 0, 1), + Rzz(np.pi / 2, 0, 1), + Ryy(np.pi / 2, 0, 1), + PRX(np.pi / 3, 1, 0), +] + + +@pytest.mark.parametrize( + "gate ,language", + [ + pytest.param( + Rxx(np.pi / 2, 0, 1), + Language.QISKIT, + marks=pytest.mark.provider("qiskit"), + ), + pytest.param( + Ryy(np.pi / 2, 0, 1), + Language.QISKIT, + marks=pytest.mark.provider("qiskit"), + ), + pytest.param( + Rzz(np.pi / 2, 0, 1), + Language.BRAKET, + marks=pytest.mark.provider("braket"), + ), + pytest.param( + PRX(np.pi / 3, 1, 0), + Language.BRAKET, + marks=pytest.mark.provider("braket"), + ), + ], +) +def test_composedgate_compatible(gate: Gate, language: Language) -> None: + translated = QCircuit([gate]).to_other_language(language) + assert translated is not None + + +@pytest.mark.parametrize( + "gate,gate_set,expected", + [ + (PRX(1.0, 0.5, 0), {Rx, Rz}, [Rz, Rx, Rz]), + (Rzz(1.0, 0, 1), {CNOT, Rz}, [CNOT, Rz, CNOT]), + ], +) +def test_composed_gate_is_decomposed( + gate: Gate, + gate_set: set[type[Gate]], + expected: list[type[Gate]], +) -> None: + resolved = resolve_gate(gate, gate_set) + + assert [type(item) for item in resolved] == expected + + +@pytest.mark.parametrize( + "language, provider, gate_set_getter, gate, native_gates", + [ + pytest.param( + Language.QISKIT, + "qiskit", + "get_qiskit_gate_set", + Rxx(np.pi / 2, 0, 1), + {Rx}, + marks=pytest.mark.provider("qiskit"), + ), + pytest.param( + Language.QISKIT, + "qiskit", + "get_qiskit_gate_set", + Ryy(np.pi / 2, 0, 1), + {Rx, Rz}, + marks=pytest.mark.provider("qiskit"), + ), + pytest.param( + Language.BRAKET, + "braket", + "get_braket_gate_set", + Rzz(np.pi / 2, 0, 1), + {Rz}, + marks=pytest.mark.provider("braket"), + ), + pytest.param( + Language.CIRQ, + "cirq", + "get_cirq_gate_set", + PRX(np.pi / 3, 1, 0), + {Rx}, + marks=pytest.mark.provider("cirq"), + ), + ], +) +def test_composedgate_not_compatible_with_provider( + monkeypatch: pytest.MonkeyPatch, + language: Language, + provider: str, + gate_set_getter: str, + gate: Gate, + native_gates: set[type[Gate]], +) -> None: + monkeypatch.setattr( + f"mpqp.translation.{provider}.{gate_set_getter}", + lambda: native_gates, + ) + + with pytest.raises(ValueError): + QCircuit([gate]).to_other_language(language) + + +def define_parameters(): + provider_by_language = { + Language.QISKIT: "qiskit", + Language.BRAKET: "braket", + Language.CIRQ: "cirq", + Language.MY_QLM: "myqlm", + } + return [ + pytest.param( + gate, + language, + marks=( + pytest.mark.provider(provider_by_language[language]) + if language in provider_by_language + else () + ), + ) + for gate in COMPOSED_GATES + for language in [ + Language.QISKIT, + Language.BRAKET, + Language.CIRQ, + Language.MY_QLM, + Language.QASM2, + Language.QASM3, + ] + ] + + +@pytest.mark.parametrize( + "gate, language", + define_parameters(), +) +def test_composedgate_translation_no_decomposition(gate: Gate, language: Language): + c = QCircuit() + c.add(gate) + translated = c.to_other_language(language) + c_re = QCircuit().from_other_language(translated) + assert matrix_eq(c_re.to_matrix(), c.to_matrix()) + + +@pytest.mark.parametrize( + "gate, language", + define_parameters(), +) +def test_composedgate_translation_decomposition(gate: Gate, language: Language): + c = QCircuit() + c.add(gate) + translated = c.to_other_language(language) + c_re = QCircuit().from_other_language(translated) + assert matrix_eq(c_re.to_matrix(), c.to_matrix(), 1e-5, 1e-5) + + +@pytest.mark.parametrize( + "gate", + COMPOSED_GATES, +) +def test_composedgates_decomposition(gate: ComposedGate): + c = QCircuit(gate.decompose()) + assert matrix_eq(c.to_matrix(), gate.to_matrix()) + + +@pytest.mark.parametrize( + "gate, qasm", + [ + ( + PRX(1.0, 0.5, 2) + .to_other_language(Language.QASM2) + .splitlines(), # pyright: ignore[reportAttributeAccessIssue] + [ + "rz(-0.5) q[2];", + "rx(1.0) q[2];", + "rz(0.5) q[2];", + ], + ) + ], +) +def test_prx_qasm2_translation_matches_decomposition(gate: Gate, qasm: list[str]): + assert gate == qasm + + +@pytest.mark.parametrize("gate", [Rxx(np.pi / 2, 0, 1)]) +def test_composedgates_error(gate: Gate): + with pytest.raises( + UnsupportedGateError, + match=r"Rxx cannot be represented.*Missing gates: CNOT", + ): + resolve_composed_gate(gate, {Rx}) diff --git a/tests/core/instruction/gates/test_controlled_gate.py b/tests/core/instruction/gates/test_controlled_gate.py index 3ac0176bb..498c03473 100644 --- a/tests/core/instruction/gates/test_controlled_gate.py +++ b/tests/core/instruction/gates/test_controlled_gate.py @@ -29,3 +29,4 @@ ) def test_gate_repr(gate: Gate, expected_repr: str) -> None: assert repr(gate) == expected_repr + assert eval(repr(gate)) == gate diff --git a/tests/core/instruction/gates/test_custom_controlled_gate.py b/tests/core/instruction/gates/test_custom_controlled_gate.py index 2354dfa28..85f83b828 100644 --- a/tests/core/instruction/gates/test_custom_controlled_gate.py +++ b/tests/core/instruction/gates/test_custom_controlled_gate.py @@ -4,7 +4,30 @@ import pytest from numpy import array # pyright: ignore[reportUnusedImport] +from mpqp.core.circuit import QCircuit +from mpqp.core.languages import Language from mpqp.gates import * +from mpqp.tools.maths import closest_unitary, matrix_eq, rand_unitary_matrix + + +def all_cases_controlled_gates(): + return [ + CustomControlledGate([0, 2], X(1)), + CustomControlledGate([0, 1], X(2)), + CustomControlledGate([1, 2], X(0)), + CustomControlledGate( + [1], CustomGate(closest_unitary(rand_unitary_matrix(4)), [0, 2]) + ), + CustomControlledGate( + [2], CustomGate(closest_unitary(rand_unitary_matrix(4)), [0, 1]) + ), + CustomControlledGate( + [0], CustomGate(closest_unitary(rand_unitary_matrix(4)), [1, 2]) + ), + CustomControlledGate( + [1], CustomGate(closest_unitary(rand_unitary_matrix(8)), [0, 2, 3]) + ), + ] @pytest.mark.parametrize( @@ -56,3 +79,74 @@ def test_negative_indices(gate: Type[Gate], args: tuple[Any]): ) def test_inverse(gate: CustomControlledGate, expected: CustomControlledGate): assert gate.inverse() == expected + + +def test_print_keeps_custom_gate_controls_visible(): + circuit = QCircuit( + [CustomControlledGate([0, 2], CustomGate(np.diag([1, -1]), [1]))] + ) + + drawing = str(circuit) + + assert drawing.count("■") == 2 + assert "Unitary" in drawing + + +@pytest.mark.provider("qiskit") +@pytest.mark.parametrize( + "gate", + all_cases_controlled_gates(), +) +def test_translation_customcontrolledgate_qiskit(gate: CustomControlledGate): + c = QCircuit([gate]) + c_qiskit = c.to_other_language(Language.QISKIT) + c_translated = QCircuit().from_other_language(c_qiskit) + assert matrix_eq(c.to_matrix(), c_translated.to_matrix(), atol=1e10, rtol=1e10) + + +@pytest.mark.provider("cirq") +@pytest.mark.parametrize( + "gate", + all_cases_controlled_gates(), +) +def test_translation_customcontrolledgate_cirq(gate: CustomControlledGate): + c = QCircuit([gate]) + c_cirq = c.to_other_language(Language.CIRQ) + c_translated = QCircuit().from_other_language(c_cirq) + assert matrix_eq(c.to_matrix(), c_translated.to_matrix(), atol=1e10, rtol=1e10) + + +@pytest.mark.provider("braket") +@pytest.mark.parametrize( + "gate", + all_cases_controlled_gates(), +) +def test_translation_customcontrolledgate_braket(gate: CustomControlledGate): + c = QCircuit([gate]) + c_braket = c.to_other_language(Language.BRAKET) + c_translated = QCircuit().from_other_language(c_braket) + assert matrix_eq(c.to_matrix(), c_translated.to_matrix(), atol=1e10, rtol=1e10) + + +@pytest.mark.provider("qasm3") +@pytest.mark.parametrize( + "gate", + all_cases_controlled_gates(), +) +def test_translation_customcontrolledgate_qasm3(gate: CustomControlledGate): + c = QCircuit([gate]) + c_qasm3 = c.to_other_language(Language.QASM3) + c_translated = QCircuit().from_other_language(c_qasm3) + assert matrix_eq(c.to_matrix(), c_translated.to_matrix(), atol=1e10, rtol=1e10) + + +@pytest.mark.provider("qasm2") +@pytest.mark.parametrize( + "gate", + all_cases_controlled_gates(), +) +def test_translation_customcontrolledgate_qasm2(gate: CustomControlledGate): + c = QCircuit([gate]) + c_qasm2 = c.to_other_language(Language.QASM2) + c_translated = QCircuit().from_other_language(c_qasm2) + assert matrix_eq(c.to_matrix(), c_translated.to_matrix()) diff --git a/tests/core/instruction/gates/test_custom_gate.py b/tests/core/instruction/gates/test_custom_gate.py index 06fb35d03..bec98340d 100644 --- a/tests/core/instruction/gates/test_custom_gate.py +++ b/tests/core/instruction/gates/test_custom_gate.py @@ -150,11 +150,11 @@ def exec_custom_gate_with_random_circuit( result1 = run(random_circ, device) result2 = run(custom_gate_circ, device) - assert isinstance(result1, Result) - assert isinstance(result2, Result) # precision reduced from approximation errors (CustomGate usage) tolerance = 1e-2 if device == ATOSDevice.MYQLM_PYLINALG and circ_size >= 4 else 1e-4 - assert matrix_eq(result1.amplitudes, result2.amplitudes, tolerance, tolerance) + density_matrix1 = np.outer(result1.amplitudes, result1.amplitudes.conjugate()) + density_matrix2 = np.outer(result2.amplitudes, result2.amplitudes.conjugate()) + assert matrix_eq(density_matrix1, density_matrix2, tolerance, tolerance) def _test_matrix_equality( diff --git a/tests/core/instruction/gates/test_native_gates.py b/tests/core/instruction/gates/test_native_gates.py index 8f24355c6..f87af478d 100644 --- a/tests/core/instruction/gates/test_native_gates.py +++ b/tests/core/instruction/gates/test_native_gates.py @@ -7,14 +7,17 @@ from mpqp.tools.maths import cos, exp, matrix_eq, sin theta: Expr +phi: Expr k: Expr -theta, k = symbols("θ k") +theta, phi, k = symbols("θ φ k") + c, s, e = cos(theta), sin(theta), exp(1.0 * I * theta) c2, s2, e2 = ( cos(theta / 2), sin(theta / 2), exp(1.0 * I * theta / 2), ) +e_phi = exp(1.0 * I * phi) @pytest.mark.parametrize( @@ -116,6 +119,115 @@ def test_Rz(angle: float, result_matrix: Matrix): assert matrix_eq(Rz(angle, 0).to_matrix(), result_matrix) +@pytest.mark.parametrize( + "angle, result_matrix", + [ + (0, np.eye(4)), + ( + np.pi, + np.array( + [ + [0, 0, 0, -1j], + [0, 0, -1j, 0], + [0, -1j, 0, 0], + [-1j, 0, 0, 0], + ] + ), + ), + ( + theta, + np.array( + [ + [c2, 0, 0, -1j * s2], + [0, c2, -1j * s2, 0], + [0, -1j * s2, c2, 0], + [-1j * s2, 0, 0, c2], + ] + ), + ), + ], +) +def test_Rxx(angle: float, result_matrix: Matrix): + assert matrix_eq(Rxx(angle, 0, 1).to_matrix(), result_matrix) + + +@pytest.mark.parametrize( + "angle, result_matrix", + [ + (0, np.eye(4)), + ( + np.pi, + np.array( + [ + [0, 0, 0, 1j], + [0, 0, -1j, 0], + [0, -1j, 0, 0], + [1j, 0, 0, 0], + ] + ), + ), + ( + theta, + np.array( + [ + [c2, 0, 0, 1j * s2], + [0, c2, -1j * s2, 0], + [0, -1j * s2, c2, 0], + [1j * s2, 0, 0, c2], + ] + ), + ), + ], +) +def test_Ryy(angle: float, result_matrix: Matrix): + assert matrix_eq(Ryy(angle, 0, 1).to_matrix(), result_matrix) + + +@pytest.mark.parametrize( + "angle, result_matrix", + [ + (0, np.eye(4)), + (np.pi, np.diag([-1j, 1j, 1j, -1j])), + ( + np.pi / 3, + np.diag( + [ + np.exp(-1j * np.pi / 6), + np.exp(1j * np.pi / 6), + np.exp(1j * np.pi / 6), + np.exp(-1j * np.pi / 6), + ] + ), + ), + (theta, np.diag([1 / e2, e2, e2, 1 / e2])), # pyright: ignore + ], +) +def test_Rzz(angle: float, result_matrix: Matrix): + assert matrix_eq(Rzz(angle, 0, 1).to_matrix(), result_matrix) + + +@pytest.mark.parametrize( + "theta, phi, result_matrix", + [ + (0, 0, np.eye(2)), + (np.pi, 0, np.array([[0, -1j], [-1j, 0]])), + (np.pi, np.pi / 2, np.array([[0, -1], [1, 0]])), + ( + theta, + phi, + np.array( + [ + [c2, -1j * s2 / e_phi], + [-1j * s2 * e_phi, c2], + ] + ), + ), + ], +) +def test_PRX(theta: float, phi: float, result_matrix: Matrix): + assert matrix_eq(PRX(theta, phi, 0).to_matrix(), result_matrix) + + @pytest.mark.parametrize( "angle_bin_pow, result_matrix", [ diff --git a/tests/core/instruction/measurement/test_basis_measure.py b/tests/core/instruction/measurement/test_basis_measure.py index 8831c6c35..54bc6321a 100644 --- a/tests/core/instruction/measurement/test_basis_measure.py +++ b/tests/core/instruction/measurement/test_basis_measure.py @@ -1,14 +1,14 @@ import pytest from mpqp import ( - BasisMeasure, - ComputationalBasis, - run, - QCircuit, - IBMDevice, ATOSDevice, AWSDevice, + BasisMeasure, + ComputationalBasis, GOOGLEDevice, + IBMDevice, + QCircuit, + run, ) from mpqp.core.instruction.gates.native_gates import X from mpqp.execution.devices import AvailableDevice diff --git a/tests/core/instruction/test_breakpoint.py b/tests/core/instruction/test_breakpoint.py index bd2fc3381..06b6ebfb3 100644 --- a/tests/core/instruction/test_breakpoint.py +++ b/tests/core/instruction/test_breakpoint.py @@ -3,11 +3,11 @@ from mpqp import CNOT, ATOSDevice, Breakpoint, H, QCircuit, Y, run from mpqp.execution.devices import ( - AvailableDevice, - IBMDevice, - GOOGLEDevice, ATOSDevice, + AvailableDevice, AWSDevice, + GOOGLEDevice, + IBMDevice, ) list_circuit_expected_out = [ @@ -41,8 +41,7 @@ q_0: ┤ H ├┤ Y ├ └───┘└───┘ q_1: ────────── - -""", +""" + " \n", ), ( QCircuit([H(0), CNOT(0, 1), Breakpoint(enabled=False), Y(1)]), diff --git a/tests/core/test_circuit.py b/tests/core/test_circuit.py index 1088a37d4..1b25a7351 100644 --- a/tests/core/test_circuit.py +++ b/tests/core/test_circuit.py @@ -762,8 +762,8 @@ def test_from_cirq(list_random_cirq_circuit: list[cirq_Circuit]): "circuit", [ QCircuit([H(0), CNOT(0, 1)]), - random_circuit(None, 2), - random_circuit(None, 10), + random_circuit(None, 2, use_all_qubits=True), + random_circuit(None, 10, use_all_qubits=True), ], ) def test_from_braket(circuit: QCircuit): diff --git a/tests/examples/test_demonstrations.py b/tests/examples/test_demonstrations.py index 4f93455a7..c284962b4 100644 --- a/tests/examples/test_demonstrations.py +++ b/tests/examples/test_demonstrations.py @@ -1,13 +1,14 @@ from typing import Any, Callable + import numpy as np import pytest from mpqp import ( ATOSDevice, AWSDevice, - GOOGLEDevice, BasisMeasure, ExpectationMeasure, + GOOGLEDevice, IBMDevice, Language, Observable, @@ -16,8 +17,8 @@ ) from mpqp.execution.devices import AvailableDevice from mpqp.gates import * -from mpqp.translation.qasm.qasm_to_braket import qasm3_to_braket_Circuit from mpqp.tools.errors import UnsupportedBraketFeaturesWarning +from mpqp.translation.qasm.qasm_to_braket import qasm3_to_braket_Circuit # TODO: add CIRQ local simulator devices to this file @@ -36,9 +37,7 @@ def test_sample_demo_qiskit(): [ IBMDevice.AER_SIMULATOR, IBMDevice.AER_SIMULATOR_MATRIX_PRODUCT_STATE, - # IBMDevice.AER_SIMULATOR_EXTENDED_STABILIZER, IBMDevice.AER_SIMULATOR_STATEVECTOR, - # IBMDevice.AER_SIMULATOR_STABILIZER, IBMDevice.AER_SIMULATOR_DENSITY_MATRIX, ], ) @@ -113,14 +112,18 @@ def test_sample_demo_aer_stabilizers(): circuit.add(BasisMeasure([0, 1, 2, 3], shots=2000)) # Run the circuit on a selected device - run( - circuit, - [ - IBMDevice.AER_SIMULATOR, - IBMDevice.AER_SIMULATOR_EXTENDED_STABILIZER, - IBMDevice.AER_SIMULATOR_STABILIZER, - ], - ) + with pytest.warns( + UserWarning, + match=r"For IBMDevice\.AER_SIMULATOR_(?:EXTENDED_)?STABILIZER", + ): + run( + circuit, + [ + IBMDevice.AER_SIMULATOR, + IBMDevice.AER_SIMULATOR_EXTENDED_STABILIZER, + IBMDevice.AER_SIMULATOR_STABILIZER, + ], + ) assert True diff --git a/tests/execution/providers/test_google.py b/tests/execution/providers/test_google.py index a3212f7fb..8532da822 100644 --- a/tests/execution/providers/test_google.py +++ b/tests/execution/providers/test_google.py @@ -1,4 +1,5 @@ from typing import TYPE_CHECKING + import numpy as np import pytest diff --git a/tests/execution/test_devices.py b/tests/execution/test_devices.py index 60397e01f..f0cca3cb1 100644 --- a/tests/execution/test_devices.py +++ b/tests/execution/test_devices.py @@ -1,6 +1,8 @@ from unittest.mock import patch -from mpqp import AWSDevice +import pytest + +from mpqp import CZ, PRX, AWSDevice, QCircuit, Rxx, Ryy, Rzz # TODO: test methods @@ -19,3 +21,43 @@ def test_get_arn(): AWSDevice.RIGETTI_ANKAA_3.get_arn() == "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-3" ) + + +def test_iqm_native_gates(): + assert AWSDevice.IQM_GARNET.compatible_gates(native_set=True) == {CZ, PRX} + assert AWSDevice.IQM_EMERALD.compatible_gates(native_set=True) == {CZ, PRX} + + +@pytest.mark.provider("braket") +def test_iqm_translation_preserves_qubit_indices(): + circuit = QCircuit([PRX(0.1, 0.2, 0), CZ(0, 2)]) + + translated = circuit.to_other_device(AWSDevice.IQM_GARNET) + + assert circuit.instructions[0].targets == [0] + controlled_gate = circuit.instructions[1] + assert isinstance(controlled_gate, CZ) + assert controlled_gate.controls == [0] + assert controlled_gate.targets == [2] + assert {int(qubit) for qubit in translated.qubits} == {0, 2} + + +@pytest.mark.provider("braket") +def test_iqm_translation_preserves_supported_rotation_gates(): + circuit = QCircuit( + [ + Rxx(0.1, 0, 1), + Ryy(0.2, 0, 1), + Rzz(0.3, 0, 1), + PRX(0.4, 0.5, 0), + ] + ) + + translated = circuit.to_other_device(AWSDevice.IQM_GARNET) + + assert [instruction.operator.name for instruction in translated.instructions] == [ + "XX", + "YY", + "ZZ", + "PRx", + ] diff --git a/tests/execution/test_result.py b/tests/execution/test_result.py index 947815a13..8a2b6948f 100644 --- a/tests/execution/test_result.py +++ b/tests/execution/test_result.py @@ -178,7 +178,14 @@ def test_result_str(result: Result, expected_string: str): @pytest.mark.provider("qiskit") @pytest.mark.parametrize("device", sampling_devices_qiskit) def test_sample_nb_shot_handle_qiskit(device: AvailableDevice): - exec_sample_nb_shot_handle(device) + if device in { + IBMDevice.AER_SIMULATOR_STABILIZER, + IBMDevice.AER_SIMULATOR_EXTENDED_STABILIZER, + }: + with pytest.warns(UserWarning, match=rf"For {device}"): + exec_sample_nb_shot_handle(device) + else: + exec_sample_nb_shot_handle(device) @pytest.mark.provider("braket") diff --git a/tests/execution/test_runner.py b/tests/execution/test_runner.py index 859f85693..b00169724 100644 --- a/tests/execution/test_runner.py +++ b/tests/execution/test_runner.py @@ -4,6 +4,7 @@ from mpqp import ExpectationMeasure, H, Observable, QCircuit, Rx, pI, pX, pY, pZ from mpqp.core.instruction.measurement import PauliString from mpqp.execution import adjust_measure +from mpqp.tools.errors import NumberQubitsError from mpqp.tools.maths import matrix_eq @@ -61,37 +62,51 @@ def test_adjust_measure_target_order( circuit_size: int, expected_observable: PauliString, ): - measure = ExpectationMeasure(Observable(observable), measure_targets) + with pytest.warns( + UserWarning, + match=( + r"^Non contiguous or non sorted observable target will introduce " + r"additional CNOT/SWAP gates\.$" + ), + ): + measure = ExpectationMeasure(Observable(observable), measure_targets) - adjusted_measure = adjust_measure(measure, QCircuit(circuit_size)) + adjusted_measure = adjust_measure(measure, QCircuit(circuit_size)) - assert adjusted_measure.targets == list(range(circuit_size)) - assert matrix_eq( - adjusted_measure.observables[0].matrix, - expected_observable.to_matrix(), - ) + assert adjusted_measure.targets == list(range(circuit_size)) + assert matrix_eq( + adjusted_measure.observables[0].matrix, + expected_observable.to_matrix(), + ) def test_adjust_measure_matrix_reordering(): observable = Observable((pX @ pY @ pZ).to_matrix()) - measure = ExpectationMeasure( - observable, - targets=[1, 2, 0], - optimize_measurement=False, - ) - original_matrix = observable.matrix + with pytest.warns( + UserWarning, + match=( + r"^Non contiguous or non sorted observable target will introduce " + r"additional CNOT/SWAP gates\.$" + ), + ): + measure = ExpectationMeasure( + observable, + targets=[1, 2, 0], + optimize_measurement=False, + ) + original_matrix = observable.matrix - adjusted_measure = adjust_measure(measure, QCircuit(3)) + adjusted_measure = adjust_measure(measure, QCircuit(3)) - assert matrix_eq( - adjusted_measure.observables[0].matrix, - (pZ @ pX @ pY).to_matrix(), - ) - assert matrix_eq(measure.observables[0].matrix, original_matrix) + assert matrix_eq( + adjusted_measure.observables[0].matrix, + (pZ @ pX @ pY).to_matrix(), + ) + assert matrix_eq(measure.observables[0].matrix, original_matrix) def test_adjust_measure_targets_mismatch(): - measure = ExpectationMeasure(Observable(pX), targets=[0, 1]) - - with pytest.raises(ValueError, match="Each observable must act on 2 qubits"): - adjust_measure(measure, QCircuit(2)) + with pytest.raises( + NumberQubitsError, match="Target size 2 doesn't match observable size 1" + ): + ExpectationMeasure(Observable(pX), targets=[0, 1]) diff --git a/tests/execution/test_simulated_devices.py b/tests/execution/test_simulated_devices.py index 22e3ea617..0a195161e 100644 --- a/tests/execution/test_simulated_devices.py +++ b/tests/execution/test_simulated_devices.py @@ -54,6 +54,9 @@ def list_ibm_simulated_device() -> list[tuple[QCircuit, StaticIBMSimulatedDevice @pytest.mark.provider("qiskit") +@pytest.mark.filterwarnings( + "ignore:Properties of fake_nighthawk are not intended.*:UserWarning" +) def running_sample_job_ibm_simulated_devices( list_ibm_simulated_device: list[tuple[QCircuit, StaticIBMSimulatedDevice]], ): diff --git a/tests/execution/test_validity.py b/tests/execution/test_validity.py index e1761cbd4..23bda853a 100644 --- a/tests/execution/test_validity.py +++ b/tests/execution/test_validity.py @@ -41,6 +41,7 @@ from mpqp.tools.circuit import random_gate, random_noise from mpqp.tools.errors import ( DeviceJobIncompatibleError, + UnsupportedGateError, ) from mpqp.tools.maths import matrix_eq, rand_unitary_matrix @@ -599,7 +600,26 @@ def circuits_type(): def test_validity_run_job_type_qiskit( device: AvailableDevice, circuits_type: list[QCircuit] ): - exec_validity_run_job_type(device, circuits_type) + if device in { + IBMDevice.AER_SIMULATOR_STABILIZER, + IBMDevice.AER_SIMULATOR_EXTENDED_STABILIZER, + }: + with pytest.warns(UserWarning, match=rf"For {device}"): + exec_validity_run_job_type(device, circuits_type) + else: + exec_validity_run_job_type(device, circuits_type) + + +@pytest.mark.provider("qiskit") +def test_unsupported_non_composed_gate_is_rejected_before_translation(): + circuit = QCircuit([T(0)]) + + with pytest.warns(UserWarning, match=r"AER_SIMULATOR_STABILIZER"): + with pytest.raises( + UnsupportedGateError, + match=r"T cannot be represented with the target gate set", + ): + circuit.to_other_device(IBMDevice.AER_SIMULATOR_STABILIZER) @pytest.mark.provider("cirq") @@ -729,7 +749,15 @@ def exec_validity_native_gate_to_other_language(language: Language): with pytest.raises(NotImplementedError): gate_build.to_other_language(language) else: - assert gate_build.to_other_language(language) is not None + if isinstance(gate_build, ComposedGate): + assert all( + [ + gate.to_other_language(language) is not None + for gate in gate_build.decompose() + ] + ) + else: + assert gate_build.to_other_language(language) is not None @pytest.fixture @@ -1097,8 +1125,8 @@ def test_validity_optim_ideal_multi_diag_obs_and_regular_run( ], ) def test_global_phase_statevector(matrix: Matrix, gphase: float): - from math import log2 from itertools import pairwise + from math import log2 circuit = QCircuit([CustomGate(matrix, list(range(int(log2(len(matrix))))))]) circuit.input_g_phase = gphase diff --git a/tests/qasm/test_mpqp_to_qasm.py b/tests/qasm/test_mpqp_to_qasm.py index efb951048..1a96105d1 100644 --- a/tests/qasm/test_mpqp_to_qasm.py +++ b/tests/qasm/test_mpqp_to_qasm.py @@ -3,9 +3,8 @@ from mpqp import Barrier, BasisMeasure, Instruction, Language, QCircuit from mpqp.gates import * -from mpqp.translation.qasm.mpqp_to_qasm import mpqp_to_qasm2 from mpqp.tools.circuit import random_circuit -from mpqp.tools.display import format_element_str +from mpqp.translation.qasm.mpqp_to_qasm import mpqp_to_qasm2 @pytest.mark.parametrize( @@ -492,35 +491,17 @@ def test_mpqp_to_qasm_simplify(instructions: list[Instruction], qasm_expectation assert qasm_expectation == qasm -def normalize_string(string: str): - import re - from typing import Match - - def simplify_expression(match: Match[str]): - from numpy import e, pi - - gate = match.group(1) - if gate == 'u': - gate = 'u3' - components = match.group(2).split(',') - simplified = [ - format_element_str(eval(comp, {"pi": pi, "e": e}), 4) for comp in components - ] - return f"{gate}({','.join(simplified)})" - - pattern = r'([a-zA-Z]*)\(([^()]+)\)' - return re.sub(pattern, simplify_expression, string) - - def test_random_mpqp_to_qasm(): for _ in range(15): qcircuit = random_circuit(nb_qubits=6, nb_gates=20) from qiskit import QuantumCircuit, qasm2 + from qiskit.quantum_info import Operator qiskit_circuit = qcircuit.to_other_language(Language.QISKIT) assert isinstance(qiskit_circuit, QuantumCircuit) - qiskit_qasm = normalize_string(qasm2.dumps(qiskit_circuit)) mpqp_qasm = qcircuit.to_other_language(Language.QASM2) assert isinstance(mpqp_qasm, str) - mpqp_qasm = normalize_string(mpqp_qasm) - assert qiskit_qasm == mpqp_qasm + qasm_circuit = qasm2.loads( + mpqp_qasm, custom_instructions=qasm2.LEGACY_CUSTOM_INSTRUCTIONS + ) + assert Operator(qiskit_circuit).equiv(Operator(qasm_circuit)) diff --git a/tests/qasm/test_open_qasm_2_and_3.py b/tests/qasm/test_open_qasm_2_and_3.py index 2cd523a2b..062cf9cb2 100644 --- a/tests/qasm/test_open_qasm_2_and_3.py +++ b/tests/qasm/test_open_qasm_2_and_3.py @@ -8,6 +8,8 @@ from mpqp import CNOT, H, IBMDevice, Instruction, QCircuit, Result, U, run from mpqp.execution.devices import IBMDevice +from mpqp.tools.errors import OpenQASMTranslationWarning +from mpqp.tools.theoretical_simulation import amplitude from mpqp.translation.qasm.open_qasm_2_and_3 import ( open_qasm_2_to_3, open_qasm_3_to_2, @@ -18,7 +20,6 @@ remove_user_gates, ) from mpqp.translation.qasm.qasm_to_mpqp import qasm2_parse -from mpqp.tools.theoretical_simulation import amplitude qasm_folder = "tests/qasm/qasm_examples/" @@ -265,7 +266,14 @@ def test_conversion_2_and_3(qasm_code: str): ], ) def test_conversion_2_to_3(qasm_code: str, expected_output: str): - convert = open_qasm_2_to_3(qasm_code) + if re.search(r"\bu\s*\(", qasm_code): + with pytest.warns( + OpenQASMTranslationWarning, + match=r"There is a phase.*difference between U", + ): + convert = open_qasm_2_to_3(qasm_code) + else: + convert = open_qasm_2_to_3(qasm_code) assert normalize_whitespace(convert) == normalize_whitespace(expected_output) diff --git a/tests/qasm/test_qasm_to_braket.py b/tests/qasm/test_qasm_to_braket.py index 52dd0b164..d038bdb39 100644 --- a/tests/qasm/test_qasm_to_braket.py +++ b/tests/qasm/test_qasm_to_braket.py @@ -1,11 +1,12 @@ -import pytest from typing import TYPE_CHECKING +import pytest + if TYPE_CHECKING: from braket.circuits import Operator -from mpqp.translation.qasm.qasm_to_braket import qasm3_to_braket_Circuit from mpqp.tools import UnsupportedBraketFeaturesWarning +from mpqp.translation.qasm.qasm_to_braket import qasm3_to_braket_Circuit @pytest.mark.provider("braket") diff --git a/tests/qasm/test_qasm_to_mpqp.py b/tests/qasm/test_qasm_to_mpqp.py index 6c8e6a9a3..45f9adb34 100644 --- a/tests/qasm/test_qasm_to_mpqp.py +++ b/tests/qasm/test_qasm_to_mpqp.py @@ -1,10 +1,11 @@ from typing import TYPE_CHECKING +import numpy as np import pytest -from mpqp import CNOT, CP, BasisMeasure, H, Language -from mpqp.translation.qasm.qasm_to_mpqp import qasm2_parse +from mpqp import CNOT, CP, BasisMeasure, H, Language, U from mpqp.tools.circuit import random_circuit +from mpqp.translation.qasm.qasm_to_mpqp import qasm2_parse @pytest.mark.parametrize( @@ -198,3 +199,21 @@ def test_random_qasm_code(): if TYPE_CHECKING: assert isinstance(qasm_code, str) assert qcircuit.is_equivalent(qasm2_parse(qasm_code)) + + +@pytest.mark.parametrize( + "instruction, expected_gate", + [ + ("u1(pi/4) q[0];", U(0, 0, np.pi / 4, 0)), + ("u2(pi/3,pi/4) q[0];", U(np.pi / 2, np.pi / 3, np.pi / 4, 0)), + ("u3(pi/2,pi/3,pi/4) q[0];", U(np.pi / 2, np.pi / 3, np.pi / 4, 0)), + ("U(pi/2,pi/3,pi/4) q[0];", U(np.pi / 2, np.pi / 3, np.pi / 4, 0)), + ], +) +def test_standard_u_gates(instruction: str, expected_gate: U): + circuit = qasm2_parse(f'''OPENQASM 2.0; + include "qelib1.inc"; + qreg q[1]; + {instruction}''') + + assert circuit.instructions == [expected_gate] diff --git a/tests/test_doc.py b/tests/test_doc.py index 0f5df433d..93a231c39 100644 --- a/tests/test_doc.py +++ b/tests/test_doc.py @@ -17,6 +17,11 @@ from sympy import symbols from mpqp import * +from mpqp.core.instruction.gates.gate_decomposition import ( + resolve_composed_gate, + resolve_gate, + resolve_instructions, +) from mpqp.core.instruction.measurement import PauliString, pauli_string from mpqp.environment.env_manager import ( _create_config_if_needed, # pyright: ignore[reportPrivateUsage] @@ -76,34 +81,6 @@ from mpqp.local_storage.setup import setup_local_storage from mpqp.measures import PauliString, pI, pX, pY, pZ from mpqp.noise.noise_model import _plural_marker # pyright: ignore[reportPrivateUsage] -from mpqp.translation.qasm import ( - qasm2_to_cirq_Circuit, - qasm2_to_myqlm_Circuit, - qasm2_to_Qiskit_Circuit, - qasm3_to_braket_Program, -) -from mpqp.translation.qasm.mpqp_to_qasm import mpqp_to_qasm2 -from mpqp.translation.qasm.myqlm_to_mpqp import from_myqlm_to_mpqp -from mpqp.translation.qasm.open_qasm_2_and_3 import ( - convert_instruction_3_to_2, - open_qasm_2_to_3, - open_qasm_3_to_2, - open_qasm_file_conversion_2_to_3, - open_qasm_file_conversion_3_to_2, - open_qasm_hard_includes, - parse_user_gates, - remove_include_and_comment, - remove_user_gates, -) -from mpqp.translation.qasm.qasm_to_braket import ( - braket_custom_gates_to_mpqp, - braket_noise_to_mpqp, - qasm3_to_braket_Circuit, -) -from mpqp.translation.braket import * -from mpqp.translation.qiskit import * -from mpqp.translation import * -from mpqp.translation.qasm.qasm_to_mpqp import qasm2_parse from mpqp.tools.circuit import ( random_circuit, random_gate, @@ -138,10 +115,41 @@ rand_unitary_2x2_matrix, rand_unitary_matrix, rearrange_matrix, + rotation_denominator, + symbolic_divide, + symbolic_product, ) from mpqp.tools.operators import * from mpqp.tools.pauli_grouping import CommutingTypes, pauli_grouping_greedy from mpqp.tools.unitary_decomposition import quantum_shannon_decomposition +from mpqp.translation import * +from mpqp.translation.braket import * +from mpqp.translation.qasm import ( + qasm2_to_cirq_Circuit, + qasm2_to_myqlm_Circuit, + qasm2_to_Qiskit_Circuit, + qasm3_to_braket_Program, +) +from mpqp.translation.qasm.mpqp_to_qasm import mpqp_to_qasm2 +from mpqp.translation.qasm.myqlm_to_mpqp import from_myqlm_to_mpqp +from mpqp.translation.qasm.open_qasm_2_and_3 import ( + convert_instruction_3_to_2, + open_qasm_2_to_3, + open_qasm_3_to_2, + open_qasm_file_conversion_2_to_3, + open_qasm_file_conversion_3_to_2, + open_qasm_hard_includes, + parse_user_gates, + remove_include_and_comment, + remove_user_gates, +) +from mpqp.translation.qasm.qasm_to_braket import ( + braket_custom_gates_to_mpqp, + braket_noise_to_mpqp, + qasm3_to_braket_Circuit, +) +from mpqp.translation.qasm.qasm_to_mpqp import qasm2_parse +from mpqp.translation.qiskit import * theta, k = symbols("θ k") obs = Observable(np.array([[0, 1], [1, 0]]))