Repository: PennyLaneAI/pennylane
Pull request #6958 (2025-02-12 20:02:16)

[FTQC] Add Lattice class to ftqc module
### 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-84004]

Fault-Tolerant Quantum Computing (FTQC) and Measurement-Based Quantum Computing (MBQC) require efficient representation and manipulation of qubit lattices.  These lattices define the connectivity of qubits, which is crucial for implementing quantum operations and error correction.

**Description of the Change:**

This PR introduces a Lattice class to represent qubit lattices using NetworkX graph objects. The Lattice class provides functionalities to:

Store and access lattice connectivity information.
Create lattices of various shapes (chain, rectangle, honeycomb, triangle, cubic) using the generate_lattice function.
Set and retrieve attributes on nodes (qubits) and edges (connections) to store additional information (e.g., stabilizers, entanglement).
Relabel nodes for indexing and manipulation.
This change enables the representation and manipulation of lattices, laying the groundwork for implementing Cluster State as well as Graph State within PennyLane.

**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:**


github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6958?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.59%. Comparing base [(`b0b77fb`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/b0b77fbf791e0b332ea197cce05bbac529d3f4f1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`8306156`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/83061564b18e965d2b0b8aae54e14858038e0546?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    #6958   +/-   ##
=======================================
  Coverage   99.59%   99.59%           
=======================================
  Files         481      482    +1     
  Lines       45784    45841   +57     
=======================================
+ Hits        45599    45656   +57     
  Misses        185      185           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6958?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).


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 5f8b0d5b2..643eacc8e 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -61,6 +61,9 @@
 * `qml.SWAP` now has sparse representation.
   [(#6965)](https://github.com/PennyLaneAI/pennylane/pull/6965)
 
+* A `Lattice` class and a `generate_lattice` method is added to the `qml.ftqc` module. The `generate_lattice` method is to generate 1D, 2D, 3D grid graphs with the given geometric parameters.
+  [(#6958)](https://github.com/PennyLaneAI/pennylane/pull/6958)
+
 * `qml.QubitUnitary` now accepts sparse CSR matrices (from `scipy.sparse`). This allows efficient representation of large unitaries with mostly zero entries. Note that sparse unitaries are still in early development and may not support all features of their dense counterparts.
   [(#6889)](https://github.com/PennyLaneAI/pennylane/pull/6889)
 
@@ -501,4 +504,5 @@ Korbinian Kottmann,
 Christina Lee,
 Mudit Pandey,
 Andrija Paurevic,
+Shuli Shu,
 David Wierichs
diff --git a/pennylane/ftqc/__init__.py b/pennylane/ftqc/__init__.py
index abd8a2124..d33458cd7 100644
--- a/pennylane/ftqc/__init__.py
+++ b/pennylane/ftqc/__init__.py
@@ -25,6 +25,7 @@ Modules
     :toctree: api
 
 """
+from pennylane import ExperimentalWarning
 from .parametric_midmeasure import (
     ParametricMidMeasureMP,
     XMidMeasureMP,
@@ -35,8 +36,10 @@ from .parametric_midmeasure import (
     measure_z,
     diagonalize_mcms,
 )
+from .lattice import Lattice, generate_lattice
 
 __all__ = [
+    "Lattice",
     "ParametricMidMeasureMP",
     "XMidMeasureMP",
     "YMidMeasureMP",
@@ -45,4 +48,5 @@ __all__ = [
     "measure_y",
     "measure_z",
     "diagonalize_mcms",
+    "generate_lattice",
 ]
diff --git a/pennylane/ftqc/lattice.py b/pennylane/ftqc/lattice.py
new file mode 100644
index 000000000..1134ed93d
--- /dev/null
+++ b/pennylane/ftqc/lattice.py
@@ -0,0 +1,167 @@
+# 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=protected-access, inconsistent-return-statements
+"""
+This file defines classes and functions for creating lattice objects that store topological
+connectivity information.
+"""
+
+from collections.abc import Sequence
+from enum import Enum, auto
+from functools import lru_cache
+
+import networkx as nx
+
+
+class Lattice:
+    """Represents a qubit lattice structure.
+
+    This Lattice class, inspired by the design of :class:`~pennylane.spin.Lattice`, leverages `NetworkX` to represent the relationships within the lattice structure.
+
+        Args:
+            lattice_shape: Name of the lattice shape.
+            graph (nx.Graph): A NetworkX undirected graph object. If provided, `nodes` and `edges` are ignored.
+            nodes (List): Nodes to construct a graph object. Ignored if `graph` is provided.
+            edges (List): Edges to construct the graph. Ignored if `graph` is provided.
+        Raises:
+            ValueError: If neither `graph` nor both `nodes` and `edges` are provided.
+    """
+
+    # TODOs: To support braiding operations, Lattice should support nodes/edges addition/deletion.
+
+    def __init__(
+        self, lattice_shape: str, graph: nx.Graph = None, nodes: list = None, edges: list = None
+    ):
+        self._lattice_shape = lattice_shape
+        if graph is None:
+            if nodes is None and edges is None:
+                raise ValueError(
+                    "Neither a networkx Graph object nor nodes together with edges are provided."
+                )
+            self._graph = nx.Graph()
+            self._graph.add_nodes_from(nodes)
+            self._graph.add_edges_from(edges)
+        else:
+            self._graph = graph
+
+    @property
+    def shape(self) -> str:
+        r"""Returns the lattice shape name."""
+        return self._lattice_shape
+
+    def get_neighbors(self, node):
+        r"""Returns the neighbors of a given node in the lattice.
+
+        Args:
+            node: a target node label.
+        """
+        return self._graph.neighbors(node)
+
+    @property
+    def nodes(self):
+        r"""Returns all nodes in the lattice."""
+        return self._graph.nodes
+
+    @property
+    def edges(self):
+        r"""Returns all edges in the lattice."""
+        return self._graph.edges
+
+    @property
+    def graph(self) -> nx.Graph:
+        r"""Returns the underlying NetworkX graph object representing the lattice."""
+        return self._graph
+
+
+class LatticeShape(Enum):
+    """Enum to define valid set of lattice shape supported."""
+
+    chain = auto()
+    square = auto()
+    rectangle = auto()
+    triangle = auto()
+    honeycomb = auto()
+    cubic = auto()
+
+
+# map between lattice name and dimensions
+_LATTICE_DIM_MAP = {
+    "chain": 1,
+    "square": 2,
+    "rectangle": 2,
+    "cubic": 3,
+    "triangle": 2,
+    "honeycomb": 2,
+}
+
+# map between lattice name and networkx method name
+_LATTICE_GENERATOR_MAP = {
+    "chain": "grid_graph",
+    "square": "grid_graph",
+    "rectangle": "grid_graph",
+    "cubic": "grid_graph",
+    "triangle": "triangular_lattice_graph",
+    "honeycomb": "hexagonal_lattice_graph",
+}
+
+
+@lru_cache
+def _supported_shapes():
+    r"""Return the supported shape in str"""
+    return [shape.name for shape in LatticeShape]
+
+
+def generate_lattice(dims: Sequence[int], lattice: str) -> Lattice:
+    r"""Generates a :class:`~pennylane.ftqc.Lattice` object with a given geometric parameters and its shape name.
+
+    Args:
+        dims(List[int]): Geometric parameters for lattice generation. For lattices generated by `nx.grid_graph` ( ``'chain'``, ``'rectangle'``,  ``'square'``, ``'cubic'``),
+        `dims` contains the number of nodes in the each direction of grid. Per ``'honeycomb'`` or ``'triangle'``, the generated lattices will have dims[0] rows and dims[1]
+        columns of hexagons or triangles.
+        lattice (str): Shape of the lattice. Input values can be ``'chain'``, ``'square'``, ``'rectangle'``, ``'honeycomb'``, ``'triangle'``, ``'cubic'``.
+
+    Returns:
+        a :class:`~pennylane.ftqc.Lattice` object.
+
+    Raises:
+        ValueError: If the lattice shape is not supported or the dimensions are invalid.
+    """
+
+    lattice_shape = lattice.strip().lower()
+    supported_shapes = _supported_shapes()
+
+    if lattice_shape not in supported_shapes:
+        raise ValueError(
+            f"Lattice shape, '{lattice}' is not supported."
+            f"Please set lattice to: {supported_shapes}."
+        )
+
+    if _LATTICE_DIM_MAP[lattice_shape] != len(dims):
+        raise ValueError(
+            f"For a {lattice_shape} lattice, the length of dims should be {_LATTICE_DIM_MAP[lattice_shape]} instead of {len(dims)}"
+        )
+
+    lattice_generate_method = getattr(nx, _LATTICE_GENERATOR_MAP[lattice_shape])
+
+    if _LATTICE_GENERATOR_MAP[lattice_shape] == "grid_graph":
+        lattice_obj = Lattice(lattice_shape, lattice_generate_method(dims))
+        return lattice_obj
+
+    if _LATTICE_GENERATOR_MAP[lattice_shape] in [
+        "triangular_lattice_graph",
+        "hexagonal_lattice_graph",
+    ]:
+        lattice_obj = Lattice(lattice_shape, lattice_generate_method(dims[0], dims[1]))
+        return lattice_obj
+
+    raise NotImplementedError  # pragma: no cover


Test patch:
diff --git a/tests/ftqc/test_ftqc_lattice.py b/tests/ftqc/test_ftqc_lattice.py
new file mode 100644
index 000000000..027986edb
--- /dev/null
+++ b/tests/ftqc/test_ftqc_lattice.py
@@ -0,0 +1,129 @@
+# 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 lattice module"""
+
+import networkx as nx
+import pytest
+
+from pennylane.ftqc import Lattice, generate_lattice
+
+
+class TestLattice:
+    """Test for the qml.ftqc.Lattice class."""
+
+    def test_lattice_creation_with_graph(self):
+        """Test for Lattice object created by a nx.Graph object."""
+        graph = nx.Graph([(0, 1), (1, 2)])
+        lattice = Lattice("test", graph=graph)
+        assert lattice.shape == "test"
+        assert len(lattice.nodes) == 3
+        assert len(lattice.edges) == 2
+
+    def test_lattice_creation_with_nodes_and_edges(self):
+        """Test for Lattice object created by a list of nodes and edges."""
+        nodes = [0, 1, 2]
+        edges = [(0, 1), (1, 2)]
+        lattice = Lattice("test", nodes=nodes, edges=edges)
+        assert len(lattice.nodes) == 3
+        assert len(lattice.edges) == 2
+
+    def test_lattice_creation_invalid(self):
+        """Test for Lattice object created with lattice name only. ValueError will be raised as neither graph nor nodes/edges provided"""
+        with pytest.raises(ValueError, match="Neither a networkx Graph object nor nodes together"):
+            Lattice("test")
+
+    def test_get_lattice_shape(self):
+        """Test for get_lattice_shape()."""
+        lattice = Lattice("test_shape", nx.Graph())
+        assert lattice.shape == "test_shape"
+
+    def test_get_neighbors(self):
+        """Test for getting the neighbors of a node."""
+        graph = nx.grid_graph([3, 3])
+        lattice = Lattice("rectangle", graph)
+        assert set(lattice.get_neighbors((1, 1))) == set(graph.neighbors((1, 1)))
+
+    def test_get_nodes(self):
+        """Test for getting nodes."""
+        nodes = [0, 1, 2]
+        edges = [(0, 1), (1, 2)]
+        lattice = Lattice("test", nodes=nodes, edges=edges)
+        assert set(lattice.nodes) == set(nodes)
+
+    def test_get_edges(self):
+        """Test for getting edges."""
+        edges = [(0, 1), (1, 2)]
+        lattice = Lattice("test", nodes=[0, 1, 2], edges=edges)
+        assert set(lattice.edges) == set(edges)
+
+    def test_get_graph(self):
+        """Test for getting graph."""
+        graph = nx.Graph([(0, 1), (1, 2)])
+        lattice = Lattice("test", graph=graph)
+        assert lattice.graph is graph
+
+
+class TestGenerateLattice:
+    """Test for generate_lattice method."""
+
+    def test_generate_chain_lattice(self):
+        """Test to generate a 1D chain lattice."""
+        lattice = generate_lattice([5], "chain")
+        assert isinstance(lattice, Lattice)
+        assert len(lattice.nodes) == 5
+
+    def test_generate_rectangle_lattice(self):
+        """Test to generate a 2D rectangle lattice."""
+        lattice = generate_lattice([3, 4], "rectangle")
+        assert isinstance(lattice, Lattice)
+        assert len(lattice.nodes) == 12
+
+    def test_generate_cubic_lattice(self):
+        """Test to generate a 3D cubic lattice."""
+        lattice = generate_lattice([2, 2, 2], "cubic")
+        assert isinstance(lattice, Lattice)
+        assert len(lattice.nodes) == 8
+
+    def test_generate_triangle_lattice(self):
+        """Test to generate a 2D triangle lattice."""
+        lattice = generate_lattice([3, 4], "triangle")
+        assert isinstance(lattice, Lattice)
+        assert len(lattice.nodes) == 12
+
+    def test_generate_honeycomb_lattice(self):
+        """Test to generate a 2D honeycomb lattice."""
+        lattice = generate_lattice([1, 4], "honeycomb")
+        assert isinstance(lattice, Lattice)
+        assert len(lattice.nodes) == 18
+
+    def test_generate_invalid_lattice_shape(self):
+        """Test for an unsupported lattice shape."""
+        with pytest.raises(ValueError):
+            generate_lattice([2, 2], "invalid_shape")
+
+    @pytest.mark.parametrize(
+        "dims, shape",
+        [
+            ([2, 2], "chain"),
+            ([2], "rectangle"),
+            ([2, 2], "cubic"),
+            ([2, 2, 2], "triangle"),
+            ([2], "honeycomb"),
+        ],
+    )
+    def test_generate_invalid_dimensions(self, dims, shape):
+        """Test for an incorrect dims input."""
+        with pytest.raises(ValueError, match="the length of dims should be"):
+            generate_lattice(dims, shape)