diff --git a/opensquirrel/circuit_builder.py b/opensquirrel/circuit_builder.py index 9b01497c..58c30960 100644 --- a/opensquirrel/circuit_builder.py +++ b/opensquirrel/circuit_builder.py @@ -9,15 +9,16 @@ from typing_extensions import Self from opensquirrel.circuit import Circuit +from opensquirrel.default_directives import default_directive_set from opensquirrel.default_gates import default_gate_aliases, default_gate_set from opensquirrel.default_measures import default_measure_set from opensquirrel.default_resets import default_reset_set -from opensquirrel.instruction_library import GateLibrary, MeasureLibrary, ResetLibrary -from opensquirrel.ir import ANNOTATIONS_TO_TYPE_MAP, IR, Comment, Gate, Measure, Qubit, QubitLike, Reset +from opensquirrel.instruction_library import DirectiveLibrary, GateLibrary, MeasureLibrary, ResetLibrary +from opensquirrel.ir import ANNOTATIONS_TO_TYPE_MAP, IR, Comment, Directive, Gate, Measure, Qubit, QubitLike, Reset from opensquirrel.register_manager import BitRegister, QubitRegister, RegisterManager -class CircuitBuilder(GateLibrary, MeasureLibrary, ResetLibrary): +class CircuitBuilder(GateLibrary, MeasureLibrary, ResetLibrary, DirectiveLibrary): """ A class using the builder pattern to make construction of circuits easy from Python. Adds corresponding instruction when a method is called. Checks that instructions are known and called with the right @@ -35,7 +36,7 @@ class CircuitBuilder(GateLibrary, MeasureLibrary, ResetLibrary): >>> CircuitBuilder(qubit_register_size=3, bit_register_size=3).\ H(0).CNOT(0, 1).CNOT(0, 2).\ to_circuit() - version 3.0 + version 3. qubit[3] q @@ -54,10 +55,12 @@ def __init__( gate_aliases: Mapping[str, Callable[..., Gate]] = default_gate_aliases, measure_set: list[Callable[..., Measure]] = default_measure_set, reset_set: list[Callable[..., Reset]] = default_reset_set, + directive_set: list[Callable[..., Directive]] = default_directive_set, ) -> None: GateLibrary.__init__(self, gate_set, gate_aliases) MeasureLibrary.__init__(self, measure_set) ResetLibrary.__init__(self, reset_set) + DirectiveLibrary.__init__(self, directive_set) self.register_manager = RegisterManager(QubitRegister(qubit_register_size), BitRegister(bit_register_size)) self.ir = IR() @@ -80,6 +83,10 @@ def _add_instruction(self, attr: str, *args: Any) -> Self: generator_f_reset = ResetLibrary.get_reset_f(self, attr) self._check_generator_f_args(generator_f_reset, attr, args) self.ir.add_reset(generator_f_reset(*args)) + elif any(attr == directive.__name__ for directive in self.directive_set): + generator_f_directive = DirectiveLibrary.get_directive_f(self, attr) + self._check_generator_f_args(generator_f_directive, attr, args) + self.ir.add_directive(generator_f_directive(*args)) else: generator_f_gate = GateLibrary.get_gate_f(self, attr) self._check_generator_f_args(generator_f_gate, attr, args) @@ -109,7 +116,7 @@ def _check_bit_out_of_bounds_access(self, index: int) -> None: def _check_generator_f_args( self, - generator_f: Callable[..., Gate | Measure | Reset], + generator_f: Callable[..., Gate | Measure | Reset | Directive], attr: str, args: tuple[Any, ...], ) -> None: diff --git a/opensquirrel/default_directives.py b/opensquirrel/default_directives.py new file mode 100644 index 00000000..fdb08e12 --- /dev/null +++ b/opensquirrel/default_directives.py @@ -0,0 +1,9 @@ +from opensquirrel.ir import Barrier, QubitLike, named_directive + + +@named_directive +def barrier(q: QubitLike) -> Barrier: + return Barrier(q) + + +default_directive_set = [barrier] diff --git a/opensquirrel/default_gates.py b/opensquirrel/default_gates.py index 2506da36..facdf385 100644 --- a/opensquirrel/default_gates.py +++ b/opensquirrel/default_gates.py @@ -137,13 +137,7 @@ def CRk(control: QubitLike, target: QubitLike, k: SupportsInt) -> ControlledGate Rz, ] default_gate_set: list[Callable[..., Gate]] -default_gate_set = [ - *default_bloch_sphere_rotations, - CNOT, - CZ, - CR, - CRk, -] +default_gate_set = [*default_bloch_sphere_rotations, CNOT, CZ, CR, CRk] default_gate_aliases = { "Hadamard": H, diff --git a/opensquirrel/instruction_library.py b/opensquirrel/instruction_library.py index 888fe174..3a5d6a45 100644 --- a/opensquirrel/instruction_library.py +++ b/opensquirrel/instruction_library.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from opensquirrel.ir import Gate, Measure, Reset + from opensquirrel.ir import Directive, Gate, Measure, Reset class InstructionLibrary: @@ -53,3 +53,15 @@ def get_reset_f(self, reset_name: str) -> Callable[..., Reset]: except StopIteration as exc: msg = f"unknown instruction `{reset_name}`" raise ValueError(msg) from exc + + +class DirectiveLibrary(InstructionLibrary): + def __init__(self, directive_set: Iterable[Callable[..., Directive]]) -> None: + self.directive_set = directive_set + + def get_directive_f(self, directive_name: str) -> Callable[..., Directive]: + try: + return next(f for f in self.directive_set if f.__name__ == directive_name) + except StopIteration as exc: + msg = f"unknown instruction `{directive_name}`" + raise ValueError(msg) from exc diff --git a/opensquirrel/ir.py b/opensquirrel/ir.py index dc423b9b..3070e2b3 100644 --- a/opensquirrel/ir.py +++ b/opensquirrel/ir.py @@ -58,6 +58,12 @@ def visit_matrix_gate(self, matrix_gate: MatrixGate) -> Any: def visit_controlled_gate(self, controlled_gate: ControlledGate) -> Any: pass + def visit_directive(self, directive: Directive) -> Any: + pass + + def visit_barrier(self, barrier: Barrier) -> Any: + pass + class IRNode(ABC): @abstractmethod @@ -380,6 +386,66 @@ def get_qubit_operands(self) -> list[Qubit]: return [self.qubit] +class Directive(Statement, ABC): + def __init__( + self, + generator: Callable[..., Directive] | None = None, + arguments: tuple[Expression, ...] | None = None, + ) -> None: + self.generator = generator + self.arguments = arguments + + @property + def name(self) -> str: + if self.generator: + return self.generator.__name__ + return "Anonymous directive: " + self.__repr__() + + @property + def is_abstract(self) -> bool: + return self.arguments is None + + @abstractmethod + def get_qubit_operands(self) -> list[Qubit]: + """Get the qubit operands of the Gate. + + Returns: + List of qubits on which the Gate operates. + """ + + +class Barrier(Directive): + def __init__( + self, + qubit: QubitLike, + generator: Callable[..., Barrier] | None = None, + arguments: tuple[Expression, ...] | None = None, + ) -> None: + Directive.__init__(self, generator, arguments) + self.generator = generator + self.arguments = arguments + self.qubit = Qubit(qubit) + + def __repr__(self) -> str: + return f"Barrier(qubit={self.qubit})" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Barrier): + return False + + return self.qubit == other.qubit + + def accept(self, visitor: IRVisitor) -> Any: + visitor.visit_directive(self) + return visitor.visit_barrier(self) + + def get_qubit_operands(self) -> list[Qubit]: + return [self.qubit] + + def is_identity(self) -> bool: + return False + + class Gate(Statement, ABC): def __init__( self, @@ -657,6 +723,32 @@ def wrapper(*args: Any, **kwargs: Any) -> Reset: return wrapper +def named_directive(directive_generator: Callable[..., Directive]) -> Callable[..., Directive]: + @wraps(directive_generator) + def wrapper(*args: Any, **kwargs: Any) -> Directive: + result = directive_generator(*args, **kwargs) + result.generator = wrapper + + all_args: list[Any] = [] + for par in inspect.signature(directive_generator).parameters.values(): + next_arg = kwargs[par.name] if par.name in kwargs else args[len(all_args)] + next_annotation = ( + ANNOTATIONS_TO_TYPE_MAP[par.annotation] if isinstance(par.annotation, str) else par.annotation + ) + + # Convert to correct expression for IR + if is_qubit_like_annotation(next_annotation): + next_arg = Qubit(next_arg) + + # Append parsed argument + all_args.append(next_arg) + + result.arguments = tuple(all_args) + return result + + return wrapper + + def compare_gates(g1: Gate, g2: Gate) -> bool: union_mapping = [q.index for q in list(set(g1.get_qubit_operands()) | set(g2.get_qubit_operands()))] @@ -702,6 +794,9 @@ def add_statement(self, statement: Statement) -> None: def add_comment(self, comment: Comment) -> None: self.statements.append(comment) + def add_directive(self, directive: Directive) -> None: + self.statements.append(directive) + def __eq__(self, other: object) -> bool: if not isinstance(other, IR): return False diff --git a/opensquirrel/merger/general_merger.py b/opensquirrel/merger/general_merger.py index 944ffa33..b6f7f6b0 100644 --- a/opensquirrel/merger/general_merger.py +++ b/opensquirrel/merger/general_merger.py @@ -1,11 +1,18 @@ +from __future__ import annotations + +import copy from math import acos, cos, floor, log10, sin +from typing import TYPE_CHECKING, Any import numpy as np -from opensquirrel.circuit import Circuit from opensquirrel.common import ATOL from opensquirrel.default_gates import I, default_bloch_sphere_rotations_without_params -from opensquirrel.ir import BlochSphereRotation, Comment, Qubit +from opensquirrel.ir import Barrier, BlochSphereRotation, Comment, Qubit, Statement +from opensquirrel.utils.list import flatten_irregular_list, flatten_list + +if TYPE_CHECKING: + from opensquirrel.circuit import Circuit def compose_bloch_sphere_rotations(a: BlochSphereRotation, b: BlochSphereRotation) -> BlochSphereRotation: @@ -78,6 +85,87 @@ def try_name_anonymous_bloch(bsr: BlochSphereRotation) -> BlochSphereRotation: return bsr +def merge_barriers(statement_list: list[Statement]) -> list[Statement]: + """Receives a list of statements. + Returns an ordered version of the input list of statements where groups of barriers are merged together, + and placed as late in the list as possible. + + Args: + statement_list: list of statements + + Returns: + Statement list with the barriers merged. + """ + barrier_list: list[Barrier] = [] + ordered_statement_list: list[Statement] = [] + for statement in statement_list: + if isinstance(statement, Comment): + continue + if isinstance(statement, Barrier): + barrier_list.append(statement) + else: + if len(barrier_list) > 0 and hasattr(statement, "get_qubit_operands"): + instruction_qubits = statement.get_qubit_operands() + barrier_qubits = flatten_list([barrier.get_qubit_operands() for barrier in barrier_list]) + if any(qubit in barrier_qubits for qubit in instruction_qubits): + ordered_statement_list.extend(barrier_list) + barrier_list = [] + ordered_statement_list.append(statement) + + if len(barrier_list) > 0: + ordered_statement_list.extend(barrier_list) + + return ordered_statement_list + + +def sticky_barriers(initial_circuit: list[Statement], current_circuit: list[Statement]) -> list[Statement]: + """This process takes the initial circuit inputted by the user and joins the barriers that were originally + placed together before the single qubit gate merge. + + Args: + initial_circuit: The original order of the statement list + current_circuit: The current order of the statement list + + Returns: + List of statements with the respected original barrier positions + """ + barrier_groups: list[list[Barrier]] = [] + local_group = [] + modified_circuit: list[Any] = copy.deepcopy(current_circuit) + + for i, statement in enumerate(initial_circuit): + if isinstance(statement, Barrier): + local_group.append(statement) + elif len(local_group) > 0: + barrier_groups.append(local_group) + local_group = [] + if len(local_group) > 0 and i + 1 == len(initial_circuit): + barrier_groups.append(local_group) + local_group = [] + + group_counter = 0 + placement_counter = 0 + if len(barrier_groups) > 0: + for i, statement in enumerate(modified_circuit): + if placement_counter != 0: + placement_counter -= 1 + continue + if barrier_groups[group_counter][-1] == statement: + del modified_circuit[i] + modified_circuit.insert(i, barrier_groups[group_counter]) + placement_counter = len(barrier_groups[group_counter]) + group_counter += 1 + + elif isinstance(statement, Barrier): + modified_circuit[i] = None + + modified_circuit = [statement for statement in modified_circuit if statement is not None] + + modified_circuit = flatten_irregular_list(modified_circuit) + + return modified_circuit + + def merge_single_qubit_gates(circuit: Circuit) -> None: """Merge all consecutive 1-qubit gates in the circuit. @@ -92,6 +180,7 @@ def merge_single_qubit_gates(circuit: Circuit) -> None: ir = circuit.ir statement_index = 0 + initial_circuit = copy.deepcopy(circuit.ir.statements) while statement_index < len(ir.statements): statement = ir.statements[statement_index] @@ -126,3 +215,5 @@ def merge_single_qubit_gates(circuit: Circuit) -> None: if accumulated_bloch_sphere_rotation.is_anonymous: accumulated_bloch_sphere_rotation = try_name_anonymous_bloch(accumulated_bloch_sphere_rotation) ir.statements.append(accumulated_bloch_sphere_rotation) + + ir.statements = sticky_barriers(initial_circuit, ir.statements) diff --git a/opensquirrel/utils/list.py b/opensquirrel/utils/list.py new file mode 100644 index 00000000..5d302cd6 --- /dev/null +++ b/opensquirrel/utils/list.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import functools +import operator +from typing import Any + + +def flatten_list(list_to_flatten: list[list[Any]]) -> list[Any]: + return functools.reduce(operator.iadd, list_to_flatten, []) + + +def flatten_irregular_list(list_to_flatten: list[Any]) -> list[Any]: + if isinstance(list_to_flatten, list): + return [element for i in list_to_flatten for element in flatten_irregular_list(i)] + + return [list_to_flatten] diff --git a/opensquirrel/writer/writer.py b/opensquirrel/writer/writer.py index 833b3e69..94a2f995 100644 --- a/opensquirrel/writer/writer.py +++ b/opensquirrel/writer/writer.py @@ -2,7 +2,19 @@ from typing import SupportsInt from opensquirrel.circuit import Circuit -from opensquirrel.ir import Bit, Comment, Float, Gate, Int, IRVisitor, Measure, Qubit, QubitLike, Reset +from opensquirrel.ir import ( + Bit, + Comment, + Directive, + Float, + Gate, + Int, + IRVisitor, + Measure, + Qubit, + QubitLike, + Reset, +) from opensquirrel.register_manager import RegisterManager @@ -54,6 +66,13 @@ def visit_reset(self, reset: Reset) -> None: qubit_argument = reset.arguments[0].accept(self) # type: ignore[index] self.output += f"{reset.name} {qubit_argument}\n" + def visit_directive(self, directive: Directive) -> None: + if directive.is_abstract: + self.output += f"{directive.name}\n" + return + qubit_argument = directive.arguments[0].accept(self) # type: ignore[index] + self.output += f"{directive.name} {qubit_argument}\n" + def visit_gate(self, gate: Gate) -> None: gate_name = gate.name gate_generator = [] diff --git a/test/merger/test_merger.py b/test/merger/test_merger.py index 72ed0cd3..b833217c 100644 --- a/test/merger/test_merger.py +++ b/test/merger/test_merger.py @@ -1,6 +1,8 @@ import math -from opensquirrel import CircuitBuilder +import pytest + +from opensquirrel import Circuit, CircuitBuilder from opensquirrel.default_gates import Ry, Rz from opensquirrel.ir import Bit, BlochSphereRotation, Float, Qubit from opensquirrel.merger import general_merger @@ -169,3 +171,195 @@ def test_no_merge_across_reset() -> None: builder2.reset(0) expected_qc = builder2.to_circuit() modify_circuit_and_check(qc, general_merger.merge_single_qubit_gates, expected_qc) + + +@pytest.mark.parametrize( + ("circuit", "expected_result"), + [ + ( + (CircuitBuilder(2).H(0).barrier(0).H(1).barrier(1).H(0).Rx(0, Float(math.pi / 3)).barrier(0).to_circuit()), + """version 3.0 + +qubit[2] q + +H q[0] +H q[1] +barrier q[0] +barrier q[1] +Anonymous gate: BlochSphereRotation(Qubit[0], axis=[ 0.65465 -0.37796 0.65465], angle=-2.41886, phase=1.5708) +barrier q[0] +""", + ), + ( + (CircuitBuilder(2).X(0).barrier(0).X(1).barrier(1).CNOT(0, 1).barrier(1).X(1).to_circuit()), + """version 3.0 + +qubit[2] q + +X q[0] +X q[1] +barrier q[0] +barrier q[1] +CNOT q[0], q[1] +barrier q[1] +X q[1] +""", + ), + ( + (CircuitBuilder(2).X(0).X(1).barrier(0).barrier(1).X(0).to_circuit()), + """version 3.0 + +qubit[2] q + +X q[0] +X q[1] +barrier q[0] +barrier q[1] +X q[0] +""", + ), + ( + ( + CircuitBuilder(4) + .H(0) + .barrier(0) + .H(1) + .barrier(1) + .H(2) + .barrier(2) + .H(3) + .barrier(3) + .CNOT(0, 3) + .barrier(0) + .barrier(1) + .barrier(3) + .to_circuit() + ), + """version 3.0 + +qubit[4] q + +H q[0] +H q[1] +H q[2] +H q[3] +barrier q[0] +barrier q[1] +barrier q[2] +barrier q[3] +CNOT q[0], q[3] +barrier q[0] +barrier q[1] +barrier q[3] +""", + ), + ], + ids=["anonymous_gate", "circuit_with_multi_and_single_qubit_gates", "unpacking_usecase", "circuit_with_4_qubits"], +) +def test_merge_barriers(circuit: Circuit, expected_result: str) -> None: + from opensquirrel.merger.general_merger import merge_barriers + + circuit.merge_single_qubit_gates() + circuit.ir.statements = merge_barriers(circuit.ir.statements) + assert str(circuit) == expected_result + + +@pytest.mark.parametrize( + ("circuit", "expected_result"), + [ + ( + ( + CircuitBuilder(2) + .H(1) + .Rz(1, Float(2.332)) + .H(0) + .Rx(0, Float(1.423)) + .barrier(0) + .barrier(1) + .H(1) + .Rz(1, Float(2.332)) + .H(0) + .Rx(1, Float(1.423)) + .barrier(0) + .barrier(1) + .to_circuit() + ), + """version 3.0 + +qubit[2] q + +Anonymous gate: BlochSphereRotation(Qubit[0], axis=[ 0.60376 -0.52053 0.60376], angle=-2.18173, phase=1.5708) +Anonymous gate: BlochSphereRotation(Qubit[1], axis=[0.36644 0.85525 0.36644], angle=-1.72653, phase=1.5708) +barrier q[0] +barrier q[1] +H q[0] +barrier q[0] +Anonymous gate: BlochSphereRotation(Qubit[1], axis=[ 0.28903 -0.42028 -0.86013], angle=1.66208, phase=1.5708) +barrier q[0] +barrier q[1] +""", + ), + ( + CircuitBuilder(3) + .H(0) + .Ry(0, Float(2)) + .barrier(2) + .barrier(0) + .barrier(1) + .H(0) + .H(1) + .Ry(1, Float(2)) + .barrier(1) + .H(2) + .barrier(2) + .barrier(0) + .barrier(1) + .to_circuit(), + """version 3.0 + +qubit[3] q + +Anonymous gate: BlochSphereRotation(Qubit[0], axis=[ 0.97706 0. -0.21296], angle=3.14159, phase=1.5708) +barrier q[2] +barrier q[0] +barrier q[1] +Anonymous gate: BlochSphereRotation(Qubit[1], axis=[ 0.97706 0. -0.21296], angle=3.14159, phase=1.5708) +barrier q[1] +H q[2] +H q[0] +barrier q[1] +""", + ), + ( + CircuitBuilder(2) + .H(0) + .Ry(0, Float(2)) + .barrier(1) + .barrier(0) + .barrier(1) + .H(0) + .H(1) + .Ry(1, Float(2)) + .barrier(1) + .to_circuit(), + """version 3.0 + +qubit[2] q + +barrier q[1] +barrier q[0] +barrier q[1] +Anonymous gate: BlochSphereRotation(Qubit[0], axis=[ 0.97706 0. -0.21296], angle=3.14159, phase=1.5708) +barrier q[0] +barrier q[1] +Anonymous gate: BlochSphereRotation(Qubit[1], axis=[ 0.97706 0. -0.21296], angle=3.14159, phase=1.5708) +barrier q[1] +H q[0] +""", + ), + ], + ids=["generic_case", "circuit_with_irregular_barrier_order", "repeating_barrier"], +) +def test_sticky_barriers(circuit: Circuit, expected_result: str) -> None: + circuit.merge_single_qubit_gates() + assert str(circuit) == expected_result