lib/solver/constraints/propagators/minimum.ex

defmodule CPSolver.Propagator.Minimum do
  use CPSolver.Propagator

  @moduledoc """
  The propagator for Minimum constraint.
  minimum(y, x) constrains y to be a minimum of variables in the list x.
  """
  @spec new(Common.variable_or_view(), [Common.variable_or_view()]) :: Propagator.t()
  def new(min_var, vars) do
    new([min_var | vars])
  end

  @impl true
  def arguments(args) do
    Arrays.new(args, implementation: Aja.Vector)
  end

  @impl true
  def variables(vars) do
    Enum.map(vars, fn var -> set_propagate_on(var, :bound_change) end)
  end

  @impl true
  def filter(vars, state, changes) do
    if state do
      reduce_state(state, vars, changes)
    else
      initial_state(vars)
    end
    |> finalize(vars)
  end

  defp initial_state(vars) do
    array_length = Propagator.arg_size(vars) - 1
    %{
      active_var_indices: MapSet.new(1..array_length)
    }
    ## Initialize reduction by the 'min' variable change
    |> reduce_state(vars, %{0 => :bound_change})
  end

  defp finalize(state, vars) do
    if exists_fixed_to_min(state, vars) do
        :passive
    else
      {:state, state}
    end
  end

  defp exists_fixed_to_min(%{active_var_indices: active_var_indices} = _state, vars) do
    min_var = vars[0]
    if fixed?(min_var) do
      fixed_min = min(min_var)
      Enum.any?(active_var_indices, fn idx ->
        var = vars[idx]
        fixed?(var) && min(var) == fixed_min
      end)
    end
  end

  defp no_support?(_min_var, active_var_indices, _vars) do
    Enum.empty?(active_var_indices)
  end

  defp reduce_state(
    %{
      active_var_indices: active_var_indices,
      } = state, vars,
      _changes) do

    min_var = vars[0]

    min_max = min(min_var)
    max_max = max(min_var)

    {lb, ub, active_var_indices} =
      ## Try to reduce "array" variables

      Enum.reduce(active_var_indices, {nil, nil, active_var_indices}, fn idx, {min_acc, max_acc, active_acc} = _acc ->
        x_var = vars[idx]

        removeBelow(x_var, min_max)
        active_acc = if min(x_var) > max_max do
          ## The domain of the element is disjoint with domain of "max" variable.
          ## Hence we will ignore them
          MapSet.delete(active_acc, idx)
        else
          active_acc
        end

        x_min = min(x_var)
        x_max = max(x_var)

        {
          Kernel.min(min_acc || x_min, x_min),
          Kernel.min(max_acc || x_max, x_max),
          active_acc
        }
      end)

    ## If no "active" array variables (sucn that max(X) >= max(y)),
    ## then there is no support for y => failure
    if no_support?(min_var, active_var_indices, vars) do
      fail()
    end

    ## Reduce 'max' var
    removeAbove(min_var, ub)
    removeBelow(min_var, lb)

    ## Special case: if there is a unique element X
    ## that:
    ## a) has a support for max(min_var)
    ## b) max(X) > max(min_var)
    ##
    ##, then we can reduce it above max(min_var)
    ##
    try_reduce_x(min_var, vars, active_var_indices)

    state
    |> Map.put(:active_var_indices, active_var_indices)
  end

  defp try_reduce_x(min_var, vars, indices) do
    max_min_value = max(min_var)

    case Enum.reduce_while(indices, {false, nil}, fn idx, {found_support?, _supporting_idx} = acc ->
      if contains?(vars[idx], max_min_value) do
        if found_support? do
          ## More than one such element
          {:halt, {false, nil}}
        else
          ## First element
          {:cont, {true, idx}}
        end
      else
        {:cont, acc}
      end
    end) do
      {false, nil} -> false
      {true, supporting_idx} ->
        :no_change != removeAbove(vars[supporting_idx], max_min_value)
    end

  end

  defp fail() do
    throw(:fail)
  end

end