From 455989531dcdbf962e2be0b85fdeb37f4af11359 Mon Sep 17 00:00:00 2001 From: Codrut Date: Sat, 12 Apr 2025 19:36:56 +0200 Subject: [PATCH 1/7] Implement case A.2 for quantum shannon decomposition. --- .../quantum_shannon_decomposition.py | 137 ++++++++++++++---- .../quantum_shannon_decomposition_test.py | 15 ++ .../two_qubit_to_cz_test.py | 18 +++ 3 files changed, 138 insertions(+), 32 deletions(-) diff --git a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py index bd2ea263307..1b2a2172dbf 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py @@ -19,6 +19,7 @@ Synthesis of Quantum Logic Circuits. Tech. rep. 2006, https://arxiv.org/abs/quant-ph/0406176 """ +from types import SimpleNamespace from typing import Callable, Iterable, List, TYPE_CHECKING import numpy as np @@ -28,11 +29,9 @@ from cirq.circuits.frozen_circuit import FrozenCircuit from cirq.linalg import decompositions, predicates from cirq.protocols import unitary_protocol -from cirq.transformers.analytical_decompositions.three_qubit_decomposition import ( - three_qubit_matrix_to_operations, -) from cirq.transformers.analytical_decompositions.two_qubit_to_cz import ( two_qubit_matrix_to_cz_operations, + two_qubit_matrix_to_diagonal_and_cz_operations, ) if TYPE_CHECKING: @@ -42,7 +41,7 @@ def quantum_shannon_decomposition( qubits: 'List[cirq.Qid]', u: np.ndarray, atol: float = 1e-8 ) -> Iterable['cirq.Operation']: - """Decomposes n-qubit unitary 1-q, 2-q and GlobalPhase gates, preserving global phase. + """Decomposes n-qubit unitary into 1-q, 2-q and GlobalPhase gates, preserving global phase. The gates used are CX/YPow/ZPow/CNOT/GlobalPhase/CZ/PhasedXZGate/PhasedXPowGate. @@ -65,9 +64,7 @@ def quantum_shannon_decomposition( 1. _single_qubit_decomposition OR (Recursive Case) - 1. _msb_demuxer - 2. _multiplexed_cossin - 3. _msb_demuxer + 1. _recursive_decomposition Yields: A single 2-qubit or 1-qubit operations from OP TREE @@ -96,30 +93,94 @@ def quantum_shannon_decomposition( yield from _single_qubit_decomposition(qubits[0], u) return - if n == 4: - operations = tuple( - two_qubit_matrix_to_cz_operations( - qubits[0], qubits[1], u, allow_partial_czs=True, clean_operations=True, atol=atol - ) + # Collect all operations from the recursive decomposition + shannon_decomp = [op for op in _recursive_decomposition(qubits, u)] + # Separate all 2-qubit generic gates while keeping track of location + two_qubit_mat = [ + SimpleNamespace(location=loc, matrix=unitary_protocol.unitary(o)) + for loc, o in enumerate(shannon_decomp) + if isinstance(o.gate, ops.MatrixGate) + ] + # Apply case A.2 from Shende et al. + q0 = qubits[-2] + q1 = qubits[-1] + for idx in range(len(two_qubit_mat) - 1, 0, -1): + diagonal, operations = two_qubit_matrix_to_diagonal_and_cz_operations( + q0, + q1, + two_qubit_mat[idx].matrix, + allow_partial_czs=True, + clean_operations=True, + atol=atol, + ) + global_phase = _global_phase_difference( + two_qubit_mat[idx].matrix, [ops.MatrixGate(diagonal)(q0, q1), *operations] ) - yield from operations - i, j = np.unravel_index(np.argmax(np.abs(u)), u.shape) - new_unitary = unitary_protocol.unitary(FrozenCircuit.from_moments(*operations)) - global_phase = np.angle(u[i, j]) - np.angle(new_unitary[i, j]) if np.abs(global_phase) > 1e-9: - yield ops.global_phase_operation(np.exp(1j * global_phase)) - return - - if n == 8: - operations = tuple( - three_qubit_matrix_to_operations(qubits[0], qubits[1], qubits[2], u, atol=atol) + operations.append(ops.global_phase_operation(np.exp(1j * global_phase))) + # Replace the generic gate with ops from OP TREE + shannon_decomp[two_qubit_mat[idx].location] = operations + # Join the diagonal with the unitary to be decomposed in the next step + two_qubit_mat[idx - 1].matrix = diagonal @ two_qubit_mat[idx - 1].matrix + if len(two_qubit_mat) > 0: + operations = two_qubit_matrix_to_cz_operations( + q0, + q1, + two_qubit_mat[0].matrix, + allow_partial_czs=True, + clean_operations=True, + atol=atol, ) - yield from operations - i, j = np.unravel_index(np.argmax(np.abs(u)), u.shape) - new_unitary = unitary_protocol.unitary(FrozenCircuit.from_moments(*operations)) - global_phase = np.angle(u[i, j]) - np.angle(new_unitary[i, j]) + global_phase = _global_phase_difference(two_qubit_mat[0].matrix, operations) if np.abs(global_phase) > 1e-9: - yield ops.global_phase_operation(np.exp(1j * global_phase)) + operations.append(ops.global_phase_operation(np.exp(1j * global_phase))) + shannon_decomp[two_qubit_mat[0].location] = operations + # Yield the final operations in order + for op in shannon_decomp: + if isinstance(op, List): + yield from op + else: + yield op + + +def _recursive_decomposition(qubits: 'List[cirq.Qid]', u: np.ndarray) -> Iterable['cirq.Operation']: + """Recursive step in the quantum shannon decomposition. + + Decomposes n-qubit unitary into generic 2-qubit gates, CNOT and 1-qubit gates. + All generic 2-qubit gates are applied to the two least significant qubits and + are not decomposed further here. + + Args: + qubits: List of qubits in order of significance + u: Numpy array for unitary matrix representing gate to be decomposed + + Calls: + 1. _msb_demuxer + 2. _multiplexed_cossin + 3. _msb_demuxer + + Yields: + Generic 2-qubit gates or operations from {ry,rz,CNOT}. + + Raises: + ValueError: If the u matrix is not of shape (2^n,2^n) + ValueError: If the u matrix is not of size at least 4 + """ + n = u.shape[0] + if n & (n - 1): + raise ValueError( + f"Expected input matrix u to be a (2^n x 2^n) shaped numpy array, \ + but instead got shape {u.shape}" + ) + + if n <= 2: + raise ValueError( + f"Expected input matrix u for recursive step to have size at least 4, \ + but it has size {n}" + ) + + if n == 4: + yield ops.MatrixGate(u).on(*qubits) return # Perform a cosine-sine (linalg) decomposition on u @@ -139,6 +200,15 @@ def quantum_shannon_decomposition( yield from _msb_demuxer(qubits, u1, u2) +def _global_phase_difference(u: np.ndarray, ops: List['cirq.Operation']) -> float: + """Returns the difference in global phase between unitary u and + a list of operations computing u. + """ + i, j = np.unravel_index(np.argmax(np.abs(u)), u.shape) + new_unitary = unitary_protocol.unitary(FrozenCircuit.from_moments(*ops)) + return np.angle(u[i, j]) - np.angle(new_unitary[i, j]) + + def _single_qubit_decomposition(qubit: 'cirq.Qid', u: np.ndarray) -> Iterable['cirq.Operation']: """Decomposes single-qubit gate, and returns list of operations, keeping phase invariant. @@ -200,11 +270,14 @@ def _msb_demuxer( u2: Lower-right quadrant of total unitary to be decomposed (see diagram) Calls: - 1. quantum_shannon_decomposition + 1. _recursive_decomposition 2. _multiplexed_cossin - 3. quantum_shannon_decomposition + 3. _recursive_decomposition - Yields: Single operation from OP TREE of 2-qubit and 1-qubit operations + Yields: + Generic 2-qubit gates on the two least significant qubits, + CNOT gates with the target not on the two least significant qubits, + ry or rz """ # Perform a diagonalization to find values u1 = u1.astype(np.complex128) @@ -229,7 +302,7 @@ def _msb_demuxer( # Last term is given by ( I ⊗ W ), demultiplexed # Remove most-significant (demuxed) control-qubit # Yield operations for QSD on W - yield from quantum_shannon_decomposition(demux_qubits[1:], W, atol=1e-6) + yield from _recursive_decomposition(demux_qubits[1:], W) # Use complex phase of d_i to give theta_i (so d_i* gives -theta_i) # Observe that middle part looks like Σ_i( Rz(theta_i)⊗|i> int: diff --git a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition_test.py b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition_test.py index 13ec9a9e64c..b1004b386b2 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition_test.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition_test.py @@ -18,6 +18,7 @@ import cirq from cirq.ops import common_gates +from cirq.testing import random_two_qubit_circuit_with_czs from cirq.transformers.analytical_decompositions.quantum_shannon_decomposition import ( _msb_demuxer, _multiplexed_cossin, @@ -201,3 +202,17 @@ def test_qft5(): ) new_unitary = cirq.unitary(shannon_circuit) np.testing.assert_allclose(new_unitary, desired_unitary, atol=1e-6) + + +def test_random_circuit_decomposition(): + qubits = cirq.LineQubit.range(3) + test_circuit = ( + random_two_qubit_circuit_with_czs(3, qubits[0], qubits[1]) + + random_two_qubit_circuit_with_czs(3, qubits[1], qubits[2]) + + random_two_qubit_circuit_with_czs(3, qubits[0], qubits[2]) + ) + circuit = cirq.Circuit(quantum_shannon_decomposition(qubits, test_circuit.unitary())) + # Test return is equal to initial unitary + assert cirq.approx_eq(test_circuit.unitary(), circuit.unitary(), atol=1e-9) + # Test all operations have at most 2 qubits. + assert all(cirq.num_qubits(op) <= 2 for op in circuit.all_operations()) diff --git a/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz_test.py b/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz_test.py index 86578b46016..63ed0132861 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz_test.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz_test.py @@ -259,6 +259,24 @@ def test_decompose_to_diagonal_and_circuit(v): cirq.testing.assert_allclose_up_to_global_phase(circuit_unitary, v, atol=2e-6) +@pytest.mark.parametrize( + "mat, num_czs", + [ + (cirq.unitary(random_two_qubit_circuit_with_czs(3)), 2), + (cirq.unitary(random_two_qubit_circuit_with_czs(2)), 2), + (cirq.unitary(random_two_qubit_circuit_with_czs(1)), 1), + (cirq.unitary(random_two_qubit_circuit_with_czs(0)), 0), + ], +) +def test_decompose_to_diagonal_and_circuit_returns_circuit_with_expected_number_of_czs( + mat, num_czs +): + b, c = cirq.LineQubit.range(2) + _, ops = two_qubit_matrix_to_diagonal_and_cz_operations(b, c, mat, atol=1e-8) + circuit = cirq.Circuit(ops) + assert len(list(circuit.findall_operations_with_gate_type(cirq.CZPowGate))) == num_czs + + def test_remove_partial_czs_or_fail(): CZ = cirq.CZ(*cirq.LineQubit.range(2)) assert ( From 8787e6e720196862691958826aa75d4ea55bc898 Mon Sep 17 00:00:00 2001 From: Codrut Date: Fri, 16 May 2025 21:41:35 +0200 Subject: [PATCH 2/7] Reduce runtime of cleanup_operations in two_qubit_to_cz. --- .../quantum_shannon_decomposition.py | 2 +- .../two_qubit_to_cz.py | 53 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py index 1b2a2172dbf..2567488b68a 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py @@ -94,7 +94,7 @@ def quantum_shannon_decomposition( return # Collect all operations from the recursive decomposition - shannon_decomp = [op for op in _recursive_decomposition(qubits, u)] + shannon_decomp = [*_recursive_decomposition(qubits, u)] # Separate all 2-qubit generic gates while keeping track of location two_qubit_mat = [ SimpleNamespace(location=loc, matrix=unitary_protocol.unitary(o)) diff --git a/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py b/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py index 5f906c31e3f..1f98826dffc 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py @@ -24,7 +24,6 @@ from cirq.transformers.analytical_decompositions import single_qubit_decompositions from cirq.transformers.eject_phased_paulis import eject_phased_paulis from cirq.transformers.eject_z import eject_z -from cirq.transformers.merge_single_qubit_gates import merge_single_qubit_gates_to_phased_x_and_z if TYPE_CHECKING: import cirq @@ -183,14 +182,64 @@ def _xx_yy_zz_interaction_via_full_czs( def cleanup_operations(operations: Sequence[ops.Operation]): + operations = _merge_single_qubit_gates(operations) circuit = circuits.Circuit(operations) - circuit = merge_single_qubit_gates_to_phased_x_and_z(circuit) circuit = eject_phased_paulis(circuit) circuit = eject_z(circuit) circuit = circuits.Circuit(circuit.all_operations(), strategy=circuits.InsertStrategy.EARLIEST) return list(circuit.all_operations()) +def _transform_single_qubit_operations_to_phased_x_and_z( + operations: Sequence[ops.Operation], +) -> Sequence[ops.Operation]: + """Transforms operations on the same qubit to a PhasedXPowGate followed by a Z gate. + + Args: + operations: sequence of operations on the same qubit + Returns: + A PhasedXPowGate followed by a Z gate. If one the gates is not needed, it will be omitted. + """ + u = np.eye(2) + for op in operations: + u = protocols.unitary(op) @ u + return [ + g(op.qubits[0]) + for g in single_qubit_decompositions.single_qubit_matrix_to_phased_x_z(u, atol=1e-8) + ] + + +def _merge_single_qubit_gates(operations: Sequence[ops.Operation]) -> Sequence[ops.Operation]: + """Merge consecutive single qubit gates. + + Traverses the sequence of operations maintaining a list of consecutive single qubit + operations for each qubit. When a 2-qubit gate is encountered, it transforms pending + operations to a PhasedXPowGate followed by a Z gate. + + Args: + operations: sequence of operations + Returns: + new sequence of operations after merging gates + """ + merged_ops: list[ops.Operation] = [] + pending_ops: dict[Tuple['cirq.Qid', ...], list[ops.Operation]] = dict() + for op in operations: + if protocols.num_qubits(op) == 2: + for _, qubit_ops in pending_ops.items(): + merged_ops.extend(_transform_single_qubit_operations_to_phased_x_and_z(qubit_ops)) + pending_ops.clear() + # Add the 2-qubit gate + merged_ops.append(op) + elif protocols.num_qubits(op) == 1: + if op.qubits not in pending_ops: + pending_ops[op.qubits] = [] + pending_ops[op.qubits].append(op) + # Merge remaining pending operations + for _, qubit_ops in pending_ops.items(): + merged_ops.extend(_transform_single_qubit_operations_to_phased_x_and_z(qubit_ops)) + return merged_ops + + def _kak_decomposition_to_operations( q0: 'cirq.Qid', q1: 'cirq.Qid', From d52e31b87e0bbc7a822fb5229457c2339fe5c374 Mon Sep 17 00:00:00 2001 From: Codrut Date: Thu, 29 May 2025 21:30:00 +0200 Subject: [PATCH 3/7] Implement optimization A.1 from Shende et al. --- .../quantum_shannon_decomposition.py | 28 +++++++++++++++---- .../quantum_shannon_decomposition_test.py | 19 ++++++++++++- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py index 2567488b68a..ef27ff5eb11 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py @@ -69,7 +69,7 @@ def quantum_shannon_decomposition( Yields: A single 2-qubit or 1-qubit operations from OP TREE composed from the set - { CNOT, rz, ry, ZPowGate } + { CNOT, CZ, rz, ry, ZPowGate } Raises: ValueError: If the u matrix is non-unitary @@ -146,7 +146,7 @@ def quantum_shannon_decomposition( def _recursive_decomposition(qubits: 'List[cirq.Qid]', u: np.ndarray) -> Iterable['cirq.Operation']: """Recursive step in the quantum shannon decomposition. - Decomposes n-qubit unitary into generic 2-qubit gates, CNOT and 1-qubit gates. + Decomposes n-qubit unitary into generic 2-qubit gates, CNOT, CZ and 1-qubit gates. All generic 2-qubit gates are applied to the two least significant qubits and are not decomposed further here. @@ -160,7 +160,7 @@ def _recursive_decomposition(qubits: 'List[cirq.Qid]', u: np.ndarray) -> Iterabl 3. _msb_demuxer Yields: - Generic 2-qubit gates or operations from {ry,rz,CNOT}. + Generic 2-qubit gates or operations from {ry,rz,CNOT,CZ}. Raises: ValueError: If the u matrix is not of shape (2^n,2^n) @@ -196,6 +196,17 @@ def _recursive_decomposition(qubits: 'List[cirq.Qid]', u: np.ndarray) -> Iterabl # Yield ops from multiplexed Ry part yield from _multiplexed_cossin(qubits, theta, ops.ry) + # Optimization A.1 in Shende et al. - the last CZ gate in the multiplexed Ry part + # is merged into the generic multiplexor (u1, u2) + # This gate is CZ(qubits[1], qubits[0]) = CZ(qubits[0], qubits[1]) + # as CZ is symmetric. + # For the u1⊕u2 multiplexor operator: + # as u1 is the operator in case qubits[0] = |0>, + # and u2 is the operator in case qubits[0] = |1> + # we can represent the merge by phasing u2 with Z ⊗ I + cz_diag = np.concatenate((np.ones(n >> 2), np.full(n >> 2, -1))) + u2 = u2 @ np.diag(cz_diag) + # Yield ops from decomposition of multiplexed u1/u2 part yield from _msb_demuxer(qubits, u1, u2) @@ -334,7 +345,7 @@ def _multiplexed_cossin( Calls: No major calls - Yields: Single operation from OP TREE from set 1- and 2-qubit gates: {ry,rz,CNOT} + Yields: Single operation from OP TREE from set 1- and 2-qubit gates: {ry,rz,CNOT,CZ} """ # Most significant qubit is main qubit with rotation function applied main_qubit = cossin_qubits[0] @@ -375,4 +386,11 @@ def _multiplexed_cossin( yield rot_func(rotation).on(main_qubit) # Add a CNOT from the select qubit to the main qubit - yield ops.CNOT(control_qubits[select_qubit], main_qubit) + # Optimization A.1 in Shende et al. - use CZ instead of CNOT for ry rotations + if rot_func == ops.ry: + # Don't emit the last gate, as it will be merged into the generic multiplexor + # in the cosine-sine decomposition + if j < len(angles) - 1: + yield ops.CZ(control_qubits[select_qubit], main_qubit) + else: + yield ops.CNOT(control_qubits[select_qubit], main_qubit) diff --git a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition_test.py b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition_test.py index b1004b386b2..279838b2b76 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition_test.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition_test.py @@ -23,6 +23,7 @@ _msb_demuxer, _multiplexed_cossin, _nth_gray, + _recursive_decomposition, _single_qubit_decomposition, quantum_shannon_decomposition, ) @@ -48,6 +49,14 @@ def test_qsd_n_qubit_errors(): cirq.Circuit(quantum_shannon_decomposition(qubits, np.ones((8, 8)))) +def test_recursive_decomposition_n_qubit_errors(): + qubits = [cirq.NamedQubit(f'q{i}') for i in range(3)] + with pytest.raises(ValueError, match="shaped numpy array"): + cirq.Circuit(_recursive_decomposition(qubits, np.eye(9))) + with pytest.raises(ValueError, match="size at least 4"): + cirq.Circuit(_recursive_decomposition(qubits, np.eye(2))) + + def test_random_single_qubit_decomposition(): U = unitary_group.rvs(2) qubit = cirq.NamedQubit('q0') @@ -79,10 +88,18 @@ def test_multiplexed_cossin(): multiplexed_ry = np.array(multiplexed_ry) qubits = [cirq.NamedQubit(f'q{i}') for i in range(2)] circuit = cirq.Circuit(_multiplexed_cossin(qubits, [angle_1, angle_2])) + # Add back the CZ gate removed by the A.1 optimization + circuit += cirq.CZ(qubits[1], qubits[0]) # Test return is equal to inital unitary assert cirq.approx_eq(multiplexed_ry, circuit.unitary(), atol=1e-9) # Test all operations in gate set - gates = (common_gates.Rz, common_gates.Ry, common_gates.ZPowGate, common_gates.CXPowGate) + gates = ( + common_gates.Rz, + common_gates.Ry, + common_gates.ZPowGate, + common_gates.CXPowGate, + common_gates.CZPowGate, + ) assert all(isinstance(op.gate, gates) for op in circuit.all_operations()) From c6e0b60aabf4b509f2b5621d1f428fe8f8d5a34e Mon Sep 17 00:00:00 2001 From: Codrut Date: Thu, 29 May 2025 23:42:13 +0200 Subject: [PATCH 4/7] Fix types. --- .../analytical_decompositions/quantum_shannon_decomposition.py | 2 +- .../transformers/analytical_decompositions/two_qubit_to_cz.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py index 2460dba13d3..3cc0bad6ba4 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py @@ -139,7 +139,7 @@ def quantum_shannon_decomposition( shannon_decomp[two_qubit_mat[0].location] = operations # Yield the final operations in order for op in shannon_decomp: - if isinstance(op, List): + if isinstance(op, list): yield from op else: yield op diff --git a/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py b/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py index 86c07fa3d97..f11c53829c5 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py @@ -222,7 +222,7 @@ def _merge_single_qubit_gates(operations: Sequence[ops.Operation]) -> Sequence[o new sequence of operations after merging gates """ merged_ops: list[ops.Operation] = [] - pending_ops: dict[Tuple['cirq.Qid', ...], list[ops.Operation]] = dict() + pending_ops: dict[tuple['cirq.Qid', ...], list[ops.Operation]] = dict() for op in operations: if protocols.num_qubits(op) == 2: for _, qubit_ops in pending_ops.items(): From b31d996a276706d6da22b0879a1c4cf0bb5074ec Mon Sep 17 00:00:00 2001 From: Codrut Date: Fri, 30 May 2025 00:12:29 +0200 Subject: [PATCH 5/7] Fix Lint error. --- .../transformers/analytical_decompositions/two_qubit_to_cz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py b/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py index f11c53829c5..924aeb43296 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py @@ -222,7 +222,7 @@ def _merge_single_qubit_gates(operations: Sequence[ops.Operation]) -> Sequence[o new sequence of operations after merging gates """ merged_ops: list[ops.Operation] = [] - pending_ops: dict[tuple['cirq.Qid', ...], list[ops.Operation]] = dict() + pending_ops: dict[tuple[cirq.Qid, ...], list[ops.Operation]] = dict() for op in operations: if protocols.num_qubits(op) == 2: for _, qubit_ops in pending_ops.items(): From 645543ae24275fc2ed166620c9cc21881ddce5f2 Mon Sep 17 00:00:00 2001 From: Codrut Date: Thu, 19 Jun 2025 22:50:09 +0200 Subject: [PATCH 6/7] Change according to feedback in comments. --- .../quantum_shannon_decomposition.py | 48 ++++++++++--------- .../two_qubit_to_cz.py | 28 +++++++---- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py index 3cc0bad6ba4..dcee7dca779 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py @@ -21,10 +21,10 @@ from __future__ import annotations -from types import SimpleNamespace -from typing import Callable, Iterable, TYPE_CHECKING +from typing import Callable, cast, Iterable, TYPE_CHECKING import numpy as np +from attr import define from scipy.linalg import cossin from cirq import ops @@ -40,6 +40,12 @@ import cirq +@define +class _TwoQubitGate: + location: int + matrix: np.ndarray + + def quantum_shannon_decomposition( qubits: list[cirq.Qid], u: np.ndarray, atol: float = 1e-8 ) -> Iterable[cirq.Operation]: @@ -96,53 +102,51 @@ def quantum_shannon_decomposition( return # Collect all operations from the recursive decomposition - shannon_decomp = [*_recursive_decomposition(qubits, u)] + shannon_decomp: list[cirq.Operation | list[cirq.Operation]] = [ + *_recursive_decomposition(qubits, u) + ] # Separate all 2-qubit generic gates while keeping track of location - two_qubit_mat = [ - SimpleNamespace(location=loc, matrix=unitary_protocol.unitary(o)) - for loc, o in enumerate(shannon_decomp) + two_qubit_gates = [ + _TwoQubitGate(location=loc, matrix=unitary_protocol.unitary(o)) + for loc, o in enumerate(cast(list[cirq.Operation], shannon_decomp)) if isinstance(o.gate, ops.MatrixGate) ] # Apply case A.2 from Shende et al. q0 = qubits[-2] q1 = qubits[-1] - for idx in range(len(two_qubit_mat) - 1, 0, -1): + for idx in range(len(two_qubit_gates) - 1, 0, -1): diagonal, operations = two_qubit_matrix_to_diagonal_and_cz_operations( q0, q1, - two_qubit_mat[idx].matrix, + two_qubit_gates[idx].matrix, allow_partial_czs=True, clean_operations=True, atol=atol, ) global_phase = _global_phase_difference( - two_qubit_mat[idx].matrix, [ops.MatrixGate(diagonal)(q0, q1), *operations] + two_qubit_gates[idx].matrix, [ops.MatrixGate(diagonal)(q0, q1), *operations] ) - if np.abs(global_phase) > 1e-9: + if not np.isclose(global_phase, 0, atol=atol): operations.append(ops.global_phase_operation(np.exp(1j * global_phase))) # Replace the generic gate with ops from OP TREE - shannon_decomp[two_qubit_mat[idx].location] = operations + shannon_decomp[two_qubit_gates[idx].location] = operations # Join the diagonal with the unitary to be decomposed in the next step - two_qubit_mat[idx - 1].matrix = diagonal @ two_qubit_mat[idx - 1].matrix - if len(two_qubit_mat) > 0: + two_qubit_gates[idx - 1].matrix = diagonal @ two_qubit_gates[idx - 1].matrix + if len(two_qubit_gates) > 0: operations = two_qubit_matrix_to_cz_operations( q0, q1, - two_qubit_mat[0].matrix, + two_qubit_gates[0].matrix, allow_partial_czs=True, clean_operations=True, atol=atol, ) - global_phase = _global_phase_difference(two_qubit_mat[0].matrix, operations) - if np.abs(global_phase) > 1e-9: + global_phase = _global_phase_difference(two_qubit_gates[0].matrix, operations) + if not np.isclose(global_phase, 0, atol=atol): operations.append(ops.global_phase_operation(np.exp(1j * global_phase))) - shannon_decomp[two_qubit_mat[0].location] = operations + shannon_decomp[two_qubit_gates[0].location] = operations # Yield the final operations in order - for op in shannon_decomp: - if isinstance(op, list): - yield from op - else: - yield op + yield from cast(Iterable[cirq.Operation], ops.flatten_op_tree(shannon_decomp)) def _recursive_decomposition(qubits: list[cirq.Qid], u: np.ndarray) -> Iterable[cirq.Operation]: diff --git a/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py b/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py index 924aeb43296..9e55beef2a7 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py @@ -80,8 +80,8 @@ def two_qubit_matrix_to_cz_operations( if clean_operations: if not allow_partial_czs: # CZ^t is not allowed for any $t$ except $t=1$. - return _remove_partial_czs_or_fail(cleanup_operations(operations), atol=atol) - return cleanup_operations(operations) + return _remove_partial_czs_or_fail(cleanup_operations(operations, atol=atol), atol=atol) + return cleanup_operations(operations, atol=atol) return operations @@ -181,8 +181,8 @@ def _xx_yy_zz_interaction_via_full_czs(q0: cirq.Qid, q1: cirq.Qid, x: float, y: yield ops.H(q1) -def cleanup_operations(operations: Sequence[ops.Operation]): - operations = _merge_single_qubit_gates(operations) +def cleanup_operations(operations: Sequence[ops.Operation], atol: float = 1e-8): + operations = _merge_single_qubit_gates(operations, atol=atol) circuit = circuits.Circuit(operations) circuit = eject_phased_paulis(circuit) circuit = eject_z(circuit) @@ -191,12 +191,14 @@ def cleanup_operations(operations: Sequence[ops.Operation]): def _transform_single_qubit_operations_to_phased_x_and_z( - operations: Sequence[ops.Operation], + operations: Sequence[ops.Operation], atol: float ) -> Sequence[ops.Operation]: """Transforms operations on the same qubit to a PhasedXPowGate followed by a Z gate. Args: operations: sequence of operations on the same qubit + atol: a limit on the amount of absolute error introduced by the + transformation. Returns: A PhasedXPowGate followed by a Z gate. If one the gates is not needed, it will be omitted. """ @@ -205,11 +207,13 @@ def _transform_single_qubit_operations_to_phased_x_and_z( u = protocols.unitary(op) @ u return [ g(op.qubits[0]) - for g in single_qubit_decompositions.single_qubit_matrix_to_phased_x_z(u, atol=1e-8) + for g in single_qubit_decompositions.single_qubit_matrix_to_phased_x_z(u, atol=atol) ] -def _merge_single_qubit_gates(operations: Sequence[ops.Operation]) -> Sequence[ops.Operation]: +def _merge_single_qubit_gates( + operations: Sequence[ops.Operation], atol: float +) -> Sequence[ops.Operation]: """Merge consecutive single qubit gates. Traverses the sequence of operations maintaining a list of consecutive single qubit @@ -218,6 +222,8 @@ def _merge_single_qubit_gates(operations: Sequence[ops.Operation]) -> Sequence[o Args: operations: sequence of operations + atol: a limit on the amount of absolute error introduced by the + transformation. Returns: new sequence of operations after merging gates """ @@ -226,7 +232,9 @@ def _merge_single_qubit_gates(operations: Sequence[ops.Operation]) -> Sequence[o for op in operations: if protocols.num_qubits(op) == 2: for _, qubit_ops in pending_ops.items(): - merged_ops.extend(_transform_single_qubit_operations_to_phased_x_and_z(qubit_ops)) + merged_ops.extend( + _transform_single_qubit_operations_to_phased_x_and_z(qubit_ops, atol=atol) + ) pending_ops.clear() # Add the 2-qubit gate merged_ops.append(op) @@ -236,7 +244,9 @@ def _merge_single_qubit_gates(operations: Sequence[ops.Operation]) -> Sequence[o pending_ops[op.qubits].append(op) # Merge remaining pending operations for _, qubit_ops in pending_ops.items(): - merged_ops.extend(_transform_single_qubit_operations_to_phased_x_and_z(qubit_ops)) + merged_ops.extend( + _transform_single_qubit_operations_to_phased_x_and_z(qubit_ops, atol=atol) + ) return merged_ops From f44f3286abe02bb4d35c1948a18f6a0d6468b7ec Mon Sep 17 00:00:00 2001 From: Codrut Date: Thu, 19 Jun 2025 23:20:24 +0200 Subject: [PATCH 7/7] Fix type error. --- .../quantum_shannon_decomposition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py index dcee7dca779..bed6c80da4a 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/quantum_shannon_decomposition.py @@ -108,7 +108,7 @@ def quantum_shannon_decomposition( # Separate all 2-qubit generic gates while keeping track of location two_qubit_gates = [ _TwoQubitGate(location=loc, matrix=unitary_protocol.unitary(o)) - for loc, o in enumerate(cast(list[cirq.Operation], shannon_decomp)) + for loc, o in enumerate(cast(list[ops.Operation], shannon_decomp)) if isinstance(o.gate, ops.MatrixGate) ] # Apply case A.2 from Shende et al. @@ -146,7 +146,7 @@ def quantum_shannon_decomposition( operations.append(ops.global_phase_operation(np.exp(1j * global_phase))) shannon_decomp[two_qubit_gates[0].location] = operations # Yield the final operations in order - yield from cast(Iterable[cirq.Operation], ops.flatten_op_tree(shannon_decomp)) + yield from cast(Iterable[ops.Operation], ops.flatten_op_tree(shannon_decomp)) def _recursive_decomposition(qubits: list[cirq.Qid], u: np.ndarray) -> Iterable[cirq.Operation]: