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