Source code for pennylane.ftqc.graph_state_preparation
# 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
[docs]
def make_graph_state(graph, wires, one_qubit_ops=qml.H, two_qubit_ops=qml.CZ):
"""A program-capture compatible way to create a GraphStatePrep template.
We can't capture the graph object in plxpr, so instead, if capture is enabled,
we capture the operations generated in computing the decomposition."""
if qml.capture.enabled():
GraphStatePrep.compute_decomposition(wires, graph, one_qubit_ops, two_qubit_ops)
else:
GraphStatePrep(
graph=graph, wires=wires, one_qubit_ops=one_qubit_ops, two_qubit_ops=two_qubit_ops
)
[docs]
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. The node labels of ``graph`` must be sortable.
one_qubit_ops (Operation): Operator to prepare the initial state of each qubit. Defaults to
:class:`~.pennylane.Hadamard`.
two_qubit_ops (Operation): Operator to entangle nearest qubits. Defaults to
:class:`~.pennylane.CZ`.
wires (Optional[Wires]): Wires the operator applies on. Wires are be mapped 1:1 to the graph
nodes sorted in ascending order. 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
.. details::
:title: A Note on Node Ordering
The graph structures used for defining qubit connectivity are inherently *unordered* data
structures. Mapping the nodes in the graph to the *ordered* sequence of wires can therefore
result in ambiguity. To ensure this mapping is reliable and deterministic, the sequence of
wires is mapped to the list of graph nodes sorted in ascending order.
Consider the following example:
.. code-block:: python3
import networkx as nx
import pennylane as qml
from pennylane.ftqc import GraphStatePrep
dev = qml.device("default.qubit")
@qml.qnode(dev)
def circuit(graph, wires):
GraphStatePrep(graph=graph, one_qubit_ops=qml.H, two_qubit_ops=qml.CZ, wires=wires)
return qml.state()
Defining a graph structure and drawing the circuit shows how the graph node labels have been
mapped to wires:
>>> g1 = nx.Graph([("a", "b"), ("b", "c"), ("c", "d")]) # (a) -- (b) -- (c) -- (d)
>>> print(qml.draw(circuit, level="device")(g1, wires=range(4)))
0: ──H─╭●───────┤ State
1: ──H─╰Z─╭●────┤ State
2: ──H────╰Z─╭●─┤ State
3: ──H───────╰Z─┤ State
In other words, ``GraphStatePrep`` has defined the node-label-to-wire mapping::
{"a": 0, "b": 1, "c": 2, "d": 3}
which corresponds to the graph structure with wire indices::
(0) -- (1) -- (2) -- (3)
as shown in the circuit diagram with ``CZ`` operations applied along each edge in the graph.
Drawing the circuit for a graph with the same structure but with different node labels
gives:
>>> g2 = nx.Graph([("b", "a"), ("a", "c"), ("c", "d")]) # (b) -- (a) -- (c) -- (d)
>>> print(qml.draw(circuit, level="device")(g2, wires=range(4)))
0: ──H─╭Z─╭●────┤ State
1: ──H─╰●─│─────┤ State
2: ──H────╰Z─╭●─┤ State
3: ──H───────╰Z─┤ State
As before, ``GraphStatePrep`` defined the node-label-to-wire mapping to be::
{"a": 0, "b": 1, "c": 2, "d": 3}
but now, this corresponds to the graph structure with wire indices::
(1) -- (0) -- (2) -- (3)
as shown in the circuit diagram.
While these two circuit might appear to be the same, they are indeed distinct for this
sequence of wires, and result in different state vectors. It is therefore important to
remember that the node labels influence how nearest-neighbour wires are interpreted.
"""
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)
[docs]
def label(self, *args, **kwargs) -> str: # pylint: disable=unused-argument
r"""Defines how the graph state preparation is represented in diagrams and drawings.
Args:
*args (Optional[Union[int, str]]): positional arguments for decimals and base_label.
**kwargs (Optional[dict]): keyword arguments for cache.
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})"
[docs]
@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
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 = []
nodes = graph.node_labels if isinstance(graph, QubitGraph) else graph.nodes
try:
sorted_nodes = sorted(nodes)
except TypeError as e:
# Attempting to sort a list with a mix of incompatible types results in a TypeError:
# >>> sorted([0, 'a'])
# TypeError: '<' not supported between instances of 'str' and 'int'
raise TypeError(
"GraphStatePrep requires the node labels of the input graph to be sortable"
) from e
wire_map = dict(zip(sorted_nodes, wires))
edges = graph.edge_labels if isinstance(graph, QubitGraph) else graph.edges
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
_modules/pennylane/ftqc/graph_state_preparation
Download Python script
Download Notebook
View on GitHub