lib/solver/search/strategy/variable/shared/action.ex

defmodule CPSolver.Search.VariableSelector.Action do
  @moduledoc """
  Action (activity-based) variable selector
  (https://www.gecode.org/doc-latest/MPG.pdf, p.8.5.3)
  """
  use CPSolver.Search.VariableSelector
  alias CPSolver.Space
  alias CPSolver.Shared
  alias CPSolver.Variable.Interface
  alias CPSolver.Utils

  @default_action_value 1

  @impl true
  def select(variables, data, opts) do
    select_impl(variables, data, opts[:mode])
    |> Enum.map(fn {var, _action} -> var end)
  end

  defp select_impl(variables, data, :action_min) do
    Utils.minimals(
      variable_actions(variables, Space.get_shared(data)),
      fn {_var, action} -> action end
    )
  end

  defp select_impl(variables, data, :action_max) do
    Utils.maximals(
      variable_actions(variables, Space.get_shared(data)),
      fn {_var, action} -> action end
    )
  end

  defp select_impl(variables, data, :action_size_min) do
    Utils.minimals(
      variable_actions(variables, Space.get_shared(data)),
      fn {var, action} -> action / Interface.size(var) end
    )
  end

  defp select_impl(variables, data, :action_size_max) do
    Utils.maximals(
      variable_actions(variables, Space.get_shared(data)),
      fn {var, action} ->
        action / Interface.size(var)
      end
    )
  end

  @doc """
  Initialize Action data
  """
  @impl true
  def initialize(%{variables: variables} = space_data, opts) do
    shared = Space.get_shared(space_data)

    Shared.get_auxillary(shared, :action) ||
      (
        action_table = Shared.create_shared_ets_table(shared)
        init_variable_actions(variables, action_table)
        Shared.put_auxillary(shared, :action, %{variable_actions: action_table, decay: opts[:decay]})
        Shared.add_handler(shared, :on_space_finalized,
        fn solver,  %{variables: variables} = _space_data, _reason ->
          Shared.complete?(solver) ||
          update_actions(variables, solver)
        end)
      )
  end

  defp init_variable_actions(variables, action_table) do
    Enum.each(variables, fn var ->
      :ets.insert(action_table, {Interface.id(var), @default_action_value})
    end)
  end

  @doc """
  Compute actions of variables in one pass
  """
  def variable_actions(variables, shared) do
    action_data = Shared.get_auxillary(shared, :action)

    if action_data do
      %{variable_actions: action_table} = action_data

      actions =
        :ets.select(
          action_table,
          for(var <- variables, do: {{Interface.id(var), :_}, [], [:"$_"]})
        )
        |> Map.new()

      Enum.map(variables, fn var ->
        var_id = Interface.id(var)
        {var, Map.get(actions, var_id, @default_action_value)}
      end)
    else
      Enum.map(variables, fn var -> {var, @default_action_value} end)
    end
  end

  def update_actions(variables, shared) do
    case Shared.get_auxillary(shared, :action) do
      nil -> :ok
      %{variable_actions: action_table, decay: decay} ->
        Enum.each(variables, fn var -> update_variable_action(var, action_table, decay) end)
      end
  end

  ## Update action for individual variable in 'shared'
  defp update_variable_action(
         %{id: variable_id, initial_size: initial_size} = variable,
         action_table,
         decay
       ) do
    updated_action =
      case get_action(action_table, variable_id) do
        nil ->
          @default_action_value

        current_action ->
          ## Some variables may fail
          current_size =
            try do
              Interface.size(variable)
            catch
              :fail ->
                0
            end

          (initial_size > current_size && current_action + 1) || current_action * decay
      end

    :ets.insert(action_table, {variable_id, updated_action})
  end

  defp get_action(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)) ||
        @default_action_value
    end)
  end
end