Repository: PennyLaneAI/pennylane
Pull request #7722 (2025-06-20 18:25:15)

BugFix: handle lists in resource params of BasisState
**Context:**
Fixes a bug where `BasisState` cannot be used in the decomposition graph with the error raised:
```
TypeError: unhashable type: 'list'
```

**Description of the Change:**

- Fixes a bug
- Removes unnecessary information from the resource params of `BasisState`, and make the decomposition of `BasisState` with capture enabled more efficient.

**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:**

[sc-93811]


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7722?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.68%. Comparing base [(`9a213c1`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/9a213c122c2594d306c87f02799ae6638e24c64c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`5fb32d9`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/5fb32d9ad980a6602f0e359ea66f5ed654f6a882?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #7722   +/-   ##
=======================================
  Coverage   99.68%   99.68%           
=======================================
  Files         538      538           
  Lines       54253    54258    +5     
=======================================
+ Hits        54083    54088    +5     
  Misses        170      170           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7722?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index a526659d1..a32d6e297 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -745,6 +745,14 @@
 
 * Updated documentation check to remove duplicate docstring references. [(#7453)](https://github.com/PennyLaneAI/pennylane/pull/7453)
 
+* Improved performance for `qml.clifford_t_decomposition` transform by introducing caching support and changed the
+  default basis set of `qml.ops.sk_decomposition` to `(H, S, T)`, resulting in shorter decomposition sequences.
+  [(#7454)](https://github.com/PennyLaneAI/pennylane/pull/7454)
+
+* The decomposition of `qml.BasisState` with capture and the graph-based decomposition system enabled is more efficient. 
+  Additionally, the resource params of `qml.BasisState` is simplified to the number of wires.
+  [(#7722)](https://github.com/PennyLaneAI/pennylane/pull/7722)
+
 <h3>Labs: a place for unified and rapid prototyping of research software 🧪</h3>
 
 * The imports of dependencies introduced by ``labs`` functionalities have been modified such that
@@ -1229,6 +1237,9 @@ may move operations across a `Snapshot`.
   off from the original tape by a global phase.
   [(#7619)](https://github.com/PennyLaneAI/pennylane/pull/7619)
 
+* Fixes a bug where an error is raised from the decomposition graph when the resource params of an operator contains lists.
+  [(#7722)](https://github.com/PennyLaneAI/pennylane/pull/7722)
+
 * Updated documentation for mid-circuit measurements using the Tree Traversal algorithm
   to reflect supported devices and usage in analytic simulations,
   in the :doc:`/introduction/dynamic_quantum_circuits` page.
diff --git a/pennylane/decomposition/resources.py b/pennylane/decomposition/resources.py
index a6666e1f8..3cce6ed37 100644
--- a/pennylane/decomposition/resources.py
+++ b/pennylane/decomposition/resources.py
@@ -157,7 +157,9 @@ def _make_hashable(d):
             sorted(((str(k), _make_hashable(v)) for k, v in d.items()), key=lambda x: x[0])
         )
     if hasattr(d, "tolist"):
-        return d.tolist()
+        d = d.tolist()
+    if isinstance(d, list):
+        return tuple(_make_hashable(v) for v in d)
     return d
 
 
diff --git a/pennylane/ops/qubit/state_preparation.py b/pennylane/ops/qubit/state_preparation.py
index b20c1694b..dc85d2fcd 100644
--- a/pennylane/ops/qubit/state_preparation.py
+++ b/pennylane/ops/qubit/state_preparation.py
@@ -75,11 +75,11 @@ class BasisState(StatePrepBase):
     [0.+0.j 0.+0.j 0.+0.j 1.+0.j]
     """
 
-    resource_keys = {"state", "wires"}
+    resource_keys = {"num_wires"}
 
     @property
     def resource_params(self) -> dict:
-        return {"state": self.parameters[0], "wires": self.wires}
+        return {"num_wires": len(self.wires)}
 
     def __init__(self, state, wires: WiresLike, id=None):
 
@@ -185,23 +185,32 @@ class BasisState(StatePrepBase):
         return math.convert_like(ket, prep_vals)
 
 
-def _basis_state_decomp_resources(state, wires):
-    if not qml.math.is_abstract(state):
-        return {qml.X: len([wire for wire, basis in zip(wires, state, strict=True) if basis == 1])}
-    return {qml.PhaseShift: 2 * len(wires), qml.RX: len(wires)}
+def _basis_state_decomp_resources(num_wires):
+    # Represent one of the X gates as an RX and a GlobalPhase because RX is
+    # used when jax-jit is enabled without capture/qjit.
+    return {qml.X: num_wires - 1 or num_wires, qml.RX: 1, qml.GlobalPhase: 1}
 
 
 @register_resources(_basis_state_decomp_resources)
 def _basis_state_decomp(state, wires, **__):
-    if not qml.math.is_abstract(state):
-        for wire, basis in zip(wires, state, strict=True):
-            if basis == 1:
-                qml.X(wire)
-    else:
+
+    if qml.math.is_abstract(state) and not (qml.capture.enabled() or qml.compiler.active()):
+        # This branch is for supporting jax-jit without capture/qjit.
+        global_phase = 0.0
         for wire, basis in zip(wires, state):
-            qml.PhaseShift(basis * np.pi / 2, wire)
-            qml.RX(basis * np.pi, wire)
-            qml.PhaseShift(basis * np.pi / 2, wire)
+            qml.RX(basis * np.pi, wires=wire)
+            global_phase += basis * np.pi / 2
+        qml.GlobalPhase(-global_phase)
+        return
+
+    def _X(w):
+        qml.X(w)
+
+    @qml.for_loop(0, len(wires), 1)
+    def _loop(i):
+        qml.cond(qml.math.allclose(state[i], 1), _X)(wires[i])
+
+    _loop()  # pylint: disable=no-value-for-parameter
 
 
 add_decomps(BasisState, _basis_state_decomp)
diff --git a/pennylane/templates/embeddings/basis.py b/pennylane/templates/embeddings/basis.py
index e254f85ac..9bc9075a6 100644
--- a/pennylane/templates/embeddings/basis.py
+++ b/pennylane/templates/embeddings/basis.py
@@ -16,7 +16,8 @@ Contains the BasisEmbedding template.
 """
 # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access
 
-from pennylane.ops.qubit.state_preparation import BasisState
+from pennylane.decomposition import add_decomps
+from pennylane.ops.qubit.state_preparation import BasisState, _basis_state_decomp
 
 
 class BasisEmbedding(BasisState):
@@ -69,3 +70,6 @@ class BasisEmbedding(BasisState):
 
     def __init__(self, features, wires, id=None):
         super().__init__(features, wires=wires, id=id)
+
+
+add_decomps(BasisEmbedding, _basis_state_decomp)


Test patch:
diff --git a/tests/decomposition/test_resources.py b/tests/decomposition/test_resources.py
index ddd4e902a..1c91ae080 100644
--- a/tests/decomposition/test_resources.py
+++ b/tests/decomposition/test_resources.py
@@ -172,6 +172,20 @@ class TestCompressedResourceOp:
         )
         assert isinstance(hash(op), int)
 
+    def test_hash_list_params(self):
+        """Tests when the resource params contains a list."""
+
+        class CustomOp(qml.operation.Operator):  # pylint: disable=too-few-public-methods
+
+            resource_keys = {"foo", "bar"}
+
+            @property
+            def resource_params(self) -> dict:
+                return {"foo": [1, 2, 3], "bar": [1, 2, [3, 4, 5]]}
+
+        op = CompressedResourceOp(CustomOp, {"foo": [1, 2, 3], "bar": [1, 2, [3, 4, 5]]})
+        assert isinstance(hash(op), int)
+
     def test_same_params_same_hash(self):
         """Tests that two ops with the same params have the same hash."""
 
diff --git a/tests/ops/functions/conftest.py b/tests/ops/functions/conftest.py
index 8ef514f3e..b47c84497 100644
--- a/tests/ops/functions/conftest.py
+++ b/tests/ops/functions/conftest.py
@@ -40,7 +40,7 @@ def _trotterize_qfunc_dummy(time, theta, phi, wires, flip=False):
 _INSTANCES_TO_TEST = [
     (qml.sum(qml.PauliX(0), qml.PauliZ(0)), {}),
     (qml.sum(qml.X(0), qml.X(0), qml.Z(0), qml.Z(0)), {}),
-    (qml.BasisState([1], wires=[0]), {"skip_differentiation": True}),
+    (qml.BasisState([1], wires=[0]), {"skip_differentiation": True, "heuristic_resources": True}),
     (
         qml.ControlledQubitUnitary(np.eye(2), wires=[1, 0]),
         {"skip_differentiation": True, "heuristic_resources": True},
diff --git a/tests/ops/qubit/test_state_prep.py b/tests/ops/qubit/test_state_prep.py
index 413947306..d20ecf68c 100644
--- a/tests/ops/qubit/test_state_prep.py
+++ b/tests/ops/qubit/test_state_prep.py
@@ -40,7 +40,7 @@ def test_assert_valid():
     """Tests that BasisState operators are valid"""
 
     op = qml.BasisState(np.array([0, 1]), wires=[0, 1])
-    qml.ops.functions.assert_valid(op, skip_differentiation=True)
+    qml.ops.functions.assert_valid(op, skip_differentiation=True, heuristic_resources=True)
 
     def abstract_check(state):
         op = qml.BasisState(state, wires=[0, 1])
@@ -56,7 +56,7 @@ def test_assert_valid():
             for _op in tape.operations:
                 resource_rep = qml.resource_rep(type(_op), **_op.resource_params)
                 actual_gate_counts[resource_rep] += 1
-            assert gate_counts == actual_gate_counts
+            assert all(_op in gate_counts for _op in actual_gate_counts)
 
             # Tests that the decomposition produces the same matrix
             op_matrix = qml.matrix(op)
diff --git a/tests/templates/test_embeddings/test_basis.py b/tests/templates/test_embeddings/test_basis.py
index c552cc254..be37de9d3 100644
--- a/tests/templates/test_embeddings/test_basis.py
+++ b/tests/templates/test_embeddings/test_basis.py
@@ -25,7 +25,7 @@ def test_standard_validity():
     """Check the operation using the assert_valid function."""
     wires = qml.wires.Wires((0, 1, 2))
     op = qml.BasisEmbedding(features=np.array([1, 1, 1]), wires=wires)
-    qml.ops.functions.assert_valid(op, skip_differentiation=True)
+    qml.ops.functions.assert_valid(op, skip_differentiation=True, heuristic_resources=True)
 
 
 # pylint: disable=protected-access