lib/examples/hakank/assignment.ex

#
# Assignment problems in Elixir.
#
  # Different assignments problem, both minimization and maximization.
  # See the sources of the problem below.

  # Compare to the following MiniZinc models, from which these problems
  # are taken:
  # * http://www.hakank.org/minizinc/assignment.mzn
  # * http://www.hakank.org/minizinc/assignment2.mzn
  # * http://www.hakank.org/minizinc/assignment2_2.mzn
  # * http://www.hakank.org/minizinc/assignment3.mzn
  # * http://www.hakank.org/minizinc/assignment5.mzn
  # * http://www.hakank.org/minizinc/assignment6.mzn

# This program was created by Hakan Kjellerstrand, hakank@gmail.com
# See also my Elixir page: http://www.hakank.org/elxir/
#
defmodule CPSolver.Examples.Hakank.Assignment do

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

  import CPSolver.Constraint.Factory
  import CPSolver.Variable.View.Factory

  import Hakank.CPUtils
  #
  # Data from
  # Winston "Operations Research", Assignment Problems, page 393f
  # I added the fifth column
  #
  def problem(1) do
    op = :minimize
    cost = [[14,  5, 8,  7, 15],
            [ 2, 12, 6,  5,  3],
            [ 7,  8, 3,  9,  7],
            [ 2,  4, 6, 10,  1]]
    [op,cost]
  end

  #
  # Winston "Operations Research", page 398, swimming team example
  # (original version]
  # See http://www.hakank.org/minizinc/assignment2.mzn
  #
  def problem(2) do
    op = :minimize
    cost = [[54, 54, 51, 53],
            [51, 57, 52, 52],
            [50, 53, 54, 56],
            [56, 54, 55, 53]]
    [op,cost]
  end

  #
  # Winston "Operations Research", page 398, swimming team example
  # See http://www.hakank.org/minizinc/assignment2_2.mzn
  # expanded version
  #
  def problem(3) do
    op = :minimize
    cost = [[54, 54, 51, 53,   50,60,70,80,90,100],
            [51, 57, 52, 52,   40,50,60,70,80, 90],
            [50, 53, 54, 56,   40,50,60,80,93, 69],
            [56, 54, 55, 53,   60,80,40,60,50,100]]
    [op,cost]
  end


  #
  # Winston "Operations Research", page 399
  #
  # """
  # Tom Cruise, Freddy Prinze Jr, Harrison Ford, and Matt LeBlanc
  # are marooned on a desert island with Jennifer Anniston,
  # Courtney Cos, Gwynneth Paltrow, and Julia Roberts.
  # The 'compatibility matrix' in Table 52 indicate how much happiness
  # each couple would experience if the spend all their time toghether.
  # The happiness earned by a couple is proportional to the fraction
  # of time the spend toghether.
  # ...
  # The optimal solution requires that that each person send all their
  # time with one person of the opposite sex, so this result is often
  # referred to as the Marriage Theorem.
  # """
  #
  # See http://www.hakank.org/minizinc/assignment3.mzn

  # males:
  # 1 "Tom Cruise"
  # 2 "Freddie Prinz Jr"
  # 3 "Harrison Ford"
  # 4 "Mark LeBlanc"
  #
  # females:
  # 1 "Jennifer Anniston"
  # 2 "Courtney Cox"
  # 3 "Gwynneth Paltrow"
  # 4 "Julia Roberts"
  def problem(4) do
    op = :maximize
    cost = [[7, 5, 8, 2],
            [7, 8, 9, 4],
            [3, 5, 7, 9],
            [5, 5, 6, 7]]
    [op,cost]
  end


  # From
  #  "SAS OR 9.1 User's Guide Mathematical Programming"
  # """
  # Consider assigning five programmers to five programming jobs. Each
  # programmer prefers specific programming job over others. [...]
  # Suppose you ask each programmer to rank the jobs according to preference
  # (using 1 for the most preferred job and 5 for the least preffered job].
  # PROC ASSIGN maximizes the total preference of the group by minimizing the
  # sum of the preferences.
  #
  #    PROGRAMMER     JOB1 JOB2 JOB3 JOB4 JOB5
  #    PROGRAMMER1    4    1    3    5    2
  #              2    2    1    3    4    5
  #              3    3    2    4    1    5
  #              4    2    3    4    5    1
  #              5    4    2    3    1    5
  #
  # """
  #
  # See http://www.hakank.org/minizinc/assignment5.mzn
  #
  def problem(5) do
    op = :minimize
    cost = [[4, 1, 3, 5, 2],
            [2, 1, 3, 4, 5],
            [3, 2, 4, 1, 5],
            [2, 3, 4, 5, 1],
            [4, 2, 3, 1, 5]]
    [op,cost]
  end


  #
  # From GLPK:s example assign.mod:
  # """
  # The assignment problem is one of the fundamental combinatorial
  # optimization problems.
  #
  # In its most general form, the problem is as follows:
  #
  # There are a number of agents and a number of tasks. Any agent can be
  # assigned to perform any task, incurring some cost that may vary
  # depending on the agent-task assignment. It is required to perform all
  # tasks by assigning exactly one agent to each task in such a way that
  # the total cost of the assignment is minimized.
  #
  # (From Wikipedia, the free encyclopedia.]
  # """
  #
  # """
  # These data correspond to an example from [Christofides].
  # """
  #
  # See http://www.hakank.org/minizinc/assignment6.mzn
  #
  ## Boris Okner: modified to sync with the latest API,
  ## change naming and result handling.
  ##
  def problem(6) do
    op = :minimize
    cost = [[13, 21, 20, 12,  8, 26, 22, 11],
            [12, 36, 25, 41, 40, 11,  4,  8],
            [35, 32, 13, 36, 26, 21, 13, 37],
            [34, 54,  7,  8, 12, 22, 11, 40],
            [21,  6, 45, 18, 24, 34, 12, 48],
            [42, 19, 39, 15, 14, 16, 28, 46],
            [16, 34, 38,  3, 34, 40, 22, 24],
            [26, 20,  5, 17, 45, 31, 37, 43]]
    [op,cost]
  end



  def run() do

    for p <- 1..6 do
      IO.puts("\nProblem #{p}")
      [op,cost] = problem(p)
      assignment(op, cost)
    end

  end

  def assignment(op, cost) do

    rows = length(cost)
    cols = length(Enum.at(cost,0))

    # Who to assign to what task
    x = for i <- 0..rows-1 do
           for j <- 0..cols-1 do
              IntVariable.new(0..1, name: "x[#{i},#{j}]")
           end
    end

    x_flatten = x |> List.flatten

    total_cost = IntVariable.new(0..Enum.sum(cost |> List.flatten), name: "total_cost")


    # exacly one assignment per row, all rows must be assigned
    row_constraints = for i <- 0..rows-1  do
                        Sum.new(1,for j <- 0..cols-1 do mat_at(x,i,j) end)
    end

    # zero or one assignments per column
    column_constraints = for j <- 0..cols-1  do
                                           {c_var,c_cons} = sum(for i <- 0..rows-1 do mat_at(x,i,j) end)
                                           less_cons = LessOrEqual.new(c_var,1)
                                           [c_cons,less_cons]
    end

    # calculate total_cost
    total_cost_constraint = Sum.new(total_cost, for i <- 0..rows-1, j <- 0..cols-1 do
                                                    mul(mat_at(x,i,j),mat_at(cost,i,j))
                                                end)

    model = Model.new(x_flatten ++ [total_cost],
      (row_constraints ++ column_constraints ++ [total_cost_constraint]) |> List.flatten,
      objective: if op == :minimize do Objective.minimize(total_cost) else Objective.maximize(total_cost) end
    )

    Logger.configure(level: :info)

    opts = [
      search: {:first_fail, :indomain_min},
      #search: {:input_order, :indomain_min},
      # search: {:first_fail, :indomain_random},
      timeout: :infinity,
      # stop_on: {:max_solutions, 2},
      ]
    {:ok, res} = CPSolver.solve(model,
                                     opts
                                     )
    # IO.inspect(res.statistics)
    sol = res.solutions |> List.last

    total_cost_val = Enum.at(sol,rows*cols)
    IO.puts("total_cost: #{total_cost_val}")
    mat = Enum.take(sol,rows*cols)
    print_matrix(mat,rows,cols)

    for i <- 0..rows-1, j <- 0..cols-1, Enum.at(mat,i*cols+j) == 1 do j end
    |> IO.inspect(label: "assigned")


  end

end