defmodule CPSolver.Search.VariableSelector do
@callback initialize(map(), any()) :: :ok
@callback update(map(), Keyword.t()) :: :ok
@callback select(map(), Keyword.t()) :: Variable.t() | nil
@callback select([Variable.t()], map(), Keyword.t()) :: Variable.t() | nil
alias CPSolver.Search.VariableSelector.{
FirstFail,
InputOrder,
MostConstrained,
MostCompleted,
DomDeg,
MaxRegret,
AFC,
Action,
CHB
}
alias CPSolver.Variable.UnfixedTracker, as: Tracker
defmacro __using__(_) do
quote do
alias CPSolver.Search.VariableSelector
alias CPSolver.Variable.Interface
alias CPSolver.DefaultDomain, as: Domain
alias CPSolver.Variable.UnfixedTracker, as: Tracker
@behaviour VariableSelector
def initialize(_data, _opts) do
:ok
end
def update(_data, _opts) do
:ok
end
def select(data, opts) do
Tracker.iterate(data)
|> then(fn
[] -> VariableSelector.all_vars_fixed_exception()
unfixed_vars -> select(unfixed_vars, data, opts)
end)
end
def select(unfixed_variables, _data, _opts) do
List.first(unfixed_variables)
end
defoverridable initialize: 2, update: 2, select: 2, select: 3
end
end
def initialize(selector, _space_data) do
selector
end
def select_variable(data, variable_choice) when is_atom(variable_choice) do
select_variable(data, strategy(variable_choice))
end
def select_variable(
data,
variable_choice
)
when is_function(variable_choice) do
normalize_choice_fun(variable_choice).(data)
end
def all_vars_fixed_exception() do
throw(:all_vars_fixed)
end
def failed_variables_in_search_exception() do
:failed_variables_in_search
end
defp normalize_choice_fun(variable_choice)
when is_function(variable_choice, 1) do
fn data -> variable_choice.(data) end
end
defp normalize_choice_fun(variable_choice)
when is_function(variable_choice, 2) do
variable_choice
end
######################################
## Variable choice (shortcuts) ##
######################################
defp strategy({afc_mode, decay})
when afc_mode in [:afc_min, :afc_max, :afc_size_min, :afc_size_max] do
afc({afc_mode, decay}, &Enum.random/1)
end
defp strategy({action_mode, decay})
when action_mode in [:action_min, :action_max, :action_size_min, :action_size_max] do
action({action_mode, decay}, &Enum.random/1)
end
defp strategy(chb_mode)
when chb_mode in [:chb_min, :chb_max, :chb_size_min, :chb_size_max] do
chb(chb_mode, &Enum.random/1)
end
defp strategy(:first_fail) do
first_fail(&List.first/1)
end
defp strategy(:input_order) do
input_order()
end
defp strategy(:most_constrained) do
most_constrained(&Enum.random/1)
end
defp strategy(:most_completed) do
most_completed(&Enum.random/1)
end
defp strategy(:dom_deg) do
dom_deg(&Enum.random/1)
end
defp strategy(:max_regret) do
max_regret(&Enum.random/1)
end
defp strategy(impl) when is_atom(impl) do
if Code.ensure_loaded(impl) == {:module, impl} && function_exported?(impl, :select, 2) do
impl
else
throw({:unknown_strategy, impl})
end
end
###################################
## Implementations (top-level) ##
###################################
def most_constrained(break_even_fun \\ &Enum.random/1)
def most_constrained(break_even_fun) when is_function(break_even_fun) do
variable_choice(MostConstrained, break_even_fun)
end
def most_completed(break_even_fun \\ &Enum.random/1)
def most_completed(break_even_fun) do
variable_choice(MostCompleted, extract_strategy(break_even_fun))
end
def max_regret(break_even_fun \\ &Enum.random/1)
def max_regret(break_even_fun) when is_function(break_even_fun) do
variable_choice(MaxRegret, break_even_fun)
end
def first_fail(break_even_fun \\ &Enum.random/1)
def first_fail(break_even_fun) when is_function(break_even_fun) do
variable_choice(FirstFail, break_even_fun)
end
def input_order() do
variable_choice(InputOrder, nil)
end
def dom_deg(break_even_fun \\ &Enum.random/1)
def dom_deg(break_even_fun) when is_function(break_even_fun) do
variable_choice(DomDeg, break_even_fun)
end
def afc({afc_mode, decay}, break_even_fun \\ FirstFail)
when afc_mode in [:afc_min, :afc_max, :afc_size_min, :afc_size_max] do
variable_choice({AFC, mode: afc_mode, decay: decay}, break_even_fun)
end
def action({action_mode, decay}, break_even_fun \\ FirstFail)
when action_mode in [:action_min, :action_max, :action_size_min, :action_size_max] do
variable_choice({Action, mode: action_mode, decay: decay}, break_even_fun)
end
def chb(chb_mode, break_even_fun \\ FirstFail)
def chb({chb_mode, q_score}, break_even_fun)
when chb_mode in [:chb_min, :chb_max, :chb_size_min, :chb_size_max] do
variable_choice(
{CHB, mode: chb_mode, q_score: q_score},
break_even_fun
)
end
def chb(chb_mode, break_even_fun) do
chb({chb_mode, CHB.default_q_score()}, break_even_fun)
end
defp extract_strategy(shortcut) when is_atom(shortcut) do
strategy(shortcut)
end
defp extract_strategy(strategy) when is_function(strategy) do
strategy
end
def mixed(strategies) do
Enum.random(strategies)
|> extract_strategy()
end
defp execute_break_even(selection, _data, nil) do
selection
end
defp execute_break_even(selection, _data, break_even_fun) when is_function(break_even_fun, 1) do
break_even_fun.(selection)
end
defp execute_break_even(selection, data, break_even_fun) when is_function(break_even_fun, 2) do
break_even_fun.(selection, data)
end
defp variable_choice(strategy_fun, break_even_fun) when is_function(strategy_fun) do
fn data ->
data
|> strategy_fun.()
|> execute_break_even(data, break_even_fun)
end
end
defp variable_choice({strategy_impl, args}, break_even_fun) when is_atom(strategy_impl) do
impl = strategy(strategy_impl)
initialize? = function_exported?(impl, :initialize, 2)
strategy_fun = fn data ->
args = args || []
initialize? && impl.initialize(data, args)
impl.select(data, args)
|> tap(fn selection -> Enum.empty?(List.wrap(selection)) && all_vars_fixed_exception() end)
end
variable_choice(strategy_fun, break_even_fun)
end
defp variable_choice(strategy_impl, break_even_fun) when is_atom(strategy_impl) do
variable_choice({strategy_impl, nil}, break_even_fun)
end
end