Repository: PennyLaneAI/pennylane
Pull request #6985 (2025-02-20 15:23:52)

[FTQC] Add graph state prep class
### Before submitting

Please complete the following checklist when submitting a PR:

- [ ] All new features must include a unit test.
      If you've fixed a bug or added code that should be tested, add a test to the
      test directory!

- [ ] All new functions and code must be clearly commented and documented.
      If you do make documentation changes, make sure that the docs build and
      render correctly by running `make docs`.

- [x] Ensure that the test suite passes, by running `make test`.

- [x] Add a new entry to the `doc/releases/changelog-dev.md` file, summarizing the
      change, and including a link back to the PR.

- [x] The PennyLane source code conforms to
      [PEP8 standards](https://www.python.org/dev/peps/pep-0008/).
      We check all of our code against [Pylint](https://www.pylint.org/).
      To lint modified files, simply `pip install pylint`, and then
      run `pylint pennylane/path/to/file.py`.

When all the above are checked, delete everything above the dashed
line and fill in the pull request template.

------------------------------------------------------------------------------------------------------------

**Context:**

[sc-84008]

**Description of the Change:**

**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:**


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6985?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: Patch coverage is `98.95833%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 99.60%. Comparing base [(`a99d542`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/a99d542ba0e40c7fde4939488cf772cf1df817c1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`9ab0ce3`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/9ab0ce3d94d60f09adca4aa66edcbff650bf7caa?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 8 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6985?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Patch % | Lines |
|---|---|---|
| [pennylane/ftqc/graph\_state\_preparation.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6985?src=pr&el=tree&filepath=pennylane%2Fftqc%2Fgraph_state_preparation.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2Z0cWMvZ3JhcGhfc3RhdGVfcHJlcGFyYXRpb24ucHk=) | 91.30% | [2 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6985?src=pr&el=tree&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    #6985      +/-   ##
==========================================
- Coverage   99.60%   99.60%   -0.01%     
==========================================
  Files         488      490       +2     
  Lines       46632    47007     +375     
==========================================
+ Hits        46449    46821     +372     
- Misses        183      186       +3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6985?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>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
multiphaseCFD: > One more thing: could you wrap the lines in the docstrings to 100 characters? I don't think black enforces this, but it makes it easier to read in an editor

Thanks @joeycarter ! Done

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 71055616d..0aa3a5b84 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -394,6 +394,9 @@
   `jnp.arange`, and `jnp.full`.
   [#6865)](https://github.com/PennyLaneAI/pennylane/pull/6865)
 
+* A template class, `qml.ftqc.GraphStatePrep`, is added for the Graph state construction.
+  [(#6985)](https://github.com/PennyLaneAI/pennylane/pull/6985)
+
 * `qml.cond` can return arrays with dynamic shapes.
   [(#6888)](https://github.com/PennyLaneAI/pennylane/pull/6888/)
 
diff --git a/pennylane/ftqc/__init__.py b/pennylane/ftqc/__init__.py
index 87779692f..52727a00a 100644
--- a/pennylane/ftqc/__init__.py
+++ b/pennylane/ftqc/__init__.py
@@ -37,9 +37,11 @@ from .parametric_midmeasure import (
     diagonalize_mcms,
 )
 from .lattice import Lattice, generate_lattice
+from .graph_state_preparation import GraphStatePrep
 from .qubit_graph import QubitGraph
 
 __all__ = [
+    "GraphStatePrep",
     "Lattice",
     "QubitGraph",
     "ParametricMidMeasureMP",
diff --git a/pennylane/ftqc/graph_state_preparation.py b/pennylane/ftqc/graph_state_preparation.py
new file mode 100644
index 000000000..6b4fa9ca2
--- /dev/null
+++ b/pennylane/ftqc/graph_state_preparation.py
@@ -0,0 +1,181 @@
+# Copyright 2025 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+r"""This module contains the GraphStatePrep template."""
+
+from typing import Optional, Union
+
+import networkx as nx
+
+import pennylane as qml
+from pennylane.operation import Operation
+from pennylane.wires import Wires
+
+from .qubit_graph import QubitGraph
+
+
+class GraphStatePrep(Operation):
+    r"""
+    Encode a graph state with a single graph operation applied on each qubit, and an entangling
+    operation applied on nearest-neighbor qubits defined by the graph connectivity.
+    The initial graph is :math:`|0\rangle^{\otimes V}`, given each qubit or graph vertex node
+    (:math:`V`) in the graph is in the :math:`|0\rangle` state and is not entangled with any
+    other qubit.
+    The target graph state :math:`| \psi \rangle` is:
+    :math:`| \psi \rangle = \prod\limits_{\{a, b\} \in E} U_{ab}|+\rangle^{\otimes V}`
+    where :math:`U_{ab}` is a phase gate applied to the vertices :math:`a`, :math:`b` of a edge
+    :math:`E` in the graph as illustrated in eq. (24)
+    in `arxiv:quant-ph/0602096 <https://arxiv.org/pdf/quant-ph/0602096>`_.
+
+    The target graph state can be prepared as below:
+
+        1. Each qubit is prepared as :math:`|+\rangle^{\otimes V}` state by applying the
+        ``one_qubit_ops`` (:class:`~.pennylane.H` gate) operation.
+
+        2. Entangle every nearest qubit pair in the graph with ``two_qubit_ops``
+        (:class:`~.pennylane.CZ` gate) operation.
+
+    Args:
+        graph (Union[QubitGraph, networkx.Graph]): QubitGraph or networkx.Graph object mapping qubit to wires.
+        one_qubit_ops (Operation): Operator to prepare the initial state of each qubit. Default to :class:`~.pennylane.H`.
+        two_qubit_ops (Operation): Operator to entangle nearest qubits. Default to :class:`~.pennylane.CZ`.
+        wires (Optional[Wires]): Wires the operator applies on. Wires will be mapped 1:1 to graph nodes. Optional only `graph`
+          is a QubitGraph. If no wires are provided, the ``children`` of the provided ``QubitGraph`` will be used as wires.
+
+    .. todo::
+
+        1. To define more complex starting states not relying on a single ops (``one_qubit_ops``
+            and ``two_qubit_ops``).
+        2. Ensure ``wires`` works with multiple dimensional ``nx.Graph()`` object after the wires
+           indexing scheme is added to the ``ftqc`` module.
+
+    **Example:**
+        The graph state preparation layer can be customized by the user.
+
+        .. code-block:: python3
+
+            import pennylane as qml
+            from pennylane.ftqc import generate_lattice, GraphStatePrep, QubitGraph
+
+            dev = qml.device('default.qubit')
+
+            @qml.qnode(dev)
+            def circuit(q, one_qubit_ops, two_qubit_ops, wires = None):
+                GraphStatePrep(graph=q, one_qubit_ops=one_qubit_ops, two_qubit_ops=two_qubit_ops, wires = wires)
+                return qml.probs()
+
+            lattice = generate_lattice([2, 2], "square")
+            q = QubitGraph(lattice.graph, id="square")
+
+            one_qubit_ops = qml.Y
+            two_qubit_ops = qml.CNOT
+
+        If the wires argument is not explicitly passed to the circuit, the child nodes of the
+        ``QubitGraph`` are used as the wires. The resulting circuit after applying the
+        ``GraphStatePrep`` template is:
+
+        >>> print(qml.draw(circuit, level="device")(q, one_qubit_ops, two_qubit_ops))
+        QubitGraph<id=(0, 0), loc=[square]>: ──Y─╭●─╭●───────┤  Probs
+        QubitGraph<id=(0, 1), loc=[square]>: ──Y─│──╰X─╭●────┤  Probs
+        QubitGraph<id=(1, 0), loc=[square]>: ──Y─╰X────│──╭●─┤  Probs
+        QubitGraph<id=(1, 1), loc=[square]>: ──Y───────╰X─╰X─┤  Probs
+
+        The circuit wires can also be customized by passing a wires argument to the circuit as follows:
+
+        >>> print(qml.draw(circuit, level="device")(q, one_qubit_ops, two_qubit_ops, wires=[0, 1, 2, 3]))
+        0: ──Y─╭●─╭●───────┤  Probs
+        1: ──Y─│──╰X─╭●────┤  Probs
+        2: ──Y─╰X────│──╭●─┤  Probs
+        3: ──Y───────╰X─╰X─┤  Probs
+
+    """
+
+    def __init__(
+        self,
+        graph: Union[nx.Graph, QubitGraph],
+        one_qubit_ops: Operation = qml.H,
+        two_qubit_ops: Operation = qml.CZ,
+        wires: Optional[Wires] = None,
+    ):
+        self.hyperparameters["graph"] = graph
+        self.hyperparameters["one_qubit_ops"] = one_qubit_ops
+        self.hyperparameters["two_qubit_ops"] = two_qubit_ops
+
+        if isinstance(graph, QubitGraph):
+            if wires is not None and len(set(wires)) != len(set(graph.node_labels)):
+                raise ValueError(
+                    "Please ensure the length of wires objects match the number of children in QubitGraph."
+                )
+            super().__init__(wires=wires if wires is not None else list(graph.children))
+        else:
+            if wires is None:
+                raise ValueError("Please ensure wires is specified.")
+            if len(wires) != len(set(graph.nodes)):
+                raise ValueError(
+                    "Please ensure the length of wires objects match that of labels in graph."
+                )
+            super().__init__(wires=wires)
+
+    def label(self) -> str:  # pylint: disable=arguments-differ
+        r"""Defines how the graph state preparation is represented in diagrams and drawings.
+
+        Returns:
+            str: label to use in drawings
+        """
+        return repr(self)
+
+    def __repr__(self):
+        """Method defining the string representation of this class."""
+        return f"GraphStatePrep({self.hyperparameters['one_qubit_ops'](wires=0).name}, {self.hyperparameters['two_qubit_ops'].name})"
+
+    @staticmethod
+    def compute_decomposition(
+        wires: Wires,
+        graph: Union[nx.Graph, QubitGraph],
+        one_qubit_ops: Operation = qml.H,
+        two_qubit_ops: Operation = qml.CZ,
+    ):  # pylint: disable=arguments-differ, unused-argument
+        r"""Representation of the operator as a product of other operators (static method).
+
+        .. note::
+
+            Operations making up the decomposition should be queued within the
+            ``compute_decomposition`` method.
+
+        .. seealso:: :meth:`~.Operator.decomposition`.
+
+        Args:
+            wires (Wires): Wires the decomposition applies on. Wires will be mapped 1:1 to graph nodes.
+            graph (Union[nx.Graph, QubitGraph]): QubitGraph or nx.Graph object mapping qubit to wires.
+            one_qubit_ops (Operation): Operator to prepare the initial state of each qubit. Default to :class:`~.pennylane.H`.
+            two_qubit_ops (Operation): Operator to entangle nearest qubits. Default to :class:`~.pennylane.CZ`.
+
+        Returns:
+            list[Operator]: decomposition of the operator
+        """
+
+        op_list = []
+
+        edges = graph.edge_labels if isinstance(graph, QubitGraph) else graph.edges
+        nodes = graph.node_labels if isinstance(graph, QubitGraph) else graph.nodes
+
+        if set(wires) != set(nodes):
+            wire_map = dict(zip(nodes, wires))
+            edges = [(wire_map[edge[0]], wire_map[edge[1]]) for edge in edges]
+
+        for wire in wires:
+            op_list.append(one_qubit_ops(wires=wire))
+        for edge in edges:
+            op_list.append(two_qubit_ops(wires=edge))
+        return op_list
diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py
index 3205e7875..f3582975c 100644
--- a/pennylane/ops/functions/assert_valid.py
+++ b/pennylane/ops/functions/assert_valid.py
@@ -219,13 +219,14 @@ def _check_generator(op):
         )()
 
 
-def _check_copy(op):
+def _check_copy(op, skip_deepcopy):
     """Check that copies and deep copies give identical objects."""
     copied_op = copy.copy(op)
     assert qml.equal(copied_op, op), "copied op must be equal with qml.equal"
     assert copied_op == op, "copied op must be equivalent to original operation"
     assert copied_op is not op, "copied op must be a separate instance from original operaiton"
-    assert qml.equal(copy.deepcopy(op), op), "deep copied op must also be equal"
+    if not skip_deepcopy:
+        assert qml.equal(copy.deepcopy(op), op), "deep copied op must also be equal"
 
 
 # pylint: disable=import-outside-toplevel, protected-access
@@ -355,6 +356,7 @@ def _check_wires(op, skip_wire_mapping):
 
 def assert_valid(
     op: qml.operation.Operator,
+    skip_deepcopy=False,
     skip_pickle=False,
     skip_wire_mapping=False,
     skip_differentiation=False,
@@ -366,6 +368,7 @@ def assert_valid(
         op (.Operator): an operator instance to validate
 
     Keyword Args:
+        skip_deepcopy=False: If `True`, deepcopy tests are not run.
         skip_pickle=False : If ``True``, pickling tests are not run. Set to ``True`` when
             testing a locally defined operator, as pickle cannot handle local objects
         skip_differentiation: If ``True``, differentiation tests are not run. Set to `True` when
@@ -413,7 +416,7 @@ def assert_valid(
 
     if len(op.wires) <= 26:
         _check_wires(op, skip_wire_mapping)
-    _check_copy(op)
+    _check_copy(op, skip_deepcopy)
     _check_pytree(op)
     if not skip_pickle:
         _check_pickle(op)


Test patch:
diff --git a/tests/ftqc/test_ftqc_graph_state_prep.py b/tests/ftqc/test_ftqc_graph_state_prep.py
new file mode 100644
index 000000000..f8cbea3dc
--- /dev/null
+++ b/tests/ftqc/test_ftqc_graph_state_prep.py
@@ -0,0 +1,289 @@
+# Copyright 2025 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# pylint: disable=no-name-in-module, no-self-use, protected-access
+"""Unit tests for the GraphStatePrep module"""
+
+import networkx as nx
+import numpy as np
+import pytest
+
+import pennylane as qml
+from pennylane.ftqc import GraphStatePrep, QubitGraph, generate_lattice
+from pennylane.ops.functions import assert_valid
+
+
+class TestGraphStatePrep:
+    """Test for graph state prep"""
+
+    @pytest.mark.xfail(reason="Jax JIT cannot trace a graph object")
+    def test_non_jaxjit_circuit_graph_state_prep(self):
+        """Test if Jax JIT works with GraphStatePrep"""
+        jax = pytest.importorskip("jax")
+
+        lattice = generate_lattice([2, 2], "square")
+        q = QubitGraph(lattice.graph)
+        dev = qml.device("default.qubit")
+
+        @jax.jit
+        @qml.qnode(dev)
+        def circuit(q):
+            GraphStatePrep(graph=q)
+            return qml.probs()
+
+        circuit(q)
+
+    def test_jaxjit_circuit_graph_state_prep(self):
+        """Test if Jax JIT works with GraphStatePrep"""
+        jax = pytest.importorskip("jax")
+
+        lattice = generate_lattice([2, 2], "square")
+        q = QubitGraph(lattice.graph)
+        dev = qml.device("default.qubit")
+
+        @jax.jit
+        @qml.qnode(dev)
+        def circuit(x):
+            GraphStatePrep(q, wires=[0, 1, 2, 3])
+            qml.RX(x, 3)
+            return qml.probs()
+
+        circuit(1.23)
+
+    @pytest.mark.parametrize(
+        "dims, shape, wires",
+        [
+            ([5], "chain", None),
+            ([2, 2], "square", None),
+            ([2, 3], "rectangle", None),
+            ([2, 2, 2], "cubic", None),
+            ([5], "chain", range(5)),
+            ([2, 2], "square", range(4)),
+            ([2, 2], "rectangle", range(4)),
+            (
+                [2, 2, 2],
+                "cubic",
+                range(8),
+            ),
+        ],
+    )
+    def test_circuit_accept_graph_state_prep(self, dims, shape, wires):
+        """Test if a quantum function accepts GraphStatePrep."""
+        lattice = generate_lattice(dims, shape)
+        q = QubitGraph(lattice.graph)
+        dev = qml.device("default.qubit")
+
+        @qml.qnode(dev)
+        def circuit(q):
+            GraphStatePrep(graph=q, wires=wires)
+            return qml.probs()
+
+        res = circuit(q)
+        assert len(res) == 2 ** len(lattice.graph)
+        assert np.isclose(np.sum(res), 1.0, rtol=0)
+
+    @pytest.mark.parametrize(
+        "dims, shape, wires",
+        [
+            ([5], "chain", None),
+            ([2, 2], "square", None),
+            ([2, 3], "rectangle", None),
+            ([2, 2, 2], "cubic", None),
+            ([5], "chain", range(5)),
+            ([2, 2], "square", range(4)),
+            ([2, 2], "rectangle", range(4)),
+            (
+                [2, 2, 2],
+                "cubic",
+                range(8),
+            ),
+        ],
+    )
+    def test_graph_state_prep_creation_with_qubit_graph(self, dims, shape, wires):
+        lattice = generate_lattice(dims, shape)
+        q = QubitGraph(lattice.graph)
+        # Test for object construction via QubitGraph
+        assert_valid(GraphStatePrep(graph=q, wires=wires), skip_deepcopy=True, skip_pickle=True)
+        assert repr(GraphStatePrep(graph=q, wires=wires)) == "GraphStatePrep(Hadamard, CZ)"
+        assert GraphStatePrep(graph=q, wires=wires).label() == "GraphStatePrep(Hadamard, CZ)"
+
+    @pytest.mark.parametrize(
+        "dims, shape, wires",
+        [
+            pytest.param(
+                [5],
+                "chain",
+                None,
+                marks=pytest.mark.xfail(reason="Wires must be specified with nx.Graph objects."),
+            ),
+            ([5], "chain", range(5)),
+            ([2, 2], "square", range(4)),
+            ([2, 2], "rectangle", range(4)),
+            (
+                [2, 2, 2],
+                "cubic",
+                range(8),
+            ),
+        ],
+    )
+    def test_graph_state_prep_creation_with_nx_graph(self, dims, shape, wires):
+        lattice = generate_lattice(dims, shape)
+        # Test for object construction via nx graph
+        assert_valid(
+            GraphStatePrep(graph=lattice.graph, wires=wires), skip_deepcopy=True, skip_pickle=True
+        )
+        assert (
+            repr(GraphStatePrep(graph=lattice.graph, wires=wires)) == "GraphStatePrep(Hadamard, CZ)"
+        )
+        assert (
+            GraphStatePrep(graph=lattice.graph, wires=wires).label()
+            == "GraphStatePrep(Hadamard, CZ)"
+        )
+
+    @pytest.mark.parametrize(
+        "dims, shape, wires",
+        [
+            ([5], "chain", [0, 1, 2, 3, 4]),
+            pytest.param(
+                [5],
+                "chain",
+                None,
+                marks=pytest.mark.xfail(
+                    reason="Wires must be specified when building circuit with nx.Graph objects."
+                ),
+            ),
+            ([2, 2], "square", [0, 1, 2, 3]),
+            ([2, 2], "rectangle", [0, 1, 2, 3]),
+            ([2, 2, 2], "cubic", [0, 1, 2, 3, 4, 5, 6, 7]),
+        ],
+    )
+    def test_circuit_accept_graph_state_prep_with_nx_wires(self, dims, shape, wires):
+        """Test if GraphStatePrep can be created with nx.Graph and user provided wires."""
+        dev = qml.device("default.qubit")
+        lattice = generate_lattice(dims, shape).graph
+
+        @qml.qnode(dev)
+        def circuit(lattice, wires):
+            GraphStatePrep(graph=lattice, wires=wires)
+            return qml.probs()
+
+        res = circuit(lattice, wires)
+        assert len(res) == 2 ** len(lattice)
+        assert np.isclose(np.sum(res), 1.0, rtol=0)
+
+    @pytest.mark.parametrize(
+        "dims, shape, expected",
+        [
+            ([5], "chain", 9),
+            ([2, 2], "square", 8),
+            ([2, 3], "rectangle", 13),
+            ([1, 2], "triangle", 9),
+            ([2, 1], "honeycomb", 21),
+            ([2, 2, 2], "cubic", 20),
+        ],
+    )
+    def test_compute_decompose(self, dims, shape, expected):
+        """Test the compute_decomposition method of the GraphStatePrep class."""
+        lattice = generate_lattice(dims, shape)
+        q = QubitGraph(lattice.graph)
+        wires = set(lattice.graph)
+        queue = GraphStatePrep.compute_decomposition(wires=range(len(wires)), graph=q)
+        assert len(queue) == expected
+
+    @pytest.mark.parametrize(
+        "one_qubit_ops, two_qubit_ops",
+        [
+            (qml.H, qml.CZ),
+            (qml.X, qml.CNOT),
+        ],
+    )
+    def test_decompose(self, one_qubit_ops, two_qubit_ops):
+        """Test the decomposition method of the GraphStatePrep class."""
+        lattice = generate_lattice([2, 2, 2], "cubic")
+        q = QubitGraph(lattice.graph)
+        op = GraphStatePrep(graph=q, one_qubit_ops=one_qubit_ops, two_qubit_ops=two_qubit_ops)
+        queue = op.decomposition()
+        assert len(queue) == 20  # 8 ops for |0> -> |+> and 12 ops to entangle nearest qubits
+        for op in queue[:8]:
+            assert op.name == one_qubit_ops(0).name
+            assert isinstance(op.wires[0], QubitGraph)
+        for op in queue[8:]:
+            assert op.name == two_qubit_ops.name
+            assert all(isinstance(w, QubitGraph) for w in op.wires)
+
+    @pytest.mark.parametrize(
+        "dims, shape, wires",
+        [
+            ([5], "chain", [0, 1, 2, 3, 4]),
+            ([5], "chain", ["a", 1, 2, "c", 4]),
+            ([2, 2], "square", [0, 1, 2, 3]),
+            ([2, 2], "square", ["d", 1, 2, "f"]),
+            ([2, 3], "rectangle", ["a", "b", "c", "d", "e", "f"]),
+            ([2, 2, 2], "cubic", [0, 1, 2, 3, 4, 5, 6, 7]),
+        ],
+    )
+    def test_decompose_wires(self, dims, shape, wires):
+        """Test the decomposition method of the GraphStatePrep class when wires are provided."""
+        lattice = generate_lattice(dims, shape)
+        q = QubitGraph(lattice.graph)
+
+        # GraphStatePrep built from qubit graph
+        op = GraphStatePrep(graph=q, wires=wires)
+        queue = op.decomposition()
+        for idx, op in enumerate(queue[: len(wires)]):
+            assert op.wires[0] == wires[idx]
+
+        # GraphStatePrep built from nx graph
+        op = GraphStatePrep(graph=lattice.graph, wires=wires)
+        queue = op.decomposition()
+        for idx, op in enumerate(queue[: len(wires)]):
+            assert op.wires[0] == wires[idx]
+
+    @pytest.mark.parametrize(
+        "one_qubit_ops, two_qubit_ops",
+        [
+            (qml.H, qml.CZ),
+            (qml.X, qml.CNOT),
+        ],
+    )
+    def test_wires_graph_mismatch(self, one_qubit_ops, two_qubit_ops):
+        """Test for wire-graph label mismatches."""
+        wires = [0, 1, 2, 3]
+        edges = [(0, 1), (1, 2), (2, 3)]
+        lattice = nx.Graph()
+        lattice.add_nodes_from(wires)
+        lattice.add_edges_from(edges)
+        wires.append(5)
+        with pytest.raises(
+            ValueError,
+            match="Please ensure the length of wires objects match that of labels in graph",
+        ):
+            GraphStatePrep(
+                wires=wires, graph=lattice, one_qubit_ops=one_qubit_ops, two_qubit_ops=two_qubit_ops
+            )
+
+        with pytest.raises(ValueError, match="Please ensure wires is specified."):
+            GraphStatePrep(
+                wires=None, graph=lattice, one_qubit_ops=one_qubit_ops, two_qubit_ops=two_qubit_ops
+            )
+
+        with pytest.raises(
+            ValueError,
+            match="Please ensure the length of wires objects match the number of children in QubitGraph.",
+        ):
+            GraphStatePrep(
+                wires=wires,
+                graph=QubitGraph(lattice),
+                one_qubit_ops=one_qubit_ops,
+                two_qubit_ops=two_qubit_ops,
+            )
diff --git a/tests/ops/functions/test_assert_valid.py b/tests/ops/functions/test_assert_valid.py
index d67c71d55..65356d28c 100644
--- a/tests/ops/functions/test_assert_valid.py
+++ b/tests/ops/functions/test_assert_valid.py
@@ -381,6 +381,9 @@ def test_generated_list_of_ops(class_to_validate, str_wires):
     if class_to_validate.__module__[14:20] == "qutrit":
         pytest.xfail(reason="qutrit ops fail matrix validation")
 
+    if class_to_validate.__module__[10:14] == "ftqc":
+        pytest.skip(reason="skip tests for ftqc ops")
+
     # If you defined a new Operator and this call to `create_op_instance` failed, it might
     # be the fault of the test and not your Operator. Please do one of the following things:
     #   1. Update your Operator to meet PL standards so it passes