defmodule CPSolver.Search.VariableSelector.CHB do @moduledoc """ Conflict-history based variable selector (https://www.gecode.org/doc-latest/MPG.pdf, p.8.5.4) """ use CPSolver.Search.VariableSelector alias CPSolver.Space alias CPSolver.Shared alias CPSolver.Variable.Interface alias CPSolver.Utils @default_q_score 0.05 @impl true def select(variables, data, opts) do select_impl(variables, data, opts[:mode]) |> Enum.map(fn {var, _chb} -> var end) end defp select_impl(variables, data, :chb_min) do Utils.minimals( variable_chbs(variables, Space.get_shared(data)), fn {_var, %{q_score: q_score} = _chb} -> q_score end ) end defp select_impl(variables, data, :chb_max) do Utils.maximals( variable_chbs(variables, Space.get_shared(data)), fn {_var, %{q_score: q_score} = _chb} -> q_score end ) end defp select_impl(variables, data, :chb_size_min) do Utils.minimals( variable_chbs(variables, Space.get_shared(data)), fn {var, %{q_score: q_score} = _chb} -> q_score / Interface.size(var) end ) end defp select_impl(variables, data, :chb_size_max) do Utils.maximals( variable_chbs(variables, Space.get_shared(data)), fn {var, %{q_score: q_score} = _chb} -> q_score / Interface.size(var) end ) end def default_q_score() do @default_q_score end @doc """ Initialize CHB data """ @impl true def initialize(%{variables: variables} = space_data, opts) do shared = Space.get_shared(space_data) Shared.get_auxillary(shared, :chb) || ( chb_table = Shared.create_shared_ets_table(shared) init_variable_chbs(variables, chb_table, opts[:q_score] || @default_q_score) Shared.put_auxillary(shared, :chb, %{variable_chbs: chb_table}) Shared.add_handler(shared, :on_space_finalized, fn solver, %{variables: variables} = _space_data, reason -> Shared.complete?(solver) || update_chbs(variables, reason == :failure, solver) end) ) end defp init_variable_chbs(variables, chb_table, q_score) do Enum.each(variables, fn var -> :ets.insert(chb_table, {Interface.id(var), chb_record(q_score, 0)}) end) end defp chb_record(q_score, last_failure) do %{q_score: q_score, last_failure: last_failure} end @doc """ Compute chbs of variables in one pass """ def variable_chbs(variables, shared) do chb_data = Shared.get_auxillary(shared, :chb) if chb_data do %{variable_chbs: chb_table} = chb_data chbs = :ets.select( chb_table, for(var <- variables, do: {{Interface.id(var), :_}, [], [:"$_"]}) ) |> Map.new() Enum.map(variables, fn var -> var_id = Interface.id(var) {var, Map.get(chbs, var_id, chb_record(@default_q_score, 0))} end) else Enum.map(variables, fn var -> {var, chb_record(@default_q_score, 0)} end) end end def update_chbs(variables, failure?, shared) do case Shared.get_auxillary(shared, :chb) do nil -> :ok %{variable_chbs: chb_table} -> Enum.reduce_while(variables, :ok, fn var, _acc -> (Shared.complete?(shared) && {:halt, :ok}) || ( update_variable_chb(var, chb_table, failure?, shared) {:cont, :ok} ) end) end end ## Update chb for individual variable in 'shared' defp update_variable_chb(%{id: variable_id} = variable, chb_table, failure?, shared) do case Shared.get_failure_count(shared) do global_failure_count when is_integer(global_failure_count) -> cond do pruned?(variable) -> chb = get_chb(chb_table, variable_id) updated_chb = %{chb | q_score: q_score(chb, failure?, global_failure_count)} |> then(fn rec -> (failure? && %{rec | last_failure: max(chb.last_failure, global_failure_count)}) || rec end) :ets.insert(chb_table, {variable_id, updated_chb}) true -> :ignore end _ -> :ok end end defp pruned?(%{initial_size: initial_size} = variable) do current_size = try do Interface.size(variable) catch :fail -> 0 end initial_size > current_size end defp q_score( %{q_score: current_qs, last_failure: last_failure} = _current_chb_record, failure?, global_failure_count ) do alpha = step_size(global_failure_count) reward = ((failure? && 1) || 0.9) / (max(global_failure_count - last_failure, 0) + 1) (1 - alpha) * current_qs + alpha * reward end def step_size(failure_count) do max(0.06, 0.4 - failure_count * 1.0e-6) end defp get_chb(table, variable_id) when is_reference(table) and is_reference(variable_id) do table |> :ets.lookup(variable_id) |> then(fn rec -> (!Enum.empty?(rec) && elem(hd(rec), 1)) || chb_record(@default_q_score, 0) end) end end