Packages

Bin packing constraint solver + cost-optimizer.

Current section

Files

Jump to
bin_packer lib bin_packer.ex
Raw

lib/bin_packer.ex

defmodule BinPacker do
alias BinPacker.Assignments
alias BinPacker.Ball
alias BinPacker.Bin
alias BinPacker.Diff
alias BinPacker.MoveProposition
alias BinPacker.SwapProposition
alias BinPacker.Neighbourhood
alias BinPacker.MoveNeighbourhood
alias BinPacker.SwapNeighbourhood
alias BinPacker.Constraint
alias BinPacker.DummyConstraint
alias BinPacker.Objective
@default_neighbourhoods %{
MoveNeighbourhood => %{max_bins: 100, max_balls: 1000},
SwapNeighbourhood => %{max_swaps: 100}
}
@default_constraints [{DummyConstraint, []}]
defstruct objectives: Map.new(),
neighbourhoods: Map.new(),
constraints: [],
assignments: %Assignments{}
@type id :: any
@type bin_id :: id
@type ball_id :: id
@type ball_ids :: [ball_id]
@type bin_ids :: [bin_id]
@type cost :: number
@type state :: any
@type args :: any
@type ma :: {module, args}
@type weight :: number
@type objective_opts :: %{required(ma) => weight}
@type objectives :: %{required({ma, state}) => weight}
@type neighbourhood_opts :: %{module => args}
@type neighbourhoods :: %{required(ma) => state}
@type constraint_opts :: [ma]
@type constraints :: [{ma, state}]
@type proposition :: MoveProposition.t() | SwapProposition.t()
@type propositions :: [propositions]
@type proposition_type :: MoveProposition | SwapProposition
@type proposition_types :: [proposition_types]
@type search_opts :: [{:neighbourhoods, neighbourhood_opts} | {:constraints, constraint_opts}]
@type t ::
%__MODULE__{
objectives: Objectives.t(),
neighbourhoods: neighbourhoods,
constraints: constraints,
assignments: Assignments.t()
}
@spec new([Bin.t()], [Ball.t()], objective_opts, search_opts) :: t
def new(bins, balls, objectives, opts \\ [])
when is_list(bins) and
is_list(balls) and
is_map(objectives) and
is_list(opts) do
neighbourhoods = Keyword.get(opts, :neighbourhoods, @default_neighbourhoods)
constraints =
opts
|> Keyword.get(:constraints, @default_constraints)
|> case do
[] ->
@default_constraints
constraints ->
constraints
end
%__MODULE__{}
|> Neighbourhood.initialize(neighbourhoods)
|> initialize_constraints(constraints)
|> Objective.initialize(objectives)
|> add_bins(bins)
|> add_balls(balls)
end
defp initialize_constraints(%__MODULE__{} = bin_packer, constraints) do
constraints =
Enum.map(constraints, fn {module, args} = constraint ->
{constraint, module.init(args)}
end)
%__MODULE__{bin_packer | constraints: constraints}
end
#
# TODO: proper error
#
@spec add_bin(t, Bin.t()) :: t
def add_bin(%__MODULE__{} = bin_packer, bin) do
if Assignments.has_bin?(bin_packer, bin) do
raise "refusing to add bin #{inspect bin}', already present"
end
bin_packer
|> Assignments.put_bin(bin)
|> Objective.bin_added(bin)
|> Constraint.bin_added(bin)
end
@spec add_bins(t, [Bin.t()]) :: t
def add_bins(%__MODULE__{} = bin_packer, bins) when is_list(bins) do
Enum.reduce(bins, bin_packer, &add_bin(&2, &1))
end
#
# TODO
# - proper errors
#
@spec add_ball(t, Ball.t()) :: t
def add_ball(%__MODULE__{constraints: []} = bin_packer, ball) do
bin_ids = Assignments.bin_ids(bin_packer)
place_ball_in_lowest_cost_bin(bin_packer, ball, bin_ids)
end
def add_ball(%__MODULE__{constraints: [{{first_constraint, _init_args}, constraint_state} | other_constraints]} = bin_packer, ball) do
if Assignments.has_ball?(bin_packer, ball) do
raise "refusing to add ball '#{inspect ball}', already present"
end
bin_packer
|> first_constraint.propose_new_placements(ball, constraint_state)
|> Enum.filter(&Constraint.valid_new_placement?(other_constraints, bin_packer, ball, &1))
|> case do
[] ->
raise "unable to add ball '#{inspect ball}', unable to find a bin that satisfies all constraints"
bin_ids ->
place_ball_in_lowest_cost_bin(bin_packer, ball, bin_ids)
end
end
@spec add_balls(t, [Ball.t()]) :: t
def add_balls(%__MODULE__{} = bin_packer, balls) when is_list(balls) do
Enum.reduce(balls, bin_packer, &add_ball(&2, &1))
end
defdelegate diff(bin_packer, other_bin_packer), to: Diff
defdelegate to_map(bin_packer), to: Assignments
defdelegate cost(bin_packer), to: Objective
defdelegate search(bin_packer, opts \\ []), to: Neighbourhood
defp place_ball(bin_packer, bin_id, ball) do
bin_packer
|> Assignments.put_ball(bin_id, ball)
|> Objective.ball_added(ball)
|> Constraint.ball_added(ball)
end
defp place_ball_in_lowest_cost_bin(bin_packer, ball, [bin_id]) do
place_ball(bin_packer, bin_id, ball)
end
defp place_ball_in_lowest_cost_bin(bin_packer, ball, [bin_id | bin_ids]) do
lowest_cost_bin_packer = place_ball(bin_packer, bin_id, ball)
lowest_cost = cost(lowest_cost_bin_packer)
bin_ids
|> Enum.reduce({lowest_cost, lowest_cost_bin_packer}, fn bin_id, {lowest_cost, _lowest_bin_packer} = acc ->
bin_packer = place_ball(bin_packer, bin_id, ball)
cost = cost(bin_packer)
if cost < lowest_cost do
{cost, bin_packer}
else
acc
end
end)
|> fn {_cost, bin_packer} -> bin_packer end.()
end
end