Search code examples
pythonz3z3py

Find all optimal solutions with Optimize() from z3py


z3 can solve optimisation problems using z3.Optimize. My goal is to apply it in the following way: Given a Boolean formula of propositional logic and a set of constraints, find all models that optimise the constraints, i.e. fulfil as many as possible. My ultimate goal is to select one of these models randomly.

Minimal example: Given the formula Or(x,y) and constraints Not(x) and Not(y), find the solutions [x: True, y: False] and [x: False, y: True].

While z3.Optimize.model() prints one optimal solution, it seems to be deterministic and to always print the solution [x: False, y: True]:

from z3 import *

x, y = Bools('x y')
s = Optimize()
s.add(Or(x,y))
s.add_soft(Not(x), weight="1")
s.add_soft(Not(y), weight="1")
s.check()
print(s.model())

When all_smt() provided by @alias is applied to this problem, list(all_smt(s, [x,y])) outputs:

[[x = False, y = True], [y = False, x = True], [y = True, x = True]]

So all_smt() finds all solutions, including the non-optimal [x: True, y: True].

What is the best way to find all optimal solutions? Alternatively, is it possible to find an optimal solution in a random way without generating all optimal solutions first?


Solution

  • It's not quite clear what you mean by "optimal" in this context. It appears you allow the soft-constraints to be violated, but at least one of them must hold? (After all, ([x=True, y=True] is a valid solution, violating both your soft-constraints; which you did allow by explicitly calling all_smt.)

    Assuming you are indeed looking for solutions that satisfy at least one of the soft-constraints, you should explicitly code that up and assert as a separate predicate. The easiest way to do this would be to use tracker variables:

    from z3 import *
    
    def all_smt(s, initial_terms):
        def block_term(s, m, t):
            s.add(t != m.eval(t, model_completion=True))
        def fix_term(s, m, t):
            s.add(t == m.eval(t, model_completion=True))
        def all_smt_rec(terms):
            if sat == s.check():
               m = s.model()
               yield m
               for i in range(len(terms)):
                   s.push()
                   block_term(s, m, terms[i])
                   for j in range(i):
                       fix_term(s, m, terms[j])
                   yield from all_smt_rec(terms[i:])
                   s.pop()
        yield from all_smt_rec(list(initial_terms))
    
    x, y = Bools('x y')
    s = Optimize()
    
    s.add(Or(x,y))
    
    p1, p2 = Bools('p1 p2')
    s.add(Implies(p1, Not(x)))
    s.add(Implies(p2, Not(y)))
    
    asserted = If(p1, 1, 0) + If(p2, 1, 0)
    s.add(asserted >= 1)
    s.minimize(asserted)
    
    print([[(x, m[x]), (y, m[y])] for m in all_smt(s, [x,y])])
    

    Here, we use p1 and p2 to track whether the soft-constraints are asserted, and we minimize the number of soft-constraints via the variable asserted yet requiring it should at least be 1. When run, the above prints:

    [[(x, True), (y, False)], [(x, False), (y, True)]]
    

    which seems to be what you wanted to achieve in the first place.