Source code for flamingpy.codes.graphs.egraph
# Copyright 2022 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.
"""A class for representing quantum graph states."""
# pylint: disable=import-outside-toplevel
from typing import Union, Optional
import warnings
import networkx as nx
import numpy as np
from flamingpy.utils.linalg import are_lc_equivalent
def macronize(can_graph, pad_boundary=False, disp=0.1):
"""Create a macronode graph out of canonical graph can_graph.
Assume can_graph represents a 'canonical' or 'reduced' graph state.
Replace each vertex v in can_graph with a macronode (a collection of n
vertices, where n is the size of the neighborhood of v). This is
achieved by replacing each pair of vertices in can_graph connected by an
edge with a 'dumbbell'.
Assumes that: (1) the nodes of G are three-tuples; (2) edges associated
with periodic boundary conditions, if they exist, have the attribute
'periodic' set to True.
If there is a list of perfect qubits stored as a graph attribute
(specifically, can_graph.graph["perfect_points"]), this list is updated to
reflect the new perfect qubits in the macronode graph.
Args:
can_graph (nx.Graph): the graph to macronize. All graph, node, and
edge attributes of can_graph are preserved. Will access and process
can_graph.graph["perfect_points"] if it exists.
disp (float, optional): how much to displace the nodes
within each macronode from the central vertex. This number
should be small and positive, and no larger than 0.5.
pad_boundary (bool, optional): if True, pad each boundary
macronode to have the same number of nodes as a bulk
macronode. For now, this only works for the RHG lattice
connectivity (i.e. pad to 4 nodes).
Returns:
nx.Graph: the macronized graph, with a macronode-to-micronode
dictionary stored as a graph attribute.
"""
if disp >= 0.5 or disp < 0:
raise ValueError("Please set disp to a positive value strictly less than 0.5.")
macro_graph = nx.Graph(**can_graph.graph)
# The macronode-to-micronode dictionary
macro_dict = {}
for edge in can_graph.edges:
old_point_1, old_point_2 = np.array(edge[0]), np.array(edge[1])
direction_vec = old_point_2 - old_point_1
distance = np.linalg.norm(direction_vec)
periodic_flip = -1 if can_graph.edges[edge].get("periodic") else 1
shortened_vec = periodic_flip * disp * direction_vec / distance
shortened_vec = np.round(shortened_vec, -int(np.log10(disp)) + 2)
new_point_1 = tuple(old_point_1 + shortened_vec)
new_point_2 = tuple(old_point_2 - shortened_vec)
macro_graph.add_node(new_point_1, **can_graph.nodes[edge[0]])
macro_graph.add_node(new_point_2, **can_graph.nodes[edge[1]])
macro_graph.add_edge(new_point_1, new_point_2, **can_graph.edges[edge])
# Add to the macronode-to-micronode dictionary
if not macro_dict.get(edge[0]):
macro_dict[edge[0]] = []
if not macro_dict.get(edge[1]):
macro_dict[edge[1]] = []
macro_dict[edge[0]].append(new_point_1)
macro_dict[edge[1]].append(new_point_2)
if pad_boundary:
for node in can_graph:
macro_size = len(macro_dict[node])
if macro_size < 4:
n_new = 4 - macro_size
for i in range(n_new):
new_node = list(node)
new_node[i] = new_node[i] + 0.05
new_node_tup = tuple(new_node)
macro_graph.add_node(new_node_tup, **can_graph.nodes[node])
macro_dict[node].append(new_node_tup)
macro_graph.graph["macro_dict"] = macro_dict
old_perfect_points = can_graph.graph.get("perfect_points")
if old_perfect_points:
new_perfect_qubits = [macro_dict[point] for point in old_perfect_points]
macro_graph.graph["perfect_points"] = [a for b in new_perfect_qubits for a in b]
return macro_graph
[docs]class EGraph(nx.Graph):
"""An enhanced graph for representing quantum graph states.
A class that builds on a NetworkX graph to better represent graph states.
Includes indexing, drawing, and convenience methods.
Attributes:
macro_to_micro (dict): if macronodes is set to True, the macro_dict
object from the underlying graph (None or a dictionary of the form
{central coordinate of macronode: [all micronode coordinates]})
to_indices (dict): if self.index_generator() has been run,
a dictionary of the form {points: indices}
to_points (dict): if self.index_generator() has been run,
a dictionary of the form {indices: points}
adj_mat (np.array): if self.adj_generator() has been run,
the adjacency matrix of the graph.
"""
def __init__(self, *args, indexer="default", macronodes=False, **kwargs):
super().__init__(*args, **kwargs)
self._macronodes = macronodes
self.macro_to_micro = None
if macronodes:
self.macro_to_micro = self.graph.get("macro_dict")
self.to_indices = None
self.to_points = None
self.adj_mat = None
[docs] def index_generator(self):
"""Generate indices for the nodes of self.
Set the to_indices and to_points attribute of self with points-
to-indices and indices-to-points dictionaries, respectively.
Indices are generated using the built-in sorted function (in
case of macronodes, the central/integer coordinates are sorted).
"""
# TODO: User-specified index mapping?
N = self.order()
if self.to_indices is not None:
return self.to_indices
if not self._macronodes:
ind_dict = dict(zip(sorted(self.nodes()), range(N)))
elif self._macronodes:
sorted_macro = sorted(self.macro_to_micro)
points = []
for vertex in sorted_macro:
points += self.macro_to_micro[vertex]
ind_dict = {points[i]: i for i in range(N)}
self.to_indices = ind_dict
self.to_points = {index: point for point, index in ind_dict.items()}
return ind_dict
[docs] def adj_generator(self, sparse=True):
"""Return the correctly indexed adjacency matrix of the graph and set
the self.adj_mat attribute.
Calling the NetworkX adjacency matrix methods with default
options may create a mismatch between the indices of the
rows/columns of the matrix and the indices generated by
self.index_generator(). This function demands that the indices
match.
"""
if self.adj_mat is not None:
return self.adj_mat
if self.to_points is None:
self.index_generator()
sorted_nodes = [self.to_points[i] for i in range(self.order())]
# TODO: Reconsider data type for more intricate weights.
if sparse:
adj = nx.to_scipy_sparse_array(self, nodelist=sorted_nodes, dtype=np.int8)
else:
adj = nx.to_numpy_array(self, nodelist=sorted_nodes, dtype=np.int8)
self.adj_mat = adj
return adj
[docs] def slice_coords(self, plane, number):
"""Obtain all the coordinates in an x, y, or z slice of self.
Args:
plane (str): 'x', 'y', or 'z', denoting the slice direction
number (int): the index of the slice. The allowable range is from 0
to the total number of slices in the given direction.
Returns:
list of tuples: the coordinates of the slice.
"""
plane_dict = {"x": 0, "y": 1, "z": 2}
plane_ind = plane_dict[plane]
coords = [point for point in self.nodes if point[plane_ind] == number]
return coords
[docs] def macronize(self, pad_boundary=False, disp=0.1):
"""Return a new, macronized version of self.
See egraph.macronize for more details.
"""
return EGraph(macronize(self, pad_boundary, disp), macronodes=True)
[docs] def draw(self, backend="matplotlib", **kwargs):
"""Draw the graph state with Matplotlib.
See flamingpy.utils.viz.draw_EGraph for more details.
"""
from flamingpy.utils.viz import draw_EGraph
return draw_EGraph(self, backend=backend, **kwargs)
[docs] def draw_adj(self, **kwargs):
"""Draw the adjacency matrix with matplotlib.
See flamingpy.utils.viz.plot_mat_heat_map for more details.
"""
from flamingpy.utils.viz import plot_mat_heat_map
adj = self.adj_generator(sparse=False)
return plot_mat_heat_map(adj, **kwargs)
[docs] def add_qubit(
self,
qubit: Optional[tuple] = None,
neighbors: Optional[list] = None,
add_to_macronode: Optional[bool] = None,
) -> None:
"""Add qubit to EGraph connected to neighbors.
Args:
qubit (tuple[int, int, int] or None): qubit to add. If a 3-tuple,
the qubit is added in that position. If qubit is None, the qubit is
positioned one unit further than the maximum position in the x
direction, in position (x_max + 1, 0, 0).
neighbors (list[int], list[tuple], or None): neighbors of qubit specified
with indices or positions.
add_to_macronode (bool, optional): if True, add qubit to a macronode.
The qubit must be a tuple that rounds to an integer tuple corresponding
to the macronode.
"""
# Makes sure that input qubit value and type is supported
if isinstance(qubit, tuple):
if len(qubit) != 3:
raise ValueError(
"The position should be a 3-tuple, but it was given a "
+ f"{len(qubit)}D tuple."
)
if qubit in self:
raise ValueError(
f"Could not add qubit because there is already a node in position {qubit}."
)
elif qubit is None:
x_max = max(map(lambda tup: tup[0], self.nodes()))
qubit = (x_max + 1, 0, 0)
else:
raise TypeError(
"Qubit type not supported. Excepted 3-tuple or None, but was"
+ f" given {type(qubit)}"
)
if add_to_macronode and self.macro_to_micro is None:
raise ValueError(
"Cannot add a qubit to macronode because current EGraph is not macronized."
)
# Add node
self.add_node(qubit)
# Update dictionaries when adding qubit
self._update_attributes_add_qubit(qubit, add_to_macronode)
# Update neighbors
if neighbors is not None:
if len(neighbors) != 0:
if isinstance(neighbors[0], int):
if self.to_points is None:
self.index_generator()
neighborhood = [(self.to_points[ind], qubit) for ind in neighbors]
elif isinstance(neighbors[0], tuple):
neighborhood = [(tup, qubit) for tup in neighbors]
else:
raise TypeError(
"Unsupported type of neighbors. Expected tuples but"
f" {type(neighbors[0])} was given. Type int can only be used when "
"macro is False."
)
self.add_edges_from(neighborhood)
self.adj_mat = None
def _update_attributes_add_qubit(self, qubit, add_to_macro):
"""Update self.macro_to_micro, self.to_points, and self.to_indices."""
# This method reduces the complexity of self.add_qubit
# Add qubit to macro_to_micro dictionary
if add_to_macro:
macronode = tuple(round(i) for i in qubit)
if macronode in self.macro_to_micro:
self.macro_to_micro[macronode].append(qubit)
else:
self.macro_to_micro[macronode] = [qubit]
# Update dictionaries
if self.to_indices is not None:
new_index = max(self.to_points) + 1
self.to_points[new_index] = qubit
self.to_indices[qubit] = new_index
[docs] def remove_qubit(self, qubit: Union[tuple, int]) -> None:
"""Remove qubit from EGraph.
Args:
qubit (tuple[int, int, int] or int): if 3-tuple, remove qubit at that
position. If int and index/point dictionary is available, remove
qubit at that index.
"""
# Remove qubit if dictionaries are not initialized
if self.to_indices is None:
if isinstance(qubit, tuple):
self.remove_node(qubit)
else:
raise ValueError(
"Cannot remove qubit at given index because indices have not been generated."
)
# Remove qubit if dictionaries are initialized
else:
if not isinstance(qubit, (int, tuple)):
raise TypeError(
"Qubit type not supported. Excepted 3-tuple, int, or None, "
+ f"but was given {type(qubit)}."
)
qubit = self.to_points[qubit] if isinstance(qubit, int) else qubit
self.remove_node(qubit)
self.to_indices.pop(qubit)
self.to_points = {v: k for k, v in self.to_indices.items()}
# Remove qubit from macro_to_micro dict if EGraph is macronized
macro_dict = self.macro_to_micro if self.macro_to_micro is not None else {}
for k in set(macro_dict):
if qubit in macro_dict[k]:
macro_dict[k].remove(qubit)
# If macronode is empty, then delete it from macro_to_micro
if not macro_dict[k]:
del macro_dict[k]
self.macro_to_micro = macro_dict if self.macro_to_micro is not None else None
self.adj_mat = None
[docs] def is_lc_equivalent(self, graph2, clifford_form="tensor"):
"""Check if two EGraphs are LC equivalent, and return the Clifford
operation if so. Implemented as in arXiv:quant-ph/0405023.
Args:
graph2 (EGraph): the graph to check Clifford equivalence against.
clifford_form: a string describing the output form of local Clifford operation, if
it exists.
If 'tensor' (default), produce a list of length n of 2x2 numpy arrays corresponding
to single-qubit tensor factors.
If 'global', return a single 2nx2n numpy array corresponding to the global operator
acting on all n qubits.
Returns:
(bool, numpy.array): whether the states are LC equivalent, and if they are, the local
Clifford output according to 'clifford_form' specification.
"""
# wrapper using utils function from utils/linalg.py module
return are_lc_equivalent(self, graph2, clifford_form)
_modules/flamingpy/codes/graphs/egraph
Download Python script
Download Notebook
View on GitHub