A small announcement. Mostly as a way to play with Crystal, I created Crystal bindings for Z3 library, and I think it might be quite usable as a MVP.
Check included examples and specs for how to use it.
The best kittens, technology, and video games blog in the world.
A small announcement. Mostly as a way to play with Crystal, I created Crystal bindings for Z3 library, and I think it might be quite usable as a MVP.
Check included examples and specs for how to use it.
Posted by
taw
at
16:50
0
comments
Z3_context Z3_ast (Abstract Syntax Tree), and a bunch of functions to operate on them.
For example to create a node representing equality of two nodes, you call:Z3_ast Z3_API Z3_mk_eq(Z3_context c, Z3_ast l, Z3_ast r);
Z3_ast, but it needs to be particular kind of Z3_ast, otherwise you get a segfault.
It's not even static limitation - l and r can be anything, but they must represent the same type.
So any kind of thin wrapper is out of the question.
ffi to setup Z3::VeryLowLevel with direct C calls. For example the aforementioned function is attached like this:
attach_function :Z3_mk_eq, [:ctx_pointer, :ast_pointer, :ast_pointer], :ast_pointer
api and generates most of it with some regular expressions.
A list of C API calls is extracted from Z3 documentation into api/definitions.h. They look like this:
def_API('Z3_mk_eq', AST, (_in(CONTEXT), _in(AST), _in(AST)))
api/gen_api script translates it into proper ruby code. It might seem like it could be handled by ffi library, but there are too many Z3-specific hacks needed.
A small number of function calls can't be handled automatically, so they're written manually.Z3_mk_add function creates a node representing addition of any number of nodes, and has signature of:
attach_function :Z3_mk_add, [:ctx_pointer, :int, :pointer], :ast_pointer
Z3::LowLevel is also mostly generated by api/gen_api.
Here's an example of automatically generated code:
def mk_eq(ast1, ast2) #=> :ast_pointer
VeryLowLevel.Z3_mk_eq(_ctx_pointer, ast1._ast, ast2._ast)
end
def mk_and(asts) #=> :ast_pointer
VeryLowLevel.Z3_mk_and(_ctx_pointer, asts.size, asts_vector(asts))
end
Z3_context pointer for almost all of its calls - we automatically provide it with singleton _ctx_pointer.FFI::Pointer and leave responsibility for wrapping them into ruby objects to the caller, as we actually don't have enough information here to do so.Z3::LowLevel API does is setting up error callback, to convert Z3 errors into Ruby exceptions.Z3::AST, which is a wrapper for FFI::Pointer representing Z3_ast.
Other Z3 C data types get similar treatment.
module Z3
class AST
attr_reader :_ast
def initialize(_ast)
raise Z3::Exception, "AST expected, got #{_ast.class}" unless _ast.is_a?(FFI::Pointer)
@_ast = _ast
end
# ...
private_class_method :new
end
end
First weird thing is this Python-style pseudo-private ._ast. This really shouldn't ever be accessed by user of the gem, but it needs to be accessed by Z3::LowLevel a lot. Ruby doesn't have any concept of C++ style "friend" classes. I've chosen Python pseudo-private convention as opposed to a lot of .instance_eval or similar.Z3::AST class prevents object creation - only its subclasses representing nodes of specific type can be instantiated.Z3::BoolSort, which should only have a single object.module Z3
class Sort < AST
def initialize(_ast)
super(_ast)
raise Z3::Exception, "Sorts must have AST kind sort" unless ast_kind == :sort
end
# ...
module Z3
class BoolSort < Sort
def initialize
super LowLevel.mk_bool_sort
end
def expr_class
BoolExpr
end
def from_const(val)
if val == true
BoolExpr.new(LowLevel.mk_true, self)
elsif val == false
BoolExpr.new(LowLevel.mk_false, self)
else
raise Z3::Exception, "Cannot convert #{val.class} to #{self.class}"
end
end
public_class_method :new
end
end
ast_kind check is for additional segfault prevention.BoolSort.new creates Ruby object with instance variable _sort pointing to Z3_ast describing Boolean sort.BoolSort with just two instance values, but some Sort classes have multiple Sort instances. For example Bit Vectors of width n are:module Z3
class BitvecSort < Sort
def initialize(n)
super LowLevel.mk_bv_sort(n)
end
def expr_class
BitvecExpr
end
module Z3
class Expr < AST
attr_reader :sort
def initialize(_ast, sort)
super(_ast)
@sort = sort
unless [:numeral, :app].include?(ast_kind)
raise Z3::Exception, "Values must have AST kind numeral or app"
end
end
Bool true, but it's extremely important for BitvecExpr to know if it's 8-bit or 24-bit. Because if they get mixed up, segfault.IntSort.new.from_const(42)
IntSort.new.var("x")
module Z3
class BitvecExpr < Expr
def rotate_left(num)
sort.new(LowLevel.mk_rotate_left(num, self))
end
Z3::Int("a") + 2 == Z3::Int("b")
SignedBitvectorExpr and UnsignedBitvectorExpr as separate types with nice APIs, unfortunately there's no way to infer if answer Z3 gave came from Ruby SignedBitvectorExpr or UnsignedBitvectorExpr, so that idea can't work.examples/ directory. They're a good starting point to learn common use cases.==.let(:a) { Z3.Real("a") }
let(:b) { Z3.Real("b") }
let(:c) { Z3.Real("c") }
it "+" do
expect([a == 2, b == 4, c == a + b]).to have_solution(c => 6)
end
examples and verify that output is exactly as expected. I like reusing other things as test cases like this.
require "z3"
class SelfRefPuzzleSolver
attr_reader :q
def initialize
@solver = Z3::Solver.new
@q = {}
(1..20).each do |i|
@q[i] = Z3.Int("Q#{i}")
@solver.assert @q[i] >= 1
@solver.assert @q[i] <= 5
end
end
def answer(question_number, a)
@q[question_number] == " ABCDE".index(a)
end
def define(question_number, a)
@solver.assert answer(question_number, a).implies(yield)
end
def print_answers!
raise "Something went wrong" unless @solver.satisfiable?
model = @solver.model
(1..20).each do |i|
answer = " ABCDE"[model[q[i]].to_i]
puts "Q#{'%2d' % i}: #{answer}"
end
end
def solve!
# Question code goes here
print_answers!
end
end def count(*statements)
Z3.Add(*statements.map{|b| b.ite(1,0)})
end
def count_answers(a, range=1..20)
count(*range.map{|i| answer(i, a)})
end define(1, "A"){ answer(1, "B") }
define(1, "B"){ !answer(1, "B") &
answer(2, "B") }
define(1, "C"){ !answer(1, "B") &
!answer(2, "B") &
answer(3, "B") }
define(1, "D"){ !answer(1, "B") &
!answer(2, "B") &
!answer(3, "B") &
answer(4, "B") }
define(1, "E"){ !answer(1, "B") &
!answer(2, "B") &
!answer(3, "B") &
!answer(4, "B") &
answer(5, "B") } question_pairs = (1..20).each_cons(2)
define(2, "A") {
Z3.And(*question_pairs.map{|i,j| (q[i] == q[j]) == (i == 6) })
}
define(2, "B") {
Z3.And(*question_pairs.map{|i,j| (q[i] == q[j]) == (i == 7) })
}
define(2, "C") {
Z3.And(*question_pairs.map{|i,j| (q[i] == q[j]) == (i == 8) })
}
define(2, "D") {
Z3.And(*question_pairs.map{|i,j| (q[i] == q[j]) == (i == 9) })
}
define(2, "E") {
Z3.And(*question_pairs.map{|i,j| (q[i] == q[j]) == (i == 10) })
} define(3, "A") { count_answers("E") == 0 }
define(3, "B") { count_answers("E") == 1 }
define(3, "C") { count_answers("E") == 2 }
define(3, "D") { count_answers("E") == 3 }
define(3, "E") { count_answers("E") == 4 } define(4, "A") { count_answers("A") == 4 }
define(4, "B") { count_answers("A") == 5 }
define(4, "C") { count_answers("A") == 6 }
define(4, "D") { count_answers("A") == 7 }
define(4, "E") { count_answers("A") == 8 } define(5, "A") { q[5] == q[1] }
define(5, "B") { q[5] == q[2] }
define(5, "C") { q[5] == q[3] }
define(5, "D") { q[5] == q[4] }
define(5, "E") { q[5] == q[5] } define(6, "A") { answer(17, "C") }
define(6, "B") { answer(17, "D") }
define(6, "C") { answer(17, "E") }
define(6, "D") { answer(17, "A") | answer(17, "B") }
define(6, "E") { false } def abs(x)
(x >= 0).ite(x, -x)
end distance_7_8 = abs(q[7] - q[8])
define(7, "A") { distance_7_8 == 4 }
define(7, "B") { distance_7_8 == 3 }
define(7, "C") { distance_7_8 == 2 }
define(7, "D") { distance_7_8 == 1 }
define(7, "E") { distance_7_8 == 0 }
distance_7_8 = q[7] - q[8]
define(7, "A") { (distance_7_8 == 4) | (distance_7_8 == -4) }
define(7, "B") { (distance_7_8 == 3) | (distance_7_8 == -3) }
define(7, "C") { (distance_7_8 == 2) | (distance_7_8 == -2) }
define(7, "D") { (distance_7_8 == 1) | (distance_7_8 == -1) }
define(7, "E") { distance_7_8 == 0 }
count_vowel_answers = count_answers("A") + count_answers("E")
define(8, "A") { count_vowel_answers == 4 }
define(8, "B") { count_vowel_answers == 5 }
define(8, "C") { count_vowel_answers == 6 }
define(8, "D") { count_vowel_answers == 7 }
define(8, "E") { count_vowel_answers == 8 } define(9, "A") { Z3.And(q[9] == q[10]) }
define(9, "B") { Z3.And(q[9] != q[10],
q[9] == q[11]) }
define(9, "C") { Z3.And(q[9] != q[10],
q[9] != q[11],
q[9] == q[12]) }
define(9, "D") { Z3.And(q[9] != q[10],
q[9] != q[11],
q[9] != q[12],
q[9] == q[13]) }
define(9, "E") { Z3.And(q[9] != q[10],
q[9] != q[11],
q[9] != q[12],
q[9] != q[13],
q[9] == q[14]) } define(10, "A") { answer(16, "D") }
define(10, "B") { answer(16, "A") }
define(10, "C") { answer(16, "E") }
define(10, "D") { answer(16, "B") }
define(10, "E") { answer(16, "C") } preceding_B_answers = count_answers("B", 1..10)
define(11, "A") { preceding_B_answers == 0 }
define(11, "B") { preceding_B_answers == 1 }
define(11, "C") { preceding_B_answers == 2 }
define(11, "D") { preceding_B_answers == 3 }
define(11, "E") { preceding_B_answers == 4 } consonant_answers = count_answers("B") +
count_answers("C") +
count_answers("D")
define(12, "A") {
equals_one_of(consonant_answers, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
}
define(12, "B") {
equals_one_of(consonant_answers, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19)
}
define(12, "C") {
equals_one_of(consonant_answers, 0, 1, 4, 9, 16)
}
define(12, "D") {
equals_one_of(consonant_answers, 2, 3, 5, 7, 11, 13, 17, 19)
}
define(12, "E") {
equals_one_of(consonant_answers, 0, 5, 10, 15, 20)
} odd_questions = 1.step(19, 2)
define(13, "A") {
Z3.And(*odd_questions.map{|i| answer(i, "A") == (i == 9) })
}
define(13, "B") {
Z3.And(*odd_questions.map{|i| answer(i, "A") == (i == 11) })
}
define(13, "C") {
Z3.And(*odd_questions.map{|i| answer(i, "A") == (i == 13) })
}
define(13, "D") {
Z3.And(*odd_questions.map{|i| answer(i, "A") == (i == 15) })
}
define(13, "E") {
Z3.And(*odd_questions.map{|i| answer(i, "A") == (i == 17) })
} @solver.assert !answer(1, "A")
@solver.assert !answer(3, "A")
@solver.assert !answer(5, "A")
@solver.assert !answer(7, "A")
@solver.assert answer(9, "A") == answer(13, "A")
@solver.assert answer(11, "A") == answer(13, "B")
@solver.assert answer(13, "A") == answer(13, "C")
@solver.assert answer(15, "A") == answer(13, "D")
@solver.assert answer(17, "A") == answer(13, "E")
@solver.assert !answer(19, "A")
define(14, "A") { count_answers("D") == 6 }
define(14, "B") { count_answers("D") == 7 }
define(14, "C") { count_answers("D") == 8 }
define(14, "D") { count_answers("D") == 9 }
define(14, "E") { count_answers("D") == 10 }
define(15, "A") { answer(12, "A") }
define(15, "B") { answer(12, "B") }
define(15, "C") { answer(12, "C") }
define(15, "D") { answer(12, "D") }
define(15, "E") { answer(12, "E") }
define(16, "A") { answer(10, "D") }
define(16, "B") { answer(10, "C") }
define(16, "C") { answer(10, "B") }
define(16, "D") { answer(10, "A") }
define(16, "E") { answer(10, "E") }
define(17, "A") { answer(6, "C") }
define(17, "B") { answer(6, "D") }
define(17, "C") { answer(6, "E") }
define(17, "D") { answer(6, "A") | answer(6, "B") }
define(17, "E") { false }
define(18, "A") { count_answers("A") == count_answers("B") }
define(18, "B") { count_answers("A") == count_answers("C") }
define(18, "C") { count_answers("A") == count_answers("D") }
define(18, "D") { count_answers("A") == count_answers("E") }
define(18, "E") {
Z3.And(
count_answers("A") != count_answers("B"),
count_answers("A") != count_answers("C"),
count_answers("A") != count_answers("D"),
count_answers("A") != count_answers("E"),
)
}
define(19, "A") { answer(19, "A") }
define(19, "B") { answer(19, "B") }
define(19, "C") { answer(19, "C") }
define(19, "D") { answer(19, "D") }
define(19, "E") { answer(19, "E") }
define(20, "A") { false }
define(20, "B") { false }
define(20, "C") { false }
define(20, "D") { false }
define(20, "E") { true } @solver.assert answer(20, "E")
Q 1: D
Q 2: A
Q 3: D
Q 4: B
Q 5: E
Q 6: D
Q 7: D
Q 8: E
Q 9: D
Q10: A
Q11: B
Q12: A
Q13: D
Q14: B
Q15: A
Q16: D
Q17: B
Q18: A
Q19: B
Q20: E
Posted by
taw
at
15:32
2
comments
Z3 is quite amazing, and whenever I show it to people on various unconferences response is very enthusiastic, but it desperately needs some tutorials.
I wrote one for sudoku, but sudoku is maybe just a too straightforward example - variables are simply what goes into cells, and constraints are simply game rules. Usually you'll need at least a bit more modelling than that.
So let's try something just a bit more complicated - nonograms:
class NonogramSolver
def initialize
@row_constraints = [
[3],
[5],
[3,1],
[2,1],
[3,3,4],
[2,2,7],
[6,1,1],
[4,2,2],
[1,1],
[3,1],
[6],
[2,7],
[6,3,1],
[1,2,2,1,1],
[4,1,1,3],
[4,2,2],
[3,3,1],
[3,3],
[3],
[2,1],
]
@column_constraints = [
[2],
[1,2],
[2,3],
[2,3],
[3,1,1],
[2,1,1],
[1,1,1,2,2],
[1,1,3,1,3],
[2,6,4],
[3,3,9,1],
[5,3,2],
[3,1,2,2],
[2,1,7],
[3,3,2],
[2,4],
[2,1,2],
[2,2,1],
[2,2],
[1],
[1],
]
@row_count = @row_constraints.size
@column_count = @column_constraints.size
@solver = Z3::Solver.new
end
end
def cell(x,y)
Z3.Bool("cell#{x},#{y}")
end
def row(y)
(0...@column_count).map{|x| cell(x,y) }
end
def column(x)
(0...@row_count).map{|y| cell(x,y) }
end
def setup_grid_constraints!
(0...@column_count).each do |x|
setup_stripe_constraints! "column-#{x}", @column_constraints[x], column(x)
end
(0...@row_count).each do |y|
setup_stripe_constraints! "row-#{y}", @row_constraints[y], row(y)
end
end
def setup_stripe_constraints!(stripe_name, stripe_constraints, stripe)
group_count = stripe_constraints.size
group_start = (0...group_count).map{|i| Z3.Int("#{stripe_name}-#{i}-start")}
group_end = (0...group_count).map{|i| Z3.Int("#{stripe_name}-#{i}-end")}
# Start and end of each group
(0...group_count).each do |i|
@solver.assert (group_start[i] >= 0) & (group_start[i] < stripe.size)
@solver.assert (group_end[i] >= 0) & (group_end[i] < stripe.size)
@solver.assert group_end[i] - group_start[i] == stripe_constraints[i] - 1
end
# Gap between each group and following group
(0...group_count).each_cons(2) do |i,j|
@solver.assert group_start[j] >= group_end[i] + 2
end
# Cells
(0...stripe.size).each do |k|
cell_in_specific_group = (0...group_count).map{|i|
(k >= group_start[i]) & (k <= group_end[i])
}
@solver.assert stripe[k] == Z3.Or(*cell_in_specific_group)
end
end
def solve!
setup_grid_constraints!
if @solver.satisfiable?
model = @solver.model
(0...@row_count).each do |y|
(0...@column_count).each do |x|
value = model[cell(x,y)].to_s
print value == "true" ? "\u25FC" : "\u25FB"
end
print "\n"
end
else
puts "Nonogram has no solution"
end
end
_ 6 _ 5 _ 9 _ 4 _
9 2 _ _ _ _ _ 7 6
_ _ 1 _ _ _ 9 _ _
7 _ _ 6 _ 3 _ _ 9
_ _ _ _ _ _ _ _ _
3 _ _ 4 _ 1 _ _ 7
_ _ 6 _ _ _ 7 _ _
2 4 _ _ _ _ _ 6 5
_ 9 _ 1 _ 8 _ 3 _
File.read(path).strip.split("\n").map do |line|
line.split.map{|c| c == "_" ? nil : c.to_i}
end
[[nil, 6, nil, 5, nil, 9, nil, 4, nil],
[9, 2, nil, nil, nil, nil, nil, 7, 6],
[nil, nil, 1, nil, nil, nil, 9, nil, nil],
[7, nil, nil, 6, nil, 3, nil, nil, 9],
[nil, nil, nil, nil, nil, nil, nil, nil, nil],
[3, nil, nil, 4, nil, 1, nil, nil, 7],
[nil, nil, 6, nil, nil, nil, 7, nil, nil],
[2, 4, nil, nil, nil, nil, nil, 6, 5],
[nil, 9, nil, 1, nil, 8, nil, 3, nil]]
solver = Z3::Solver.new
cells = (0..8).map do |j|
(0..8).map do |i|
Z3.Int("cell[#{i},#{j}]")
end
end
cells.flatten.each do |v|
solver.assert v >= 1
solver.assert v <= 9
end
cells.each_with_index do |row, i|
row.each_with_index do |var, j|
solver.assert var == data[i][j] if data[i][j]
end
end
cells.each do |row|
solver.assert Z3.Distinct(*row)
end
cells.transpose.each do |column|
solver.assert Z3.Distinct(*column)
end
cells.each_slice(3) do |rows|
rows.transpose.each_slice(3) do |square|
solver.assert Z3.Distinct(*square.flatten)
end
end
By the way, you should really take a look at Enumerable API, it contains a lot of useful methods which will save you a ton of time.
raise "Failed to solve" unless solver.check == :sat
model = solver.model
cells.each do |row|
puts row.map{|v| model[v]}.join(" ")
end
8 6 3 5 7 9 2 4 1
9 2 5 3 1 4 8 7 6
4 7 1 8 2 6 9 5 3
7 1 4 6 8 3 5 2 9
6 8 9 7 5 2 3 1 4
3 5 2 4 9 1 6 8 7
1 3 6 2 4 5 7 9 8
2 4 8 9 3 7 1 6 5
5 9 7 1 6 8 4 3 2
Posted by
taw
at
06:01
2
comments
Posted by
taw
at
03:55
0
comments
Unless otherwise expressly stated, all original material of whatever nature created by Tomasz Węgrzanowski and included in this blog, is licensed under a Creative Commons License. It is also licensed under GFDL (for Wikipedia compatibility).