Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Minimal Job Shop

notebooks/minimal-job-shop.livemd

Minimal Job Shop

Setup

Mix.install([
  {:exhort, git: "https://github.com/elixir-or-tools/exhort"}
])
use Exhort.SAT.Builder

Data

# task = (machine_id, processing_time).
jobs_data = [
  # Job0
  [{0, 3}, {1, 2}, {2, 2}],
  # Job1
  [{0, 2}, {2, 1}, {1, 4}],
  # Job2
  [{1, 4}, {2, 3}]
]
# Computes horizon dynamically as the sum of all durations.
horizon =
  jobs_data
  |> List.flatten()
  |> Enum.map(&elem(&1, 1))
  |> Enum.sum()

# [
#   {0, [{0, {0, 3}}, {1, {1, 2}}, {2, {2, 2}}]},
#   {1, [{0, {0, 2}}, {1, {2, 1}}, {2, {1, 4}}]},
#   {2, [{0, {1, 4}}, {1, {2, 3}}]}
# ]
jobs =
  jobs_data
  |> Enum.with_index()
  |> Enum.map(fn {job, job_id} ->
    {
      job_id,
      job
      |> Enum.with_index()
      |> Enum.map(fn {{machine_id, processing_time}, task_id} ->
        {task_id, {machine_id, processing_time}}
      end)
    }
  end)

# %{{job_id, task_id} => %{machine: _, start: _, processing_time: _, end: _, interval: _}}
tasks =
  jobs
  |> Enum.map(fn {job_id, job} ->
    job
    |> Enum.map(fn {task_id, {machine_id, processing_time}} ->
      suffix = "#{job_id}_#{task_id}"

      {
        {job_id, task_id},
        %{
          machine: machine_id,
          start: "start_#{suffix}",
          processing_time: processing_time,
          end: "end_#{suffix}",
          interval: "interval_#{suffix}"
        }
      }
    end)
  end)
  |> List.flatten()
  |> Enum.into(%{})

Constraints

machine_constraints =
  tasks
  |> Enum.map(fn {_, %{machine: machine_id, interval: interval_var}} ->
    {machine_id, interval_var}
  end)
  |> Enum.group_by(fn {machine_id, _} -> machine_id end, fn {_, interval_var} ->
    interval_var
  end)
  |> Enum.map(fn {_machine_id, intervals} ->
    Constraint.no_overlap(intervals)
  end)
task_vars =
  tasks
  |> Map.values()
  |> Enum.map(fn
    %{
      start: start_var,
      processing_time: processing_time,
      end: end_var,
      interval: interval_var
    } ->
      [
        IntVar.new(start_var, {0, horizon}),
        IntVar.new(end_var, {0, horizon}),
        IntervalVar.new(interval_var, start_var, processing_time, end_var)
      ]
  end)
  |> List.flatten()
task_constraints =
  jobs
  |> Enum.map(fn {job_id, job} ->
    job
    |> Enum.slice(0, length(job) - 1)
    |> Enum.map(fn {task_id, _task} ->
      task_start = tasks[{job_id, task_id + 1}].start
      task_end = tasks[{job_id, task_id}].end
      Constraint.new(task_start >= task_end)
    end)
  end)
  |> List.flatten()

Solve

builder =
  Builder.new()
  |> Builder.add(task_vars)
  |> Builder.add(machine_constraints)
  |> Builder.add(task_constraints)
  |> Builder.def_int_var("makespan", {0, horizon})
  |> Builder.max_equality(
    "makespan",
    jobs
    |> Enum.map(fn {job_id, job} ->
      tasks[{job_id, length(job) - 1}].end
    end)
  )
  |> Builder.minimize("makespan")

response =
  builder
  |> Builder.build()
  |> Model.solve()
tasks
|> Enum.map(fn {{job_id, task_id}, %{machine: machine_id, start: start_var, end: end_var}} ->
  {
    "Machine: #{machine_id}",
    {
      "#{job_id}_#{task_id}",
      SolverResponse.int_val(response, start_var),
      SolverResponse.int_val(response, end_var)
    }
  }
end)
|> Enum.group_by(fn {machine, _} -> machine end)
|> Enum.map(fn {machine, jobs} ->
  {
    machine,
    jobs
    |> Enum.map(fn {_machine, {id, task_start, task_end}} ->
      {id, {task_start, task_end}}
    end)
    |> Enum.into(%{})
  }
end)
|> Enum.into(%{})