From f9255c608cfc980f4976e0790cb473df4a63b352 Mon Sep 17 00:00:00 2001 From: rochisha0 Date: Fri, 25 Jul 2025 13:37:50 +0900 Subject: [PATCH] shift decomposition to gate class --- src/qutip_qip/operations/decomposer.py | 49 ++++++++++++++++++++++++++ src/qutip_qip/operations/gateclass.py | 18 ++++++++++ tests/test_dec.py | 14 ++++++++ 3 files changed, 81 insertions(+) create mode 100644 src/qutip_qip/operations/decomposer.py create mode 100644 tests/test_dec.py diff --git a/src/qutip_qip/operations/decomposer.py b/src/qutip_qip/operations/decomposer.py new file mode 100644 index 00000000..eace80b6 --- /dev/null +++ b/src/qutip_qip/operations/decomposer.py @@ -0,0 +1,49 @@ +from qutip_qip.operations.gateclass import Gate + +__all__ = ["resolve_decomposition", "fallback_decomposition"] + +def resolve_decomposition(gate: Gate, basis_1q: list[str], basis_2q: list[str]) -> list[Gate]: + """ + Attempt to decompose a gate into the given basis. + """ + try: + return gate.decompose_to_basis(basis_1q, basis_2q) + except NotImplementedError: + return fallback_decomposition(gate, basis_1q, basis_2q) + + +# Optional graph-based fallback resolver +GATE_DECOMP_GRAPH = { + "MS": ["CNOT"], + "CNOT": ["ISWAP"], + # You can add more conversion steps here +} + + +def fallback_decomposition(gate: Gate, basis_1q: list[str], basis_2q: list[str]) -> list[Gate]: + from collections import deque + + visited = set() + queue = deque([(gate.name, [])]) + + while queue: + current_gate, path = queue.popleft() + if current_gate in basis_2q: + # Reconstruct intermediate decompositions + decomposed = gate + for intermediate in path: + decomposed = decomposed.decompose_to_basis(basis_1q, [intermediate]) + decomposed = sum( + (g.decompose_to_basis(basis_1q, basis_2q) for g in decomposed), + start=[] + ) + return decomposed + + for neighbor in GATE_DECOMP_GRAPH.get(current_gate, []): + if neighbor not in visited: + visited.add(neighbor) + queue.append((neighbor, path + [neighbor])) + + raise NotImplementedError( + f"No valid decomposition path from {gate.name} to basis {basis_2q}" + ) diff --git a/src/qutip_qip/operations/gateclass.py b/src/qutip_qip/operations/gateclass.py index 317f40da..ce4f82d7 100644 --- a/src/qutip_qip/operations/gateclass.py +++ b/src/qutip_qip/operations/gateclass.py @@ -413,6 +413,15 @@ def get_qobj(self, num_qubits=None, dims=None): dims=dims, targets=all_targets, ) + def decompose_to_basis(self, basis_1q: list[str], basis_2q: list[str]) -> list["Gate"]: + """ + Return a decomposition of this gate into the given basis. + This default method raises an error unless overridden in subclasses. + """ + raise NotImplementedError( + f"Decomposition not implemented for {self.name} to basis {basis_2q}" + ) + class SingleQubitGate(Gate): @@ -580,6 +589,14 @@ def __init__(self, targets, **kwargs): def get_compact_qobj(self): return snot() + + def decompose_to_basis(self, basis_1q, basis_2q): + if "RX" in basis_1q and "RY" in basis_1q: + return [ + RY(self.targets[0], arg_value=np.pi / 2, arg_label=r"\pi/2"), + RX(self.targets[0], arg_value=np.pi, arg_label=r"\pi") + ] + raise NotImplementedError("Cannot decompose Hadamard to given basis") SNOT = H @@ -606,6 +623,7 @@ def __init__(self, targets, **kwargs): def get_compact_qobj(self): return sqrtnot() + class S(SingleQubitGate): diff --git a/tests/test_dec.py b/tests/test_dec.py new file mode 100644 index 00000000..9ebdf5dd --- /dev/null +++ b/tests/test_dec.py @@ -0,0 +1,14 @@ +import numpy as np +from qutip_qip.operations import H, RX, RY +from qutip_qip.operations.decomposer import resolve_decomposition + + +def test_hadamard_decomposition(): + gate = H(0) + decomposed = resolve_decomposition(gate, basis_1q=["RX", "RY"], basis_2q=["CNOT"]) + + assert len(decomposed) == 2 + assert isinstance(decomposed[0], RY) + assert isinstance(decomposed[1], RX) + np.testing.assert_allclose(decomposed[0].arg_value, np.pi / 2) + np.testing.assert_allclose(decomposed[1].arg_value, np.pi)