Skip to content

Commit

Permalink
Version 1.0 release
Browse files Browse the repository at this point in the history
  • Loading branch information
sammorley-short committed Nov 18, 2018
1 parent 21d5d6a commit 2f043ac
Show file tree
Hide file tree
Showing 18 changed files with 1,065 additions and 38 deletions.
29 changes: 29 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
# Ignore all
*

# Unignore all with extensions
!*.*

# Unignore all dirs
!*/

# Specific filetypes to ignore
*.csv
*.json
*.pyc
*.png
*.sublime*
*.o
*.so
.DS_Store

# Nauty directories to ignore
nauty25r9
nauty

# Virtualenv directories to ignore
bin
include
lib
man
share
.Python
pip-selfcheck.json
thebigselfdualfile.txt
143 changes: 143 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Tools for exploring graph state equivalence classes

*By Sam Morley-Short*

**Version 1.0**

---

This repository contains a number of tools used for exploring the local complementation equivalence classes of quantum graph states.
Specifically, it contains functions that:

* Explores the entire LC-equivalence class, or *orbit* of a given graph state (up to isomorphism).
* Detect if two graph states $\left|G\right\rangle$, $\left|G^\prime\right\rangle$ are LC-equivalent and if so returns all local unitaries $U$ such that $\left|G^\prime\right\rangle = U\left|G\right\rangle$.
* Find a graphs' minimum and maximum edge representatives.
* Visualize graphs and their equivalence classes

Specific instructions on using these tools are given below and a glossary is also provided.

N.B. this README contains latex-formatted equations. If viewing on GitHub these can be easily rendered using browser extensions such as [MathJax for GitHub](https://chrome.google.com/webstore/detail/github-with-mathjax/ioemnmodlmafdkllaclgeombjnmnbima). For mac users, [MacDown](https://macdown.uranusjr.com/) is recommended for offline viewing.

## Exploring LC-equivalence classes

### What is?

Full exploration of a graph's local complementation class is not generally efficient.
One reason for this is that within a class there may exist many isomorphs of a single graph, each produced by some unique series of LC's applied to some input graph.
However, in many cases one only cares about the general structure of graphs attainable from the input graph, rather than specific labellings.

By leveraging properties of graphs related to their [automorphisms](https://www.wikiwand.com/en/Graph_automorphism), our algorithm efficiently explores the class up to isomorphism.
This is achieved using a recursive tree-search approach, whereby each edge that links two graphs within the class (indicating they are separated by at most one LC operation) is only traversed once, and hence the class graph

### How do?

The search itself is performed by the function `explore_LC_isomorphic_orbit` found in `explore_class.py`.
This function takes some NetworkX graph as input and outputs a JSON format database of the graph's LC class.

In practise, this is achieved by the recipe depicted in the following python script (found in `tests/test_README_LC_explore`:

```python
# Import Python packages
import networkx as nx
# Import local modules
from explore_class import *

# Create the input graph
edges = [(0, 1), (1, 2), (2, 3), (3, 4)]
graph = nx.Graph()
graph.add_edges_from(edges)

# Find the class graph
class_graph = explore_LC_isomorphic_orbit(graph)

# Export class graph to JSON file
filename = 'L5_class_graph.json'
export_class_graph(class_graph, filename)
```

For example, the above code finds the orbit for the 5-qubit linear graph state.
The output JSON file uses a standard NetworkX format so that in principle can be reloaded into NetworkX to reproduce the class graph in some other code.
However, from our perspective it contains two important data structures representing the class graph's nodes and edges respectively.
Firstly, the list keyed by `"nodes"` stores a list of dictionaries, each representing a node (i.e. graph) in the orbit containing the following information:

* `"edges"`: The edges of the graph the node represents.
* `"hash"`: A hash value of the canonically relabelled version of the graph (such that two isomorphic graphs will have the same hash value).
* `"id"`: A short integer label for the graph used to define the edges.

Secondly, the list keyed by `"adjacency"` gives an list in which the $i$th element is a list of dictionaries that represent each edge $(i, j)$ connected to node $i$.
Each dictionary representing $(i, j)$ contains the `"id"` of node $j$ and `"equiv"`, a list of nodes in the graph $i$ that produce the same graph $j$ after LC (up to isomorphism).

**Notes:**

* All node labels on the input graph must be integers. If this is not the case, `int_relabel_graph` can be used to return a relabelled graph and the labelling applied.

## Testing for LC-equivalence

### What is?

For the reduced task of testing whether two known graphs $\left|G\right\rangle$ and $\left|G^\prime\right\rangle$ are LC-equivalent, we include an implementation of an algorithm [originally presented](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.70.034302) by Van Den Nest, et. al.
In the case that two graphs are equivalent, the algorithm also returns all local unitaries $U$ such that $\left|G^\prime\right\rangle = U\left|G\right\rangle$.

### How do?

LC equivalence is checked by function `are_lc_equiv` found in `is_lc_equiv.py`. The function takes as input two NetworkX graphs and outputs the tuple `(are_lc_equiv, local_ops)` which are a boolean and list of possible $U$.

For example, consider the following python script (found in `tests/test_README_LC_equiv.py`):

```python
# Import Python packages
import networkx as nx
# Import local modules
from is_lc_equiv import *

# Create a linear 4 node graph
edges = [(0, 1), (1, 2), (2, 3)]
graph_a = nx.Graph()
graph_a.add_edges_from(edges)

# Create a 4 node ring graph
edges = [(0, 1), (1, 2), (2, 3), (3, 0)]
graph_b = nx.Graph()
graph_b.add_edges_from(edges)

# Create a 4 node ring graph
edges = [(0, 2), (2, 1), (1, 3), (3, 0)]
graph_c = nx.Graph()
graph_c.add_edges_from(edges)

# Checks equivalence between graph A and graph B
is_equiv, local_us = are_lc_equiv(graph_a, graph_b)
print is_equiv, local_us

# Checks equivalence between graph A and graph C
is_equiv, local_us = are_lc_equiv(graph_a, graph_c)
print is_equiv, local_us
```

which outputs the following:

```
False None
True [['I', 'H', 'H', 'I'], ['I', 'H', 'SH', 'S'], ['S', 'SH', 'H', 'I'], ['S', 'SH', 'SH', 'S']]
```

The validity of the output can be seen by referring to Fig. 7 of the following [paper](https://arxiv.org/abs/quant-ph/0307130v7).

## Dependancies

This module relies on the following packages:

* Nauty (found [here](http://pallini.di.uniroma1.it/))
* Pynauty-hack (found [here](https://github.com/sammorley-short/pynauty-hack))
* Various python modules: NetworkX, Numpy, pprint, abp (all installed via pip)

## Glossary

* **Graph state**: a quantum state $\left|G\right\rangle$ represented by some graph $G$.
* **Local complementation (LC)**: the graph operation that represents taking graph states to other graph states using only local Clifford unitaries.
* **Class graph**: a simple, connected and undirected graph representing the structure of an LC-equivalence class. Each node represents some graph state within the class with each edge between two nodes the application of an LC operation that takes one graph to another.
* **Isomorphic**: Two graphs are isomorphic if they are the same up to relabelling. This is equivalent to saying they have the same [*canonical labelling*](https://www.wikiwand.com/en/Graph_canonization).
* **Automorphism**: An automorphism of a graph is any relabelling that produces the same graph. E.g. for a ring graph with $V = \{0, \ldots, n\}$ nodes, the node relabelling $i \mapsto (i+1) \;\textrm{mod} \; |V|$ is a valid automorphism as it produces the same graph.
* **NetworkX**: A useful python package for creating and manipulating graphs. On Unix systems it can be most easily installed via pip through the command `$ pip install networkx`. Documentation can be found [here](https://networkx.github.io/documentation/stable/index.html).
* **ABP**: A python package for efficiently simulating and visualising quantum graph states based on Anders and Briegel's original algorithm. It can also be installed via pip, with installation instructions and documentation can be found [here](https://github.com/peteshadbolt/abp).
* **Nauty**: A popular C library for finding graph auto- and isomorphisms, found [here](http://pallini.di.uniroma1.it/) which must be installed. As well as this, our code relies on a hacked version of the python wrapper `pynauty` found [here](https://github.com/sammorley-short/pynauty-hack).
113 changes: 113 additions & 0 deletions explore_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Python packages
import os
import json
import pynauty as pyn
import networkx as nx
import itertools as it
from copy import deepcopy
from networkx.readwrite import json_graph
from pprint import pprint
# Local modules
from utils import canonical_edge_order
from get_nauty import find_unique_lcs, hash_graph


def local_complementation(graph, node, copy=True):
""" Returns the graph for local complementation applied to node """
neighs = graph.neighbors(node)
neigh_k_edges = it.combinations(neighs, 2)
lc_graph = deepcopy(graph) if copy else graph
for u, v in neigh_k_edges:
if lc_graph.has_edge(u, v):
lc_graph.remove_edge(u, v)
else:
lc_graph.add_edge(u, v)
return lc_graph


def edge_lc(graph, edge):
u, v = edge
edge_lc_graph = apply_lcs(graph, [u, v, u])
return edge_lc_graph


def apply_lcs(graph, nodes):
lc_graph = deepcopy(graph)
for node in nodes:
lc_graph = local_complementation(lc_graph, node, copy=False)
return lc_graph


def init_EC_database_dir(directory='EC_database'):
""" Initialises the equivalence class database directory """
if not os.path.exists(directory):
os.makedir(directory)


def recursive_LC_search(class_graph, graph_label, node_equivs):
""" Recursive search for exploring the isomorphic LC space of graphs """
graph = class_graph.node[graph_label]['nx_graph']
for rep_node, equiv_nodes in node_equivs.iteritems():
lc_graph = local_complementation(graph, rep_node)
lc_edges = canonical_edge_order(lc_graph.edges())
try:
lc_label = class_graph.member_hash_table[lc_edges]
class_graph.add_edge(graph_label, lc_label,
equivs=equiv_nodes)
continue
except KeyError:
lc_label = max(class_graph.nodes()) + 1
lc_graph_hash = hash_graph(lc_graph)
class_graph.add_node(lc_label, nx_graph=lc_graph,
edges=lc_edges, hash=lc_graph_hash)
class_graph.add_edge(graph_label, lc_label,
equivs=equiv_nodes)
class_graph.member_hash_table.update({lc_edges: lc_label})
lc_equivs = find_unique_lcs(lc_graph)
recursive_LC_search(class_graph, lc_label, lc_equivs)
return class_graph


def int_relabel_graph(graph):
"""
Relabels graphs with tuple node names to int node names.
Returns relabelled graph and node mapping applied.
"""
int_labels = {node: i for i, node in enumerate(graph.nodes())}
int_graph = nx.relabel_nodes(graph, int_labels)
return int_graph, int_labels


def explore_LC_isomorphic_orbit(init_graph):
""" Explores the LC equivalence class orbit up to isomorphism """
if not nx.is_connected(init_graph):
raise TypeError("Initial graph must be connected.")
init_graph_equivs = find_unique_lcs(init_graph)
init_edges = canonical_edge_order(init_graph.edges())
init_hash = hash_graph(init_graph)
class_graph = nx.Graph()
class_graph.add_node(0, nx_graph=init_graph, edges=init_edges,
hash=init_hash)
class_graph.member_hash_table = {init_edges: 0}
class_graph = recursive_LC_search(class_graph, 0, init_graph_equivs)
return class_graph


def export_class_graph(class_graph, filename):
""" Exports class graph to JSON file """
for node, attrs in class_graph.node.iteritems():
class_graph.node[node].pop('nx_graph')
class_graph_data = json_graph.adjacency_data(class_graph)
with open(filename, 'w') as fp:
json.dump(class_graph_data, fp)
return class_graph_data


if __name__ == '__main__':
n = 4
init_edges = [(i, (i + 1) % n) for i in range(n)]
init_graph = nx.Graph(init_edges)
print init_graph.edges()
print
class_graph = explore_LC_isomorphic_orbit(init_graph)
pprint(dict(class_graph.node))
98 changes: 98 additions & 0 deletions get_nauty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Python packages
import networkx as nx
import pynauty as pyn
from pprint import pprint
from collections import defaultdict
# Local modules
from utils import flatten


def convert_nx_to_pyn(nx_g):
""" Takes a NetworkX graph and outputs a PyNauty graph """
nodes, neighs = zip(*nx_g.adjacency())
graph_adj = {node: neighs.keys() for node, neighs in zip(nodes, neighs)}
# print graph_adj
n_v = len(graph_adj)
pyn_g = pyn.Graph(n_v, directed=False, adjacency_dict=graph_adj)
return pyn_g


def hash_graph(graph):
""" Returns a hash for the graph based on PyNauty's certificate fn """
pyn_g = convert_nx_to_pyn(graph)
g_cert = pyn.certificate(pyn_g)
return hash(g_cert)


def canonical_relabel(nx_g):
""" Returns isomorphic graph with canonical relabelling """
nodes, neighs = zip(*nx_g.adjacency())
pyn_g = convert_nx_to_pyn(nx_g)
canon_lab = pyn.canon_label(pyn_g)
canon_relab = {o_node: i_node for i_node, o_node in zip(nodes, canon_lab)}
nx_g_canon = nx.relabel_nodes(nx_g, canon_relab)
return nx_g_canon


def find_automorphisms(nx_g):
"""
Takes a NetworkX graph and returns the automorphism generators
"""
nodes, neighs = zip(*nx_g.adjacency())
pyn_g = convert_nx_to_pyn(nx_g)
gens, grp1_size, grp2_size, orbits, n_orbits = pyn.autgrp(pyn_g)
am_node_maps = [{i_node: o_node for i_node, o_node in zip(nodes, gen)}
for gen in gens]
return nx_g, am_node_maps


def find_unique_lcs(g):
"""
Takes a NetworkX graph and finds groups of nodes that are equivalent
up to automorphism
"""
g, am_node_maps = find_automorphisms(g)
aut_edges = flatten([am_node_map.items() for am_node_map in am_node_maps])
aut_g = nx.Graph(aut_edges)
equiv_groups = [equiv for equiv in map(list, list(
nx.connected_components(aut_g))) if len(equiv) > 1]
all_nodes = g.nodes()
node_equivs = {}
for equiv_group in equiv_groups:
rep_node = equiv_group[0]
node_equivs.update({rep_node: equiv_group})
all_nodes = [node for node in all_nodes if node not in equiv_group]
node_equivs.update({node: [node] for node in all_nodes})
# Removes any LC's that act trivially on the graph (i.e. act on d=1 nodes)
node_equivs = {node: equivs for node, equivs in node_equivs.iteritems()
if g.degree(node) > 1}
return node_equivs


# TODO: Comment functions
# TODO: Package up hacked nauty for distribution


if __name__ == '__main__':
n = 12
edge_sets = [[(i, (i + 1) % n) for i in range(n)],
[(i, (i + 1) % n) for i in range(n)] + [(1, 5)],
[(i, (i + 1)) for i in range(n)] + [(2, 7)]]
for edges in edge_sets:
g = nx.Graph(edges)
print "Edges:"
pprint(list(g.edges()))
# node_equivs = find_unique_lcs(g)
# print "Node LC equivalences:", node_equivs
# print hash_graph(g)
relab_g = canonical_relabel(g)
print "Relabelled edges:"
pprint(list(relab_g.edges()))
rr_g = canonical_relabel(relab_g)
print "Relabelled edges:"
pprint(list(rr_g.edges()))
print

# g = nx.Graph([(0, 2), (0, 3), (0, 5), (1, 4), (1, 5), (3, 4), (3, 5)])
# g, g_ams = find_automorphisms(g)
# print g_ams
Loading

0 comments on commit 2f043ac

Please sign in to comment.