diff --git a/mpqp/__init__.py b/mpqp/__init__.py index fe95468e..aa3c410f 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 52c9d116..c042aa9c 100644 --- a/mpqp/core/circuit.py +++ b/mpqp/core/circuit.py @@ -44,9 +44,10 @@ from mpqp.core.instruction import Instruction from mpqp.core.instruction.barrier import Barrier from mpqp.core.instruction.breakpoint import Breakpoint -from mpqp.core.instruction.gates import ControlledGate, CRk, Gate, Id +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 ComposedGate 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 @@ -959,7 +960,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 @@ -1128,6 +1129,7 @@ def to_other_language( language: Literal[Language.QASM2, Language.QASM3], skip_pre_measure: bool = False, skip_measurements: bool = False, + authorized_gates: set[type[Gate]] = set(), printing: bool = False, ) -> str: ... @@ -1137,6 +1139,7 @@ def to_other_language( language: Literal[Language.CIRQ], skip_pre_measure: bool = False, skip_measurements: bool = False, + authorized_gates: set[type[Gate]] = set(), printing: bool = False, ) -> cirq_Circuit: ... @@ -1146,6 +1149,7 @@ def to_other_language( language: Literal[Language.BRAKET], skip_pre_measure: bool = False, skip_measurements: bool = False, + authorized_gates: set[type[Gate]] = set(), printing: bool = False, ) -> braket_Circuit: ... @overload @@ -1154,6 +1158,7 @@ def to_other_language( language: Literal[Language.MY_QLM], skip_pre_measure: bool = False, skip_measurements: bool = False, + authorized_gates: set[type[Gate]] = set(), printing: bool = False, ) -> myQLM_Circuit: ... @@ -1163,6 +1168,7 @@ def to_other_language( language: Literal[Language.QISKIT], skip_pre_measure: bool = False, skip_measurements: bool = False, + authorized_gates: set[type[Gate]] = set(), printing: bool = False, ) -> QuantumCircuit: ... @@ -1172,6 +1178,7 @@ def to_other_language( language: Language, skip_pre_measure: bool = False, skip_measurements: bool = False, + authorized_gates: set[type[Gate]] = set(), printing: bool = False, ) -> QuantumCircuit | myQLM_Circuit | braket_Circuit | cirq_Circuit | str: ... @@ -1180,6 +1187,7 @@ def to_other_language( language: Language = Language.QISKIT, skip_pre_measure: bool = False, skip_measurements: bool = False, + authorized_gates: set[type[Gate]] = set(), printing: bool = False, ) -> QuantumCircuit | myQLM_Circuit | braket_Circuit | cirq_Circuit | str: """Transforms this circuit into the corresponding circuit in the language @@ -1255,113 +1263,22 @@ def to_other_language( """ self._generated_g_phase = 0 if language == Language.QISKIT: - from qiskit.circuit import Operation, QuantumCircuit - from qiskit.circuit.quantumcircuit import CircuitInstruction - from qiskit.quantum_info import Operator - - # 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 - qiskit_parameters = set() - if self.nb_cbits == 0: - new_circ = QuantumCircuit(self.nb_qubits) - else: - new_circ = QuantumCircuit(self.nb_qubits, self.nb_cbits) - - if self.label is not None: - new_circ.name = self.label - - for instruction in self.instructions: - if isinstance(instruction, (Measure, Breakpoint)): - continue - options = ( - {"printing": printing} - if isinstance(instruction, CustomGate) - else {} - ) - qiskit_inst = instruction.to_other_language( - language, qiskit_parameters, **options - ) - if TYPE_CHECKING: - assert isinstance( - qiskit_inst, (CircuitInstruction, Operation, Operator) - ) - cargs = [] - - if isinstance(instruction, CustomGate): - if TYPE_CHECKING: - assert isinstance(qiskit_inst, Operator) - if printing and len(instruction.free_symbols) > 0: - new_circ.append( - qiskit_inst, list(reversed(instruction.targets)) - ) - else: - new_circ.unitary( - qiskit_inst, - list(reversed(instruction.targets)), - instruction.label, - ) - else: - if isinstance(instruction, ControlledGate): - qargs = list(reversed(instruction.controls)) + list( - reversed(instruction.targets) - ) - elif isinstance(instruction, Gate): - qargs = list(reversed(instruction.targets)) - elif isinstance(instruction, Barrier): - qargs = range(self.nb_qubits) - else: - raise ValueError(f"Instruction not handled: {instruction}") - - if TYPE_CHECKING: - assert not isinstance(qiskit_inst, Operator) - new_circ.append( - qiskit_inst, - list(qargs), - cargs, - ) - - for measurement in self.measurements: - if not skip_pre_measure: - - for pre_measure in measurement.pre_measure: - cargs = [] - qiskit_pre_measure = pre_measure.to_other_language( - language, qiskit_parameters - ) - new_circ.append( - qiskit_pre_measure, - list(reversed(pre_measure.targets)), - cargs=cargs, - ) - if not skip_measurements: - if isinstance(measurement, ExpectationMeasure): - continue - qiskit_inst = measurement.to_other_language( - language, qiskit_parameters - ) - if isinstance(measurement, BasisMeasure): - if TYPE_CHECKING: - assert measurement.c_targets is not None - else: - raise ValueError(f"measurement not handled: {measurement}") - - if TYPE_CHECKING: - assert not isinstance(qiskit_inst, Operator) - new_circ.append( - qiskit_inst, - [measurement.targets], - [measurement.c_targets], - ) + from mpqp.tools.circuit import mpqp_to_qiskit - new_circ.global_phase += self.input_g_phase + self._generated_g_phase - return new_circ + return mpqp_to_qiskit( + self, + skip_pre_measure, + skip_measurements, + printing, + authorized_gates=authorized_gates, + ) 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.qasm.qasm_to_myqlm import qasm2_to_myqlm_Circuit @@ -1369,149 +1286,18 @@ def to_other_language( return myqlm_circuit elif language == Language.BRAKET: - # filling the circuit with identity gates when some qubits don't have any instruction - used_qubits = set().union( - *( - inst.connections() - for inst in self.instructions - if isinstance(inst, Gate) - ) - ) - circuit = QCircuit( - [ - Id(qubit) - for qubit in range(self.nb_qubits) - if qubit not in used_qubits - ], - nb_qubits=self.nb_qubits, - ) + deepcopy(self) - - from mpqp.execution.providers.aws import apply_noise_to_braket_circuit - - if len(self.noises) != 0: - if any(isinstance(instr, CRk) for instr in self.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 + from mpqp.tools.circuit import mpqp_to_braket - braket_circuit = BracketCircuit() - for instruction in circuit.instructions: - if isinstance(instruction, (Barrier, Breakpoint)): - continue - if isinstance(instruction, Measure): - if not skip_pre_measure: - for pre_measure in instruction.pre_measure: - bracket_pre_measure = pre_measure.to_other_language( - Language.BRAKET - ) - braket_circuit.add(bracket_pre_measure, instruction.targets) - if not skip_measurements: - if isinstance(instruction, BasisMeasure): - braket_circuit.measure(instruction.targets) - continue - braket_instr = instruction.to_other_language(Language.BRAKET) - try: - target = instruction.targets - if isinstance(instruction, ControlledGate): - target = instruction.controls + target - braket_circuit.add_instruction(braket_instr, target=target) - except Exception as e: - raise ValueError( - f"{type(braket_instr)}{braket_instr} cannot be added to the braket circuit: {e}" - ) - - if len(self.noises) == 0: - return braket_circuit - - return apply_noise_to_braket_circuit( - braket_circuit, - self.noises, - self.nb_qubits, + return mpqp_to_braket( + self, skip_pre_measure, authorized_gates=authorized_gates ) - elif language == Language.CIRQ: - from cirq.circuits.circuit import Circuit as CirqCircuit - from cirq.ops.identity import I - from cirq.ops.named_qubit import NamedQubit - - cirq_qubits = [NamedQubit(f"q_{i}") for i in range(self.nb_qubits)] - cirq_circuit = CirqCircuit() - - for qubit in cirq_qubits: - cirq_circuit.append(I(qubit)) - for instruction in self.instructions: - if not skip_pre_measure: - if isinstance(instruction, Measure): - for pre_measure in instruction.pre_measure: - if isinstance( - pre_measure, (CustomGate, CustomControlledGate) - ): - qasm2_code, gphase = pre_measure.to_other_language( - Language.QASM2 - ) # pyright: ignore[reportGeneralTypeIssues] - if TYPE_CHECKING: - assert isinstance(qasm2_code, str) - from mpqp.qasm.qasm_to_cirq import qasm2_to_cirq_Circuit - - qasm2_code = qasm_str = ( - "OPENQASM 2.0;" - + "\ninclude \"qelib1.inc\";" - + f"\nqreg q[{self.nb_qubits}];\n" - + qasm2_code - ) - custom_cirq_circuit = qasm2_to_cirq_Circuit(qasm2_code) - cirq_circuit += custom_cirq_circuit - # TODO: handle gphase in the circuit - self._generated_g_phase += gphase - else: - cirq_pre_measure = pre_measure.to_other_language( - Language.CIRQ - ) - targets = [] - for target in pre_measure.targets: - targets.append(cirq_qubits[target]) - cirq_circuit.append(cirq_pre_measure.on(*targets)) - if isinstance(instruction, (ExpectationMeasure, Barrier, Breakpoint)): - continue - elif isinstance(instruction, ControlledGate) and not isinstance( - instruction, CustomControlledGate - ): - targets = [] - for target in instruction.targets: - targets.append(cirq_qubits[target]) - controls = [] - for control in instruction.controls: - controls.append(cirq_qubits[control]) - cirq_instruction = instruction.to_other_language(Language.CIRQ) - cirq_circuit.append(cirq_instruction.on(*controls, *targets)) - else: - if skip_measurements and isinstance(instruction, Measure): - continue - targets = [] - if isinstance(instruction, CustomControlledGate): - instruction = instruction.to_custom_gate() - for target in instruction.targets: - targets.append(cirq_qubits[target]) - cirq_instruction = instruction.to_other_language(Language.CIRQ) - if TYPE_CHECKING: - assert cirq_instruction - cirq_circuit.append( - cirq_instruction.on( # pyright: ignore[reportAttributeAccessIssue] - *targets - ) - ) - - if self.noises: - from mpqp.execution.providers.google import apply_noise_to_cirq_circuit - - return apply_noise_to_cirq_circuit( - cirq_circuit, - self.noises, - ) + elif language == Language.CIRQ: + from mpqp.tools.circuit import mpqp_to_cirq - return cirq_circuit + return mpqp_to_cirq( + self, skip_pre_measure, skip_measurements, authorized_gates + ) elif language == Language.QASM2: from mpqp.qasm.mpqp_to_qasm import mpqp_to_qasm2 @@ -1647,17 +1433,29 @@ def to_other_device( skip_measurements = False + # Checks if all the gates or its direct decomposition are available on the device. + authorized_gates = device.compatible_gate() + if authorized_gates != set(): + for instr in self.instructions: + if not isinstance(instr, Gate): + continue + if not type(instr) in authorized_gates: + if isinstance(instr, ComposedGate): + if not all( + isinstance(gate, tuple(device.compatible_gate())) + for gate in instr.decompose() + ): + raise ValueError( + f"Gate: {type(instr)} and its decomposition is not available on {device}.\n\nThis device\'s compatible gates are: {authorized_gates}." + ) + else: + raise ValueError( + f"Gate: {type(instr)} is not available on {device}.\n\nThis device\'s compatible gates are: {authorized_gates}." + ) + if isinstance(device, (IBMDevice, StaticIBMSimulatedDevice)): if job_type == JobType.STATE_VECTOR: skip_measurements = True - - if any( - isinstance(i, tuple(device.incompatible_gate())) - for i in self.instructions - ): - raise ValueError( - f"Gate(s) {', '.join(map(str, device.incompatible_gate()))} cannot be simulated on {device}." - ) if ( isinstance(device, StaticIBMSimulatedDevice) and device.value().num_qubits < self.nb_qubits @@ -1666,10 +1464,13 @@ 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, + from mpqp.tools.circuit import mpqp_to_qiskit + + qiskit_circuit = mpqp_to_qiskit( + self, skip_pre_measure, skip_measurements, + authorized_gates=device.compatible_gate(), ) if TYPE_CHECKING: assert isinstance(qiskit_circuit, QuantumCircuit) @@ -1799,12 +1600,17 @@ def to_other_device( elif isinstance(device, AWSDevice): if job_type == JobType.STATE_VECTOR: skip_measurements = True + if device == AWSDevice.IQM_EMERALD or device == AWSDevice.IQM_GARNET: + for instr in self.instructions: + for i in range(len(instr.targets)): + instr.targets[i] += 1 aws_circuit = self.to_other_language( Language.BRAKET, skip_pre_measure, skip_measurements, ) + return aws_circuit elif isinstance(device, ATOSDevice): circuit = self.to_other_language( @@ -1925,11 +1731,19 @@ def from_other_language( from mpqp.qasm import open_qasm_3_to_2 from mpqp.qasm.qasm_to_mpqp import qasm2_parse + from qiskit.transpiler.passes.synthesis import UnitarySynthesis - qasm3_code = qasm3.dumps(qcircuit) + # translation step for custom gates so that we keep the g_phase + # UnitarySynthesis only translates unitaries so it doesn't affect the rest + unitary_translator = UnitarySynthesis(basis_gates=['u3', 'cx']) + translated_dag = unitary_translator.run(qcircuit.to_dag()) + cq: QuantumCircuit = translated_dag.to_circuit() + + qasm3_code = qasm3.dumps(cq) qasm2_code = open_qasm_3_to_2(str(qasm3_code), language=Language.QISKIT) qc = qasm2_parse(qasm2_code) + qc.input_g_phase = cq.global_phase return qc if InstalledProviders.CIRQ in _INSTALLED_MPQP_PROVIDERS: from cirq.circuits.circuit import Circuit as cirq_Circuit @@ -1937,13 +1751,27 @@ def from_other_language( if isinstance(qcircuit, cirq_Circuit) or isinstance(qcircuit, Moment): from mpqp.qasm.qasm_to_mpqp import parse_qasm2_gates, qasm2_parse + from mpqp.qasm.open_qasm_2_and_3 import open_qasm_3_to_2 + from cirq import GlobalPhaseGate + import numpy as np if isinstance(qcircuit, Moment): qcircuit = cirq_Circuit([qcircuit]) + g_phase = 0 + for op in qcircuit.all_operations(): + if isinstance(op.gate, GlobalPhaseGate): + + g_phase += np.round( + np.log( + op.gate.coefficient # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess] + ) + / 1j, + 10, + ) qasm2_code, gphase = parse_qasm2_gates(qcircuit.to_qasm()) qc = qasm2_parse(qasm2_code) - qc.input_g_phase = gphase + qc.input_g_phase = gphase + g_phase return qc @@ -1963,7 +1791,6 @@ def from_other_language( if instr.operator.name == "Measure": remove_measure = False break - qasm3_code = qcircuit.to_ir(IRType.OPENQASM) if TYPE_CHECKING: diff --git a/mpqp/core/instruction/gates/__init__.py b/mpqp/core/instruction/gates/__init__.py index a9ca3a52..5d276ef3 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 62661b88..1d81f3ba 100644 --- a/mpqp/core/instruction/gates/custom_controlled_gate.py +++ b/mpqp/core/instruction/gates/custom_controlled_gate.py @@ -34,11 +34,13 @@ class CustomControlledGate(ControlledGate): >>> circuit = QCircuit(3) >>> circuit.add(CustomControlledGate([0,2], CustomGate(np.array([[1,0],[0,-1]]),[1]))) >>> print(circuit) # doctest: +NORMALIZE_WHITESPACE - q_0: ─────■───── - ┌────┴────┐ - q_1: ┤ Unitary ├ - └────┬────┘ - q_2: ─────■───── + ┌──────────┐ + q_0: ┤2 ├ + │ │ + q_1: ┤1 Unitary ├ + │ │ + q_2: ┤0 ├ + └──────────┘ """ @@ -85,7 +87,10 @@ def to_other_language( language: Language = Language.QISKIT, qiskit_parameters: Optional[set["Parameter"]] = None, ) -> Any: + if isinstance(self.non_controlled_gate, CustomGate): + 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() @@ -93,13 +98,23 @@ 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) + elif language == Language.CIRQ: + + from cirq import ControlledGate as cirqControlledGate - return gate.to_other_language(Language.QASM2) + 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 dd67cccd..fd3b1a26 100644 --- a/mpqp/core/instruction/gates/custom_gate.py +++ b/mpqp/core/instruction/gates/custom_gate.py @@ -129,12 +129,11 @@ def to_other_language( from qiskit import QuantumCircuit dummy_circuit = QuantumCircuit(self.nb_qubits) - for param in qiskit_parameters: - # Rx is just a random choice so to have the parameter in the - # list of inputs - dummy_circuit.rx(param, 0) - return dummy_circuit.to_gate(label="CustomGate") - return UnitaryGate(self.matrix) + dummy_circuit.id(0) + return dummy_circuit.to_gate( + label=f"CustomGate({', '.join([str(s) for s in gate_symbols])})" + ) + return UnitaryGate(self.matrix, label=self.label, check_input=False) elif language == Language.BRAKET: from sympy import Expr @@ -162,32 +161,9 @@ def to_other_language( target=self.targets, ) elif language == Language.CIRQ: - from cirq import Gate as cirqGate - - # TODO: find clean way of initiating this class once - # Cost is negli - class cirqCustomGate(cirqGate): - def __init__(self, matrix: Matrix, label: str | None): - import numpy as np - - self.matrix = matrix - self.label = label - self._nb_qubits = int(np.log2(len(matrix))) - super(cirqCustomGate, self) - - def _num_qubits_(self) -> int: - return self._nb_qubits - - def _unitary_(self) -> Matrix: - return self.matrix - - def _circuit_diagram_info_(self, args: list[float]) -> str | list[str]: - # we keep args for later implementation - if self.label: - return [self.label] * self._nb_qubits - return ["CustomGate"] * self._nb_qubits + from cirq import MatrixGate - return cirqCustomGate(self.matrix, self.label) + return MatrixGate(matrix=self.matrix, name=self.label, unitary_check=False) elif language == Language.QASM2: from qiskit import QuantumCircuit, qasm2 @@ -209,9 +185,7 @@ def _circuit_diagram_info_(self, args: list[float]) -> str | list[str]: 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() @@ -251,7 +225,22 @@ def decompose(self) -> "QCircuit": """ from mpqp.tools.unitary_decomposition import quantum_shannon_decomposition - return quantum_shannon_decomposition(self.matrix) + # non ordered targets + if any( + self.targets[i + 1] < self.targets[i] for i in range(len(self.targets) - 1) + ): + from copy import deepcopy + + from mpqp.tools import rearrange_matrix + + targets = deepcopy(self.targets) + targets.sort() + + return quantum_shannon_decomposition( + rearrange_matrix(self.matrix, self.targets, copy=True), targets + ) + + return quantum_shannon_decomposition(self.matrix, self.targets) def subs(self, values: dict[Expr | str, Complex]) -> CustomGate: res = copy(self) diff --git a/mpqp/core/instruction/gates/native_gates.py b/mpqp/core/instruction/gates/native_gates.py index 87df881d..71e83908 100644 --- a/mpqp/core/instruction/gates/native_gates.py +++ b/mpqp/core/instruction/gates/native_gates.py @@ -19,6 +19,25 @@ from numbers import Integral from typing import TYPE_CHECKING, Optional +from qiskit.circuit.library import ( + CCXGate, + CPhaseGate, + CXGate, + CZGate, + HGate, + IGate, + PhaseGate, + RXGate, + RYGate, + RZGate, + SGate, + SwapGate, + TGate, + XGate, + YGate, + ZGate, +) + if TYPE_CHECKING: from sympy import Expr from qiskit._accelerate.circuit import Parameter @@ -86,8 +105,8 @@ 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: @@ -217,11 +236,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, target: list[int] | int + ): + if not isinstance(theta, list): + theta = [theta] + self.parameters = theta definition = UnitaryMatrix(self.to_canonical_matrix()) + if isinstance(target, int): + target = [target] ParametrizedGate.__init__( - self, definition, [target], [self.theta], type(self).__name__.capitalize() + self, definition, target, self.parameters, type(self).__name__.capitalize() ) @property @@ -230,7 +255,8 @@ def theta(self): return self.parameters[0] def __repr__(self): - return f"{type(self).__name__}({self.theta}, {self.targets[0]})" + target = ", ".join(str(t) for t in self.targets) + return f"{type(self).__name__}({self.theta}, {target})" def to_other_language( self, @@ -249,7 +275,7 @@ def to_other_language( elif language == Language.BRAKET: from braket.circuits import Instruction - connection = self.targets + connection = [target for target in self.targets] if isinstance(self, ControlledGate): connection += self.controls return Instruction( @@ -362,9 +388,10 @@ def to_other_language( elif language == Language.BRAKET: from braket.circuits import Instruction - connection = self.targets + connection = [target for target in 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 @@ -398,6 +425,17 @@ 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) + + def decompose(self) -> list[Gate]: + """Method used to return the decomposed version of a ComposedGate.""" + return [self] + + class Id(OneQubitNoParamGate, InvolutionGate): r"""One qubit identity gate. @@ -656,9 +694,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) + pass qlm_aqasm_keyword = "PH" qiskit_string = "p" @@ -666,6 +702,20 @@ 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( [ @@ -1279,6 +1329,460 @@ def to_canonical_matrix(self): return np.array([[e, 0], [0, 1 / e]]) +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): + pass + + 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 to_canonical_matrix(self): + phi = self.parameters[0] + c = cos(phi / 2) + s = sin(phi / 2) + + return np.array( + [ + [c, 0, 0, -1j * s], + [0, c, -1j * s, 0], + [0, -1j * s, c, 0], + [-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: + from typing import Any, Generator + + from cirq import Qid + + from mpqp.tools.cirq import cirqCustomGate + + def cirq_decomposition(qubits: list[Qid]) -> Generator[Any]: + from cirq.ops import common_gates + from cirq.ops.common_gates import rx as CirqRx + + q1, q2 = qubits + yield common_gates.CNOT(q1, q2) + yield CirqRx(self.parameters[0]).on(q1) + yield common_gates.CNOT(q1, q2) + + return cirqCustomGate(self.to_matrix(), cirq_decomposition, self.label) + + 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): + pass + + 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 to_canonical_matrix(self): + phi = self.parameters[0] + c = cos(phi / 2) + s = sin(phi / 2) + + return np.array( + [ + [c, 0, 0, 1j * s], + [0, c, -1j * s, 0], + [0, -1j * s, c, 0], + [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: + from typing import Any, Generator + + import numpy as np + from cirq import Qid + + from mpqp.tools.cirq import cirqCustomGate + + def cirq_decomposition(qubits: list[Qid]) -> Generator[Any]: + from cirq.ops import common_gates + from cirq.ops.common_gates import rx as CirqRx + from cirq.ops.common_gates import rz as CirqRz + + q1, q2 = qubits + + yield CirqRx(np.pi / 2).on(q1) + yield CirqRx(np.pi / 2).on(q2) + + yield common_gates.CNOT(q1, q2) + yield CirqRz(self.parameters[0]).on(q2) + yield common_gates.CNOT(q1, q2) + + yield CirqRx(-np.pi / 2).on(q1) + yield CirqRx(-np.pi / 2).on(q2) + + return cirqCustomGate(self.to_matrix(), cirq_decomposition, self.label) + + 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): + pass + + 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 to_canonical_matrix(self): + phi = self.parameters[0] + e_minus = exp(-1j * phi / 2) + e_plus = exp(1j * phi / 2) + + 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: + from typing import Any, Generator + + from cirq import Qid + + from mpqp.tools.cirq import cirqCustomGate + + # Need to do these warcrimes because cirq doesn't have a Rzz gate + # This function is create so that the following custom gate still has a nice decomposition + def cirq_decomposition(qubits: list[Qid]) -> Generator[Any]: + from cirq.ops import common_gates + from cirq.ops.common_gates import rz as CirqRz + + q1, q2 = qubits + yield common_gates.CNOT(q1, q2) + yield CirqRz(self.parameters[0]).on(q2) + yield common_gates.CNOT(q1, q2) + + return cirqCustomGate(self.to_matrix(), cirq_decomposition, self.label) + + 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[0], self.parameters[1]) + rz_plus = Rz(phi, self.targets[0]).to_matrix() + rx = Rx(theta, self.targets[0]).to_matrix() + rz_minus = Rz(-phi, self.targets[0]).to_matrix() + + return rz_plus @ rx @ rz_minus + + def to_matrix(self, desired_gate_size: int = 0): + return self.to_canonical_matrix() + + 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: + from qiskit.circuit.library import RGate + + if qiskit_parameters is None: + qiskit_parameters = set() + + return RGate( + _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: + from cirq import PhasedXPowGate + + return PhasedXPowGate( + phase_exponent=self.parameters[1] / np.pi, + exponent=self.parameters[0] / np.pi, + ) + if language == Language.QASM2: + target = self.targets[0] + + return f"rz({self.parameters[1]}) q[{target}];\nrx({self.parameters[0]}) q[{target}];\nrz({-self.parameters[1]}) q[{target}];" + else: + raise NotImplementedError(f"Error: {language} is not supported") + + class Rk(RotationGate, SingleQubitGate): r"""One qubit Phase gate of angle `\frac{2i\pi}{2^k}`. @@ -1399,9 +1903,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) + pass qlm_aqasm_keyword = "PH" qiskit_string = "p" @@ -1435,7 +1937,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.qasm.mpqp_to_qasm import float_to_qasm_str instruction_str = self.qasm2_gate diff --git a/mpqp/core/instruction/gates/parametrized_gate.py b/mpqp/core/instruction/gates/parametrized_gate.py index 4961019e..fff8eaf6 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 d09a9c0d..e0b41e9e 100644 --- a/mpqp/core/instruction/measurement/expectation_value.py +++ b/mpqp/core/instruction/measurement/expectation_value.py @@ -9,12 +9,10 @@ class to define your observable, and a :class:`ExpectationMeasure` to perform from numbers import Real from typing import TYPE_CHECKING, Literal, Optional, Union, overload from typing_extensions import Never -from warnings import warn import numpy as np import numpy.typing as npt -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, @@ -24,7 +22,6 @@ 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 @@ -469,7 +466,7 @@ def __init__( self.observables.append(new_obs) if targets is None: - self.targets = list(range(observable[0].nb_qubits)) + self.targets = list(range(self.observables[0].nb_qubits)) self._check_targets_order() @property @@ -484,47 +481,8 @@ 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.rearranged_targets = list(self.targets) 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 CNOTs." - ) - - for t_index, target in enumerate(tweaked_tgt): # sort the targets - min_index = tweaked_tgt.index(min(tweaked_tgt[t_index:])) - if t_index != min_index: - self._pre_measure.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]: diff --git a/mpqp/execution/devices.py b/mpqp/execution/devices.py index dc438f95..61972374 100644 --- a/mpqp/execution/devices.py +++ b/mpqp/execution/devices.py @@ -25,10 +25,13 @@ from __future__ import annotations from abc import abstractmethod from enum import Enum, auto +import warnings from mpqp.core.instruction.gates import Gate from mpqp.environment.env_manager import get_env_variable +from mpqp.core.instruction.gates.native_gates import * + class AvailableDevice(Enum): """Class used to define a generic device (quantum computer or simulator).""" @@ -90,7 +93,7 @@ def supports_observable(self) -> bool: def supports_observable_ideal(self) -> bool: pass - def incompatible_gate(self) -> set[type[Gate]]: + def compatible_gate(self, verbatim: bool = False) -> set[type[Gate]]: return set() @@ -115,7 +118,6 @@ class IBMDevice(AvailableDevice): IBM_BRISBANE = "ibm_brisbane" IBM_KYIV = "ibm_kyiv" - IBM_FEZ = "ibm_fez" IBM_RENSSELAER = "ibm_rensselaer" IBM_BRUSSELS = "ibm_brussels" IBM_KAWASAKI = "ibm_kawasaki" @@ -124,16 +126,19 @@ class IBMDevice(AvailableDevice): IBM_NAZCA = "ibm_nazca" IBM_STRASBOURG = "ibm_strasbourg" - # RETIRED - IBM_OSAKA = "ibm_osaka" - # RETIRED - IBM_KYOTO = "ibm_kyoto" - # RETIRED - IBM_CUSCO = "ibm_cusco" - # RETIRED - IBM_ITHACA = "ibm_ithaca" + # NightHawk chips + IBM_BOSTON = "ibm_boston" + IBM_KINGSTON = "ibm_kingston" + IBM_PITTSBURGH = "ibm_pittsburgh" + IBM_FEZ = "ibm_fez" + IBM_MARRAKESH = "ibm_marrakesh" + IBM_AACHEN = "IBM_AACHEN" + + # Heron chips + IBM_MIAMI = "ibm_miami" + IBM_BERLIN = "ibm_berlin" + IBM_CLEVELAND = "ibm_cleveland" - # RETIRED - IBM_CAIRO = "ibm_cairo" - # RETIRED - IBM_HANOI = "ibm_hanoi" - # RETIRED - IBM_ALGIERS = "ibm_algiers" - # RETIRED - IBM_KOLKATA = "ibm_kolkata" - # RETIRED - IBM_MUMBAI = "ibm_mumbai" IBM_PEEKSKILL = "ibm_peekskill" IBM_LEAST_BUSY = "ibm_least_busy" @@ -188,13 +193,26 @@ def supports_observable_ideal(self) -> bool: IBMDevice.AER_SIMULATOR_MATRIX_PRODUCT_STATE, } - def incompatible_gate(self) -> set[type[Gate]]: - from mpqp.core.instruction.gates import TOF, CRk, P, Rk, Rx, Ry, Rz, T, U + def compatible_gate(self, verbatim: bool = False) -> set[type[Gate]]: if self == IBMDevice.AER_SIMULATOR_STABILIZER: - return {CRk, P, Rk, Rx, Ry, Rz, T, TOF, U} + warnings.warn( + UserWarning( + f"For {self} the gates Rx, Ry and Rz are allowed but only at angles 0, π, π/2 and 3*π/2" + ) + ) + return {Rx, Ry, Rz, X, Y, Z, H, CNOT, CZ, S, S_dagger, SWAP} elif self == IBMDevice.AER_SIMULATOR_EXTENDED_STABILIZER: - return {Rx, Rz} + warnings.warn( + UserWarning( + f"For {self} the gates Rx, Ry and Rz are allowed but only at angles 0, π, π/2 and 3*π/2" + ) + ) + return {Rx, Ry, Rz, X, Y, Z, H, CNOT, CZ, S, S_dagger, SWAP} + elif self in IBM_CHIPS_HERON: + return {CZ, Id, Rx, Rz, Rzz, X} + elif self in IBM_CHIPS_NIGHTHAWK: + return {CZ, Id, Rx, Rz, X} else: return set() @@ -361,6 +379,69 @@ def get_region(self) -> str: else: return get_env_variable("AWS_DEFAULT_REGION") + def compatible_gate(self, verbatim: 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 verbatim: # 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, + CZ, + H, + Id, + Rx, + Ry, + Rz, + S, + T, + X, + Y, + Z, + ] + ) + + elif self == AWSDevice.RIGETTI_ANKAA_3: + if verbatim: # '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. @@ -512,3 +593,14 @@ def supports_observable(self) -> bool: def supports_observable_ideal(self) -> bool: return False + + +IBM_CHIPS_HERON = [IBMDevice.IBM_MIAMI, IBMDevice.IBM_BERLIN] +IBM_CHIPS_NIGHTHAWK = [ + IBMDevice.IBM_BOSTON, + IBMDevice.IBM_KINGSTON, + IBMDevice.IBM_PITTSBURGH, + IBMDevice.IBM_FEZ, + IBMDevice.IBM_MARRAKESH, + IBMDevice.IBM_AACHEN, +] diff --git a/mpqp/execution/providers/aws.py b/mpqp/execution/providers/aws.py index 8c83f6a9..be5576f7 100644 --- a/mpqp/execution/providers/aws.py +++ b/mpqp/execution/providers/aws.py @@ -187,11 +187,14 @@ def run_braket_observable(job: Job): if job.measure.pre_transpiled is None: grouping = job.measure.get_pauli_grouping() + pre_measure = [ + QCircuit(find_qubitwise_rotations(group)) for group in grouping + ] + for circuit in pre_measure: + for instr in circuit.instructions: + instr.targets[0] = job.measure.targets[instr.targets[0]] transpiled_pre_measures = [ - QCircuit(find_qubitwise_rotations(group)).to_other_language( - Language.BRAKET - ) - for group in grouping + pre_m.to_other_language(Language.BRAKET) for pre_m in pre_measure ] eigenvalues = [ {monom.name: pauli_monomial_eigenvalues(monom) for monom in group} @@ -211,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] @@ -223,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.circuit.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)}" @@ -278,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}) @@ -314,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: @@ -402,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: @@ -410,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 @@ -428,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/google.py b/mpqp/execution/providers/google.py index adae0652..315d1832 100644 --- a/mpqp/execution/providers/google.py +++ b/mpqp/execution/providers/google.py @@ -610,5 +610,9 @@ def extract_result_STATE_VECTOR( Returns: The formatted result. """ - state_vector = StateVector(result.final_state_vector, job.circuit.nb_qubits) - return Result(job, state_vector, 0, 0) + state_vector = result.final_state_vector + if job.circuit.input_g_phase: + import numpy as np + + state_vector = state_vector / np.exp(1j * job.circuit.input_g_phase) + return Result(job, StateVector(state_vector, job.circuit.nb_qubits), 0, 0) diff --git a/mpqp/execution/providers/ibm.py b/mpqp/execution/providers/ibm.py index e355c875..471de409 100644 --- a/mpqp/execution/providers/ibm.py +++ b/mpqp/execution/providers/ibm.py @@ -94,9 +94,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: @@ -106,7 +104,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 @@ -118,7 +118,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 ] @@ -617,7 +616,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 1dcb0b95..71516719 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/runner.py b/mpqp/execution/runner.py index abc2bb3a..e599df03 100644 --- a/mpqp/execution/runner.py +++ b/mpqp/execution/runner.py @@ -55,14 +55,16 @@ def adjust_measure(measure: ExpectationMeasure, circuit: QCircuit): + # TODO: to enhance docs + """We allow the measure to not span the entire circuit, but providers usually do not support this behavior. To make this work, we tweak the measure this function to match the expected behavior. In order to do this, we add identity measures on the qubits not targeted by - the measure. In addition to this, some swaps are automatically added so the - the qubits measured are ordered and contiguous (though this is done in - :func:`generate_job`) + the measure. pauli observables are directly embeded on their target qubits, + while matrix observables are padded with identity matrices when the targets + are ordered and contiguous, otherwise are embedded through their pauli decomposition. Args: measure: The expectation measure, potentially incomplete. @@ -78,25 +80,64 @@ def adjust_measure(measure: ExpectationMeasure, circuit: QCircuit): if measure.targets == list(range(circuit.nb_qubits)): return measure - tweaked_observables = [] - n_before = measure.rearranged_targets[0] - n_after = circuit.nb_qubits - measure.rearranged_targets[-1] - 1 + nb_qubits = circuit.nb_qubits + targets = measure.targets + + targets_is_ordered = all( + [targets[i] > targets[i - 1] for i in range(1, len(targets))] + ) + targets_is_contiguous = ( + len(targets) > 0 + and targets_is_ordered + and (targets[-1] - targets[0] + 1 == len(targets)) + ) + + tweaked_observables: list[Observable] = [] + for obs in measure.observables: - if obs._pauli_string is not None: # pyright: ignore[reportPrivateUsage] - from mpqp.measures import pI + from mpqp.core.instruction.measurement.pauli_string import ( + PauliString, + PauliStringMonomial, + ) + from mpqp.measures import pI + + if ( + obs._pauli_string is None # pyright: ignore[reportPrivateUsage] + and targets_is_contiguous + ): + n_before = targets[0] + n_after = nb_qubits - targets[-1] - 1 + + full_matrix = obs.matrix - pauli = pI(n_before - 1) @ obs.pauli_string @ pI(n_after - 1) - tweaked_observables.append(Observable(pauli)) - else: Id_before = np.eye(2**n_before) Id_after = np.eye(2**n_after) + + if n_before > 0: + full_matrix = np.kron(Id_before, full_matrix) + + if n_after > 0: + full_matrix = np.kron(full_matrix, Id_after) + tweaked_observables.append( Observable( - np.kron( - np.kron(Id_before, obs.matrix), Id_after - ) # pyright: ignore[reportArgumentType] + full_matrix, label=obs.label # pyright: ignore[reportArgumentType] ) ) + continue + + pauli = obs.pauli_string + embedded = PauliString() + + for mono in pauli.monomials: + full_register = [pI] * nb_qubits + + for local_idx, target in enumerate(targets): + full_register[target] = mono.atoms[local_idx] + + embedded += PauliStringMonomial(mono.coef, full_register) + + tweaked_observables.append(Observable(embedded.simplify(), label=obs.label)) tweaked_measure = ExpectationMeasure( tweaked_observables, @@ -104,7 +145,9 @@ def adjust_measure(measure: ExpectationMeasure, circuit: QCircuit): measure.shots, measure.commuting_type, measure.grouping_method, + label=measure.label, optimize_measurement=measure.optimize_measurement, + optim_diagonal=measure.optim_diagonal, ) return tweaked_measure @@ -145,14 +188,21 @@ def generate_job( else: job = Job(JobType.SAMPLE, circuit, device) elif isinstance(measurement, ExpectationMeasure): - m = adjust_measure(measurement, circuit) - c = circuit.without_measurements(deep_copy=False) - c.add(m) - job = Job( - JobType.OBSERVABLE, - c, - device, - ) + if measurement.optimize_measurement and isinstance(device, AWSDevice): + job = Job( + JobType.OBSERVABLE, + circuit, + device, + ) + else: + m = adjust_measure(measurement, circuit) + c = circuit.without_measurements(deep_copy=False) + c.add(m) + job = Job( + JobType.OBSERVABLE, + c, + device, + ) else: raise NotImplementedError( f"Measurement type {type(measurement)} not handled" diff --git a/mpqp/qasm/lexer_utils.py b/mpqp/qasm/lexer_utils.py index 000c6d9f..c54822ad 100644 --- a/mpqp/qasm/lexer_utils.py +++ b/mpqp/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/qasm/mpqp_to_qasm.py b/mpqp/qasm/mpqp_to_qasm.py index 8d502af1..ecbbf10b 100644 --- a/mpqp/qasm/mpqp_to_qasm.py +++ b/mpqp/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/qasm/open_qasm_2_and_3.py b/mpqp/qasm/open_qasm_2_and_3.py index e389f8fd..3a8768bb 100644 --- a/mpqp/qasm/open_qasm_2_and_3.py +++ b/mpqp/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/qasm/qasm_to_braket.py b/mpqp/qasm/qasm_to_braket.py index f8940a5e..8d5f3e45 100644 --- a/mpqp/qasm/qasm_to_braket.py +++ b/mpqp/qasm/qasm_to_braket.py @@ -228,9 +228,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/qasm/qasm_to_mpqp.py b/mpqp/qasm/qasm_to_mpqp.py index 4802cea5..c77a6da9 100644 --- a/mpqp/qasm/qasm_to_mpqp.py +++ b/mpqp/qasm/qasm_to_mpqp.py @@ -72,9 +72,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' @@ -235,6 +233,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' @@ -308,9 +308,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 += "(" @@ -336,7 +343,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( @@ -346,13 +354,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 @@ -368,10 +382,8 @@ def _Gate_U(circuit: QCircuit, gate_str: str, tokens: list[LexToken], idx: int) theta, idx = _eval_expr(tokens, idx) phi, idx = _eval_expr(tokens, idx) 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) - + list_params, idx = _eval_expr(tokens, idx) + theta, phi, lbda = tuple(list_params) if check_Id(tokens, idx): raise SyntaxError( f'GateU: {" ".join(str(token.value) for token in tokens[idx : idx + 4])}' diff --git a/mpqp/tools/circuit.py b/mpqp/tools/circuit.py index 16b1fbe2..674b5c5a 100644 --- a/mpqp/tools/circuit.py +++ b/mpqp/tools/circuit.py @@ -7,12 +7,16 @@ from numpy.random import Generator from mpqp.core.circuit import QCircuit +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, SingleQubitGate from mpqp.core.instruction.gates.native_gates import ( NATIVE_GATES, + PRX, TOF, CRk, P, + ComposedGate, Rk, RotationGate, Rx, @@ -21,6 +25,7 @@ U, ) from mpqp.core.instruction.gates.parametrized_gate import ParametrizedGate +from mpqp.core.languages import Language from mpqp.noise.noise_model import ( NOISE_MODELS, AmplitudeDamping, @@ -33,7 +38,9 @@ from mpqp.tools.maths import closest_unitary if TYPE_CHECKING: - from qiskit import QuantumCircuit + from braket.circuits import Circuit as braket_Circuit + from cirq.circuits.circuit import Circuit as cirq_Circuit + from qiskit.circuit import QuantumCircuit from qiskit._accelerate.circuit import CircuitInstruction @@ -41,8 +48,9 @@ def random_circuit( gate_classes: Optional[Sequence[type[Gate]]] = None, nb_qubits: int = 5, nb_gates: Optional[int] = None, + use_all_qubits: bool = False, seed: Optional[int] = None, -): +) -> QCircuit: """This function creates a QCircuit with a specified number of qubits and gates. The gates are chosen randomly from the provided list of native gate classes. @@ -86,6 +94,11 @@ def random_circuit( qcircuit = QCircuit(nb_qubits) for _ in range(nb_gates): qcircuit.add(random_gate(gate_classes, nb_qubits, rng)) + if use_all_qubits: # used in case we want to test braket + from mpqp.gates import H + + for i in range(nb_qubits): + qcircuit.add(H(i)) return qcircuit @@ -106,12 +119,12 @@ 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 ] + >>> pprint(statevector_from_random_circuit(2, seed=123)) # doctest: +NORMALIZE_WHITESPACE + [0.43644+0.13833j, 0, 0.2176+0.86199j, 0] """ from mpqp.execution import IBMDevice, Result, run - mpqp_circ = random_circuit(None, nb_qubits, None, seed) + mpqp_circ = random_circuit(None, nb_qubits, None, seed=seed) res = run(mpqp_circ, IBMDevice.AER_SIMULATOR_STATEVECTOR) if TYPE_CHECKING: assert isinstance(res, Result) @@ -164,7 +177,6 @@ def random_gate( gate_class = rng.choice(np.array(gate_classes)) target = rng.choice(qubits).item() - if issubclass(gate_class, SingleQubitGate): if issubclass(gate_class, ParametrizedGate): if issubclass(gate_class, U): @@ -176,6 +188,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)) @@ -294,7 +312,9 @@ def compute_expected_matrix(qcircuit: QCircuit): def replace_custom_gate( - custom_unitary: "CircuitInstruction", nb_qubits: int, targets: list[int] + custom_unitary: "CircuitInstruction | QuantumCircuit", + nb_qubits: int, + targets: list[int], ) -> "tuple[QuantumCircuit, float]": """Decompose and replace the (custom) qiskit unitary given in parameter by a qiskit `QuantumCircuit` composed of ``U`` and ``CX`` gates. @@ -319,8 +339,13 @@ def replace_custom_gate( from qiskit.exceptions import QiskitError from qiskit.circuit.library import UnitaryGate - transpilation_circuit = QuantumCircuit(nb_qubits) - transpilation_circuit.append(custom_unitary) + if not isinstance(custom_unitary, QuantumCircuit): + transpilation_circuit = QuantumCircuit(nb_qubits) + transpilation_circuit.append(custom_unitary) + matrix = custom_unitary.matrix + else: + transpilation_circuit = custom_unitary + matrix = custom_unitary.data[0].matrix try: transpiled = transpile( transpilation_circuit, basis_gates=['u3', 'cx'], optimization_level=0 @@ -329,7 +354,7 @@ def replace_custom_gate( # if the error is arising from TwoQubitWeylDecomposition, we replace the # matrix by the closest unitary if "TwoQubitWeylDecomposition" in str(e): - custom_closest_unitary = UnitaryGate(closest_unitary(custom_unitary.matrix)) + custom_closest_unitary = UnitaryGate(closest_unitary(matrix)) transpilation_circuit = QuantumCircuit(nb_qubits) transpilation_circuit.unitary( custom_closest_unitary, list(reversed(targets)) @@ -342,3 +367,381 @@ def replace_custom_gate( else: raise e return transpiled, transpiled.global_phase + + +def verify_convert_instructions( + gate: Gate, authorized_gates: set[type[Gate]] +) -> list[Gate]: + if len(authorized_gates) != 0: + if type(gate) not in authorized_gates: + if isinstance(gate, ComposedGate): + instr = gate.decompose() + if any(type(gate) not in authorized_gates for gate in instr): + raise ValueError( + f"The gate {type(gate)} and it's decomposition f{[type(g) for g in instr]} are not in the set: f{authorized_gates}" + ) + return instr + 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 + ): + return [gate.to_custom_gate()] + if isinstance(gate, ComposedGate): + return gate.decompose() + + return [gate] + + +def mpqp_to_qiskit( + circuit: QCircuit, + skip_pre_measure: bool = False, + skip_measurements: bool = False, + printing: bool = False, + authorized_gates: set[type[Gate]] | None = None, +) -> QuantumCircuit: + from qiskit.circuit import Operation, QuantumCircuit + from qiskit.circuit.quantumcircuit import CircuitInstruction + from qiskit.quantum_info import Operator + + from mpqp.core.instruction import ( + Measure, + Breakpoint, + CustomGate, + Barrier, + ControlledGate, + BasisMeasure, + ExpectationMeasure, + ) + + # 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) + else: + new_circ = QuantumCircuit(circuit.nb_qubits, circuit.nb_cbits) + + if circuit.label is not None: + new_circ.name = circuit.label + + for instruction in circuit.instructions: + if isinstance(instruction, (Measure, Breakpoint)): + continue + options = {"printing": printing} if isinstance(instruction, CustomGate) else {} + if isinstance(instruction, Gate): + instr = verify_convert_instructions(instruction, authorized_gates) + else: + instr = [instruction] + for instruction in instr: + qiskit_inst = instruction.to_other_language( + Language.QISKIT, qiskit_parameters, **options + ) + if isinstance(instruction, CustomControlledGate) and isinstance( + instruction.non_controlled_gate, CustomGate + ): + instruction = instruction.to_custom_gate() + if TYPE_CHECKING: + assert isinstance( + qiskit_inst, (CircuitInstruction, Operation, Operator) + ) + cargs = [] + + if isinstance(instruction, CustomGate) and not isinstance( + instruction, CustomControlledGate + ): + if TYPE_CHECKING: + assert isinstance(qiskit_inst, Operator) + if printing and len(instruction.free_symbols) > 0: + new_circ.append(qiskit_inst, list(reversed(instruction.targets))) + else: + new_circ.append( + qiskit_inst, + list(reversed(instruction.targets)), + ) + else: + qargs = [] + if isinstance(instruction, ControlledGate): + qargs = list(reversed(instruction.controls)) + list( + reversed(instruction.targets) + ) + elif isinstance(instruction, Gate): + qargs = list(reversed(instruction.targets)) + elif isinstance(instruction, Barrier): + qargs = range(circuit.nb_qubits) + else: + raise ValueError(f"Instruction not handled: {instruction}") + + if TYPE_CHECKING: + assert not isinstance(qiskit_inst, Operator) + new_circ.append( + qiskit_inst, + list(qargs), + cargs, + ) + for measurement in circuit.measurements: + if not skip_pre_measure: + + for pre_measure in measurement.pre_measure: + cargs = [] + qiskit_pre_measure = pre_measure.to_other_language( + Language.QISKIT, qiskit_parameters + ) + new_circ.append( + qiskit_pre_measure, + list(reversed(pre_measure.targets)), + cargs=cargs, + ) + if not skip_measurements: + if isinstance(measurement, ExpectationMeasure): + continue + qiskit_inst = measurement.to_other_language( + Language.QISKIT, qiskit_parameters + ) + if isinstance(measurement, BasisMeasure): + if TYPE_CHECKING: + assert measurement.c_targets is not None + else: + raise ValueError(f"measurement not handled: {measurement}") + + if TYPE_CHECKING: + assert not isinstance(qiskit_inst, Operator) + new_circ.append( + qiskit_inst, + [measurement.targets], + [measurement.c_targets], + ) + + new_circ.global_phase += ( + circuit.input_g_phase + + circuit._generated_g_phase # type: ignore[reporPrivateUsage] + ) + return new_circ + + +def mpqp_to_braket( + circuit: QCircuit, + skip_pre_measure: bool = False, + skip_measurements: bool = False, + authorized_gates: set[type[Gate]] | None = None, +) -> braket_Circuit: + from mpqp.execution.providers.aws import apply_noise_to_braket_circuit + from mpqp.core.instruction import ( + Measure, + Breakpoint, + Barrier, + ControlledGate, + BasisMeasure, + ) + + 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() + + # If the number of qubits are defined by the user, we ensure that every qubits are used. + # Otherwise the circuit can remain non continuous. + if circuit._user_nb_qubits is not None: # pyright: ignore[reportPrivateUsage] + used_qubits = set().union( + *( + inst.connections() + for inst in circuit.instructions + if isinstance(inst, Gate) + ) + ) + if len(used_qubits) != circuit.nb_qubits: + from mpqp.gates import Id + from copy import deepcopy + + circuit = QCircuit( + [ + Id(qubit) + for qubit in range(circuit.nb_qubits) + if qubit not in used_qubits + ], + nb_qubits=circuit.nb_qubits, + ) + deepcopy(circuit) + + for instruction in circuit.instructions: + targets = [target for target in instruction.targets] + if isinstance(instruction, (Barrier, Breakpoint)): + continue + if isinstance(instruction, Measure): + if not skip_pre_measure: + for pre_measure in instruction.pre_measure: + bracket_pre_measure = pre_measure.to_other_language(Language.BRAKET) + braket_circuit.add(bracket_pre_measure, targets) + if not skip_measurements: + if isinstance(instruction, BasisMeasure) and instruction.shots != 0: + braket_circuit.measure(targets) + continue + if isinstance(instruction, Gate): + instr = verify_convert_instructions(instruction, authorized_gates) + else: + instr = [instruction] + + for instruction in instr: + braket_instr = instruction.to_other_language(Language.BRAKET) + try: + targets = [target for target in instruction.targets] + if isinstance(instruction, CustomControlledGate): + if isinstance(instruction.non_controlled_gate, CustomGate): + targets = [ + control for control in instruction.controls + ] + targets + targets.sort() + elif isinstance(instruction, ControlledGate): + targets = [control for control in instruction.controls] + targets + braket_circuit.add_instruction(braket_instr, target=targets) + except Exception as e: + raise ValueError( + f"{type(braket_instr)}{braket_instr} cannot be added to the braket circuit: {e}" + ) + if len(circuit.noises) != 0: + braket_circuit = apply_noise_to_braket_circuit( + braket_circuit, + circuit.noises, + circuit.nb_qubits, + ) + return braket_circuit + + +def mpqp_to_cirq( + circuit: QCircuit, + skip_pre_measure: bool = False, + skip_measurements: bool = False, + authorized_gates: set[type[Gate]] | None = None, +) -> cirq_Circuit: + 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, + ControlledGate, + CustomControlledGate, + ExpectationMeasure, + ) + + if authorized_gates is None: + authorized_gates = set() + cirq_qubits = [NamedQubit(f"q_{i}") for i in range(circuit.nb_qubits)] + cirq_circuit = CirqCircuit() + + for qubit in cirq_qubits: + cirq_circuit.append(I(qubit)) + + for instruction in circuit.instructions: + if not skip_pre_measure: + 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 + ) + qasm2_code, gphase = pre_measure.to_other_language( + Language.QASM2 + ) # pyright: ignore[reportGeneralTypeIssues] + if TYPE_CHECKING: + assert isinstance(qasm2_code, str) + from mpqp.qasm.qasm_to_cirq import qasm2_to_cirq_Circuit + + qasm2_code = ( + "OPENQASM 2.0;" + + "\ninclude \"qelib1.inc\";" + + f"\nqreg q[{circuit.nb_qubits}];\n" + + qasm2_code + ) + custom_cirq_circuit = qasm2_to_cirq_Circuit(qasm2_code) + cirq_circuit += custom_cirq_circuit + # TODO: handle gphase in the circuit + circuit._generated_g_phase += gphase # type: ignore[reporPrivateUsage] + else: + cirq_pre_measure = pre_measure.to_other_language(Language.CIRQ) + targets = [] + for target in pre_measure.targets: + targets.append(cirq_qubits[target]) + cirq_circuit.append(cirq_pre_measure.on(*targets)) + + if isinstance(instruction, Gate): + instr = verify_convert_instructions(instruction, authorized_gates) + else: + instr = [instruction] + + if isinstance(instr[0], CustomGate): + if isinstance(instruction, CustomGate): + from cirq.ops.raw_types import Gate as CirqGate + + custom_gate = instr[0] + + targets = [] + for target in custom_gate.targets: + targets.append(cirq_qubits[target]) + + cirq_instruction = custom_gate.to_other_language(Language.CIRQ) + assert isinstance(cirq_instruction, CirqGate) + + cirq_circuit.append(cirq_instruction.on(*targets)) + continue + + from cirq import GlobalPhaseGate + + tmp_circuit = instr[0].decompose() + instr = tmp_circuit.instructions + + cirq_circuit.insert( + 0, GlobalPhaseGate(np.exp(1j * tmp_circuit.input_g_phase)).on() + ) + + for gate in instr: + if isinstance(gate, (ExpectationMeasure, Barrier, Breakpoint)): + continue + elif isinstance(gate, ControlledGate): + targets = [] + for target in gate.targets: + targets.append(cirq_qubits[target]) + controls = [] + for control in gate.controls: + controls.append(cirq_qubits[control]) + cirq_instruction = gate.to_other_language(Language.CIRQ) + cirq_circuit.append(cirq_instruction.on(*controls, *targets)) + else: + if skip_measurements and isinstance(gate, Measure): + continue + targets = [] + for target in gate.targets: + targets.append(cirq_qubits[target]) + cirq_instruction = gate.to_other_language(Language.CIRQ) + if TYPE_CHECKING: + assert cirq_instruction + cirq_circuit.append(cirq_instruction.on(*targets)) + + if circuit.noises: + from mpqp.execution.providers.google import apply_noise_to_cirq_circuit + + return apply_noise_to_cirq_circuit( + cirq_circuit, + circuit.noises, + ) + + if circuit.input_g_phase != 0: + from cirq import GlobalPhaseGate + + cirq_circuit.insert(0, GlobalPhaseGate(np.exp(1j * circuit.input_g_phase)).on()) + + return cirq_circuit diff --git a/mpqp/tools/cirq.py b/mpqp/tools/cirq.py new file mode 100644 index 00000000..eded0459 --- /dev/null +++ b/mpqp/tools/cirq.py @@ -0,0 +1,43 @@ +import sys +from typing import Any, Optional + +if "cirq" in sys.modules: # if cirq is imported instanciate the following classes + + from cirq import Gate as cirqGate + from cirq import Qid + from mpqp.tools.generics import Matrix + + class cirqCustomGate(cirqGate): + def __init__( + self, matrix: Matrix, decomposition: Any = None, label: Optional[str] = None + ): + import numpy as np + + self.decomposition = decomposition + self.matrix = matrix + self.label = label + self._nb_qubits = int(np.log2(len(matrix))) + super(cirqCustomGate, self) + + def _num_qubits_(self) -> int: + return self._nb_qubits + + def _unitary_(self) -> Matrix: + return self.matrix + + def _decompose_(self, qubits: list[Qid]): + if self.decomposition is None: + pass + else: + return self.decomposition(qubits) + + def _circuit_diagram_info_(self, args: list[float]) -> str | list[str]: + # we keep args for later implementation + if self.label: + return [self.label] * self._nb_qubits + return ["CustomGate"] * self._nb_qubits + + def __str__(self) -> str: + if self.label: + return f"{self.label}" + return f"MPQP custom gate" diff --git a/mpqp/tools/errors.py b/mpqp/tools/errors.py index 54a14154..5c631a05 100644 --- a/mpqp/tools/errors.py +++ b/mpqp/tools/errors.py @@ -2,6 +2,13 @@ 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.execution.result import JobType + class InstructionParsingError(ValueError): """Raised when an QASM instruction encountered by the parser is malformed.""" @@ -77,3 +84,19 @@ class AdditionalGateNoiseWarning(UserWarning): class NonReversibleWarning(UserWarning): """Warning for nonreversible instruction used in inverse function.""" + + +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 dfa5e053..1cef42ff 100644 --- a/mpqp/tools/maths.py +++ b/mpqp/tools/maths.py @@ -6,11 +6,13 @@ import math from functools import reduce from numbers import Complex, Real + from typing import TYPE_CHECKING, Any, Optional, Union import numpy as np import numpy.typing as npt from scipy.linalg import inv, sqrtm +from typeguard import typechecked if TYPE_CHECKING: from sympy import Expr @@ -389,6 +391,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. @@ -413,12 +420,17 @@ def rand_product_local_unitaries( ) # pyright: ignore[reportReturnType] -def rand_unitary_matrix(size: int) -> Matrix: +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. @@ -429,8 +441,12 @@ def rand_unitary_matrix(size: int) -> Matrix: True """ from scipy.stats import unitary_group + import numpy as np - 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( @@ -478,3 +494,83 @@ def is_power_of_two(n: int) -> bool: """ return n >= 1 and (n & (n - 1)) == 0 + + +@typechecked +def rearrange_matrix(m: Matrix, targets: list[int], copy: bool = True) -> Matrix: + """Function to reorder the rows and columns of a matrix in order to change the targets of a gate. + The intended order for a gate is having continuous targets in growing order. + + For example the targets for a 3 qubit gate should be [1,2,3], changing it for [3,2,1] would + reverse the effects on the qubits 3 and 1 (akin to a SWAP gate on those qubits). + + Note: This function's goal is not to move around a gate in a circuit but to shuffle the targets in a sense. + + Args: + m: The matrix for which we want to reorder the targets. + targets: The targets + copy: If True performs the copy of the matrix, to prevent overwriting the original matrix. + + Returns: + The shuffled matrix according to the given targets. + + Example: + >>> I = np.eye(2) + >>> X = np.array([[0,1], [1,0]]) + >>> matrix = np.kron(I, X) + >>> pprint(matrix) + [[0, 1, 0, 0], + [1, 0, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0]] + >>> pprint(rearrange_matrix(matrix, [1,0])) + [[0, 0, 1, 0], + [0, 0, 0, 1], + [1, 0, 0, 0], + [0, 1, 0, 0]] + """ + from copy import deepcopy + + if copy: + matrix = deepcopy(m) + else: + matrix = m + l = len(targets) + shuffled = deepcopy(targets) + shuffled.sort() + for index in range(l - 1): + if targets[index] == index: + continue + # If no swaps happened of the target then shuffled_index = targets[index] + shuffled_index = shuffled.index(targets[index]) + + i = 1 << (l - 1 - shuffled_index) + j = 1 << (l - 1 - index) + for change in range(1 << l): + current = bin(change)[2:].zfill(l) + if current[shuffled_index - l] == "0" and current[index - l] == "1": + current = int(current, 2) + conjugate = current + i - j + for k in range(len(matrix)): + hold = matrix[k][current] + matrix[k][current] = matrix[k][conjugate] + matrix[k][conjugate] = hold + + for k in range(len(matrix)): + hold = matrix[current][k] + matrix[current][k] = matrix[conjugate][k] + matrix[conjugate][k] = hold + + # keeps tracks of the position of the targets in the matrix + + shuffled[index], shuffled[targets[index]] = ( + shuffled[targets[index]], + shuffled[index], + ) + + i = targets.index(index) + targets[i], targets[index] = ( + targets[index], + targets[i], + ) + return matrix diff --git a/mpqp/tools/unitary_decomposition.py b/mpqp/tools/unitary_decomposition.py index bce2bb7d..3e65e044 100644 --- a/mpqp/tools/unitary_decomposition.py +++ b/mpqp/tools/unitary_decomposition.py @@ -2,15 +2,17 @@ unitary operator into elementary gates regrouped in a quantum circuit.""" from __future__ import annotations + import math from typing import Union import numpy as np +from scipy.linalg import cossin + from mpqp.core.circuit import QCircuit from mpqp.gates import CNOT, Ry, Rz from mpqp.tools import Matrix from mpqp.tools.maths import is_power_of_two -from scipy.linalg import cossin PRECISION = 1e-9 @@ -60,13 +62,17 @@ def _unitary_SVD(U: Matrix) -> tuple[Matrix, Matrix, Matrix]: G1 = np.array(g1) # Build G as G = V @ D² @ V† - G = G0 @ G1.conj().T + g = G0 @ G1.conj().T + G = np.round(g, 10) + eigvals, v = np.linalg.eig(G) + from mpqp.tools import closest_unitary, is_unitary - eigvals, V = np.linalg.eig(G) + if not is_unitary(v): + v = closest_unitary(v) D = np.diag(np.sqrt(eigvals.astype(complex))) # W = D @ V† @ G1 - W = np.asarray(D @ V.conj().T @ G1, dtype=np.complex128) + W = np.asarray(D @ v.conj().T @ G1, dtype=np.complex128) D_dagg = D.conj().T # Reconstruct the whole D matrix @@ -79,12 +85,13 @@ def _unitary_SVD(U: Matrix) -> tuple[Matrix, Matrix, Matrix]: for i in range(length // 2): D_result.append(list(padding) + list(D_dagg[0 : length // 2][i])) - return V, np.array(D_result), W + return v, np.array(D_result), W def _gray_code_decomposition( thetas: Matrix, circuit: QCircuit, + targets: list[int], position: int, rotation: Union[type[Rz], type[Ry]], ) -> QCircuit: @@ -116,14 +123,16 @@ def _gray_code_decomposition( # CNOT's control is the changed bit of two consecutive numbers in gray code changed = _gray_code(i) ^ _gray_code(i + 1) control = next(i for i in range(len(thetas)) if (changed >> i & 1)) - control = max(-control - position - 1 + circuit.nb_qubits, 1) + control = max(-control - targets[position] - 1 + circuit.nb_qubits, 1) if np.abs(angle) > PRECISION: # Dodge unnecessary rotations - circuit.add(rotation(angle, position)) - circuit.add(CNOT(control + position, position)) + circuit.add(rotation(angle, int(targets[position]))) + circuit.add(CNOT(int(control + targets[position]), int(targets[position]))) return circuit -def _decompose(U: Matrix, circuit: QCircuit, position: int = 0) -> QCircuit: +def _decompose( + U: Matrix, circuit: QCircuit, targets: list[int], position: int = 0 +) -> QCircuit: """ This function recursively decompose the matrix U into the circuit then returns it. @@ -138,62 +147,77 @@ def _decompose(U: Matrix, circuit: QCircuit, position: int = 0) -> QCircuit: # extract the global phase so that SU is a special unitary, note that if U is a special unitary then delta = 0 SU = U / np.exp(1j * delta) - beta = 2 * math.acos(np.abs(SU[0][0])) + beta = 2 * math.acos(np.round(np.abs(SU[0][0]), 10)) alpha = -np.angle(SU[0][0]) - np.angle(SU[1][0]) gamma = -np.angle(SU[0][0]) + np.angle(SU[1][0]) - circuit.add(Rz(alpha, position)) - circuit.add(Ry(beta, position)) - circuit.add(Rz(gamma, position)) + circuit.add(Rz(alpha, int(targets[position]))) + circuit.add(Ry(beta, int(targets[position]))) + circuit.add(Rz(gamma, int(targets[position]))) circuit.input_g_phase += delta # Stores the gphase in the circuit return circuit 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) # Extracts the rotations of both multiplexed Rz for later decomposition - du = np.angle( - MuxRzu.diagonal() # pyright: ignore[reportCallIssue, reportArgumentType] + du = np.asarray( + np.angle( + MuxRzu.diagonal() # pyright: ignore[reportCallIssue, reportArgumentType] + ), + dtype=np.float64, ) for i in range(len(du) // 2): du[i] *= -1 - dv = np.angle( - MuxRzv.diagonal() # pyright: ignore[reportCallIssue, reportArgumentType] + dv = np.asarray( + np.angle( + MuxRzv.diagonal() # pyright: ignore[reportCallIssue, reportArgumentType] + ), + dtype=np.float64, ) for i in range(len(dv) // 2): dv[i] *= -1 # Now recursively decompose every obtained matrices. - circuit = _decompose(Wv, circuit, position + 1) + circuit = _decompose(Wv, circuit, targets, position + 1) circuit = _gray_code_decomposition( - dv, circuit, position, Rz # pyright: ignore[reportArgumentType] + dv, + circuit, + targets, + position, + Rz, # pyright: ignore[reportArgumentType] ) - circuit = _decompose(Vv, circuit, position + 1) + circuit = _decompose(Vv, circuit, targets, position + 1) circuit = _gray_code_decomposition( thetas, circuit, + targets, position, - Ry, + Ry, # pyright: ignore[reportArgumentType] ) - circuit = _decompose(Wu, circuit, position + 1) + circuit = _decompose(Wu, circuit, targets, position + 1) circuit = _gray_code_decomposition( - du, circuit, position, Rz # pyright: ignore[reportArgumentType] + du, + circuit, + targets, + position, + Rz, # pyright: ignore[reportArgumentType] ) - circuit = _decompose(Vu, circuit, position + 1) + circuit = _decompose(Vu, circuit, targets, position + 1) return circuit @@ -216,11 +240,10 @@ def _optimize_circuit(circuit: QCircuit) -> QCircuit: break j += 1 i += 1 - return circuit -def quantum_shannon_decomposition(U: Matrix) -> QCircuit: +def quantum_shannon_decomposition(U: Matrix, targets: list[int]) -> QCircuit: """ Returns a circuit containing the decomposition of a unitary. The resulting circuit is composed of gates CNOT, Ry and Rz. @@ -243,7 +266,7 @@ def quantum_shannon_decomposition(U: Matrix) -> QCircuit: Examples: >>> 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 """ @@ -251,6 +274,6 @@ def quantum_shannon_decomposition(U: Matrix) -> QCircuit: raise ValueError( f"The size of the unitary matrix should be of the form 2**n : got {len(U)}" ) - circuit = QCircuit(int(np.log2(len(U)))) - circuit = _decompose(U, circuit, 0) + circuit = QCircuit() + circuit = _decompose(U, circuit, targets, 0) return _optimize_circuit(circuit) 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 00000000..4274ad75 --- /dev/null +++ b/tests/core/instruction/gates/test_composed_gate.py @@ -0,0 +1,110 @@ +import numpy as np +import pytest + +from mpqp.core.circuit import QCircuit +from mpqp.core.languages import Language +from mpqp.gates import * +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, gate_set", + [ + (Rxx(np.pi / 2, 0, 1), {}), + (Rxx(np.pi / 2, 0, 1), {Rxx}), + (Rxx(np.pi / 2, 0, 1), {Rx, CNOT}), + (Ryy(np.pi / 2, 0, 1), {}), + (Ryy(np.pi / 2, 0, 1), {Ryy}), + (Ryy(np.pi / 2, 0, 1), {Rx, Rz, CNOT}), + (Rzz(np.pi / 2, 0, 1), {}), + (Rzz(np.pi / 2, 0, 1), {Rzz}), + (Rzz(np.pi / 2, 0, 1), {Rz, CNOT}), + (PRX(np.pi / 3, 1, 0), {}), + (PRX(np.pi / 3, 1, 0), {PRX}), + (PRX(np.pi / 3, 1, 0), {Rx, Rz}), + ], +) +def test_composedgate_compatible(gate: Gate, gate_set: set[type[Gate]]) -> None: + QCircuit([gate]).to_other_language(Language.QISKIT, authorized_gates=gate_set) + + +@pytest.mark.parametrize( + "gate, gate_set", + [ + (Rxx(np.pi / 2, 0, 1), {Rx}), + (Rxx(np.pi / 2, 0, 1), {CNOT}), + (Ryy(np.pi / 2, 0, 1), {Rx}), + (Ryy(np.pi / 2, 0, 1), {Rz}), + (Ryy(np.pi / 2, 0, 1), {CNOT}), + (Rzz(np.pi / 2, 0, 1), {Rz}), + (Rzz(np.pi / 2, 0, 1), {CNOT}), + (PRX(np.pi / 3, 1, 0), {Rx}), + (PRX(np.pi / 3, 1, 0), {Rz}), + ], +) +def test_composedgate_notcompatible(gate: Gate, gate_set: set[type[Gate]]) -> None: + with pytest.raises(ValueError): + QCircuit([gate]).to_other_language(Language.QISKIT, authorized_gates=gate_set) + + +def define_parameters(decomposition: bool): + result = [] + providers = [ + Language.QISKIT, + Language.BRAKET, + Language.CIRQ, + Language.MY_QLM, + Language.QASM2, + Language.QASM3, + ] + for gate in COMPOSED_GATES: + for provider in providers: + if decomposition: + result.append((gate, provider, {})) + else: + result.append((gate, provider, {type(gate)})) + return result + + +@pytest.mark.parametrize( + "gate, language, authorized_gates", + define_parameters(False), +) +def test_composedgate_translation_no_decomposition( + gate: Gate, language: Language, authorized_gates: set[type[Gate]] +): + c = QCircuit() + c.add(gate) + translated = c.to_other_language(language, authorized_gates=authorized_gates) + c_re = QCircuit().from_other_language(translated) + assert matrix_eq(c_re.to_matrix(), c.to_matrix()) + + +@pytest.mark.parametrize( + "gate, language, authorized_gates", + define_parameters(True), +) +def test_composedgate_translation_decomposition( + gate: Gate, language: Language, authorized_gates: set[type[Gate]] +): + c = QCircuit() + c.add(gate) + translated = c.to_other_language(language, authorized_gates=authorized_gates) + 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()) diff --git a/tests/core/instruction/gates/test_custom_controlled_gate.py b/tests/core/instruction/gates/test_custom_controlled_gate.py index 2354dfa2..e63189ca 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,63 @@ def test_negative_indices(gate: Type[Gate], args: tuple[Any]): ) def test_inverse(gate: CustomControlledGate, expected: CustomControlledGate): assert gate.inverse() == expected + + +@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 a7513892..2349b99b 100644 --- a/tests/core/instruction/gates/test_custom_gate.py +++ b/tests/core/instruction/gates/test_custom_gate.py @@ -61,7 +61,6 @@ def exec_random_orthogonal_matrix(circ_size: int, device: AvailableDevice): result = run(c, device) # we reduce the precision because of approximation errors coming from CustomGate usage - assert isinstance(result, Result) assert matrix_eq(result.amplitudes, exp_state_vector, 1e-5, 1e-5) @@ -144,8 +143,9 @@ def exec_custom_gate_with_random_circuit(circ_size: int, device: AvailableDevice result1 = run(random_circ, device) result2 = run(custom_gate_circ, device) - assert isinstance(result1, Result) - assert isinstance(result2, Result) + for inst in random_circ.instructions: + print(type(inst)) + # precision reduced from approximation errors (CustomGate usage) assert matrix_eq(result1.amplitudes, result2.amplitudes, 1e-4, 1e-4) diff --git a/tests/core/instruction/gates/test_native_gates.py b/tests/core/instruction/gates/test_native_gates.py index 8f24355c..f87af478 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_expectation_value.py b/tests/core/instruction/measurement/test_expectation_value.py index 82ae65e3..9ba7a3e5 100644 --- a/tests/core/instruction/measurement/test_expectation_value.py +++ b/tests/core/instruction/measurement/test_expectation_value.py @@ -24,23 +24,6 @@ def test_expectation_measure_right_targets(targets: list[int]): ExpectationMeasure(obs, targets) -@pytest.mark.parametrize( - "targets, expected_swaps", - [ - ([1, 3, 4], [{2, 3}, {3, 4}]), - ([1, 0, 2], [{1, 0}]), - ([2, 0, 3], [{0, 2}, {1, 2}, {2, 3}]), - ], -) -def test_expectation_measure_wrong_targets( - targets: list[int], expected_swaps: list[tuple[int, int]] -): - obs = Observable(np.diag([1] * 2 ** len(targets))) - with pytest.warns(UserWarning): - measure = ExpectationMeasure(obs, targets) - assert [set(swap.targets) for swap in measure.pre_measure] == expected_swaps - - # TODO: complete this @pytest.fixture def list_to_cirq_pauli() -> ( diff --git a/tests/core/instruction/test_breakpoint.py b/tests/core/instruction/test_breakpoint.py index bd2fc338..6384916a 100644 --- a/tests/core/instruction/test_breakpoint.py +++ b/tests/core/instruction/test_breakpoint.py @@ -29,19 +29,6 @@ └───┘┌─┴─┐ q_1: ─────┤ X ├ └───┘ -""", - ), - ( - QCircuit([H(0), Y(0), Breakpoint(draw_circuit=True), CNOT(0, 1)]), - """\ -DEBUG: After instruction 2, state is - 0.707j|00⟩ + 0.707j|10⟩ - and circuit is - ┌───┐┌───┐ - q_0: ┤ H ├┤ Y ├ - └───┘└───┘ - q_1: ────────── - """, ), ( diff --git a/tests/core/test_circuit.py b/tests/core/test_circuit.py index 633f8839..14d50deb 100644 --- a/tests/core/test_circuit.py +++ b/tests/core/test_circuit.py @@ -761,8 +761,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 8837a390..bbb457bc 100644 --- a/tests/examples/test_demonstrations.py +++ b/tests/examples/test_demonstrations.py @@ -36,9 +36,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, ], ) diff --git a/tests/execution/test_validity.py b/tests/execution/test_validity.py index 69980245..9b4495a9 100644 --- a/tests/execution/test_validity.py +++ b/tests/execution/test_validity.py @@ -729,7 +729,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 diff --git a/tests/test_doc.py b/tests/test_doc.py index 807eced9..fded9a86 100644 --- a/tests/test_doc.py +++ b/tests/test_doc.py @@ -133,6 +133,7 @@ rand_product_local_unitaries, rand_unitary_2x2_matrix, rand_unitary_matrix, + rearrange_matrix, ) from mpqp.tools.operators import * from mpqp.tools.pauli_grouping import CommutingTypes, pauli_grouping_greedy