From 4c10f6da25062d92602d87a4b67378391744ff4b Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 17 Jan 2025 15:21:33 -0500 Subject: [PATCH 01/15] improve set generation --- .../op_math/decompositions/solovay_kitaev.py | 48 +++++++++++++++---- tests/ops/op_math/test_solovay_kitaev.py | 2 +- tests/transforms/test_cliffordt_transform.py | 4 +- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/pennylane/ops/op_math/decompositions/solovay_kitaev.py b/pennylane/ops/op_math/decompositions/solovay_kitaev.py index 4f55c09e717..9b8d5f14a9e 100644 --- a/pennylane/ops/op_math/decompositions/solovay_kitaev.py +++ b/pennylane/ops/op_math/decompositions/solovay_kitaev.py @@ -72,16 +72,16 @@ def _contains_SU2(op_mat, ops_vecs, tol=1e-8): tol (float): Tolerance for the match to be considered ``True``. Returns: - Tuple(bool, TensorLike): A bool that shows whether an operation similar to the given operations - was found, and the quaternion representation of the searched operation. + Tuple(bool, TensorLike, int): A bool that shows whether an operation similar to the given operations + was found, the quaternion representation of the searched operation and their index in the list. """ node_points = qml.math.array(ops_vecs) gate_points = qml.math.array([_quaternion_transform(op_mat)]) tree = KDTree(node_points) - dist = tree.query(gate_points, workers=-1)[0][0] + dist, indx = tree.query(gate_points, workers=-1) - return (dist < tol, gate_points[0]) + return (dist[0] < tol, gate_points[0], indx[0]) @lru_cache() @@ -113,6 +113,7 @@ def _approximate_set(basis_gates, max_length=10): } # Maintains the basis gates basis = [_CLIFFORD_T_BASIS[gate.upper()] for gate in basis_gates] + t_set = {qml.T(0), qml.adjoint(qml.T(0))} # Get the SU(2) data for the gates in basis set basis_mat, basis_gph = {}, {} @@ -125,18 +126,22 @@ def _approximate_set(basis_gates, max_length=10): gtrie_ids = [[[gate] for gate in basis]] gtrie_mat = [list(basis_mat.values())] gtrie_gph = [list(basis_gph.values())] + gtrie_sum = [[int(gate in t_set) for gate in basis]] # Maintains the approximate set for gates' names, SU(2)s, global phases and quaternions approx_set_ids = list(gtrie_ids[0]) approx_set_mat = list(gtrie_mat[0]) approx_set_gph = list(gtrie_gph[0]) + approx_set_sum = list(gtrie_sum[0]) approx_set_qat = [_quaternion_transform(mat) for mat in approx_set_mat] # We will perform a breadth-first search (BFS) style set building for the set for depth in range(max_length - 1): # Add the containers for next depth while we explore the current - gtrie_id, gtrie_mt, gtrie_gp = [], [], [] - for node, su2m, gphase in zip(gtrie_ids[depth], gtrie_mat[depth], gtrie_gph[depth]): + gtrie_id, gtrie_mt, gtrie_gp, gtrie_sm = [], [], [], [] + for node, su2m, gphase, tgsum in zip( + gtrie_ids[depth], gtrie_mat[depth], gtrie_gph[depth], gtrie_sum[depth] + ): # Get the last operation in the current node last_op = qml.adjoint(node[-1], lazy=False) if node else None @@ -149,8 +154,10 @@ def _approximate_set(basis_gates, max_length=10): # Extend and check if the node already exists in the approximate set. su2_gp = basis_gph[op] + gphase su2_op = (-1.0) ** bool(su2_gp >= math.pi) * (basis_mat[op] @ su2m) - exists, quaternion = _contains_SU2(su2_op, approx_set_qat) - if not exists: + exists, quaternion, index = _contains_SU2(su2_op, approx_set_qat) + + global_phase = qml.math.mod(su2_gp, math.pi) + if not exists or global_phase != approx_set_gph[index]: approx_set_ids.append(node + [op]) approx_set_mat.append(su2_op) approx_set_qat.append(quaternion) @@ -160,14 +167,35 @@ def _approximate_set(basis_gates, max_length=10): gtrie_mt.append(su2_op) # Add the global phase data - global_phase = qml.math.mod(su2_gp, math.pi) approx_set_gph.append(global_phase) gtrie_gp.append(global_phase) + # Add the T gate sum data + tbool = int(op in t_set) + approx_set_sum.append(tgsum + tbool) + gtrie_sm.append(tgsum + tbool) + # Add to the next depth for next iteration gtrie_ids.append(gtrie_id) gtrie_mat.append(gtrie_mt) gtrie_gph.append(gtrie_gp) + gtrie_sum.append(gtrie_sm) + + # Prune the approximate set for equivalent operations with higher T-gate counts + if approx_set_qat: + tree, tsum = KDTree(approx_set_qat), qml.math.array(approx_set_sum) + dists, indxs = tree.query(approx_set_qat, workers=-1, k=10) + + prune_ixs = [] + for dist, indx in zip(dists, indxs): + eq_idx = qml.math.sort(indx[qml.math.where(dist.round(8) == 0.0)]) + prune_ixs.extend(eq_idx[qml.math.argsort(tsum[eq_idx])][1:]) + + for ix in sorted(set(prune_ixs), reverse=True): + del approx_set_ids[ix] + del approx_set_mat[ix] + del approx_set_gph[ix] + del approx_set_qat[ix] return approx_set_ids, approx_set_mat, approx_set_gph, approx_set_qat @@ -340,7 +368,7 @@ def _solovay_kitaev(umat, n, u_n1_ids, u_n1_mat): [map_tape], _ = qml.map_wires(new_tape, wire_map={0: op.wires[0]}, queue=True) # Get phase information based on the decomposition effort - phase = approx_set_gph[index] - gate_gph if depth or qml.math.allclose(gate_gph, 0.0) else 0.0 + phase = approx_set_gph[index] - gate_gph global_phase = qml.GlobalPhase(qml.math.array(phase, like=interface)) # Return the gates from the mapped tape and global phase diff --git a/tests/ops/op_math/test_solovay_kitaev.py b/tests/ops/op_math/test_solovay_kitaev.py index 8e0952b61ea..40c263bd1db 100644 --- a/tests/ops/op_math/test_solovay_kitaev.py +++ b/tests/ops/op_math/test_solovay_kitaev.py @@ -85,7 +85,7 @@ def test_contains_SU2(): approx_ids, _, _, approx_vec = _approximate_set(("T", "T*", "H"), max_length=3) - exists, quaternion = _contains_SU2(target, approx_vec) + exists, quaternion, _ = _contains_SU2(target, approx_vec) assert exists result = [qml.adjoint(qml.T(0)), qml.adjoint(qml.T(0)), qml.adjoint(qml.T(0))] diff --git a/tests/transforms/test_cliffordt_transform.py b/tests/transforms/test_cliffordt_transform.py index 759f44684f2..0e889eb45d8 100644 --- a/tests/transforms/test_cliffordt_transform.py +++ b/tests/transforms/test_cliffordt_transform.py @@ -171,7 +171,7 @@ def qfunc(): for op in tape.operations ) - @pytest.mark.parametrize("epsilon", [2e-2, 5e-2, 9e-2]) + @pytest.mark.parametrize("epsilon", [2e-2, 5e-2, 7e-2]) @pytest.mark.parametrize("circuit", [circuit_3, circuit_4, circuit_5]) def test_total_error(self, epsilon, circuit): """Ensure that given a certain epsilon, the total operator error is below the threshold.""" @@ -411,7 +411,7 @@ def test_zero_global_phase(self): [tape], _ = qml.clifford_t_decomposition(tape) - assert not sum([isinstance(op, qml.GlobalPhase) for op in tape.operations]) + assert not sum(isinstance(op, qml.GlobalPhase) for op in tape.operations) def test_raise_with_decomposition_method(self): """Test that exception is correctly raise when using incorrect decomposing method""" From c5e5e420e9eb269fe9438ec82d040750349bb8f2 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 17 Jan 2025 15:52:11 -0500 Subject: [PATCH 02/15] add `changelog` --- doc/releases/changelog-dev.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index e2b5cdb4597..44891255bd5 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -44,6 +44,9 @@ * An informative error is raised when a `QNode` with `diff_method=None` is differentiated. [(#6770)](https://github.com/PennyLaneAI/pennylane/pull/6770) +* `qml.ops.sk_decomposition` now has an improved approximate set generation. + [(#6855)](https://github.com/PennyLaneAI/pennylane/pull/6855) +

Breaking changes 💔

* `qml.execute` now has a collection of keyword-only arguments. From a05eccac9968b17181c16cc126f1bdfbc9313de4 Mon Sep 17 00:00:00 2001 From: Utkarsh Date: Fri, 17 Jan 2025 15:53:52 -0500 Subject: [PATCH 03/15] Update pennylane/ops/op_math/decompositions/solovay_kitaev.py --- pennylane/ops/op_math/decompositions/solovay_kitaev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/ops/op_math/decompositions/solovay_kitaev.py b/pennylane/ops/op_math/decompositions/solovay_kitaev.py index 9b8d5f14a9e..f2555633fce 100644 --- a/pennylane/ops/op_math/decompositions/solovay_kitaev.py +++ b/pennylane/ops/op_math/decompositions/solovay_kitaev.py @@ -73,7 +73,7 @@ def _contains_SU2(op_mat, ops_vecs, tol=1e-8): Returns: Tuple(bool, TensorLike, int): A bool that shows whether an operation similar to the given operations - was found, the quaternion representation of the searched operation and their index in the list. + was found, the quaternion representation of the searched operation and its index in the list. """ node_points = qml.math.array(ops_vecs) gate_points = qml.math.array([_quaternion_transform(op_mat)]) From 5b317fe5ad8ecc17285fc8ba409bbc1697e2afe9 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Mon, 20 Jan 2025 08:11:28 -0500 Subject: [PATCH 04/15] look up speedup --- .../op_math/decompositions/solovay_kitaev.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pennylane/ops/op_math/decompositions/solovay_kitaev.py b/pennylane/ops/op_math/decompositions/solovay_kitaev.py index 9b8d5f14a9e..a43223f9db1 100644 --- a/pennylane/ops/op_math/decompositions/solovay_kitaev.py +++ b/pennylane/ops/op_math/decompositions/solovay_kitaev.py @@ -63,27 +63,28 @@ def _quaternion_transform(matrix): ) -def _contains_SU2(op_mat, ops_vecs, tol=1e-8): +def _contains_SU2(op_mat, ops_vecs=None, kd_tree=None, tol=1e-8): r"""Checks if a given SU(2) matrix is contained in a list of quaternions for a given tolerance. Args: op_mat (TensorLike): SU(2) matrix for the operation to be searched op_vecs (list(TensorLike)): List of quaternion for the operations that makes the search space. + kd_tree (KDTree): KDTree object built from the list of quaternions. Default is ``None``. tol (float): Tolerance for the match to be considered ``True``. Returns: Tuple(bool, TensorLike, int): A bool that shows whether an operation similar to the given operations was found, the quaternion representation of the searched operation and their index in the list. """ - node_points = qml.math.array(ops_vecs) gate_points = qml.math.array([_quaternion_transform(op_mat)]) - tree = KDTree(node_points) + tree = kd_tree or KDTree(qml.math.array(ops_vecs)) dist, indx = tree.query(gate_points, workers=-1) return (dist[0] < tol, gate_points[0], indx[0]) +# pylint: disable=too-many-statements @lru_cache() def _approximate_set(basis_gates, max_length=10): r"""Builds an approximate unitary set required for the `Solovay-Kitaev algorithm `_. @@ -137,8 +138,9 @@ def _approximate_set(basis_gates, max_length=10): # We will perform a breadth-first search (BFS) style set building for the set for depth in range(max_length - 1): + kdtree = KDTree(qml.math.array(approx_set_qat)) # Add the containers for next depth while we explore the current - gtrie_id, gtrie_mt, gtrie_gp, gtrie_sm = [], [], [], [] + gtrie_id, gtrie_mt, gtrie_gp, gtrie_sm, gtrie_qt = [], [], [], [], [] for node, su2m, gphase, tgsum in zip( gtrie_ids[depth], gtrie_mat[depth], gtrie_gph[depth], gtrie_sum[depth] ): @@ -154,13 +156,24 @@ def _approximate_set(basis_gates, max_length=10): # Extend and check if the node already exists in the approximate set. su2_gp = basis_gph[op] + gphase su2_op = (-1.0) ** bool(su2_gp >= math.pi) * (basis_mat[op] @ su2m) - exists, quaternion, index = _contains_SU2(su2_op, approx_set_qat) + + exists = False + if gtrie_qt: + exists, quaternion, index = _contains_SU2(su2_op, ops_vecs=gtrie_qt) + + if exists: + index += len(approx_set_qat) - len(gtrie_qt) + else: + exists, quaternion, index = _contains_SU2(su2_op, kd_tree=kdtree) global_phase = qml.math.mod(su2_gp, math.pi) if not exists or global_phase != approx_set_gph[index]: approx_set_ids.append(node + [op]) approx_set_mat.append(su2_op) + + # Add the quaternion data approx_set_qat.append(quaternion) + gtrie_qt.append(quaternion) # Add to the containers for next depth gtrie_id.append(node + [op]) From 3b392cacdb42a426db7d7b965c0f311d432c2649 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Mon, 20 Jan 2025 12:57:49 -0500 Subject: [PATCH 05/15] apply suggestions --- doc/releases/changelog-dev.md | 3 ++- pennylane/ops/op_math/decompositions/solovay_kitaev.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 44891255bd5..2522ea30e76 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -44,7 +44,8 @@ * An informative error is raised when a `QNode` with `diff_method=None` is differentiated. [(#6770)](https://github.com/PennyLaneAI/pennylane/pull/6770) -* `qml.ops.sk_decomposition` now has an improved approximate set generation. +* `qml.ops.sk_decomposition` has been improved to produce less gates for certain edge cases. This greatly impacts + the performance of `qml.clifford_t_decomposition`, which should now give less extraneous `qml.T` gates for them. [(#6855)](https://github.com/PennyLaneAI/pennylane/pull/6855)

Breaking changes 💔

diff --git a/pennylane/ops/op_math/decompositions/solovay_kitaev.py b/pennylane/ops/op_math/decompositions/solovay_kitaev.py index 14c5574bf51..ae0516d4bc6 100644 --- a/pennylane/ops/op_math/decompositions/solovay_kitaev.py +++ b/pennylane/ops/op_math/decompositions/solovay_kitaev.py @@ -157,7 +157,7 @@ def _approximate_set(basis_gates, max_length=10): su2_gp = basis_gph[op] + gphase su2_op = (-1.0) ** bool(su2_gp >= math.pi) * (basis_mat[op] @ su2m) - exists = False + exists, quaternion, index = False, None, -1 if gtrie_qt: exists, quaternion, index = _contains_SU2(su2_op, ops_vecs=gtrie_qt) From c7ef551e050abe36b87e2e9c008c9288bd73dba2 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Thu, 23 Jan 2025 14:30:24 -0500 Subject: [PATCH 06/15] add a test case --- tests/ops/op_math/test_solovay_kitaev.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/ops/op_math/test_solovay_kitaev.py b/tests/ops/op_math/test_solovay_kitaev.py index 40c263bd1db..fbd292c0f3a 100644 --- a/tests/ops/op_math/test_solovay_kitaev.py +++ b/tests/ops/op_math/test_solovay_kitaev.py @@ -138,19 +138,23 @@ def test_group_commutator_decompose(op): @pytest.mark.parametrize( - ("op"), + ("op", "max_depth"), [ - qml.RX(math.pi / 42, wires=[1]), - qml.RY(math.pi / 7, wires=["a"]), - qml.prod(*[qml.RX(1.0, "a"), qml.T("a")]), - qml.prod(*[qml.T(0), qml.Hadamard(0)] * 5), + (qml.RX(math.pi / 42, wires=[1]), 5), + (qml.RY(math.pi / 7, wires=["a"]), 5), + (qml.prod(*[qml.RX(1.0, "a"), qml.T("a")]), 5), + (qml.prod(*[qml.T(0), qml.Hadamard(0)] * 5), 5), + (qml.RZ(-math.pi / 2, wires=[1]), 1), + (qml.adjoint(qml.S(wires=["a"])), 1), + (qml.PhaseShift(5 * math.pi / 2, wires=[0]), 1), + (qml.PhaseShift(-3 * math.pi / 4, wires=["b"]), 1), ], ) -def test_solovay_kitaev(op): - """Test Solovay-Kitaev decomposition method""" +def test_solovay_kitaev(op, max_depth): + """Test Solovay-Kitaev decomposition method with specified max-depth""" with qml.queuing.AnnotatedQueue() as q: - gates = sk_decomposition(op, epsilon=1e-4, max_depth=5, basis_set=("T", "T*", "H")) + gates = sk_decomposition(op, epsilon=1e-4, max_depth=max_depth, basis_set=("T", "T*", "H")) assert q.queue == gates matrix_sk = qml.matrix(qml.tape.QuantumScript(gates)) From 08963050df4e7e8d5446254fd4fc337cb8ba58bf Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 24 Jan 2025 11:08:24 -0500 Subject: [PATCH 07/15] add comments --- .../op_math/decompositions/solovay_kitaev.py | 111 +++++++++++------- 1 file changed, 70 insertions(+), 41 deletions(-) diff --git a/pennylane/ops/op_math/decompositions/solovay_kitaev.py b/pennylane/ops/op_math/decompositions/solovay_kitaev.py index ae0516d4bc6..9cd19e80dd1 100644 --- a/pennylane/ops/op_math/decompositions/solovay_kitaev.py +++ b/pennylane/ops/op_math/decompositions/solovay_kitaev.py @@ -84,7 +84,39 @@ def _contains_SU2(op_mat, ops_vecs=None, kd_tree=None, tol=1e-8): return (dist[0] < tol, gate_points[0], indx[0]) -# pylint: disable=too-many-statements +def _prune_approximate_set( + approx_set_ids, approx_set_mat, approx_set_gph, approx_set_qat, approx_set_sum +): + """Prune the approximate set for equivalent gate sequences with higher T-gate counts. + Args: + approx_set_ids (list[list[~pennylane.operation.Operation]]): List of gate sequences. + approx_set_mat (list[TensorLike]): List of SU(2) matrices. + approx_set_gph (list[float]): List of global phases. + approx_set_qat (list[TensorLike]): List of quaternion representations. + approx_set_sum (list[int]): List of numbers of the T and Adjoint(T) gates in the sequences. + Returns: + Tuple[list[list[~pennylane.operation.Operation]], list[TensorLike], list[float], list[TensorLike]]: + A tuple containing the pruned approximate set. + """ + if approx_set_qat: + tree, tsum = KDTree(approx_set_qat), qml.math.array(approx_set_sum) + dists, indxs = tree.query(approx_set_qat, workers=-1, k=10) + + prune_ixs = [] + for dist, indx in zip(dists, indxs): + eq_idx = qml.math.sort(indx[qml.math.where(dist.round(8) == 0.0)]) + prune_ixs.extend(eq_idx[qml.math.argsort(tsum[eq_idx])][1:]) + + for ix in sorted(set(prune_ixs), reverse=True): + del approx_set_ids[ix] + del approx_set_mat[ix] + del approx_set_gph[ix] + del approx_set_qat[ix] + + return approx_set_ids, approx_set_mat, approx_set_gph, approx_set_qat + + +# pylint: disable=too-many- @lru_cache() def _approximate_set(basis_gates, max_length=10): r"""Builds an approximate unitary set required for the `Solovay-Kitaev algorithm `_. @@ -123,23 +155,34 @@ def _approximate_set(basis_gates, max_length=10): basis_mat.update({gate: su2_mat}) basis_gph.update({gate: su2_gph}) - # Maintains a trie-like structure for each depth + # Maintain a trie-like structure that consists of - + # gtrie_ stores + # each of them are list of lists, where each inner list store the data at a depth D, + # gtrie_[D] = [data1, data2, ...] where data1, data2, ... is stored at D-th depth. gtrie_ids = [[[gate] for gate in basis]] gtrie_mat = [list(basis_mat.values())] gtrie_gph = [list(basis_gph.values())] gtrie_sum = [[int(gate in t_set) for gate in basis]] - # Maintains the approximate set for gates' names, SU(2)s, global phases and quaternions + # Maintains the approximate set for gates, SU2s, global phases, T-gate sums and quaternions, + # where each of the approx_set_ is the corresponding flattened verison of gtrie_. + # We store the quaternions representation for the SU2 matrices for building a KDTree that allow + # querying possible neighbours of any newly built gate sequence and test its prior existence. approx_set_ids = list(gtrie_ids[0]) approx_set_mat = list(gtrie_mat[0]) approx_set_gph = list(gtrie_gph[0]) approx_set_sum = list(gtrie_sum[0]) approx_set_qat = [_quaternion_transform(mat) for mat in approx_set_mat] - # We will perform a breadth-first search (BFS) style set building for the set + # We will perform a breadth-first search (BFS)-style trie building, starting from basis gates: + # We attempt to extend every gate sequence at previous depth (defined by a node) with all + # basis gates. We add the extended sequence and its corresponding data to the next depth by + # comparing its quaternion representation with the gate sequences already added to the trie. for depth in range(max_length - 1): + # Build a KDTree for the quaternions stored up to the current depth for querying. kdtree = KDTree(qml.math.array(approx_set_qat)) - # Add the containers for next depth while we explore the current + + # Add the containers for extending the trie to the next depth while traversing current depth. gtrie_id, gtrie_mt, gtrie_gp, gtrie_sm, gtrie_qt = [], [], [], [], [] for node, su2m, gphase, tgsum in zip( gtrie_ids[depth], gtrie_mat[depth], gtrie_gph[depth], gtrie_sum[depth] @@ -147,70 +190,56 @@ def _approximate_set(basis_gates, max_length=10): # Get the last operation in the current node last_op = qml.adjoint(node[-1], lazy=False) if node else None - # Now attempt extending the current node for each basis gate + # Now attempt extending the current node with each gate in the basis set. for op in basis: - # If basis gate is adjoint of last op in the node, skip. + # If the op is the adjoint of last operation in the node, skip. if qml.equal(op, last_op): continue - # Extend and check if the node already exists in the approximate set. + # Extend and check if the node already exists in the approximate set in two steps: + # 1. (local check) => within the gate sequences built in the current iteration. + # 2. (global check) => within the gate sequences built in the previous iterations. su2_gp = basis_gph[op] + gphase su2_op = (-1.0) ** bool(su2_gp >= math.pi) * (basis_mat[op] @ su2m) - exists, quaternion, index = False, None, -1 - if gtrie_qt: - exists, quaternion, index = _contains_SU2(su2_op, ops_vecs=gtrie_qt) + exists, quaternion, global_index, local_index = False, None, -1, -1 + if gtrie_qt: # local check + exists, quaternion, local_index = _contains_SU2(su2_op, ops_vecs=gtrie_qt) - if exists: - index += len(approx_set_qat) - len(gtrie_qt) - else: - exists, quaternion, index = _contains_SU2(su2_op, kd_tree=kdtree) + if exists: # get the global index from the local index + global_index = local_index + len(approx_set_qat) - len(gtrie_qt) + else: # global check + exists, quaternion, global_index = _contains_SU2(su2_op, kd_tree=kdtree) global_phase = qml.math.mod(su2_gp, math.pi) - if not exists or global_phase != approx_set_gph[index]: + if not exists or global_phase != approx_set_gph[global_index]: + # Add the data to the approximate set approx_set_ids.append(node + [op]) approx_set_mat.append(su2_op) - - # Add the quaternion data + approx_set_gph.append(global_phase) approx_set_qat.append(quaternion) - gtrie_qt.append(quaternion) - # Add to the containers for next depth + # Add the data to the containers for next depth gtrie_id.append(node + [op]) gtrie_mt.append(su2_op) - - # Add the global phase data - approx_set_gph.append(global_phase) gtrie_gp.append(global_phase) + gtrie_qt.append(quaternion) # Add the T gate sum data tbool = int(op in t_set) approx_set_sum.append(tgsum + tbool) gtrie_sm.append(tgsum + tbool) - # Add to the next depth for next iteration + # Add to the next depth for new iteration gtrie_ids.append(gtrie_id) gtrie_mat.append(gtrie_mt) gtrie_gph.append(gtrie_gp) gtrie_sum.append(gtrie_sm) - # Prune the approximate set for equivalent operations with higher T-gate counts - if approx_set_qat: - tree, tsum = KDTree(approx_set_qat), qml.math.array(approx_set_sum) - dists, indxs = tree.query(approx_set_qat, workers=-1, k=10) - - prune_ixs = [] - for dist, indx in zip(dists, indxs): - eq_idx = qml.math.sort(indx[qml.math.where(dist.round(8) == 0.0)]) - prune_ixs.extend(eq_idx[qml.math.argsort(tsum[eq_idx])][1:]) - - for ix in sorted(set(prune_ixs), reverse=True): - del approx_set_ids[ix] - del approx_set_mat[ix] - del approx_set_gph[ix] - del approx_set_qat[ix] - - return approx_set_ids, approx_set_mat, approx_set_gph, approx_set_qat + # Prune the approximate set for equivalent operations with higher T-gate counts and return + return _prune_approximate_set( + approx_set_ids, approx_set_mat, approx_set_gph, approx_set_qat, approx_set_sum + ) def _group_commutator_decompose(matrix, tol=1e-5): From 93fcc50614c9aec43ba7b7deaf2f2098af90bdb7 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 24 Jan 2025 11:16:51 -0500 Subject: [PATCH 08/15] minor tweaks --- .../op_math/decompositions/solovay_kitaev.py | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/pennylane/ops/op_math/decompositions/solovay_kitaev.py b/pennylane/ops/op_math/decompositions/solovay_kitaev.py index 9cd19e80dd1..9eff3ac997a 100644 --- a/pennylane/ops/op_math/decompositions/solovay_kitaev.py +++ b/pennylane/ops/op_math/decompositions/solovay_kitaev.py @@ -73,8 +73,8 @@ def _contains_SU2(op_mat, ops_vecs=None, kd_tree=None, tol=1e-8): tol (float): Tolerance for the match to be considered ``True``. Returns: - Tuple(bool, TensorLike, int): A bool that shows whether an operation similar to the given operations - was found, the quaternion representation of the searched operation and its index in the list. + Tuple(bool, TensorLike, int): A bool that shows whether an operation similar to the given operations was + found, the quaternion representation of the searched operation and its index in the `op_vecs` or `kd_tree`. """ gate_points = qml.math.array([_quaternion_transform(op_mat)]) @@ -116,7 +116,6 @@ def _prune_approximate_set( return approx_set_ids, approx_set_mat, approx_set_gph, approx_set_qat -# pylint: disable=too-many- @lru_cache() def _approximate_set(basis_gates, max_length=10): r"""Builds an approximate unitary set required for the `Solovay-Kitaev algorithm `_. @@ -182,8 +181,8 @@ def _approximate_set(basis_gates, max_length=10): # Build a KDTree for the quaternions stored up to the current depth for querying. kdtree = KDTree(qml.math.array(approx_set_qat)) - # Add the containers for extending the trie to the next depth while traversing current depth. - gtrie_id, gtrie_mt, gtrie_gp, gtrie_sm, gtrie_qt = [], [], [], [], [] + # Add the local containers for extending the trie to the next depth while traversing current one. + ltrie_id, ltrie_mt, ltrie_gp, ltrie_sm, ltrie_qt = [], [], [], [], [] for node, su2m, gphase, tgsum in zip( gtrie_ids[depth], gtrie_mat[depth], gtrie_gph[depth], gtrie_sum[depth] ): @@ -203,11 +202,11 @@ def _approximate_set(basis_gates, max_length=10): su2_op = (-1.0) ** bool(su2_gp >= math.pi) * (basis_mat[op] @ su2m) exists, quaternion, global_index, local_index = False, None, -1, -1 - if gtrie_qt: # local check - exists, quaternion, local_index = _contains_SU2(su2_op, ops_vecs=gtrie_qt) + if ltrie_qt: # local check + exists, quaternion, local_index = _contains_SU2(su2_op, ops_vecs=ltrie_qt) if exists: # get the global index from the local index - global_index = local_index + len(approx_set_qat) - len(gtrie_qt) + global_index = local_index + len(approx_set_qat) - len(ltrie_qt) else: # global check exists, quaternion, global_index = _contains_SU2(su2_op, kd_tree=kdtree) @@ -220,21 +219,21 @@ def _approximate_set(basis_gates, max_length=10): approx_set_qat.append(quaternion) # Add the data to the containers for next depth - gtrie_id.append(node + [op]) - gtrie_mt.append(su2_op) - gtrie_gp.append(global_phase) - gtrie_qt.append(quaternion) + ltrie_id.append(node + [op]) + ltrie_mt.append(su2_op) + ltrie_gp.append(global_phase) + ltrie_qt.append(quaternion) # Add the T gate sum data tbool = int(op in t_set) approx_set_sum.append(tgsum + tbool) - gtrie_sm.append(tgsum + tbool) + ltrie_sm.append(tgsum + tbool) # Add to the next depth for new iteration - gtrie_ids.append(gtrie_id) - gtrie_mat.append(gtrie_mt) - gtrie_gph.append(gtrie_gp) - gtrie_sum.append(gtrie_sm) + gtrie_ids.append(ltrie_id) + gtrie_mat.append(ltrie_mt) + gtrie_gph.append(ltrie_gp) + gtrie_sum.append(ltrie_sm) # Prune the approximate set for equivalent operations with higher T-gate counts and return return _prune_approximate_set( From 46ab3e8c5b2a48a119b79b9448855151a0f4cea5 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 24 Jan 2025 15:11:49 -0500 Subject: [PATCH 09/15] apply suggestions --- pennylane/ops/op_math/decompositions/solovay_kitaev.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pennylane/ops/op_math/decompositions/solovay_kitaev.py b/pennylane/ops/op_math/decompositions/solovay_kitaev.py index 9eff3ac997a..c6f7c52f5ee 100644 --- a/pennylane/ops/op_math/decompositions/solovay_kitaev.py +++ b/pennylane/ops/op_math/decompositions/solovay_kitaev.py @@ -73,8 +73,9 @@ def _contains_SU2(op_mat, ops_vecs=None, kd_tree=None, tol=1e-8): tol (float): Tolerance for the match to be considered ``True``. Returns: - Tuple(bool, TensorLike, int): A bool that shows whether an operation similar to the given operations was - found, the quaternion representation of the searched operation and its index in the `op_vecs` or `kd_tree`. + Tuple(bool, TensorLike, int): A tuple including `True`/`False` for whether an operation similar to the + given operations was found, the quaternion representation of the searched operations, and its index in + the `op_vecs` or `kd_tree`. """ gate_points = qml.math.array([_quaternion_transform(op_mat)]) @@ -88,12 +89,14 @@ def _prune_approximate_set( approx_set_ids, approx_set_mat, approx_set_gph, approx_set_qat, approx_set_sum ): """Prune the approximate set for equivalent gate sequences with higher T-gate counts. + Args: approx_set_ids (list[list[~pennylane.operation.Operation]]): List of gate sequences. approx_set_mat (list[TensorLike]): List of SU(2) matrices. approx_set_gph (list[float]): List of global phases. approx_set_qat (list[TensorLike]): List of quaternion representations. approx_set_sum (list[int]): List of numbers of the T and Adjoint(T) gates in the sequences. + Returns: Tuple[list[list[~pennylane.operation.Operation]], list[TensorLike], list[float], list[TensorLike]]: A tuple containing the pruned approximate set. From f26257acc4fb46db64f5ed30a882dbef97c11353 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Mon, 27 Jan 2025 14:29:26 -0500 Subject: [PATCH 10/15] apply suggestions --- .../op_math/decompositions/solovay_kitaev.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pennylane/ops/op_math/decompositions/solovay_kitaev.py b/pennylane/ops/op_math/decompositions/solovay_kitaev.py index c6f7c52f5ee..57055f676bf 100644 --- a/pennylane/ops/op_math/decompositions/solovay_kitaev.py +++ b/pennylane/ops/op_math/decompositions/solovay_kitaev.py @@ -91,11 +91,11 @@ def _prune_approximate_set( """Prune the approximate set for equivalent gate sequences with higher T-gate counts. Args: - approx_set_ids (list[list[~pennylane.operation.Operation]]): List of gate sequences. - approx_set_mat (list[TensorLike]): List of SU(2) matrices. - approx_set_gph (list[float]): List of global phases. - approx_set_qat (list[TensorLike]): List of quaternion representations. - approx_set_sum (list[int]): List of numbers of the T and Adjoint(T) gates in the sequences. + approx_set_ids (list[list[~pennylane.operation.Operation]]): list of gate sequences + approx_set_mat (list[TensorLike]): list of SU(2) matrices + approx_set_gph (list[float]): list of global phases + approx_set_qat (list[TensorLike]): list of quaternion representations + approx_set_sum (list[int]): list of numbers of the T and Adjoint(T) gates in the sequences Returns: Tuple[list[list[~pennylane.operation.Operation]], list[TensorLike], list[float], list[TensorLike]]: @@ -159,8 +159,7 @@ def _approximate_set(basis_gates, max_length=10): # Maintain a trie-like structure that consists of - # gtrie_ stores - # each of them are list of lists, where each inner list store the data at a depth D, - # gtrie_[D] = [data1, data2, ...] where data1, data2, ... is stored at D-th depth. + # each of them are list of lists, where each inner list stores the data at a depth D, gtrie_ids = [[[gate] for gate in basis]] gtrie_mat = [list(basis_mat.values())] gtrie_gph = [list(basis_gph.values())] @@ -168,8 +167,8 @@ def _approximate_set(basis_gates, max_length=10): # Maintains the approximate set for gates, SU2s, global phases, T-gate sums and quaternions, # where each of the approx_set_ is the corresponding flattened verison of gtrie_. - # We store the quaternions representation for the SU2 matrices for building a KDTree that allow - # querying possible neighbours of any newly built gate sequence and test its prior existence. + # We store the quaternion representations for the SU2 matrices to build a KDTree. This allows us to + # query for nearest neighbours of any newly built gate sequence and test for its prior existence. approx_set_ids = list(gtrie_ids[0]) approx_set_mat = list(gtrie_mat[0]) approx_set_gph = list(gtrie_gph[0]) @@ -213,7 +212,8 @@ def _approximate_set(basis_gates, max_length=10): else: # global check exists, quaternion, global_index = _contains_SU2(su2_op, kd_tree=kdtree) - global_phase = qml.math.mod(su2_gp, math.pi) + # Add the sequence if it is unique, i.e., new SU(2) representation or global phase. + global_phase = qml.math.mod(su2_gp, math.pi) # Get the global phase in [0, \pi) if not exists or global_phase != approx_set_gph[global_index]: # Add the data to the approximate set approx_set_ids.append(node + [op]) From cd83c85c2e20494fa0c3c53f34c5899751dae703 Mon Sep 17 00:00:00 2001 From: Utkarsh Date: Mon, 27 Jan 2025 14:36:37 -0500 Subject: [PATCH 11/15] Update pennylane/ops/op_math/decompositions/solovay_kitaev.py --- pennylane/ops/op_math/decompositions/solovay_kitaev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/ops/op_math/decompositions/solovay_kitaev.py b/pennylane/ops/op_math/decompositions/solovay_kitaev.py index 57055f676bf..e189dd66c41 100644 --- a/pennylane/ops/op_math/decompositions/solovay_kitaev.py +++ b/pennylane/ops/op_math/decompositions/solovay_kitaev.py @@ -159,7 +159,7 @@ def _approximate_set(basis_gates, max_length=10): # Maintain a trie-like structure that consists of - # gtrie_ stores - # each of them are list of lists, where each inner list stores the data at a depth D, + # each of them are list of lists, where each inner list stores the data at a depth D. gtrie_ids = [[[gate] for gate in basis]] gtrie_mat = [list(basis_mat.values())] gtrie_gph = [list(basis_gph.values())] From 19b2452a488b1a9ba76a26e38a275e99130669ab Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Tue, 28 Jan 2025 16:22:19 -0500 Subject: [PATCH 12/15] minor tweaks --- doc/releases/changelog-dev.md | 2 +- pennylane/ops/op_math/decompositions/solovay_kitaev.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 81f1e945667..33cd1b9cfa2 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -52,7 +52,7 @@ [(#6770)](https://github.com/PennyLaneAI/pennylane/pull/6770) * `qml.ops.sk_decomposition` has been improved to produce less gates for certain edge cases. This greatly impacts - the performance of `qml.clifford_t_decomposition`, which should now give less extraneous `qml.T` gates for them. + the performance of `qml.clifford_t_decomposition`, which should now give less extraneous `qml.T` gates. [(#6855)](https://github.com/PennyLaneAI/pennylane/pull/6855) * `qml.gradients.finite_diff_jvp` has been added to compute the jvp of an arbitrary numeric diff --git a/pennylane/ops/op_math/decompositions/solovay_kitaev.py b/pennylane/ops/op_math/decompositions/solovay_kitaev.py index e189dd66c41..af70b32cd68 100644 --- a/pennylane/ops/op_math/decompositions/solovay_kitaev.py +++ b/pennylane/ops/op_math/decompositions/solovay_kitaev.py @@ -339,7 +339,7 @@ def sk_decomposition(op, epsilon, *, max_depth=5, basis_set=("T", "T*", "H"), ba with QueuingManager.stop_recording(): # Build the approximate set with caching approx_set_ids, approx_set_mat, approx_set_gph, approx_set_qat = _approximate_set( - basis_set, max_length=basis_length + tuple(basis_set), max_length=basis_length ) # Build the k-d tree with the current approximation set for querying in the base case From 4fafee13b086d585ea64c0f22a6d068fb447b7e3 Mon Sep 17 00:00:00 2001 From: Utkarsh Date: Fri, 31 Jan 2025 16:35:24 -0500 Subject: [PATCH 13/15] Update test_cliffordt_transform.py --- tests/transforms/test_cliffordt_transform.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/transforms/test_cliffordt_transform.py b/tests/transforms/test_cliffordt_transform.py index 5011a553e19..eb701955632 100644 --- a/tests/transforms/test_cliffordt_transform.py +++ b/tests/transforms/test_cliffordt_transform.py @@ -180,7 +180,6 @@ def qfunc(): for op in tape.operations ) - @pytest.mark.parametrize("epsilon", [2e-2, 5e-2, 7e-2]) def test_phase_shift_decomposition(self): """Test decomposition for the Clifford transform applied to the circuits with phase shifts.""" old_tape = qml.tape.make_qscript(circuit_6)() @@ -194,7 +193,7 @@ def test_phase_shift_decomposition(self): assert qml.equal(compiled_ops[2], qml.adjoint(qml.T(2))) assert qml.equal(compiled_ops[3], qml.T(3)) - @pytest.mark.parametrize("epsilon", [2e-2, 5e-2, 9e-2]) + @pytest.mark.parametrize("epsilon", [2e-2, 5e-2, 7e-2]) @pytest.mark.parametrize("circuit", [circuit_3, circuit_4, circuit_5]) def test_total_error(self, epsilon, circuit): """Ensure that given a certain epsilon, the total operator error is below the threshold.""" From ea1ef248999c8c9665330d4f4999b823f9893b8b Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 31 Jan 2025 18:14:19 -0500 Subject: [PATCH 14/15] minor cleanup --- .../op_math/decompositions/solovay_kitaev.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pennylane/ops/op_math/decompositions/solovay_kitaev.py b/pennylane/ops/op_math/decompositions/solovay_kitaev.py index af70b32cd68..8f61811f9b2 100644 --- a/pennylane/ops/op_math/decompositions/solovay_kitaev.py +++ b/pennylane/ops/op_math/decompositions/solovay_kitaev.py @@ -68,9 +68,9 @@ def _contains_SU2(op_mat, ops_vecs=None, kd_tree=None, tol=1e-8): Args: op_mat (TensorLike): SU(2) matrix for the operation to be searched - op_vecs (list(TensorLike)): List of quaternion for the operations that makes the search space. - kd_tree (KDTree): KDTree object built from the list of quaternions. Default is ``None``. - tol (float): Tolerance for the match to be considered ``True``. + op_vecs (list(TensorLike)): list of quaternion for the operations that makes the search space. + kd_tree (scipy.spatial.KDTree): kd-tree object built from the list of quaternions. Default is ``None``. + tol (float): tolerance for the match to be considered ``True``. Returns: Tuple(bool, TensorLike, int): A tuple including `True`/`False` for whether an operation similar to the @@ -124,9 +124,8 @@ def _approximate_set(basis_gates, max_length=10): r"""Builds an approximate unitary set required for the `Solovay-Kitaev algorithm `_. Args: - basis_gates (list(str)): Basis set to be used for Solovay-Kitaev decomposition build using - following terms, ``['X', 'Y', 'Z', 'H', 'T', 'T*', 'S', 'S*']``, where `*` refers - to the gate adjoint. + basis_gates (tuple[str]): Basis set to be used for Solovay-Kitaev decomposition build using the following + terms, ``('X', 'Y', 'Z', 'H', 'T', 'T*', 'S', 'S*')``, where `*` refers to the gate adjoint. max_length (int): Maximum expansion length of Clifford+T sequences in the approximation set. Default is `10` Returns: @@ -293,9 +292,9 @@ def sk_decomposition(op, epsilon, *, max_depth=5, basis_set=("T", "T*", "H"), ba Keyword Args: max_depth (int): The maximum number of approximation passes. A smaller :math:`\epsilon` would generally require a greater number of passes. Default is ``5``. - basis_set (list[str]): Basis set to be used for the decomposition and building an approximate set internally. - It accepts the following gate terms: ``['X', 'Y', 'Z', 'H', 'T', 'T*', 'S', 'S*']``, where ``*`` refers - to the gate adjoint. Default value is ``['T', 'T*', 'H']``. + basis_set (tuple[str]): Basis set to be used for the decomposition and building an approximate set internally. + It accepts the following gate terms: ``('X', 'Y', 'Z', 'H', 'T', 'T*', 'S', 'S*')``, where ``*`` refers + to the gate adjoint. Default value is ``('T', 'T*', 'H')``. basis_length (int): Maximum expansion length of Clifford+T sequences in the internally-built approximate set. Default is ``10``. From dd1efbc3b3d5ef8e2876b94aab7ad4606c7433bf Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 31 Jan 2025 18:53:09 -0500 Subject: [PATCH 15/15] minor tweak --- pennylane/ops/op_math/decompositions/solovay_kitaev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/ops/op_math/decompositions/solovay_kitaev.py b/pennylane/ops/op_math/decompositions/solovay_kitaev.py index 8f61811f9b2..7695b0ae9e6 100644 --- a/pennylane/ops/op_math/decompositions/solovay_kitaev.py +++ b/pennylane/ops/op_math/decompositions/solovay_kitaev.py @@ -69,7 +69,7 @@ def _contains_SU2(op_mat, ops_vecs=None, kd_tree=None, tol=1e-8): Args: op_mat (TensorLike): SU(2) matrix for the operation to be searched op_vecs (list(TensorLike)): list of quaternion for the operations that makes the search space. - kd_tree (scipy.spatial.KDTree): kd-tree object built from the list of quaternions. Default is ``None``. + kd_tree (scipy.spatial.KDTree): kd-tree built from the list of quaternions. Default is ``None``. tol (float): tolerance for the match to be considered ``True``. Returns: