Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Problem Solved!

Problem Solved!

Using Logic Programming to Find Answers: putting AI approaches in the hands of developers, to turn problem solving over to the computer.

Delivered at RubyConf 2016.

Gavin McGimpsey

November 10, 2016
Tweet

Other Decks in Programming

Transcript

  1. The ideal • Tell the computer how the world works

    • Get the answer to life, the universe, and everything class DeepThought def solve return 42 end end
  2. Atbash Cipher • Keep letters and numbers a ⇔ z

    b ⇔ y c ⇔ x d ⇔ w e ⇔ v f ⇔ u g ⇔ t h ⇔ s i ⇔ r j ⇔ q k ⇔ p l ⇔ o m ⇔ n RubyConf 2016! • Letters swap alphabet position • Downcase and chunk into “words” of 5 characters i f y b x l m u 2 0 1 6 ⇒
  3. Function Chaining Atbash Cipher • Keep letters and numbers •

    Letters swap alphabet position • Downcase and chunk into “words” of 5 characters class Atbash def self.key { 'a' => 'z', 'b' => 'y', ... } end def self.encode(str) str.chars .grep(/[[:alnum:]]/) .map { |ch| key[ch.downcase] || ch } .each_slice(5) .collect_concat(&:join) .join(' ') end end
  4. Relational thinking Relations: work with what you’ve got sum_r(18, 24,

    ?) ⇒ match ? with 42 sum_r(18, ?, 42) ⇒ match ? with 24
  5. SQL Build a database of relations (tables) and facts (rows)

    Execute queries declaratively SELECT * AS the_answer FROM thank JOIN goodness ON ... JOIN for ON ... JOIN query ON ... JOIN planners ON ...
  6. n Queens On an n by n chessboard, place n

    queens, such that they cannot attack each other. ♛
  7. n Queens Some variation on: • Pick an empty square

    and place a queen • Block affected squares • Repeat Options: • Breadth-first • Depth-first ◦ With backtracking?
  8. Constraint Propagation • Repeatedly apply rules until reaching a fixpoint

    ◦ Eliminate each square that’s already attackable ◦ If a row/column has only one empty slot, place a queen • Branch to continue moving forward ◦ n Queens: pick an empty square and place a queen
  9. n Queens Propagate Constraints • Eliminate attackable squares • Place

    queens in rows/columns with only one open square ♛ ♛
  10. Sudoku • 9 x 9 Grid or: a 3 x

    3 grid of 3 x 3 boxes • Fill in numbers 1 through 9 in each ◦ Row ◦ Column ◦ Box • No repeats within any of those three dimensions
  11. Constraint Procedure If: • a slot has one possibility, or

    • a unit has only one eligible slot for a needed value Then: • Set the slot • Remove that possibility from other slots in affected units A B C D E F G H I 1 2 3 4 5 6 7 8 9 9
  12. Sudoku – Constraint Propagation def cross(A, B): return [a+b for

    a in A for b in B] digits = '123456789' rows = 'ABCDEFGHI' cols = digits squares = cross(rows, cols) unitlist = ([cross(rows, c) for c in cols] + [cross(r, cols) for r in rows] + [cross(rs, cs) for rs in ('ABC','DEF','GHI') for cs in ('123','456','789')]) units = dict((s, [u for u in unitlist if s in u]) for s in squares) peers = dict((s, set(sum(units[s],[]))-set([s])) for s in squares) def parse_grid(grid): values = dict((s, digits) for s in squares) for s,d in grid_values(grid).items(): if d in digits and not assign(values, s, d): return False ## (Fail if we can't assign d to square s.) def grid_values(grid): chars = [c for c in grid if c in digits or c in '0.'] assert len(chars) == 81 return dict(zip(squares, chars)) def display(values): width = 1+max(len(values[s]) for s in squares) line = '+'.join(['-'*(width*3)]*3) for r in rows: print ''.join(values[r+c].center(width)+('|' if c in '36' else '') for c in cols) if r in 'CF': print line print def assign(values, s, d): other_values = values[s].replace(d, '') if all(eliminate(values, s, d2) for d2 in other_values): return values else: return False def eliminate(values, s, d): if d not in values[s]: return values ## Already eliminated values[s] = values[s].replace(d,'') if len(values[s]) == 0: return False elif len(values[s]) == 1: d2 = values[s] if not all(eliminate(values, s2, d2) for s2 in peers[s]): return False for u in units[s]: dplaces = [s for s in u if d in values[s]] if len(dplaces) == 0: return False ## Contradiction: no place for this value elif len(dplaces) == 1: # d can only be in one place in unit; assign it there if not assign(values, dplaces[0], d): return False return values def some(seq): for e in seq: if e: return e return False def search(values): if values is False: return False ## Failed earlier if all(len(values[s]) == 1 for s in squares): return values ## Solved! ## Chose the unfilled square s with the fewest possibilities n,s = min((len(values[s]), s) for s in squares if len(values[s]) > 1) return some(search(assign(values.copy(), s, d)) for d in values[s]) def solve(grid): return search(parse_grid(grid)) http://norvig.com/sudoku.html
  13. Sudoku – Logic Solver require_relative 'logic' class Sudoku col_thirds =

    [%w{A B C}, %w{D E F}, %w{G H I}] row_thirds = [%w{1 2 3}, %w{4 5 6}, %w{7 8 9}] @@boxes = row_thirds.product(col_thirds).map {|set| set[1].product(set[0])} .map { |box| box.map { |combo| combo.join.to_sym } } col_signs = col_thirds.flatten row_signs = row_thirds.flatten @@cells = row_signs.product(col_signs).map { |combo| combo.reverse.join.to_sym } @@cols = @@cells.each_slice(9).to_a @@rows = @@cols.transpose @@units = @@cols + @@rows + @@boxes def parse_grid(str) @@cells.zip(str.chars) .keep_if { |cell, char| /[[:digit:]]/ === char } .map { |cell, char| [cell, char.to_i] } end def to_s @results.map do |result| @@cells.map { |cell| result[cell].to_s } .each_slice(9) .map(&:join) end.join("\n") end def initialize(str) starting_grid = parse_grid(str) solver = Logic::Solver.new do @@cells.each { |cell| assert domain_is( cell, (1..9) ) } @@units.each do |unit| assert has_unique_contents(unit) (1..9).each { |num| assert must_include(unit, num) } end starting_grid.each { |cell, value| assert equal(cell, value) } end solver.solve @results = solver.results end end
  14. Sudoku – Constraint Propagation Core def assign(values, s, d): other_values

    = values[s].replace(d, '') if all(eliminate(values, s, d2) for d2 in other_values): return values else: return False def eliminate(values, s, d): if d not in values[s]: return values ## Already eliminated values[s] = values[s].replace(d,'') if len(values[s]) == 0: return False elif len(values[s]) == 1: d2 = values[s] if not all(eliminate(values, s2, d2) for s2 in peers[s]): return False for u in units[s]: dplaces = [s for s in u if d in values[s]] if len(dplaces) == 0: return False ## Contradiction: no place for this value elif len(dplaces) == 1: # d can only be in one place in unit; assign it there if not assign(values, dplaces[0], d): return False return values def some(seq): for e in seq: if e: return e return False def search(values): if values is False: return False ## Failed earlier if all(len(values[s]) == 1 for s in squares): return values ## Solved! ## Chose the unfilled square s with the fewest possibilities n,s = min((len(values[s]), s) for s in squares if len(values[s]) > 1) return some(search(assign(values.copy(), s, d)) for d in values[s]) def solve(grid): return search(parse_grid(grid)) http://norvig.com/sudoku.html
  15. Sudoku – Logic Solver Core def initialize(str) starting_grid = parse_grid(str)

    solver = Logic::Solver.new do @@cells.each { |cell| assert domain_is( cell, (1..9) ) } @@units.each do |unit| assert has_unique_contents(unit) (1..9).each { |num| assert must_include(unit, num) } end starting_grid.each { |cell, value| assert equal(cell, value) } end solver.solve @results = solver.results end
  16. Using a Solver Declare: • Squares hold values 1 through

    9! • Units include all 9 digits! Then, let the solver figure it out! A B C D E F G H I 1 2 3 4 5 6 7 8 9
  17. Declarative Sudoku Squares must hold values 1 through 9! solver

    = Logic::Solver.new do squares.each { |sq| assert domain_is( sq, (1..9) ) } end
  18. Declarative Sudoku Units include all 9 digits! solver.more do [rows,

    columns, boxes].each do |unit| assert has_unique_contents(unit) (1..9).each { |num| assert must_include(unit, num) } end end
  19. Declarative Sudoku Off to the races! known_squares = [[:A1, 5],

    [:B1, 1], …] known_squares.each do |sq| assert equal(*sq) end A B C D E F G H I 1 2 3 4 5 6 7 8 9
  20. Sudoku – Core 1 starting_grid = parse_grid(str) 2 3 solver

    = Logic::Solver.new do 4 @@cells.each { |cell| assert domain_is( cell, (1..9) ) } 5 6 @@units.each do |unit| 7 assert has_unique_contents(unit) 8 (1..9).each { |num| assert must_include(unit, num) } 9 end 10 11 starting_grid.each { |cell, value| assert equal(cell, value) } 12 end 13 14 solver.solve 15 @results = solver.results
  21. exactly_one_member(collection, value) assert any( *collection.map do |item| assert equal(item, value)

    (collection - item).each do |other| assert not_equal(other, value) end end )
  22. Unification Pattern matching, with knowledge storage: • Build a substitution

    list of how unknowns map to values or each other • Query it to see what unknowns resolve to concat_r([m, ? 1 , t], [? 2 ], [m, a, t, z]) ⇒ match ? 1 with a, and ? 2 with z
  23. Substitution List Variable p matches with 5 Variable q matches

    with variable r Variable r matches with 8 Query Variable p matches with variable q ? ⇒ { p: 5 } ⇒ { p: 5, q: :r} ⇒ { p: 5, q: :r, r: 8 } ⇒ (5 == 8) Assertions
  24. Unification - the algorithm • “Walk” the values we want

    to unify through the substitution list • Outcomes: ◦ Initial values already match to the same known result ◦ Initial values match to known contradictory results ◦ Substitution chain ends with a still-unknown value
  25. Constraint Propagation in Ruby 1 def propagate_constraints 2 loop do

    3 reset_changed_flags 4 5 remaining_possibilities = @possibilities.reject(&:solved) 6 7 remaining_possibilities.each do |possibility| 8 possibility.apply(rules) 9 end 10 possibilities.keep_if(&:valid) 11 12 break unless something_changed 13 end 14 end
  26. Unification in Ruby 1 def unify?(left, right) 2 left, right

    = walk(left), walk(right) # walk returns the object if there 3 extension = SubstitutionList.new # isn’t a substitution for it 4 5 if [left, right].all? { |e| e.kind_of?(Symbol) } && (left == right) 6 return extension # no changes needed, so it stays empty 7 end 8 9 if left.kind_of? Symbol # if either side is an unknown 10 extension[left] = right # attach it to a known value or 11 elsif right.kind_of? Symbol # chain it to another unknown 12 extension[right] = left 13 else 14 extension = left.class.unify(left, right, self) # defaults to == 15 end 16 extension 17 end
  27. Unification in Ruby 1 def unify?(left, right) 2 left, right

    = walk(left), walk(right) # walk returns the object if there 3 extension = SubstitutionList.new # isn’t a substitution for it
  28. Unification in Ruby 3 extension = SubstitutionList.new 4 5 if

    [left, right].all? { |e| e.kind_of?(Symbol) } && (left == right) 6 return extension 7 end
  29. Unification in Ruby 7 end 8 9 if left.kind_of? Symbol

    10 extension[left] = right 11 elsif right.kind_of? Symbol 12 extension[right] = left 13 else
  30. Unification 1 def unify?(left, right) 2 left, right = walk(left),

    walk(right) 3 extension = SubstitutionList.new 4 5 if [left, right].all? { |e| e.kind_of?(Symbol) } && (left == right) 6 return extension 7 end 8 9 if left.kind_of? Symbol 10 extension[left] = right 11 elsif right.kind_of? Symbol 12 extension[right] = left 13 else 14 extension = left.class.unify(left, right, self) 15 end 16 extension 17 end
  31. Disjunction 1 would_unify = unifications.unify?(left, right) 2 if !would_unify 3

    # All set! 4 elsif would_unify.empty? 5 invalidate 6 else 7 disequalities.push(would_unify) 8 end
  32. API

  33. Equal and Not Equal 1 def equal(left, right) 2 [

    3 IdentityStore.new do 4 unify(left, right) 5 end 6 ] 7 end 1 def not_equal(left, right) 2 [ 3 IdentityStore.new do 4 disjoin(left, right) 5 end 6 ] 7 end
  34. AND and OR 1 def all(*stores_lists) 2 prod = Util.product(stores_lists)

    3 prod.map { |combo| combo.unshift(IdentityStore.new).inject(&:combine) } 4 .keep_if(&:valid) 5 end 1 def any(*stores_lists) 2 stores_lists.flatten.keep_if(&:valid) 3 end
  35. How • Embedded in Ruby ◦ Prolog style – rulog

    or ruby-prolog package/gem ◦ miniKanren style – kanren gem • External Solver ◦ ruby-minisat gem for MiniSAT boolean solver • Logic Languages ◦ Prolog ◦ Mercury ◦ Picat
  36. When to avoid • Can’t describe relations / how the

    world works • Too little information to go on Still waiting...
  37. When to use • Inputs: constrained and variable • Generating

    possibilities within a system • Enforcing rules
  38. Next Steps • Get involved in building tooling for Ruby

    logic programming • Try writing an expert system / solver using existing tools
  39. Other Resources • Clojure’s core.logic • miniKanren papers/uncourse by William

    Byrd • The Art of Prolog by Leon Sterling and Ehud Shapiro • Constraint Processing by Rita Dechter • sentient-lang.org
  40. Sources Drawings: Doan Trang – https://www.fiverr.com/doantrang LSAT Example: Official LSAT

    Prep Test – www.lsac.org/docs/default-source/jd-docs/sampleptjune.pdf Sudoku puzzle: Vegard Hanssen Puzzle #5194746 “Super Hard” – www.menneske.no/sudoku Weather modeling image: https://upload.wikimedia.org/wikipedia/commons/c/c0/NOAA_W avewatch_III_Sample_Forecast.gif Musical staff image: https://upload.wikimedia.org/wikipedia/commons/4/4c/Grand_s taff.svg Peter Norvig’s Sudoku Solver: http://norvig.com/sudoku.html Watson image: https://i.ytimg.com/vi/P18EdAKuC1U/maxresdefault.jpg Music exercise generation: Oskar Wickström – https://wickstrom.tech/generative-music/2016/08/07/generating -sight-reading-exercises-using-constraint-logic-programming-in-c lojure-part-1.html