1

I'm trying to model a directed graph in z3, but I've gotten stuck. I've added a single axiom to the graph here, being that the existence of an edge implies the existence of the nodes it connects. But just this alone results in unsat

GraphSort = Datatype('GraphSort')
GraphSort.declare('Graph',
    ('V', ArraySort(IntSort(), BoolSort())),
    ('E', ArraySort(IntSort(), ArraySort(IntSort(), BoolSort()))),
)
GraphSort = GraphSort.create()
V = GraphSort.V
E = GraphSort.E

G = Const('G', GraphSort)
n, m = Consts('n m', IntSort())
Graph_axioms = [
    ForAll([G, n, m], Implies(E(G)[n][m], And(V(G)[n], V(G)[m]))),
]

s = Solver()
s.add(Graph_axioms)

I'm trying to model graphs such that V(G)[n] implies the existence of node n and E(G)[n][m] implies the existance of an edge from n to m. Does anyone have any tips as to what's going wrong here? Or better even, any general tips to modelling graphs in z3?

Edit:

With the explanation given by alias, I came up with the following slightly hacky solution:

from itertools import product
from z3 import *
import networkx as nx


GraphSort = Datatype('GraphSort')
GraphSort.declare('Graph',
    ('V', ArraySort(IntSort(), BoolSort())),
    ('E', ArraySort(IntSort(), ArraySort(IntSort(), BoolSort()))),
)
GraphSort = GraphSort.create()
V = GraphSort.V
E = GraphSort.E

class Graph(DatatypeRef):
    def __new__(cls, name):
        # Hijack z3 DatatypeRef instance
        inst = Const(name, GraphSort)
        inst.__class__ = Graph
        return inst

    def __init__(G, name):
        G.axioms = []
        n, m = Ints('n m')
        G.add(ForAll(
            [n, m],
            Implies(E(G)[n][m], And(V(G)[n], V(G)[m]))
        ))

    def add(G, *v):
        G.axioms.extend(v)

    def add_networkx(G, nx_graph):
        g = nx.convert_node_labels_to_integers(nx_graph)

        Vs = g.number_of_nodes()
        Es = g.number_of_edges()

        n = Int('n')
        G.add(ForAll(n, V(G)[n] == And(0 <= n, n < Vs)))
        G.add(*[E(G)[i][k] for i, k in g.edges()])
        G.add(Sum([
            If(E(G)[i][k], 1, 0) for i, k in product(range(Vs), range(Vs))
        ]) == Es)

    def assert_into(G, solver):
        for ax in G.axioms:
            solver.add(ax)


s = Solver()
G = Graph('G')
G.add_networkx(nx.petersen_graph())
G.assert_into(s)
Jens
  • 113
  • 1
  • 10

1 Answers1

2

Your model is unsat because data-types are freely generated. This is a common misconception: When you create a data-type and assert an axiom, you are not restricting z3 to consider only those models that satisfy the axiom. What you're instead saying is that check that all instances of this datatype satisfy the axiom. Which is clearly not true, and hence unsat. This is similar to saying:

a = Int("a")
s.add(ForAll([a], a > 0))

which is also unsat for the very same reason; but hopefully is easier to see why. Also see this answer for an explanation of what "freely generated" means: https://stackoverflow.com/a/60998125/936310

To model graphs such as these, you should only state these axioms on individual instances of the nodes of your graph, not generalized/quantified axioms. Instead of asserting this axiom, focus on other aspects of what you are trying to model. Since you didn't really give us any further details on the problem you want to solve, it's hard to opine any further.

alias
  • 28,120
  • 2
  • 23
  • 40
  • Thanks! That cleared it up nicely! :D Is there a way to put constraints on types? Or would a "normal" way to implement this for example be to wrap instances inside functions that state the axioms? – Jens Oct 20 '20 at 20:39
  • 1
    Putting constraints on types is known as predicate subtyping, but SMTLib does not support it. (The earlier versions of Yices used to support predicate subtyping, but I believe the support is no longer there even in Yices these days.) In short, there's no way to put such constraints directly on types in SMT solvers. To do so, you'd need a more powerful system, i.e., maybe SAL can be used for that purpose (http://sal.csl.sri.com/), or systems like Isabelle/HOL etc. – alias Oct 20 '20 at 21:57
  • Thanks! Updated my answer with a workaround that keeps the interface z3y – Jens Oct 20 '20 at 22:06
  • 1
    Hmm, I'm not sure how that works; but if it does the job for you, great! In general, I'd think you'd start with an empty graph (i.e., an array storing all `False`'s), then build your graph by incrementally updating the array by inserting the nodes. This assumes you've a fixed graph of course. But hard to tell from your question exactly what problem you're trying to solve. – alias Oct 21 '20 at 01:41
  • I'm really just exploring what the possibilities are, I'm very new so z3, SMT- or even SAT-solvers in general. What I'd like to try next is to generate a set of operations to transform one graph into another, for example, two similar graphs where one is missing an edge. Maybe it could be `add_edge(G0, n, m) == G1`. My attempts at this did not terminate though, so I have a long way to go before I get a feel to what z3 can solve in a reasonable amount of time. I'll try an approach starting with empty graphs instead next, then It'll be more analogous to Array as well! Thanks for the tips! – Jens Oct 21 '20 at 10:43