lib/examples/hakank/all_intervals.ex

#
# All interval problem in Elixir.
#
# CSPLib problem number 7
# http://www.cs.st-andrews.ac.uk/~ianm/CSPLib/prob/prob007/index.html
# """
# Given the twelve standard pitch-classes (c, c , d, ...), represented by
# numbers 0,1,...,11, find a series in which each pitch-class occurs exactly
# once and in which the musical intervals between neighbouring notes cover
# the full set of intervals from the minor second (1 semitone) to the major
# seventh (11 semitones). That is, for each of the intervals, there is a
# pair of neigbhouring pitch-classes in the series, between which this
# interval appears. The problem of finding such a series can be easily
# formulated as an instance of a more general arithmetic problem on Z_n,
# the set of integer residues modulo n. Given n in N, find a vector
# s = (s_1, ..., s_n), such that (i) s is a permutation of
# Z_n = {0,1,...,n-1}; and (ii) the interval vector
# v = (|s_2-s_1|, |s_3-s_2|, ... |s_n-s_{n-1}|) is a permutation of
# Z_n-{0} = {1,2,...,n-1}. A vector v satisfying these conditions is
# called an all-interval series of size n; the problem of finding such
# a series is the all-interval series problem of size n. We may also be
# interested in finding all possible series of a given size.
# """
#
# Note: This version is based on a model created by Boris Okner.
#
# Result for n=8:
# x: [4,1,8,2,7,3,5,6] diffs: [3,7,6,5,4,2,1]
# x: [5,4,2,6,3,8,1,7] diffs: [1,2,4,3,5,7,6]
# x: [2,8,1,6,5,3,7,4] diffs: [6,7,5,1,2,4,3]
# x: [5,2,8,1,6,4,3,7] diffs: [3,6,7,5,2,1,4]
# x: [3,4,6,2,7,1,8,5] diffs: [1,2,4,5,6,7,3]
# x: [2,8,1,6,3,7,5,4] diffs: [6,7,5,3,4,2,1]
# x: [5,4,2,8,1,6,3,7] diffs: [1,2,6,7,5,3,4]
# x: [4,3,8,1,7,5,2,6] diffs: [1,5,7,6,2,3,4]
# x: [5,2,6,4,3,8,1,7] diffs: [3,4,2,1,5,7,6]
# x: [2,7,1,8,4,5,3,6] diffs: [5,6,7,4,1,2,3]
# x: [3,7,2,8,1,4,6,5] diffs: [4,5,6,7,3,2,1]
# x: [4,5,3,6,2,7,1,8] diffs: [1,2,3,4,5,6,7]
# x: [3,8,1,7,4,2,6,5] diffs: [5,7,6,3,2,4,1]
# x: [4,3,7,5,2,8,1,6] diffs: [1,4,2,3,6,7,5]
# x: [4,3,5,8,1,7,2,6] diffs: [1,2,3,7,6,5,4]
#
#
# Comparison with Picat and MiniZinc/Gecode (in seconds) for all solutions:
#
#  n   Fixpoint  Picat    Gecode
# -----------------------------
#  8     0.5s    0.005s   0.003s
#  9     2.4s    0.024s   0.008s
# 10    13.7s    0.048s   0.034s
# 11    79.3s    0.297s   0.184s
# 12   469.3s    1.584s   0.922s
# 13  2982.2s    9.462s   5.178s
#
#
# 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 naming and result handling.
##
defmodule CPSolver.Examples.Hakank.AllInterval do

  alias CPSolver.IntVariable
  alias CPSolver.Constraint.AllDifferent.FWC, as: AllDifferent
  alias CPSolver.Constraint.LessOrEqual
  alias CPSolver.Constraint.Absolute
  alias CPSolver.Model

  import CPSolver.Constraint.Factory

  def run(n) do

    x =
      for i <- 0..(n - 1) do
        IntVariable.new(1..n, name: "x[#{i}]")
      end

    diffs =
      for i <- 0..(n - 2) do
        IntVariable.new(1..(n - 1), name: "diffs[#{i}]")
      end

    constraints =
      for k <- 0..(n - 2) do
        {difference_var, difference_constraint} = subtract(Enum.at(x, k + 1), Enum.at(x, k))
         [Absolute.new(difference_var, Enum.at(diffs, k)), ## |difference_var| = diffs[k]
          difference_constraint]
      end
      |> List.flatten()

    model =
      Model.new(
        x ++ diffs,
        constraints ++
          [
            AllDifferent.new(x),
            AllDifferent.new(diffs),
            # symmetry breaking
            LessOrEqual.new(Enum.at(x, 0), Enum.at(x, n - 1)),
            # symmetry breaking
            LessOrEqual.new(Enum.at(diffs, 0), Enum.at(diffs, 1))
          ]
      )

    Logger.configure(level: :info)

    opts = [
      # search: {:first_fail, :indomain_min},
      #search: {:first_fail, :indomain_max},
      search: {:input_order, :indomain_min},
      #search: {:first_fail, :indomain_min},
      # search: {:input_order, :indomain_random},
      space_threads: 8,
      timeout: :timer.hours(1),
      # stop_on: {:max_solutions, 2},
    ]

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

    res.solutions
    |> Enum.map(fn sol ->
      x_vals = Enum.slice(sol, 0, n)
      diffs_vals = Enum.slice(sol, n, n-1)
      :io.format("x: ~w diffs: ~w~n", [x_vals, diffs_vals])
    end)
    IO.inspect(res.statistics)
  end
end