Current section

Files

Jump to
fixpoint test variables value_graph_test.exs
Raw

test/variables/value_graph_test.exs

defmodule CPSolverTest.Utils.ValueGraph do
use ExUnit.Case
describe "Value Graph" do
alias CPSolver.ValueGraph
alias CPSolver.IntVariable, as: Variable
alias CPSolver.Variable.Interface
import CPSolver.Test.Helpers
test "build" do
num_variables = 4
domain = 1..5
variables = Enum.map(1..num_variables, fn idx -> Variable.new(domain, name: "x#{idx}") end)
%{value_graph: graph, left_partition: _left_partition} = ValueGraph.build(variables)
assert ValueGraph.get_variable_count(graph) == num_variables
## 4 variables and 5 values
assert BitGraph.num_vertices(graph) == 9
## No adjacency table allocation
refute get_in(graph, [:adjacency, :bit_vector])
## Ignore fixed variables
variables = Enum.map([1, 1..2, [1, 2, 4, 5], 6], fn d -> Variable.new(d) end)
%{value_graph: graph, left_partition: left_partition} = ValueGraph.build(variables,
ignore_fixed_variables: true)
assert 1 in left_partition
assert 2 in left_partition
# fixed variables are excluded
refute 0 in left_partition
refute 3 in left_partition
# Value graph does not have fixed variables and fixed values not shared with other variables
refute Enum.any?([0, 3, {:value, 1}, {:value, 6}],
fn vertex ->
BitGraph.get_vertex(graph, vertex)
end)
end
test "default neighbor finder" do
num_variables = 4
domain = 1..5
variables = Enum.map(1..num_variables, fn idx -> Variable.new(domain, name: "x#{idx}") end)
%{value_graph: graph, left_partition: _left_partition} = ValueGraph.build(variables)
## For 'variable' vertices, all neighbors are 'out' vertices {:value, domain_value}.
## The domain of variable represented by 'variable' vertex is covered by it's neighbors.
assert Enum.all?(0..(num_variables - 1), fn var_idx ->
variable_vertex = var_idx
BitGraph.out_degree(graph, variable_vertex) == Range.size(domain) &&
BitGraph.in_degree(graph, variable_vertex) == 0 &&
BitGraph.out_neighbors(graph, variable_vertex) ==
MapSet.new(domain, fn val -> {:value, val} end) &&
Enum.empty?(
BitGraph.in_neighbors(graph, variable_vertex)
)
end)
## For 'value' vertices, all neighbors are 'in' vertices `variable_index`
## The number of neighbors corresponds to the number of variables currently having the value
## in their domain
assert Enum.all?(domain, fn value ->
value_vertex = {:value, value}
BitGraph.in_degree(graph, value_vertex) == num_variables &&
BitGraph.out_degree(graph, value_vertex) == 0 &&
BitGraph.in_neighbors(graph, value_vertex) ==
MapSet.new(0..num_variables - 1) &&
Enum.empty?(
BitGraph.out_neighbors(graph, value_vertex)
)
end)
## Remove value from the domain of variable
some_value = Enum.random(domain)
some_variable_index = Enum.random(0..(num_variables - 1))
Interface.remove(Enum.at(variables, some_variable_index), some_value)
## The 'value' vertex is removed from neighbors of the 'variable' vertex
assert BitGraph.out_neighbors(graph, some_variable_index) ==
MapSet.new(List.delete(Range.to_list(domain), some_value), fn val ->
{:value, val}
end)
# ... and vice versa
assert BitGraph.in_neighbors(graph, {:value, some_value}) ==
MapSet.new(
List.delete(Range.to_list(0..(num_variables - 1)), some_variable_index)
)
## ... nothing changes otherwise
assert Enum.empty?(
BitGraph.out_neighbors(graph, {:value, some_value})
)
assert Enum.empty?(
BitGraph.in_neighbors(graph, some_variable_index)
)
end
test "'matching' neighbor_finder" do
domain = 1..5
num_variables = 4
variables = Enum.map(1..num_variables, fn idx -> Variable.new(domain, name: "x#{idx}") end)
%{value_graph: graph, left_partition: left_partition} = ValueGraph.build(variables)
matching =
BitGraph.bipartite_matching(graph, left_partition: left_partition)
assert MapSet.size(matching.free) == 1
## Matching is valid
# 4 variables in the matching map
assert map_size(matching.matching) == 4
# 4 values in reverse matching map
assert map_size(Map.new(matching.matching, fn {var, value} -> {value, var} end)) == 4
## Remove all edges to free node
{:value, free_node_value} = free_vertex = MapSet.to_list(matching.free) |> hd()
## free node is in the graph before edge removals
assert BitGraph.get_vertex(graph, free_vertex)
graph = Enum.reduce(0..num_variables-1, graph, fn var_idx, graph_acc ->
ValueGraph.delete_edge(graph_acc, var_idx, BitGraph.V.get_vertex_index(graph, free_vertex), variables)
end)
## Free node is no longer in the graph
refute BitGraph.get_vertex(graph, free_vertex)
## Free node value is no longer in variable's domains
refute Enum.any?(variables, fn var ->
Interface.contains?(var, free_node_value)
end)
matching2 =
BitGraph.Algorithm.bipartite_matching(graph, left_partition: left_partition,
process_mode: :preprocess)
## No free nodes
assert Enum.empty?(matching2.free)
## Matching is valid
# 4 variables in the matching map
assert map_size(matching2.matching) == 4
matching_neighbor_finder =
ValueGraph.matching_neighbor_finder(graph, variables, matching2.matching, matching2.free)
matching_graph = BitGraph.set_neighbor_finder(graph, matching_neighbor_finder)
assert Enum.all?(matching2.matching, fn {var_vertex, value_vertex} ->
iterables_equal?(BitGraph.V.out_neighbors(matching_graph, value_vertex), [var_vertex])
&& iterables_equal?(BitGraph.V.in_neighbors(matching_graph, var_vertex), [value_vertex])
&& iterables_equal?(BitGraph.V.out_neighbors(matching_graph, var_vertex),
Map.values(matching2.matching) -- [value_vertex])
&& iterables_equal?(BitGraph.V.in_neighbors(matching_graph, value_vertex),
Map.keys(matching2.matching) -- [var_vertex])
end)
## Original graph has edges from variables to values
## hence, not strongly connected
refute BitGraph.strongly_connected?(graph)
## With matching edges oriented from values to variables,
## the graph becomes a cycle.
matching_neighbor_finder =
ValueGraph.matching_neighbor_finder(graph, variables, matching2.matching, matching2.free)
assert BitGraph.strongly_connected?(graph,
neighbor_finder: matching_neighbor_finder
)
end
test "matching neighbor finder with fixed variables" do
domain = 1..3
variables = Enum.map(domain, fn idx -> Variable.new(domain, name: "x#{idx}") end)
%{value_graph: value_graph, left_partition: variable_vertices} = ValueGraph.build(variables)
%{matching: matching, free: free_nodes}
= BitGraph.Algorithm.bipartite_matching(value_graph, left_partition: variable_vertices,
process_mode: :preprocess)
var_index = 0
## Fix a variable...
:fixed = Interface.fix(Enum.at(variables, var_index), 1)
fixed_variable_vertex_index = ValueGraph.variable_vertex_index(var_index)
matched_value_vertex_index = Map.get(matching, fixed_variable_vertex_index)
reduced_matching = Map.delete(matching, fixed_variable_vertex_index)
## Create a 'matching' graph with reduced matching
neighbor_finder = ValueGraph.matching_neighbor_finder(value_graph, variables, reduced_matching, free_nodes)
matching_graph = BitGraph.update_opts(value_graph, neighbor_finder: neighbor_finder)
## 'Variable' vertices that are not part of (reduced) matching are isolated
assert BitGraph.degree(matching_graph, var_index) == 0
## The 'fixed value' vertex is still connected to 'non-fixed' variable vertices
assert BitGraph.V.in_degree(matching_graph, matched_value_vertex_index) == 2
## No out-neighbors for the 'fixed value' vertex.
assert BitGraph.out_degree(matching_graph, matched_value_vertex_index) == 0
refute fixed_variable_vertex_index in
(BitGraph.V.in_neighbors(matching_graph, matched_value_vertex_index) |> Iter.Iterable.to_list())
end
end
end