defmodule CPSolver.Propagator.ConstraintGraph do
@moduledoc """
The constraint graph connects propagators and their variables.
The edge between a propagator and a variable represents a notification
the propagator receives upon variable's domain change.
"""
alias CPSolver.Propagator
alias CPSolver.Variable.Interface
alias CPSolver.Utils.Digraph
require Logger
@spec create([Propagator.t()]) :: Graph.t()
def create(propagators) when is_list(propagators) do
Enum.reduce(propagators, Graph.new(), fn p, graph_acc ->
add_propagator(graph_acc, p)
end)
end
def copy(%Graph{} = graph) do
graph
end
def copy(graph) when elem(graph, 0) == :digraph do
#:digraph_utils.subgraph(graph, :digraph.vertices(graph))
Digraph.copy(graph)
end
def get_propagators(constraint_graph) do
constraint_graph
|> vertices()
## Get %{id => propagator} map
|> Enum.flat_map(fn
{:propagator, p_id} ->
[get_propagator(constraint_graph, p_id)]
_ ->
[]
end)
end
## Get a list of propagator ids for variable id
def get_propagator_ids(constraint_graph, variable_id) do
edges(constraint_graph, variable_vertex(variable_id))
|> Enum.flat_map(fn
%{v2: {:propagator, p_id}} = _edge ->
[p_id]
_ ->
[]
end)
end
## Get a list of propagator ids that "listen" to the domain change of given variable.
def get_propagator_ids(
constraint_graph,
variable_id,
domain_change
)
when is_atom(domain_change) do
propagators_by_variable(constraint_graph, variable_id, fn p_id, propagator_variable_edge ->
domain_change in propagator_variable_edge.propagate_on &&
get_propagator_data(
propagator_variable_edge,
domain_change,
get_propagator(constraint_graph, p_id)
)
end)
end
def vertices(%Graph{} = constraint_graph) do
Graph.vertices(constraint_graph)
end
def vertices(constraint_graph) when elem(constraint_graph, 0) == :digraph do
Digraph.vertices(constraint_graph)
end
def edges(%Graph{} = graph) do
Graph.edges(graph)
end
def edges(graph) when elem(graph, 0) == :digraph do
Digraph.edges(graph)
end
def edges(%Graph{} = constraint_graph, vertex) do
Graph.edges(constraint_graph, vertex)
end
def edges(constraint_graph, vertex) when elem(constraint_graph, 0) == :digraph do
Digraph.edges(constraint_graph, vertex)
end
def add_vertex(%Graph{} = graph, vertex, label) do
Graph.add_vertex(graph, vertex, label)
end
def add_vertex(graph, vertex, label) when elem(graph, 0) == :digraph do
Digraph.add_vertex(graph, vertex, label)
end
def add_edge(%Graph{} = graph, from, to, label) do
Graph.add_edge(graph, from, to, label: label)
end
def add_edge(graph, from, to, label) when elem(graph, 0) == :digraph do
Digraph.add_edge(graph, from, to, label)
end
defp propagators_by_variable(constraint_graph, variable_id, reduce_fun)
when is_function(reduce_fun, 2) do
constraint_graph
|> edges(variable_vertex(variable_id))
|> Enum.reduce(Map.new(), fn edge, acc ->
{:propagator, p_id} = edge.v2
((p_data = reduce_fun.(p_id, edge.label)) && p_data && Map.put(acc, p_id, p_data)) ||
acc
end)
end
defp get_propagator_data(_edge, domain_change, propagator) do
%{
domain_change: domain_change,
propagator: propagator
}
end
def add_variable(graph, variable) do
add_vertex(graph, variable_vertex(variable.id), [variable])
end
def add_propagator(graph, propagator) do
add_propagator_impl(graph, propagator)
end
defp add_propagator_impl(graph, propagator) do
propagator_vertex = propagator_vertex(propagator.id)
propagator
|> Propagator.variables()
|> Enum.reduce(graph, fn var, graph_acc ->
interface_var = Interface.variable(var)
graph_acc
|> add_variable(interface_var)
|> add_vertex(propagator_vertex, propagator)
|> then(fn graph ->
(Interface.fixed?(interface_var) && graph) ||
add_edge(graph, variable_vertex(interface_var.id), propagator_vertex,
%{
propagate_on: get_propagate_on(var)
}
)
end)
end)
end
def get_propagator(graph, {:propagator, _propagator_id} = vertex) do
get_label(graph, vertex)
|> tap(fn ps ->
is_list(ps) &&
Logger.error(
inspect(
{"Multiple propagators", vertex, Enum.map(ps, fn p -> Map.take(p, [:id, :mod]) end)}
)
)
end)
end
def get_propagator(graph, propagator_id) do
get_propagator(graph, propagator_vertex(propagator_id))
end
def update_propagator(
%Graph{vertex_labels: labels, vertex_identifier: identifier} = graph,
propagator_id,
propagator
) do
vertex = propagator_vertex(propagator_id)
if propagator.id != elem(vertex, 1) do
Logger.error("""
Mismatch between propagator id and CG vertex
Propagator id: #{propagator.id}"
Propagator vertex: #{inspect(vertex)}
""")
graph
else
graph
|> Map.put(:vertex_labels, Map.put(labels, identifier.(vertex), propagator))
end
end
def update_propagator(
graph,
propagator_id,
propagator
)
when elem(graph, 0) == :digraph do
vertex = propagator_vertex(propagator_id)
Digraph.add_vertex(graph, vertex, propagator)
end
def variable_vertex(variable_id) do
{:variable, variable_id}
end
def propagator_vertex(propagator_id) do
{:propagator, propagator_id}
end
def variable_degree(graph, variable_id) do
out_degree(graph, variable_vertex(variable_id))
end
def propagator_degree(graph, propagator_id) do
in_degree(graph, propagator_vertex(propagator_id))
end
def remove_propagator(graph, propagator_id) do
remove_vertex(graph, propagator_vertex(propagator_id))
end
def entailed_propagator?(graph, propagator) do
Enum.empty?(in_neighbors(graph, propagator_vertex(propagator.id)))
end
def get_variable(graph, {:variable, _variable_id} = vertex) do
get_label(graph, vertex)
end
def get_variable(graph, variable_id) do
get_variable(graph, variable_vertex(variable_id))
end
## Remove variable
def remove_variable(graph, variable_id) do
remove_vertex(graph, variable_vertex(variable_id))
end
def disconnect_variable(graph, variable_id) do
delete_edges(graph, edges(graph, variable_vertex(variable_id)))
end
### This is called on creation of new space.
###
### Stop notifications from fixed variables and update propagators with variable domains.
### Returns updated graph and a list of propagators bound to variable domains
def update(graph, vars) do
Enum.reduce(vars, graph, fn v, graph_acc ->
update_variable(graph_acc, v)
end)
end
def remove_vertex(graph, vertex) do
delete_vertex(graph, vertex)
end
defp get_propagate_on(variable) do
Map.get(variable, :propagate_on) || Propagator.to_domain_events(:fixed)
end
defp get_label(%Graph{} = graph, vertex) do
case Graph.vertex_labels(graph, vertex) do
[] -> nil
[p] -> p
p -> p
end
end
defp get_label(graph, vertex) when elem(graph, 0) == :digraph do
{_vertex, label} = Digraph.vertex(graph, vertex)
label
end
defp in_degree(%Graph{} = graph, vertex) do
Graph.in_degree(graph, vertex)
end
defp in_degree(graph, vertex) when elem(graph, 0) == :digraph do
Digraph.in_degree(graph, vertex)
end
defp out_degree(%Graph{} = graph, vertex) do
Graph.out_degree(graph, vertex)
end
defp out_degree(graph, vertex) when elem(graph, 0) == :digraph do
Digraph.out_degree(graph, vertex)
end
defp in_neighbors(%Graph{} = graph, vertex) do
Graph.in_neighbors(graph, vertex)
end
defp in_neighbors(graph, vertex) when elem(graph, 0) == :digraph do
Digraph.in_neighbours(graph, vertex)
end
def update_variable(
%Graph{vertex_labels: labels, vertex_identifier: identifier} = graph,
variable
) do
vertex = variable_vertex(Interface.id(variable))
Map.put(graph, :vertex_labels, Map.put(labels, identifier.(vertex), variable))
#graph
end
def update_variable(
graph,
variable
)
when elem(graph, 0) == :digraph do
vertex = variable_vertex(Interface.id(variable))
Digraph.add_vertex(graph, vertex, variable)
end
defp delete_vertex(%Graph{} = graph, vertex) do
Graph.delete_vertex(graph, vertex)
end
defp delete_vertex(graph, vertex) when elem(graph, 0) == :digraph do
Digraph.delete_vertex(graph, vertex)
end
defp delete_edges(%Graph{} = graph, edges) do
Graph.delete_edges(graph, edges)
end
defp delete_edges(graph, edges) when elem(graph, 0) == :digraph do
Digraph.delete_edges(graph, edges)
end
end