I am trying out different solvers for a toy clique problem and was surprised to find that ortools using SAT seems much faster than z3. I am wondering if I am doing something wrong given that z3 does so well in the published benchmarks. Here is an MWE:
First create a random graph with 150 vertices:
# First make a random graph and find the largest clique size in it
import igraph as ig
import random
from tqdm import tqdm
random.seed(7)
num_variables = 150 # bigger gives larger running time gap between z3 and ortools grow
print("Making graph")
g = ig.Graph.Erdos_Renyi(num_variables, 0.6)
# Make a set of edges. Maybe this isn't necessary
print("Making set of edges")
edges = set()
for edge in tqdm(g.es):
edges.add((edge.source, edge.target))
Now use z3 to find the max clique size:
import z3
z3.set_option(verbose=1)
myVars = []
for i in range(num_variables):
myVars += [z3.Int('v%d' % i)]
opt = z3.Optimize()
for i in range(num_variables):
opt.add(z3.Or(myVars[i]==0, myVars[i] == 1))
for i in tqdm(range(num_variables)):
for j in range(i+1, num_variables):
if not (i, j) in edges:
opt.add(myVars[i] + myVars[j] <= 1)
t = time()
h = opt.maximize(sum(myVars))
opt.check()
print(round(time()-t,2))
This takes around 70 seconds on my PC.
Now do the same thing using SAT from ortools.
from ortools.linear_solver import pywraplp
solver = pywraplp.Solver.CreateSolver('SAT')
solver.EnableOutput()
myVars = []
for i in range(num_variables):
myVars += [solver.IntVar(0.0, 1.0, 'v%d' % i)]
for i in tqdm(range(num_variables)):
for j in range(i+1, num_variables):
if not (i, j) in edges:
solver.Add(myVars[i] + myVars[j] <= 1)
print("Solving")
solver.Maximize(sum(myVars))
t = time()
status = solver.Solve()
print(round(time()-t,2))
This takes about 4 seconds on my PC.
If you increase num_variables the gap grows even larger.
Am I doing something wrong or is this just a really bad case for the z3 optimizer?
Update
It turns out that ortools using SAT is multi-core by default using 8 cores so the timings are unfair. Using @AxelKemper's improvement I now get (on a different machine):
- Z3 time 88 seconds. If we assume perfect parallelisation that is 11 seconds on 8 cores.
- ortools 4.3 seconds
So Z3 is only 2.5 times slower than ortools.
Update 2
Using @alias's improved code that uses s.add(z3.PbGe([(x, 1) for x in myVars], k))
it now takes 30.4 seconds which when divided by 8 is faster than ortools!