Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

# Fixpoint ```elixir Mix.install([ :fixpoint, :kino ]) Logger.configure(level: :notice) defmodule RenderHTML do use Kino.JS def new(html) when is_binary(html) do Kino.JS.new(__MODULE__, html) end asset "main.js" do """ export function init(ctx, html) { ctx.root.innerHTML = html; } """ end ## Sudoku rendering ## Grid is NxN array def render_sudoku(grid) when is_list(hd(grid)) do header = """ <div id="sudoku"> """ body = Enum.reduce(grid, "", fn row, acc -> acc <> row_to_string(row) end) footer = """ </div> """ ## Render new(header <> body <> footer) end def render_sudoku(array1d) do dim = floor(:math.sqrt(length(array1d))) Enum.chunk_every(array1d, dim) |> render_sudoku() end defp row_to_string(row) do "<div>\n" <> Enum.map_join(row, "\n", fn cell -> sudoku_cell(cell) end) <> "\n</div>" end defp sudoku_cell(value) do """ <input style="background: white; width: 30px; height: 30px; color: blue; border: 2px solid; font-size: 20px; font-weight: bold; text-align: center;" maxlength="1" size="1" value="#{(value == 0 && "") || value}" disabled=""> """ end ## N-Queens rendering def render_nqueens(solution) do (nqueens_header() <> nqueens_board(solution) <> nqueens_footer()) |> new() end defp nqueens_header() do """ <head> <title></title> <meta charset="UTF-8"> <style> .chess-board { border-spacing: 0; border-collapse: collapse; } .chess-board th { padding: .5em; } .chess-board th + th { border-bottom: 1px solid #000; } .chess-board th:first-child, .chess-board td:last-child { border-right: 1px solid #000; } .chess-board tr:last-child td { border-bottom: 1px solid; } .chess-board th:empty { border: none; } .chess-board td { width: 1.5em; height: 1.5em; text-align: center; font-size: 32px; line-height: 0;} .chess-board .light { background: rgb(255, 233, 197); } .chess-board .dark { background: rgb(128, 149, 183); } <!-- 184,139,74 --> </style> </head> <body> <table class="chess-board"> <tbody> </body> """ end defp nqueens_board(solution) do by_rows = solution |> CPSolver.Examples.Queens.inside_out_to_normal() |> Enum.with_index() |> Enum.sort_by(fn {_q, idx} -> idx end) |> Enum.map(fn {q, _idx} -> q end) {board_html, _} = Enum.reduce(by_rows, {"", false}, fn q, {board, even} -> {board <> nqueens_board_row(q, length(solution), even), !even} end) board_html end defp nqueens_board_row(queen_pos, num_rows, even_row?) do white_queen_sym = "♕" black_queen_sym = "♛" light? = !even_row? {row_str, _} = Enum.reduce(1..num_rows, {"", light?}, fn p, {cell, l?} -> {cell <> "<td class=#{(l? && "light") || "dark"}>#{(p == queen_pos && ((l? && white_queen_sym) || black_queen_sym)) || ""}</td>", !l?} end) "<tr>" <> row_str <> "</tr>" end defp nqueens_footer() do """ </tbody> </table> </body> """ end end ``` ## Introduction ***Fixpoint*** is a [Constraint Programming](https://en.wikipedia.org/wiki/Constraint_programming) solver. Constraint programming is widely used for solving hard combinatorial problems, such as planning, scheduling, production and resource optimization, and more. Constraint programming is [***declarative***](https://en.wikipedia.org/wiki/Declarative_programming#:~:text=Declarative%20programming%20is%20a%20non,by%20a%20declarative%20programming%20style.). That is, instead of coding the solution to the problem, we formulate it as a [**constraint satisfaction problem**](https://en.wikipedia.org/wiki/Constraint_satisfaction_problem), abbreviated as **CSP**. This is done by putting together a set of constraints (usually called a "model") that describes the problem and then feeding it to CP solver. The solver then either produces solutions that satisfy the model (called "feasible solutions"), if any; otherwise, the solver reports the problem unsolvable in terms of the model. In addition, CSP could be extended to ***[constraint optimization problem (COP)](https://en.wikipedia.org/wiki/Combinatorial_optimization)***. For the ***COP***, on top of ***CSP*** formulation the model will also specify an optimization objective. The solver will then find the "best" feasible solution with respect to that objective. To make it more concrete, we will solve some combinatorial problems using ***Fixpoint***. ## N-Queens **The N-queens problem is about finding how many different ways queens can be placed on a chessboard so that none attack each other.** <!-- livebook:{"break_markdown":true} --> [N-Queens code](https://github.com/bokner/fixpoint/blob/main/lib/examples/queens.ex) #### Let's find one solution to 8-Queens: ```elixir alias CPSolver.Examples.Queens n = 8 timeout = 180_000 {:ok, res} = CPSolver.solve( Queens.model(n, symmetry: :half_symmetry ), stop_on: {:max_solutions, 1}, timeout: timeout, space_threads: 4 ) if res.status == :unsatisfiable do IO.puts(IO.ANSI.red() <> "There is no solution for n = #{n}!" <> IO.ANSI.reset()) else solution = List.first(res.solutions) if solution do IO.puts( ((Queens.check_solution(solution) && IO.ANSI.green() <> "Solution is correct!") || IO.ANSI.red() <> "Solution is wrong!") <> IO.ANSI.reset() ) IO.puts("Solved in #{div(res.statistics.elapsed_time, 1000)} \u33b3") else IO.puts("No solution was found within #{timeout} \u33b3") end view_limit = 20 if n <= view_limit do RenderHTML.render_nqueens(solution) else IO.puts("Chess board will only be shown for n <= #{view_limit}") end end ``` ## Sudoku https://en.wikipedia.org/wiki/Sudoku <!-- livebook:{"break_markdown":true} --> The instance below is taken from http://www.tellmehowto.net/sudoku/veryhardsudoku.html ```elixir alias CPSolver.Examples.Sudoku puzzles = Sudoku.puzzles() hard_puzzle = puzzles.hard9x9 RenderHTML.render_sudoku(hard_puzzle) ``` ### Let's solve it! ```elixir alias CPSolver.Search.VariableSelector.{MostConstrained, FirstFail} alias CPSolver.Search.VariableSelector, as: Strategy {:ok, res} = CPSolver.solve(Sudoku.model(hard_puzzle) ) IO.puts("Elapsed time: #{div(res.statistics.elapsed_time, 1000)} \u33b3") solution = hd(res.solutions) IO.puts( Sudoku.check_solution(solution) && (IO.ANSI.green() <> "Solution is correct!" || IO.ANSI.red() <> "Solution is wrong!") <> IO.ANSI.reset() ) RenderHTML.render_sudoku(hd(res.solutions)) ``` [Sudoku code](https://github.com/bokner/fixpoint/blob/main/lib/examples/sudoku.ex) ## Reindeer Ordering https://dmcommunity.org/challenge/challenge-dec-2017/ **Santa always leaves plans for his elves to determine the order in which the reindeer will pull his sleigh. This year, for the European leg of his journey, his elves are working to the following schedule, which will form a single line of nine reindeer.** ##### Here are the rules: ``` Comet behind Rudolph, Prancer and Cupid Blitzen behind Cupid Blitzen in front of Donder, Vixen and Dancer Cupid in front of Comet, Blitzen and Vixen Donder behind Vixen, Dasher and Prancer Rudolph behind Prancer Rudolph in front of Donder, Dancer and Dasher Vixen in front of Dancer and Comet Dancer behind Donder, Rudolph and Blitzen Prancer in front of Cupid, Donder and Blitzen Dasher behind Prancer Dasher in front of Vixen, Dancer and Blitzen Donder behind Comet and Cupid Cupid in front of Rudolph and Dancer Vixen behind Rudolph, Prancer and Dasher. ``` Try to solve it by hand first! [Reindeers code](https://github.com/bokner/fixpoint/blob/main/lib/examples/reindeers.ex) <!-- livebook:{"break_markdown":true} --> The rules above are encoded as constraints [here](https://github.com/bokner/fixpoint/blob/main/lib/examples/reindeers.ex#L61-L76). The implementation of `behind/2` and `in_front_of/2` uses a universal `Less` constraint. <!-- livebook:{"force_markdown":true} --> ```elixir domain = 1..length(reindeers) positions = [blitzen, comet, cupid, dancer, dasher, donder, prancer, rudolph, vixen] = Enum.map(reindeers, fn name -> Variable.new(domain, name: name) end) rules = behind(comet, [rudolph, prancer, cupid]) ++ behind(blitzen, [cupid]) ++ in_front_of(blitzen, [donder, vixen, dancer]) ++ in_front_of(cupid, [comet, blitzen, vixen]) ++ behind(donder, [vixen, dasher, prancer]) ++ behind(rudolph, [prancer]) ++ in_front_of(rudolph, [donder, dancer, dasher]) ++ in_front_of(vixen, [dancer, comet]) ++ behind(dancer, [donder, rudolph, blitzen]) ++ in_front_of(prancer, [cupid, donder, blitzen]) ++ behind(dasher, [prancer]) ++ in_front_of(dasher, [vixen, dancer, blitzen]) ++ behind(donder, [comet, cupid]) ++ in_front_of(cupid, [rudolph, dancer]) ++ behind(vixen, [rudolph, prancer, dasher]) Model.new( positions, ## AllDifferent constraint is optional [AllDifferent.new(positions) | rules] ) end defp behind(reindeer, list) do Enum.map(list, fn r -> Less.new(reindeer, r) end) end defp in_front_of(reindeer, list) do Enum.map(list, fn r -> Less.new(r, reindeer) end) end ``` So the encoding of the rules does not require any programming except wiring rules to the constraint implementations. <!-- livebook:{"break_markdown":true} --> ### Let's solve it now: ```elixir alias CPSolver.Examples.Reindeers {:ok, _res} = Reindeers.solve() ``` ## SEND + MORE = MONEY This is a classic "cryptarithmetic" problem. Each letter corresponds to a separate digit, we want to find this correspondence, so SEND + MORE = MONEY holds. ### Solve it! ```elixir alias CPSolver.Examples.SendMoreMoney SendMoreMoney.solve() |> Enum.map_join(", ", fn {letter, digit} -> "#{inspect(letter)} = #{digit}" end) |> then(fn output -> IO.puts(IO.ANSI.cyan() <> output <> IO.ANSI.reset()) end) ``` ##### Indeed: ```text 9 5 6 7 + 1 0 8 5 _________ = 1 0 6 5 2 ``` <!-- livebook:{"break_markdown":true} --> [SEND+MORE=MONEY code](https://github.com/bokner/fixpoint/blob/main/lib/examples/send_more_money.ex) ## Knapsack Enough puzzles, we want to do something practical now. How about packing a knapsack? https://rosettacode.org/wiki/Knapsack_problem/0-1 This is a ***constrained optimization problem (COP)***, as opposed to ***constraint satisfaction problems (CSPs)*** we've seen before. That is, we want to satisfy constraints (in this case, the items we choose have to fit into the knapsack), and we also want a total value of items in the knapsack to be maximized (it's our objective). We will derive a model for this instance from `CPSolver.Examples.Knapsack.model/3`, and then solve it: ```elixir alias CPSolver.Examples.Knapsack ## Instance data (item list) items = [ # {name, weight, value} {:map, 9, 150}, {:compass, 13, 35}, {:water, 153, 200}, {:sandwich, 50, 160}, {:glucose, 15, 60}, {:tin, 68, 45}, {:banana, 27, 60}, {:apple, 39, 40}, {:cheese, 23, 30}, {:beer, 52, 10}, {:suntan_cream, 11, 70}, {:camera, 32, 30}, {:t_shirt, 24, 15}, {:trousers, 48, 10}, {:umbrella, 73, 40}, {:waterproof_trousers, 42, 70}, {:waterproof_overclothes, 43, 75}, {:note_case, 22, 80}, {:sunglasses, 7, 20}, {:towel, 18, 12}, {:socks, 4, 50}, {:book, 30, 10} ] capacity = 400 ########### #### Build data for the model ### We are sorting items by their values. ### The purpose will be explained in a moment. sorted_by_value = Enum.sort_by(items, fn {_name, _weight, value} -> value end, :desc) {item_names, weights, values} = List.foldr(sorted_by_value, {[], [], []}, fn {n, w, v}, {n_acc, w_acc, v_acc} = _acc -> {[n | n_acc], [w | w_acc], [v | v_acc]} end) :ok ``` ***Once we have a model, we're ready to pack!*** Note that for solving, we'll use ***a custom search strategy***. <!-- livebook:{"force_markdown":true} --> ```elixir search_strategy = {:input_order, :indomain_max} ``` `:input_order` forces the solver to choose variables in order they were listed in the model. Recall that we have ordered decision variables for items by their values in descending order. Hence, the solver will choose the items with higher values first. `:indomain_max` will force the solver to choose the maximum value in the domain of the variable. Recall that the domain of decision variable for every item is ```{0, 1}```, where `0` means we leave the item out, and `1` means we pack it. So `:indomain_max` makes the solution process to try to place items first before deciding not to. Overall, our search strategy translates to "pack the items with higher values first". ```elixir model = Knapsack.model(values, weights, capacity) search_strategy = {:input_order, :indomain_max} {:ok, res} = CPSolver.solve(model, space_threads: 8, timeout: 1_000, search: search_strategy ) ## The best (in this case, optimal) solution is the last in the list of feasible solutions optimal_solution = List.last(res.solutions) ## Build the output ## If item variable was resolved to 1, we'll pack it {items_to_pack, total_weight} = List.foldr(Enum.with_index(item_names), {[], 0}, fn {item, idx}, {item_list, total_weight} -> in_the_list = Enum.at(optimal_solution, idx) == 1 { (in_the_list && [item | item_list]) || item_list, (in_the_list && total_weight + Enum.at(weights, idx)) || total_weight } end) formatted_knapsack = Enum.chunk_every(items_to_pack, 4) |> Enum.map_join("\n", fn row -> Enum.map(row, fn item -> " \u2705 #{item}" end) end) IO.puts("Items to pack: \n\n#{IO.ANSI.blue()}#{formatted_knapsack}#{IO.ANSI.reset()}\n") IO.puts("Total value: #{IO.ANSI.red()}#{res.objective}#{IO.ANSI.reset()}") IO.puts("Total weight: #{IO.ANSI.red()}#{total_weight}/#{capacity}#{IO.ANSI.reset()}") IO.puts("Solved in: #{div(res.statistics.elapsed_time, 1000)} \u33b3") ``` ## Benchmarking (local) ```elixir IEx.Helpers.c("scripts/sudoku_benchmark.exs") ``` ```elixir require Logger alias CPSolver.Examples.Sudoku res = SudokuBenchmark.run("data/sudoku/hardest", 100, 12, 30_000) |> Enum.map(fn s -> s.solutions |> List.first() |> tap(fn sol -> (sol && Sudoku.check_solution(sol) && Logger.notice("OK")) || Logger.error("Wrong solution!") end) s.statistics.elapsed_time end) |> Enum.sort() ``` ```elixir {Enum.max(res), Enum.sum(res) / length(res)} # CPSolver.ConstraintStore.default_store() ```
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run on your machine

with Livebook Desktop

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More ×