Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ parameter
- Add `interaction_graph` as a property of the `Circuit`.
- Add `mapping` as an attribute of the `Circuit`, which contains an identity mapping, prior to any
arbitrarily applied mapper pass.
- The measure instruction accepts an axis parameter
- `MeasureDecomposer` to decompose arbitrary measurements to a decomposition of single-qubit gates and a +Z measurement.

## [ 0.9.0 ] - [ 2025-12-19 ]

Expand Down
25 changes: 25 additions & 0 deletions docs/compilation-passes/decomposition/measure-decomposer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
The measure decomposer (`MeasureDecomposer`) is used to decompose measurements along an arbitrary axis into
at most 2 single-qubit gates and a measurement along the +Z-axis.

If the measurement axis is given by the unit vector
$\\hat{n} = (n_x, n_y, n_z) = (\\sin\\theta\\cos\\phi, \\sin\\theta\\sin\\phi, \\cos\\theta)$,
with $\\theta = \\arccos(n_z)$ and $\\phi = \\arctan2(n_y, n_x)$. We can define a unitary
$U = Rz(\\phi) Ry(\\theta)$, that maps the +Z eigenstates to the $\\hat{n}$ eigenstates.

Measuring in the $\\hat{n}$ direction is equivalent to first applying
$U^\\dagger = R_y(-\\theta) R_z(-\\phi)$ and then measuring in the +Z direction.

Order of instructions:
1. Apply Rz(-φ).
2. Apply Ry(-θ).
3. Measure in the +Z direction.


Example decompositions are:

|Measurement axis |Decomposition |
|-----------------|-----------------------------------------|
|Z |I |
|X |Rz(\pi/2) \cdot X^{1/2} \cdot Rz(\pi/2)|
|Y |$X^{1/2} \cdot X^{1/2}$ |
|H |$X^{1/2} \cdot X^{1/2} \cdot Rz(\pi)$ |
8 changes: 4 additions & 4 deletions opensquirrel/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,9 @@ def decompose(self, decomposer: Decomposer) -> None:
decomposer (Decomposer): The decomposer to apply.

"""
from opensquirrel.passes.decomposer import general_decomposer
from opensquirrel.passes.decomposer.general_decomposer import decompose

general_decomposer.decompose(self.ir, decomposer)
decompose(self.ir, decomposer)

def export(self, exporter: Exporter) -> Any:
"""Exports the circuit using the specified exporter.
Expand Down Expand Up @@ -208,9 +208,9 @@ def replace(self, gate: type[Gate], replacement_gates_function: Callable[..., li
replacement_gates_function (Callable[..., list[Gate]]): function that describes the replacement gates.

"""
from opensquirrel.passes.decomposer import general_decomposer
from opensquirrel.passes.decomposer.gate_replacer import replace

general_decomposer.replace(self.ir, gate, replacement_gates_function)
replace(self.ir, gate, replacement_gates_function)

def validate(self, validator: Validator) -> None:
"""Validates the circuit using the specified validator.
Expand Down
8 changes: 5 additions & 3 deletions opensquirrel/passes/decomposer/aba_decomposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def Ra(self) -> Callable[..., SingleQubitGate]: ... # noqa: N802
@abstractmethod
def Rb(self) -> Callable[..., SingleQubitGate]: ... # noqa: N802

def decompose(self, gate: Gate) -> list[Gate]:
def decompose(self, instruction: 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$.

Expand All @@ -46,8 +46,10 @@ def decompose(self, gate: Gate) -> list[Gate]:
A sequence of (at most) three gates, following the R$a$-R$b$-R$a$ decomposition.

"""
if not isinstance(gate, SingleQubitGate):
return [gate]
if not isinstance(instruction, SingleQubitGate):
return [instruction]

gate = instruction

theta_a1, theta_b, theta_a2 = self._determine_rotation_angles(gate.bsr.axis, gate.bsr.angle)
return filter_out_identities(
Expand Down
14 changes: 6 additions & 8 deletions opensquirrel/passes/decomposer/cnot2cz_decomposer.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
from __future__ import annotations

from math import pi
from typing import TYPE_CHECKING

from opensquirrel import CZ, Ry
from opensquirrel.ir import Gate
from opensquirrel.passes.decomposer.general_decomposer import Decomposer

if TYPE_CHECKING:
from opensquirrel.ir import Gate


class CNOT2CZDecomposer(Decomposer):
def decompose(self, gate: Gate) -> list[Gate]:
def decompose(self, instruction: Gate) -> list[Gate]:
"""Predefined decomposition of CNOT gate into CZ gate with Ry rotations.

![image](../../../_static/cnot2cz.png#only-light)
Expand All @@ -21,15 +18,16 @@ def decompose(self, gate: Gate) -> list[Gate]:
This decomposition preserves the global phase of the CNOT gate.

Args:
gate (Gate): CNOT gate to decompose.
instruction (Instruction): 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]
if not isinstance(instruction, Gate) or instruction.name != "CNOT":
return [instruction]

gate = instruction
control_qubit, target_qubit = gate.qubit_operands
return [
Ry(target_qubit, -pi / 2),
Expand Down
10 changes: 7 additions & 3 deletions opensquirrel/passes/decomposer/cnot_decomposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class CNOTDecomposer(Decomposer):
[Quantum Gates by G.E. Crooks (2024), Section 7.5](https://threeplusone.com/pubs/on_gates.pdf).
"""

def decompose(self, gate: Gate) -> list[Gate]:
def decompose(self, instruction: Gate) -> list[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.

Expand All @@ -35,13 +35,17 @@ def decompose(self, gate: Gate) -> list[Gate]:
or the [SWAP2CZDecomposer][opensquirrel.passes.decomposer.swap2cz_decomposer.SWAP2CZDecomposer].

Args:
gate (Gate): Two-qubit controlled gate to decompose.
instruction (Instruction): Two-qubit controlled gate to decompose.

Returns:
A sequence of (at most 2) CNOT gates and single-qubit gates.

"""
if not isinstance(gate, TwoQubitGate) or not gate.controlled:
if not isinstance(instruction, TwoQubitGate):
return [instruction]
gate = instruction

if not gate.controlled:
return [gate]

control_qubit = gate.qubit0
Expand Down
10 changes: 6 additions & 4 deletions opensquirrel/passes/decomposer/cz_decomposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class CZDecomposer(Decomposer):
Source of the math: https://threeplusone.com/pubs/on_gates.pdf, chapter 7.5 "ABC decomposition"
"""

def decompose(self, gate: Gate) -> list[Gate]:
def decompose(self, instruction: 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.

Expand All @@ -37,14 +37,16 @@ def decompose(self, gate: Gate) -> list[Gate]:
or the [SWAP2CNOTDecomposer][opensquirrel.passes.decomposer.swap2cnot_decomposer.SWAP2CNOTDecomposer].

Args:
gate (Gate): Two-qubit controlled gate to decompose.
instruction (Instruction): 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]
if not isinstance(instruction, TwoQubitGate):
return [instruction]

gate = instruction

if not gate.controlled:
# Do nothing:
Expand Down
30 changes: 30 additions & 0 deletions opensquirrel/passes/decomposer/gate_replacer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from collections.abc import Callable

from opensquirrel.default_instructions import is_anonymous_gate
from opensquirrel.ir import IR, Gate
from opensquirrel.passes.decomposer.general_decomposer import Decomposer, decompose


class _GenericReplacer(Decomposer):
def __init__(self, gate_type: type[Gate], replacement_gates_function: Callable[..., list[Gate]]) -> None:
self.gate_type = gate_type
self.replacement_gates_function = replacement_gates_function

def decompose(self, instruction: Gate) -> list[Gate]:
if is_anonymous_gate(instruction.name) or type(instruction) is not self.gate_type:
return [instruction]
return self.replacement_gates_function(*instruction.qubit_operands, *instruction.arguments)


def replace(ir: IR, gate: type[Gate], replacement_gates_function: Callable[..., list[Gate]]) -> None:
"""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)
108 changes: 44 additions & 64 deletions opensquirrel/passes/decomposer/general_decomposer.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,22 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import Callable, Iterable
from typing import Any
from collections.abc import Iterable
from typing import Any, TypeVar

from opensquirrel.circuit_matrix_calculator import get_circuit_matrix
from opensquirrel.common import are_matrices_equivalent_up_to_global_phase, is_identity_matrix_up_to_a_global_phase
from opensquirrel.default_instructions import is_anonymous_gate
from opensquirrel.ir import IR, Gate
from opensquirrel.ir import IR, Gate, Instruction, Measure
from opensquirrel.reindexer import get_reindexed_circuit

InstructionType = TypeVar("InstructionType", bound=Instruction)


class Decomposer(ABC):
def __init__(self, **kwargs: Any) -> None: ...

@abstractmethod
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))

if is_identity_matrix_up_to_a_global_phase(replaced_matrix):
return

for replacement_gate in replacement_gates:
replacement_gates_qubit_indices.update(replacement_gate.qubit_indices)

if set(gate_qubit_indices) != replacement_gates_qubit_indices:
msg = f"replacement for gate {gate.name!r} does not operate on the correct qubits"
raise ValueError(msg)

replacement_matrix = get_circuit_matrix(get_reindexed_circuit(replacement_gates, gate_qubit_indices))

if not are_matrices_equivalent_up_to_global_phase(replaced_matrix, replacement_matrix):
msg = f"replacement for gate {gate.name!r} does not preserve the quantum state"
raise ValueError(msg)
def decompose(self, instruction: InstructionType) -> list[InstructionType]: ...


def decompose(ir: IR, decomposer: Decomposer) -> None:
Expand All @@ -64,38 +31,51 @@ def decompose(ir: IR, decomposer: Decomposer) -> None:
while statement_index < len(ir.statements):
statement = ir.statements[statement_index]

if not isinstance(statement, Gate):
if isinstance(statement, Gate):
gate = statement
replacement_gates: list[Gate] = decomposer.decompose(statement)
check_gate_decomposition(gate, replacement_gates)

ir.statements[statement_index : statement_index + 1] = replacement_gates
statement_index += len(replacement_gates)

elif isinstance(statement, Measure):
measure_decomposition = decomposer.decompose(statement)
ir.statements[statement_index : statement_index + 1] = measure_decomposition
statement_index += len(measure_decomposition)
else:
statement_index += 1
continue

gate = statement
replacement_gates: list[Gate] = decomposer.decompose(statement)
check_gate_replacement(gate, replacement_gates)

ir.statements[statement_index : statement_index + 1] = replacement_gates
statement_index += len(replacement_gates)
def check_gate_decomposition(gate: Gate, decomposition_gates: Iterable[Gate]) -> None:
"""Checks that the decomposition 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 decomposed.
decomposition_gates (Iterable[Gate]): Gate(s) that are decomposing the original gate.

Raises:
ValueError: If the decomposition gates do not operate on the same qubits as the original gate.
ValueError: If the decomposition gates do not preserve the quantum state up to a global phase.

class _GenericReplacer(Decomposer):
def __init__(self, gate_type: type[Gate], replacement_gates_function: Callable[..., list[Gate]]) -> None:
self.gate_type = gate_type
self.replacement_gates_function = replacement_gates_function
"""
gate_qubit_indices = gate.qubit_indices
decomposition_gates_qubit_indices = set()
decomposed_matrix = get_circuit_matrix(get_reindexed_circuit([gate], gate_qubit_indices))

def decompose(self, gate: Gate) -> list[Gate]:
if is_anonymous_gate(gate.name) or type(gate) is not self.gate_type:
return [gate]
return self.replacement_gates_function(*gate.qubit_operands, *gate.arguments)
if is_identity_matrix_up_to_a_global_phase(decomposed_matrix):
return

for decomposition_gate in decomposition_gates:
decomposition_gates_qubit_indices.update(decomposition_gate.qubit_indices)

def replace(ir: IR, gate: type[Gate], replacement_gates_function: Callable[..., list[Gate]]) -> None:
"""Replaces all occurrences of a specific gate in the circuit IR with a given sequence of other
gates.
if set(gate_qubit_indices) != decomposition_gates_qubit_indices:
msg = f"decomposition for gate {gate.name!r} does not operate on the correct qubits"
raise ValueError(msg)

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.
decomposition_matrix = get_circuit_matrix(get_reindexed_circuit(decomposition_gates, gate_qubit_indices))

"""
generic_replacer = _GenericReplacer(gate, replacement_gates_function)
decompose(ir, generic_replacer)
if not are_matrices_equivalent_up_to_global_phase(decomposed_matrix, decomposition_matrix):
msg = f"decomposition for gate {gate.name!r} does not preserve the quantum state"
raise ValueError(msg)
8 changes: 5 additions & 3 deletions opensquirrel/passes/decomposer/mckay_decomposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


class McKayDecomposer(Decomposer):
def decompose(self, gate: Gate) -> list[Gate]:
def decompose(self, instruction: Gate) -> list[Gate]:
"""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.
Expand All @@ -29,8 +29,10 @@ def decompose(self, gate: Gate) -> list[Gate]:
A sequence of (at most) 5 single-qubit gates that decompose the original gate.

"""
if not isinstance(gate, SingleQubitGate) or gate == X90(gate.qubit):
return [gate]
if not isinstance(instruction, SingleQubitGate) or instruction == X90(instruction.qubit):
return [instruction]

gate = instruction

if abs(gate.bsr.angle) < ATOL:
return [I(gate.qubit)]
Expand Down
Loading
Loading