lib/examples/hakank/quasigroup_completion.ex

#
# Quasigroup Completion in Elixir.
#
# See
# Carla P. Gomes and David Shmoys:
# "Completing Quasigroups or Latin Squares: Structured Graph Coloring Problem"
#
# See also
# Ivars Peterson "Completing Latin Squares"
# http://www.maa.org/mathland/mathtrek_5_8_00.html
# """
# Using only the numbers 1, 2, 3, and 4, arrange four sets of these
# numbers into a four-by-four array so that no column or row contains
# the same two numbers. The result is known as a Latin square.
# ...
# The so-called quasigroup completion problem concerns a table that is
# correctly but only partially filled in. The question is whether the
# remaining blanks in the table can be filled in to obtain a complete
# Latin square (or a proper quasigroup multiplication table).
# """
#
#
# This program was created by Hakan Kjellerstrand, hakank@gmail.com
# See also my Elixir page: http://www.hakank.org/elxir/
#
## Boris Okner: modified to sync with the latest API,
## change then naming and the result handling.
##

defmodule CPSolver.Examples.Hakank.QuasigroupCompletion do
  import Hakank.CPUtils

  alias CPSolver.IntVariable
  # alias CPSolver.Constraint.Sum
  # alias CPSolver.Constraint.Equal
  # alias CPSolver.Constraint.AllDifferent.FWC, as: AllDifferent
  alias CPSolver.Model

  #
  # Example from Ruben Martins and Inès Lynce
  # Breaking Local Symmetries in Quasigroup Completion Problems, page 3
  # The solution is unique:
  # 1 3 2 5 4
  # 2 5 4 1 3
  # 4 1 3 2 5
  # 5 4 1 3 2
  # 3 2 5 4 1
  #
  def puzzle(1) do
    [[1, 0, 0, 0, 4],   # 0 are the unknowns
     [0, 5, 0, 0, 0],
     [4, 0, 0, 2, 0],
     [0, 4, 0, 0, 0],
     [0, 0, 5, 0, 1]]
  end

  #
  # Example from Gomes & Shmoys, page 3.
  # Solution:
  # 4 1 2 3
  # 2 3 4 1
  # 1 4 3 2
  # 3 2 1 4
  #
  def puzzle(2) do
    [[0, 1, 2, 3],
     [2, 0, 4, 1],
     [1, 4, 0, 2],
     [3, 0, 1, 0]]
  end

  # Example from Gomes & Shmoys, page 7
  # Two solutions.
  #
  def puzzle(3) do
    [[0, 1, 0, 0],
     [0, 0, 2, 0],
     [0, 3, 0, 0],
     [0, 0, 0, 4]]
  end


  #
  # Example from Global Constraint Catalogue
  # http://www.emn.fr/x-info/sdemasse/gccat/sec2.7.108.html
  #
  # 12 solutions.
  #
  def puzzle(4) do
    [[1, 0, 0, 0],
     [0, 0, 0, 3],
     [3, 0, 0, 0],
     [0, 0, 0, 1]]
  end


  #
  # Problem from http://www.cs.cornell.edu/gomes/QUASIdemo.html
  # (n = 10]
  # Pattern #1.
  # There are _many_ solutions to this problem.
  #
  def puzzle(5) do
    [
      [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
      [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
      [0, 1, 0, 0, 0, 2, 0, 0, 0, 0],
      [1, 0, 0, 0, 2, 0, 0, 0, 0, 0],
      [0, 0, 0, 2, 1, 0, 0, 0, 0, 0],
      [0, 0, 2, 0, 0, 1, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 1, 0, 2],
      [0, 0, 0, 0, 0, 0, 0, 0, 2, 0],
      [0, 0, 0, 0, 0, 0, 0, 2, 0, 0]
    ]
  end

  #
  # Problem from http://www.cs.cornell.edu/gomes/QUASIdemo.html
  # (n = 10]
  # Pattern #2.
  # There are many solutions to this problem.
  #
  def puzzle(6) do
    [
      [0, 0, 1, 2, 3, 4, 0, 0, 0, 0],
      [0, 1, 2, 3, 0, 0, 4, 0, 0, 0],
      [1, 2, 3, 0, 0, 0, 0, 4, 0, 0],
      [2, 3, 0, 0, 0, 0, 0, 0, 4, 0],
      [3, 0, 0, 0, 0, 0, 0, 0, 0, 4],
      [5, 6, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 5, 6, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 5, 6, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 5, 6, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 5, 6, 0, 0, 0, 0]
    ]
  end

  #
  # Problem from http://www.cs.cornell.edu/gomes/QUASIdemo.html
  # (n = 10]
  # Pattern #3.
  # Coding:
  #    dark red   = 1
  #    light blue = 2
  #    dark blue  = 3
  #    light red  = 4
  #    brown      = 5
  #    green      = 6
  #    pink       = 7
  #    grey       = 8
  #    black      = 9
  #    yellow     = 10
  # There are 40944 solutions for this pattern.
  #
  # This takes 9.5s, about to solve and print all solutions.
  #
  def puzzle(7) do
    [
      [0, 0, 1, 5, 2, 6, 7, 8, 0, 0],
      [0, 1, 5, 2, 0, 0, 6, 7, 8, 0],
      [1, 5, 2, 0, 0, 0, 0, 6, 7, 8],
      [5, 2, 0, 0, 0, 0, 0, 0, 6, 7],
      [2, 0, 0, 0, 0, 0, 0, 0, 0, 6],
      [4, 10, 0, 0, 0, 0, 0, 0, 3, 9],
      [0, 4, 10, 0, 0, 0, 0, 3, 9, 0],
      [0, 0, 4, 10, 0, 0, 3, 9, 0, 0],
      [0, 0, 0, 4, 10, 3, 9, 0, 0, 0],
      [0, 0, 0, 0, 4, 9, 0, 0, 0, 0]
    ]
  end

  #
  # Problem from http://www.cs.cornell.edu/gomes/QUASIdemo.html
  # (n = 10]
  # Pattern #4.
  #  dark red   = 1
  #  light blue = 2
  #  dark blue  = 3
  #  light red  = 4
  # Note: There are no solutions to this problem.
  #
  def puzzle(8) do
    [
      [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [2, 1, 0, 0, 0, 0, 0, 0, 0, 4],
      [3, 2, 1, 0, 0, 0, 0, 0, 4, 0],
      [0, 3, 2, 1, 0, 0, 0, 4, 0, 0],
      [0, 0, 3, 2, 1, 0, 4, 0, 0, 0],
      [0, 0, 0, 3, 2, 1, 0, 0, 0, 0],
      [0, 0, 0, 0, 3, 2, 1, 0, 0, 0],
      [0, 0, 0, 4, 0, 3, 2, 1, 0, 0],
      [0, 0, 4, 0, 0, 0, 3, 2, 1, 0],
      [0, 4, 0, 0, 0, 0, 0, 3, 2, 1]
    ]
  end

  #
  # Problem from http://www.cs.cornell.edu/gomes/QUASIdemo.html
  # (n = 10]
  # Pattern #5
  # Note: There are no solutions to this problem.
  #
  def puzzle(9) do
    [
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
      [0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
      [0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
      [0, 0, 0, 0, 0, 0, 2, 0, 0, 0],
      [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
      [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
      [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
      [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
      [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
      [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ]
  end

  #
  #
  #
  def run(opts \\ []) do
    # Problem 5..7 yields a huge number of solutions.
    # Let's just pick the first.
    1..7
    |> Enum.map(fn p ->
      IO.puts("Running problem #{p}")

      cond do
        p in [5..7] -> solve_puzzle(p, Keyword.put(opts, :num_solutions, 1))
        true -> solve_puzzle(p, opts)
      end
    end)
  end

  def solve_puzzle(id, opts \\ []) do
    quasigroup_completion(puzzle(id), opts)
  end

  #
  # Running problems 8 and 9 (no solution)
  # Takes long time in current Fixpoint version
  ##
  ## Boris Okner: if using AllDifferent.DC.Fast for `latin_square`,
  ## these puzzles will instantly fail due to early propagation.
  def run_unsatisfiable(opts \\ []) do
    Enum.each([8, 9], fn puzzle_id ->
      try do
        solve_puzzle(puzzle_id, opts)
      catch
        {:fail, _} ->
          IO.puts("Puzzle #{puzzle_id} doesn't have solutions")
      end
    end)
  end

  @doc """
  quasigroup_completion(mat,num_sols \\ :infinity)

  Solves the Quasigroup completion problem for the matrix `mat`.
  `num_sols` are the required number of solutions, defaults to :infinity.

  """
  def quasigroup_completion(mat, opts \\ []) do
    n = length(mat)
    dom = 1..n

    #
    # Decision variables
    #

    x =
      for i <- 0..(n - 1) do
        for j <- 0..(n - 1) do
          v = mat_at(mat, i, j)

          if v > 0 do
            # > 0: this is a hint
            IntVariable.new(v, name: "x[#{i},#{j}]")
          else
            # 0: unknown
            IntVariable.new(dom, name: "x[#{i},#{j}]")
          end
        end
      end

    x_flatten = List.flatten(x)

    #
    # Constraints
    #
    constraints = latin_square(x)

    model =
      Model.new(
        x_flatten,
        constraints
      )

    Logger.configure(level: :info)

    opts =
      Keyword.merge(default_opts(), opts)
      |> then(fn opts ->
        Keyword.put(opts, :stop_on, {:max_solutions, opts[:num_solutions]})
      end)

    {:ok, result} =
      CPSolver.solve(
        model,
        opts
      )

    ## Print last solution
    print_matrix(result.solutions |> List.last(), n, n, "~3w")
    ##
    IO.inspect(result.statistics)

    {:ok, result}
  end

  defp default_opts() do
    [
      search: {:first_fail, :indomain_max},
      # search: {:first_fail, :indomain_min},
      # search: {:first_fail, :indomain_random},
      # search: {:input_order, :indomain_max},
      num_solutions: :infinity,
      timeout: :timer.minutes(5)
    ]
  end
end