Current section

Files

Jump to
fixpoint lib examples bin_packing search.ex
Raw

lib/examples/bin_packing/search.ex

defmodule CPSolver.Examples.BinPacking.Search do
alias CPSolver.Variable.Interface
alias CPSolver.Search.Partition
@doc """
Complete decreasing best fit branching,
roughly as per https://www.gecode.dev/doc-latest/MPG.pdf, chapter 20
"""
def cdbf(item_weights, item_assignment_vars, bin_load_vars, capacity) do
## Create a list [{item_assignment_index, item_weight}]
## (will be used for matching the item assignment variables with items' weights)
##
## Note: item weights are sorted in decreasing order
item_assignment_map =
Enum.zip(item_assignment_vars, item_weights)
|> Map.new(fn {var, weight} -> {var.name, weight} end)
fn
:init, _, _ ->
:ok
:branch, variables, _data ->
case get_item_variables(variables, item_assignment_map) do
nil ->
[]
item_variables ->
selected_variable = List.first(item_variables)
{bins, slack, num_loads} =
value_branching(selected_variable, bin_load_vars, item_assignment_map, capacity)
partitions(
item_variables,
bins,
slack,
num_loads,
bin_load_vars,
item_assignment_map,
capacity
)
end
end
end
## Get the (unfixed) variables with the largest item weight.
defp get_item_variables(variables, item_assignment_map) do
## We rely on:
## - item weights sorted in descending order
## - the item assignment variables are adjacent to each other within the variable list;
## that is, all of them are located in the single block.
Enum.reduce_while(variables, {nil, nil}, fn v, {last_item_weight, item_vars_acc} = acc ->
## Is variable an 'item assignment' variable?
item_weight = Map.get(item_assignment_map, v.name)
cond do
is_nil(item_vars_acc) ->
(item_weight && {:cont, {item_weight, [v]}}) || {:cont, {nil, nil}}
true ->
if item_weight do
## Found another item assignment var
## The weight is different?
## We are only interested in the vars with the identical weights
if last_item_weight == item_weight do
{:cont, {item_weight, [v | item_vars_acc]}}
else
{:halt, acc}
end
else
## The end of item assignment variables' block
## (these variables have to be adjacent in the list of all variables)
{:halt, acc}
end
end
end)
|> then(fn {_, res} -> if res, do: Enum.reverse(res) end)
end
defp value_branching(var, bin_load_vars, item_assignment_map, capacity) do
## The variable is quaranteed to be unfixed `item assignment`,
## (see `choose_variable_fun`)
item_weight = get_item_weight(var, item_assignment_map)
## Find bin with minimal slack
## TODO: advanced branching, as described by Gecode docs ("two alternatives" case)
##
{bins, bin_slack, num_loads} =
Enum.reduce_while(Enum.with_index(bin_load_vars, 1), {[], nil, 0}, fn
{load_var, bin_idx}, {min_bins, min_slack, load_count} = slack_acc ->
cond do
Interface.contains?(var, bin_idx) ->
slack = slack(item_weight, capacity, load_var)
cond do
Interface.fixed?(load_var) ->
## The bin load has already been fixed,
## so the item has to be there (no choice).
## TODO: this is the case where further branching doesn't make sense.
## The related issue: https://github.com/bokner/fixpoint/issues/96
{:halt, {[bin_idx], nil, nil}}
slack == 0 ->
## Perfect fit
{:halt, {[bin_idx], 0, nil}}
slack < 0 ->
## No fit
{:cont, slack_acc}
slack < min_slack ->
## Better fit
{:cont, {[bin_idx], slack, load_count + 1}}
true ->
## Keep current min, add bin to the list of current min bins
{:cont, {[bin_idx | min_bins], min_slack, load_count + 1}}
end
true ->
{:cont, slack_acc}
end
end)
{bins, bin_slack, num_loads}
end
defp partitions(
[selected_variable | other_item_variables] = _item_variables,
bins,
slack,
num_loads,
bin_load_vars,
item_assignment_map,
capacity
) do
bin = List.first(bins)
cond do
is_nil(bin) || num_loads == 0 ->
throw(:fail)
slack in [0, nil] ->
## Perfect fit or the bin being fixed.
## We only need a single partition.
##
[
Partition.fixed_value_partition(selected_variable, bin)
]
true ->
## As suggested by Gecode docs for 2-alternative branching
## (https://www.gecode.dev/doc-latest/MPG.pdf, chapter 20):
###
### – Not only prune bin b from the potential bins for item i,
## but also prune all bins with the same slack as b
## from the potential bins for all items with the same size as i
##
## At this point, all item variables have the same size as the first item variable;
## For each item variable, we will iterate over bin load vars to
## identify the ones with the same slack as computed for the first variable.
##
prune_partition =
Enum.reduce(
other_item_variables,
Partition.removed_value_partition(selected_variable, bin),
fn variable, acc ->
w = get_item_weight(variable, item_assignment_map)
{_idx, acc} =
Enum.reduce(bin_load_vars, {1, acc}, fn load_var, {bin, acc2} ->
{bin + 1,
cond do
!Interface.contains?(variable, bin) ->
acc2
Interface.fixed?(load_var) ->
Map.put(acc2, variable.id, fn variable ->
Interface.fix(variable, bin)
end)
slack(w, capacity, load_var) == slack ->
Map.put(acc2, variable.id, fn variable ->
Interface.remove(variable, bin)
end)
true ->
acc2
end}
end)
acc
end
)
[
Partition.fixed_value_partition(selected_variable, bin),
prune_partition
]
end
# |> List.wrap()
end
defp slack(item_weight, capacity, load_variable) do
capacity - Interface.min(load_variable) - item_weight
end
defp get_item_weight(variable, item_assignment_map) do
Map.get(item_assignment_map, variable.name)
end
end