Module optgraphstate.graph_tools
Expand source code
import itertools
import random
import numpy as np
import igraph as ig
def get_graph_from_edges(edges):
"""
Generate an [`igraph.Graph`](https://python.igraph.org/en/stable/api/igraph.Graph.html) object from the list of edges.
Parameters
----------
edges : list of 2-tuple of int
List of edges that form the graph, where each integer indicates a
vertex label.
Returns
-------
graph : igraph.Graph
Generated graph.
"""
return ig.Graph(edges=edges)
def get_sample_graph(shape, *prms):
"""
Generate a predefined graph with a given shape and parameters.
See the description of `optgraphstate.GraphState.__init__()`.
Returns
-------
graph : igraph.Graph
Generated graph.
"""
if shape == 'random':
assert len(prms) in [2, 3]
if len(prms) == 3:
random.seed(prms[2])
g = ig.Graph.Erdos_Renyi(n=prms[0], m=prms[1])
random.seed()
elif shape == 'complete':
assert len(prms) == 1
g = ig.Graph.Full(n=prms[0])
elif shape == 'star':
assert len(prms) == 1
g = ig.Graph.Star(prms[0])
elif shape == 'linear':
g = ig.Graph.Lattice(dim=prms, circular=False)
elif shape == 'cycle':
g = ig.Graph.Ring(prms[0])
elif shape == 'lattice':
g = ig.Graph.Lattice(dim=prms, circular=False)
# graph_info['size'] = tuple(prms)
elif shape == 'tree':
# graph_info['num_children'] = tuple(prms)
g: ig.Graph = ig.Graph.Star(prms[0] + 1)
parents_start = 1
n_parents = prms[0]
for n_children in prms[1:]:
for parent in range(parents_start, parents_start + n_parents):
g.add_vertices(n_children)
vcount = g.vcount()
g.add_edges([(parent, child) for child in
range(vcount - n_children, vcount)])
parents_start += n_parents
n_parents *= n_children
elif shape == 'rhg':
assert len(prms) == 3
g = _get_rhg_lattice(*prms)
# g['size'] = tuple(prms)
elif shape == 'repeater':
assert len(prms) == 1
g = get_sample_graph('complete', 2 * prms[0])
vcount = g.vcount()
for v in range(vcount):
new_v = g.add_vertex()
g.add_edge(v, new_v)
# g['m'] = prms[0]
elif shape == 'parity_encoding':
assert len(prms) in [3, 4]
g = _get_parity_encoded_graph(*prms)
# g['logical_graph'] = prms[0]
# g['n'] = prms[1]
# g['m'] = prms[2]
elif shape == 'ptqc':
assert len(prms) == 4
g = _get_ptqc_graph(*prms)
# g['n'] = prms[0]
# g['m'] = prms[1]
# g['HIC'] = prms[2]
# g['central'] = prms[3]
else:
raise ValueError("Unsupported shape")
return g
def find_nonoverlapping_bcss(g: ig.Graph, get_name=False):
"""
Find a maximum set of bipartitely-complete subgraphs (BCSs) that do not
share any vertices.
A *maximum* set means that it cannot be enlarged by adding another BCS.
The obtained set may vary each time the function is called since the
iterations of vertices are randomized by `numpy.random`.
Parameters
----------
g : igraph.Graph
Traget graph.
get_name : bool (default: False)
Whether to get the names or indices of vertices.
Returns
-------
bcss : list of 2-tuple of list of {int or str}
Each element corresponds to a BCS found and has the structure of
([v1, v2, ...], [u1, u2, ...]), where v1, v2, ... are the
indices/names of the vertices in one part of the BCS and
u1, u2, ... are the indices/names of the vertices in another part.
"""
bcss = []
vids_in_bcs = set() # Vertices that are already contained in a bcs
edges_checked = set() # Edges that are not contained in any bcs
vids = np.arange(g.vcount())
np.random.shuffle(vids)
for vid in vids:
if vid in vids_in_bcs:
continue
ngh_vids = g.neighbors(vid)
np.random.shuffle(ngh_vids)
for ngh_vid1 in ngh_vids:
if ngh_vid1 in vids_in_bcs or (vid, ngh_vid1) in edges_checked:
continue
for ngh_vid2 in ngh_vids:
if ngh_vid1 >= ngh_vid2 or ngh_vid2 in vids_in_bcs or (
vid, ngh_vid2) in edges_checked:
continue
part1 = set(g.neighbors(ngh_vid1)) & set(g.neighbors(ngh_vid2))
if len(part1) == 1:
continue
marked = False
for part1_vid in part1:
if part1_vid in vids_in_bcs:
marked = True
break
if marked:
continue
part1_neighbors = [set(g.neighbors(vid)) for vid in part1]
part2 = set.intersection(*part1_neighbors)
marked = False
for part2_vid in part2:
if part2_vid in vids_in_bcs:
marked = True
break
if marked:
continue
if get_name:
bcss.append((g.vs[part1]['name'], g.vs[part2]['name']))
else:
bcss.append((part1, part2))
vids_in_bcs.update(part1)
vids_in_bcs.update(part2)
for ngh_vid in ngh_vids:
edges_checked.update({(vid, ngh_vid), (ngh_vid, vid)})
return bcss
def find_nonoverlapping_cliques(g: ig.Graph, get_name=False):
"""
Find a maximum set of cliques that do not share any vertices.
A *maximum* set means that it cannot be enlarged by adding another clique.
The obtained set may vary each time the function is called since the
iterations of vertices are randomized by `numpy.random`.
Parameters
----------
g : igraph.Graph
Target graph.
get_name : bool (default: False)
Whether to get the names or indices of vertices.
Returns
-------
cliques : list of set of {int or str}
Each element is the set of the indices/names of vertices in a clique.
"""
cliques = g.maximal_cliques(min=3)
cliques = [set(clique) for clique in cliques]
# num_cliques = len(cliques)
# Remove overlapping cliques
np.random.shuffle(cliques)
nonoverlapping_cliques = []
all_vids = set()
for clique in cliques:
if not (clique & all_vids):
nonoverlapping_cliques.append(clique)
all_vids.update(clique)
cliques = nonoverlapping_cliques
if get_name:
cliques = [{g.vs[vid]['name'] for vid in clique} for clique in cliques]
return cliques
# def get_all_vertices(graph):
# """
# Get all the vertex names of a graph.
#
# Parameters
# ----------
# graph : igraph.Graph
# Target graph.
#
# Returns
# -------
# vertices : list of str
# List of the names of the vertices in `graph`.
# """
#
# return graph.vs['name']
#
#
# def get_adjacency(graph):
# """
# Get the adjacency matrix of a graph.
#
# Parameters
# ----------
# graph : igraph.Graph
# Target graph.
#
# Returns
# -------
# adjacency : numpy.ndarray
# Adjacency matrix of `graph`.
# """
# adj = graph.get_adjacency()
# return np.array(list(adj))
#
#
# def get_vertex_attrs(graph, vertex):
# """
# Get the attributes of a vertex of a graph.
#
# Parameters
# ----------
# graph : igraph.Graph
# Graph that the vertex belongs to.
#
# vertex : str or int
# Name of the vertex.
#
# Returns
# -------
# attributes : dict
# Attributes of the vertex.
# """
# vertex = str(vertex)
# attrs = graph.vs.find(name=vertex).attributes()
#
# return attrs
#
#
# def get_neighbors(graph, vertex):
# """
# Get the neighbors of a vertex in a graph.
#
# Parameters
# ----------
# graph: igraph.Graph
# Graph that the vertex belongs to.
#
# vertex: str or int
# Name of the vertex.
#
# Returns
# -------
# neighbors: list of str
# List of the names of the neighbors of the vertex.
# """
#
# vertex = str(vertex)
#
# return graph.vs[graph.neighbors(str(vertex))]['name']
#
#
# def get_all_edges(graph):
# """
# Get all the edges of a graph in terms of pairs of vertex names.
#
# Parameters
# ----------
# graph : igraph.Graph.
# Target graph.
#
# Returns
# -------
# edges : list of 2-tuple of str
# List of the connected pairs of vertex names.
# """
#
# edges = []
# for e in graph.es:
# edges.append((e.source_vertex['name'], e.target_vertex['name']))
# return edges
#
#
# def get_edge_attrs(graph, v1, v2):
# """
# Get the attributes of an edge in a graph.
#
# Parameters
# ----------
# graph : igraph.Graph.
# Graph that the edge belongs to.
# v1, v2 : str or int
# Names of the vertices connected by the edge.
#
# Returns
# -------
# attributes : dict
# Dictionary that contains the attributes of the edge.
# """
# n1 = str(v1)
# n2 = str(v2)
#
# link_attrs = graph.es[graph.get_eid(v1, v2)].attributes()
# return link_attrs
def _connect_vertex_sets(graph, inds1, inds2, **attrs):
if inds1 and inds2:
graph.add_edges(itertools.product(inds1, inds2), attributes=attrs)
def _get_ptqc_graph(n, m, hic, center):
if center:
graph = ig.Graph(2 * n * m + 1, vertex_attrs={"clifford": None})
else:
graph = ig.Graph(3 * n * m, vertex_attrs={"clifford": None})
def build_logical_qubit(lq, inbetween_edge_each_block):
if center:
vid_start = 1 + lq * n * m
else:
vid_start = lq * n * m
if inbetween_edge_each_block:
vids_all_1 = range(vid_start, vid_start + (n - 1) * m + 1, m)
if m > 1:
for vid_x_1 in vids_all_1:
vids_x_not1 = range(vid_x_1 + 1, vid_x_1 + m)
_connect_vertex_sets(graph, [vid_x_1], vids_x_not1)
graph.vs[vids_all_1]['clifford'] = 'H'
return vids_all_1
else:
vids_1_all = range(vid_start, vid_start + m)
if n > 1:
vids_not1_1 = range(vid_start + m,
vid_start + (n - 1) * m + 1,
m)
_connect_vertex_sets(graph, vids_1_all, vids_not1_1)
if m > 1:
for vid_x_1 in vids_not1_1:
vids_x_not1 = range(vid_x_1 + 1, vid_x_1 + m)
_connect_vertex_sets(graph, [vid_x_1], vids_x_not1)
graph.vs[vids_not1_1]['clifford'] = 'H'
return vids_1_all
if hic:
inbetween_edge_each_block = [True, True] if center else [True, False,
False]
else:
inbetween_edge_each_block = [False, False] if center else [True, True,
False]
vids_lqs = [build_logical_qubit(lq, inbetween_edge_each_block[lq]) for lq
in range(2 if center else 3)]
if center:
_connect_vertex_sets(graph, [0], itertools.chain(*vids_lqs))
else:
_connect_vertex_sets(graph, vids_lqs[0], vids_lqs[1])
_connect_vertex_sets(graph, vids_lqs[1], vids_lqs[2])
return graph
def _get_parity_encoded_graph(logical_graph, n, m, orientation=True):
if isinstance(logical_graph, str):
if logical_graph in ['3-star', '3-linear']:
logical_graph = get_sample_graph('star', 3)
elif logical_graph in ['6-ring', '6-cycle']:
logical_graph = get_sample_graph('cycle', 6)
else:
raise ValueError("Unsupported logical graph")
logical_vcount = logical_graph.vcount()
vcount = logical_vcount * n * m
g = ig.Graph(vcount, vertex_attrs={"clifford": None})
# Internal structure of each logical qubit
for vid_1_1 in range(0, vcount, n * m):
# |0_L(1_L)> = (|0>^m + |1>^m)^n \pm (|0>^m - |1>^m)^n
if orientation:
vids_1_all = range(vid_1_1, vid_1_1 + m)
vids_not1_1 = range(vid_1_1 + m, vid_1_1 + n * m, m)
# The first block is connected with the first vertex of each block.
_connect_vertex_sets(g, vids_1_all, vids_not1_1)
# For each block besides the first one, the first vertex is
# connected with the other vertices.
for vid_x_1 in vids_not1_1:
vids_x_not1 = range(vid_x_1 + 1, vid_x_1 + m)
_connect_vertex_sets(g, [vid_x_1], vids_x_not1)
# The first vertex of each block has the Hadamard gate.
g.vs[vids_not1_1]['clifford'] = 'H'
# |0_L(1_L)> = (|+>^m + |->^m)^n
else:
vids_all_1 = range(vid_1_1, vid_1_1 + n * m, m)
for vid_x_1 in vids_all_1:
vids_x_not1 = range(vid_x_1 + 1, vid_x_1 + m)
_connect_vertex_sets(g, [vid_x_1], vids_x_not1)
g.vs[vids_x_not1]['clifford'] = 'H'
# Connection between logical qubits
for logical_edge in logical_graph.es:
logical_vids = logical_edge.source, logical_edge.target
physical_vids = []
for logical_vid in logical_vids:
vid_1_1 = logical_vid * n * m
if orientation:
physical_vids_sng = range(vid_1_1, vid_1_1 + m)
else:
physical_vids_sng = range(vid_1_1, vid_1_1 + n * m, m)
physical_vids.append(physical_vids_sng)
_connect_vertex_sets(g, *physical_vids)
return g
def _get_rhg_lattice(Lx, Ly, Lz):
g = ig.Graph()
size = (2 * Lx, 2 * Ly, 2 * Lz)
def _adjacent_coords(*coords):
for axis, coord in enumerate(coords):
for diff in [1, -1]:
adjacent_vertex = list(coords[:])
adjacent_vertex[axis] += diff
if 0 <= adjacent_vertex[axis] <= size[axis]:
yield tuple(adjacent_vertex)
def duality_condition(x, y, z, primal):
if primal:
return not (x + y + z) % 2 and (
x % 2 or y % 2 or z % 2) # one even, two odds
else:
return (x + y + z) % 2 and not (
x % 2 and y % 2 and z % 2) # two evens, one odd
def add_qubit(x, y, z, primal):
vertex = g.add_vertex(x=x, y=y, z=z, primal=primal)
return vertex
# Add vertices
primal_qubits = []
for x in range(size[0] + 1):
for y in range(size[1] + 1):
for z in range(size[2] + 1):
if duality_condition(x, y, z, True):
new_qubit = add_qubit(x, y, z, True)
primal_qubits.append(new_qubit)
elif duality_condition(x, y, z, False):
add_qubit(x, y, z, False)
# Add edges
get_vertex_by_coords = lambda x, y, z: g.vs.find(x=x, y=y, z=z)
for vertex in primal_qubits:
edges = [(vertex, get_vertex_by_coords(*adj_coords)) for adj_coords in
_adjacent_coords(vertex["x"], vertex["y"], vertex["z"]) if
duality_condition(*adj_coords, False)]
g.add_edges(edges)
return g
Functions
def find_nonoverlapping_bcss(g: igraph.Graph, get_name=False)
-
Find a maximum set of bipartitely-complete subgraphs (BCSs) that do not share any vertices.
A maximum set means that it cannot be enlarged by adding another BCS. The obtained set may vary each time the function is called since the iterations of vertices are randomized by
numpy.random
.Parameters
g
:igraph.Graph
- Traget graph.
get_name
:bool (default: False)
- Whether to get the names or indices of vertices.
Returns
bcss
:list
of2-tuple
oflist
of{int
orstr}
- Each element corresponds to a BCS found and has the structure of ([v1, v2, …], [u1, u2, …]), where v1, v2, … are the indices/names of the vertices in one part of the BCS and u1, u2, … are the indices/names of the vertices in another part.
Expand source code
def find_nonoverlapping_bcss(g: ig.Graph, get_name=False): """ Find a maximum set of bipartitely-complete subgraphs (BCSs) that do not share any vertices. A *maximum* set means that it cannot be enlarged by adding another BCS. The obtained set may vary each time the function is called since the iterations of vertices are randomized by `numpy.random`. Parameters ---------- g : igraph.Graph Traget graph. get_name : bool (default: False) Whether to get the names or indices of vertices. Returns ------- bcss : list of 2-tuple of list of {int or str} Each element corresponds to a BCS found and has the structure of ([v1, v2, ...], [u1, u2, ...]), where v1, v2, ... are the indices/names of the vertices in one part of the BCS and u1, u2, ... are the indices/names of the vertices in another part. """ bcss = [] vids_in_bcs = set() # Vertices that are already contained in a bcs edges_checked = set() # Edges that are not contained in any bcs vids = np.arange(g.vcount()) np.random.shuffle(vids) for vid in vids: if vid in vids_in_bcs: continue ngh_vids = g.neighbors(vid) np.random.shuffle(ngh_vids) for ngh_vid1 in ngh_vids: if ngh_vid1 in vids_in_bcs or (vid, ngh_vid1) in edges_checked: continue for ngh_vid2 in ngh_vids: if ngh_vid1 >= ngh_vid2 or ngh_vid2 in vids_in_bcs or ( vid, ngh_vid2) in edges_checked: continue part1 = set(g.neighbors(ngh_vid1)) & set(g.neighbors(ngh_vid2)) if len(part1) == 1: continue marked = False for part1_vid in part1: if part1_vid in vids_in_bcs: marked = True break if marked: continue part1_neighbors = [set(g.neighbors(vid)) for vid in part1] part2 = set.intersection(*part1_neighbors) marked = False for part2_vid in part2: if part2_vid in vids_in_bcs: marked = True break if marked: continue if get_name: bcss.append((g.vs[part1]['name'], g.vs[part2]['name'])) else: bcss.append((part1, part2)) vids_in_bcs.update(part1) vids_in_bcs.update(part2) for ngh_vid in ngh_vids: edges_checked.update({(vid, ngh_vid), (ngh_vid, vid)}) return bcss
def find_nonoverlapping_cliques(g: igraph.Graph, get_name=False)
-
Find a maximum set of cliques that do not share any vertices.
A maximum set means that it cannot be enlarged by adding another clique. The obtained set may vary each time the function is called since the iterations of vertices are randomized by
numpy.random
.Parameters
g
:igraph.Graph
- Target graph.
get_name
:bool (default: False)
- Whether to get the names or indices of vertices.
Returns
cliques
:list
ofset
of{int
orstr}
- Each element is the set of the indices/names of vertices in a clique.
Expand source code
def find_nonoverlapping_cliques(g: ig.Graph, get_name=False): """ Find a maximum set of cliques that do not share any vertices. A *maximum* set means that it cannot be enlarged by adding another clique. The obtained set may vary each time the function is called since the iterations of vertices are randomized by `numpy.random`. Parameters ---------- g : igraph.Graph Target graph. get_name : bool (default: False) Whether to get the names or indices of vertices. Returns ------- cliques : list of set of {int or str} Each element is the set of the indices/names of vertices in a clique. """ cliques = g.maximal_cliques(min=3) cliques = [set(clique) for clique in cliques] # num_cliques = len(cliques) # Remove overlapping cliques np.random.shuffle(cliques) nonoverlapping_cliques = [] all_vids = set() for clique in cliques: if not (clique & all_vids): nonoverlapping_cliques.append(clique) all_vids.update(clique) cliques = nonoverlapping_cliques if get_name: cliques = [{g.vs[vid]['name'] for vid in clique} for clique in cliques] return cliques
def get_graph_from_edges(edges)
-
Generate an
igraph.Graph
object from the list of edges.Parameters
edges
:list
of2-tuple
ofint
- List of edges that form the graph, where each integer indicates a vertex label.
Returns
graph
:igraph.Graph
- Generated graph.
Expand source code
def get_graph_from_edges(edges): """ Generate an [`igraph.Graph`](https://python.igraph.org/en/stable/api/igraph.Graph.html) object from the list of edges. Parameters ---------- edges : list of 2-tuple of int List of edges that form the graph, where each integer indicates a vertex label. Returns ------- graph : igraph.Graph Generated graph. """ return ig.Graph(edges=edges)
def get_sample_graph(shape, *prms)
-
Generate a predefined graph with a given shape and parameters.
See the description of
GraphState
.Returns
graph
:igraph.Graph
- Generated graph.
Expand source code
def get_sample_graph(shape, *prms): """ Generate a predefined graph with a given shape and parameters. See the description of `optgraphstate.GraphState.__init__()`. Returns ------- graph : igraph.Graph Generated graph. """ if shape == 'random': assert len(prms) in [2, 3] if len(prms) == 3: random.seed(prms[2]) g = ig.Graph.Erdos_Renyi(n=prms[0], m=prms[1]) random.seed() elif shape == 'complete': assert len(prms) == 1 g = ig.Graph.Full(n=prms[0]) elif shape == 'star': assert len(prms) == 1 g = ig.Graph.Star(prms[0]) elif shape == 'linear': g = ig.Graph.Lattice(dim=prms, circular=False) elif shape == 'cycle': g = ig.Graph.Ring(prms[0]) elif shape == 'lattice': g = ig.Graph.Lattice(dim=prms, circular=False) # graph_info['size'] = tuple(prms) elif shape == 'tree': # graph_info['num_children'] = tuple(prms) g: ig.Graph = ig.Graph.Star(prms[0] + 1) parents_start = 1 n_parents = prms[0] for n_children in prms[1:]: for parent in range(parents_start, parents_start + n_parents): g.add_vertices(n_children) vcount = g.vcount() g.add_edges([(parent, child) for child in range(vcount - n_children, vcount)]) parents_start += n_parents n_parents *= n_children elif shape == 'rhg': assert len(prms) == 3 g = _get_rhg_lattice(*prms) # g['size'] = tuple(prms) elif shape == 'repeater': assert len(prms) == 1 g = get_sample_graph('complete', 2 * prms[0]) vcount = g.vcount() for v in range(vcount): new_v = g.add_vertex() g.add_edge(v, new_v) # g['m'] = prms[0] elif shape == 'parity_encoding': assert len(prms) in [3, 4] g = _get_parity_encoded_graph(*prms) # g['logical_graph'] = prms[0] # g['n'] = prms[1] # g['m'] = prms[2] elif shape == 'ptqc': assert len(prms) == 4 g = _get_ptqc_graph(*prms) # g['n'] = prms[0] # g['m'] = prms[1] # g['HIC'] = prms[2] # g['central'] = prms[3] else: raise ValueError("Unsupported shape") return g