diff --git a/docs/tutorial/applying-compilation-passes.md b/docs/tutorial/applying-compilation-passes.md index 056b203f..129c6f7e 100644 --- a/docs/tutorial/applying-compilation-passes.md +++ b/docs/tutorial/applying-compilation-passes.md @@ -367,6 +367,7 @@ circuit.decompose(decomposer=McKayDecomposer()) X90 q[1] Rz(-1.5707963) q[1] CZ q[0], q[1] + Rz(1.5707963) q[0] Rz(-1.5707963) q[1] X90 q[1] Rz(1.5707963) q[1] @@ -374,6 +375,7 @@ circuit.decompose(decomposer=McKayDecomposer()) X90 q[0] Rz(-1.5707963) q[0] CZ q[1], q[0] + Rz(-1.5707963) q[1] Rz(-1.5707963) q[0] X90 q[0] Rz(1.5707963) q[0] @@ -381,6 +383,7 @@ circuit.decompose(decomposer=McKayDecomposer()) X90 q[1] Rz(-1.5707963) q[1] CZ q[0], q[1] + Rz(1.5707963) q[0] Rz(-1.5707963) q[1] X90 q[1] Rz(1.5707963) q[1] @@ -388,6 +391,7 @@ circuit.decompose(decomposer=McKayDecomposer()) X90 q[2] Rz(-1.5707963) q[2] CZ q[1], q[2] + Rz(-2.3561945) q[1] Rz(-1.5707963) q[2] X90 q[2] Rz(1.5707963) q[2] diff --git a/docs/tutorial/writing-out-and-exporting.md b/docs/tutorial/writing-out-and-exporting.md index 9ad79b0b..ab1e41b1 100644 --- a/docs/tutorial/writing-out-and-exporting.md +++ b/docs/tutorial/writing-out-and-exporting.md @@ -53,6 +53,7 @@ circuit to a cQASM 1.0 string. x90 q[1] rz q[1], -1.5707963 cz q[0], q[1] + rz q[0], 1.5707963 rz q[1], -1.5707963 x90 q[1] rz q[1], 1.5707963 @@ -60,6 +61,7 @@ circuit to a cQASM 1.0 string. x90 q[0] rz q[0], -1.5707963 cz q[1], q[0] + rz q[1], -1.5707963 rz q[0], -1.5707963 x90 q[0] rz q[0], 1.5707963 @@ -67,6 +69,7 @@ circuit to a cQASM 1.0 string. x90 q[1] rz q[1], -1.5707963 cz q[0], q[1] + rz q[0], 1.5707963 rz q[1], -1.5707963 x90 q[1] rz q[1], 1.5707963 @@ -74,6 +77,7 @@ circuit to a cQASM 1.0 string. x90 q[2] rz q[2], -1.5707963 cz q[1], q[2] + rz q[1], -2.3561945 rz q[2], -1.5707963 x90 q[2] rz q[2], 1.5707963 diff --git a/opensquirrel/circuit.py b/opensquirrel/circuit.py index dcc142d3..7cdbcd77 100644 --- a/opensquirrel/circuit.py +++ b/opensquirrel/circuit.py @@ -5,6 +5,7 @@ from opensquirrel.ir import IR, AsmDeclaration, Gate from opensquirrel.passes.exporter import ExportFormat +from opensquirrel.phase_record import PhaseRecord if TYPE_CHECKING: from opensquirrel.passes.decomposer.general_decomposer import Decomposer @@ -43,6 +44,7 @@ def __init__(self, register_manager: RegisterManager, ir: IR) -> None: """Create a circuit object from a register manager and an IR.""" self.register_manager = register_manager self.ir = ir + self.phase_record = PhaseRecord(self.register_manager.qubit_register.register_size) def __repr__(self) -> str: """Write the circuit to a cQASM 3 string.""" @@ -102,7 +104,7 @@ def decompose(self, decomposer: Decomposer) -> None: """ from opensquirrel.passes.decomposer import general_decomposer - general_decomposer.decompose(self.ir, decomposer) + general_decomposer.decompose(self, decomposer) def export(self, fmt: ExportFormat | None = None) -> Any: if fmt == ExportFormat.QUANTIFY_SCHEDULER: @@ -139,7 +141,7 @@ def replace(self, gate: type[Gate], replacement_gates_function: Callable[..., li """ from opensquirrel.passes.decomposer import general_decomposer - general_decomposer.replace(self.ir, gate, replacement_gates_function) + general_decomposer.replace(self, gate, replacement_gates_function) def validate(self, validator: Validator) -> None: """Generic validator pass. It applies the given validator to the circuit.""" diff --git a/opensquirrel/common.py b/opensquirrel/common.py index 011fe077..f7a415d1 100644 --- a/opensquirrel/common.py +++ b/opensquirrel/common.py @@ -1,7 +1,7 @@ from __future__ import annotations from math import tau -from typing import SupportsFloat +from typing import SupportsComplex, SupportsFloat import numpy as np from numpy.typing import NDArray @@ -39,6 +39,7 @@ def are_matrices_equivalent_up_to_global_phase( Returns: Whether two matrices are equivalent up to a global phase. """ + first_non_zero = next( (i, j) for i in range(matrix_a.shape[0]) for j in range(matrix_a.shape[1]) if abs(matrix_a[i, j]) > ATOL ) @@ -51,6 +52,24 @@ def are_matrices_equivalent_up_to_global_phase( return np.allclose(matrix_a, phase_difference * matrix_b, atol=ATOL) +def calculate_phase_difference(matrix_a: NDArray[np.complex128], matrix_b: NDArray[np.complex128]) -> np.complex128: + """Calculates the phase difference between two matrices. + Args: + matrix_a: first matrix. + matrix_b: second matrix. + Returns: + The phase difference between the two matrices. + """ + first_non_zero = next( + (i, j) for i in range(matrix_a.shape[0]) for j in range(matrix_a.shape[1]) if abs(matrix_a[i, j]) > ATOL + ) + + if abs(matrix_b[first_non_zero]) < ATOL: + return np.complex128(1) + + return np.complex128(matrix_a[first_non_zero] / matrix_b[first_non_zero]) + + def is_identity_matrix_up_to_a_global_phase(matrix: NDArray[np.complex128]) -> bool: """Checks whether matrix is an identity matrix up to a global phase. @@ -60,3 +79,14 @@ def is_identity_matrix_up_to_a_global_phase(matrix: NDArray[np.complex128]) -> b Whether matrix is an identity matrix up to a global phase. """ return are_matrices_equivalent_up_to_global_phase(matrix, np.eye(matrix.shape[0], dtype=np.complex128)) + + +def get_phase_angle(scalar: SupportsComplex) -> float: + """Derives the Euler rotation angle from a scalar. + Args: + scalar: scalar to convert. + Returns: + Euler phase angle of scalar. + """ + scalar = np.complex128(scalar) + return float(-1j * np.log(scalar)) diff --git a/opensquirrel/ir.py b/opensquirrel/ir.py index 02accefb..e12e58bb 100644 --- a/opensquirrel/ir.py +++ b/opensquirrel/ir.py @@ -276,6 +276,12 @@ def __init__(self, index: QubitLike) -> None: msg = "index must be a QubitLike" raise TypeError(msg) + def __eq__(self, other: Any) -> bool: + """Compare two qubits.""" + if not isinstance(other, Qubit): + return False + return self.__hash__() == other.__hash__() + def __hash__(self) -> int: return hash(str(self.__class__) + str(self.index)) diff --git a/opensquirrel/passes/decomposer/general_decomposer.py b/opensquirrel/passes/decomposer/general_decomposer.py index 72bea18d..277e70bf 100644 --- a/opensquirrel/passes/decomposer/general_decomposer.py +++ b/opensquirrel/passes/decomposer/general_decomposer.py @@ -2,14 +2,25 @@ from abc import ABC, abstractmethod from collections.abc import Callable, Iterable -from typing import Any +from typing import TYPE_CHECKING, Any + +import numpy as np from opensquirrel.circuit_matrix_calculator import get_circuit_matrix -from opensquirrel.common import are_matrices_equivalent_up_to_global_phase, is_identity_matrix_up_to_a_global_phase +from opensquirrel.common import ( + ATOL, + are_matrices_equivalent_up_to_global_phase, + calculate_phase_difference, + get_phase_angle, + is_identity_matrix_up_to_a_global_phase, +) from opensquirrel.default_instructions import is_anonymous_gate -from opensquirrel.ir import IR, Gate +from opensquirrel.ir import Float, Gate, Rz from opensquirrel.reindexer import get_reindexed_circuit +if TYPE_CHECKING: + from opensquirrel.circuit import Circuit + class Decomposer(ABC): def __init__(self, **kwargs: Any) -> None: ... @@ -19,13 +30,23 @@ def decompose(self, gate: Gate) -> list[Gate]: raise NotImplementedError() -def check_gate_replacement(gate: Gate, replacement_gates: Iterable[Gate]) -> None: +def check_gate_replacement(gate: Gate, replacement_gates: Iterable[Gate], circuit: Circuit | None = None) -> list[Gate]: + """ + Verifies the replacement gates against the given gate. + Args: + gate: original gate + replacement_gates: gates replacing the gate + circuit: circuit to verify + Returns: + Returns verified list of replacement gates with possible correction. + """ gate_qubit_indices = [q.index for q in gate.get_qubit_operands()] replacement_gates_qubit_indices = set() replaced_matrix = get_circuit_matrix(get_reindexed_circuit([gate], gate_qubit_indices)) + qubits = gate.get_qubit_operands() if is_identity_matrix_up_to_a_global_phase(replaced_matrix): - return + return [] for g in replacement_gates: replacement_gates_qubit_indices.update([q.index for q in g.get_qubit_operands()]) @@ -40,15 +61,34 @@ def check_gate_replacement(gate: Gate, replacement_gates: Iterable[Gate]) -> Non msg = f"replacement for gate {gate.name} does not preserve the quantum state" raise ValueError(msg) + replacement_gates = list(replacement_gates) + + if circuit is not None: + phase_difference = calculate_phase_difference(replaced_matrix, replacement_matrix) + euler_phase = get_phase_angle(phase_difference) + for qubit in qubits: + circuit.phase_record.add_qubit_phase(qubit, euler_phase) + + if len(qubits) > 1: + relative_phase = float( + np.real( + circuit.phase_record.get_qubit_phase(qubits[0]) - circuit.phase_record.get_qubit_phase(qubits[1]) + ) + ) + if abs(relative_phase) > ATOL: + replacement_gates.append(Rz(qubits[0], Float(relative_phase))) + + return replacement_gates + -def decompose(ir: IR, decomposer: Decomposer) -> None: +def decompose(circuit: Circuit, decomposer: Decomposer) -> None: """Applies `decomposer` to every gate in the circuit, replacing each gate by the output of `decomposer`. When `decomposer` decides to not decomposer a gate, it needs to return a list with the intact gate as single element. """ statement_index = 0 - while statement_index < len(ir.statements): - statement = ir.statements[statement_index] + while statement_index < len(circuit.ir.statements): + statement = circuit.ir.statements[statement_index] if not isinstance(statement, Gate): statement_index += 1 @@ -56,9 +96,9 @@ def decompose(ir: IR, decomposer: Decomposer) -> None: gate = statement replacement_gates: list[Gate] = decomposer.decompose(statement) - check_gate_replacement(gate, replacement_gates) + replacement_gates = check_gate_replacement(gate, replacement_gates, circuit) - ir.statements[statement_index : statement_index + 1] = replacement_gates + circuit.ir.statements[statement_index : statement_index + 1] = replacement_gates statement_index += len(replacement_gates) @@ -73,8 +113,8 @@ def decompose(self, gate: Gate) -> list[Gate]: return self.replacement_gates_function(*gate.arguments) -def replace(ir: IR, gate: type[Gate], replacement_gates_function: Callable[..., list[Gate]]) -> None: +def replace(circuit: Circuit, gate: type[Gate], replacement_gates_function: Callable[..., list[Gate]]) -> None: """Does the same as decomposer, but only applies to a given gate.""" generic_replacer = _GenericReplacer(gate, replacement_gates_function) - decompose(ir, generic_replacer) + decompose(circuit, generic_replacer) diff --git a/opensquirrel/passes/validator/primitive_gate_validator.py b/opensquirrel/passes/validator/primitive_gate_validator.py index 22ce4f1b..ccc7e9bb 100644 --- a/opensquirrel/passes/validator/primitive_gate_validator.py +++ b/opensquirrel/passes/validator/primitive_gate_validator.py @@ -1,3 +1,4 @@ +from collections.abc import Iterable from typing import Any from opensquirrel.ir import IR, Instruction @@ -5,7 +6,7 @@ class PrimitiveGateValidator(Validator): - def __init__(self, primitive_gate_set: list[str], **kwargs: Any) -> None: + def __init__(self, primitive_gate_set: Iterable[str], **kwargs: Any) -> None: super().__init__(**kwargs) self.primitive_gate_set = primitive_gate_set diff --git a/opensquirrel/phase_record.py b/opensquirrel/phase_record.py new file mode 100644 index 00000000..4c3e0e76 --- /dev/null +++ b/opensquirrel/phase_record.py @@ -0,0 +1,18 @@ +from opensquirrel.ir import Qubit, QubitLike + + +class PhaseRecord: + def __init__(self, qubit_register_size: int) -> None: + """Initialize a PhaseMap object.""" + + self.qubit_phase_record = [0.0] * qubit_register_size + + def __contains__(self, qubit: QubitLike) -> bool: + """Checks if qubit is in the phase map.""" + return qubit in self.qubit_phase_record + + def add_qubit_phase(self, qubit: QubitLike, phase: float) -> None: + self.qubit_phase_record[Qubit(qubit).index] += phase + + def get_qubit_phase(self, qubit: QubitLike) -> float: + return float(self.qubit_phase_record[Qubit(qubit).index]) diff --git a/tests/docs/tutorial/test_tutorial.py b/tests/docs/tutorial/test_tutorial.py index 76892a68..64c1d947 100644 --- a/tests/docs/tutorial/test_tutorial.py +++ b/tests/docs/tutorial/test_tutorial.py @@ -174,6 +174,7 @@ def circuit_6() -> Circuit: X90 q[1] Rz(-1.5707963) q[1] CZ q[0], q[1] +Rz(1.5707963) q[0] Rz(-1.5707963) q[1] X90 q[1] Rz(1.5707963) q[1] @@ -181,6 +182,7 @@ def circuit_6() -> Circuit: X90 q[0] Rz(-1.5707963) q[0] CZ q[1], q[0] +Rz(-1.5707963) q[1] Rz(-1.5707963) q[0] X90 q[0] Rz(1.5707963) q[0] @@ -188,6 +190,7 @@ def circuit_6() -> Circuit: X90 q[1] Rz(-1.5707963) q[1] CZ q[0], q[1] +Rz(1.5707963) q[0] Rz(-1.5707963) q[1] X90 q[1] Rz(1.5707963) q[1] @@ -195,6 +198,7 @@ def circuit_6() -> Circuit: X90 q[2] Rz(-1.5707963) q[2] CZ q[1], q[2] +Rz(-2.3561945) q[1] Rz(-1.5707963) q[2] X90 q[2] Rz(1.5707963) q[2] @@ -348,6 +352,7 @@ def test_merging(self, circuit_4: Circuit, circuit_5: Circuit) -> None: def test_decomposition_inferred(self, circuit_5: Circuit, circuit_6: Circuit) -> None: circuit = circuit_5 circuit.decompose(decomposer=McKayDecomposer()) + assert str(circuit) == str(circuit_6) def test_validation(self, circuit_6: Circuit) -> None: @@ -376,6 +381,7 @@ def test_exporting_to_cqasm_v1(self, circuit_6: Circuit) -> None: x90 q[1] rz q[1], -1.5707963 cz q[0], q[1] +rz q[0], 1.5707963 rz q[1], -1.5707963 x90 q[1] rz q[1], 1.5707963 @@ -383,6 +389,7 @@ def test_exporting_to_cqasm_v1(self, circuit_6: Circuit) -> None: x90 q[0] rz q[0], -1.5707963 cz q[1], q[0] +rz q[1], -1.5707963 rz q[0], -1.5707963 x90 q[0] rz q[0], 1.5707963 @@ -390,6 +397,7 @@ def test_exporting_to_cqasm_v1(self, circuit_6: Circuit) -> None: x90 q[1] rz q[1], -1.5707963 cz q[0], q[1] +rz q[0], 1.5707963 rz q[1], -1.5707963 x90 q[1] rz q[1], 1.5707963 @@ -397,6 +405,7 @@ def test_exporting_to_cqasm_v1(self, circuit_6: Circuit) -> None: x90 q[2] rz q[2], -1.5707963 cz q[1], q[2] +rz q[1], -2.3561945 rz q[2], -1.5707963 x90 q[2] rz q[2], 1.5707963 diff --git a/tests/instructions/test_measure.py b/tests/instructions/test_measure.py index 87bf293c..99ff8aa4 100644 --- a/tests/instructions/test_measure.py +++ b/tests/instructions/test_measure.py @@ -164,6 +164,7 @@ def test_multiple_qubit_bit_definitions_and_mid_circuit_measure_instructions() - X90 q[1] Rz(1.5707963) q[1] CNOT q[1], q[0] +Rz(0.78539816) q[1] b[1] = measure q[1] b[0] = measure q[0] """ diff --git a/tests/integration/test_spin_2_plus.py b/tests/integration/test_spin_2_plus.py index 892ce3b9..02239942 100644 --- a/tests/integration/test_spin_2_plus.py +++ b/tests/integration/test_spin_2_plus.py @@ -102,33 +102,40 @@ def test_complete_circuit(self, data: DataType) -> None: X90 q[0] Rz(-1.5707964) q[0] b[0] = measure q[0] -Rz(2.3561946) q[1] +Rz(0.78539824) q[1] X90 q[1] -Rz(3.1415926) q[1] +Rz(1.5707963) q[1] +X90 q[1] +Rz(1.5707963) q[1] b[2] = measure q[1] Rz(1.5707963) q[1] X90 q[1] Rz(-1.5707963) q[1] CZ q[0], q[1] +Rz(-1.5707964) q[0] Rz(-1.5707963) q[1] X90 q[1] Rz(1.5707963) q[1] Rz(3.1415927) q[0] CZ q[1], q[0] +Rz(0.78539828) q[1] Rz(3.1415927) q[0] Rz(3.1415927) q[1] CZ q[0], q[1] +Rz(-0.78539828) q[0] Rz(3.1415927) q[1] Rz(2.3561944) q[0] X90 q[0] Rz(-1.5707963) q[0] CZ q[1], q[0] +Rz(1.5707964) q[1] Rz(-1.5707963) q[0] X90 q[0] Rz(2.3561946) q[0] X90 q[0] Rz(-1.5707963) q[0] CZ q[1], q[0] +Rz(-3.1415925) q[1] Rz(-1.5707963) q[0] X90 q[0] Rz(1.5707963) q[0] @@ -136,6 +143,7 @@ def test_complete_circuit(self, data: DataType) -> None: X90 q[1] Rz(-1.5707963) q[1] CZ q[0], q[1] +Rz(3.1415925) q[0] Rz(-1.5707963) q[1] X90 q[1] Rz(1.5707963) q[1] @@ -143,6 +151,7 @@ def test_complete_circuit(self, data: DataType) -> None: X90 q[0] Rz(-1.5707963) q[0] CZ q[1], q[0] +Rz(-3.1415925) q[1] Rz(-1.5707963) q[0] X90 q[0] Rz(1.5707963) q[0] @@ -150,6 +159,7 @@ def test_complete_circuit(self, data: DataType) -> None: X90 q[1] Rz(-1.5707963) q[1] CZ q[0], q[1] +Rz(3.1415925) q[0] Rz(-1.5707963) q[1] X90 q[1] Rz(1.5707963) q[1] diff --git a/tests/test_phase_record.py b/tests/test_phase_record.py new file mode 100644 index 00000000..0dd49b03 --- /dev/null +++ b/tests/test_phase_record.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import math + +import numpy as np + +from opensquirrel import Circuit, CircuitBuilder +from opensquirrel.passes.decomposer import CNOT2CZDecomposer, CNOTDecomposer, XYXDecomposer +from opensquirrel.passes.merger import SingleQubitGatesMerger + + +def test_high_level_integration_global_phase() -> None: + circuit = Circuit.from_string( + """version 3.0 + + qubit[2] q + + H q[0:1] + CNOT q[1], q[0] + Ry(3.09) q[0] + Ry(0.318) q[1] + CNOT q[1], q[0] + """, + ) + + # Decompose 2-qubit gates to a decomposition where the 2-qubit interactions are captured by CNOT gates + circuit.decompose(decomposer=CNOTDecomposer()) + + # Replace CNOT gates with CZ gates + circuit.decompose(decomposer=CNOT2CZDecomposer()) + # Merge single-qubit gates and decompose with McKay decomposition. + circuit.merge(merger=SingleQubitGatesMerger()) + circuit.decompose(decomposer=XYXDecomposer()) + + assert ( + str(circuit) + == """version 3.0 + +qubit[2] q + +Ry(1.5707963) q[1] +Rx(3.1415927) q[1] +Rx(-1.5707963) q[0] +Ry(3.1415927) q[0] +Rx(1.5707963) q[0] +CZ q[1], q[0] +Ry(0.318) q[1] +Rx(3.1415927) q[0] +Ry(-3.09) q[0] +Rx(3.1415927) q[0] +CZ q[1], q[0] +Rz(3.1415927) q[1] +Ry(1.5707963) q[0] +""" + ) + + +def test_phase_record_circuit_builder() -> None: + circuit_builder = CircuitBuilder(4) + circuit_builder.H(0).H(1).H(2).H(3).Rz(0, math.pi / 8).Ry(1, math.pi / 4).Rx(2, 3 * math.pi / 8).Ry( + 3, math.pi / 2 + ).CNOT(1, 2).CNOT(0, 3).Rz(0, math.pi / 8).Ry(1, math.pi / 4).Rx(2, 3 * math.pi / 8).Ry(3, math.pi / 2).CNOT( + 3, 1 + ).CNOT(2, 0).Rx(2, 3 * math.pi / 8).Rx(1, 3 * math.pi / 8).Rx(0, 3 * math.pi / 8).Rx(3, 3 * math.pi / 8) + circuit = circuit_builder.to_circuit() + circuit.merge(SingleQubitGatesMerger()) + circuit.decompose(XYXDecomposer()) + assert np.allclose(circuit.phase_record.qubit_phase_record, [math.pi / 2, math.pi / 2, -math.pi / 2, math.pi / 2]) diff --git a/tests/test_registers.py b/tests/test_registers.py index 81af6cc2..0812df85 100644 --- a/tests/test_registers.py +++ b/tests/test_registers.py @@ -35,6 +35,7 @@ def test_qubit_variable_b_and_bit_variable_q() -> None: X90 q[1] Rz(1.5707963) q[1] CNOT q[1], q[0] +Rz(0.78539816) q[1] b[1] = measure q[1] b[0] = measure q[0] """ diff --git a/tests/test_replacer.py b/tests/test_replacer.py index 9e297f09..e4bb5990 100644 --- a/tests/test_replacer.py +++ b/tests/test_replacer.py @@ -60,7 +60,7 @@ def decompose(self, g: Gate) -> list[Gate]: return [I(g.qubit), g, I(g.qubit)] return [g] - decompose(circuit.ir, decomposer=TestDecomposer()) + decompose(circuit, decomposer=TestDecomposer()) builder2 = CircuitBuilder(3) builder2.I(0) @@ -76,7 +76,7 @@ def test_replace(self) -> None: builder1.H(0) circuit = builder1.to_circuit() - replace(circuit.ir, H, lambda q: [Y90(q), X(q)]) + replace(circuit, H, lambda q: [Y90(q), X(q)]) builder2 = CircuitBuilder(3) builder2.Y90(0)