diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f4b5a2..cc4ee5b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added - The cycle time [seconds] can be set when instantiating the `QuantifySchedulerExporter` through the `cycle_time` -parameter. -- `CircuitBuilder` accepts multiple (qu)bit registers through `add_register` method. +parameter +- `CircuitBuilder` accepts multiple (qu)bit registers through `add_register` method +- Docstrings added to (user facing) public methods - Add `interaction_graph` as a property of the `Circuit`. ## [ 0.9.0 ] - [ 2025-12-19 ] diff --git a/mkdocs.yaml b/mkdocs.yaml index d3c622c0..e272821a 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -119,6 +119,16 @@ plugins: - scripts/gen_reference_page.py - literate-nav: nav_file: reference.md - - mkdocstrings + - autorefs + - mkdocstrings: + handlers: + python: + options: + docstring_style: google + show_bases: true + show_signature: true + separate_signature: true + show_signature_annotations: true - glightbox - search + diff --git a/opensquirrel/circuit.py b/opensquirrel/circuit.py index 15e41bab..78e168c7 100644 --- a/opensquirrel/circuit.py +++ b/opensquirrel/circuit.py @@ -29,25 +29,33 @@ class Circuit: """The Circuit class is the only interface to access OpenSquirrel's features. - Examples: - >>> c = Circuit.from_string("version 3.0; qubit[3] q; h q[0]") - >>> c + Example: + ```python + >>> circuit = Circuit.from_string("version 3.0; qubit[3] q; h q[0]") + >>> circuit + ``` + ``` version 3.0 - + qubit[3] q - + h q[0] - - >>> c.decomposer(decomposer=mckay_decomposer.McKayDecomposer) - >>> c + ``` + ```python + >>> circuit.decompose(decomposer=McKayDecomposer()) + >>> circuit + ``` + ``` version 3.0 - + qubit[3] q - + x90 q[0] rz q[0], 1.5707963 x90 q[0] - + + ``` + """ def __init__(self, register_manager: RegisterManager, ir: IR) -> None: @@ -55,33 +63,20 @@ def __init__(self, register_manager: RegisterManager, ir: IR) -> None: self.register_manager = register_manager self.ir = ir - def __repr__(self) -> str: - """Write the circuit to a cQASM 3 string.""" - from opensquirrel.writer import writer - - return writer.circuit_to_string(self) - - def __eq__(self, other: Any) -> bool: - if not isinstance(other, Circuit): - return False - return self.register_manager == other.register_manager and self.ir == other.ir - @classmethod - def from_string(cls, cqasm3_string: str) -> Circuit: - """Create a circuit object from a cQasm3 string. All the gates in the circuit need to be defined in - the `gates` argument. - - * type-checking is performed, eliminating qubit indices errors and incoherences - * checks that used gates are supported and mentioned in `gates` with appropriate signatures - * does not support map or variables, and other things... - * for example of `gates` dictionary, please look at TestGates.py + def from_string(cls, cqasm_string: str) -> Circuit: + """Create a circuit from a [cQASM](https://qutech-delft.github.io/cQASM-spec/) string. Args: - cqasm3_string: a cQASM 3 string + cqasm_string (str): A cQASM string. + + Returns: + Circuit: The circuit generated from the cQASM string. + """ from opensquirrel.reader import LibQasmParser - return LibQasmParser().circuit_from_string(cqasm3_string) + return LibQasmParser().circuit_from_string(cqasm_string) @property def qubit_register_size(self) -> int: @@ -135,6 +130,15 @@ def interaction_graph(self) -> InteractionGraph: return graph def asm_filter(self, backend_name: str) -> None: + """Filter the assembly declarations in the circuit for a specific backend. + + Note: + This will remove all assembly declarations that do not match the specified backend name. + + Args: + backend_name (str): The backend name to filter for. + + """ self.ir.statements = [ statement for statement in self.ir.statements @@ -143,23 +147,32 @@ def asm_filter(self, backend_name: str) -> None: ] def decompose(self, decomposer: Decomposer) -> None: - """Generic decomposition pass. - It applies the given decomposer function to every gate in the circuit. + """Decomposes the circuit using to the specified decomposer. + + Args: + decomposer (Decomposer): The decomposer to apply. + """ from opensquirrel.passes.decomposer import general_decomposer general_decomposer.decompose(self.ir, decomposer) def export(self, exporter: Exporter) -> Any: - """Generic export pass. - Exports the circuit using the specified exporter. + """Exports the circuit using the specified exporter. + + Args: + exporter (Exporter): The exporter to apply. """ return exporter.export(self) def map(self, mapper: Mapper) -> None: - """Generic qubit mapper pass. - Map the (virtual) qubits of the circuit to the physical qubits of the target hardware. + """Maps the (virtual) qubits of the circuit to the physical qubits of the target hardware + using the specified mapper. + + Args: + mapper (Mapper): The mapper to apply. + """ from opensquirrel.passes.mapper.qubit_remapper import remap_ir @@ -168,22 +181,51 @@ def map(self, mapper: Mapper) -> None: remap_ir(self, mapping) def merge(self, merger: Merger) -> None: - """Generic merge pass. It applies the given merger to the circuit.""" + """Merges the circuit using the specified merger. + + Args: + merger (Merger): The merger to apply. + + """ merger.merge(self.ir, self.qubit_register_size) def route(self, router: Router) -> None: - """Generic router pass. It applies the given router to the circuit.""" + """Routes the circuit using the specified router. + + Args: + router (Router): The router to apply. + + """ router.route(self.ir, self.qubit_register_size) def replace(self, gate: type[Gate], replacement_gates_function: Callable[..., list[Gate]]) -> None: """Manually replace occurrences of a given gate with a list of gates. - `replacement_gates_function` is a callable that takes the arguments of the gate that is to be replaced and - returns the decomposition as a list of gates. + + Args: + gate (type[Gate]): The gate type to be replaced. + replacement_gates_function (Callable[..., list[Gate]]): function that describes the replacement gates. + """ from opensquirrel.passes.decomposer import general_decomposer general_decomposer.replace(self.ir, gate, replacement_gates_function) def validate(self, validator: Validator) -> None: - """Generic validator pass. It applies the given validator to the circuit.""" + """Validates the circuit using the specified validator. + + Args: + validator (Validator): The validator to apply. + + """ validator.validate(self.ir) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Circuit): + return False + return self.register_manager == other.register_manager and self.ir == other.ir + + def __repr__(self) -> str: + """Write the circuit to a cQASM 3 string.""" + from opensquirrel.writer import writer + + return writer.circuit_to_string(self) diff --git a/opensquirrel/circuit_builder.py b/opensquirrel/circuit_builder.py index 14747381..bfa579eb 100644 --- a/opensquirrel/circuit_builder.py +++ b/opensquirrel/circuit_builder.py @@ -24,8 +24,8 @@ class CircuitBuilder: """ 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 - arguments. + Adds corresponding instruction when a method is called. Checks that instructions are known and + called with the right arguments. Mainly here to allow for Qiskit-style circuit construction: Args: @@ -33,17 +33,19 @@ class CircuitBuilder: bit_register_size (int): Size of the bit register Example: - >>> CircuitBuilder(qubit_register_size=3, bit_register_size=3).\ - H(0).CNOT(0, 1).CNOT(0, 2).\ - to_circuit() + ```python + >>> CircuitBuilder(qubit_register_size=3, bit_register_size=3).H(0).CNOT(0, 1).CNOT(0, 2).to_circuit() + ``` + ``` version 3.0 - + qubit[3] q - + h q[0] cnot q[0], q[1] cnot q[0], q[2] - + + ``` """ def __init__( @@ -77,6 +79,12 @@ def __getattr__(self, attr: str) -> Any: return self.__getattribute__(attr) def add_register(self, register: QubitRegister | BitRegister) -> None: + """Add a (qu)bit register to the circuit builder. + + Args: + register (QubitRegister | BitRegister): (Qu)bit register to add. + + """ self.register_manager.add_register(register) def _check_qubit_out_of_bounds_access(self, qubit: QubitLike) -> None: @@ -135,4 +143,10 @@ def _add_statement(self, attr: str, *args: Any) -> Self: return self def to_circuit(self) -> Circuit: + """Build the circuit. + + Returns: + Circuit: The built circuit. + + """ return Circuit(deepcopy(self.register_manager), deepcopy(self.ir)) diff --git a/opensquirrel/circuit_matrix_calculator.py b/opensquirrel/circuit_matrix_calculator.py index 79e699bd..30bf1fc1 100644 --- a/opensquirrel/circuit_matrix_calculator.py +++ b/opensquirrel/circuit_matrix_calculator.py @@ -25,11 +25,14 @@ def visit_gate(self, gate: Gate) -> None: def get_circuit_matrix(circuit: Circuit) -> NDArray[np.complex128]: """Compute the (large) unitary matrix corresponding to the circuit. - This matrix has 4**n elements, where n is the number of qubits. Result is stored as a numpy array of complex - numbers. + This matrix has $4^n$ elements, where $n$ is the number of qubits. + + Args: + circuit (Circuit): The circuit for which to compute the matrix. Returns: Matrix representation of the circuit. + """ impl = _CircuitMatrixCalculator(circuit.qubit_register_size) diff --git a/opensquirrel/common.py b/opensquirrel/common.py index 67f29d3e..99e02856 100644 --- a/opensquirrel/common.py +++ b/opensquirrel/common.py @@ -15,13 +15,14 @@ def normalize_angle(x: SupportsFloat) -> float: - r"""Normalize the angle to be in between the range of $(-\pi, \pi]$. + """Normalize the angle to be in between the range of $(-\\pi, \\pi]$. Args: - x: value to normalize. + x (SupportsFloat): value to normalize. Returns: The normalized angle. + """ x = float(x) t = x - tau * (x // tau + 1) @@ -38,11 +39,12 @@ def are_matrices_equivalent_up_to_global_phase( """Checks whether two matrices are equivalent up to a global phase. Args: - matrix_a: first matrix. - matrix_b: second matrix. + matrix_a (NDArray[np.complex128]): first matrix. + matrix_b (NDArray[np.complex128]): second matrix. Returns: - Whether two matrices are equivalent up to a global phase. + True if two matrices are equivalent up to a global phase, otherwise False. + """ 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 @@ -60,21 +62,28 @@ def is_identity_matrix_up_to_a_global_phase(matrix: NDArray[np.complex128]) -> b """Checks whether matrix is an identity matrix up to a global phase. Args: - matrix: matrix to check. + matrix (NDArray[np.complex128]): matrix to check. + Returns: - Whether matrix is an identity matrix up to a global phase. + True if matrix is an identity matrix up to a global phase, otherwise False. + """ return are_matrices_equivalent_up_to_global_phase(matrix, np.eye(matrix.shape[0], dtype=np.complex128)) def repr_round(value: float | BaseAxis | NDArray[np.complex128], decimals: int = REPR_DECIMALS) -> str: - """ - Given a numerical value (of type `float`, `Axis`, or `NDArray[np.complex128]`): + """Given a numerical value: + - rounds it to `REPR_DECIMALS`, - converts it to string, and - - removes the newlines. + - removes any newlines. + + Args: + value (float | BaseAxis | NDArray[np.complex128]): The numerical value to represent. + decimals (int): Number of decimals to round to. Default is `REPR_DECIMALS`. Returns: - A single-line string representation of a numerical value. + A single-line string representation of a rounded numerical value. + """ return f"{np.round(value, decimals)}".replace("\n", "") diff --git a/opensquirrel/default_instructions.py b/opensquirrel/default_instructions.py index bf285ab4..779c11c7 100644 --- a/opensquirrel/default_instructions.py +++ b/opensquirrel/default_instructions.py @@ -108,4 +108,13 @@ def is_anonymous_gate(name: str) -> bool: + """Checks whether the input name corresponds to a non-default gate. + + Args: + name (str): Name of the gate. + + Returns: + True if the name does not correspond to a default gate, otherwise False. + + """ return name not in default_gate_set diff --git a/opensquirrel/ir/control_instruction.py b/opensquirrel/ir/control_instruction.py index 2a22755c..2350edb4 100644 --- a/opensquirrel/ir/control_instruction.py +++ b/opensquirrel/ir/control_instruction.py @@ -13,8 +13,7 @@ def __init__(self, qubit: QubitLike, name: str) -> None: @property @abstractmethod - def arguments(self) -> tuple[Expression, ...]: - pass + def arguments(self) -> tuple[Expression, ...]: ... @property def qubit_operands(self) -> tuple[Qubit, ...]: @@ -30,20 +29,21 @@ def __init__(self, qubit: QubitLike) -> None: ControlInstruction.__init__(self, qubit=qubit, name="barrier") self.qubit = Qubit(qubit) - def __repr__(self) -> str: - return f"{self.__class__.__name__}(qubit={self.qubit})" - - def __eq__(self, other: object) -> bool: - return isinstance(other, Barrier) and self.qubit == other.qubit - @property def arguments(self) -> tuple[Expression, ...]: return () def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" visitor.visit_control_instruction(self) return visitor.visit_barrier(self) + def __eq__(self, other: object) -> bool: + return isinstance(other, Barrier) and self.qubit == other.qubit + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(qubit={self.qubit})" + class Wait(ControlInstruction): def __init__(self, qubit: QubitLike, time: SupportsInt) -> None: @@ -51,16 +51,17 @@ def __init__(self, qubit: QubitLike, time: SupportsInt) -> None: self.qubit = Qubit(qubit) self.time = Int(time) - def __repr__(self) -> str: - return f"{self.name}(qubit={self.qubit}, time={self.time})" - - def __eq__(self, other: object) -> bool: - return isinstance(other, Wait) and self.qubit == other.qubit and self.time == other.time - @property def arguments(self) -> tuple[Expression, ...]: return (self.time,) def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" visitor.visit_control_instruction(self) return visitor.visit_wait(self) + + def __eq__(self, other: object) -> bool: + return isinstance(other, Wait) and self.qubit == other.qubit and self.time == other.time + + def __repr__(self) -> str: + return f"{self.name}(qubit={self.qubit}, time={self.time})" diff --git a/opensquirrel/ir/expression.py b/opensquirrel/ir/expression.py index 82c19e11..f4b2c56f 100644 --- a/opensquirrel/ir/expression.py +++ b/opensquirrel/ir/expression.py @@ -10,8 +10,7 @@ from opensquirrel.ir.ir import IRNode, IRVisitor -class Expression(IRNode, ABC): - pass +class Expression(IRNode, ABC): ... @runtime_checkable @@ -42,6 +41,10 @@ def __init__(self, value: SupportsStr) -> None: msg = f"value {value!r} must be a str" raise TypeError(msg) + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" + return visitor.visit_str(self) + def __str__(self) -> str: """Cast the ``String`` object to a built-in Python ``str``. @@ -50,9 +53,6 @@ def __str__(self) -> str: """ return self.value - def accept(self, visitor: IRVisitor) -> Any: - return visitor.visit_str(self) - @dataclass(init=False) class Float(Expression): @@ -60,6 +60,7 @@ class Float(Expression): Attributes: value: value of the ``Float`` object. + """ value: float @@ -77,6 +78,10 @@ def __init__(self, value: SupportsFloat) -> None: msg = f"value {value!r} must be a float" raise TypeError(msg) + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" + return visitor.visit_float(self) + def __float__(self) -> float: """Cast the ``Float`` object to a built-in Python ``float``. @@ -85,9 +90,6 @@ def __float__(self) -> float: """ return self.value - def accept(self, visitor: IRVisitor) -> Any: - return visitor.visit_float(self) - @dataclass(init=False) class Int(Expression): @@ -112,6 +114,10 @@ def __init__(self, value: SupportsInt) -> None: msg = f"value {value!r} must be an int" raise TypeError(msg) + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" + return visitor.visit_int(self) + def __int__(self) -> int: """Cast the ``Int`` object to a built-in Python ``int``. @@ -120,9 +126,6 @@ def __int__(self) -> int: """ return self.value - def accept(self, visitor: IRVisitor) -> Any: - return visitor.visit_int(self) - @dataclass(init=False) class Bit(Expression): @@ -137,15 +140,16 @@ def __init__(self, index: BitLike) -> None: msg = f"index {index!r} must be a BitLike" raise TypeError(msg) + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" + return visitor.visit_bit(self) + def __hash__(self) -> int: return hash(str(self.__class__) + str(self.index)) def __repr__(self) -> str: return f"Bit[{self.index}]" - def accept(self, visitor: IRVisitor) -> Any: - return visitor.visit_bit(self) - @dataclass(init=False) class Qubit(Expression): @@ -166,15 +170,16 @@ def __init__(self, index: QubitLike) -> None: msg = f"index {index!r} must be a QubitLike" raise TypeError(msg) + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" + return visitor.visit_qubit(self) + def __hash__(self) -> int: return hash(str(self.__class__) + str(self.index)) def __repr__(self) -> str: return f"Qubit[{self.index}]" - def accept(self, visitor: IRVisitor) -> Any: - return visitor.visit_qubit(self) - class BaseAxis(Expression, ABC): _len = 3 @@ -204,6 +209,19 @@ def value(self, axis: AxisLike) -> None: """ self._value = self.parse(axis) + def __array__(self, dtype: DTypeLike | None = None, *, copy: bool | None = None) -> NDArray[Any]: + """Convert the ``BaseAxis`` data to an array.""" + return np.array(self.value, dtype=dtype, copy=copy) + + def __eq__(self, other: Any) -> bool: + """Check if `self` is equal to `other`. + + Two ``BaseAxis`` objects are considered equal if their axes are equal. + """ + if not isinstance(other, self.__class__): + return False + return np.array_equal(self, other) + @overload def __getitem__(self, i: int, /) -> np.float64: ... @@ -222,19 +240,6 @@ def __repr__(self) -> str: """String representation of the ``BaseAxis``.""" return f"{self.__class__.__name__}{self.value}" - def __array__(self, dtype: DTypeLike | None = None, *, copy: bool | None = None) -> NDArray[Any]: - """Convert the ``BaseAxis`` data to an array.""" - return np.array(self.value, dtype=dtype, copy=copy) - - def __eq__(self, other: Any) -> bool: - """Check if `self` is equal to `other`. - - Two ``BaseAxis`` objects are considered equal if their axes are equal. - """ - if not isinstance(other, self.__class__): - return False - return np.array_equal(self, other) - class Axis(BaseAxis): """The ``Axis`` object parses and stores a vector containing 3 elements. @@ -246,7 +251,7 @@ class Axis(BaseAxis): def parse(axis: AxisLike) -> NDArray[np.float64]: """Parse and validate an ``AxisLike``. - Check if the `axis` can be cast to a 1DArray of length 3, raise an error otherwise. + Checks if the `axis` can be cast to a 1DArray of length 3, raise an error otherwise. After casting to an array, the axis is normalized. Args: @@ -274,11 +279,10 @@ def parse(axis: AxisLike) -> NDArray[np.float64]: return axis / np.linalg.norm(axis) def accept(self, visitor: IRVisitor) -> Any: - """Accept the ``Axis``.""" + """Accepts visitor and processes this IR node.""" return visitor.visit_axis(self) -# Type Aliases BitLike = SupportsInt | Bit QubitLike = SupportsInt | Qubit AxisLike = ArrayLike | Axis diff --git a/opensquirrel/ir/ir.py b/opensquirrel/ir/ir.py index b640477e..1140846e 100644 --- a/opensquirrel/ir/ir.py +++ b/opensquirrel/ir/ir.py @@ -39,130 +39,126 @@ class IRVisitor: - def visit_str(self, s: String) -> Any: - pass + def visit_str(self, s: String) -> Any: ... - def visit_int(self, i: Int) -> Any: - pass + def visit_int(self, i: Int) -> Any: ... - def visit_float(self, f: Float) -> Any: - pass + def visit_float(self, f: Float) -> Any: ... - def visit_bit(self, bit: Bit) -> Any: - pass + def visit_bit(self, bit: Bit) -> Any: ... - def visit_qubit(self, qubit: Qubit) -> Any: - pass + def visit_qubit(self, qubit: Qubit) -> Any: ... - def visit_axis(self, axis: Axis) -> Any: - pass + def visit_axis(self, axis: Axis) -> Any: ... - def visit_canonical_axis(self, axis: CanonicalAxis) -> Any: - pass + def visit_canonical_axis(self, axis: CanonicalAxis) -> Any: ... - def visit_statement(self, statement: Statement) -> Any: - pass + def visit_statement(self, statement: Statement) -> Any: ... - def visit_asm_declaration(self, asm_declaration: AsmDeclaration) -> Any: - pass + def visit_asm_declaration(self, asm_declaration: AsmDeclaration) -> Any: ... - def visit_instruction(self, instruction: Instruction) -> Any: - pass + def visit_instruction(self, instruction: Instruction) -> Any: ... - def visit_unitary(self, unitary: Unitary) -> Any: - pass + def visit_unitary(self, unitary: Unitary) -> Any: ... - def visit_gate(self, gate: Gate) -> Any: - pass + def visit_gate(self, gate: Gate) -> Any: ... - def visit_single_qubit_gate(self, gate: SingleQubitGate) -> Any: - pass + def visit_single_qubit_gate(self, gate: SingleQubitGate) -> Any: ... - def visit_two_qubit_gate(self, gate: TwoQubitGate) -> Any: - pass + def visit_two_qubit_gate(self, gate: TwoQubitGate) -> Any: ... - def visit_bloch_sphere_rotation(self, bloch_sphere_rotation: BlochSphereRotation) -> Any: - pass + def visit_bloch_sphere_rotation(self, bloch_sphere_rotation: BlochSphereRotation) -> Any: ... - def visit_bsr_no_params(self, gate: BsrNoParams) -> Any: - pass + def visit_bsr_no_params(self, gate: BsrNoParams) -> Any: ... - def visit_bsr_full_params(self, gate: BsrFullParams) -> Any: - pass + def visit_bsr_full_params(self, gate: BsrFullParams) -> Any: ... - def visit_bsr_angle_param(self, gate: BsrAngleParam) -> Any: - pass + def visit_bsr_angle_param(self, gate: BsrAngleParam) -> Any: ... - def visit_bsr_unitary_params(self, gate: BsrUnitaryParams) -> Any: - pass + def visit_bsr_unitary_params(self, gate: BsrUnitaryParams) -> Any: ... - def visit_non_unitary(self, non_unitary: NonUnitary) -> Any: - pass + def visit_non_unitary(self, non_unitary: NonUnitary) -> Any: ... - def visit_control_instruction(self, control_instruction: ControlInstruction) -> Any: - pass + def visit_control_instruction(self, control_instruction: ControlInstruction) -> Any: ... - def visit_measure(self, measure: Measure) -> Any: - pass + def visit_measure(self, measure: Measure) -> Any: ... - def visit_init(self, init: Init) -> Any: - pass + def visit_init(self, init: Init) -> Any: ... - def visit_reset(self, reset: Reset) -> Any: - pass + def visit_reset(self, reset: Reset) -> Any: ... - def visit_barrier(self, barrier: Barrier) -> Any: - pass + def visit_barrier(self, barrier: Barrier) -> Any: ... - def visit_wait(self, wait: Wait) -> Any: - pass + def visit_wait(self, wait: Wait) -> Any: ... - def visit_canonical_gate_semantic(self, canonical: CanonicalGateSemantic) -> Any: - pass + def visit_canonical_gate_semantic(self, canonical: CanonicalGateSemantic) -> Any: ... - def visit_controlled_gate_semantic(self, controlled: ControlledGateSemantic) -> Any: - pass + def visit_controlled_gate_semantic(self, controlled: ControlledGateSemantic) -> Any: ... - def visit_matrix_gate_semantic(self, matrix: MatrixGateSemantic) -> Any: - pass + def visit_matrix_gate_semantic(self, matrix: MatrixGateSemantic) -> Any: ... class IRNode(ABC): @abstractmethod - def accept(self, visitor: IRVisitor) -> Any: - pass + def accept(self, visitor: IRVisitor) -> Any: ... class IR: def __init__(self) -> None: self.statements: list[Statement] = [] - def __eq__(self, other: object) -> bool: - if not isinstance(other, IR): - return False - return self.statements == other.statements - - def __repr__(self) -> str: - return f"IR: {self.statements}" + def accept(self, visitor: IRVisitor) -> None: + """Accepts visitor and processes the IR nodes.""" + for statement in self.statements: + statement.accept(visitor) def add_asm_declaration(self, asm_declaration: AsmDeclaration) -> None: + """Adds an assembly declaration to the IR. + + Args: + asm_declaration (AsmDeclaration): The assembly declaration to add. + + """ self.statements.append(asm_declaration) def add_gate(self, gate: Gate) -> None: + """Adds a gate to the IR. + + Args: + gate (Gate): The gate to add. + + """ self.statements.append(gate) def add_non_unitary(self, non_unitary: NonUnitary) -> None: + """Adds a non-unitary operation to the IR. + + Args: + non_unitary (NonUnitary): The non-unitary operation to add. + + """ self.statements.append(non_unitary) def add_statement(self, statement: Statement) -> None: + """Adds a generic statement to the IR. + + Args: + statement (Statement): The statement to add. + + """ self.statements.append(statement) def reverse(self) -> IR: + """Reverses the order of statements in the IR.""" ir = IR() for statement in self.statements[::-1]: ir.add_statement(statement) return ir - def accept(self, visitor: IRVisitor) -> None: - for statement in self.statements: - statement.accept(visitor) + def __eq__(self, other: object) -> bool: + if not isinstance(other, IR): + return False + return self.statements == other.statements + + def __repr__(self) -> str: + return f"IR: {self.statements}" diff --git a/opensquirrel/ir/non_unitary.py b/opensquirrel/ir/non_unitary.py index d92870a3..172743fb 100644 --- a/opensquirrel/ir/non_unitary.py +++ b/opensquirrel/ir/non_unitary.py @@ -14,15 +14,6 @@ def __init__(self, qubit: QubitLike, name: str) -> None: Instruction.__init__(self, name) self.qubit = Qubit(qubit) - def __repr__(self) -> str: - if self.arguments: - args = ", ".join(f"{arg.__class__.__name__.lower()}={arg}" for arg in self.arguments) - return f"{self.__class__.__name__}(qubit={self.qubit}, {args})" - return f"{self.__class__.__name__}(qubit={self.qubit})" - - def __eq__(self, other: object) -> bool: - return isinstance(other, self.__class__) and self.qubit == other.qubit - @property def arguments(self) -> tuple[Expression, ...]: return () @@ -36,8 +27,18 @@ def bit_operands(self) -> tuple[Bit, ...]: return () def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" return visitor.visit_non_unitary(self) + def __repr__(self) -> str: + if self.arguments: + args = ", ".join(f"{arg.__class__.__name__.lower()}={arg}" for arg in self.arguments) + return f"{self.__class__.__name__}(qubit={self.qubit}, {args})" + return f"{self.__class__.__name__}(qubit={self.qubit})" + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) and self.qubit == other.qubit + class Measure(NonUnitary): def __init__(self, qubit: QubitLike, bit: BitLike, axis: AxisLike = (0, 0, 1)) -> None: @@ -45,22 +46,23 @@ def __init__(self, qubit: QubitLike, bit: BitLike, axis: AxisLike = (0, 0, 1)) - self.bit = Bit(bit) self.axis = Axis(axis) - def __eq__(self, other: object) -> bool: - return ( - isinstance(other, Measure) and self.qubit == other.qubit and np.allclose(self.axis, other.axis, atol=ATOL) - ) - @property def arguments(self) -> tuple[Expression, ...]: return self.bit, self.axis + @property + def bit_operands(self) -> tuple[Bit, ...]: + return (self.bit,) + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" non_unitary_visit = super().accept(visitor) return non_unitary_visit if non_unitary_visit is not None else visitor.visit_measure(self) - @property - def bit_operands(self) -> tuple[Bit, ...]: - return (self.bit,) + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, Measure) and self.qubit == other.qubit and np.allclose(self.axis, other.axis, atol=ATOL) + ) class Init(NonUnitary): @@ -68,6 +70,7 @@ def __init__(self, qubit: QubitLike) -> None: NonUnitary.__init__(self, qubit=qubit, name="init") def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" non_unitary_visit = super().accept(visitor) return non_unitary_visit if non_unitary_visit is not None else visitor.visit_init(self) @@ -77,5 +80,6 @@ def __init__(self, qubit: QubitLike) -> None: NonUnitary.__init__(self, qubit=qubit, name="reset") def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" non_unitary_visit = super().accept(visitor) return non_unitary_visit if non_unitary_visit is not None else visitor.visit_reset(self) diff --git a/opensquirrel/ir/semantics/bsr.py b/opensquirrel/ir/semantics/bsr.py index efd43e87..241e996e 100644 --- a/opensquirrel/ir/semantics/bsr.py +++ b/opensquirrel/ir/semantics/bsr.py @@ -19,46 +19,6 @@ from opensquirrel.ir import IRVisitor -def bsr_from_matrix(matrix: ArrayLike | list[list[int | DTypeLike]]) -> BlochSphereRotation: - cmatrix = np.asarray(matrix, dtype=np.complex128) - - if cmatrix.shape != (2, 2): - msg = ( - f"matrix has incorrect shape for generating a Bloch sphere rotation {cmatrix.shape}," - " required shape is (2, 2)" - ) - raise ValueError(msg) - - a, b, c, d = map(complex, cmatrix.flatten()) - phase = cmath.phase(a * d - c * b) / 2 - - a *= cmath.exp(-1j * phase) - b *= cmath.exp(-1j * phase) - c *= cmath.exp(-1j * phase) - d *= cmath.exp(-1j * phase) - - angle = -2 * acos((1 / 2) * (a + d).real) - nx = -1j * (b + c) - ny = b - c - nz = -1j * (a - d) - - nx, ny, nz = (x.real for x in (nx, ny, nz)) - - if math.sqrt(nx**2 + ny**2 + nz**2) < ATOL: - return BlochSphereRotation(axis=(0, 0, 1), angle=0.0, phase=phase) - - if angle <= -pi: - angle += 2 * pi - - if nx + ny + nz < 0: - nx = -nx - ny = -ny - nz = -nz - angle = -angle - axis = Axis((nx, ny, nz)) - return BlochSphereRotation(axis=axis, angle=angle, phase=phase) - - class BlochSphereRotation(GateSemantic, IRNode): normalize_angle_params: bool = True @@ -73,17 +33,17 @@ def __init__( self.phase = normalize_angle(phase) if self.normalize_angle_params else float(phase) def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" return visitor.visit_bloch_sphere_rotation(self) def is_identity(self) -> bool: - # Angle and phase are already normalized. - return abs(self.angle) < ATOL and abs(self.phase) < ATOL + """Checks if the Bloch sphere rotation represents an identity operation. - def __repr__(self) -> str: - return ( - f"BlochSphereRotation(axis={repr_round(self.axis)}, angle={repr_round(self.angle)}, " - f"phase={repr_round(self.phase)})" - ) + Returns: + True if the Bloch sphere rotation represents an identity operation, False otherwise. + + """ + return abs(self.angle) < ATOL and abs(self.phase) < ATOL def __eq__(self, other: object) -> bool: if not isinstance(other, BlochSphereRotation): @@ -98,20 +58,20 @@ def __eq__(self, other: object) -> bool: return False def __mul__(self, other: BlochSphereRotation) -> BlochSphereRotation: - """Computes the single qubit gate resulting from the composition of two single - qubit gates, by composing the Bloch sphere rotations of the two gates. - The first rotation (A) is applied and then the second (B): + """Computes the Bloch sphere rotation resulting from the multiplication of two Bloch sphere + rotations. Note that the multiplication of Bloch sphere rotations `A * B` corrensponds to + linear operation $B \\cdot A$. - As separate gates: - A q - B q + Notes: + - It is checked whether the result is a known gate. + - Uses [Rodrigues' rotation formula](https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula). - As linear operations: - (B * A) q + Args: + other (BlochSphereRotation): The second Bloch sphere rotation to multiply with. - If the final single qubit gate is anonymous, we try to match it to a default gate. + Returns: + The resulting Bloch sphere rotation. - Uses Rodrigues' rotation formula (see https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula). """ acos_argument = cos(self.angle / 2) * cos(other.angle / 2) - sin(self.angle / 2) * sin( other.angle / 2 @@ -142,6 +102,12 @@ def __mul__(self, other: BlochSphereRotation) -> BlochSphereRotation: phase=combined_phase, ) + def __repr__(self) -> str: + return ( + f"BlochSphereRotation(axis={repr_round(self.axis)}, angle={repr_round(self.angle)}, " + f"phase={repr_round(self.phase)})" + ) + class BsrNoParams(BlochSphereRotation): def __init__( @@ -153,6 +119,7 @@ def __init__( BlochSphereRotation.__init__(self, axis, angle, phase) def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" visit_bsr = super().accept(visitor) return visit_bsr if visit_bsr is not None else visitor.visit_bsr_no_params(self) @@ -169,6 +136,7 @@ def arguments(self) -> tuple[Float, ...]: return self.nx, self.ny, self.nz, self.theta, self.phi def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" visit_bsr = super().accept(visitor) return visit_bsr if visit_bsr is not None else visitor.visit_bsr_full_params(self) @@ -188,6 +156,7 @@ def arguments(self) -> tuple[Float, ...]: return (self.theta,) def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" visit_bsr = super().accept(visitor) return visit_bsr if visit_bsr is not None else visitor.visit_bsr_angle_param(self) @@ -199,18 +168,26 @@ def __init__( phi: SupportsFloat, lmbda: SupportsFloat, ) -> None: - bsr = self._get_bsr(theta, phi, lmbda) + bsr = self.get_bsr(theta, phi, lmbda) BlochSphereRotation.__init__(self, bsr.axis, bsr.angle, bsr.phase) self.theta = Float(theta) self.phi = Float(phi) self.lmbda = Float(lmbda) - @property - def arguments(self) -> tuple[Float, ...]: - return self.theta, self.phi, self.lmbda - @staticmethod - def _get_bsr(theta: SupportsFloat, phi: SupportsFloat, lmbda: SupportsFloat) -> BlochSphereRotation: + def get_bsr(theta: SupportsFloat, phi: SupportsFloat, lmbda: SupportsFloat) -> BlochSphereRotation: + """Generates the corresponding Bloch sphere rotation from the given Euler angles, $\\theta$, + $\\phi$, and $\\lambda$. + + Args: + theta (SupportsFloat): The Euler angle $\\theta$. + phi (SupportsFloat): The Euler angle $\\phi$. + lmbda (SupportsFloat): The Euler angle $\\lambda$. + + Returns: + The corresponding Bloch sphere rotation. + + """ a = BlochSphereRotation((0, 0, 1), lmbda, 0) b = BlochSphereRotation((0, 1, 0), theta, 0) c = BlochSphereRotation((0, 0, 1), phi, (float(phi) + float(lmbda)) / 2) @@ -220,6 +197,60 @@ def _get_bsr(theta: SupportsFloat, phi: SupportsFloat, lmbda: SupportsFloat) -> mc = can1(c.axis, c.angle, c.phase) return bsr_from_matrix(mc @ mb @ ma) + @property + def arguments(self) -> tuple[Float, ...]: + return self.theta, self.phi, self.lmbda + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" visit_bsr = super().accept(visitor) return visit_bsr if visit_bsr is not None else visitor.visit_bsr_unitary_params(self) + + +def bsr_from_matrix(matrix: ArrayLike | list[list[int | DTypeLike]]) -> BlochSphereRotation: + """Generates a Bloch sphere rotation from a $2\\times 2$ unitary matrix $U(2)$. + + Args: + matrix (ArrayLike | list[list[int | DTypeLike]]): A $2\\times 2$ unitary matrix $U(2)$. + + Returns: + The corresponding Bloch sphere rotation. + + """ + cmatrix = np.asarray(matrix, dtype=np.complex128) + + if cmatrix.shape != (2, 2): + msg = ( + f"matrix has incorrect shape for generating a Bloch sphere rotation {cmatrix.shape}," + " required shape is (2, 2)" + ) + raise ValueError(msg) + + a, b, c, d = map(complex, cmatrix.flatten()) + phase = cmath.phase(a * d - c * b) / 2 + + a *= cmath.exp(-1j * phase) + b *= cmath.exp(-1j * phase) + c *= cmath.exp(-1j * phase) + d *= cmath.exp(-1j * phase) + + angle = -2 * acos((1 / 2) * (a + d).real) + nx = -1j * (b + c) + ny = b - c + nz = -1j * (a - d) + + nx, ny, nz = (x.real for x in (nx, ny, nz)) + + if math.sqrt(nx**2 + ny**2 + nz**2) < ATOL: + return BlochSphereRotation(axis=(0, 0, 1), angle=0.0, phase=phase) + + if angle <= -pi: + angle += 2 * pi + + if nx + ny + nz < 0: + nx = -nx + ny = -ny + nz = -nz + angle = -angle + axis = Axis((nx, ny, nz)) + return BlochSphereRotation(axis=axis, angle=angle, phase=phase) diff --git a/opensquirrel/ir/semantics/canonical_gate.py b/opensquirrel/ir/semantics/canonical_gate.py index d6f9bc81..0b62d6f1 100644 --- a/opensquirrel/ir/semantics/canonical_gate.py +++ b/opensquirrel/ir/semantics/canonical_gate.py @@ -11,16 +11,21 @@ class CanonicalAxis(BaseAxis): @staticmethod def parse(axis: AxisLike) -> NDArray[np.float64]: - """Parse and validate an ``AxisLike``. + """Parse and validate an `AxisLike`. - Check if the `axis` can be cast to a 1DArray of length 3, raise an error otherwise. + Checks if the axis can be cast to a 1DArray of length 3, raise an error otherwise. After casting to an array, the elements of the canonical axis are restricted to the Weyl chamber. Args: - axis: ``AxisLike`` to validate and parse. + axis (AxisLike): Axis to validate and parse. Returns: - Parsed axis represented as a 1DArray of length 3. + Parsed axis to 1DArray of length 3. + + Raises: + TypeError: If the axis cannot be cast to an ArrayLike. + ValueError: If the axis cannot be flattened to length 3. + """ if isinstance(axis, CanonicalAxis): return axis.value @@ -39,18 +44,29 @@ def parse(axis: AxisLike) -> NDArray[np.float64]: @staticmethod def restrict_to_weyl_chamber(axis: NDArray[np.float64]) -> NDArray[np.float64]: - """Restrict the given axis to the Weyl chamber. The six rules that are - (implicitly) used are: - 1. The canonical parameters are periodic with a period of 2 (neglecting - a global phase). - 2. Can(tx, ty, tz) ~ Can(tx - 1, ty, tz) (for any parameter) - 3. Can(tx, ty, tz) ~ Can(tx, -ty, -tz) (for any pair of parameters) - 4. Can(tx, ty, tz) ~ Can(ty, tx, tz) (for any pair of parameters) - 5. Can(tx, ty, 0) ~ Can(1 - tx, ty, 0) - 6. Can(tx, ty, tz) x Can(tx', ty', tz') = Can(tx + tx', ty + ty', tz + tz') - (here x represents matrix multiplication) - - Based on the rules described in Chapter 5 of https://threeplusone.com/pubs/on_gates.pdf + """Restricts the given axis to the Weyl chamber. + + The six rules that are (implicitly) used are: + + 1. The canonical parameters are periodic with a period of 2 (neglecting + a global phase). + 2. $\\text{Can}(t_x, t_y, t_z)\\sim\\text{Can}(t_x - 1, t_y, t_z)$ (for any parameter) + 3. $\\text{Can}(t_x, t_y, t_z)\\sim\\text{Can}(t_x, -t_y, -t_z)$ (for any pair of parameters) + 4. $\\text{Can}(t_x, t_y, t_z)\\sim\\text{Can}(t_y, t_x, t_z)$ (for any pair of parameters) + 5. $\\text{Can}(t_x, t_y, 0)\\sim\\text{Can}(1 - t_x, t_y, 0)$ + 6. $\\text{Can}(t_x, t_y, t_z) * \\text{Can}(t_x', t_y', t_z') = + \\text{Can}(t_x + t_x', t_y + t_y', t_z + t_z')$ + + Note: + Based on the rules described in + [Quantum Gates by G.E. Crooks (2024), Section 5](https://threeplusone.com/pubs/on_gates.pdf). + + Args: + axis (NDArray[np.float64]): Axis to restrict to the Weyl chamber. + + Returns: + Axis restricted to the Weyl chamber. + """ axis = (axis + 1) % 2 - 1 @@ -71,6 +87,7 @@ def restrict_to_weyl_chamber(axis: NDArray[np.float64]) -> NDArray[np.float64]: return np.sort(axis)[::-1] def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" return visitor.visit_canonical_axis(self) @@ -78,11 +95,18 @@ class CanonicalGateSemantic(GateSemantic): def __init__(self, axis: AxisLike) -> None: self.axis = CanonicalAxis(axis) - def __repr__(self) -> str: - return f"CanonicalGateSemantic(axis={self.axis})" + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" + return visitor.visit_canonical_gate_semantic(self) def is_identity(self) -> bool: + """Checks if the canonical gate semantic represents an identity operation. + + Returns: + True if the canonical gate semantic represents an identity operation, False otherwise. + + """ return self.axis == CanonicalAxis((0, 0, 0)) - def accept(self, visitor: IRVisitor) -> Any: - return visitor.visit_canonical_gate_semantic(self) + def __repr__(self) -> str: + return f"CanonicalGateSemantic(axis={self.axis})" diff --git a/opensquirrel/ir/semantics/controlled_gate.py b/opensquirrel/ir/semantics/controlled_gate.py index 17c1975c..c255ea27 100644 --- a/opensquirrel/ir/semantics/controlled_gate.py +++ b/opensquirrel/ir/semantics/controlled_gate.py @@ -13,11 +13,18 @@ class ControlledGateSemantic(GateSemantic): def __init__(self, target_bsr: BlochSphereRotation) -> None: self.target_bsr = target_bsr + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" + return visitor.visit_controlled_gate_semantic(self) + def is_identity(self) -> bool: + """Checks if the controlled gate semantic represents an identity operation. + + Returns: + True if the controlled gate semantic represents an identity operation, False otherwise. + + """ return self.target_bsr.is_identity() def __repr__(self) -> str: return f"ControlledGateSemantic(target_bsr={self.target_bsr})" - - def accept(self, visitor: IRVisitor) -> Any: - return visitor.visit_controlled_gate_semantic(self) diff --git a/opensquirrel/ir/semantics/gate_semantic.py b/opensquirrel/ir/semantics/gate_semantic.py index 611102a3..62d03a4d 100644 --- a/opensquirrel/ir/semantics/gate_semantic.py +++ b/opensquirrel/ir/semantics/gate_semantic.py @@ -5,9 +5,7 @@ class GateSemantic(IRNode, ABC): @abstractmethod - def is_identity(self) -> bool: - pass + def is_identity(self) -> bool: ... @abstractmethod - def __repr__(self) -> str: - pass + def __repr__(self) -> str: ... diff --git a/opensquirrel/ir/semantics/matrix_gate.py b/opensquirrel/ir/semantics/matrix_gate.py index 4099c461..c74c4a80 100644 --- a/opensquirrel/ir/semantics/matrix_gate.py +++ b/opensquirrel/ir/semantics/matrix_gate.py @@ -27,7 +27,16 @@ def __init__(self, matrix: ArrayLike | list[list[int | DTypeLike]]) -> None: ) raise ValueError(msg) + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" + return visitor.visit_matrix_gate_semantic(self) + def is_identity(self) -> bool: + """Checks if the matrix gate semantic represents an identity operation. + + Returns: + True if the matrix gate semantic represents an identity operation, False otherwise. + """ return np.allclose(self.matrix, np.eye(self.matrix.shape[0]), atol=ATOL) def __array__(self, *args: Any, **kwargs: Any) -> NDArray[np.complex128]: @@ -35,6 +44,3 @@ def __array__(self, *args: Any, **kwargs: Any) -> NDArray[np.complex128]: def __repr__(self) -> str: return f"MatrixGateSemantic(matrix={repr_round(self.matrix)})" - - def accept(self, visitor: IRVisitor) -> Any: - return visitor.visit_matrix_gate_semantic(self) diff --git a/opensquirrel/ir/single_qubit_gate.py b/opensquirrel/ir/single_qubit_gate.py index a514a9b5..466b4852 100644 --- a/opensquirrel/ir/single_qubit_gate.py +++ b/opensquirrel/ir/single_qubit_gate.py @@ -11,12 +11,22 @@ def try_match_replace_with_default_gate(gate: SingleQubitGate) -> SingleQubitGate: - """Try replacing a given SingleQubitGate with a default SingleQubitGate. - It does that by matching the input SingleQubitGate to a default SingleQubitGate. + """Tries to match a given single-qubit gate with a _default_ single-qubit gate. + It does that by checking if the parameters of the Bloch sphere rotation (BSR) semantic of the + input single-qubit gate are close to those of any of the default single-qubit gates. + + Note: + - The default (single-qubit) gates are defined in the + [cQASM standard gate set](https://qutech-delft.github.io/cQASM-spec/latest/standard_gate_set/index.html). + - If no specific match is found, the general Rn gate is returned with the same parameters + values as those of the input gate. + + Args: + gate: The single-qubit gate to be matched. Returns: - A default SingleQubitGate if this SingleQubitGate is close to it, - or the input SingleQubitGate otherwise. + A default single-qubit gate if this single-qubit gate matches it, the Rn gate otherwise. + """ from opensquirrel.default_instructions import ( default_bsr_with_param_set, @@ -64,10 +74,26 @@ def matrix(self) -> MatrixGateSemantic: self._matrix = MatrixGateSemantic(can1(self.bsr.axis, self.bsr.angle, self.bsr.phase)) return self._matrix + @property + def qubit_operands(self) -> tuple[Qubit, ...]: + return (self.qubit,) + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" visit_gate = super().accept(visitor) return visit_gate if visit_gate is not None else visitor.visit_single_qubit_gate(self) + def is_identity(self) -> bool: + """Checks if the single-qubit gate is an identity gate. + + Returns: + True if the single-qubit gate is an identity gate, False otherwise. + + """ + if self.bsr is not None: + return self.bsr.is_identity() + return self.matrix.is_identity() if self.matrix else False + def __eq__(self, other: Any) -> bool: if not isinstance(other, SingleQubitGate): return False @@ -82,12 +108,3 @@ def __mul__(self, other: SingleQubitGate) -> SingleQubitGate: msg = "cannot merge two single-qubit gates on different qubits" raise ValueError(msg) return SingleQubitGate(self.qubit, self.bsr * other.bsr) - - @property - def qubit_operands(self) -> tuple[Qubit, ...]: - return (self.qubit,) - - def is_identity(self) -> bool: - if self.bsr is not None: - return self.bsr.is_identity() - return self.matrix.is_identity() if self.matrix else False diff --git a/opensquirrel/ir/statement.py b/opensquirrel/ir/statement.py index 11d97a8c..a0cf4843 100644 --- a/opensquirrel/ir/statement.py +++ b/opensquirrel/ir/statement.py @@ -5,16 +5,15 @@ from opensquirrel.ir.ir import IRNode, IRVisitor -class Statement(IRNode, ABC): - pass +class Statement(IRNode, ABC): ... class AsmDeclaration(Statement): """``AsmDeclaration`` is used to define an assembly declaration statement in the IR. Args: - backend_name: Name of the backend that is to process the provided backend code. - backend_code: (Assembly) code to be processed by the specified backend. + backend_name (SupportsStr): Name of the backend that is to process the provided backend code. + backend_code (SupportsStr): Assembly code to be processed by the specified backend. """ def __init__( @@ -27,6 +26,7 @@ def __init__( Statement.__init__(self) def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" visitor.visit_statement(self) return visitor.visit_asm_declaration(self) @@ -37,22 +37,20 @@ def __init__(self, name: str) -> None: @property @abstractmethod - def arguments(self) -> tuple[Expression, ...]: - pass + def arguments(self) -> tuple[Expression, ...]: ... @property @abstractmethod - def qubit_operands(self) -> tuple[Qubit, ...]: - pass + def qubit_operands(self) -> tuple[Qubit, ...]: ... @property - def qubit_indices(self) -> list[int]: - return [qubit.index for qubit in self.qubit_operands] + @abstractmethod + def bit_operands(self) -> tuple[Bit, ...]: ... @property - @abstractmethod - def bit_operands(self) -> tuple[Bit, ...]: - pass + def qubit_indices(self) -> list[int]: + return [qubit.index for qubit in self.qubit_operands] def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" return visitor.visit_instruction(self) diff --git a/opensquirrel/ir/two_qubit_gate.py b/opensquirrel/ir/two_qubit_gate.py index dc5e0017..4ce384c1 100644 --- a/opensquirrel/ir/two_qubit_gate.py +++ b/opensquirrel/ir/two_qubit_gate.py @@ -26,9 +26,6 @@ def __init__( msg = "qubit operands cannot be the same qubit" raise ValueError(msg) - def __repr__(self) -> str: - return f"TwoQubitGate(qubits=[{self.qubit0, self.qubit1}], gate_semantic={self.gate_semantic})" - @cached_property def matrix(self) -> MatrixGateSemantic: if self._matrix: @@ -52,15 +49,22 @@ def canonical(self) -> CanonicalGateSemantic | None: def controlled(self) -> ControlledGateSemantic | None: return self._controlled - def accept(self, visitor: IRVisitor) -> Any: - visit_parent = super().accept(visitor) - return visit_parent if visit_parent is not None else visitor.visit_two_qubit_gate(self) - @property def qubit_operands(self) -> tuple[Qubit, ...]: return (self.qubit0, self.qubit1) + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" + visit_parent = super().accept(visitor) + return visit_parent if visit_parent is not None else visitor.visit_two_qubit_gate(self) + def is_identity(self) -> bool: + """Checks if the two-qubit gate is an identity gate. + + Returns: + True if the two-qubit gate is an identity gate, False otherwise. + + """ if self.controlled: return self.controlled.is_identity() if self.matrix: @@ -68,3 +72,6 @@ def is_identity(self) -> bool: if self.canonical: return self.canonical.is_identity() return False + + def __repr__(self) -> str: + return f"TwoQubitGate(qubits=[{self.qubit0, self.qubit1}], gate_semantic={self.gate_semantic})" diff --git a/opensquirrel/ir/unitary.py b/opensquirrel/ir/unitary.py index f02ff2d8..8c54d5a6 100644 --- a/opensquirrel/ir/unitary.py +++ b/opensquirrel/ir/unitary.py @@ -26,8 +26,7 @@ def _check_repeated_qubit_operands(qubits: Sequence[Qubit]) -> bool: return len(qubits) != len(set(qubits)) @abstractmethod - def is_identity(self) -> bool: - pass + def is_identity(self) -> bool: ... @property def arguments(self) -> tuple[Expression, ...]: @@ -38,6 +37,7 @@ def bit_operands(self) -> tuple[Bit, ...]: return () def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" return visitor.visit_gate(self) def __eq__(self, other: object) -> bool: @@ -46,13 +46,23 @@ def __eq__(self, other: object) -> bool: return compare_gates(self, other) -def compare_gates(g1: Gate, g2: Gate) -> bool: - union_mapping = list(set(g1.qubit_indices) | set(g2.qubit_indices)) +def compare_gates(gate_1: Gate, gate_2: Gate) -> bool: + """Checks if two gates are equivalent up to a global phase. + + Args: + gate_1 (Gate): The first gate to compare. + gate_2 (Gate): The second gate to compare. + + Returns: + True if the two gates are equivalent up to a global phase, False otherwise. + + """ + union_mapping = list(set(gate_1.qubit_indices) | set(gate_2.qubit_indices)) from opensquirrel.circuit_matrix_calculator import get_circuit_matrix from opensquirrel.reindexer import get_reindexed_circuit - matrix_g1 = get_circuit_matrix(get_reindexed_circuit([g1], union_mapping)) - matrix_g2 = get_circuit_matrix(get_reindexed_circuit([g2], union_mapping)) + matrix_gate_1 = get_circuit_matrix(get_reindexed_circuit([gate_1], union_mapping)) + matrix_gate_2 = get_circuit_matrix(get_reindexed_circuit([gate_2], union_mapping)) - return are_matrices_equivalent_up_to_global_phase(matrix_g1, matrix_g2) + return are_matrices_equivalent_up_to_global_phase(matrix_gate_1, matrix_gate_2) diff --git a/opensquirrel/passes/decomposer/aba_decomposer.py b/opensquirrel/passes/decomposer/aba_decomposer.py index b17397d5..7b6ca1dd 100644 --- a/opensquirrel/passes/decomposer/aba_decomposer.py +++ b/opensquirrel/passes/decomposer/aba_decomposer.py @@ -18,201 +18,200 @@ class ABADecomposer(Decomposer, ABC): + _gate_list: ClassVar[list[Callable[..., SingleQubitGate]]] = [Rx, Ry, Rz] + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.index_a = self._gate_list.index(self.Ra) + self.index_b = self._gate_list.index(self.Rb) + @property @abstractmethod - def ra(self) -> Callable[..., SingleQubitGate]: ... + def Ra(self) -> Callable[..., SingleQubitGate]: ... # noqa: N802 @property @abstractmethod - def rb(self) -> Callable[..., SingleQubitGate]: ... + def Rb(self) -> Callable[..., SingleQubitGate]: ... # noqa: N802 - _gate_list: ClassVar[list[Callable[..., SingleQubitGate]]] = [Rx, Ry, Rz] + def decompose(self, gate: Gate) -> list[Gate]: + """Decomposes a single-qubit gate into (at most) three single-qubit gates following the + R$a$-R$b$-R$a$ decomposition, where [$ab$] are in $\\{x,y,z\\}$ and $a$ is not equal to $b$. - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - self.index_a = self._gate_list.index(self.ra) - self.index_b = self._gate_list.index(self.rb) + For instance, the ZYZ decomposer decomposes a single-qubit gate into Rz-Ry-Rz. - def _find_unused_index(self) -> int: - """Finds the index of the axis object that is not used in the decomposition. - For example, if one selects the ZYZ decomposition, the integer returned will be 0 (since it is X). + Args: + gate (Gate): Single-qubit gate to decompose. Returns: - Index of the axis object that is not used in the decomposition. - """ - return ({0, 1, 2} - {self.index_a, self.index_b}).pop() + A sequence of (at most) three gates, following the R$a$-R$b$-R$a$ decomposition. - def _set_a_b_c_axes_values(self, axis: AxisLike) -> tuple[Any, Any, Any]: - """Given: - - an A-B-A decomposition strategy (where A and B can be either X, Y, or Z), and - - a rotation axis { X: x, Y: y, Z: z } corresponding to a Bloch sphere rotation. - Sets a new rotation axis (a, b, c) such that a = axis[A], b = axis[B], and c = axis[C]. - For example, given a Z-X-Z decomposition strategy, and an axis (x, y, z), sets (a, b, c) = (z, x, y). + """ + if not isinstance(gate, SingleQubitGate): + return [gate] - Parameters: - axis: _normalized_ axis of a Bloch sphere rotation + theta_a1, theta_b, theta_a2 = self._determine_rotation_angles(gate.bsr.axis, gate.bsr.angle) + return filter_out_identities( + [ + self.Ra(gate.qubit, theta_a1), + self.Rb(gate.qubit, theta_b), + self.Ra(gate.qubit, theta_a2), + ] + ) - Returns: - A triplet (a, b, c) where a, b, and c are the values of x, y, and z reordered. - """ - axis_ = Axis(axis) - return axis_[self.index_a], axis_[self.index_b], axis_[self._find_unused_index()] + def _b_and_c_components_in_negative_octant(self, b_component: float, c_component: float) -> bool: + """Checks if the values for + the B and C axes fall in one of the two negative octants (a positive or negative, and B and + C negative, or one of them zero). - @staticmethod - def _are_b_and_c_axes_in_negative_octant(b_axis_value: float, c_axis_value: float) -> bool: - """Given an ABC axis system, and the values for axes B and C. - Checks if the values for the B and C axes fall in one of the two negative octants (A positive or negative, - and B and C negative, or one of them zero). + Args: + b_component (float): Value of the B component. + c_component (float): Value of the C component. Returns: True if the values for axis B and C are both negative or zero, but not zero at the same time. False otherwise. """ return ( - (b_axis_value < 0 or abs(b_axis_value) < ATOL) - and (c_axis_value < 0 or abs(c_axis_value) < ATOL) - and not (abs(b_axis_value) < ATOL and abs(c_axis_value) < ATOL) + (b_component < 0 or abs(b_component) < ATOL) + and (c_component < 0 or abs(c_component) < ATOL) + and not (abs(b_component) < ATOL and abs(c_component) < ATOL) ) - def get_decomposition_angles(self, axis: AxisLike, alpha: float) -> tuple[float, float, float]: - """Given: - - an A-B-A decomposition strategy (where A and B can be either X, Y, or Z), and - - the rotation axis and angle corresponding to a Bloch sphere rotation. - Calculates the rotation angles around axes A, B, and C, - such that the original Bloch sphere rotation can be expressed as U = Ra(theta3) Rb(theta2) Rc(theta1), - Rn meaning rotation around axis N + def _determine_rotation_angles(self, axis: AxisLike, theta: float) -> tuple[float, float, float]: + """Determines the rotation angles for the R$a$-R$b$-R$a$ decomposition. - Parameters: - axis: _normalized_ axis of a Bloch sphere rotation - alpha: angle of a Bloch sphere rotation + Args: + axis (AxisLike): Axis of the Bloch sphere rotation. + theta (float): Angle $\\theta$ of the Bloch sphere rotation. Returns: - A triplet (theta_1, theta_2, theta_3), where theta_1, theta_2, and theta_3 are the rotation angles around - axes A, B, and C, respectively. + The rotation angles $\\theta_{a_1}$, $\\theta_b$, and $\\theta_{a_2}$ around axes $a$, + $b$, and $a$, respectively. + """ - if not (-math.pi + ATOL < alpha <= math.pi + ATOL): - msg = f"angle {alpha!r} is not normalized between -pi and pi" + if not (-math.pi + ATOL < theta <= math.pi + ATOL): + msg = f"angle {theta!r} is not normalized between -pi and pi" raise ValueError(msg) - a_axis_value, b_axis_value, c_axis_value = self._set_a_b_c_axes_values(axis) - - # Calculate primary angle - p = 2 * math.atan2(a_axis_value * math.sin(alpha / 2), math.cos(alpha / 2)) + component_a, component_b, component_c = self._set_components(axis) - # Calculate theta 2 - theta_2 = 2 * acos(math.cos(alpha / 2) * math.sqrt(1 + (a_axis_value * math.tan(alpha / 2)) ** 2)) - theta_2 = math.copysign(theta_2, alpha) + theta_b = 2 * acos(math.cos(theta / 2) * math.sqrt(1 + (component_a * math.tan(theta / 2)) ** 2)) + theta_b = math.copysign(theta_b, theta) - # Calculate secondary angle - if abs(math.sin(theta_2 / 2)) < ATOL: - # This can be anything, but setting m = p means theta_3 == 0, which is better for gate count. + p = 2 * math.atan2(component_a * math.sin(theta / 2), math.cos(theta / 2)) + if abs(math.sin(theta_b / 2)) < ATOL: m = p else: - m = 2 * acos(float(b_axis_value) * math.sin(alpha / 2) / math.sin(theta_2 / 2)) + m = 2 * acos(float(component_b) * math.sin(theta / 2) / math.sin(theta_b / 2)) if math.pi - abs(m) > ATOL: - ret_sign = 2 * math.atan2(c_axis_value, a_axis_value) + ret_sign = 2 * math.atan2(component_c, component_a) m = math.copysign(m, ret_sign) - # Check if the sign of the secondary angle has to be flipped if are_axes_consecutive(self.index_a, self.index_b): m = -m - # Calculate theta 1 and theta 2 - theta_1 = (p + m) / 2 - theta_3 = p - theta_1 + theta_a1 = (p + m) / 2 + theta_a2 = p - theta_a1 - # Check if theta 1 and theta 3 have to be swapped - if ABADecomposer._are_b_and_c_axes_in_negative_octant(b_axis_value, c_axis_value): - theta_1, theta_3 = theta_3, theta_1 + if self._b_and_c_components_in_negative_octant(component_b, component_c): + theta_a1, theta_a2 = theta_a2, theta_a1 - return theta_1, theta_2, theta_3 + return theta_a1, theta_b, theta_a2 - def decompose(self, gate: Gate) -> list[Gate]: - """General A-B-A decomposition function for a single gate. + def _find_unused_index(self) -> int: + """Finds the index of the axis component that is not used in the decomposition. + For example, for the Rz-Ry-Rz decomposition, the index returned is 0 (since it is x). - Args: - gate: gate to decompose. Returns: - Three gates, following the A-B-A convention, corresponding to the decomposition of the input gate. + Index of the axis component that is not used in the decomposition. + """ - if not isinstance(gate, SingleQubitGate): - return [gate] + return ({0, 1, 2} - {self.index_a, self.index_b}).pop() + + def _set_components(self, axis: AxisLike) -> tuple[Any, Any, Any]: + """Sets a new rotation axis (a, b, c). - theta1, theta2, theta3 = self.get_decomposition_angles(gate.bsr.axis, gate.bsr.angle) - a1 = self.ra(gate.qubit, theta1) - b = self.rb(gate.qubit, theta2) - a2 = self.ra(gate.qubit, theta3) + For instance, for an Rz-Ry-Rz decomposition the initial axis (x, y, z) is set to + (a, b, c) = (z, y, x). - return filter_out_identities([a1, b, a2]) + Parameters: + axis (AxisLike): Axis of a Bloch sphere rotation + + Returns: + A triplet (a, b, c) where a, b, and c are the values of x, y, and z reordered. + + """ + axis_ = Axis(axis) + return axis_[self.index_a], axis_[self.index_b], axis_[self._find_unused_index()] class XYXDecomposer(ABADecomposer): - """Class responsible for the X-Y-X decomposition.""" + """Decomposes single-qubit gates into a Rx-Ry-Rx decomposition.""" @property - def ra(self) -> Callable[..., SingleQubitGate]: + def Ra(self) -> Callable[..., SingleQubitGate]: # noqa: N802 return Rx @property - def rb(self) -> Callable[..., SingleQubitGate]: + def Rb(self) -> Callable[..., SingleQubitGate]: # noqa: N802 return Ry class XZXDecomposer(ABADecomposer): - """Class responsible for the X-Z-X decomposition.""" + """Decomposes single-qubit gates into a Rx-Rz-Rx decomposition.""" @property - def ra(self) -> Callable[..., SingleQubitGate]: + def Ra(self) -> Callable[..., SingleQubitGate]: # noqa: N802 return Rx @property - def rb(self) -> Callable[..., SingleQubitGate]: + def Rb(self) -> Callable[..., SingleQubitGate]: # noqa: N802 return Rz class YXYDecomposer(ABADecomposer): - """Class responsible for the Y-X-Y decomposition.""" + """Decomposes single-qubit gates into a Ry-Rx-Ry decomposition.""" @property - def ra(self) -> Callable[..., SingleQubitGate]: + def Ra(self) -> Callable[..., SingleQubitGate]: # noqa: N802 return Ry @property - def rb(self) -> Callable[..., SingleQubitGate]: + def Rb(self) -> Callable[..., SingleQubitGate]: # noqa: N802 return Rx class YZYDecomposer(ABADecomposer): - """Class responsible for the Y-Z-Y decomposition.""" + """Decomposes single-qubit gates into a Ry-Rz-Ry decomposition.""" @property - def ra(self) -> Callable[..., SingleQubitGate]: + def Ra(self) -> Callable[..., SingleQubitGate]: # noqa: N802 return Ry @property - def rb(self) -> Callable[..., SingleQubitGate]: + def Rb(self) -> Callable[..., SingleQubitGate]: # noqa: N802 return Rz class ZXZDecomposer(ABADecomposer): - """Class responsible for the Z-X-Z decomposition.""" + """Decomposes single-qubit gates into a Rz-Rx-Rz decomposition.""" @property - def ra(self) -> Callable[..., SingleQubitGate]: + def Ra(self) -> Callable[..., SingleQubitGate]: # noqa: N802 return Rz @property - def rb(self) -> Callable[..., SingleQubitGate]: + def Rb(self) -> Callable[..., SingleQubitGate]: # noqa: N802 return Rx class ZYZDecomposer(ABADecomposer): - """Class responsible for the Z-Y-Z decomposition.""" + """Decomposes single-qubit gates into a Rz-Ry-Rz decomposition.""" @property - def ra(self) -> Callable[..., SingleQubitGate]: + def Ra(self) -> Callable[..., SingleQubitGate]: # noqa: N802 return Rz @property - def rb(self) -> Callable[..., SingleQubitGate]: + def Rb(self) -> Callable[..., SingleQubitGate]: # noqa: N802 return Ry diff --git a/opensquirrel/passes/decomposer/cnot2cz_decomposer.py b/opensquirrel/passes/decomposer/cnot2cz_decomposer.py index 6452ed0e..f16643bb 100644 --- a/opensquirrel/passes/decomposer/cnot2cz_decomposer.py +++ b/opensquirrel/passes/decomposer/cnot2cz_decomposer.py @@ -11,17 +11,22 @@ class CNOT2CZDecomposer(Decomposer): - """Predefined decomposition of CNOT gate to CZ gate with Y rotations. + def decompose(self, gate: Gate) -> list[Gate]: + """Predefined decomposition of CNOT gate into CZ gate with Ry rotations. - ---•--- -----------------•---------------- - | → | - ---⊕--- --[Ry(-pi/2)]---[Z]---[Ry(pi/2)]-- + ![image](../../../_static/cnot2cz.png#only-light) + ![image](../../../_static/cnot2cz_dm.png#only-dark) - Note: - This decomposition preserves the global phase of the CNOT gate. - """ + Note: + This decomposition preserves the global phase of the CNOT gate. - def decompose(self, gate: Gate) -> list[Gate]: + Args: + gate (Gate): CNOT gate to decompose. + + Returns: + A sequence of gates, Ry(-π/2)-CZ-Ry(π/2), that decompose the CNOT gate. + + """ if gate.name != "CNOT": return [gate] diff --git a/opensquirrel/passes/decomposer/cnot_decomposer.py b/opensquirrel/passes/decomposer/cnot_decomposer.py index 43597fd6..a096dfae 100644 --- a/opensquirrel/passes/decomposer/cnot_decomposer.py +++ b/opensquirrel/passes/decomposer/cnot_decomposer.py @@ -16,19 +16,32 @@ class CNOTDecomposer(Decomposer): - """ - Decomposes 2-qubit controlled unitary gates to CNOT + Rz/Ry. + """Decomposes 2-qubit controlled unitary gates to CNOT + Rz/Ry. + Applying single-qubit gate fusion after this pass might be beneficial. - Source of the math: https://threeplusone.com/pubs/on_gates.pdf, chapter 7.5 "ABC decomposition" + Based on the CNOT decomposition described in + [Quantum Gates by G.E. Crooks (2024), Section 7.5](https://threeplusone.com/pubs/on_gates.pdf). """ def decompose(self, gate: Gate) -> list[Gate]: - if not isinstance(gate, TwoQubitGate): - return [gate] + """Decomposes a controlled two-qubit gate into a sequence of (at most 2) CNOT gates and + single-qubit gates. It decomposes the CR, CRk, and CZ controlled two-qubit gates. + + Note: + The SWAP gate is not a controlled two-qubit gate and is not decomposed by this pass. + To decompose SWAP gates, use the + [SWAP2CNOTDecomposer][opensquirrel.passes.decomposer.swap2cnot_decomposer.SWAP2CNOTDecomposer] + or the [SWAP2CZDecomposer][opensquirrel.passes.decomposer.swap2cz_decomposer.SWAP2CZDecomposer]. + + Args: + gate (Gate): Two-qubit controlled gate to decompose. + + Returns: + A sequence of (at most 2) CNOT gates and single-qubit gates. - if not gate.controlled: - # Do nothing, this is not a controlled unitary gate. + """ + if not isinstance(gate, TwoQubitGate) or not gate.controlled: return [gate] control_qubit = gate.qubit0 @@ -40,7 +53,7 @@ def decompose(self, gate: Gate) -> list[Gate]: # Try special case first, see https://arxiv.org/pdf/quant-ph/9503016.pdf lemma 5.5 controlled_rotation_times_x = target_gate * X(target_qubit) - theta0_with_x, theta1_with_x, theta2_with_x = ZYZDecomposer().get_decomposition_angles( + theta0_with_x, theta1_with_x, theta2_with_x = ZYZDecomposer()._determine_rotation_angles( # noqa: SLF001 controlled_rotation_times_x.bsr.axis, controlled_rotation_times_x.bsr.angle, ) @@ -57,7 +70,7 @@ def decompose(self, gate: Gate) -> list[Gate]: ], ) - theta0, theta1, theta2 = ZYZDecomposer().get_decomposition_angles(target_gate.bsr.axis, target_gate.bsr.angle) + theta0, theta1, theta2 = ZYZDecomposer()._determine_rotation_angles(target_gate.bsr.axis, target_gate.bsr.angle) # noqa: SLF001 A = [Ry(target_qubit, theta1 / 2), Rz(target_qubit, theta2)] # noqa: N806 B = [Rz(target_qubit, -(theta0 + theta2) / 2), Ry(target_qubit, -theta1 / 2)] # noqa: N806 diff --git a/opensquirrel/passes/decomposer/cz_decomposer.py b/opensquirrel/passes/decomposer/cz_decomposer.py index 38f80287..f251c41f 100644 --- a/opensquirrel/passes/decomposer/cz_decomposer.py +++ b/opensquirrel/passes/decomposer/cz_decomposer.py @@ -24,6 +24,25 @@ class CZDecomposer(Decomposer): """ def decompose(self, gate: Gate) -> list[Gate]: + """Decomposes a controlled two-qubit gate into a sequence of (at most 2) CZ gates and + single-qubit gates. It decomposes the CR, CRk, and CNOT controlled two-qubit gates. + + Uses the ABC decomposition procedure described in + [Quantum Gates by G.E. Crooks (2024), Section 7.5](https://threeplusone.com/pubs/on_gates.pdf). + + Note: + The SWAP gate is not a controlled two-qubit gate and is not decomposed by this pass. + To decompose SWAP gates, use the + [SWAP2CZDecomposer][opensquirrel.passes.decomposer.swap2cz_decomposer.SWAP2CZDecomposer] + or the [SWAP2CNOTDecomposer][opensquirrel.passes.decomposer.swap2cnot_decomposer.SWAP2CNOTDecomposer]. + + Args: + gate (Gate): Two-qubit controlled gate to decompose. + + Returns: + A sequence of (at most 2) CZ gates and single-qubit gates. + + """ if not isinstance(gate, TwoQubitGate): return [gate] @@ -42,7 +61,7 @@ def decompose(self, gate: Gate) -> list[Gate]: # Try special case first, see https://arxiv.org/pdf/quant-ph/9503016.pdf lemma 5.5 # Note that here V = Rx(a) * Ry(th) * Rx(a) * Z to create V = AZBZ, with AB = I controlled_rotation_times_z = target_gate * Z(target_qubit) - theta0_with_z, theta1_with_z, theta2_with_z = XYXDecomposer().get_decomposition_angles( + theta0_with_z, theta1_with_z, theta2_with_z = XYXDecomposer()._determine_rotation_angles( # noqa: SLF001 controlled_rotation_times_z.bsr.axis, controlled_rotation_times_z.bsr.angle, ) @@ -59,7 +78,7 @@ def decompose(self, gate: Gate) -> list[Gate]: ], ) - theta0, theta1, theta2 = XYXDecomposer().get_decomposition_angles(target_gate.bsr.axis, target_gate.bsr.angle) + theta0, theta1, theta2 = XYXDecomposer()._determine_rotation_angles(target_gate.bsr.axis, target_gate.bsr.angle) # noqa: SLF001 A = [Ry(target_qubit, theta1 / 2), Rx(target_qubit, theta2)] # noqa: N806 B = [Rx(target_qubit, -(theta0 + theta2) / 2), Ry(target_qubit, -theta1 / 2)] # noqa: N806 diff --git a/opensquirrel/passes/decomposer/general_decomposer.py b/opensquirrel/passes/decomposer/general_decomposer.py index a11ea769..6ff1606b 100644 --- a/opensquirrel/passes/decomposer/general_decomposer.py +++ b/opensquirrel/passes/decomposer/general_decomposer.py @@ -15,11 +15,22 @@ class Decomposer(ABC): def __init__(self, **kwargs: Any) -> None: ... @abstractmethod - def decompose(self, gate: Gate) -> list[Gate]: - raise NotImplementedError() + def decompose(self, gate: Gate) -> list[Gate]: ... def check_gate_replacement(gate: Gate, replacement_gates: Iterable[Gate]) -> None: + """Checks that the replacement gate(s) are valid by verifying that they operate on the same + qubits and preserve the quantum state up to a global phase. + + Args: + gate (Gate): Gate that is being replaced. + replacement_gates (Iterable[Gate]): Gate(s) that are replacing the original gate. + + Raises: + ValueError: If the replacement gates do not operate on the same qubits as the original gate. + ValueError: If the replacement gates do not preserve the quantum state up to a global phase. + + """ gate_qubit_indices = gate.qubit_indices replacement_gates_qubit_indices = set() replaced_matrix = get_circuit_matrix(get_reindexed_circuit([gate], gate_qubit_indices)) @@ -42,9 +53,12 @@ def check_gate_replacement(gate: Gate, replacement_gates: Iterable[Gate]) -> Non def decompose(ir: IR, 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. + """Decomposes the statements in the circuit IR using the provided decomposer. + + Args: + ir (IR): The circuit IR to decompose. + decomposer (Decomposer): The decomposer to use for decomposing the gates. + """ statement_index = 0 while statement_index < len(ir.statements): @@ -74,7 +88,14 @@ def decompose(self, gate: Gate) -> list[Gate]: def replace(ir: IR, 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) + """Replaces all occurrences of a specific gate in the circuit IR with a given sequence of other + gates. + + Args: + ir (IR): The circuit IR to modify. + gate (type[Gate]): Gate to replace. + replacement_gates_function (Callable[..., list[Gate]]): Function that returns a list of replacement gates. + """ + generic_replacer = _GenericReplacer(gate, replacement_gates_function) decompose(ir, generic_replacer) diff --git a/opensquirrel/passes/decomposer/mckay_decomposer.py b/opensquirrel/passes/decomposer/mckay_decomposer.py index 53be419e..c081d38c 100644 --- a/opensquirrel/passes/decomposer/mckay_decomposer.py +++ b/opensquirrel/passes/decomposer/mckay_decomposer.py @@ -12,15 +12,22 @@ class McKayDecomposer(Decomposer): def decompose(self, gate: Gate) -> list[Gate]: - """Return the McKay decomposition of a 1-qubit gate as a list of gates. - gate ----> Rz.Rx(pi/2).Rz.Rx(pi/2).Rz + """Decomposes a single-qubit gate using the McKay decomposition into a sequence of (at most) + 5 single-qubit gates; according tot the pattern Rz-Rx(pi/2)-Rz-Rx(pi/2)-Rz, where the angles + of the Rz gates are to be determined. - The global phase is deemed _irrelevant_, therefore a simulator backend might produce different output. - The results should be equivalent modulo global phase. - Notice that, if the gate is Rz or X90, it will not be decomposed further, since they are natively used - in the McKay decomposition. + Note: + The global phase of the original gate is (in general) not preserved in this decomposition. + + The decomposition is based on the procedure described in + [McKay et al. (2016)](https://arxiv.org/abs/1612.00858). + + Args: + gate (Gate): Single-qubit gate to decompose. + + Returns: + A sequence of (at most) 5 single-qubit gates that decompose the original gate. - Relevant literature: https://arxiv.org/abs/1612.00858 """ if not isinstance(gate, SingleQubitGate) or gate == X90(gate.qubit): return [gate] diff --git a/opensquirrel/passes/decomposer/swap2cnot_decomposer.py b/opensquirrel/passes/decomposer/swap2cnot_decomposer.py index 43d8869d..665c5d5c 100644 --- a/opensquirrel/passes/decomposer/swap2cnot_decomposer.py +++ b/opensquirrel/passes/decomposer/swap2cnot_decomposer.py @@ -10,15 +10,22 @@ class SWAP2CNOTDecomposer(Decomposer): - """Predefined decomposition of SWAP gate to 3 CNOT gates. - ---x--- ----•---[X]---•---- - | → | | | - ---x--- ---[X]---•---[X]--- - Note: - This decomposition preserves the global phase of the SWAP gate. - """ - def decompose(self, gate: Gate) -> list[Gate]: + """Predefined decomposition of SWAP gate to 3 CNOT gates. + + ![image](../../../_static/swap2cnot.png#only-light) + ![image](../../../_static/swap2cnot_dm.png#only-dark) + + Note: + This decomposition preserves the global phase of the SWAP gate. + + Args: + gate: SWAP gate to decompose. + + Returns: + A sequence of 3 CNOT gates that decompose the SWAP gate. + + """ if gate.name != "SWAP": return [gate] qubit0, qubit1 = gate.qubit_operands diff --git a/opensquirrel/passes/decomposer/swap2cz_decomposer.py b/opensquirrel/passes/decomposer/swap2cz_decomposer.py index b89e27aa..072d88c0 100644 --- a/opensquirrel/passes/decomposer/swap2cz_decomposer.py +++ b/opensquirrel/passes/decomposer/swap2cz_decomposer.py @@ -11,15 +11,22 @@ class SWAP2CZDecomposer(Decomposer): - """Predefined decomposition of SWAP gate to Ry rotations and 3 CZ gates. - ---x--- -------------•-[Ry(-pi/2)]-•-[Ry(+pi/2)]-•------------- - | → | | | - ---x--- -[Ry(-pi/2)]-•-[Ry(+pi/2)]-•-[Ry(-pi/2)]-•-[Ry(+pi/2)]- - Note: - This decomposition preserves the global phase of the SWAP gate. - """ - def decompose(self, gate: Gate) -> list[Gate]: + """Predefined decomposition of SWAP gate to 3 CZ gates and Ry rotations. + + ![image](../../../_static/swap2cz.png#only-light) + ![image](../../../_static/swap2cz_dm.png#only-dark) + + Note: + This decomposition preserves the global phase of the SWAP gate. + + Args: + gate: SWAP gate to decompose. + + Returns: + A sequence of 3 CZ gates and Ry rotations that decompose the SWAP gate. + + """ if gate.name != "SWAP": return [gate] qubit0, qubit1 = gate.qubit_operands diff --git a/opensquirrel/passes/exporter/cqasmv1_exporter.py b/opensquirrel/passes/exporter/cqasmv1_exporter.py index d20f9519..546d4e3d 100644 --- a/opensquirrel/passes/exporter/cqasmv1_exporter.py +++ b/opensquirrel/passes/exporter/cqasmv1_exporter.py @@ -39,6 +39,16 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) def export(self, circuit: Circuit) -> str: + """Exports the given circuit to the [cQASM v1](https://libqasm.readthedocs.io/en/latest/index.html) + format. + + Args: + circuit (Circuit): The quantum circuit to export. + + Returns: + The [cQASM v1](https://libqasm.readthedocs.io/en/latest/index.html) representation of the circuit. + + """ cqasmv1_creator = _CQASMv1Creator(circuit.register_manager) circuit.ir.accept(cqasmv1_creator) diff --git a/opensquirrel/passes/exporter/general_exporter.py b/opensquirrel/passes/exporter/general_exporter.py index c212ecec..de4f5494 100644 --- a/opensquirrel/passes/exporter/general_exporter.py +++ b/opensquirrel/passes/exporter/general_exporter.py @@ -9,5 +9,4 @@ def __init__(self, **kwargs: Any) -> None: """Generic router class""" @abstractmethod - def export(self, circuit: Circuit) -> Any: - raise NotImplementedError + def export(self, circuit: Circuit) -> Any: ... diff --git a/opensquirrel/passes/exporter/quantify_scheduler_exporter.py b/opensquirrel/passes/exporter/quantify_scheduler_exporter.py index 2a8c8989..f755d163 100644 --- a/opensquirrel/passes/exporter/quantify_scheduler_exporter.py +++ b/opensquirrel/passes/exporter/quantify_scheduler_exporter.py @@ -45,6 +45,18 @@ def __init__( self._operation_cycles = operation_cycles def export(self, circuit: Circuit) -> quantify_scheduler.Schedule: + """Exports the given circuit to the [quantify-scheduler](https://quantify-os.org/docs/quantify-scheduler/) + format. + + Args: + circuit (Circuit): The quantum circuit to export. + + Returns: + The quantify-scheduler [Schedule](https://quantify-os.org/docs/quantify-scheduler/v0.26.0/autoapi/quantify_scheduler/index.html#quantify_scheduler.Schedule) + representation of the circuit. + + """ + if "quantify_scheduler" not in globals(): msg = "quantify-scheduler is not installed, or cannot be installed on your system" raise ModuleNotFoundError(msg) @@ -77,7 +89,7 @@ def export(self, circuit: Circuit) -> quantify_scheduler.Schedule: return schedule_creator.schedule -class OperationRecord: +class _OperationRecord: def __init__( self, qubit_register_size: int, @@ -183,10 +195,10 @@ def __init__( operation_cycles: OperationCycles | None, ) -> None: self._qubit_register_size = register_manager.qubit_register_size - self._operation_record = OperationRecord(self._qubit_register_size, schedulables, cycle_time, operation_cycles) + self._operation_record = _OperationRecord(self._qubit_register_size, schedulables, cycle_time, operation_cycles) @property - def operation_record(self) -> OperationRecord: + def operation_record(self) -> _OperationRecord: return self._operation_record def visit_gate(self, gate: Gate) -> None: diff --git a/opensquirrel/passes/mapper/check_mapper.py b/opensquirrel/passes/mapper/check_mapper.py index bdb606fd..a97b75f7 100644 --- a/opensquirrel/passes/mapper/check_mapper.py +++ b/opensquirrel/passes/mapper/check_mapper.py @@ -22,11 +22,12 @@ def _check_scenario(circuit: Circuit, mapper: Mapper) -> None: - """Check if the given scenario can be mapped. + """Checks if the given scenario can be mapped. Args: circuit: Circuit containing the scenario to check against. mapper: Mapper to use. + """ ir_copy = deepcopy(circuit.ir) circuit.map(mapper) @@ -34,12 +35,14 @@ def _check_scenario(circuit: Circuit, mapper: Mapper) -> None: def check_mapper(mapper: Mapper) -> None: - """Check if the `mapper` complies with the OpenSquirrel requirements. + """Checks if the mapper complies with the OpenSquirrel requirements. - If a ``Mapper`` implementation passes these checks, it should be compatible with the ``Circuit.map`` method. + If a ``Mapper`` implementation passes these checks, it should be compatible with the + ``Circuit.map`` method. Args: mapper: Mapper to check. + """ assert isinstance(mapper, Mapper) diff --git a/opensquirrel/passes/mapper/mip_mapper.py b/opensquirrel/passes/mapper/mip_mapper.py index 9274955e..299fe458 100644 --- a/opensquirrel/passes/mapper/mip_mapper.py +++ b/opensquirrel/passes/mapper/mip_mapper.py @@ -27,17 +27,20 @@ class MIPMapper(Mapper): the sum of distances between mapped operands of all two-qubit gates, using a linearized MIP formulation based on OpenQL's approach. - The formulation uses binary variables x[i][k] indicating whether virtual qubit i - is mapped to physical qubit k, and continuous variables w[i][k] representing the - cost contribution for mapping i to k. + The formulation uses binary variables $x_{ik}$ indicating whether virtual qubit $i$ + is mapped to physical qubit $k$, and continuous variables $w_{ik}$ representing the + cost contribution for mapping $i$ to $k$. - The objective function minimizes sum(w[i][k]) + epsilon * sum_{i,k: i!=k} x[i][k], + The objective function minimizes $\\sum w_{ik} + \\epsilon \\sum_{i \\neq k} x_{ik}$, where the epsilon term provides a small penalty for non-identity mappings as a tiebreaker. Subject to the following constraints: - 1. Each virtual qubit assigned to exactly one physical qubit: sum_k x[i][k] == 1 - 2. Each physical qubit assigned to at most one virtual qubit: sum_i x[i][k] <= 1 - 3. Linearization: costmax[i][k] * x[i][k] + sum_{j,l} refcount[i][j]*distance[k][l]*x[j][l] - w[i][k] <= costmax[i][k] + + 1. Each virtual qubit assigned to exactly one physical qubit: $\\sum_k x_{ik} = 1$ + + 2. Each physical qubit assigned to at most one virtual qubit: $\\sum_i x_{ik} \\leq 1$ + + 3. Linearization: $\\Gamma_{ik} x_{ik} + \\sum_{j,l} C^\\text{ref}_{ij}d_{kl}x_{jl} - w_{ik} \\leq \\Gamma_{ik}$ Args: connectivity: Physical qubit connectivity graph. @@ -45,10 +48,13 @@ class MIPMapper(Mapper): epsilon: Small penalty coefficient for non-identity mappings (default: 1e-6). Example: + ```python >>> connectivity = {"0": [1], "1": [0, 2], "2": [1]} >>> mapper = MIPMapper(connectivity=connectivity, timeout=10.0) >>> mapping = mapper.map(circuit, qubit_register_size=3) - """ # noqa: E501 + ``` + + """ def __init__( self, @@ -72,13 +78,12 @@ def map( circuit: Circuit, qubit_register_size: int, ) -> Mapping: - """ - Find an initial mapping of virtual qubits to physical qubits that minimizes + """Find an initial mapping of virtual qubits to physical qubits that minimizes the sum of distances between mapped operands of all two-qubit gates, using Mixed Integer Programming (MIP). This method formulates the mapping as a linear assignment problem, where the - objective is to minimize the total "distance cost" of executing all two-qubit + objective is to minimize the total _distance cost_ of executing all two-qubit gates, given the connectivity. Args: @@ -86,11 +91,12 @@ def map( qubit_register_size (int): The number of virtual qubits in the circuit. Returns: - Mapping: Mapping from virtual to physical qubits. + Mapping from virtual to physical qubits. Raises: RuntimeError: If the MIP solver fails to find a feasible mapping or times out. RuntimeError: If the number of virtual qubits exceeds the number of physical qubits. + """ self.num_virtual_qubits = qubit_register_size self.num_physical_qubits = len(self.connectivity) diff --git a/opensquirrel/passes/mapper/qgym_mapper.py b/opensquirrel/passes/mapper/qgym_mapper.py index be04d1a5..b10101f3 100644 --- a/opensquirrel/passes/mapper/qgym_mapper.py +++ b/opensquirrel/passes/mapper/qgym_mapper.py @@ -46,20 +46,21 @@ def map( qubit_register_size: int, ) -> Mapping: """ - Compute an initial logical-to-physical qubit mapping using a trained - Stable-Baselines3 agent acting in the QGym InitialMapping environment. + Compute an initial logical-to-physical qubit mapping using a trained Stable-Baselines3 + agent acting in the QGym InitialMapping environment. Args: circuit (Circuit): The quantum circuit to be mapped. qubit_register_size (int): Number of logical (virtual) qubits in the circuit. Returns: - Mapping: Mapping from virtual to physical qubits. + Mapping from virtual to physical qubits. Raises: ValueError: If the number of logical qubits differs from the number of physical qubits. ValueError: If the agent produces an incomplete or invalid mapping. - RuntimeError: If no 'mapping' key is found in the final observation. + RuntimeError: If no mapping key is found in the final observation. + """ num_physical = self.hardware_connectivity.number_of_nodes() if qubit_register_size != num_physical: diff --git a/opensquirrel/passes/mapper/qubit_remapper.py b/opensquirrel/passes/mapper/qubit_remapper.py index 740f9e97..897fa0d3 100644 --- a/opensquirrel/passes/mapper/qubit_remapper.py +++ b/opensquirrel/passes/mapper/qubit_remapper.py @@ -59,6 +59,16 @@ def visit_two_qubit_gate(self, gate: TwoQubitGate) -> TwoQubitGate: def get_remapped_ir(circuit: Circuit, mapping: Mapping) -> IR: + """Get replacement IR where the qubit indices are remapped according to the provided mapping. + + Args: + circuit (Circuit): The input circuit from which the replacement IR is to be generated. + mapping (Mapping): Mapping of virtual qubit indices to physical qubit indices. + + Returns: + Replacement IR with remapped qubit indices. + + """ if len(mapping) > circuit.qubit_register_size: msg = ( f"size of the mapping {len(mapping)!r} is larger than the number of qubits {circuit.qubit_register_size!r}" @@ -72,6 +82,13 @@ def get_remapped_ir(circuit: Circuit, mapping: Mapping) -> IR: def remap_ir(circuit: Circuit, mapping: Mapping) -> None: + """Remap the IR of the circuit according to the provided mapping. + + Args: + circuit (Circuit): The input circuit whose IR is to be remapped. + mapping (Mapping): Mapping of virtual qubit indices to physical qubit indices. + + """ if len(mapping) > circuit.qubit_register_size: msg = ( f"size of the mapping {len(mapping)!r} is larger than the number of qubits {circuit.qubit_register_size!r}" diff --git a/opensquirrel/passes/mapper/simple_mappers.py b/opensquirrel/passes/mapper/simple_mappers.py index b4d4d0a1..3fff093d 100644 --- a/opensquirrel/passes/mapper/simple_mappers.py +++ b/opensquirrel/passes/mapper/simple_mappers.py @@ -27,7 +27,16 @@ def map( circuit: Circuit, qubit_register_size: int, ) -> Mapping: - """Create identity mapping.""" + """Map the circuit according to a identity mapping. + + Args: + ir (IR): The intermediate representation of the quantum circuit to be mapped. + qubit_register_size (int): The size of the (virtual) qubit register. + + Returns: + Mapping from virtual to physical qubits. + + """ return Mapping(list(range(qubit_register_size))) diff --git a/opensquirrel/passes/mapper/utils.py b/opensquirrel/passes/mapper/utils.py index b3c9cfab..2df9099d 100644 --- a/opensquirrel/passes/mapper/utils.py +++ b/opensquirrel/passes/mapper/utils.py @@ -4,6 +4,15 @@ def make_interaction_graph(ir: IR) -> nx.Graph: + """Determine the interaction graph of the circuit. + + Args: + ir (IR): Intermediate representation of the quantum circuit. + + Returns: + The interaction graph of the circuit. + + """ interaction_graph = nx.Graph() gates = (statement for statement in ir.statements if isinstance(statement, Gate)) diff --git a/opensquirrel/passes/merger/general_merger.py b/opensquirrel/passes/merger/general_merger.py index e77dfa21..8c9f65bd 100644 --- a/opensquirrel/passes/merger/general_merger.py +++ b/opensquirrel/passes/merger/general_merger.py @@ -11,23 +11,39 @@ class Merger(ABC): def __init__(self, **kwargs: Any) -> None: ... @abstractmethod - def merge(self, ir: IR, qubit_register_size: int) -> None: - raise NotImplementedError + def merge(self, ir: IR, qubit_register_size: int) -> None: ... def can_move_statement_before_barrier(instruction: Instruction, barriers: list[Instruction]) -> bool: - """Checks whether an instruction can be moved before a group of 'linked' barriers. - Returns True if none of the qubits used by the instruction are part of any barrier, False otherwise. + """Checks whether an instruction can be moved before a group of _linked_ barriers. + + Args: + instruction (Instruction): The instruction to be moved. + barriers (list[Instruction]): The group of linked barriers. + + Returns: + True if none of the qubits used by the instruction are part of the linked barriers, otherwise False. + """ barriers_group_qubit_operands = set(flatten_list([list(barrier.qubit_operands) for barrier in barriers])) return not any(qubit in barriers_group_qubit_operands for qubit in instruction.qubit_operands) def can_move_before(statement: Statement, statement_group: list[Statement]) -> bool: - """Checks whether a statement can be moved before a group of statements, following the logic below: + """Checks whether a statement can be moved before a group of statements, following the logic + below: + - A barrier cannot be moved up. - A (non-barrier) statement cannot be moved before another (non-barrier) statement. - - A (non-barrier) statement may be moved before a group of 'linked' barriers. + - A (non-barrier) statement may be moved before a group of _linked_ barriers. + + Args: + statement (Statement): The statement to be moved. + statement_group (list[Statement]): The group of statements to move before. + + Returns: + True if the statement can be moved before the group of statements, otherwise False. + """ if isinstance(statement, Barrier): return False @@ -39,9 +55,14 @@ def can_move_before(statement: Statement, statement_group: list[Statement]) -> b def group_linked_barriers(statements: list[Statement]) -> list[list[Statement]]: - """Receives a list of statements. - Returns a list of lists of statements, where each list of statements is - either a single instruction, or a list of 'linked' barriers (consecutive barriers that cannot be split). + """Groups linked barriers in the input list of statements. + + Args: + statements (list[Statement]): The list of statements. + + Returns: + A list of 'lists of statements', which are either single instructions or linked barriers. + """ ret: list[list[Statement]] = [] index = -1 @@ -57,11 +78,16 @@ def group_linked_barriers(statements: list[Statement]) -> list[list[Statement]]: def rearrange_barriers(ir: IR) -> None: - """Receives an IR. - Builds an enumerated list of lists of statements, where each list of statements is - either a single instruction, or a list of 'linked' barriers (consecutive barriers that cannot be split). - Then sorts that enumerated list of lists so that instructions can be moved before groups of barriers. - And updates the input IR with the flattened list of sorted statements. + """Rearrages barriers in the input IR. + + Builds an enumerated list of lists of statements, where each list of statements is either a + single instruction, or a list of 'linked' barriers (consecutive barriers that cannot be split). + Then sorts that enumerated list of lists so that instructions can be moved before groups of + barriers. And updates the input IR with the flattened list of sorted statements. + + Args: + ir (IR): The input IR to be modified. + """ statements_groups = group_linked_barriers(ir.statements) for i, statement_group in enumerate(statements_groups): diff --git a/opensquirrel/passes/merger/single_qubit_gates_merger.py b/opensquirrel/passes/merger/single_qubit_gates_merger.py index 0e239d3b..fb37e6b0 100644 --- a/opensquirrel/passes/merger/single_qubit_gates_merger.py +++ b/opensquirrel/passes/merger/single_qubit_gates_merger.py @@ -9,12 +9,11 @@ class SingleQubitGatesMerger(Merger): def merge(self, ir: IR, qubit_register_size: int) -> None: - """Merge all consecutive 1-qubit gates in the circuit. - Gates obtained from merging other gates become anonymous gates. + """Merge all consecutive single-qubit gates in the circuit. Args: - ir: Intermediate representation of the circuit. - qubit_register_size: Size of the qubit register + ir (IR): Intermediate representation of the circuit. + qubit_register_size (int): Size of the qubit register """ accumulators_per_qubit: dict[Qubit, SingleQubitGate] = { diff --git a/opensquirrel/passes/router/astar_router.py b/opensquirrel/passes/router/astar_router.py index 3f490f20..864d3478 100644 --- a/opensquirrel/passes/router/astar_router.py +++ b/opensquirrel/passes/router/astar_router.py @@ -18,6 +18,17 @@ def __init__( self._distance_metric = distance_metric def route(self, ir: IR, qubit_register_size: int) -> IR: + """Route the input IR using the [A*](https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.astar.astar_path.html) + search algorithm. + + Args: + ir (IR): The input IR to be routed. + qubit_register_size (int): Size of the qubit register. + + Returns: + The routed IR. + + """ pathfinder: PathFinderType = self._astar_pathfinder return ProcessSwaps.process_swaps(ir, qubit_register_size, self._connectivity, pathfinder) diff --git a/opensquirrel/passes/router/common.py b/opensquirrel/passes/router/common.py index 0ab51d82..db967b59 100644 --- a/opensquirrel/passes/router/common.py +++ b/opensquirrel/passes/router/common.py @@ -24,7 +24,7 @@ def get_graph(connectivity: Connectivity) -> nx.Graph: connectivity (dict[str, list[int]]): Connectivity mapping of physical qubits. Returns: - nx.Graph: Networkx graph from the given connectivity. + Graph of the given connectivity. """ return nx.Graph({int(start): ends for start, ends in connectivity.items()}) @@ -37,7 +37,7 @@ def get_identity_mapping(qubit_register_size: int) -> dict[int, int]: qubit_register_size (int): The size of the qubit register. Returns: - dict[int, int]: An identity mapping. + An identity mapping. """ return {qubit_index: qubit_index for qubit_index in range(qubit_register_size)} @@ -59,7 +59,7 @@ def process_swaps( connectivity (dict[str, list[int]]): Connectivity mapping of physical qubits. pathfinder (PathFinderType): The pathfinder algorithm. Returns: - IR: IR with the SWAPs processed through. + IR with the SWAPs processed through. """ graph = get_graph(connectivity) @@ -83,7 +83,7 @@ def _plan_swaps( initial_mapping (dict[int, int]): The initial mapping of the qubits. pathfinder (Callable[[nx.Graph, int, int], list[int]]): Returns: - dict[int, SWAP]: An mapping from the insert position to SWAP instruction. + An mapping from the insert position to SWAP instruction. """ planned_swaps: dict[int, SWAP] = {} @@ -121,7 +121,7 @@ def _apply_swaps(ir: IR, planned_swaps: dict[int, SWAP], initial_mapping: dict[i planned_swaps (dict[int, SWAP]): The mapping from the insert position to SWAP instruction. initial_mapping (dict[int, int]): The initial mapping of the qubits. Returns: - list[Statement]: An updated list of IR Statements. + An updated list of IR Statements. """ new_ir_statements: list[Statement] = [] diff --git a/opensquirrel/passes/router/general_router.py b/opensquirrel/passes/router/general_router.py index 7cb4e37a..8414d41e 100644 --- a/opensquirrel/passes/router/general_router.py +++ b/opensquirrel/passes/router/general_router.py @@ -11,5 +11,4 @@ def __init__(self, connectivity: Connectivity, **kwargs: Any) -> None: """Generic router class""" @abstractmethod - def route(self, ir: IR, qubit_register_size: int) -> IR: - raise NotImplementedError + def route(self, ir: IR, qubit_register_size: int) -> IR: ... diff --git a/opensquirrel/passes/router/heuristics.py b/opensquirrel/passes/router/heuristics.py index 15947eeb..1f3efde4 100644 --- a/opensquirrel/passes/router/heuristics.py +++ b/opensquirrel/passes/router/heuristics.py @@ -10,15 +10,17 @@ class DistanceMetric(Enum): def calculate_distance(q0_index: int, q1_index: int, num_columns: int, distance_metric: DistanceMetric) -> float: - """ - Calculate the distance between two qubits based on the specified distance metric. + """Calculate the distance between two qubits based on the specified distance metric. + Args: q0_index (int): The index of the first qubit. q1_index (int): The index of the second qubit. num_columns (int): The number of columns in the grid. distance_metric (DistanceMetric): Distance metric to be used (Manhattan, Euclidean, or Chebyshev). + Returns: - float: The distance between the two qubits. + The distance between the two qubits. + """ x1, y1 = divmod(q0_index, num_columns) x2, y2 = divmod(q1_index, num_columns) diff --git a/opensquirrel/passes/router/shortest_path_router.py b/opensquirrel/passes/router/shortest_path_router.py index a0d1f67d..93432f10 100644 --- a/opensquirrel/passes/router/shortest_path_router.py +++ b/opensquirrel/passes/router/shortest_path_router.py @@ -13,5 +13,16 @@ def __init__(self, connectivity: Connectivity, **kwargs: Any) -> None: super().__init__(connectivity, **kwargs) def route(self, ir: IR, qubit_register_size: int) -> IR: + """Route the input IR using the [shortest-path](https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.generic.shortest_path.html) + search algorithm. + + Args: + ir (IR): The input IR to be routed. + qubit_register_size (int): Size of the qubit register. + + Returns: + The routed IR. + + """ pathfinder: PathFinderType = nx.shortest_path return ProcessSwaps.process_swaps(ir, qubit_register_size, self._connectivity, pathfinder) diff --git a/opensquirrel/passes/validator/general_validator.py b/opensquirrel/passes/validator/general_validator.py index 38366234..6df47105 100644 --- a/opensquirrel/passes/validator/general_validator.py +++ b/opensquirrel/passes/validator/general_validator.py @@ -8,6 +8,4 @@ class Validator(ABC): def __init__(self, **kwargs: Any) -> None: ... @abstractmethod - def validate(self, ir: IR) -> None: - """Base validate method to be implemented by inheriting validator classes.""" - raise NotImplementedError + def validate(self, ir: IR) -> None: ... diff --git a/opensquirrel/passes/validator/interaction_validator.py b/opensquirrel/passes/validator/interaction_validator.py index bf1793b6..a6c55d29 100644 --- a/opensquirrel/passes/validator/interaction_validator.py +++ b/opensquirrel/passes/validator/interaction_validator.py @@ -12,14 +12,14 @@ def __init__(self, connectivity: dict[str, list[int]], **kwargs: Any) -> None: self.connectivity = connectivity def validate(self, ir: IR) -> None: - """ - Check if the circuit interactions faciliate a 1-to-1 mapping to the target hardware. + """Check if the circuit interactions faciliate a 1-to-1 mapping to the target hardware. Args: ir (IR): The intermediate representation of the circuit to be checked. Raises: - ValueError: If the circuit can't be mapped to the target hardware. + ValueError: If the circuit cannot be mapped to the target hardware. + """ non_executable_interactions = [] for statement in ir.statements: diff --git a/opensquirrel/passes/validator/primitive_gate_validator.py b/opensquirrel/passes/validator/primitive_gate_validator.py index 22ce4f1b..876c636b 100644 --- a/opensquirrel/passes/validator/primitive_gate_validator.py +++ b/opensquirrel/passes/validator/primitive_gate_validator.py @@ -10,14 +10,14 @@ def __init__(self, primitive_gate_set: list[str], **kwargs: Any) -> None: self.primitive_gate_set = primitive_gate_set def validate(self, ir: IR) -> None: - """ - Check if all unitary gates in the circuit are part of the primitive gate set. + """Check if all gates in the circuit are part of the primitive gate set. Args: ir (IR): The intermediate representation of the circuit to be checked. Raises: - ValueError: If any unitary gate in the circuit is not part of the primitive gate set. + ValueError: If any gate in the circuit is not part of the primitive gate set. + """ gates_not_in_primitive_gate_set = [ statement.name diff --git a/opensquirrel/reader/libqasm_parser.py b/opensquirrel/reader/libqasm_parser.py index 89f12c8f..4fee7903 100644 --- a/opensquirrel/reader/libqasm_parser.py +++ b/opensquirrel/reader/libqasm_parser.py @@ -247,7 +247,16 @@ def _create_register_manager(self, ast: Any) -> RegisterManager: return RegisterManager(qubit_registry, bit_registry) def circuit_from_string(self, s: str) -> Circuit: - # Analyzer will return an Abstract Syntax Tree (AST). + """Parse a cQASM string into a [Circuit][opensquirrel.Circuit] using the + [libQASM](https://qutech-delft.github.io/libqasm/) parser. + + Args: + s (str): The cQASM string to be parsed. + + Returns: + The parsed circuit. + + """ analyzer = LibQasmParser._create_analyzer() ast = analyzer.analyze_string(s) if not isinstance(ast, cqasm.semantic.Program): diff --git a/opensquirrel/register_manager.py b/opensquirrel/register_manager.py index 8f7de286..fc1bd9a5 100644 --- a/opensquirrel/register_manager.py +++ b/opensquirrel/register_manager.py @@ -123,6 +123,15 @@ def bit_register_name(self) -> str: @staticmethod def generate_virtual_register(registry: Registry) -> Register: + """Generate a virtual (qu)bit register from a registry of (qu)bit registers. + + Args: + registry (Registry): Registry of (qu)bit registers. + + Returns: + Register: Virtual (qu)bit register. + + """ registers = list(registry.values()) register_cls = registers[0].__class__ virtual_index = 0 @@ -138,6 +147,12 @@ def add_register(self, register: QubitRegister) -> None: ... def add_register(self, register: BitRegister) -> None: ... def add_register(self, register: QubitRegister | BitRegister) -> None: + """Add a (qu)bit register to the register manager. + + Args: + register (QubitRegister | BitRegister): (Qu)bit register to add. + + """ if isinstance(register, QubitRegister): if register.name in self._qubit_registry: msg = f"qubit register with name {register.name!r} already exists" @@ -155,16 +170,28 @@ def add_register(self, register: QubitRegister | BitRegister) -> None: raise TypeError(msg) def get_qubit_register(self, qubit_register_name: str) -> QubitRegister: + """Get a qubit register by name. + + Args: + qubit_register_name (str): Name of the qubit register. + + Returns: + The qubit register with the specified name. + + """ return self._qubit_registry[qubit_register_name] def get_bit_register(self, bit_register_name: str) -> BitRegister: - return self._bit_registry[bit_register_name] + """Get a bit register by name. - def __repr__(self) -> str: - return ( - f"virtual_qubit_register:\n{self._virtual_qubit_register}\n" - f"virtual_bit_register:\n{self._virtual_bit_register}" - ) + Args: + bit_register_name (str): Name of the bit register. + + Returns: + The bit register with the specified name. + + """ + return self._bit_registry[bit_register_name] def __eq__(self, other: Any) -> bool: if not isinstance(other, RegisterManager): @@ -173,3 +200,9 @@ def __eq__(self, other: Any) -> bool: self._virtual_qubit_register == other._virtual_qubit_register and self._virtual_bit_register == other._virtual_bit_register ) + + def __repr__(self) -> str: + return ( + f"virtual_qubit_register:\n{self._virtual_qubit_register}\n" + f"virtual_bit_register:\n{self._virtual_bit_register}" + ) diff --git a/opensquirrel/reindexer/qubit_reindexer.py b/opensquirrel/reindexer/qubit_reindexer.py index 294a1f23..765fa660 100644 --- a/opensquirrel/reindexer/qubit_reindexer.py +++ b/opensquirrel/reindexer/qubit_reindexer.py @@ -76,6 +76,17 @@ def get_reindexed_circuit( qubit_indices: list[int], bit_register_size: int = 0, ) -> Circuit: + """Reindex the qubit indices of the given (replacement) gates in a circuit. + + Args: + replacement_gates (Iterable[Gate]): The gates to be reindexed. + qubit_indices (list[int]): The new qubit indices. + bit_register_size (int): The size of the bit register. + + Returns: + The reindexed circuit. + + """ from opensquirrel.circuit import Circuit qubit_reindexer = _QubitReindexer(qubit_indices) diff --git a/opensquirrel/utils/context.py b/opensquirrel/utils/context.py index 8a034392..eb144082 100644 --- a/opensquirrel/utils/context.py +++ b/opensquirrel/utils/context.py @@ -10,9 +10,10 @@ def temporary_class_attr(cls: type[Any], attr: str, value: Any) -> Generator[Non The assigned value will only be held within the context. Args: - cls: Class of which the class attribute value is to be assigned. - attr: Name of class attribute. - value: Value to assign to class attribute (must be correct type). + cls (type[Any]): Class of which the class attribute value is to be assigned. + attr (str): Name of class attribute. + value (Any): Value to assign to class attribute (must be correct type). + """ original_value = getattr(cls, attr) setattr(cls, attr, value) diff --git a/opensquirrel/utils/general_math.py b/opensquirrel/utils/general_math.py index 3727f32c..75cf4ea2 100644 --- a/opensquirrel/utils/general_math.py +++ b/opensquirrel/utils/general_math.py @@ -2,11 +2,28 @@ def acos(value: float) -> float: - """Fix float approximations like 1.0000000000002, which acos does not like.""" + """Fix float approximations like 1.0000000000002, which acos does not like. + + Args: + value (float): The input value. + + Returns: + The arc cosine of the input value. + + """ value = max(min(value, 1.0), -1.0) return math.acos(value) def are_axes_consecutive(axis_a_index: int, axis_b_index: int) -> bool: - """Check if axis 'a' immediately precedes axis 'b' (in a circular fashion [x, y, z, x...]).""" + """Check if axis 'a' immediately precedes axis 'b' (in a circular fashion [x, y, z, x...]). + + Args: + axis_a_index (int): The index of axis 'a'. + axis_b_index (int): The index of axis 'b'. + + Returns: + True if axis 'a' immediately precedes axis 'b', otherwise False. + + """ return axis_a_index - axis_b_index in (-1, 2) diff --git a/opensquirrel/utils/identity_filter.py b/opensquirrel/utils/identity_filter.py index 3120797f..78f6d5cd 100644 --- a/opensquirrel/utils/identity_filter.py +++ b/opensquirrel/utils/identity_filter.py @@ -8,4 +8,13 @@ def filter_out_identities(gates: Iterable[Gate]) -> list[Gate]: + """Filter out identity gates from a list of gates. + + Args: + gates (Iterable[Gate]): The list of gates to filter. + + Returns: + A list of gates with identity gates removed. + + """ return [gate for gate in gates if not gate.is_identity()] diff --git a/opensquirrel/utils/list.py b/opensquirrel/utils/list.py index fa2b7460..337e4bd4 100644 --- a/opensquirrel/utils/list.py +++ b/opensquirrel/utils/list.py @@ -6,4 +6,13 @@ def flatten_list(list_to_flatten: list[list[Any]]) -> list[Any]: + """Flattens a list of lists into a single list. + + Args: + list_to_flatten (list[list[Any]]): The list of lists to flatten. + + Returns: + A single flattened list. + + """ return functools.reduce(operator.iadd, list_to_flatten, []) diff --git a/opensquirrel/utils/matrix_expander.py b/opensquirrel/utils/matrix_expander.py index fbfcc929..ec6dcff9 100644 --- a/opensquirrel/utils/matrix_expander.py +++ b/opensquirrel/utils/matrix_expander.py @@ -25,20 +25,20 @@ def get_reduced_ket(ket: int, qubits: Iterable[QubitLike]) -> int: - """ - Given a quantum ket represented by its corresponding base-10 integer, this computes the reduced ket - where only the given qubits appear, in order. - Roughly equivalent to the `pext` assembly instruction (bits extraction). + """Given a quantum ket represented by its corresponding base-10 integer, this computes the + reduced ket where only the given qubits appear, in order. + + Roughly equivalent to the PEXT assembly instruction (bits extraction). Args: - ket: A quantum ket, represented by its corresponding non-negative integer. + ket (int): A quantum ket, represented by its corresponding non-negative integer. By convention, qubit #0 corresponds to the least significant bit. - qubits: The indices of the qubits to extract. Order matters. + qubits (Iterable[QubitLike]): The indices of the qubits to extract. Order matters. Returns: The non-negative integer corresponding to the reduced ket. - Examples: + Example: >>> get_reduced_ket(1, [Qubit(0)]) # 0b01 1 >>> get_reduced_ket(1111, [Qubit(2)]) # 0b01 @@ -51,6 +51,7 @@ def get_reduced_ket(ket: int, qubits: Iterable[QubitLike]) -> int: 2 >>> get_reduced_ket(101, [Qubit(0), Qubit(1)]) # 0b01 1 + """ reduced_ket = 0 for i, qubit in enumerate(qubits): @@ -61,22 +62,24 @@ def get_reduced_ket(ket: int, qubits: Iterable[QubitLike]) -> int: def expand_ket(base_ket: int, reduced_ket: int, qubits: Iterable[QubitLike]) -> int: - """ - Given a base quantum ket on n qubits and a reduced ket on a subset of those qubits, this computes the expanded ket - where the reduction qubits and the other qubits are set based on the reduced ket and the base ket, respectively. - Roughly equivalent to the `pdep` assembly instruction (bits deposit). + """Given a base quantum ket on $n$ qubits and a reduced ket on a subset of those qubits, this + computes the expanded ket where the reduction qubits and the other qubits are set based on the + reduced ket and the base ket, respectively. + + Roughly equivalent to the PDEP assembly instruction (bits deposit). Args: - base_ket: A quantum ket, represented by its corresponding non-negative integer. + base_ket (int): A quantum ket, represented by its corresponding non-negative integer. By convention, qubit #0 corresponds to the least significant bit. - reduced_ket: A quantum ket, represented by its corresponding non-negative integer. + reduced_ket (int): A quantum ket, represented by its corresponding non-negative integer. By convention, qubit #0 corresponds to the least significant bit. - qubits: The indices of the qubits to expand from the reduced ket. Order matters. + qubits (Iterable[QubitLike]): The indices of the qubits to expand from the reduced ket. + Order matters. Returns: The non-negative integer corresponding to the expanded ket. - Examples: + Example: >>> expand_ket(0b00000, 0b0, [Qubit(5)]) # 0b000000 0 >>> expand_ket(0b00000, 0b1, [Qubit(5)]) # 0b100000 @@ -95,6 +98,7 @@ def expand_ket(base_ket: int, reduced_ket: int, qubits: Iterable[QubitLike]) -> 10 >>> expand_ket(0b0001, 0b101, [Qubit(1), Qubit(2), Qubit(3)]) # 0b1011 11 + """ expanded_ket = base_ket for i, qubit in enumerate(qubits): @@ -105,7 +109,7 @@ def expand_ket(base_ket: int, reduced_ket: int, qubits: Iterable[QubitLike]) -> return expanded_ket -class MatrixExpander(IRVisitor): +class _MatrixExpander(IRVisitor): def __init__(self, qubit_register_size: int) -> None: self.qubit_register_size = qubit_register_size @@ -244,16 +248,35 @@ def visit_canonical_gate(self, gate: TwoQubitGate) -> Any: def can1(axis: AxisLike, angle: float, phase: float = 0) -> NDArray[np.complex128]: + """Generates the unitary matrix for a canonical single-qubit gate. + + Args: + axis (AxisLike): The rotation axis. + angle (float): The rotation angle in radians. + phase (float): The phase in radians. + + Returns: + The unitary $2\\times 2$ matrix. + + """ nx, ny, nz = Axis(axis) result = cmath.rect(1, phase) * ( math.cos(angle / 2) * np.identity(2) - 1j * math.sin(angle / 2) * (nx * X + ny * Y + nz * Z) ) - return np.asarray(result, dtype=np.complex128) def can2(canonical_axis: AxisLike) -> NDArray[np.complex128]: + """Generates the unitary matrix for a canonical two-qubit gate. + + Args: + canonical_axis (AxisLike): The canonical axis. + + Returns: + The unitary $4\\times 4$ matrix. + + """ tx, ty, tz = CanonicalAxis(canonical_axis) return np.array( @@ -288,25 +311,27 @@ def can2(canonical_axis: AxisLike) -> NDArray[np.complex128]: def get_matrix(gate: Gate, qubit_register_size: int) -> NDArray[np.complex128]: - """ - Compute the unitary matrix corresponding to the gate applied to those qubit operands, taken among any number of - qubits. This can be used for, e.g., + """Compute the unitary matrix corresponding to the gate applied to those qubit operands, taken + among any number of qubits. This can be used for, e.g., + - testing, - permuting the operands of multi-qubit gates, - simulating a circuit (simulation in this way is inefficient for large numbers of qubits). Args: - gate: The gate, including the qubits on which it is operated on. - qubit_register_size: The size of the qubit register. + gate (Gate): The gate, including the qubits on which it is operated on. + qubit_register_size (int): The size of the qubit register. + + Returns: + The unitary matrix corresponding to the gate applied to the qubit operands. - Examples: + Example: >>> X = lambda q: BlochSphereRotation(qubit=q, axis=(1, 0, 0), angle=math.pi, phase=math.pi / 2) >>> get_matrix(X(1), 2).astype(int) # X q[1] array([[0, 0, 1, 0], [0, 0, 0, 1], [1, 0, 0, 0], [0, 1, 0, 0]]) - >>> CNOT02 = ControlledGate(0, X(2)) >>> get_matrix(CNOT02, 3).astype(int) # CNOT q[0], q[2] array([[1, 0, 0, 0, 0, 0, 0, 0], @@ -326,6 +351,7 @@ def get_matrix(gate: Gate, qubit_register_size: int) -> NDArray[np.complex128]: [0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0]]) + """ - expander = MatrixExpander(qubit_register_size) + expander = _MatrixExpander(qubit_register_size) return np.asarray(gate.accept(expander), dtype=np.complex128) diff --git a/opensquirrel/writer/writer.py b/opensquirrel/writer/writer.py index e9a43532..b8b62276 100644 --- a/opensquirrel/writer/writer.py +++ b/opensquirrel/writer/writer.py @@ -135,8 +135,16 @@ def visit_wait(self, wait: Wait) -> None: def circuit_to_string(circuit: Circuit) -> str: + """Convert a circuit to its [cQASM](https://qutech-delft.github.io/cQASM-spec/) + string representation. + + Args: + circuit (Circuit): The circuit to convert. + + Returns: + The [cQASM](https://qutech-delft.github.io/cQASM-spec/) string representation of the circuit. + + """ writer_impl = _WriterImpl(circuit.register_manager) circuit.ir.accept(writer_impl) - - # Remove all trailing lines and leave only one return writer_impl.output.rstrip() + "\n"