diff --git a/Makefile b/Makefile index 1b805ae..ca50c64 100644 --- a/Makefile +++ b/Makefile @@ -6,5 +6,8 @@ CFLAGS = -Wall -Ofast all: SOCgen SOCadmissible SOCgraphviz +clean: + rm -v SOCgen SOCadmissible SOCgraphviz + %: src/%.c gcc $(CFLAGS) -o $@ $^ diff --git a/README.md b/README.md index 7f160a6..4bd52c2 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ SPDX-License-Identifier: GPL-3.0-or-later # SOC Observation Code ## Description -SOC stands for **S**OC **O**bservation **C**ode, and is composed of three C programs: +SOC stands for **S**OC **O**bservation **C**ode, and is composed of three C programs and a python script: - One, `SOCgen`, to generate SOC graphs (here, SOC stands for Siblings-on-Cycles), - and another, `SOCadmissible`, to verify the admissibility of these graphs as quantum causal structures, -- and the last, `SOCgraphviz`, to translate adjancency matrices into the [Graphviz language](https://graphviz.org/). +- and the last, `SOCgraphviz`, to translate adjacency matrices into the [Graphviz language](https://graphviz.org/). +- The python script `/tools/removeiso.py` removes isomorphic graphs. The first two programs are used in support of Conjecture 1 in the article [Admissible Causal Structures and Correlations, arXiv:2210.12796 \[quant-ph\]](https://arxiv.org/abs/2210.12796). @@ -31,16 +32,17 @@ To display help and exit, run the respective program without command-line argume ### SOCgen ``` $ ./SOCgen -Usage: ./SOCgen -n [-r ] [FILTER ...] +Usage: ./SOCgen -n [-r ] [--grpahviz] [FILTER ...] -n Generate SOCs with `order' connected nodes -r Pick directed graphs at random, and exit after having found `num' SOCs + --graphviz Output SOCs in Graphviz format, arcs of common parents are highlighted [FILTER] Consider only simple directed graphs ... -c ... that are cyclic (i.e., not DAGs) --no-sink ... without sink nodes (this logically implies -c) --no-source ... without source nodes (also this logically implies -c) -This program prints the found SOCs as adjacency matrices to stdout. +This program prints the found SOCs as adjacency matrices to stdout, unless --grpahviz has been specified. To exclude (some) of the isomorphic SOCs, it uses a degree-order filter. ``` @@ -75,8 +77,17 @@ Usage: ./SOCgraphviz This program translates to adjacency matrices into the Graphviz format, and prints them to stdout. ``` +### removeiso.py +``` +$ ./tools/removeiso.py +Usage: ./tools/removeiso.py -f + +Filters out the isomorphic graphs in `filename'. +``` +This script requires [NumPy](https://numpy.org/) and [NetworkX](https://networkx.org/). + ### Examples -To generate all SOCs with three nodes, and save them in the file `3.soc`, run: +To generate all SOCs with three nodes, and save them in the file `3.soc`, you may run: ``` $ ./SOCgen -n 3 > 3.soc Generating SOCs with 3 nodes @@ -93,7 +104,11 @@ These graphs are admissible The SOCs generated can easily be displayed with the [Graphviz](https://graphviz.org/) tools: ``` -./SOCgraphviz 3.soc | dot | gvpack | circo -Nshape=point -Tx11 +$ ./SOCgraphviz 3.soc | dot | gvpack | circo -Nshape=point -Tx11 +``` +also with the `--graphviz` option: +``` +$ ./SOCgen -n 3 --graphviz | dot | gvpack | neato -Tx11 ``` or in [Wolfram Mathematica](https://www.wolfram.com/mathematica/) using: ``` @@ -102,10 +117,11 @@ SOCs = DeleteDuplicatesBy[SOCs, CanonicalGraph]; SOCs ``` -To generate all SOCs with four nodes, and to display them using Graphviz, you may run: +To generate all SOCs with four nodes, save them in the file `4.soc`, remove isomorphic ones (as stated below, `SOCgen` only rudimentary checks for isomorphic graphs), and to display them using Graphviz, you may run: ``` -./SOCgen -n 4 | ./SOCgraphviz /dev/stdin | dot | gvpack | circo -Nshape=point -Tx11 +$ ./SOCgen -n 4 | tee 4.soc | ./tools/removeiso.py -f 4.soc | ./SOCgraphviz /dev/stdin | dot | gvpack | circo -Nshape=point -Tx11 ``` + ![All SOCs with four nodes](./example.png "SOCs with four nodes") ## Limitations diff --git a/example.png b/example.png index d7ffd58..0b86cf8 100644 Binary files a/example.png and b/example.png differ diff --git a/src/SOCadmissible.c b/src/SOCadmissible.c index 424f94a..20e9e50 100644 --- a/src/SOCadmissible.c +++ b/src/SOCadmissible.c @@ -130,7 +130,7 @@ int nextintervention(int n, int *interventionslen, int *interventionidx) { * With this, we might find some fixed-points entries without the need of entering any recursion. * If `party' is not in the range of a parent's intervention, then `party' will definitely receive a 0. * If `party' has no parents, then `party' will definitely receive a 1. - * If the fixed-point value cannot be inferred, return -1, otherwise return the fixed-point entry. + * If the fixed-point value cannot be inferred, return -1. ***/ int alphapre(int party, const int *parents, const int *parentslen, const int *interventions, const int *interventionidx, int maxn) { if(parentslen[party] == 0) diff --git a/src/SOCgen.c b/src/SOCgen.c index 6e53bcc..eb9a1a9 100644 --- a/src/SOCgen.c +++ b/src/SOCgen.c @@ -31,6 +31,24 @@ void dumpgraph(int n, const int *children, const int *childrenlen) { printf("}\n"); } +/*** + * Print the graph as graphviz command + ***/ +void dumpgv(int n, int graphnr, const int *children, const int *childrenlen) { + printf("strict digraph G%d {node[shape=point];", graphnr); + for(int v=0; v G%dN%d", graphnr, v, graphnr, w); + if(childrenlen[v] >= 2) + printf(" [color=\"red\"]"); + printf(";"); + } + } + printf("}\n"); + return; +} + /*** * Block size of our datastructure. ***/ @@ -112,10 +130,10 @@ void graphnrtolists(int n, unsigned long long int graph, int *parents, int *pare void canonical_cycle(int n, int *cycle, int *cyclelen, const int *path, int pathlen) { int startidx; int pathhascycle = 0; - const int last = path[pathlen-1]; // This is the last node + const int last = path[pathlen-1]; // This is the last node for(int k=0; k [-r ] [FILTER ...]\n", argv[0]); + fprintf(stderr, "Usage: %s -n [-r ] [--grpahviz] [FILTER ...]\n", argv[0]); fprintf(stderr, " -n Generate SOCs with `order' connected nodes\n"); fprintf(stderr, " -r Pick directed graphs at random, and exit after having found `num' SOCs\n"); + fprintf(stderr, " --graphviz Output SOCs in Graphviz format, arcs of common parents are highlighted\n"); fprintf(stderr, "\n"); fprintf(stderr, "[FILTER] Consider only simple directed graphs ...\n"); fprintf(stderr, " -c ... that are cyclic (i.e., not DAGs)\n"); fprintf(stderr, " --no-sink ... without sink nodes (this logically implies -c)\n"); fprintf(stderr, " --no-source ... without source nodes (also this logically implies -c)\n"); fprintf(stderr, "\n"); - fprintf(stderr, "This program prints the found SOCs as adjacency matrices to stdout.\n"); + fprintf(stderr, "This program prints the found SOCs as adjacency matrices to stdout, unless --grpahviz has been specified.\n"); fprintf(stderr, "To exclude (some) of the isomorphic SOCs, it uses a degree-order filter.\n"); return -1; } @@ -440,7 +462,7 @@ int main(int argc, char *argv[]) { fprintf(stderr, "Too many graphs to enumarate with an unsiged long long integer (number of node pairs = %d)\n", m); return -1; } - const unsigned long long max = 1L << m; // Largest `graphnumber' + unsigned long long max = 1L << m; // Largest `graphnumber' const int padlen = (int)((float)m/3.322)+1; // Convert log 2 to log 10 int len = 0; time_t t0 = time(NULL); @@ -506,7 +528,10 @@ int main(int argc, char *argv[]) { if(num_cycles > 0 && !gissoc(n, parents, parentslen, children, childrenlen, cycles, cyclescnt)) continue; // We have found a SOC len++; - dumpgraph(n, children, childrenlen); + if(GRAPHVIZ) + dumpgv(n, graphnumber, children, childrenlen); + else + dumpgraph(n, children, childrenlen); } const int deltat = time(NULL) - t0; const float rate = (deltat == 0) ? graphschecked : (float)graphschecked/(float)deltat; diff --git a/tools/removeiso.py b/tools/removeiso.py new file mode 100755 index 0000000..f9337dd --- /dev/null +++ b/tools/removeiso.py @@ -0,0 +1,77 @@ +#!/usr/bin/python3 +""" +SPDX-FileCopyrightText: 2023 Ă„min Baumeler and Eleftherios-Ermis Tselentis + +SPDX-License-Identifier: GPL-3.0-or-later +""" +import sys +import getopt +import numpy as np +import networkx as nx + +"""Parse command line arguments first.""" +helpstr = """Usage: """+sys.argv[0]+""" -f + +Filters out the isomorphic graphs in `filename'.""" + +FILENAME = False + +try: + opts, args = getopt.getopt(sys.argv[1:], "hf:") +except getopt.GetoptError: + print(helpstr, file=sys.stderr) + sys.exit(2) +for opt, arg in opts: + if opt == '-h': + print(helpstr) + sys.exit() + elif opt == '-f': + FILENAME = arg + +if not FILENAME: + print(helpstr, file=sys.stderr) + sys.exit(2) + + +with open(FILENAME, 'rb') as f: + num_lines = sum(1 for _ in f) +print(f"Total number of digraphs is {num_lines}", file=sys.stderr) + + +nonisodigraphs = set() + +cnt = 0 +fnd = 0 +with open(FILENAME, 'r') as f: + while 1: + cnt = cnt + 1 + print("\r{:.2f}%".format(100*cnt/num_lines), end='', file=sys.stderr) + line = f.readline().strip('{}\n') + if not line: + break + G = nx.from_numpy_matrix(np.vectorize(int)(np.matrix([x.split(',') for x in line.split('},{')])), create_using=nx.DiGraph) + + couldbeold = False + for H in nonisodigraphs: + # Returns False if graphs are definitely not isomorphic. + # True does NOT guarantee isomorphism. + if nx.faster_could_be_isomorphic(G, H): + couldbeold = True + break + + if not couldbeold: + nonisodigraphs.add(G) + fnd = fnd + 1 + print(f"{{{{{line}}}}}") + continue + + new = True + for H in nonisodigraphs: + if nx.is_isomorphic(G, H): + new = False + break + if new: + nonisodigraphs.add(G) + fnd = fnd + 1 + print(f"{{{{{line}}}}}") + print(f"\r100% Found {fnd} non-isomoric graphs", file=sys.stderr)