diff --git a/.gitignore b/.gitignore index f280e91..23bfbc5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ # Unignore all dirs !*/ -# Specific filetypes to ignore +# Specific filetypes *.csv *.json *.pyc @@ -16,17 +16,6 @@ *.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 +build +dist +gsc.egg-info diff --git a/README.md b/README.md index f926bdc..f340741 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,56 @@ -# Tools for exploring graph state equivalence classes +# gsc (graph-state compass) -*By Sam Morley-Short* +Version 2.0 -**Version 1.0** +*by Sam Morley-Short* ---- +## Why gsc? -This repository contains a number of tools used for exploring the local complementation equivalence classes of quantum graph states. +"gsc" or "graph state compass" is a Python package containing a number of tools used for mapping and depicting the local Clifford 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$. +* Explores the entire equivalence class, or *orbit* of given graph state (up to isomorphism), of prime and prime-power dimension graph states. +* Detect if two qubit 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. +* Enumeration of all non-isomorphic equivalence classes for a given $n$-qubit $p^m$-dimensional graph state. * 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. +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 +## Installation -### What is? +To install gsc, clone the repo, and then run `python setup.py install` in the top-level directory. -Full exploration of a graph's local complementation class is not generally efficient. +Note that gsc depends on a forked version of the Pynauty package, which can be found [here](https://github.com/sammorley-short/pynauty). +Pynauty is a Python wrapper for the C program Nauty for computing automorphism groups of graphs. +Please follow the instructions outlined in the linked repo's README for instructions on how to install Pynauty and Nauty. + +## Exploring LC-equivalence classes of qubit graph states + +### What? + +Full exploration of a graph state'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 +By leveraging properties of graphs related to their [automorphisms](https://www.wikiwand.com/en/Graph_automorphism), our algorithm explores the class up to isomorphism faster than previous approaches. +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. +This is acheived by identifying sets of nodes which are equivalent in the graph up to local complementation and only performing LC on one node of each set. -### How do? +### How? -The search itself is performed by the function `explore_LC_isomorphic_orbit` found in `explore_class.py`. +The search itself is a breadth-first search performed by the function `explore_lc_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`: +For example, the following snippet of code will find the LC-equivalence class of the 5-qubit linear cluster state: ```python # Import Python packages import networkx as nx # Import local modules -from explore_class import * +from explore_class import explore_lc_orbit, export_class_graph # Create the input graph edges = [(0, 1), (1, 2), (2, 3), (3, 4)] @@ -48,47 +58,57 @@ graph = nx.Graph() graph.add_edges_from(edges) # Find the class graph -class_graph = explore_LC_isomorphic_orbit(graph) +class_graph = explore_lc_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. +Here the first output that is produced is `class_graph`, a NetworkX graph object whose nodes represent the graphs within the class and edges the local complementations that connect them. +This object is then exported to JSON using a standard NetworkX format that can be reloaded into NetworkX to reproduce the class graph later. +Within the JSON dictionary there are 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. +* `"id"`: A short integer label for the graph used to define the class graph's 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). +Secondly, the list keyed by `"links"` gives a list of dictionaries representing the edges of the class graph. +Each dictionary representing the edge $(i, j)$ contains a `"source": i` and `"target": j` as well as operations were applied to which nodes to map the source to target graph state, keyed by `"equivs"` and `"ops"` respectively. +For qubit graphs `"equivs"` and `"ops"` are single element lists because only a single operation exists---namely, local complementation---however this is not the case for higher-dimensional graph states. **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. +* 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. +* Local complementation operations on degree one qubits are trivial operations and so are ignored. +* `explore_lc_class` has the following optional keyword arguments: + * `save_edges=True`: if set to `False` then only the class members themselves are stored in the class graph, discarding the operations that connect them. + For example, this is used for member enumeration, where the class' specific structure is not sought. + * `verbose=True`: By default the ratio of explored graphs to known members is displayed during the search. + To turn this off set `verbose=False`. +* `export_class_graph` has the optional keyword argument `min_edge_reps=False`. Setting `min_edge_reps=True` when also export a list of the classes Minimum Edge Representatives (MERs). +* `export_class_register` can be used to export a register of class members to a CSV table containing each graph's ID, edge list and hash. + This should be used when the equivalence class' internal structure is not needed, such as in enumeration. ## Testing for LC-equivalence -### What is? +### What? 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? +### How? -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$. +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 a list of valid unitaries. -For example, consider the following python script (found in `tests/test_README_LC_equiv.py`): +For example, consider checking the local equivalence between some set of 4-qubit graphs: ```python # Import Python packages import networkx as nx # Import local modules -from is_lc_equiv import * +from is_lc_equiv import are_lc_equiv # Create a linear 4 node graph edges = [(0, 1), (1, 2), (2, 3)] @@ -123,13 +143,133 @@ True [['I', 'H', 'H', 'I'], ['I', 'H', 'SH', 'S'], ['S', 'SH', 'H', 'I'], ['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). +## Higher-dimension graph states + +`gsc` can also explore the equivalence classes of higher-dimensional graph states. + +### Prime dimension + +Quantum stabilizer states of prime local dimension can also be described within the graph-state formalism, as described in the following [paper](https://arxiv.org/abs/quant-ph/0610267). +As such, by including the higher-dimensional local complementation and edge multiplication operations, `gsc` can also explore the equivalence classes of prime dimension graph states. + +For example, the following snippet can be used to export the equivalence class of a three-qutrit state: + +```python +# Import the prime graph state builder and class explorer and exporter +from graph_builders import create_prime_graph +from explore_lc_orbit import explore_lc_orbit, export_class_graph +# Create the input graph +prime = 3 +w_edges = [(0, 1, 1), (1, 2, 2)] +qutrit_g = create_prime_graph(w_edges, prime) +# Find the class graph +class_graph = explore_lc_orbit(qutrit_g) +# Export class graph to JSON file +filename = 'qutrit_class_graph' +class_graph_data = export_class_graph(class_graph, filename) +``` +where `class_graph_data` contains the exported JSON-formatted dictionary. + +### Prime-power dimension + +Quantum stabilizer states of prime-power dimension can also be mapped onto prime-dimensional graph states and therefore also described within the graph-state formalism. +Under the isomorphism described in Chapter 5 of my PhD thesis, each qudit of local dimension $p^m$ (for $p$ prime and some integer $m>2$) is mapped to a party of $m$ $p$-dimensional qudits between which entangling operations are now considered local. +In this case, the local complementation operation is replaced with the so-called controlled complementation operation, which with edge multiplcation completes the set of local graph-state operations. + +```python +# Import the pseudo graph state builder and class explorer and exporter +from psuedo_graphs import gen_psuedo_graph_edge_map, create_psuedo_graph, psuedo_to_real +from explore_lc_orbit import explore_lc_orbit, export_class_graph +# Create the initial prime-power pseudo graph state +prime, power = 2, 2 +c_map = gen_psuedo_graph_edge_map(prime, power) +c_edges = [(0, 1, 5), (1, 2, 2)] +psuedo_graph = create_psuedo_graph(c_edges, prime, power, c_map) +# Convert prime-power pseudo graph state to "real" prime graph state +real_graph = psuedo_to_real(psuedo_graph) +# Find the class graph +class_graph = explore_lc_orbit(real_graph, save_edges=False) +# Export class graph to JSON file +filename = 'p2_m2_qudit_class_graph' +class_graph_data = export_class_graph(class_graph, filename) +# class_graph_data also contains the exported JSON-formatted dictionary +``` +where `class_graph_data` contains the exported JSON-formatted dictionary. + +Since prime-power graphs often contain many, many edges, for convenience, we have initialised the prime-power graph state using its *pseudo graph state* representation. +A pseudo graph state is a representation of the prime-power graph state using a directed graph where each edge label or *colour* represents some configuration of edges between two qubit families on the prime-dimensional graph as defined by some coloured edge mapping. +In the above example, this mapping is defined by: + +```python +c_map = {0: [], + 1: [(0, 0, 1)], + 2: [(0, 1, 1)], + 3: [(1, 0, 1)], + 4: [(1, 1, 1)], + 5: [(0, 0, 1), (0, 1, 1)], + 6: [(0, 0, 1), (1, 0, 1)], + 7: [(0, 0, 1), (1, 1, 1)], + 8: [(0, 1, 1), (1, 0, 1)], + 9: [(0, 1, 1), (1, 1, 1)], + 10: [(1, 0, 1), (1, 1, 1)], + 11: [(0, 0, 1), (0, 1, 1), (1, 0, 1)], + 12: [(0, 0, 1), (0, 1, 1), (1, 1, 1)], + 13: [(0, 0, 1), (1, 0, 1), (1, 1, 1)], + 14: [(0, 1, 1), (1, 0, 1), (1, 1, 1)], + 15: [(0, 0, 1), (0, 1, 1), (1, 0, 1), (1, 1, 1)]} +``` +where each edge $(i, j, w)$ defines the edge between family members $i$ and $j$ of the source and sink nodes of weight $w$. +For example, in the above example the pseudo edge list `[(0, 1, 5), (1, 2, 2)]` represents the prime-dimension graph with edges `[((0, 0), (1, 0)), ((0, 0), (1, 1)), ((1, 0), (2, 1))]` (and nodes `[(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]`), where unit edge weights have been omitted for brevity. +It is easy to see that while such states are convenient to reduce excessive listing of edges, they are clearly not unique. +For example, the above state could have been equally represented by the pseudo edge list `[(1, 0, 6), (2, 1, 3)]` (recall that the graph is directed, and so the edge $(i, j, c)$ represents the edge $i \rightarrow j$ of colour $c$). + +While the pseudo graph state representation is purely used for convenience, it becomes useful for class enumeration, where listing all the edges of large states becomes unweildy. + +## Class enumeration + +`gsc` also performs equivalence class enumeration, and is performed as follows. +Firstly, initialise a register of all possible $p^m$-dimension $n$-qudit graph states (in `remaining_graphs.csv` with parameters $p, m, n$ stored in `state_params.csv`), an empty hash lookup table of found graphs (in `graph_hashes.csv`) and an empty class database directiory (in `/classes`). +In the register, a graph is stored as a list or "configuration" of pseudo-edges indexed by list of edges stored in `edge_index.csv` and are associated to their associated prime-dimensional graph-state via the pseudo-edge map defined in `psuedo_edge_map.csv`. +Next, for each state: + +1. **If disconnected:** Remove graph and all isomorphs from graph register. +2. **If graph hash already in hash table:** Remove graph and all isomorphs from graph register. +3. **Else:** **i)** Explore local equivalence class to find all members, **ii)** Export class to database, **iii)** Find all isomorphs of all members and their hashes, **iv)** Remove all isomorphs from register and add all hashes to hash table. + +This is repeated until the graph register is empty. + +**Notes:** + +* Each output equivalence class is named by the edge configuration of their first member and contains a register of each graph it contains. + Each graph in a class register is stored as a row of three columns, denoting the graph's ID, prime-dimensional edge list, and it's hash value. + For $p=2$ graph states, edge weights are omitted, and for $m=1$ graph states, so are family member labels. +* While it may appear that finding isomorphs may be redundant in step 2), given that they are also found in step 3). + This is because graphs states are stored in their pseudo-edge representation (which saving on reading and writing entire edge lists associated with their prime-dimensional representation). + Hence, some graphs with differing pseudo-edge representation actually represent the same state in their prime-dimension. + For example, the 3-ququart states with edge lists: + + ``` + [((0, 0), (1, 0)), ((1, 0), (2, 0)), ((2, 0), (0, 0))] and + [((0, 1), (1, 1)), ((1, 1), (2, 1)), ((2, 1), (0, 1))] + ``` + + where unit edge weights have been omitted for brevity). + Clearly both edge lists represent equivalent states, yet they have weighted pseudo edge lists of + + ``` + [(0, 1, 1), (1, 2, 1), (2, 0, 1)] + [(0, 1, 4), (1, 2, 4), (2, 0, 4)] + ``` + respectively. + Because isomorphic configurations are found while in the pseudo-edge representation, the following two states will not be distinguished until their prime-dimensional graph states are hashed. + ## 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) +* Various python modules: NetworkX, Numpy, tqdm, abp, matplotlib (which should all installed via pip at installation) ## Glossary @@ -137,7 +277,7 @@ This module relies on the following packages: * **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. +* **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) \bmod{|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). \ No newline at end of file diff --git a/gsc/__init__.py b/gsc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gsc/explore_lc_orbit.py b/gsc/explore_lc_orbit.py new file mode 100644 index 0000000..d588bfc --- /dev/null +++ b/gsc/explore_lc_orbit.py @@ -0,0 +1,477 @@ +# Python packages +import os +import sys +import csv +import json +import pynauty as pyn +import networkx as nx +import itertools as it +from pprint import pprint +from networkx.readwrite import json_graph +# Local modules +from gsc.utils import * +from gsc.get_nauty import find_rep_nodes, hash_graph + + +def init_EC_database_dir(directory='EC_database'): + """ Initialises the equivalence class database directory """ + if not os.path.exists(directory): + os.makedirs(directory) + + +def qubit_LC(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 = copy_graph(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, weight=1) + return lc_graph + + +def apply_qubit_LCs(graph, nodes): + """ Applies a sequence of local complementations """ + lc_graph = copy_graph(graph) + for node in nodes: + lc_graph = qubit_LC(lc_graph, node, copy=False) + return lc_graph + + +def edge_LC(graph, edge): + """ Applies edge-local complementation """ + u, v = edge + edge_lc_graph = apply_qubit_LCs(graph, [u, v, u]) + return edge_lc_graph + + +def prime_qudit_LC(graph, node, a, copy=True): + """ + Returns the graph for generalised local complementation applied to + node n with weight a + """ + p = graph.prime + neighs = list(graph.neighbors(node)) + neigh_k_edges = it.combinations(neighs, 2) + lc_graph = copy_graph(graph) if copy else graph + for u, v in neigh_k_edges: + nu_weight = lc_graph[node][u]['weight'] + nv_weight = lc_graph[node][v]['weight'] + if lc_graph.has_edge(u, v): + uv_weight = lc_graph[u][v]['weight'] + new_weight = (uv_weight + a * nv_weight * nu_weight) % p + lc_graph[u][v]['weight'] = new_weight + else: + weight = (a * nv_weight * nu_weight) % p + lc_graph.add_edge(u, v, weight=weight) + if lc_graph[u][v]['weight'] == 0: + lc_graph.remove_edge(u, v) + return lc_graph + + +def prime_qudit_EM(graph, node, b, copy=True): + """ + Returns the graph for edge multiplication operation applied on node n + with weight b + """ + em_graph = copy_graph(graph) if copy else graph + p = graph.prime + neighs = em_graph.neighbors(node) + for u in neighs: + if em_graph.has_edge(node, u): + nv_weight = em_graph[node][u]['weight'] + new_weight = (b * nv_weight) % p + em_graph[node][u]['weight'] = new_weight + if em_graph[node][u]['weight'] == 0: + em_graph.remove_edge(node, u) + return em_graph + + +def make_LC_a(a): + def lc_a(graph, node): + return prime_qudit_LC(graph, node, a) + return lc_a + + +def make_EM_b(b): + def em_b(graph, node): + return prime_qudit_EM(graph, node, b) + return em_b + + +def prime_power_qudit_CC(graph, node, t, a, copy=True): + """ Returns graph after controlled complementation """ + # Creates new graph if needed + new_graph = copy_graph(graph) if copy else graph + n, c = node + # Adds edge between control and target node + if c != t: + new_graph.add_edge((n, c), (n, t), weight=1) + # Applies LC to control + new_graph = prime_qudit_LC(new_graph, (n, c), a, copy=False) + # Removes any intra-family edges + family_edges = [((u, i), (v, j)) for (u, i), (v, j) in new_graph.edges() + if u == v] + new_graph.remove_edges_from(family_edges) + return new_graph + + +def make_pp_CC_a(a, t): + """ Controlled complementation function generator """ + def cc_a(graph, node): + return prime_power_qudit_CC(graph, node, t, a) + return cc_a + + +def queued_orbit_search(init_graph, local_ops, save_edges, verbose): + # Initialises class graph with init_graph + init_edges = list(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_hash: 0} + # Loops over queue members until empty + queue = [0] + visited = 0 + while queue: + # Prints live count of explored/known + if verbose: + out = \ + str(visited) + '/' + str(len(queue) + visited) + ' visited (' \ + + str(int(100 * float(visited)/(len(queue) + visited))) + '%)' + sys.stdout.write('%s\r' % out) + sys.stdout.flush() + visited += 1 + # Gets next graph on queue and finds representative nodes + graph_label = queue.pop() + graph = class_graph.node[graph_label]['nx_graph'] + node_equivs = find_rep_nodes(graph) + # Applies set of local ops to each representative node + for rep_node, equiv_nodes in node_equivs.iteritems(): + for op_label, local_op in local_ops: + new_graph = local_op(graph, rep_node) + new_edges = list(new_graph.edges()) + new_hash = hash_graph(new_graph) + # Checks new graph is difference to original + if sorted(new_graph.edges(data='weight')) == \ + sorted(graph.edges(data='weight')): + continue + # If different, tries to find new graph in class + try: + old_label = class_graph.member_hash_table[new_hash] + if save_edges: + # If new edge adds new edge between members + if not class_graph.has_edge(graph_label, old_label): + class_graph.add_edge(graph_label, old_label, + equivs=[equiv_nodes], + ops=[op_label]) + # Else adds any new local ops to edge label + elif op_label not in \ + class_graph[graph_label][old_label]['ops']: + class_graph[graph_label][old_label]['ops']\ + .append(op_label) + class_graph[graph_label][old_label]['equivs']\ + .append(equiv_nodes) + continue + # If not in class, creates new class graph node + except KeyError: + new_label = max(class_graph.nodes()) + 1 + class_graph.add_node(new_label, nx_graph=new_graph, + edges=new_edges, hash=new_hash) + if save_edges: + class_graph.add_edge(graph_label, new_label, + equivs=[equiv_nodes], + ops=[op_label]) + class_graph.member_hash_table.update({new_hash: new_label}) + queue.append(new_label) + 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_orbit(init_graph, save_edges=True, verbose=True): + """ Explores the LC equivalence class orbit up to isomorphism """ + # Tries to get graph dimensions p^m. If not assigned assumes d = 2 + p = init_graph.__dict__.get('prime', 2) + m = init_graph.__dict__.get('power', 1) + if m == 1 and not nx.is_connected(init_graph): + raise TypeError("Initial graph must be connected.") + # Creates finite field for arithmetic and maps graph edge int weights + if m > 1: + # Creates list of local operations accessible for search + local_ops = [('CC%d(c,%d)' % (a, t), make_pp_CC_a(a, t)) + for a in range(1, p) for t in range(m)] + local_ops += [('EM%d' % (b), make_EM_b(b)) + for b in range(2, p)] + elif p > 2 and m == 1: + local_ops = [('LC' + str(a), make_LC_a(a)) for a in range(1, p)] + local_ops += [('EM' + str(b), make_EM_b(b)) for b in range(2, p)] + else: + local_ops = [('LC', qubit_LC)] + # Performs orbit search + class_graph = queued_orbit_search(init_graph, local_ops, save_edges, + verbose) + # Adds weighted edge data for qudit class graphs + for node, data in class_graph.nodes.data(): + graph = data['nx_graph'] + edges = data['edges'] + if p ** m > 2: + data['edges'] = [(u, v, graph[u][v]['weight']) for u, v in edges] + else: + data['edges'] = [(u, v, 1) for u, v in edges] + return class_graph + + +def get_min_edge_reps(class_graph): + """ Returns all minimum edge representations for a given LC orbit """ + min_edges = min(len(graph['edges']) for graph in class_graph.node.values()) + min_edge_reps = {key: graph for key, graph in class_graph.node.iteritems() + if len(graph['edges']) == min_edges} + return min_edge_reps + + +def get_max_edge_reps(class_graph): + """ Returns all minimum edge representations for a given LC orbit """ + max_edges = max(len(graph['edges']) for graph in class_graph.node.values()) + max_edge_reps = {key: graph for key, graph in class_graph.node.iteritems() + if len(graph['edges']) == max_edges} + return max_edge_reps + + +def export_class_graph(class_graph, filename, min_edge_reps=False): + """ Exports class graph to JSON file """ + # Removes all networkx graphs and gets class graph data + for node, attrs in class_graph.node.iteritems(): + class_graph.node[node].pop('nx_graph') + class_graph_data = {key: value for key, value + in json_graph.node_link_data(class_graph).items() + if key in ('nodes', 'links')} + # Exports class graph to JSON format + cg_filename = filename + '.json' + with open(cg_filename, 'w') as fp: + json.dump(class_graph_data, fp) + # Finds minimum edge representatives and exports to file + if min_edge_reps: + min_edge_reps = get_min_edge_reps(class_graph) + mer_filename = filename + '_MERs.json' + with open(mer_filename, 'w') as fp: + json.dump(min_edge_reps, fp) + return class_graph_data + + +def export_class_register(class_graph, filename, min_edge_reps=False): + """ Exports list of all class members to file """ + # Creates class member register + register = [[node, attrs['edges'], attrs['hash']] + for node, attrs in class_graph.node.iteritems()] + reg_filename = filename + '.csv' + # Exports register to file + with open(reg_filename, 'w') as csvfile: + writer = csv.writer(csvfile) + writer.writerows(register) + # Finds minimum edge representatives and outputs to file + if min_edge_reps: + min_edges = len(min(register, key=lambda a: len(a[1]))[1]) + mer_register = [m for m in register if len(m[1]) == min_edges] + mer_filename = filename + '_MERs.csv' + with open(mer_filename, 'w') as csvfile: + writer = csv.writer(csvfile) + writer.writerows(mer_register) + return register + + +if __name__ == '__main__': +# from psuedo_graphs import gen_psuedo_graph_edge_map, psuedo_to_real, \ +# create_psuedo_graph +# prime = 2 +# power = 2 +# c_map = gen_psuedo_graph_edge_map(prime, power) +# pprint(c_map) +# c_edges = [(0, 1, 1)] +# psuedo_graph = create_psuedo_graph(c_edges, prime, power, c_map) +# print psuedo_graph.edges() +# real_graph = psuedo_to_real(psuedo_graph) +# print real_graph.edges(data='weight') +# print real_graph.nodes() +# class_graph = explore_lc_orbit(real_graph, False) +# print class_graph.edges() +# filename = 'class_graphs/tests/ququart_2GHZ_test' +# register = export_class_register(class_graph, filename, min_edge_reps=True) +# pprint(register) +# class_graph_data = export_class_graph( +# class_graph, filename, min_edge_reps=True) +# pprint(class_graph_data) + + from graph_builders import create_prime_graph + prime = 3 + w_edges = [(0, 1, 1), (1, 2, 2)] + qutrit_g = create_prime_graph(w_edges, prime) + class_graph = explore_lc_orbit(qutrit_g) + filename = 'qutrit_ring_class_graph' + class_graph_data = \ + export_class_graph(class_graph, filename, min_edge_reps=True) + print class_graph_data + # pprint(class_graph_data) + + # prime = 3 + # power = 2 + # c_map = gen_psuedo_graph_edge_map(prime, power) + # # pprint(c_map) + # c_edges = [(0, 1, 14)] + # psuedo_graph = create_psuedo_graph(c_edges, prime, power, c_map) + # real_graph = psuedo_to_real(psuedo_graph) + # # print real_graph.edges() + # class_graph = explore_lc_orbit(real_graph) + # filename = 'class_graphs/tests/qudit_3_2_2GHZ_test' + # class_graph_data = export_class_graph( + # class_graph, filename, min_edge_reps=True) + # pprint(class_graph_data) + + # prime = 2 + # power = 2 + # c_map = gen_psuedo_graph_edge_map(prime, power) + # pprint(c_map) + # c_edges = [(0, 1, 7), (1, 2, 7)] + # psuedo_graph = create_psuedo_graph(c_edges, prime, power, c_map) + # real_graph = psuedo_to_real(psuedo_graph) + # class_graph = explore_lc_orbit(real_graph, save_edges=False) + # filename = 'class_graphs/tests/ququart_3GHZ_test' + # class_graph_size = sys.getsizeof(class_graph) + # print + # print class_graph_size + # class_graph_data = export_class_graph( + # class_graph, filename, min_edge_reps=True) + # class_graph_size = sys.getsizeof(class_graph_data) + # print + # print class_graph_size + # pprint(class_graph_data) + + # prime = 2 + # power = 2 + # c_map = gen_psuedo_graph_edge_map(prime, power) + # # pprint(c_map) + # c_edges = [(0, 1, 7), (0, 2, 7), (0, 3, 7)] + # psuedo_graph = create_psuedo_graph(c_edges, prime, power, c_map) + # real_graph = psuedo_to_real(psuedo_graph) + # class_graph = explore_lc_orbit(real_graph) + # filename = 'class_graphs/ququarts/ququart_4GHZ' + # class_register = export_class_register(class_graph, filename, + # min_edge_reps=True) + # class_graph_data = export_class_graph(class_graph, filename, + # min_edge_reps=True) + # # pprint(class_graph_data) + + # prime = 3 + # power = 2 + # c_map = gen_psuedo_graph_edge_map(prime, power) + # # pprint(c_map) + # c_edges = [(0, 1, 14), (0, 2, 14)] + # psuedo_graph = create_psuedo_graph(c_edges, prime, power, c_map) + # real_graph = psuedo_to_real(psuedo_graph) + # class_graph = explore_lc_orbit(real_graph, save_edges=False) + # filename = 'class_graphs/qudit_3-2/qudit_3-2_3GHZ' + # class_register = export_class_register(class_graph, filename, + # min_edge_reps=True) + # print len(class_register) + # class_graph_data = export_class_graph( + # class_graph, filename, min_edge_reps=True) + # pprint(class_graph_data) + + # prime = 3 + # power = 2 + # c_map = gen_psuedo_graph_edge_map(prime, power) + # # pprint(c_map) + # c_edges = [(0, 1, 14), (0, 2, 14), (0, 3, 14)] + # psuedo_graph = create_psuedo_graph(c_edges, prime, power, c_map) + # real_graph = psuedo_to_real(psuedo_graph) + # class_graph = explore_lc_orbit(real_graph, save_edges=False) + # filename = 'class_graphs/qudit_3-2/qudit_3-2_4GHZ' + # class_register = export_class_register(class_graph, filename, + # min_edge_reps=True) + # print len(class_register) + # class_graph_data = export_class_graph( + # class_graph, filename, min_edge_reps=True) + # pprint(class_graph_data) + + # n = 5 + # edges = [(i, (i + 1) % n) for i in range(n)] + # g = nx.Graph(edges) + # min_edge_reps = True + # class_graph = explore_lc_orbit(g) + # filename = 'class_graphs/tests/5-ring_test' + # class_graph_data = export_class_graph( + # class_graph, filename, min_edge_reps=True) + + # n = 5 + # edges = [(i, (i + 1)) for i in range(n-1)] + # g = nx.Graph(edges) + # min_edge_reps = True + # class_graph = explore_lc_orbit(g) + # filename = 'class_graphs/tests/5-line_test' + # class_graph_data = export_class_graph( + # class_graph, filename, min_edge_reps=True) + + # n = 6 + # edges = [(i, (i + 1) % n) for i in range(n)] + # edges += [(0, 2), (5, 3), (1, 4)] + # print edges + # g = nx.Graph(edges) + # min_edge_reps = True + # class_graph = explore_lc_orbit(g) + # filename = 'class_graphs/tests/AME6-2_test' + # class_graph_data = export_class_graph( + # class_graph, filename, min_edge_reps=True) + + # from random import randint + # n = 5 + # prime = 3 + # w_edges = [(i, (i + 1) % n, randint(1, prime-1)) for i in range(n)] + # g = create_prime_graph(w_edges, prime) + # min_edge_reps = True + # class_graph = explore_lc_orbit(g) + # filename = 'class_graphs/tests/prime_ring%d-%d_test' % (n, prime) + # class_graph_data = export_class_graph( + # class_graph, filename, min_edge_reps=False) + + # from MDS_AME_states import from_MDS_code + # prime = 2 + # power = 1 + # A = [[1, 1]] + # graph = from_MDS_code(A, prime, power) + # class_graph = explore_lc_orbit(graph) + # filename = 'class_graphs/tests/AME3-2_test' + # print filename + # class_graph_data = export_class_graph( + # class_graph, filename, min_edge_reps=False) + + # prime = 3 + # power = 1 + # A = [[1, 1], [1, 2]] + # graph = from_MDS_code(A, prime, power) + # class_graph = explore_lc_orbit(graph) + # filename = 'class_graphs/tests/AME4-3_test' + # print filename + # class_graph_data = export_class_graph( + # class_graph, filename, min_edge_reps=False) + + # prime = 5 + # power = 1 + # A = [[1, 1, 1], [1, 2, 3], [1, 3, 4]] + # graph = from_MDS_code(A, prime, power) + # min_edge_reps = True + # class_graph = explore_lc_orbit(graph) + # filename = 'class_graphs/tests/AME6-5_test' + # print filename + # class_graph_data = export_class_graph( + # class_graph, filename, min_edge_reps=False) diff --git a/gsc/find_all_classes.py b/gsc/find_all_classes.py new file mode 100644 index 0000000..5e12469 --- /dev/null +++ b/gsc/find_all_classes.py @@ -0,0 +1,265 @@ +# Python modules +import os +import csv +import sys +import time +import numpy as np +import networkx as nx +import itertools as it +from time import time +from tqdm import tqdm +from random import shuffle +from pprint import pprint +# Local modules +from gsc.get_nauty import hash_graph +from gsc.psuedo_graphs import * +from gsc.explore_lc_orbit import explore_lc_orbit, export_class_register + + +def init_search_database(prime, power, nodes): + """ Initialises database for class search """ + # Initialises database folders + directory = 'class_databases/' + \ + 'prime_power_p%d_m%d_n%d' % (prime, power, nodes) + if not os.path.exists(directory): + os.makedirs(directory) + if not os.path.exists(directory + '/classes'): + os.makedirs(directory + '/classes') + # Writes edge index (used for edge configurations) to file + all_edges = list(it.combinations(range(nodes), 2)) + filename = directory + '/edge_index.csv' + with open(filename, 'w') as file: + writer = csv.writer(file) + writer.writerows(all_edges) + # Writes the state's parameters to file + filename = directory + '/state_params.csv' + with open(filename, 'w') as file: + writer = csv.writer(file) + writer.writerow([prime, power, nodes]) + # Writes complete list of edge configs to file + max_edges = nodes * (nodes - 1) / 2 + all_edge_configs = it.product(range(prime ** (power ** 2)), + repeat=max_edges) + edge_configs = \ + it.ifilter(lambda a: (max_edges - a.count(0)) >= (nodes - 1), + all_edge_configs) + edge_configs = np.fromiter(it.chain.from_iterable(edge_configs), 'int8') + edge_configs = edge_configs.reshape(-1, max_edges) + filename = directory + '/remaining_graphs.csv' + np.savetxt(filename, edge_configs, fmt='%d', delimiter=',') + # If prime-power, writes psuedo-edge map to file + if power > 1: + c_map = gen_psuedo_graph_edge_map(prime, power).items() + filename = directory + '/psuedo_edge_map.csv' + with open(filename, 'w') as file: + writer = csv.writer(file) + writer.writerows(c_map) + # Creates graph hash directory + open(directory + '/graph_hashes.csv', 'w').close() + return directory + + +def get_next_graph(directory): + """ Pops the next graph to analyse from top of remaining_graphs.txt """ + filename = directory + '/remaining_graphs.csv' + # Reads first line and writes rest to temp. file + with open(filename) as file: + edge_config = file.readline() + # Formats edge config to tuple + if edge_config: + edge_config = [int(i) for i in edge_config[:-1].split(',')] + return edge_config + + +def remove_found_graphs(directory, edge_configs): + """ Removes any found graphs from remaining_graphs.txt """ + edge_configs = [map(str, config) for config in edge_configs] + filename = directory + '/remaining_graphs.csv' + tmp_filename = directory + '/remaining_graphs_tmp.csv' + # Reads configs from file and writes any not in found configs to temp. + with open(filename) as file, open(tmp_filename, "w") as tmp_file: + reader = csv.reader(file) + writer = csv.writer(tmp_file) + for config in reader: + if config in edge_configs: + continue + writer.writerow(config) + # Removes original file and renames temp. to original + os.remove(filename) + os.rename(tmp_filename, filename) + + +def write_hashes(directory, hashes): + """ + Writes a sequence of graph hashes to graph_hashes.csv if they haven't + already been found. + """ + filename = directory + '/graph_hashes.csv' + # Reads in found hashes + with open(filename, 'r') as file: + reader = csv.reader(file) + found_hashes = set([int(h[0]) for h in reader]) + # If none have been found, write hashes to file + hashes = set(hashes) + if hashes - found_hashes == hashes: + hashes = map(lambda h: str(h) + '\n', hashes) + with open(filename, 'a+') as file: + file.writelines(hashes) + # If only a subset of the hashes have been found raise an error + elif hashes - found_hashes != set([]): + # pprint(hashes) + pprint(hashes - found_hashes) + pprint(hashes - (hashes - found_hashes)) + raise Exception("Error: Only some of hashes already known") + sys.exit() + + +def found_hash(directory, target_hash): + """ Returns whether hash has already been found """ + filename = directory + '/graph_hashes.csv' + with open(filename, 'r') as file: + reader = csv.reader(file) + hashes = [int(h[0]) for h in reader] + return target_hash in hashes + + +def make_isomorph_func(edge_index, n): + """ Returns function that makes isomorphic graph edge configurations """ + node_maps = [{i: j for i, j in enumerate(perm)} + for perm in it.permutations(range(n), n)] + iso_indices = [[tuple(sorted([n_map[i], n_map[j]])) for i, j in edge_index] + for n_map in node_maps] + config_perms = [[edge_index.index(edge) for edge in index] + for index in iso_indices] + + def isomorph_configs(edge_config): + iso_configs = [tuple(edge_config[i] for i in perm) + for perm in config_perms] + iso_configs = map(list, set(iso_configs)) + return iso_configs + + return isomorph_configs + + +def remove_disconnected_configs(edge_config, isomorph_configs): + """ Removes all graphs which are siliarly disconnected incl. isomorphs """ + # Finds the edge occupancies of all isomorphic configurations + iso_occs = set(tuple(map(bool, config)) + for config in isomorph_configs(edge_config)) + iso_occs = map(list, list(iso_occs)) + filename = directory + '/remaining_graphs.csv' + tmp_filename = directory + '/remaining_graphs_tmp.csv' + # Reads configs from file and writes any not in found configs to temp. + with open(filename) as file, open(tmp_filename, "w") as tmp_file: + reader = csv.reader(file) + writer = csv.writer(tmp_file) + for config in reader: + config_occ = [bool(int(i)) for i in config] + if config_occ in iso_occs: + continue + writer.writerow(config) + # Removes original file and renames temp. to original + os.remove(filename) + os.rename(tmp_filename, filename) + + +def find_all_classes(directory): + """ Finds all members of all classes """ + # Gets edge indices and state params and generates edge map + filename = directory + '/edge_index.csv' + with open(filename, 'r') as file: + reader = csv.reader(file) + edge_index = [tuple(map(int, edge)) for edge in reader] + filename = directory + '/state_params.csv' + with open(filename, 'r') as file: + reader = csv.reader(file) + p, m, n = map(int, next(reader)) + c_map = gen_psuedo_graph_edge_map(p, m) + # Creates function to produce config isomorphs + isomorph_configs = make_isomorph_func(edge_index, n) + pprint(c_map) + pprint(edge_index) + # Initialises progress bar + rg_file = directory + '/remaining_graphs.csv' + rem_graphs_size = os.path.getsize(rg_file) + pbar = tqdm(total=rem_graphs_size) + print rem_graphs_size + while True: + rem_update = rem_graphs_size - os.path.getsize(rg_file) + if rem_update >= 0: + pbar.update(rem_update) + rem_graphs_size = os.path.getsize(rg_file) + # Waits until a graph is available to process + edge_config = get_next_graph(directory) + if not edge_config: + break + tqdm.write("Psuedo edge config: %s" % (edge_config,)) + # Create initial graph + c_edges = [(u, v, w) for (u, v), w in zip(edge_index, edge_config)] + init_graph = create_psuedo_graph(c_edges, p, m, c_map) + # Checks if graph is connected + if not nx.is_connected(init_graph.to_undirected()): + tqdm.write("Disconnected. Removing isomorphs... ") + # Removes any isomorphic graphs from remaining + remove_disconnected_configs(edge_config, isomorph_configs) + tqdm.write("Done") + continue + init_graph = psuedo_to_real(init_graph) + # Check if graph has already been found for hash test + graph_hash = hash_graph(init_graph) + if found_hash(directory, graph_hash): + tqdm.write("Already found %d" % graph_hash) + iso_configs = isomorph_configs(edge_config) + remove_found_graphs(directory, iso_configs) + continue + # Explore class graph + tqdm.write("Exploring class...") + class_graph = explore_lc_orbit(init_graph, False, False) + class_register = [[node, attrs['edges'], + attrs['hash'], attrs['nx_graph']] + for node, attrs in class_graph.node.iteritems()] + nodes, edges, hashes, graphs = zip(*class_register) + # Formats edge list based on state parameters + if power == 1: + edges = [[(u, v, c) for (u, i), (v, j), c in edge_set] + for edge_set in edges] + if prime == 2: + edges = [[(u, v) for u, v, c in edge_set] + for edge_set in edges] + # Exports class_register to file + tqdm.write("Writing register to file...") + class_register = zip(nodes, edges, hashes) + config_label = '_'.join(map(str, edge_config)) + filename = directory + '/classes/' + config_label + '.csv' + with open(filename, 'w') as csvfile: + writer = csv.writer(csvfile) + writer.writerows(class_register) + # Finds and removes any isomorphs + tqdm.write("Removing isomorphs...") + psu_edges = [real_graph_to_psu_edges(graph, c_map, edge_index) + for graph in graphs] + edge_configs = [[c for u, v, c in w_edges] for w_edges in psu_edges] + iso_configs = [iso_config for config in edge_configs + for iso_config in isomorph_configs(config)] + remove_found_graphs(directory, iso_configs) + # Adds hashes to found hash directory + write_hashes(directory, hashes) + tqdm.write("Done") + pbar.close() + + +if __name__ == '__main__': + prime = 2 + power = 2 + nodes = 3 + print prime, power, nodes + init_search_database(prime, power, nodes) + directory = 'class_databases/' + \ + 'prime_power_p%d_m%d_n%d' % (prime, power, nodes) + + print "Searching" + start = time() + find_all_classes(directory) + end = time() - start + runtime = (int(end / 60), int(end % 60)) + print "Runtime: %dm %ds" % runtime diff --git a/gsc/get_nauty.py b/gsc/get_nauty.py new file mode 100644 index 0000000..b3a3848 --- /dev/null +++ b/gsc/get_nauty.py @@ -0,0 +1,136 @@ +# Python packages +from math import log +import networkx as nx +import pynauty as pyn +from pprint import pprint +from collections import defaultdict +import matplotlib.pyplot as plt +# Local modules +from gsc.utils import flatten, int_to_bits, copy_graph + + +def qudit_graph_map(nx_wg, partition=None): + """ + Maps an edge-weighted NX graph to a node-colored NX graph. + For prime-power graph states, can colour by member or family. + """ + # Gets list of all nodes by layer + us, vs, weights = zip(*nx_wg.edges.data('weight')) + n_layers = int(log(max(weights), 2)) + 1 + layers = range(n_layers) + # If node is prime power, applies colouring across same member-nodes + if nx_wg.__dict__.get('power', 1) > 1: + p, m, f = nx_wg.prime, nx_wg.power, nx_wg.families + # Partitions based on which member of the family node is + if partition == 'member': + coloring = [[(l, (n, i)) for n in range(f)] + for l in layers for i in range(m)] + # Partitions based on which family => must make colourings equiv. + elif partition == 'family': + # Adds extra nodes to represent exchangeable colours + # (see page 60 of nauty user guide v26) + nx_wg = copy_graph(nx_wg) + for u in range(f): + node = (u, m) + nx_wg.add_node(node) + nx_wg.add_weighted_edges_from([(node, (u, i), 1) + for i in range(m)]) + coloring = [[(l, (n, i)) for n in range(f) for i in range(m)] + for l in layers] + \ + [[(l, (n, m)) for n in range(f)] for l in layers] + else: + raise Exception("Unknown colour scheme provided") + else: + coloring = [[(l, n) for n in nx_wg.nodes()] for l in layers] + # Creates layered graph with vertical edges + nx_cg = nx.Graph() + v_nodes = [(l, n) for l in layers for n in nx_wg.nodes()] + nx_cg.add_nodes_from(v_nodes) + v_edges = [((l, n), (l + 1, n)) + for n in nx_wg.nodes() for l in layers[:-1]] + nx_cg.add_edges_from(v_edges) + # Add edges within layers + for u, v, w in nx_wg.edges.data('weight'): + # Gets binary rep. of weight, padded with zeros (written L to R) + bin_w = int_to_bits(w)[::-1] + bin_w = bin_w + (n_layers - len(bin_w)) * [0] + # Converts binary weight to list of layers and adds edges to graph + edge_layers = [l for l, b in enumerate(bin_w) if b] + edges = [((l, u), (l, v)) for l in edge_layers] + nx_cg.add_edges_from(edges) + return nx_cg, coloring + + +def convert_nx_to_pyn(nx_g, partition=None): + """ + Takes a NetworkX graph and outputs a PyNauty graph. + If graph has dimension > 2, converts into layered coloured graph + """ + # If graph represents nD qudit graph, map to coloured layer graph + coloring = [] + edges = tuple(set(nx_g.edges.data('weight'))) + if nx_g.__dict__.get('dimension', 2) > 2: + nx_g, coloring = qudit_graph_map(nx_g, partition) + # Relabels nodes with integers for compatibility with Pynauty + nodes, neighs = zip(*nx_g.adjacency()) + to_int_node_map = {n: i for i, n in enumerate(nodes)} + relabel = to_int_node_map.get + nodes = map(relabel, nodes) + neighs = [map(relabel, node_neighs.keys()) for node_neighs in neighs] + coloring = [set(map(relabel, colour)) for colour in coloring] + # Creates Pynauty graph + graph_adj = {node: node_neighs for node, node_neighs in zip(nodes, neighs)} + n_v = len(graph_adj) + pyn_g = pyn.Graph(n_v, directed=False, adjacency_dict=graph_adj, + vertex_coloring=coloring) + # Finds inverse node labelling + from_int_node_map = {i: n for n, i in to_int_node_map.items()} + return pyn_g, from_int_node_map + + +def hash_graph(graph): + """ Returns a hash for the graph based on PyNauty's certificate fn """ + if graph.__dict__.get('power', 1) > 1: + pyn_g_mem, _ = convert_nx_to_pyn(graph, partition='member') + pyn_g_fam, _ = convert_nx_to_pyn(graph, partition='family') + g_mem_hash = hash(pyn.certificate(pyn_g_mem)) + g_fam_hash = hash(pyn.certificate(pyn_g_fam)) + g_hash = hash((g_mem_hash, g_fam_hash)) + else: + pyn_g, _ = convert_nx_to_pyn(graph) + g_hash = hash(pyn.certificate(pyn_g)) + return g_hash + + +def canonical_relabel(nx_g): + """ Returns isomorphic graph with canonical relabelling """ + nodes, neighs = zip(*nx_g.adjacency()) + pyn_g, node_map = convert_nx_to_pyn(nx_g) + canon_lab = pyn.canon_label(pyn_g) + canon_relab = {node_map[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_rep_nodes(nx_g): + """ + Takes a NetworkX graph and finds groups of nodes that are equivalent + up to automorphism + """ + # Creates PyNauty graph and passes it to PyNauty to get orbits + partition = 'member' if nx_g.__dict__.get('power', 1) > 1 else None + pyn_g, node_map = convert_nx_to_pyn(nx_g, partition=partition) + _, _, _, orbits, _ = pyn.autgrp(pyn_g) + # Finds node equivalency dictionary + node_equivs = defaultdict(list) + for node, equiv in enumerate(orbits): + node_equivs[node_map[equiv]].append(node_map[node]) + # 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 nx_g.degree(node) > 1} + # If multigraph, returns orbits of nodes in first layer + if nx_g.__dict__.get('dimension', 2) > 2: + node_equivs = {u: [v for l_v, v in equivs if l_v == 0] + for (l_u, u), equivs in node_equivs.items() if l_u == 0} + return node_equivs diff --git a/gsc/graph_builders.py b/gsc/graph_builders.py new file mode 100644 index 0000000..4051e75 --- /dev/null +++ b/gsc/graph_builders.py @@ -0,0 +1,126 @@ +# Python packages +import numpy as np +import networkx as nx +import itertools as it +from pprint import pprint +from random import randint +# Local modules +from gsc.utils import * + + +def random_connected_graph(n): + """ Generates a Erdos-Renyi G_{n,m} random graph """ + g = nx.Graph([(0, 1), (2, 3)]) + while not nx.is_connected(g): + edges = randint(n - 1, n * (n - 1) / 2) + g = nx.dense_gnm_random_graph(n, edges) + return g + + +def linear_graph(l): + """ Creates a linear graph with 2D coordinates """ + g = nx.Graph() + edges = [((i, 0), (i + 1, 0)) for i in range(l - 1)] + g.add_edges_from(edges) + return g + + +def square_lattice(n, m, boundary=True): + """ Creates a square lattice with 2D coordinates """ + mod_n = n + 1 if boundary else n + mod_m = m + 1 if boundary else m + g = nx.Graph() + nodes = list(it.product(range(n), range(m))) + edges = flatten([[((i, j), ((i + 1) % mod_n, j)), + ((i, j), (i, (j + 1) % mod_m))] for i, j in nodes]) + edges = [((u_x, u_y), (v_x, v_y)) for (u_x, u_y), (v_x, v_y) in edges + if max([u_x, v_x]) < n and max([u_y, v_y]) < m] + g.add_edges_from(edges) + return g + + +def make_crazy(graph, n): + """ Converts a graph into it's crazily encoded version """ + # Defines groupings of crazy nodes and edges between them + crazy_nodes = {node: [(node, i) for i in range(n)] + for node in graph.nodes()} + crazy_edges = flatten([it.product(crazy_nodes[u], crazy_nodes[v]) + for u, v in graph.edges()]) + # Creates graph and adds edges and nodes + crazy_graph = nx.Graph() + crazy_graph.add_nodes_from(flatten(crazy_nodes.values())) + crazy_graph.add_edges_from(crazy_edges) + # Assigns encoded attribute for to_GraphState + crazy_graph.encoded = True + return crazy_graph + + +def make_ghz_like(graph, n): + """ Converts a graph into it's GHZ-encoded version """ + # Defines groupings of GHZ-like nodes and edges between them + ghz_nodes = {node: [(node, i) for i in range(n)] + for node in graph.nodes()} + ghz_edges = [((u, 0), (v, 0)) for u, v in graph.edges()] + \ + [((node, 0), (node, i)) for node in graph.nodes() for i in range(1, n)] + # Creates graph and adds edges and nodes + ghz_graph = nx.Graph() + ghz_graph.add_nodes_from(flatten(ghz_nodes.values())) + ghz_graph.add_edges_from(ghz_edges) + # Assigns encoded attribute for to_GraphState + ghz_graph.encoded = True + return ghz_graph + + +def create_prime_graph(w_edges, prime): + """ Creates a weighted graph representing a prime qudit graph state """ + if not is_prime(prime): + raise Exception("Graph state must be prime-dimensional") + us, vs, ws = zip(*w_edges) + if max(ws) >= prime or max(ws) < 0: + raise Exception("Weights must be 0 <= w < p ") + nx_wg = nx.Graph() + nx_wg.add_weighted_edges_from(w_edges) + nx_wg.prime = prime + nx_wg.power = 1 + nx_wg.dimension = prime + return nx_wg + + +def create_prime_power_graph(w_edges, prime, power): + """ Creates a weighted graph representing a prime qudit graph state """ + nx_wg = create_prime_graph(w_edges, prime) + nx_wg.power, nx_wg.dimension = power, prime ** power + fam_labels = list(set([n for n, i in nx_wg.nodes()])) + nx_wg.families = len(fam_labels) + fam_nodes = [(n, i) for n in fam_labels for i in range(power)] + # Adds any nodes that weren't in the edge list + nx_wg.add_nodes_from(fam_nodes) + return nx_wg + + +def from_MDS_code(A, prime, power): + """ Creates a graph-state representation of an MDS AME state """ + A = np.array(A) + A_t = np.transpose(A) + k, nmk = A.shape + t_l_zero = np.zeros((k, k), dtype=int) + b_r_zero = np.zeros((nmk, nmk), dtype=int) + top = np.concatenate((t_l_zero, A), axis=1) + bot = np.concatenate((A_t, b_r_zero), axis=1) + adj_mat = np.concatenate((top, bot), axis=0) + graph = nx.from_numpy_array(adj_mat) + graph.prime = prime + graph.power = power + graph.dimension = prime ** power + return graph + + +if __name__ == '__main__': + l = 5 + boundary = False + g = square_lattice(l, l, boundary) + cg = make_crazy(g, 6) + cgs = to_GraphState(cg) + url = 'https://abv.peteshadbolt.co.uk/SMS_crazy_graphs' + cgs.url = url + cgs.push() diff --git a/gsc/is_lc_equiv.py b/gsc/is_lc_equiv.py new file mode 100644 index 0000000..9bfc243 --- /dev/null +++ b/gsc/is_lc_equiv.py @@ -0,0 +1,159 @@ +# Python packages +import os +import csv +import sympy as sp +import numpy as np +import networkx as nx +import itertools as it +from pprint import pprint +from ast import literal_eval +from subprocess import check_output +# Local modules +from gsc.utils import canonical_edge_order, flatten, powerset + +bin2gate = {(1, 0, 0, 1): 'I', (0, 1, 1, 0): 'H', (1, 0, 1, 1): 'S', + (1, 1, 1, 0): 'HS', (0, 1, 1, 1): 'SH', (1, 1, 0, 1): 'HSH'} + + +def get_adjacency_matrix(graph): + """ Returns the adjacency matrix with a canonical node basis """ + # Canonically orders the nodes and edges + key = sorted(graph.nodes()) + edges = canonical_edge_order(graph.edges()) + # Creates the adjacency matrix and exports to CSV + adj_mat = np.array([[int(tuple(sorted((u, v))) in edges) for u in key] + for v in key]) + return adj_mat, key + + +def export_adjacency_matrix(graph, filename): + """ Exports adjacency matrix to CSV file """ + # Gets adjacency matrix + adj_mat, key = get_adjacency_matrix(graph) + # Writes to CSV file + with open(filename, 'wb') as f: + writer = csv.writer(f) + writer.writerows(adj_mat) + return adj_mat, key + + +def to_rref(A): + """ + Takes n x m matrix A to it's reduced row echelon form. + Algorithm from: https://www.di-mgt.com.au/matrixtransform.html + """ + n, m = A.shape + j = 0 + for i in range(n): + # While column j has all zero elements, set j = j+1. If j>m return A. + # print i, j + while all(A[i:, j] == 0): + j += 1 + if j >= m: + return A + # If element a_ij = 0, then swap row i with row x>i where a_xj != 0. + if A[i, j] == 0: + for x in range(i+1, n): + if A[x, j] != 0: + # print x + A[[x, i]] = A[[i, x]] + break + # Divide each element of row i by a_ij, thus making the pivot a_ij = 1. + A[i] /= A[i, j] + # For each row k from 1 to n, with k != i, + # subtract row i multiplied by a_kj from row k. + for k in [k for k in range(n) if k != i]: + A[k] = (A[k] - A[i] * A[k, j]) % 2 + return A + + +def GF2nullspace(A): + """ + Finds nullspace of A using RREF(A). + Follows decription here: + https://math.stackexchange.com/questions/130207/ + finding-null-space-basis-over-a-finite-field + """ + # Takes A to reduced row echelon form and removes any all-zero rows + A = to_rref(A) + A = A[~(A == 0).all(1)] + # Permutes columns of A into [ I_n | P ] form (I_n is n x n, P is n x k) + n, m = A.shape + perms = [] + I = np.eye(n, dtype=int) + for i in range(n): + while A[:, i].tolist() != I[:, i].tolist(): + perm = range(i, m) + A[:, perm] = A[:, range(i + 1, m) + [i]] + perms.append(perm) + P = A[:, n:] + # N(A) is spanned by [P^T | I_k ] (P^T is k x n and I_k is k x k) + N = np.hstack([P.T, np.eye(P.shape[1], dtype=int)]) + # Undo column permutations to retrieve N(A) in original basis + for perm in perms[::-1]: + N[:, perm] = N[:, [perm[-1]] + perm[:-1]] + return N + + +def are_lc_equiv(g1, g2): + """ + Tests whether two graphs are equivalent up to local complementation. + If True, also returns every unitary such that |g2> = U|g1>. + """ + # Gets adjacency matrices and returns false if differing bases + am1, k1 = get_adjacency_matrix(g1) + am2, k2 = get_adjacency_matrix(g2) + dim1, dim2 = len(k1), len(k2) + if k1 != k2 or am1.shape != (dim1, dim1) or am2.shape != (dim2, dim2): + raise False, None + # Defines binary matrices + I = sp.eye(dim1) + S1 = sp.Matrix(am1).col_join(I) + S2 = sp.Matrix(am2).col_join(I) + # Defines symbolic variables + A = sp.symbols('a:' + str(dim1), bool=True) + B = sp.symbols('b:' + str(dim1), bool=True) + C = sp.symbols('c:' + str(dim1), bool=True) + D = sp.symbols('d:' + str(dim1), bool=True) + # Defines solution matrix basis + abcd = flatten(zip(A, B, C, D)) + no_vars = len(abcd) + no_qubits = no_vars / 4 + # Creates symbolic binary matrix + A, B, C, D = sp.diag(*A), sp.diag(*B), sp.diag(*C), sp.diag(*D) + Q = A.row_join(B).col_join(C.row_join(D)) + P = sp.zeros(dim1).row_join(I).col_join(I.row_join(sp.zeros(dim1))) + # Constructs matrix to solve + X = [i for i in S1.T * Q.T * P * S2] + X = np.array([[x.coeff(v) for v in abcd] for x in X], dtype=int) + # Removes any duplicated and all-zero rows + X = np.unique(X, axis=0) + X = X[~(X == 0).all(1)] + # Finds the solutions (the nullspace of X) + V = list(GF2nullspace(X)) + if len(V) > 4: + V = [(v1 + v2) % 2 for v1, v2 in it.combinations(V, 2)] + else: + V = [sum(vs) % 2 for vs in powerset(V)] + V = [np.reshape(v, (no_qubits, 4)) for v in V] + V = [v for v in V if all((a * d + b * c) % 2 == 1 for a, b, c, d in v)] + if V: + V = [[bin2gate[tuple(r)] for r in v] for v in V] + return True, V + else: + return False, None + + +if __name__ == '__main__': + e1 = [(0, 1), (1, 2)] + g1 = nx.Graph(e1) + e2 = [(0, 1), (1, 2), (2, 0)] + g2 = nx.Graph(e2) + print are_lc_equiv(g1, g2) + + e1 = [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (1, 2), (1, 3), + (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)] + e2 = [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5)] + g1 = nx.Graph(e1) + g2 = nx.Graph(e2) + print are_lc_equiv(g1, g2) diff --git a/gsc/psuedo_graphs.py b/gsc/psuedo_graphs.py new file mode 100644 index 0000000..4fc0d20 --- /dev/null +++ b/gsc/psuedo_graphs.py @@ -0,0 +1,94 @@ +# Python modules +import networkx as nx +import itertools as it +# Local modules +from gsc.utils import powerset + + +def gen_psuedo_graph_edge_map(prime, power): + """ + Creates mapping from p^m psuedo graph edges to prime-dimensional graph + state edges (i.e. set of all weighted balanced bipartite graphs between + two m-families). + """ + edges = ([(u, v, w) for w in range(1, prime)] + [()] + for u, v in it.product(range(power), repeat=2)) + edge_sets = ([edge for edge in edge_set if edge != ()] + for edge_set in it.product(*edges)) + c_map = {i: edges for i, edges in enumerate(sorted(edge_sets, key=len))} + return c_map + + +def create_psuedo_graph(c_edges, prime, power, c_map): + """ + Initialises prime power psuedo graph state. + c_edges is list of coloured/weighted edges that are associated to bipartite + prime qudit graphs via c_map + """ + # Initialises directed weighted psuedo graph and adds attributes + nx_wg = nx.DiGraph() + # Adds psuedo edges and real prime edges as edge attribute + nx_wg.add_weighted_edges_from(c_edges) + nx_wg.remove_edges_from([(u, v) for u, v, c in c_edges if c == 0]) + # Add psuedo graph + nx_wg.prime, nx_wg.power, nx_wg.dimension = prime, power, prime ** power + nx_wg.c_map = c_map + return nx_wg + + +def psuedo_to_real(psu_g): + """ Turns psuedo-graph into real prime version """ + prime = psu_g.prime + power = psu_g.power + c_map = psu_g.c_map + real_g = nx.Graph() + nodes = [(n, i) for n in psu_g.nodes() for i in range(power)] + real_g.add_nodes_from(nodes) + edges = [((u, i), (v, j), w) for u, v, c in psu_g.edges(data='weight') + for i, j, w in c_map[c]] + real_g.add_weighted_edges_from(edges) + real_g.families = len(psu_g.nodes()) + real_g.prime, real_g.power, real_g.dimension = prime, power, prime ** power + return real_g + + +def real_graph_to_psu_edges(real_g, c_map, psu_edge_index): + """ + Converts real graph to list of psuedo edges ordered by the pseudo edge + index + """ + # Creates inverse colour map for psuedo edges + inv_c_map = {tuple(sorted(value)): key for key, value in c_map.items()} + # Gets configuration of real edges per psuedo edge + psu_edges = {edge: [] for edge in psu_edge_index} + m = real_g.power + for u, v in psu_edge_index: + # Gets edges of inter-family bipartite graph + bpg_edges = [(i, j, real_g[(u, i)][(v, j)]['weight']) + for i, j in it.product(range(m), repeat=2) + if real_g.has_edge((u, i), (v, j))] + if bpg_edges != []: + psu_edges[(u, v)] = bpg_edges + # Creates weighted directed graph from psuedo edges + psu_edges = [(u, v, inv_c_map[tuple(sorted(psu_edges[(u, v)]))]) + for u, v in psu_edge_index] + return psu_edges + + +def real_to_psuedo(real_g, c_map, psu_edge_index=None): + """ Converts a real graph back to a psuedo graph given a mapping func """ + # Finds value for every possible psuedo edge + psu_nodes = list(set([u for u, i in real_g.nodes()])) + all_psu_edges = psu_edge_index if psu_edge_index \ + else it.combinations(psu_nodes, 2) + psu_edges = real_graph_to_psu_edges(real_g, c_map, all_psu_edges) + psu_g = nx.DiGraph() + psu_g.prime, psu_g.power = real_g.prime, psu_g.power + psu_g.dimension, psu_g.c_map = real_g.dimension, c_map + psu_g.add_weighted_edges_from(psu_edges) + return psu_g + + +if __name__ == '__main__': + prime, power = 3, 1 + print gen_psuedo_graph_edge_map(prime, power) diff --git a/utils.py b/gsc/utils.py similarity index 70% rename from utils.py rename to gsc/utils.py index 93d254c..645ee53 100644 --- a/utils.py +++ b/gsc/utils.py @@ -1,6 +1,7 @@ # Python packages import ast import numpy as np +from math import sqrt from itertools import chain, combinations from collections import defaultdict from math import pi, cos, sin @@ -9,6 +10,16 @@ # Local modules +def copy_graph(graph): + """ Returns copy of graph including graph attributes """ + graph_copy = graph.copy() + attrs = set(dir(graph)) + attrs_copy = set(dir(graph_copy)) + for attr in attrs - attrs_copy: + graph_copy.__dict__[attr] = graph.__dict__[attr] + return graph_copy + + def canonical_edge_order(edges): return tuple(sorted(tuple(sorted(edge)) for edge in edges)) @@ -31,7 +42,7 @@ def to_GraphState(graph, r=0.2): If graph is crazy, lays out crazy nodes radially. """ # Defines qubits and coordinates for crazy or normal graph - if hasattr(graph, 'crazy'): + if hasattr(graph, 'encoded'): crazy_nodes = defaultdict(list) for node in graph.nodes(): crazy_nodes[node[0]].append(node) @@ -68,4 +79,20 @@ def flatten(array, level=1): def powerset(s): - return chain.from_iterable(combinations(s, r) for r in range(1, len(s) + 1)) + """ Returns the powerset of a list (excl. the empty set) """ + return chain.from_iterable(combinations(s, r) + for r in range(1, len(s) + 1)) + + +def int_to_bits(i): + """ Converts integer into list of bits """ + return [int(x) for x in bin(i)[2:]] + + +def is_prime(a): + if a < 2: + return False + for x in range(2, int(sqrt(a)) + 1): + if a % x == 0: + return False + return True diff --git a/gsc/viz.py b/gsc/viz.py new file mode 100644 index 0000000..0b9ac68 --- /dev/null +++ b/gsc/viz.py @@ -0,0 +1,14 @@ +# Local modules +from gsc.utils import to_GraphState + + +def push_graph_to_abv(graph, url=None): + """ + Converts a graph to a ABP GraphState and pushes to abv server. + Returns GraphState for debugging + """ + gs = to_GraphState(graph) + if url is not None: + gs.url = url + gs.push() + return gs diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..b0e5a94 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +filterwarnings = + ignore::DeprecationWarning \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..885c446 --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + +setup(name='gsc', + version='2.0', + description='"gsc" or "graph state compass" is a Python package containing a number of tools used for mapping and depicting the local Clifford equivalence classes of quantum graph states.', + url='https://github.com/sammorley-short/gsc', + author='Sam Morley-Short', + license='GNU General Public License v3.0', + packages=['gsc'], + install_requires=[ + 'abp', + 'numpy', + 'sympy', + 'matplotlib==2.2.3', + 'networkx'], + zip_safe=False) \ No newline at end of file diff --git a/tests/test_README_examples.py b/tests/test_README_examples.py new file mode 100644 index 0000000..858c2e4 --- /dev/null +++ b/tests/test_README_examples.py @@ -0,0 +1,122 @@ +# Import Python packages +import sys +import unittest +import networkx as nx +# Import local modules +from gsc.explore_lc_orbit import explore_lc_orbit, export_class_graph +from gsc.is_lc_equiv import are_lc_equiv +from gsc.graph_builders import create_prime_graph + + +class TestReadmeExamples(unittest.TestCase): + + def setUp(self): + pass + + def test_LC_explore_example(self): + # 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_orbit(graph) + # Export class graph to JSON file + file_prefix = 'L5_class_graph' + cg_data = export_class_graph(class_graph, file_prefix) + # Removes hashes (these may not be the same on a different system) + for node in cg_data['nodes']: + node.pop('hash') + data = {'nodes': + [{'edges': [(0, 1, 1), (1, 2, 1), (2, 3, 1), (3, 4, 1)], 'id': 0}, + {'edges': [(0, 1, 1), (0, 2, 1), (1, 2, 1), (2, 3, 1), (3, 4, 1)], 'id': 1}, + {'edges': [(0, 1, 1), (1, 2, 1), (1, 3, 1), (2, 3, 1), (3, 4, 1)], 'id': 2}, + {'edges': [(0, 1, 1), (0, 2, 1), (0, 3, 1), (1, 2, 1), (1, 3, 1), (3, 4, 1)], 'id': 3}, + {'edges': [(0, 2, 1), (0, 3, 1), (1, 2, 1), (1, 3, 1), (3, 4, 1)], 'id': 4}, + {'edges': [(0, 2, 1), (0, 3, 1), (0, 4, 1), (1, 2, 1), (1, 3, 1), (1, 4, 1), (3, 4, 1)], 'id': 5}, + {'edges': [(0, 2, 1), (0, 3, 1), (0, 4, 1), (1, 2, 1), (1, 3, 1), (1, 4, 1), (2, 3, 1), (2, 4, 1)], 'id': 6}, + {'edges': [(0, 1, 1), (0, 2, 1), (0, 3, 1), (0, 4, 1), (1, 2, 1), (1, 3, 1), (1, 4, 1), (3, 4, 1)], 'id': 7}, + {'edges': [(0, 1, 1), (0, 2, 1), (0, 3, 1), (0, 4, 1), (2, 3, 1), (2, 4, 1)], 'id': 8}, + {'edges': [(0, 1, 1), (0, 2, 1), (1, 2, 1), (2, 3, 1), (2, 4, 1), (3, 4, 1)], 'id': 9}], + 'links': + [{'source': 0, 'equivs': [[1, 3]], 'target': 1, 'ops': ['LC']}, + {'source': 0, 'equivs': [[2]], 'target': 2, 'ops': ['LC']}, + {'source': 1, 'equivs': [[2]], 'target': 8, 'ops': ['LC']}, + {'source': 1, 'equivs': [[0, 1, 3, 4]], 'target': 9, 'ops': ['LC']}, + {'source': 2, 'equivs': [[1, 3]], 'target': 3, 'ops': ['LC']}, + {'source': 3, 'equivs': [[2]], 'target': 4, 'ops': ['LC']}, + {'source': 3, 'equivs': [[3]], 'target': 5, 'ops': ['LC']}, + {'source': 4, 'equivs': [[3, 4]], 'target': 8, 'ops': ['LC']}, + {'source': 4, 'equivs': [[3, 4]], 'target': 7, 'ops': ['LC']}, + {'source': 5, 'equivs': [[0, 1]], 'target': 6, 'ops': ['LC']}, + {'source': 5, 'equivs': [[2]], 'target': 7, 'ops': ['LC']}, + {'source': 6, 'equivs': [[2]], 'target': 9, 'ops': ['LC']}, + {'source': 7, 'equivs': [[0, 1]], 'target': 8, 'ops': ['LC']}]} + self.maxDiff = None + self.assertEqual(data, cg_data) + + def test_LC_equiv_example(self): + # 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) + self.assertEqual((is_equiv, local_us), (False, None)) + # Checks equivalence between graph A and graph C + is_equiv, local_us = are_lc_equiv(graph_a, graph_c) + target_us = [['I', 'H', 'H', 'I'], ['I', 'H', 'SH', 'S'], + ['S', 'SH', 'H', 'I'], ['S', 'SH', 'SH', 'S']] + self.assertEqual((is_equiv, local_us), (True, target_us)) + + def test_prime_dimension_explore_example(self): + # Create the input graph + prime = 3 + w_edges = [(0, 1, 1), (1, 2, 2)] + qutrit_g = create_prime_graph(w_edges, prime) + # Find the class graph + class_graph = explore_lc_orbit(qutrit_g) + # Export class graph to JSON file + filename = 'qutrit_class_graph' + cg_data = \ + export_class_graph(class_graph, filename) + # Removes hashes (these may not be the same on a different system) + for node in cg_data['nodes']: + node.pop('hash') + data = {'nodes': + [{'edges': [(0, 1, 1), (1, 2, 2)], 'id': 0}, + {'edges': [(0, 1, 2), (1, 2, 2)], 'id': 1}, + {'edges': [(0, 1, 1), (0, 2, 2), (1, 2, 2)], 'id': 2}, + {'edges': [(0, 1, 1), (0, 2, 1), (1, 2, 2)], 'id': 3}, + {'edges': [(0, 1, 1), (1, 2, 1)], 'id': 4}, + {'edges': [(0, 1, 1), (0, 2, 1), (1, 2, 1)], 'id': 5}, + {'edges': [(0, 1, 2), (0, 2, 2), (1, 2, 2)], 'id': 6}], + 'links': + [{'source': 0, 'equivs': [[1]], 'target': 0, 'ops': ['EM2']}, + {'source': 0, 'equivs': [[0]], 'target': 1, 'ops': ['EM2']}, + {'source': 0, 'equivs': [[1], [1, 0]], 'target': 2, 'ops': ['LC1', 'LC2']}, + {'source': 0, 'equivs': [[1], [1, 2]], 'target': 3, 'ops': ['LC2', 'LC1']}, + {'source': 0, 'equivs': [[2]], 'target': 4, 'ops': ['EM2']}, + {'source': 1, 'equivs': [[2], [1]], 'target': 2, 'ops': ['LC2', 'LC1']}, + {'source': 1, 'equivs': [[1]], 'target': 4, 'ops': ['EM2']}, + {'source': 1, 'equivs': [[1, 0, 2], [1]], 'target': 6, 'ops': ['LC1', 'LC2']}, + {'source': 2, 'equivs': [[1, 0]], 'target': 2, 'ops': ['EM2']}, + {'source': 2, 'equivs': [[1, 2], [1, 0]], 'target': 3, 'ops': ['LC2', 'LC1']}, + {'source': 2, 'equivs': [[1, 0, 2]], 'target': 5, 'ops': ['EM2']}, + {'source': 2, 'equivs': [[1, 0, 2], [2]], 'target': 6, 'ops': ['LC2', 'LC1']}, + {'source': 3, 'equivs': [[1, 2]], 'target': 3, 'ops': ['EM2']}, + {'source': 3, 'equivs': [[1], [0]], 'target': 4, 'ops': ['LC2', 'LC1']}, + {'source': 3, 'equivs': [[1, 0, 2], [0]], 'target': 5, 'ops': ['LC1', 'LC2']}, + {'source': 3, 'equivs': [[0]], 'target': 6, 'ops': ['EM2']}, + {'source': 4, 'equivs': [[1], [1, 0, 2]], 'target': 5, 'ops': ['LC1', 'LC2']}]} + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_explore_class.py b/tests/test_explore_class.py index 75f269f..6b1abdb 100644 --- a/tests/test_explore_class.py +++ b/tests/test_explore_class.py @@ -4,10 +4,12 @@ import unittest import networkx as nx from abp import GraphState +from pprint import pprint # Local modules -sys.path.append('..') -from utils import canonical_edge_order -from explore_class import local_complementation +from gsc.utils import canonical_edge_order +from gsc.is_lc_equiv import are_lc_equiv +from gsc.explore_lc_orbit import qubit_LC, explore_lc_orbit +from gsc.graph_builders import create_prime_power_graph def to_GraphState(graph): @@ -34,7 +36,7 @@ class TestExploreClass(unittest.TestCase): def setUp(self): pass - def test_local_complementation(self): + def test_qubit_LC(self): """ Tests local complementation works against abp version """ for _ in range(100): # Creates a random NetworkX graph and it's equivalent GraphState @@ -46,7 +48,7 @@ def test_local_complementation(self): # Randomly picks a node for local complementation lc_node = random.choice(list(g.nodes())) # Performs local complementation on both graphs - lc_g = local_complementation(g, lc_node) + lc_g = qubit_LC(g, lc_node, 1) lc_gs = to_GraphState(g) lc_gs.local_complementation(lc_node) # Checks that their edgelists are equal @@ -54,7 +56,7 @@ def test_local_complementation(self): lc_gs_edges = canonical_edge_order(lc_gs.edgelist()) self.assertEqual(lc_g_edges, lc_gs_edges) # Performs a second local complementation on same node - lc_lc_g = local_complementation(lc_g, lc_node) + lc_lc_g = qubit_LC(lc_g, lc_node, 1) lc_lc_gs = to_GraphState(lc_g) lc_lc_gs.local_complementation(lc_node) # Checks edgelists are equal and also equal to the originals @@ -64,6 +66,55 @@ def test_local_complementation(self): self.assertEqual(lc_lc_g_edges, g_edges) self.assertEqual(lc_lc_gs_edges, gs_edges) + def test_explore_lc_orbit(self): + """ Generates class graphs for random graphs """ + for _ in range(10): + # Creates a random NetworkX graph and it's equivalent GraphState + g = gen_random_connected_graph(7) + class_graph = explore_lc_orbit(g, verbose=False) + orbit_hashes = set([graph['hash'] for graph + in class_graph.node.values()]) + self.assertEqual(len(class_graph.node), len(orbit_hashes)) + graphs = [graph['nx_graph'] for graph in class_graph.node.values()] + for i in range(10): + graph_a = random.choice(graphs) + graph_b = random.choice(graphs) + lc_equiv, lc_ops = are_lc_equiv(graph_a, graph_b) + self.assertTrue(lc_equiv) + + def test_ququart_pair(self): + """ Tests working for ququart entangled pair LC classes """ + # Tests first equivalence class + edges = [((0, 0), (1, 0), 1), ((0, 1), (1, 1), 1)] + graph = create_prime_power_graph(edges, 2, 2) + class_graph = explore_lc_orbit(graph, verbose=False) + register = set(tuple(map(tuple, attrs['edges'])) + for node, attrs in class_graph.node.iteritems()) + target = \ + [(((0, 1), (1, 0), 1), ((0, 0), (1, 1), 1)), + (((0, 1), (1, 0), 1), ((0, 1), (1, 1), 1), ((0, 0), (1, 1), 1)), + (((0, 1), (1, 0), 1), ((1, 0), (0, 0), 1), ((0, 0), (1, 1), 1)), + (((0, 1), (1, 1), 1), ((1, 0), (0, 0), 1)), + (((0, 1), (1, 1), 1), ((1, 0), (0, 0), 1), ((0, 0), (1, 1), 1))] + target = set(target) + self.assertEqual(register, target) + # Tests second equivalence class + edges = [((0, 0), (1, 0), 1)] + graph = create_prime_power_graph(edges, 2, 2) + class_graph = explore_lc_orbit(graph, verbose=False) + register = set(tuple(map(tuple, attrs['edges'])) + for node, attrs in class_graph.node.iteritems()) + target = \ + [(((0, 1), (1, 0), 1),), + (((0, 1), (1, 0), 1), ((0, 1), (1, 1), 1)), + (((0, 1), (1, 0), 1), ((0, 1), (1, 1), 1), + ((1, 0), (0, 0), 1), ((0, 0), (1, 1), 1)), + (((0, 1), (1, 0), 1), ((1, 0), (0, 0), 1)), + (((0, 1), (1, 1), 1),), + (((1, 0), (0, 0), 1),)] + target = set(target) + self.assertEqual(register, target) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_get_nauty.py b/tests/test_get_nauty.py index 8d77543..bb3f069 100644 --- a/tests/test_get_nauty.py +++ b/tests/test_get_nauty.py @@ -5,11 +5,10 @@ import pynauty as pyn import networkx as nx # Local modules -sys.path.append('..') -from get_nauty import find_unique_lcs, convert_nx_to_pyn, hash_graph, \ - canonical_relabel -from explore_class import local_complementation -from utils import canonical_edge_order +from gsc.get_nauty import find_rep_nodes, hash_graph, canonical_relabel +from gsc.explore_lc_orbit import qubit_LC +from gsc.graph_builders import create_prime_power_graph +from gsc.utils import canonical_edge_order def gen_random_connected_graph(n, p=0.1): @@ -35,12 +34,12 @@ class TestGetNauty(unittest.TestCase): def setUp(self): pass - def test_find_unique_lcs(self): + def test_find_rep_nodes(self): for _ in range(100): g = gen_random_connected_graph(10) - g_equivs = find_unique_lcs(g) + g_equivs = find_rep_nodes(g) for rep_node, equiv_nodes in g_equivs.iteritems(): - lc_equiv_graphs = [local_complementation(g, node) + lc_equiv_graphs = [qubit_LC(g, node) for node in equiv_nodes] lc_equiv_hashes = list(set(map(hash_graph, lc_equiv_graphs))) self.assertEqual(len(lc_equiv_hashes), 1) @@ -66,13 +65,36 @@ def test_canonical_relabel(self): canon_relab_edges = canonical_edge_order(canon_relab_g.edges()) self.assertEqual(canon_edges, canon_relab_edges) + def test_prime_power_hash_examples(self): + # Tests that two C-shaped prime power graphs are equivalent + prime, power = 2, 2 + e1 = [((0, 1), (1, 1), 1), ((1, 0), (0, 0), 1), ((0, 0), (0, 1), 1)] + e2 = [((0, 1), (1, 1), 1), ((1, 0), (0, 0), 1), ((1, 0), (1, 1), 1)] + g1 = create_prime_power_graph(e1, prime, power) + g2 = create_prime_power_graph(e2, prime, power) + self.assertEqual(hash_graph(g1), hash_graph(g2)) + + # Tests upper ququart bar and lower ququart bar inequivalent + e1 = [((1, 0), (0, 0), 1)] + e2 = [((0, 1), (1, 1), 1)] + g1 = create_prime_power_graph(e1, prime, power) + g2 = create_prime_power_graph(e2, prime, power) + self.assertNotEqual(hash_graph(g1), hash_graph(g2)) + + # Tests both ququart zigzags are equivalent + e1 = [((0, 1), (1, 1), 1), ((1, 0), (0, 0), 1), ((0, 0), (1, 1), 1)] + e2 = [((0, 1), (1, 1), 1), ((1, 0), (0, 0), 1), ((0, 1), (1, 0), 1)] + g1 = create_prime_power_graph(e1, prime, power) + g2 = create_prime_power_graph(e2, prime, power) + self.assertEqual(hash_graph(g1), hash_graph(g2)) + + # Checks two-bar and rotated two-bar ququarts are inequivalent + e1 = [((0, 1), (1, 1), 1), ((0, 0), (1, 0), 1)] + e2 = [((0, 0), (0, 1), 1), ((1, 0), (1, 1), 1)] + g1 = create_prime_power_graph(e1, prime, power) + g2 = create_prime_power_graph(e2, prime, power) + self.assertNotEqual(hash_graph(g1), hash_graph(g2)) + if __name__ == '__main__': unittest.main() - - # for _ in range(10): - # g = gen_random_connected_graph(4) - # relab_g = random_relabel(g) - # print g.edges() - # print relab_g.edges() - # print g.edges() == relab_g.edges() diff --git a/tests/test_graph_builders.py b/tests/test_graph_builders.py index e8ddbf8..1148396 100644 --- a/tests/test_graph_builders.py +++ b/tests/test_graph_builders.py @@ -4,14 +4,18 @@ import networkx as nx import itertools as it # Local modules -sys.path.append('..') -from utils import flatten -from graph_builders import linear_graph, make_crazy +from gsc.utils import flatten +from gsc.graph_builders import linear_graph, make_crazy, from_MDS_code, \ + create_prime_graph, create_prime_power_graph -def process_graph_nodes_edges(graph): +def process_graph_nodes_edges(graph, data=None): g_nodes = sorted(graph.nodes()) - g_edges = sorted([tuple(sorted(edge)) for edge in graph.edges()]) + if data is None: + g_edges = sorted([tuple(sorted(edge)) for edge in graph.edges()]) + else: + g_edges = sorted([tuple(sorted((u, v)) + [d]) for u, v, d + in graph.edges(data='weight')]) return g_nodes, g_edges @@ -44,6 +48,42 @@ def test_crazy_linear_graph_builder(self): self.assertEqual(cg_nodes, nodes) self.assertEqual(cg_edges, edges) + def test_from_MDS_code(self): + """ Tests building an AME graph state from an MDS code """ + prime, power = 5, 1 + A = [[1, 1, 1], [1, 2, 3], [1, 3, 4]] + graph = from_MDS_code(A, prime, power) + target_edges = [(0, 3, 1), (0, 4, 1), (0, 5, 1), (1, 3, 1), (1, 4, 2), + (1, 5, 3), (2, 3, 1), (2, 4, 3), (2, 5, 4)] + self.assertEqual(type(graph), nx.Graph) + self.assertEqual(list(graph.edges(data='weight')), target_edges) + + def test_create_prime_graph(self): + nodes, prime = 10, 5 + w_edges = [(i, i+1 % nodes, i % prime) for i in range(nodes)] + g = create_prime_graph(w_edges, prime) + self.assertEqual(g.prime, prime) + self.assertEqual(g.power, 1) + self.assertEqual(g.dimension, prime) + self.assertEqual(type(g), nx.Graph) + self.assertEqual(list(g.edges(data='weight')), w_edges) + + def test_create_prime_power_graph(self): + nodes, prime, power = 6, 7, 3 + all_nodes = [(i, j) for i in range(nodes) for j in range(power)] + w_edges = [((0, 0), (1, 0), 1), ((0, 0), (1, 1), 3), + ((0, 0), (2, 0), 6), ((0, 0), (4, 2), 2), + ((0, 2), (5, 2), 4), ((1, 0), (3, 0), 5), + ((1, 0), (4, 2), 3), ((1, 1), (2, 2), 2), + ((2, 1), (4, 2), 2), ((4, 0), (5, 1), 5)] + g = create_prime_power_graph(w_edges, prime, power) + nodes, edges = process_graph_nodes_edges(g, data='weight') + self.assertEqual(type(g), nx.Graph) + self.assertEqual(g.prime, prime) + self.assertEqual(g.power, power) + self.assertEqual(edges, w_edges) + self.assertEqual(nodes, all_nodes) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_is_lc_equiv.py b/tests/test_is_lc_equiv.py index c42e331..c3cf022 100644 --- a/tests/test_is_lc_equiv.py +++ b/tests/test_is_lc_equiv.py @@ -5,11 +5,10 @@ import networkx as nx from random import randint # Local modules -sys.path.append('..') -from utils import canonical_edge_order -from graph_builders import random_connected_graph -from explore_class import apply_lcs -from is_lc_equiv import get_adjacency_matrix, are_lc_equiv +from gsc.utils import canonical_edge_order +from gsc.graph_builders import random_connected_graph +from gsc.explore_lc_orbit import apply_qubit_LCs +from gsc.is_lc_equiv import get_adjacency_matrix, are_lc_equiv class TestIsLCEquiv(unittest.TestCase): @@ -26,7 +25,7 @@ def test_is_lc_equiv(self): for _ in range(100): lc_nodes = [randint(0, n - 1) for _ in range(lcs)] graph_init = random_connected_graph(n) - graph_fin = apply_lcs(graph_init, lc_nodes) + graph_fin = apply_qubit_LCs(graph_init, lc_nodes) lc_equiv, lc_ops = are_lc_equiv(graph_init, graph_fin) self.assertTrue(lc_equiv) @@ -37,7 +36,7 @@ def test_get_adjacency_matrix(self): graph = random_connected_graph(n) nodes = sorted(graph.nodes()) test_adj_mat, key = get_adjacency_matrix(graph) - nx_adj_mat = nx.to_numpy_matrix(graph, nodelist=nodes, dtype=int) + nx_adj_mat = nx.to_numpy_array(graph, nodelist=nodes, dtype=int) self.assertEqual(nodes, key) self.assertEqual(test_adj_mat.tolist(), nx_adj_mat.tolist()) diff --git a/tests/test_viz.py b/tests/test_viz.py index b2a0227..e943fe9 100644 --- a/tests/test_viz.py +++ b/tests/test_viz.py @@ -5,12 +5,11 @@ import networkx as nx from abp import GraphState # Local modules -sys.path.append('..') -import viz -from graph_builders import linear_graph, make_crazy -from utils import to_GraphState +import gsc.viz as viz +from gsc.graph_builders import linear_graph, make_crazy +from gsc.utils import to_GraphState -url = "https://abv.peteshadbolt.co.uk/beam-robot-reckless-facade" +URL = "https://abv.peteshadbolt.co.uk/beam-robot-reckless-facade" def encode_dict(d, codec='utf8'): @@ -51,13 +50,13 @@ def test_linear_graph_push_to_abv(self): # Creates linear graph g = linear_graph(10) # Pushes to abv - viz.push_graph_to_abv(g, url=url) + viz.push_graph_to_abv(g, url=URL) # Checks pulled graph same as pushed gs1 = to_GraphState(g) gs1_nodes = {str(key): value for key, value in gs1.node.items()} gs1_edges = sorted(gs1.edgelist()) # Gets pushed graph state nodes & edges - gs2_nodes, gs2_edges = pull_GraphState_nodes_edges(url) + gs2_nodes, gs2_edges = pull_GraphState_nodes_edges(URL) self.assertEqual(gs1_nodes, gs2_nodes) self.assertEqual(gs1_edges, gs2_edges) @@ -68,13 +67,13 @@ def test_crazy_linear_graph_push_to_abv(self): g = linear_graph(10) cg = make_crazy(g, 10) # Pushes to abv - viz.push_graph_to_abv(cg, url=url) + viz.push_graph_to_abv(cg, url=URL) # Checks pulled graph same as pushed gs1 = to_GraphState(cg) gs1_nodes = {str(key): value for key, value in gs1.node.items()} gs1_edges = sorted(gs1.edgelist()) # Gets pushed graph state nodes & edges - gs2_nodes, gs2_edges = pull_GraphState_nodes_edges(url) + gs2_nodes, gs2_edges = pull_GraphState_nodes_edges(URL) self.assertEqual(gs1_nodes, gs2_nodes) self.assertEqual(gs1_edges, gs2_edges)