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(scheduled_propagators, constraint_graph, _store) when map_size(scheduled_propagators) == 0 do constraint_graph end defp run_impl(propagators, constraint_graph, store) do case propagate(propagators, constraint_graph, store) do :fail -> :fail {scheduled_propagators, 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_map(propagators) do propagators |> Map.values() |> reorder() |> propagate(graph, store) end def propagate(propagators, graph, store) when is_list(propagators) do propagators |> Task.async_stream(fn p -> {p.id, Propagator.filter(p, store: store)} end) |> Enum.reduce_while({Map.new(), graph}, fn {:ok, {p_id, res}}, {scheduled, g} = acc -> case res do {:fail, _var} -> {: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 |> Map.merge(scheduled_by_propagator) |> unschedule(p_id) end defp schedule_by_propagator(domain_changes, graph) do {updated_graph, scheduled_propagators} = domain_changes |> Enum.reduce({graph, %{}}, fn {var_id, domain_change}, {g, propagators} -> {maybe_remove_variable(g, var_id, domain_change), Map.merge( propagators, Map.new( ConstraintGraph.get_propagators(g, var_id, domain_change), fn p -> {p.id, p} end ) )} 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{} = residue, propagators) do (Graph.vertices(residue) == [] && :solved) || {:stable, residue, propagators_from_graph(residue, propagators)} end defp propagators_from_graph(graph, propagators) do Enum.flat_map(propagators, fn p -> (p && ConstraintGraph.get_propagator(graph, p.id) && [p]) || [] 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 Map.delete(scheduled_propagators, p_id) end ## TODO: possible reordering strategy of propagators ## for the next pass defp reorder(propagators) do propagators end end