Current section

Files

Jump to
fixpoint lib solver space propagation.ex
Raw

lib/solver/space/propagation.ex

defmodule CPSolver.Space.Propagation do
alias CPSolver.Propagator.ConstraintGraph
alias CPSolver.Propagator
def run(propagators, store \\ nil)
def run(propagators, store) do
run(propagators, ConstraintGraph.create(propagators), store)
end
def run(propagators, constraint_graph, store) when is_list(propagators) do
propagators
|> run_impl(constraint_graph, store)
|> finalize(propagators)
end
defp run_impl(propagators, constraint_graph, store) do
case propagate(propagators, constraint_graph, store) do
:fail ->
:fail
{scheduled_propagators, reduced_graph} ->
(MapSet.size(scheduled_propagators) == 0 && reduced_graph) ||
run_impl(scheduled_propagators, reduced_graph, store)
end
end
@spec propagate(map(), Graph.t(), map()) ::
:fail | {map(), Graph.t()} | {:changes, map()}
@doc """
One pass of propagation.
Produces the list (up to implementation) of propagators scheduled for the next pass.
Side effect: modifies the constraint graph.
The graph will be modified on every individual Propagator.filter/1, if the latter results in any domain changes.
"""
def propagate(propagators, graph, store) when is_list(propagators) do
propagators
|> Map.new(fn p -> {p.id, p} end)
|> propagate(graph, store)
end
def propagate(%MapSet{} = propagator_ids, graph, store) do
Map.new(propagator_ids, fn p_id -> {p_id, ConstraintGraph.get_propagator(graph, p_id)} end)
|> propagate(graph, store)
end
def propagate(propagators, graph, store) when is_map(propagators) do
propagators
|> reorder()
|> Task.async_stream(fn {p_id, p} ->
{p_id, Propagator.filter(p, store: store)}
end)
|> Enum.reduce_while({MapSet.new(), graph}, fn {:ok, {p_id, res}}, {scheduled, g} = acc ->
case res do
{:fail, _var} ->
{:halt, :fail}
:fail ->
{:halt, :fail}
:stable ->
{:cont, acc}
{:changed, changes} ->
{updated_graph, scheduled_by_propagator} = schedule_by_propagator(changes, g)
{:cont,
{
reschedule(scheduled, p_id, scheduled_by_propagator),
updated_graph
}}
end
end)
end
## Note: we do not reschedule a propagator that was the source of domain changes,
## as we assume idempotence (that is, running a propagator for the second time wouldn't change domains).
## We will probably introduce the option to be used in propagator implementations
## to signify that the propagator is not idempotent.
##
defp reschedule(current_schedule, p_id, scheduled_by_propagator) do
current_schedule
|> MapSet.union(scheduled_by_propagator)
|> unschedule(p_id)
end
## Returns set of propagator ids scheduled as a result of domain changes.
defp schedule_by_propagator(domain_changes, graph) do
{updated_graph, scheduled_propagators} =
domain_changes
|> Enum.reduce({graph, MapSet.new()}, fn {var_id, domain_change}, {g, propagators} ->
{maybe_remove_variable(g, var_id, domain_change),
MapSet.union(
propagators,
MapSet.new(ConstraintGraph.get_propagators(g, var_id, domain_change))
)}
end)
{updated_graph, scheduled_propagators}
end
defp finalize(:fail, _propagators) do
:fail
end
## At this point, the space is either solved or stable.
defp finalize(%Graph{} = residual_graph, propagators) do
(Graph.edges(residual_graph) == [] && :solved) ||
(
{g, updated_propagators} = remove_entailed_propagators(residual_graph, propagators)
{:stable, g, updated_propagators}
)
end
defp remove_entailed_propagators(graph, propagators) do
Enum.reduce(propagators, {graph, []}, fn p, {g, p_list} = _acc ->
p_vertex = ConstraintGraph.propagator_vertex(p.id)
case Graph.neighbors(g, p_vertex) do
[] -> {ConstraintGraph.remove_propagator(g, p.id), p_list}
_connected_vars -> {g, [p | p_list]}
end
end)
end
defp maybe_remove_variable(graph, var_id, :fixed) do
ConstraintGraph.remove_variable(graph, var_id)
end
defp maybe_remove_variable(graph, _var_id, _domain_change) do
graph
end
defp unschedule(scheduled_propagators, p_id) do
MapSet.delete(scheduled_propagators, p_id)
end
## TODO: possible reordering strategy
## for the next pass.
## Ideas: put to-be-entailed propagators first,
## so if they fail, it'd be early.
## In general, arrange by the number of fixed variables?
##
defp reorder(propagators) do
propagators
end
end