Packages
fixpoint
0.21.3
0.22.1
0.21.5
0.21.4
0.21.3
0.21.2
0.21.1
0.21.0
0.20.6
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.18.2
0.18.1
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.9
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.13.5
0.13.4
0.13.2
0.13.1
0.12.9
0.12.8
0.12.7
0.12.6
0.12.5
0.12.4
0.12.2
0.12.1
0.11.8
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.46
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.28
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.1.3
0.1.2
0.1.1
0.1.0
Constraint Programming Solver
Current section
Files
Jump to
Current section
Files
lib/examples/bin_packing/bin_packing.ex
defmodule CPSolver.Examples.BinPacking do
@moduledoc """
Bin Packing Problem Example
Given:
- n: items, each with weights w[i]
- b: max. bin capacity
The goal is to assign each item to a bin such that:
Sum of item weights in each bin <= capacity and the
number of bins used is minimized.
"""
alias CPSolver.IntVariable, as: Variable
alias CPSolver.Model
alias CPSolver.Constraint.Sum
alias CPSolver.Constraint.{LessOrEqual, Channel}
import CPSolver.Variable.View.Factory
alias CPSolver.Objective
alias CPSolver.Examples.BinPacking.{Search, UpperBound}
require Logger
def solve(weights, capacity, opts \\ []) do
upper_bound =
Keyword.get(opts, :upper_bound, UpperBound.first_fit_decreasing(weights, capacity))
model = model(weights, capacity, upper_bound)
opts =
Keyword.merge(
[
search: model.search,
solution_handler: solution_handler(),
timeout: :timer.seconds(30),
upper_bound: upper_bound
],
opts
)
Logger.warning("Started")
Logger.warning("Upper bound: #{opts[:upper_bound]}")
CPSolver.solve(model, opts)
|> tap(fn {:ok, res} ->
if res.status not in [:unknown, :unsatisfiable] do
(check_solution(res, weights, capacity) && Logger.warning("Solution is valid")) ||
throw({:error, :invalid_solution})
else
Logger.error("No solution found")
end
end)
end
def minimization_model(item_weights, capacity, upper_bound \\ nil) do
item_weights = Enum.sort(item_weights, :desc)
min_weight = List.last(item_weights)
num_items = length(item_weights)
num_items_over_half_capacity = Enum.count(item_weights, fn w -> 2 * w > capacity end)
num_bins = upper_bound || num_items
lower_bound = max(num_items_over_half_capacity, ceil(Enum.sum(item_weights) / capacity))
# x[i][j] item i assigned to bin j
indicators =
for i <- 1..num_items do
item_over_half_capacity? = i <= num_items_over_half_capacity
for j <- 1..num_bins do
domain =
if item_over_half_capacity? do
(i == j && 1) || 0
else
0..1
end
Variable.new(domain, name: "item_#{i}_in_bin_#{j}")
end
end
total_bins_used =
Variable.new(lower_bound..num_bins,
name: "total_bins_used"
)
# indicators for bin: bin[i] is used iff bin_used[i] = 1
bin_used =
for i <- 1..num_bins do
## First `lower_bound` bins are to be used
##
d = (i <= lower_bound && 1) || 0..1
Variable.new(d, name: "bin_#{i}_used")
end
# total weight in bin i
bin_load =
for i <- 1..num_bins do
lb_cap = (i <= lower_bound && min_weight) || 0
Variable.new(lb_cap..capacity, name: "bin_load_#{i}")
end
###################
### Constraints ###
###################
upper_bound_constraint = LessOrEqual.new(total_bins_used, num_bins)
# item_indicator_constraints =
# Enum.map(indicators, fn inds ->
# Sum.new(1, inds)
# end)
x = Enum.map(1..num_items, fn idx -> Variable.new(1..num_bins, name: "x_#{idx}") end)
item_assignment_constraints =
for i <- 0..(num_items - 1) do
x_i = Enum.at(x, i)
bin_indicators = Enum.at(indicators, i)
## x[i] = j iff ind[i, j] = 1
Channel.new(x_i, bin_indicators)
end
bin_load_constraints =
Enum.with_index(bin_load)
|> Enum.map(fn {load_var, j} ->
views =
Enum.with_index(indicators)
|> Enum.map(fn {inds_for_item, i} ->
mul(Enum.at(inds_for_item, j), Enum.at(item_weights, i))
end)
Sum.new(load_var, views)
end)
capacity_constraints =
Enum.zip(bin_load, bin_used)
|> Enum.map(fn {load, used} ->
LessOrEqual.new(load, mul(used, capacity))
end)
total_bins_constraint =
Sum.new(total_bins_used, bin_used)
bin_load_sum_constraint = Sum.new(Enum.sum(item_weights), bin_load)
#####################################
### end of constraint definitions ###
#####################################
constraints =
[
upper_bound_constraint,
item_assignment_constraints,
# item_indicator_constraints,
bin_load_constraints,
capacity_constraints,
symmetry_breaking_constraints(bin_used, bin_load, num_bins, num_items_over_half_capacity),
total_bins_constraint,
bin_load_sum_constraint
]
vars =
[bin_used, total_bins_used, bin_load, indicators] |> List.flatten()
Model.new(
vars,
constraints,
objective: Objective.minimize(total_bins_used)
)
|> Map.put(:search,
## if lower_bound = upper_bound, we do not need an advanced strategy
## TODO: maybe even run linear BPP (`first_fit_decreasing` etc)
if lower_bound == num_bins do
{:first_fail, :indomain_max}
else
Search.cdbf(item_weights, x, bin_load, capacity)
end)
end
defp symmetry_breaking_constraints(bin_used, _bin_load, num_bins, _num_over_half_capacity) do
# Symmetry breaking
# 1. Only allow bin j to be used if all bins < j are used.
# This prevents the solver from seeing equivalent packings as different solutions.
used_bins_first =
Enum.map(0..(num_bins - 2), fn bin_idx ->
bin = Enum.at(bin_used, bin_idx)
next_bin = Enum.at(bin_used, bin_idx + 1)
LessOrEqual.new(next_bin, bin)
end)
## TODO: not being used, as it has varying performance effect across instances
# 2. Arrange bin loads in decreasing order
## (only for bins that do not have "over half capacity" items)
# decreasing_loads =
# if num_over_half_capacity + 1 >= num_bins - 1 do
# []
# else
# for i <- (num_over_half_capacity + 1)..num_bins - 1 do
# LessOrEqual.new(Enum.at(bin_load, i), Enum.at(bin_load, i - 1))
# end
# end
[
used_bins_first,
# decreasing_loads
]
end
def check_solution(
%{solutions: solutions, variables: variable_names} = _result,
item_weights,
capacity
) do
item_weights = Enum.sort(item_weights, :desc)
Enum.all?(solutions, fn sol ->
check_solution(sol, item_weights, capacity, variable_names)
end)
end
def check_solution(solution, item_weights, capacity, variable_names) do
item_assignments =
Enum.zip(solution, variable_names)
|> Enum.flat_map(fn {value, variable_name} ->
if is_binary(variable_name) && String.starts_with?(to_string(variable_name), "x_") do
value
end
|> List.wrap()
end)
%{loads: bin_loads, bin_contents: bin_contents} =
solution_to_bin_content(item_assignments, item_weights)
## Loads do no exceed max capacity
true = Enum.all?(bin_loads, fn l -> l <= capacity end)
## All items are placed into bins
all_item_indices =
Enum.reduce(tl(bin_contents), hd(bin_contents) |> MapSet.new(), fn bin_items, acc ->
MapSet.union(acc, MapSet.new(bin_items))
end)
true = all_item_indices == MapSet.new(1..length(item_weights))
## The total sum of bin weights equals the total sum of item weights
true = Enum.sum(item_weights) == Enum.sum(bin_loads)
end
def solution_to_bin_content(item_assignments, item_weights) do
bin_contents =
Enum.group_by(Enum.with_index(item_assignments, 1), fn {val, _} -> val end, fn {_, idx} ->
idx
end)
loads =
Map.new(bin_contents, fn {bin, content} ->
{
bin,
Enum.sum_by(content, fn item_id -> Enum.at(item_weights, item_id - 1) end)
}
end)
%{loads: Map.values(loads), bin_contents: Map.values(bin_contents)}
end
def model(item_weights, capacity, upper_bound, type \\ :minimize)
def model(item_weights, capacity, upper_bound, :minimize),
do: minimization_model(item_weights, capacity, upper_bound)
def solution_handler() do
fn solution ->
objective =
solution
|> Enum.find(fn {var_name, _value} ->
var_name == "total_bins_used"
end)
|> elem(1)
Logger.warning("total bins: #{objective}")
end
end
end