defmodule CPSolver.Propagator.Element2D do
use CPSolver.Propagator
import CPSolver.Utils
@moduledoc """
The propagator for Element2D constraint.
array2d[row_index][col_index] = value
"""
def new(array2d, row_index, col_index, value) do
new([array2d, row_index, col_index, value])
end
@impl true
def variables([_array2d, row_index, col_index, value]) do
[
set_propagate_on(row_index, :domain_change),
set_propagate_on(col_index, :domain_change),
set_propagate_on(value, :domain_change)
]
end
defp initial_state([[], _row_index, _col_index, _value]) do
throw(:fail)
end
defp initial_state([array2d, row_index, col_index, value]) do
num_rows = length(array2d)
num_cols = length(hd(array2d))
initial_reduction(array2d, row_index, col_index, value, num_rows, num_cols)
build_state(array2d, row_index, col_index, value, num_rows, num_cols)
end
def build_state(array2d, row_index, col_index, value, num_rows, num_cols) do
## Build a state graph.
## Three sets of vertices: ([{:value, value}], [{:row_index, value}], [{:col_index, value}])
## with edges from {:value, z_value} to {:row_index, x_value},
## where z_value is present in x_value row of array2d.
## Likewise, with edges from {:value, z_value} to {:col_index, y_value},
## where z_value is present in y_value column of array2d.
for i <- 0..(num_rows - 1), j <- 0..(num_cols - 1), reduce: Graph.new() do
acc ->
if contains?(row_index, i) && contains?(col_index, j) do
table_value = Enum.at(array2d, i) |> Enum.at(j)
if contains?(value, table_value) do
acc
|> Graph.add_edge({:value, table_value}, {:row_index, i}, label: {:col_index, j})
|> Graph.add_edge({:value, table_value}, {:col_index, j}, label: {:row_index, i})
else
acc
end
else
acc
end
end
end
defp initial_reduction(array2d, row_index, col_index, value, num_rows, num_cols) do
# x and y are indices in array2d,
# so we trim D(x) and D(y) accordingly.
removeBelow(row_index, 0)
removeAbove(row_index, num_rows - 1)
removeBelow(col_index, 0)
removeAbove(col_index, num_cols - 1)
## D(value) is bounded by min and max of the array2d
{arr_min, arr_max} = array2d_min_max(array2d)
removeAbove(value, arr_max)
removeBelow(value, arr_min)
end
defp maybe_reduce_domains(row_index, col_index, value, %Graph{} = graph) do
(maybe_fix(row_index, col_index, value, graph) && :passive) ||
(
{updated_graph, changed?} =
Enum.reduce(Graph.vertices(graph), {graph, false}, fn
{:value, _} = v, acc ->
maybe_remove_vertex(v, value, acc)
{:row_index, _} = v, acc ->
maybe_remove_vertex(v, row_index, acc)
{:col_index, _} = v, acc ->
maybe_remove_vertex(v, col_index, acc)
end)
## Repeat if any reductions were made
if changed? do
maybe_reduce_domains(row_index, col_index, value, updated_graph)
else
updated_graph
end
)
end
defp maybe_remove_vertex(
{_signature, value} = vertex,
variable,
{graph, _changed?} = acc,
removal_condition \\ fn graph, vertex -> Graph.degree(graph, vertex) == 0 end
) do
cond do
!contains?(variable, value) ->
{remove_vertex(graph, vertex), true}
removal_condition.(graph, vertex) ->
remove(variable, value)
{remove_vertex(graph, vertex), true}
true ->
acc
end
end
defp remove_vertex(graph, {:value, _value} = vertex) do
Graph.delete_vertex(graph, vertex)
end
defp remove_vertex(graph, {signature, _value} = vertex)
when signature in [:row_index, :col_index] do
graph
|> Graph.delete_vertex(vertex)
## We delete all edges related to this vertex (that is, labelled {:signature, value})
|> then(fn graph ->
graph
|> Graph.edges()
|> Enum.reduce(
graph,
fn edge, acc ->
(edge.label == vertex && Graph.delete_edge(acc, edge.v1, edge.v2, edge.label)) ||
acc
end
)
end)
end
@impl true
def filter(args, state, changes) do
filter_impl(args, (state && state) || initial_state(args), changes)
end
def filter_impl(args, state, _changes) do
case filter_impl(args, state) do
:passive ->
:passive
updated_state ->
if Graph.vertices(updated_state) |> Enum.empty?() do
:fail
else
{:state, updated_state}
end
end
end
defp filter_impl([_array2d, row_index, col_index, value], state) do
maybe_reduce_domains(row_index, col_index, value, state)
end
defp maybe_fix(row_index, col_index, value, graph) do
## If any 2 are fixed, fix the 3rd
case Graph.vertices(graph) do
[_vertex1, _vertex2, _vertex3] = triple ->
Enum.each(
triple,
fn
{:row_index, x_value} -> fix(row_index, x_value)
{:col_index, y_value} -> fix(col_index, y_value)
{:value, z_value} -> fix(value, z_value)
end
)
true
_more_than_one_triple ->
false
end
end
end