Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
8 changes: 6 additions & 2 deletions opensquirrel/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

import numpy as np

from opensquirrel.ir import IR, AsmDeclaration, Gate
from opensquirrel.passes.exporter import ExportFormat
from opensquirrel.phasemap import PhaseMap

if TYPE_CHECKING:
from opensquirrel.passes.decomposer import Decomposer
Expand Down Expand Up @@ -43,6 +46,7 @@ def __init__(self, register_manager: RegisterManager, ir: IR) -> None:
"""Create a circuit object from a register manager and an IR."""
self.register_manager = register_manager
self.ir = ir
self.phase_map = PhaseMap(np.zeros(self.qubit_register_size, dtype=np.complex128))

def __repr__(self) -> str:
"""Write the circuit to a cQASM 3 string."""
Expand Down Expand Up @@ -102,7 +106,7 @@ def decompose(self, decomposer: Decomposer) -> None:
"""
from opensquirrel.passes.decomposer import general_decomposer

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

def export(self, fmt: ExportFormat | None = None) -> Any:
if fmt == ExportFormat.QUANTIFY_SCHEDULER:
Expand Down Expand Up @@ -139,7 +143,7 @@ def replace(self, gate: type[Gate], replacement_gates_function: Callable[..., li
"""
from opensquirrel.passes.decomposer import general_decomposer

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

def validate(self, validator: Validator) -> None:
"""Generic validator pass. It applies the given validator to the circuit."""
Expand Down
29 changes: 29 additions & 0 deletions opensquirrel/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def are_matrices_equivalent_up_to_global_phase(
Returns:
Whether two matrices are equivalent up to a global phase.
"""

first_non_zero = next(
(i, j) for i in range(matrix_a.shape[0]) for j in range(matrix_a.shape[1]) if abs(matrix_a[i, j]) > ATOL
)
Expand All @@ -51,6 +52,24 @@ def are_matrices_equivalent_up_to_global_phase(
return np.allclose(matrix_a, phase_difference * matrix_b, atol=ATOL)


def calculate_phase_difference(matrix_a: NDArray[np.complex128], matrix_b: NDArray[np.complex128]) -> np.complex128:
"""Calculates the phase difference between two matrices.
Args:
matrix_a: first matrix.
matrix_b: second matrix.
Returns:
The phase difference between the two matrices.
"""
first_non_zero = next(
(i, j) for i in range(matrix_a.shape[0]) for j in range(matrix_a.shape[1]) if abs(matrix_a[i, j]) > ATOL
)

if abs(matrix_b[first_non_zero]) < ATOL:
return np.complex128(1)

return np.complex128(matrix_a[first_non_zero] / matrix_b[first_non_zero])


def is_identity_matrix_up_to_a_global_phase(matrix: NDArray[np.complex128]) -> bool:
"""Checks whether matrix is an identity matrix up to a global phase.

Expand All @@ -60,3 +79,13 @@ def is_identity_matrix_up_to_a_global_phase(matrix: NDArray[np.complex128]) -> b
Whether matrix is an identity matrix up to a global phase.
"""
return are_matrices_equivalent_up_to_global_phase(matrix, np.eye(matrix.shape[0], dtype=np.complex128))


def get_phase_angle(scalar: np.complex128) -> np.complex128:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The phase angle will never be complex, right? It's just a float value.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it could also be a simply built-in float, instead of a numpy.float ... right?

"""Derives the Euler rotation angle from a scalar.
Args:
scalar: scalar to convert.
Returns:
Euler phase angle of scalar.
"""
return np.complex128(-1j * np.log(scalar))
6 changes: 6 additions & 0 deletions opensquirrel/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ def __init__(self, index: QubitLike) -> None:
msg = "index must be a QubitLike"
raise TypeError(msg)

def __eq__(self, other: Any) -> bool:
"""Compare two qubits."""
if not isinstance(other, Qubit):
return False
return self.__hash__() == other.__hash__()

def __hash__(self) -> int:
return hash(str(self.__class__) + str(self.index))

Expand Down
62 changes: 50 additions & 12 deletions opensquirrel/passes/decomposer/general_decomposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,25 @@

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

import numpy as np

from opensquirrel.circuit_matrix_calculator import get_circuit_matrix
from opensquirrel.common import are_matrices_equivalent_up_to_global_phase, is_identity_matrix_up_to_a_global_phase
from opensquirrel.common import (
ATOL,
are_matrices_equivalent_up_to_global_phase,
calculate_phase_difference,
get_phase_angle,
is_identity_matrix_up_to_a_global_phase,
)
from opensquirrel.default_instructions import is_anonymous_gate
from opensquirrel.ir import IR, Gate
from opensquirrel.ir import Float, Gate, Rz
from opensquirrel.reindexer import get_reindexed_circuit

if TYPE_CHECKING:
from opensquirrel.circuit import Circuit


class Decomposer(ABC):
def __init__(self, **kwargs: Any) -> None: ...
Expand All @@ -19,13 +30,23 @@ def decompose(self, gate: Gate) -> list[Gate]:
raise NotImplementedError()


def check_gate_replacement(gate: Gate, replacement_gates: Iterable[Gate]) -> None:
def check_gate_replacement(gate: Gate, replacement_gates: Iterable[Gate], circuit: Circuit | None = None) -> list[Gate]:
"""
Verifies the replacement gates against the given gate.
Args:
gate: original gate
replacement_gates: gates replacing the gate
circuit: circuit to verify
Returns:
Returns verified list of replacement gates with possible correction.
"""
gate_qubit_indices = [q.index for q in gate.get_qubit_operands()]
replacement_gates_qubit_indices = set()
replaced_matrix = get_circuit_matrix(get_reindexed_circuit([gate], gate_qubit_indices))
qubit_list = gate.get_qubit_operands()

if is_identity_matrix_up_to_a_global_phase(replaced_matrix):
return
return []

for g in replacement_gates:
replacement_gates_qubit_indices.update([q.index for q in g.get_qubit_operands()])
Expand All @@ -40,25 +61,42 @@ def check_gate_replacement(gate: Gate, replacement_gates: Iterable[Gate]) -> Non
msg = f"replacement for gate {gate.name} does not preserve the quantum state"
raise ValueError(msg)

if circuit is not None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How could circuit be None if one is performing a replacement on a circuit object?

Is this because the replacement functionality can also be used on a single gate? If so, I'm tempted to remove that option. Or at least not have the replacer be part of the circuit anymore, because that is confusing.

phase_difference = calculate_phase_difference(replaced_matrix, replacement_matrix)
euler_phase = get_phase_angle(phase_difference)
for q in gate.get_qubit_operands():
circuit.phase_map.add_qubit_phase(q, euler_phase)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that we are indeed storing the phase_angle in the PhaseMap, but then it does not need to be complex array (np.array([], dtype=np.complex128)); it can just be a list of floats (list[float]), which is a lot simpler (less error prone) and more efficient.


if len(gate_qubit_indices) > 1:
relative_phase = float(
np.real(
circuit.phase_map.get_qubit_phase(qubit_list[1]) - circuit.phase_map.get_qubit_phase(qubit_list[0])
)
)
if abs(relative_phase) > ATOL:
list(replacement_gates).append(Rz(gate.get_qubit_operands()[0], Float(-1 * relative_phase)))

return list(replacement_gates)


def decompose(ir: IR, decomposer: Decomposer) -> None:
def decompose(circuit: Circuit, decomposer: Decomposer) -> None:
"""Applies `decomposer` to every gate in the circuit, replacing each gate by the output of `decomposer`.
When `decomposer` decides to not decomposer a gate, it needs to return a list with the intact gate as single
element.
"""
statement_index = 0
while statement_index < len(ir.statements):
statement = ir.statements[statement_index]
while statement_index < len(circuit.ir.statements):
statement = circuit.ir.statements[statement_index]

if not isinstance(statement, Gate):
statement_index += 1
continue

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

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


Expand All @@ -73,8 +111,8 @@ def decompose(self, gate: Gate) -> list[Gate]:
return self.replacement_gates_function(*gate.arguments)


def replace(ir: IR, gate: type[Gate], replacement_gates_function: Callable[..., list[Gate]]) -> None:
def replace(circuit: Circuit, gate: type[Gate], replacement_gates_function: Callable[..., list[Gate]]) -> None:
"""Does the same as decomposer, but only applies to a given gate."""
generic_replacer = _GenericReplacer(gate, replacement_gates_function)

decompose(ir, generic_replacer)
decompose(circuit, generic_replacer)
24 changes: 24 additions & 0 deletions opensquirrel/phasemap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import numpy as np
from numpy.typing import NDArray

from opensquirrel.ir import QubitLike


class PhaseMap:
def __init__(self, phase_map: NDArray[np.complex128]) -> None:
"""Initialize a PhaseMap object."""
self.qubit_phase_map = phase_map

def __contains__(self, qubit: QubitLike) -> bool:
"""Checks if qubit is in the phase map."""
return qubit in self.qubit_phase_map

def add_qubit_phase(self, qubit: QubitLike, phase: np.complex128) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def add_qubit_phase(self, qubit: QubitLike, phase: np.complex128) -> None:
def set_qubit_phase(self, qubit: QubitLike, phase: np.complex128) -> None:

from opensquirrel.ir import Qubit

self.qubit_phase_map[Qubit(qubit).index] += phase

def get_qubit_phase(self, qubit: QubitLike) -> np.complex128:
from opensquirrel.ir import Qubit

return np.complex128(self.qubit_phase_map[Qubit(qubit).index])
63 changes: 61 additions & 2 deletions tests/integration/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,11 @@ def test_spin2plus_backend() -> None:
X90 q[0]
Rz(-1.5707964) q[0]
b[0] = measure q[0]
Rz(2.3561946) q[1]
Rz(0.78539824) q[1]
X90 q[1]
Rz(-3.1415927) q[1]
Rz(1.5707963) q[1]
X90 q[1]
Rz(1.5707963) q[1]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes are due to the change made in the in check_gate_replacement where in the conditional statement if is_identity_matrix_up_to_a_global_phase(replaced_matrix): now an empty list is returned, whereas before nothing was returned. So, after the first decomposition pass (SWAP2CNOTDecomposer) the Rz(tau) q[1] is removed from the circuit (because it is identity). This is unwanted behaviour, we should not want a 2-qubit decomposition pass to process single-qubit gates, even if they are identity.

Moreover, these differences are not due to the correction of a phase difference. If you remove the Rz(tau) q[1], the compiled circuit you end up with is the same with the 'phase map' functionality, as it is without.

b[2] = measure q[1]
Rz(1.5707963) q[1]
X90 q[1]
Expand Down Expand Up @@ -780,3 +782,60 @@ def test_rydberg_backend() -> None:
X q[8]
"""
)


def test_integration_global_phase() -> None:
circuit = Circuit.from_string(
"""
version 3.0
qubit[3] q
H q[0:2]
Ry(1.5789) q[0]
H q[0]
CNOT q[1], q[0]
Ry(3.09) q[0]
Ry(0.318) q[1]
Ry(0.18) q[2]
CNOT q[2], q[0]
""",
)

# Decompose 2-qubit gates to a decomposition where the 2-qubit interactions are captured by CNOT gates
circuit.decompose(decomposer=CNOTDecomposer())

# Replace CNOT gates with CZ gates
circuit.decompose(decomposer=CNOT2CZDecomposer())
# Merge single-qubit gates and decompose with McKay decomposition.
circuit.merge(merger=SingleQubitGatesMerger())
circuit.decompose(decomposer=McKayDecomposer())
assert (
str(circuit)
== """version 3.0

qubit[3] q

Rz(1.5707963) q[1]
X90 q[1]
Rz(1.5707963) q[1]
Rz(3.1415927) q[0]
X90 q[0]
Rz(0.0081036221) q[0]
X90 q[0]
CZ q[1], q[0]
X90 q[2]
Rz(1.3907963) q[2]
X90 q[2]
Rz(3.1415927) q[0]
X90 q[0]
Rz(0.051592654) q[0]
X90 q[0]
CZ q[2], q[0]
Rz(-1.5707963) q[0]
X90 q[0]
Rz(1.5707963) q[0]
Rz(3.1415927) q[1]
X90 q[1]
Rz(2.8235927) q[1]
X90 q[1]
"""
)
4 changes: 2 additions & 2 deletions tests/test_replacer.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def decompose(self, g: Gate) -> list[Gate]:
return [I(g.qubit), g, I(g.qubit)]
return [g]

decompose(circuit.ir, decomposer=TestDecomposer())
decompose(circuit, decomposer=TestDecomposer())

builder2 = CircuitBuilder(3)
builder2.I(0)
Expand All @@ -77,7 +77,7 @@ def test_replace(self) -> None:
builder1.H(0)
circuit = builder1.to_circuit()

replace(circuit.ir, H, lambda q: [Y90(q), X(q)])
replace(circuit, H, lambda q: [Y90(q), X(q)])

builder2 = CircuitBuilder(3)
builder2.Y90(0)
Expand Down