def GraphIsomorphism(G1, G2): """Graph Isomorphism formula The formula is the CNF encoding of the statement that two simple graphs G1 and G2 are isomorphic. Parameters ---------- G1 : networkx.Graph an undirected graph object G2 : networkx.Graph an undirected graph object Returns ------- A CNF formula which is satiafiable if and only if graphs G1 and G2 are isomorphic. """ F = CNF() F.header = "Graph Isomorphism problem between graphs " +\ G1.name + " and " + G2.name + "\n" + F.header F.mode_strict() U=enumerate_vertices(G1) V=enumerate_vertices(G2) var = _graph_isomorphism_var for (u, v) in product(U,V): F.add_variable(var(u, v)) # Defined on both side for u in U: F.add_clause([(True, var(u, v)) for v in V]) for v in V: F.add_clause([(True, var(u, v)) for u in U]) # Injective on both sides for u in U: for v1, v2 in combinations(V, 2): F.add_clause([(False, var(u, v1)), (False, var(u, v2))]) for v in V: for u1, u2 in combinations(U, 2): F.add_clause([(False, var(u1, v)), (False, var(u2, v))]) # Edge consistency for u1, u2 in combinations(U, 2): for v1, v2 in combinations(V, 2): if G1.has_edge(u1, u2) != G2.has_edge(v1, v2): F.add_clause([(False, var(u1, v1)), (False, var(u2, v2))]) F.add_clause([(False, var(u1, v2)), (False, var(u2, v1))]) return F
def GraphIsomorphism(G1, G2): """Graph Isomorphism formula The formula is the CNF encoding of the statement that two simple graphs G1 and G2 are isomorphic. Parameters ---------- G1 : networkx.Graph an undirected graph object G2 : networkx.Graph an undirected graph object Returns ------- A CNF formula which is satiafiable if and only if graphs G1 and G2 are isomorphic. """ F = CNF() F.header = "Graph Isomorphism problem between graphs " +\ G1.name + " and " + G2.name + "\n" + F.header U = enumerate_vertices(G1) V = enumerate_vertices(G2) var = _graph_isomorphism_var for (u, v) in product(U, V): F.add_variable(var(u, v)) # Defined on both side for u in U: F.add_clause([(True, var(u, v)) for v in V], strict=True) for v in V: F.add_clause([(True, var(u, v)) for u in U], strict=True) # Injective on both sides for u in U: for v1, v2 in combinations(V, 2): F.add_clause([(False, var(u, v1)), (False, var(u, v2))], strict=True) for v in V: for u1, u2 in combinations(U, 2): F.add_clause([(False, var(u1, v)), (False, var(u2, v))], strict=True) # Edge consistency for u1, u2 in combinations(U, 2): for v1, v2 in combinations(V, 2): if G1.has_edge(u1, u2) != G2.has_edge(v1, v2): F.add_clause([(False, var(u1, v1)), (False, var(u2, v2))], strict=True) F.add_clause([(False, var(u1, v2)), (False, var(u2, v1))], strict=True) return F
def GraphAutomorphism(G): """Graph Automorphism formula The formula is the CNF encoding of the statement that a graph G has a nontrivial automorphism, i.e. an automorphism different from the idential one. Parameter --------- G : a simple graph Returns ------- A CNF formula which is satiafiable if and only if graph G has a nontrivial automorphism. """ tmp = CNF() header = "Graph automorphism formula for graph " + G.name + "\n" + tmp.header F = GraphIsomorphism(G, G) F.header = header var = _graph_isomorphism_var F.add_clause([(False, var(u, u)) for u in enumerate_vertices(G)], strict=True) return F
def GraphAutomorphism(G): """Graph Automorphism formula The formula is the CNF encoding of the statement that a graph G has a nontrivial automorphism, i.e. an automorphism different from the idential one. Parameter --------- G : a simple graph Returns ------- A CNF formula which is satiafiable if and only if graph G has a nontrivial automorphism. """ tmp = CNF() header = "Graph automorphism formula for graph "+ G.name +"\n"+ tmp.header F = GraphIsomorphism(G, G) F.header = header var = _graph_isomorphism_var F.add_clause([(False, var(u, u)) for u in enumerate_vertices(G)], strict=True) return F
def PerfectMatchingPrinciple(G): """Generates the clauses for the graph perfect matching principle. The principle claims that there is a way to select edges to such that all vertices have exactly one incident edge set to 1. Parameters ---------- G : undirected graph """ cnf = CNF() # Describe the formula name = "Perfect Matching Principle" if hasattr(G, 'name'): cnf.header = name + " of graph:\n" + G.name + "\n" + cnf.header else: cnf.header = name + ".\n" + cnf.header def var_name(u, v): if u <= v: return 'x_{{{0},{1}}}'.format(u, v) else: return 'x_{{{0},{1}}}'.format(v, u) # Each vertex has exactly one edge set to one. for v in enumerate_vertices(G): edge_vars = [var_name(u, v) for u in neighbors(G, v)] cnf.add_equal_to(edge_vars, 1) return cnf
def PerfectMatchingPrinciple(G): """Generates the clauses for the graph perfect matching principle. The principle claims that there is a way to select edges to such that all vertices have exactly one incident edge set to 1. Parameters ---------- G : undirected graph """ cnf=CNF() # Describe the formula name="Perfect Matching Principle" if hasattr(G,'name'): cnf.header=name+" of graph:\n"+G.name+"\n"+cnf.header else: cnf.header=name+".\n"+cnf.header def var_name(u,v): if u<=v: return 'x_{{{0},{1}}}'.format(u,v) else: return 'x_{{{0},{1}}}'.format(v,u) # Each vertex has exactly one edge set to one. for v in enumerate_vertices(G): edge_vars = [var_name(u,v) for u in neighbors(G,v)] cnf.add_equal_to(edge_vars,1) return cnf
def TseitinFormula(graph,charges=None): """Build a Tseitin formula based on the input graph. Odd charge is put on the first vertex by default, unless other vertices are is specified in input. Arguments: - `graph`: input graph - `charges': odd or even charge for each vertex """ V=enumerate_vertices(graph) if charges==None: charges=[1]+[0]*(len(V)-1) # odd charge on first vertex else: charges = [bool(c) for c in charges] # map to boolean if len(charges)<len(V): charges=charges+[0]*(len(V)-len(charges)) # pad with even charges # init formula tse=CNF() for e in sorted(graph.edges(),key=sorted): tse.add_variable(edgename(e)) # add constraints for v,c in zip(V,charges): # produce all clauses and save half of them names = [ edgename((u,v)) for u in neighbors(graph,v) ] for cls in parity_constraint(names,c): tse.add_clause(list(cls),strict=True) return tse
def EvenColoringFormula(G): """Even coloring formula The formula is defined on a graph :math:`G` and claims that it is possible to split the edges of the graph in two parts, so that each vertex has an equal number of incident edges in each part. The formula is defined on graphs where all vertices have even degree. The formula is satisfiable only on those graphs with an even number of vertices in each connected component [1]_. Arguments --------- G : networkx.Graph a simple undirected graph where all vertices have even degree Raises ------ ValueError if the graph in input has a vertex with odd degree Returns ------- CNF object References ---------- .. [1] Locality and Hard SAT-instances, Klas Markstrom Journal on Satisfiability, Boolean Modeling and Computation 2 (2006) 221-228 """ F = CNF() F.mode_strict() F.header = "Even coloring formula on graph " + G.name + "\n" + F.header def var_name(u, v): if u <= v: return 'x_{{{0},{1}}}'.format(u, v) else: return 'x_{{{0},{1}}}'.format(v, u) for (u, v) in enumerate_edges(G): F.add_variable(var_name(u, v)) # Defined on both side for v in enumerate_vertices(G): if G.degree(v) % 2 == 1: raise ValueError( "Markstrom formulas requires all vertices to have even degree." ) edge_vars = [var_name(u, v) for u in neighbors(G, v)] # F.add_exactly_half_floor would work the same F.add_exactly_half_ceil(edge_vars) return F
def EvenColoringFormula(G): """Even coloring formula The formula is defined on a graph :math:`G` and claims that it is possible to split the edges of the graph in two parts, so that each vertex has an equal number of incident edges in each part. The formula is defined on graphs where all vertices have even degree. The formula is satisfiable only on those graphs with an even number of vertices in each connected component [1]_. Arguments --------- G : networkx.Graph a simple undirected graph where all vertices have even degree Raises ------ ValueError if the graph in input has a vertex with odd degree Returns ------- CNF object References ---------- .. [1] Locality and Hard SAT-instances, Klas Markstrom Journal on Satisfiability, Boolean Modeling and Computation 2 (2006) 221-228 """ F = CNF() F.header = "Even coloring formula on graph " + G.name + "\n" + F.header def var_name(u,v): if u<=v: return 'x_{{{0},{1}}}'.format(u,v) else: return 'x_{{{0},{1}}}'.format(v,u) for (u, v) in enumerate_edges(G): F.add_variable(var_name(u, v)) # Defined on both side for v in enumerate_vertices(G): if G.degree(v) % 2 == 1: raise ValueError("Markstrom formulas requires all vertices to have even degree.") edge_vars = [ var_name(u,v) for u in neighbors(G,v) ] for cls in CNF.equal_to_constraint(edge_vars, len(edge_vars)/2): F.add_clause(cls,strict=True) return F
def PebblingFormula(digraph): """Pebbling formula Build a pebbling formula from the directed graph. If the graph has an `ordered_vertices` attribute, then it is used to enumerate the vertices (and the corresponding variables). Arguments: - `digraph`: directed acyclic graph. """ if not is_dag(digraph): raise ValueError( "Pebbling formula is defined only for directed acyclic graphs") peb = CNF() peb.mode_unchecked() if hasattr(digraph, 'name'): peb.header = "Pebbling formula of: "+digraph.name+"\n\n"+peb.header else: peb.header = "Pebbling formula\n\n"+peb.header # add variables in the appropriate order vertices = enumerate_vertices(digraph) position = dict((v, i) for (i, v) in enumerate(vertices)) for v in vertices: peb.add_variable( v, description="There is a pebble on vertex ${}$".format(v)) # add the clauses for v in vertices: # If predecessors are pebbled the vertex must be pebbled pred = sorted(digraph.predecessors(v), key=lambda x: position[x]) peb.add_clause([(False, p) for p in pred]+[(True, v)]) if digraph.out_degree(v) == 0: # the sink peb.add_clause([(False, v)]) return peb
def PebblingFormula(digraph): """Pebbling formula Build a pebbling formula from the directed graph. If the graph has an `ordered_vertices` attribute, then it is used to enumerate the vertices (and the corresponding variables). Arguments: - `digraph`: directed acyclic graph. """ if not is_dag(digraph): raise ValueError("Pebbling formula is defined only for directed acyclic graphs") peb=CNF() peb.mode_unchecked() if hasattr(digraph,'name'): peb.header="Pebbling formula of: "+digraph.name+"\n\n"+peb.header else: peb.header="Pebbling formula\n\n"+peb.header # add variables in the appropriate order vertices=enumerate_vertices(digraph) position=dict((v,i) for (i,v) in enumerate(vertices)) for v in vertices: peb.add_variable(v,description="There is a pebble on vertex ${}$".format(v)) # add the clauses for v in vertices: # If predecessors are pebbled the vertex must be pebbled pred=sorted(digraph.predecessors(v),key=lambda x:position[x]) peb.add_clause([(False,p) for p in pred]+[(True,v)]) if digraph.out_degree(v)==0: #the sink peb.add_clause([(False,v)]) return peb
def TseitinFormula(graph,charges=None): """Build a Tseitin formula based on the input graph. Odd charge is put on the first vertex by default, unless other vertices are is specified in input. Arguments: - `graph`: input graph - `charges': odd or even charge for each vertex """ V=enumerate_vertices(graph) if charges==None: charges=[1]+[0]*(len(V)-1) # odd charge on first vertex else: charges = [bool(c) for c in charges] # map to boolean if len(charges)<len(V): charges=charges+[0]*(len(V)-len(charges)) # pad with even charges # init formula tse=CNF() edgename = { } for (u,v) in sorted(graph.edges(),key=sorted): edgename[(u,v)] = "E_{{{0},{1}}}".format(u,v) edgename[(v,u)] = "E_{{{0},{1}}}".format(u,v) tse.add_variable(edgename[(u,v)]) # add constraints for v,c in zip(V,charges): # produce all clauses and save half of them names = [ edgename[(u,v)] for u in neighbors(graph,v) ] for cls in CNF.parity_constraint(names,c): tse.add_clause(list(cls),strict=True) return tse
def VertexCover(G, d): F = CNF() def D(v): return "x_{{{0}}}".format(v) def N(v): return tuple(sorted([e for e in G.edges(v)])) # Fix the vertex order V = enumerate_vertices(G) # Create variables for v in V: F.add_variable(D(v)) # Not too many true variables F.add_less_or_equal([D(v) for v in V], d) # Every edge must have a true D variable for e in G.edges(): F.add_clause([(True, D(v)) for v in e]) return F
def DominatingSetOPB(G,d,tiling,seed): F=CNF() def D(v): return "x_{{{0}}}".format(v) def N(v): return tuple(sorted([ v ] + [ u for u in G.neighbors(v) ])) # Fix the vertex order V=enumerate_vertices(G) #avgdegree=sum(len(set(N(v))) for v in V)/len(V) #d=len(V)/(avgdegree+1) # Create variables for v in V: F.add_variable(D(v)) # Not too many true variables if not tiling: F.add_less_or_equal([D(v) for v in V],d) # Every neighborhood must have a true D variable neighborhoods = sorted( set(N(v) for v in V) ) for N in neighborhoods: if tiling: F.add_equal_to([D(v) for v in N], 1) else: F.add_clause([(True,D(v)) for v in N]) # Set some vertex to true if seed: F.add_clause([(True,D(V[0]))]) return F
def VertexCover(G,d): F=CNF() def D(v): return "x_{{{0}}}".format(v) def N(v): return tuple(sorted([ e for e in G.edges(v) ])) # Fix the vertex order V=enumerate_vertices(G) # Create variables for v in V: F.add_variable(D(v)) # Not too many true variables F.add_less_or_equal([D(v) for v in V],d) # Every edge must have a true D variable for e in G.edges(): F.add_clause([ (True,D(v)) for v in e]) return F
def GraphOrderingPrinciple(graph, total=False, smart=False, plant=False, knuth=0): """Generates the clauses for graph ordering principle Arguments: - `graph` : undirected graph - `total` : add totality axioms (i.e. "x < y" or "x > y") - `smart` : "x < y" and "x > y" are represented by a single variable (implies `total`) - `plant` : allow last element to be minimum (and could make the formula SAT) - `knuth` : Don Knuth variants 2 or 3 of the formula (anything else suppress it) """ gop = CNF() # Describe the formula if total or smart: name = "Total graph ordering principle" else: name = "Ordering principle" if smart: name = name + "(compact representation)" if hasattr(graph, 'name'): gop.header = name + "\n on graph " + graph.name + ".\n\n" + gop.header else: gop.header = name + ".\n\n" + gop.header # Fix the vertex order V = enumerate_vertices(graph) # Add variables iterator = combinations if smart else permutations for v1, v2 in iterator(V, 2): gop.add_variable(varname(v1, v2)) # # Non minimality axioms # # Clause is generated in such a way that if totality is enforces, # every pair occurs with a specific orientation. # Allow minimum on last vertex if 'plant' options. for med in xrange(len(V) - (plant and 1)): clause = [] for lo in xrange(med): if graph.has_edge(V[med], V[lo]): clause += [(True, varname(V[lo], V[med]))] for hi in xrange(med + 1, len(V)): if not graph.has_edge(V[med], V[hi]): continue elif smart: clause += [(False, varname(V[med], V[hi]))] else: clause += [(True, varname(V[hi], V[med]))] gop.add_clause(clause, strict=True) # # Transitivity axiom # if len(V) >= 3: if smart: # Optimized version if smart representation of totality is used for (v1, v2, v3) in combinations(V, 3): gop.add_clause([(True, varname(v1, v2)), (True, varname(v2, v3)), (False, varname(v1, v3))], strict=True) gop.add_clause([(False, varname(v1, v2)), (False, varname(v2, v3)), (True, varname(v1, v3))], strict=True) elif total: # With totality we still need just two axiom per triangle for (v1, v2, v3) in combinations(V, 3): gop.add_clause([(False, varname(v1, v2)), (False, varname(v2, v3)), (False, varname(v3, v1))], strict=True) gop.add_clause([(False, varname(v1, v3)), (False, varname(v3, v2)), (False, varname(v2, v1))], strict=True) else: for (v1, v2, v3) in permutations(V, 3): # knuth variants will reduce the number of # transitivity axioms if knuth == 2 and ((v2 < v1) or (v2 < v3)): continue if knuth == 3 and ((v3 < v1) or (v3 < v2)): continue gop.add_clause([(False, varname(v1, v2)), (False, varname(v2, v3)), (True, varname(v1, v3))], strict=True) if not smart: # Antisymmetry axioms (useless for 'smart' representation) for (v1, v2) in combinations(V, 2): gop.add_clause([(False, varname(v1, v2)), (False, varname(v2, v1))], strict=True) # Totality axioms (useless for 'smart' representation) if total: for (v1, v2) in combinations(V, 2): gop.add_clause([(True, varname(v1, v2)), (True, varname(v2, v1))], strict=True) return gop
def StoneFormula(D, nstones): """Stone formulas The stone formulas have been introduced in [2]_ and generalized in [1]_. They are one of the classic examples that separate regular resolutions from general resolution [1]_. A \"Stones formula\" from a directed acyclic graph :math:`D` claims that each vertex of the graph is associated with one on :math:`s` stones (not necessarily in an injective way). In particular for each vertex :math:`v` in :math:`V(D)` and each stone :math:`j` we have a variable :math:`P_{v,j}` that claims that stone :math:`j` is associated to vertex :math:`v`. Each stone can be either red or blue, and not both. The propositional variable :math:`R_j` if true when the stone :math:`j` is red and false otherwise. The clauses of the formula encode the following constraints. If a stone is on a source vertex (i.e. a vertex with no incoming edges), then it must be red. If all stones on the predecessors of a vertex are red, then the stone of the vertex itself must be red. The formula furthermore enforces that the stones on the sinks (i.e. vertices with no outgoing edges) are blue. .. note:: The exact formula structure depends by the graph and on its topological order, which is determined by the ``enumerate_vertices(D)``. Parameters ---------- D : a directed acyclic graph it should be a directed acyclic graph. nstones : int the number of stones. Raises ------ ValueError if :math:`D` is not a directed acyclic graph ValueError if the number of stones is negative References ---------- .. [1] M. Alekhnovich, J. Johannsen, T. Pitassi and A. Urquhart An Exponential Separation between Regular and General Resolution. Theory of Computing (2007) .. [2] R. Raz and P. McKenzie Separation of the monotone NC hierarchy. Combinatorica (1999) """ if not is_dag(D): raise ValueError( "Stone formulas are defined only for directed acyclic graphs.") if nstones < 0: raise ValueError("There must be at least one stone.") cnf = CNF() if hasattr(D, 'name'): cnf.header = "Stone formula of: " + D.name + "\nwith " + str( nstones) + " stones\n" + cnf.header else: cnf.header = "Stone formula with " + str( nstones) + " stones\n" + cnf.header # Add variables in the appropriate order vertices = enumerate_vertices(D) position = dict((v, i) for (i, v) in enumerate(vertices)) stones = list(range(1, nstones + 1)) # Caching variable names color_vn = {} stone_vn = {} # Stones->Vertices variables for v in vertices: for j in stones: stone_vn[(v, j)] = "P_{{{0},{1}}}".format(v, j) cnf.add_variable(stone_vn[(v, j)], description="Stone ${1}$ on vertex ${0}$".format( v, j)) # Color variables for j in stones: color_vn[j] = "R_{{{0}}}".format(j) cnf.add_variable(color_vn[j], description="Stone ${}$ is red".format(j)) # Each vertex has some stone for v in vertices: cnf.add_clause_unsafe([(True, stone_vn[(v, j)]) for j in stones]) # If predecessors have red stones, the sink must have a red stone for v in vertices: for j in stones: pred = sorted(D.predecessors(v), key=lambda x: position[x]) for stones_tuple in product([s for s in stones if s != j], repeat=len(pred)): cnf.add_clause_unsafe([(False, stone_vn[(p, s)]) for (p, s) in zip(pred, stones_tuple)] + [(False, stone_vn[(v, j)])] + [(False, color_vn[s]) for s in _uniqify_list(stones_tuple)] + [(True, color_vn[j])]) if D.out_degree(v) == 0: #the sink for j in stones: cnf.add_clause_unsafe([(False, stone_vn[(v, j)]), (False, color_vn[j])]) return cnf
def DominatingSet(G,d, alternative = False): r"""Generates the clauses for a dominating set for G of size <= d The formula encodes the fact that the graph :math:`G` has a dominating set of size :math:`d`. This means that it is possible to pick at most :math:`d` vertices in :math:`V(G)` so that all remaining vertices have distance at most one from the selected ones. Parameters ---------- G : networkx.Graph a simple undirected graph d : a positive int the size limit for the dominating set alternative : bool use an alternative construction that is provably hard from resolution proofs. Returns ------- CNF the CNF encoding for dominating of size :math:`\leq d` for graph :math:`G` """ F=CNF() if not isinstance(d,int) or d<1: ValueError("Parameter \"d\" is expected to be a positive integer") # Describe the formula name="{}-dominating set".format(d) if hasattr(G,'name'): F.header=name+" of graph:\n"+G.name+".\n\n"+F.header else: F.header=name+".\n\n"+F.header # Fix the vertex order V=enumerate_vertices(G) def D(v): return "x_{{{0}}}".format(v) def M(v,i): return "g_{{{0},{1}}}".format(v,i) def N(v): return tuple(sorted([ v ] + [ u for u in G.neighbors(v) ])) # Create variables for v in V: F.add_variable(D(v)) for i,v in product(range(1,d+1),V): F.add_variable(M(v,i)) # No two (active) vertices map to the same index if alternative: for u,v in combinations(V,2): for i in range(1,d+1): F.add_clause( [ (False,D(u)),(False,D(v)), (False,M(u,i)), (False,M(v,i)) ]) else: for i in range(1,d+1): for c in CNF.less_or_equal_constraint([M(v,i) for v in V],1): F.add_clause(c) # (Active) Vertices in the sequence are not repeated if alternative: for v in V: for i,j in combinations(range(1,d+1),2): F.add_clause([(False,D(v)),(False,M(v,i)),(False,M(v,j))]) else: for i,j in combinations_with_replacement(range(1,d+1),2): i,j = min(i,j),max(i,j) for u,v in combinations(V,2): u,v = max(u,v),min(u,v) F.add_clause([(False,M(u,i)),(False,M(v,j))]) # D(v) = M(v,1) or M(v,2) or ... or M(v,d) if not alternative: for i,v in product(range(1,d+1),V): F.add_clause([(False,M(v,i)),(True,D(v))]) for v in V: F.add_clause([(False,D(v))] + [(True,M(v,i)) for i in range(1,d+1)]) # Every neighborhood must have a true D variable neighborhoods = sorted( set(N(v) for v in V) ) for N in neighborhoods: F.add_clause([ (True,D(v)) for v in N]) return F
def SubgraphFormula(graph,templates, symmetric=False): """Test whether a graph contains one of the templates. Given a graph :math:`G` and a sequence of template graphs :math:`H_1`, :math:`H_2`, ..., :math:`H_t`, the CNF formula claims that :math:`G` contains an isomorphic copy of at least one of the template graphs. E.g. when :math:`H_1` is the complete graph of :math:`k` vertices and it is the only template, the formula claims that :math:`G` contains a :math:`k`-clique. Parameters ---------- graph : networkx.Graph a simple graph templates : list-like object a sequence of graphs. symmetric: all template graphs are symmetric wrt permutations of vertices. This allows some optimization in the search space of the assignments. induce: force the subgraph to be induced (i.e. no additional edges are allowed) Returns ------- a CNF object """ F=CNF() # One of the templates is chosen to be the subgraph if len(templates)==0: return F elif len(templates)==1: selectors=[] elif len(templates)==2: selectors=['c'] else: selectors=['c_{{{}}}'.format(i) for i in range(len(templates))] for s in selectors: F.add_variable(s) if len(selectors)>1: for cls in F.equal_to_constraint(selectors,1): F.add_clause( cls , strict=True ) # comment the formula accordingly if len(selectors)>1: F.header=dedent("""\ CNF encoding of the claim that a graph contains one among a family of {0} possible subgraphs. """.format(len(templates))) + F.header else: F.header=dedent("""\ CNF encoding of the claim that a graph contains an induced copy of a subgraph. """.format(len(templates))) + F.header # A subgraph is chosen N=graph.order() k=max([s.order() for s in templates]) var_name = lambda i,j: "S_{{{0},{1}}}".format(i,j) for i,j in product(range(k),range(N)): F.add_variable( var_name(i,j) ) if symmetric: gencls = F.unary_mapping(range(k),range(N),var_name=var_name, functional=True,injective=True, nondecreasing=True) else: gencls = F.unary_mapping(range(k),range(N),var_name=var_name, functional=True,injective=True) for cls in gencls: F.add_clause( cls, strict=True ) # The selectors choose a template subgraph. A mapping must map # edges to edges and non-edges to non-edges for the active # template. if len(templates)==1: activation_prefixes = [[]] elif len(templates)==2: activation_prefixes = [[(True,selectors[0])],[(False,selectors[0])]] else: activation_prefixes = [[(True,v)] for v in selectors] # maps must preserve the structure of the template graph gV = enumerate_vertices(graph) for i in range(len(templates)): k = templates[i].order() tV = enumerate_vertices(templates[i]) if symmetric: # Using non-decreasing map to represent a subset localmaps = product(combinations(range(k),2), combinations(range(N),2)) else: localmaps = product(combinations(range(k),2), permutations(range(N),2)) for (i1,i2),(j1,j2) in localmaps: # check if this mapping is compatible tedge=templates[i].has_edge(tV[i1],tV[i2]) gedge=graph.has_edge(gV[j1],gV[j2]) if tedge == gedge: continue # if it is not, add the corresponding F.add_clause(activation_prefixes[i] + \ [(False,var_name(i1,j1)), (False,var_name(i2,j2)) ],strict=True) return F
def SparseStoneFormula(D,B): """Sparse Stone formulas This is a variant of the :py:func:`StoneFormula`. See that for a description of the formula. This variant is such that each vertex has only a small selection of which stone can go to that vertex. In particular which stones are allowed on each vertex is specified by a bipartite graph :math:`B` on which the left vertices represent the vertices of DAG :math:`D` and the right vertices are the stones. If a vertex of :math:`D` correspond to the left vertex :math:`v` in :math:`B`, then its neighbors describe which stones are allowed for it. The vertices in :math:`D` do not need to have the same name as the one on the left side of :math:`B`. It is only important that the number of vertices in :math:`D` is the same as the vertices in the left side of :math:`B`. In that case the element at position :math:`i` in the ordered sequence ``enumerate_vertices(D)`` corresponds to the element of rank :math:`i` in the sequence of left side vertices of :math:`B` according to the output of ``Left, Right = bipartite_sets(B)``. Standard :py:func:`StoneFormula` is essentially equivalent to a sparse stone formula where :math:`B` is the complete graph. Parameters ---------- D : a directed acyclic graph it should be a directed acyclic graph. B : bipartite graph Raises ------ ValueError if :math:`D` is not a directed acyclic graph ValueError if :math:`B` is not a bipartite graph ValueError when size differs between :math:`D` and the left side of :math:`B` See Also -------- StoneFormula """ if not is_dag(D): raise ValueError("Stone formulas are defined only for directed acyclic graphs.") if not has_bipartition(B): raise ValueError("Vertices to stones mapping must be specified with a bipartite graph") Left, Right = bipartite_sets(B) nstones = len(Right) if len(Left) != D.order(): raise ValueError("Formula requires the bipartite left side to match #vertices of the DAG.") cnf = CNF() if hasattr(D, 'name'): cnf.header = "Sparse Stone formula of: " + D.name + "\nwith " + str(nstones) + " stones\n" + cnf.header else: cnf.header = "Sparse Stone formula with " + str(nstones) + " stones\n" + cnf.header # add variables in the appropriate order vertices=enumerate_vertices(D) stones=range(1,nstones+1) mapping = cnf.unary_mapping(vertices,stones,sparsity_pattern=B) stone_formula_helper(cnf,D,mapping) return cnf
def stone_formula_helper(F,D,mapping): """Stones formulas helper Builds the clauses of a stone formula given the mapping object between stones and vertices of the DAG. THis is not supposed to be called by user code, and indeed this function assumes the following facts. - :math:`D` is equal to `mapping.domain()` - :math:`D` is a DAG Parameters ---------- F : CNF the CNF which will contain the clauses of the stone formula D : a directed acyclic graph it should be a directed acyclic graph. mapping : unary_mapping object mapping between stones and graph vertices See Also -------- StoneFormula : the classic stone formula SparseStoneFormula : stone formula with sparse mapping """ # add variables in the appropriate order vertices=enumerate_vertices(D) # Stones->Vertices variables for v in mapping.variables(): F.add_variable(v) # Color variables for stone in mapping.range(): F.add_variable("R_{{{0}}}".format(stone), description="Stone ${}$ is red".format(stone)) # Each vertex has some stone for cls in mapping.clauses(): F.add_clause_unsafe(cls) # If predecessors have red stones, the sink must have a red stone for v in vertices: for j in mapping.images(v): pred=sorted(D.predecessors(v),key=lambda x:mapping.RankDomain[x]) for stones_tuple in product(*tuple( [s for s in mapping.images(v) if s!=j ] for v in pred)): F.add_clause([(False, mapping.var_name(p,s)) for (p,s) in zip(pred,stones_tuple)] + [(False, mapping.var_name(v,j))] + [(False, "R_{{{0}}}".format(s)) for s in _uniqify_list(stones_tuple)] + [(True, "R_{{{0}}}".format(j))], strict=True) if D.out_degree(v)==0: #the sink for j in mapping.images(v): F.add_clause([ (False, mapping.var_name(v,j)), (False,"R_{{{0}}}".format(j))], strict = True) return F
def GraphOrderingPrinciple(graph,total=False,smart=False,plant=False,knuth=0): """Generates the clauses for graph ordering principle Arguments: - `graph` : undirected graph - `total` : add totality axioms (i.e. "x < y" or "x > y") - `smart` : "x < y" and "x > y" are represented by a single variable (implies `total`) - `plant` : allow last element to be minimum (and could make the formula SAT) - `knuth` : Don Knuth variants 2 or 3 of the formula (anything else suppress it) """ gop = CNF() # Describe the formula if total or smart: name = "Total graph ordering principle" else: name = "Ordering principle" if smart: name = name + "(compact representation)" if hasattr(graph, 'name'): gop.header = name+"\n on graph "+graph.name+".\n\n"+gop.header else: gop.header = name+".\n\n"+gop.header # Fix the vertex order V = enumerate_vertices(graph) # Add variables iterator = combinations if smart else permutations for v1,v2 in iterator(V,2): gop.add_variable(varname(v1,v2)) # # Non minimality axioms # # Clause is generated in such a way that if totality is enforces, # every pair occurs with a specific orientation. # Allow minimum on last vertex if 'plant' options. for med in xrange(len(V) - (plant and 1)): clause = [] for lo in xrange(med): if graph.has_edge(V[med], V[lo]): clause += [(True, varname(V[lo], V[med]))] for hi in xrange(med+1, len(V)): if not graph.has_edge(V[med], V[hi]): continue elif smart: clause += [(False, varname(V[med], V[hi]))] else: clause += [(True, varname(V[hi], V[med]))] gop.add_clause(clause, strict=True) # # Transitivity axiom # if len(V) >= 3: if smart: # Optimized version if smart representation of totality is used for (v1, v2, v3) in combinations(V, 3): gop.add_clause([(True, varname(v1, v2)), (True, varname(v2, v3)), (False, varname(v1, v3))], strict=True) gop.add_clause([(False, varname(v1, v2)), (False, varname(v2, v3)), (True, varname(v1, v3))], strict=True) elif total: # With totality we still need just two axiom per triangle for (v1, v2, v3) in combinations(V, 3): gop.add_clause([(False, varname(v1, v2)), (False, varname(v2, v3)), (False, varname(v3, v1))], strict=True) gop.add_clause([(False, varname(v1, v3)), (False, varname(v3, v2)), (False, varname(v2, v1))], strict=True) else: for (v1, v2, v3) in permutations(V, 3): # knuth variants will reduce the number of # transitivity axioms if knuth == 2 and ((v2 < v1) or (v2 < v3)): continue if knuth == 3 and ((v3 < v1) or (v3 < v2)): continue gop.add_clause([(False, varname(v1, v2)), (False, varname(v2, v3)), (True, varname(v1, v3))], strict=True) if not smart: # Antisymmetry axioms (useless for 'smart' representation) for (v1, v2) in combinations(V, 2): gop.add_clause([(False, varname(v1, v2)), (False, varname(v2, v1))], strict=True) # Totality axioms (useless for 'smart' representation) if total: for (v1, v2) in combinations(V, 2): gop.add_clause([(True, varname(v1, v2)), (True, varname(v2, v1))], strict=True) return gop
def StoneFormula(D,nstones): """Stone formulas The stone formulas have been introduced in [2]_ and generalized in [1]_. They are one of the classic examples that separate regular resolutions from general resolution [1]_. A \"Stones formula\" from a directed acyclic graph :math:`D` claims that each vertex of the graph is associated with one on :math:`s` stones (not necessarily in an injective way). In particular for each vertex :math:`v` in :math:`V(D)` and each stone :math:`j` we have a variable :math:`P_{v,j}` that claims that stone :math:`j` is associated to vertex :math:`v`. Each stone can be either red or blue, and not both. The propositional variable :math:`R_j` if true when the stone :math:`j` is red and false otherwise. The clauses of the formula encode the following constraints. If a stone is on a source vertex (i.e. a vertex with no incoming edges), then it must be red. If all stones on the predecessors of a vertex are red, then the stone of the vertex itself must be red. The formula furthermore enforces that the stones on the sinks (i.e. vertices with no outgoing edges) are blue. .. note:: The exact formula structure depends by the graph and on its topological order, which is determined by the ``enumerate_vertices(D)``. Parameters ---------- D : a directed acyclic graph it should be a directed acyclic graph. nstones : int the number of stones. Raises ------ ValueError if :math:`D` is not a directed acyclic graph ValueError if the number of stones is negative References ---------- .. [1] M. Alekhnovich, J. Johannsen, T. Pitassi and A. Urquhart An Exponential Separation between Regular and General Resolution. Theory of Computing (2007) .. [2] R. Raz and P. McKenzie Separation of the monotone NC hierarchy. Combinatorica (1999) """ if not is_dag(D): raise ValueError("Stone formulas are defined only for directed acyclic graphs.") if nstones<0: raise ValueError("There must be at least one stone.") cnf = CNF() if hasattr(D, 'name'): cnf.header = "Stone formula of: " + D.name + "\nwith " + str(nstones) + " stones\n" + cnf.header else: cnf.header = "Stone formula with " + str(nstones) + " stones\n" + cnf.header # Add variables in the appropriate order vertices=enumerate_vertices(D) position=dict((v,i) for (i,v) in enumerate(vertices)) stones=range(1,nstones+1) # Caching variable names color_vn = {} stone_vn = {} # Stones->Vertices variables for v in vertices: for j in stones: stone_vn[(v,j)] = "P_{{{0},{1}}}".format(v,j) cnf.add_variable(stone_vn[(v,j)], description="Stone ${1}$ on vertex ${0}$".format(v,j)) # Color variables for j in stones: color_vn[j] = "R_{{{0}}}".format(j) cnf.add_variable(color_vn[j], description="Stone ${}$ is red".format(j)) # Each vertex has some stone for v in vertices: cnf.add_clause_unsafe([(True,stone_vn[(v,j)]) for j in stones]) # If predecessors have red stones, the sink must have a red stone for v in vertices: for j in stones: pred=sorted(D.predecessors(v),key=lambda x:position[x]) for stones_tuple in product([s for s in stones if s!=j],repeat=len(pred)): cnf.add_clause_unsafe([(False, stone_vn[(p,s)]) for (p,s) in zip(pred,stones_tuple)] + [(False, stone_vn[(v,j)])] + [(False, color_vn[s]) for s in _uniqify_list(stones_tuple)] + [(True, color_vn[j])]) if D.out_degree(v)==0: #the sink for j in stones: cnf.add_clause_unsafe([ (False,stone_vn[(v,j)]), (False,color_vn[j])]) return cnf
def DominatingSet(G, d, alternative=False): r"""Generates the clauses for a dominating set for G of size <= d The formula encodes the fact that the graph :math:`G` has a dominating set of size :math:`d`. This means that it is possible to pick at most :math:`d` vertices in :math:`V(G)` so that all remaining vertices have distance at most one from the selected ones. Parameters ---------- G : networkx.Graph a simple undirected graph d : a positive int the size limit for the dominating set alternative : bool use an alternative construction that is provably hard from resolution proofs. Returns ------- CNF the CNF encoding for dominating of size :math:`\leq d` for graph :math:`G` """ F = CNF() if not isinstance(d, int) or d < 1: ValueError("Parameter \"d\" is expected to be a positive integer") # Describe the formula name = "{}-dominating set".format(d) if hasattr(G, 'name'): F.header = name + " of graph:\n" + G.name + ".\n\n" + F.header else: F.header = name + ".\n\n" + F.header # Fix the vertex order V = enumerate_vertices(G) def D(v): return "x_{{{0}}}".format(v) def M(v, i): return "g_{{{0},{1}}}".format(v, i) def N(v): return tuple(sorted([v] + [u for u in G.neighbors(v)])) # Create variables for v in V: F.add_variable(D(v)) for i, v in product(list(range(1, d + 1)), V): F.add_variable(M(v, i)) # No two (active) vertices map to the same index if alternative: for u, v in combinations(V, 2): for i in range(1, d + 1): F.add_clause([(False, D(u)), (False, D(v)), (False, M(u, i)), (False, M(v, i))]) else: for i in range(1, d + 1): for c in CNF.less_or_equal_constraint([M(v, i) for v in V], 1): F.add_clause(c) # (Active) Vertices in the sequence are not repeated if alternative: for v in V: for i, j in combinations(list(range(1, d + 1)), 2): F.add_clause([(False, D(v)), (False, M(v, i)), (False, M(v, j))]) else: for i, j in combinations_with_replacement(list(range(1, d + 1)), 2): i, j = min(i, j), max(i, j) for u, v in combinations(V, 2): u, v = max(u, v), min(u, v) F.add_clause([(False, M(u, i)), (False, M(v, j))]) # D(v) = M(v,1) or M(v,2) or ... or M(v,d) if not alternative: for i, v in product(list(range(1, d + 1)), V): F.add_clause([(False, M(v, i)), (True, D(v))]) for v in V: F.add_clause([(False, D(v))] + [(True, M(v, i)) for i in range(1, d + 1)]) # Every neighborhood must have a true D variable neighborhoods = sorted(set(N(v) for v in V)) for N in neighborhoods: F.add_clause([(True, D(v)) for v in N]) return F
def VertexCover(G,k, alternative = False): r"""Generates the clauses for a vertex cover for G of size <= k Parameters ---------- G : networkx.Graph a simple undirected graph k : a positive int the size limit for the vertex cover Returns ------- CNF the CNF encoding for vertex cover of size :math:`\leq k` for graph :math:`G` """ F=CNF() if not isinstance(k,int) or k<1: ValueError("Parameter \"k\" is expected to be a positive integer") # Describe the formula name="{}-vertex cover".format(k) if hasattr(G,'name'): F.header=name+" of graph:\n"+G.name+".\n\n"+F.header else: F.header=name+".\n\n"+F.header # Fix the vertex order V=enumerate_vertices(G) E=enumerate_edges(G) def D(v): return "x_{{{0}}}".format(v) def M(v,i): return "g_{{{0},{1}}}".format(v,i) def N(v): return tuple(sorted([ v ] + [ u for u in G.neighbors(v) ])) # Create variables for v in V: F.add_variable(D(v)) for i,v in product(range(1,k+1),V): F.add_variable(M(v,i)) # No two (active) vertices map to the same index for i in range(1,k+1): for c in CNF.less_or_equal_constraint([M(v,i) for v in V],1): F.add_clause(c) # (Active) Vertices in the sequence are not repeated for i,j in combinations_with_replacement(range(1,k+1),2): i,j = min(i,j),max(i,j) for u,v in combinations(V,2): u,v = max(u,v),min(u,v) F.add_clause([(False,M(u,i)),(False,M(v,j))]) # D(v) = M(v,1) or M(v,2) or ... or M(v,k) for i,v in product(range(1,k+1),V): F.add_clause([(False,M(v,i)),(True,D(v))]) for v in V: F.add_clause([(False,D(v))] + [(True,M(v,i)) for i in range(1,k+1)]) # Every neighborhood must have a true D variable for v1,v2 in E: F.add_clause([(True,D(v1)), (True,D(v2))]) return F
def SparseStoneFormula(D, B): """Sparse Stone formulas This is a variant of the :py:func:`StoneFormula`. See that for a description of the formula. This variant is such that each vertex has only a small selection of which stone can go to that vertex. In particular which stones are allowed on each vertex is specified by a bipartite graph :math:`B` on which the left vertices represent the vertices of DAG :math:`D` and the right vertices are the stones. If a vertex of :math:`D` correspond to the left vertex :math:`v` in :math:`B`, then its neighbors describe which stones are allowed for it. The vertices in :math:`D` do not need to have the same name as the one on the left side of :math:`B`. It is only important that the number of vertices in :math:`D` is the same as the vertices in the left side of :math:`B`. In that case the element at position :math:`i` in the ordered sequence ``enumerate_vertices(D)`` corresponds to the element of rank :math:`i` in the sequence of left side vertices of :math:`B` according to the output of ``Left, Right = bipartite_sets(B)``. Standard :py:func:`StoneFormula` is essentially equivalent to a sparse stone formula where :math:`B` is the complete graph. Parameters ---------- D : a directed acyclic graph it should be a directed acyclic graph. B : bipartite graph Raises ------ ValueError if :math:`D` is not a directed acyclic graph ValueError if :math:`B` is not a bipartite graph ValueError when size differs between :math:`D` and the left side of :math:`B` See Also -------- StoneFormula """ if not is_dag(D): raise ValueError( "Stone formulas are defined only for directed acyclic graphs.") if not has_bipartition(B): raise ValueError( "Vertices to stones mapping must be specified with a bipartite graph" ) Left, Right = bipartite_sets(B) nstones = len(Right) if len(Left) != D.order(): raise ValueError( "Formula requires the bipartite left side to match #vertices of the DAG." ) cnf = CNF() if hasattr(D, 'name'): cnf.header = "Sparse Stone formula of: " + D.name + "\nwith " + str( nstones) + " stones\n" + cnf.header else: cnf.header = "Sparse Stone formula with " + str( nstones) + " stones\n" + cnf.header # add variables in the appropriate order vertices = enumerate_vertices(D) stones = list(range(1, nstones + 1)) mapping = cnf.unary_mapping(vertices, stones, sparsity_pattern=B) stone_formula_helper(cnf, D, mapping) return cnf
def SubgraphFormula(graph,templates): """Test whether a graph contains one of the templates. Given a graph :math:`G` and a sequence of template graphs :math:`H_1`, :math:`H_2`, ..., :math:`H_t`, the CNF formula claims that :math:`G` contains an isomorphic copy of at least one of the template graphs. E.g. when :math:`H_1` is the complete graph of :math:`k` vertices and it is the only template, the formula claims that :math:`G` contains a :math:`k`-clique. Parameters ---------- graph : networkx.Graph a simple graph templates : list-like object a sequence of graphs. Returns ------- a CNF object """ F=CNF() # One of the templates is chosen to be the subgraph if len(templates)==0: return F elif len(templates)==1: selectors=[] elif len(templates)==2: selectors=['c'] else: selectors=['c_{{{}}}'.format(i) for i in range(len(templates))] for s in selectors: F.add_variable(s) if len(selectors)>1: # Exactly one of the graphs must be selected as subgraph F.add_clause([(True,v) for v in selectors],strict=True) for (a,b) in combinations(selectors): F.add_clause( [ (False,a), (False,b) ], strict=True ) # comment the formula accordingly if len(selectors)>1: F.header=dedent("""\ CNF encoding of the claim that a graph contains one among a family of {0} possible subgraphs. """.format(len(templates))) + F.header else: F.header=dedent("""\ CNF encoding of the claim that a graph contains an induced copy of a subgraph. """.format(len(templates))) + F.header # A subgraph is chosen N=graph.order() k=max([s.order() for s in templates]) for i,j in product(range(k),range(N)): F.add_variable("S_{{{0},{1}}}".format(i,j)) # each vertex has an image... for i in range(k): F.add_clause([(True,"S_{{{0},{1}}}".format(i,j)) for j in range(N)],strict=True) # ...and exactly one for i,(a,b) in product(range(k),combinations(range(N),2)): F.add_clause([(False,"S_{{{0},{1}}}".format(i,a)), (False,"S_{{{0},{1}}}".format(i,b)) ], strict = True) # Mapping is strictly monotone increasing (so it is also injective) localmaps = product(combinations(range(k),2), combinations_with_replacement(range(N),2)) for (a,b),(i,j) in localmaps: F.add_clause([(False,"S_{{{0},{1}}}".format(min(a,b),max(i,j))), (False,"S_{{{0},{1}}}".format(max(a,b),min(i,j))) ],strict=True) # The selectors choose a template subgraph. A mapping must map # edges to edges and non-edges to non-edges for the active # template. if len(templates)==1: activation_prefixes = [[]] elif len(templates)==2: activation_prefixes = [[(True,selectors[0])],[(False,selectors[0])]] else: activation_prefixes = [[(True,v)] for v in selectors] # maps must preserve the structure of the template graph gV = enumerate_vertices(graph) for i in range(len(templates)): k = templates[i].order() tV = enumerate_vertices(templates[i]) localmaps = product(combinations(range(k),2), combinations(range(N),2)) for (i1,i2),(j1,j2) in localmaps: # check if this mapping is compatible tedge=templates[i].has_edge(tV[i1],tV[i2]) gedge=graph.has_edge(gV[j1],gV[j2]) if tedge == gedge: continue # if it is not, add the corresponding F.add_clause(activation_prefixes[i] + \ [(False,"S_{{{0},{1}}}".format(min(i1,i2),min(j1,j2))), (False,"S_{{{0},{1}}}".format(max(i1,i2),max(j1,j2))) ],strict=True) return F
def GraphColoringFormula(G,colors,functional=True): """Generates the clauses for colorability formula The formula encodes the fact that the graph :math:`G` has a coloring with color set ``colors``. This means that it is possible to assign one among the elements in ``colors``to that each vertex of the graph such that no two adjacent vertices get the same color. Parameters ---------- G : networkx.Graph a simple undirected graph colors : list or positive int a list of colors or a number of colors Returns ------- CNF the CNF encoding of the coloring problem on graph ``G`` """ col=CNF() col.mode_strict() if isinstance(colors,int) and colors>=0: colors = range(1,colors+1) if not isinstance(list, collections.Iterable): ValueError("Parameter \"colors\" is expected to be a iterable") # Describe the formula name="graph colorability" if hasattr(G,'name'): col.header=name+" of graph:\n"+G.name+".\n\n"+col.header else: col.header=name+".\n\n"+col.header # Fix the vertex order V=enumerate_vertices(G) # Create the variables for vertex in V: for color in colors: col.add_variable('x_{{{0},{1}}}'.format(vertex,color)) # Each vertex has a color for vertex in V: clause = [] for color in colors: clause += [(True,'x_{{{0},{1}}}'.format(vertex,color))] col.add_clause(clause) # unique color per vertex if functional: for (c1,c2) in combinations(colors,2): col.add_clause([ (False,'x_{{{0},{1}}}'.format(vertex,c1)), (False,'x_{{{0},{1}}}'.format(vertex,c2))]) # This is a legal coloring for (v1,v2) in enumerate_edges(G): for c in colors: col.add_clause([ (False,'x_{{{0},{1}}}'.format(v1,c)), (False,'x_{{{0},{1}}}'.format(v2,c))]) return col
def stone_formula_helper(F, D, mapping): """Stones formulas helper Builds the clauses of a stone formula given the mapping object between stones and vertices of the DAG. THis is not supposed to be called by user code, and indeed this function assumes the following facts. - :math:`D` is equal to `mapping.domain()` - :math:`D` is a DAG Parameters ---------- F : CNF the CNF which will contain the clauses of the stone formula D : a directed acyclic graph it should be a directed acyclic graph. mapping : unary_mapping object mapping between stones and graph vertices See Also -------- StoneFormula : the classic stone formula SparseStoneFormula : stone formula with sparse mapping """ # add variables in the appropriate order vertices = enumerate_vertices(D) # Stones->Vertices variables for v in mapping.variables(): F.add_variable(v) # Color variables for stone in mapping.range(): F.add_variable("R_{{{0}}}".format(stone), description="Stone ${}$ is red".format(stone)) # Each vertex has some stone for cls in mapping.clauses(): F.add_clause_unsafe(cls) # If predecessors have red stones, the sink must have a red stone for v in vertices: for j in mapping.images(v): pred = sorted(D.predecessors(v), key=lambda x: mapping.RankDomain[x]) for stones_tuple in product(*tuple( [s for s in mapping.images(v) if s != j] for v in pred)): F.add_clause([(False, mapping.var_name(p, s)) for (p, s) in zip(pred, stones_tuple)] + [(False, mapping.var_name(v, j))] + [(False, "R_{{{0}}}".format(s)) for s in _uniqify_list(stones_tuple)] + [(True, "R_{{{0}}}".format(j))], strict=True) if D.out_degree(v) == 0: #the sink for j in mapping.images(v): F.add_clause([(False, mapping.var_name(v, j)), (False, "R_{{{0}}}".format(j))], strict=True) return F
def SubgraphFormula(graph, templates, symmetric=False): """Test whether a graph contains one of the templates. Given a graph :math:`G` and a sequence of template graphs :math:`H_1`, :math:`H_2`, ..., :math:`H_t`, the CNF formula claims that :math:`G` contains an isomorphic copy of at least one of the template graphs. E.g. when :math:`H_1` is the complete graph of :math:`k` vertices and it is the only template, the formula claims that :math:`G` contains a :math:`k`-clique. Parameters ---------- graph : networkx.Graph a simple graph templates : list-like object a sequence of graphs. symmetric: all template graphs are symmetric wrt permutations of vertices. This allows some optimization in the search space of the assignments. induce: force the subgraph to be induced (i.e. no additional edges are allowed) Returns ------- a CNF object """ F = CNF() # One of the templates is chosen to be the subgraph if len(templates) == 0: return F elif len(templates) == 1: selectors = [] elif len(templates) == 2: selectors = ['c'] else: selectors = ['c_{{{}}}'.format(i) for i in range(len(templates))] for s in selectors: F.add_variable(s) if len(selectors) > 1: for cls in F.equal_to_constraint(selectors, 1): F.add_clause(cls, strict=True) # comment the formula accordingly if len(selectors) > 1: F.header = dedent("""\ CNF encoding of the claim that a graph contains one among a family of {0} possible subgraphs. """.format(len(templates))) + F.header else: F.header = dedent("""\ CNF encoding of the claim that a graph contains an induced copy of a subgraph. """.format(len(templates))) + F.header # A subgraph is chosen N = graph.order() k = max([s.order() for s in templates]) var_name = lambda i, j: "S_{{{0},{1}}}".format(i, j) if symmetric: mapping = F.unary_mapping(range(k), range(N), var_name=var_name, functional=True, injective=True, nondecreasing=True) else: mapping = F.unary_mapping(range(k), range(N), var_name=var_name, functional=True, injective=True, nondecreasing=False) for v in mapping.variables(): F.add_variable(v) for cls in mapping.clauses(): F.add_clause(cls, strict=True) # The selectors choose a template subgraph. A mapping must map # edges to edges and non-edges to non-edges for the active # template. if len(templates) == 1: activation_prefixes = [[]] elif len(templates) == 2: activation_prefixes = [[(True, selectors[0])], [(False, selectors[0])]] else: activation_prefixes = [[(True, v)] for v in selectors] # maps must preserve the structure of the template graph gV = enumerate_vertices(graph) for i in range(len(templates)): k = templates[i].order() tV = enumerate_vertices(templates[i]) if symmetric: # Using non-decreasing map to represent a subset localmaps = product(combinations(range(k), 2), combinations(range(N), 2)) else: localmaps = product(combinations(range(k), 2), permutations(range(N), 2)) for (i1, i2), (j1, j2) in localmaps: # check if this mapping is compatible tedge = templates[i].has_edge(tV[i1], tV[i2]) gedge = graph.has_edge(gV[j1], gV[j2]) if tedge == gedge: continue # if it is not, add the corresponding F.add_clause(activation_prefixes[i] + \ [(False,var_name(i1,j1)), (False,var_name(i2,j2)) ],strict=True) return F
def TseitinFormula(graph, charges=None, encoding=None): """Build a Tseitin formula based on the input graph. Odd charge is put on the first vertex by default, unless other vertices are is specified in input. Arguments: - `graph`: input graph - `charges': odd or even charge for each vertex """ V = enumerate_vertices(graph) if charges == None: charges = [1] + [0] * (len(V) - 1) # odd charge on first vertex else: charges = [bool(c) for c in charges] # map to boolean if len(charges) < len(V): charges = charges + [0] * (len(V) - len(charges) ) # pad with even charges # init formula tse = CNF() edgename = {} for (u, v) in sorted(graph.edges(), key=sorted): edgename[(u, v)] = "E_{{{0},{1}}}".format(u, v) edgename[(v, u)] = "E_{{{0},{1}}}".format(u, v) tse.add_variable(edgename[(u, v)]) tse.mode_strict() # add constraints for v, charge in zip(V, charges): # produce all clauses and save half of them names = [edgename[(u, v)] for u in neighbors(graph, v)] if encoding == None: tse.add_parity(names, charge) else: def toArgs(listOfTuples, operator, degree): return list(sum(listOfTuples, ())) + [operator, degree] terms = list(map(lambda x: (1, x), names)) w = len(terms) k = w // 2 if encoding == "extendedPBAnyHelper": for i in range(k): helper = ("xor_helper", i, v) tse.add_variable(helper) terms.append((2, helper)) elif encoding == "extendedPBOneHelper": helpers = list() for i in range(k): helper = ("xor_helper", i, v) helpers.append(helper) tse.add_variable(helper) terms.append(((i + 1) * 2, helper)) tse.add_linear(*toArgs([(1, x) for x in helpers], "<=", 1)) elif encoding == "extendedPBExpHelper": for i in range(math.ceil(math.log(k, 2)) + 1): helper = ("xor_helper", i, v) tse.add_variable(helper) terms.append((2**i * 2, helper)) else: raise ValueError("Invalid encoding '%s'" % encoding) degree = 2 * k + (charge % 2) tse.add_linear(*toArgs(terms, "==", degree)) return tse
def GraphColoringFormula(G, colors, functional=True): """Generates the clauses for colorability formula The formula encodes the fact that the graph :math:`G` has a coloring with color set ``colors``. This means that it is possible to assign one among the elements in ``colors``to that each vertex of the graph such that no two adjacent vertices get the same color. Parameters ---------- G : networkx.Graph a simple undirected graph colors : list or positive int a list of colors or a number of colors Returns ------- CNF the CNF encoding of the coloring problem on graph ``G`` """ col = CNF() if isinstance(colors, int) and colors >= 0: colors = range(1, colors + 1) if not isinstance(list, collections.Iterable): ValueError("Parameter \"colors\" is expected to be a iterable") # Describe the formula name = "graph colorability" if hasattr(G, 'name'): col.header = name + " of graph:\n" + G.name + ".\n\n" + col.header else: col.header = name + ".\n\n" + col.header # Fix the vertex order V = enumerate_vertices(G) # Each vertex has a color for vertex in V: clause = [] for color in colors: clause += [(True, 'x_{{{0},{1}}}'.format(vertex, color))] col.add_clause(clause) # unique color per vertex if functional: for (c1, c2) in combinations(colors, 2): col.add_clause([(False, 'x_{{{0},{1}}}'.format(vertex, c1)), (False, 'x_{{{0},{1}}}'.format(vertex, c2))], strict=True) # This is a legal coloring for (v1, v2) in enumerate_edges(G): for c in colors: col.add_clause([(False, 'x_{{{0},{1}}}'.format(v1, c)), (False, 'x_{{{0},{1}}}'.format(v2, c))], strict=True) return col