diff --git a/src/CP/constraints/algorithms/matching.jl b/src/CP/constraints/algorithms/matching.jl new file mode 100644 index 000000000..466bcaae5 --- /dev/null +++ b/src/CP/constraints/algorithms/matching.jl @@ -0,0 +1,140 @@ +using LightGraphs +using Random + +struct Matching{T} + size::Int + matches::Vector{Pair{T, T}} +end + +""" + randomMatching(graph, lastfirst)::Matching{Int} + +Compute a random matching in a bipartite graph, between 1:lastfirst and (lastfirst + 1):nv(graph). +""" +function randomMatching(graph::Graph{Int}, lastfirst::Int)::Matching{Int} + nodesSeen = Set{Int}() + match = Vector{Pair{Int, Int}}() + for i = 1:lastfirst + nodesPossible = setdiff(neighbors(graph, i), nodesSeen) + if !isempty(nodesPossible) + node = rand(nodesPossible) + push!(match, Pair(i, node)) + push!(nodesSeen, node) + end + end + return Matching{Int}(length(match), match) +end + +""" + matchingFromDigraph(digraph, lastfirst)::Matching{Int} + +Compute a matching from a directed bipartite graph. + +The graph must be bipartite with variables in 1:lastfirst and values in +(lastfirst + 1):nv(digraph). A variable is assigned to a value if the directed +edge (Var => Val) exists. +""" +function matchingFromDigraph(digraph::DiGraph{Int}, lastfirst::Int)::Matching{Int} + matches = Vector{Pair{Int, Int}}() + for i = 1:lastfirst + nodesPossible = outneighbors(digraph, i) + if !isempty(nodesPossible) + push!(matches, Pair(i, nodesPossible[1])) + end + end + return Matching(length(matches), matches) +end + +""" + augmentMatching!(digraph, lastfirst, start, free)::Union{Nothing, Pair{Int, Int}} + +Find an alternating path in a directed graph and augment the current matching. + +From a directed bipartite graph, the boundary between the 2 groups, a free value +and an array of the free variables, find an alternating path from a free value to +a free variable and augment the matching. + +# Arguments +- `digraph::DiGraph{Int}`: the directed graph encoding the problem state. +- `lastfirst::Int`: the last element of the first group of nodes. +- `start::Int`: the free value node to start the search from. +- `free::Vector{Int}`: the list of all the free variables indexes. +""" +function augmentMatching!(digraph::DiGraph{Int}, start::Int, free::Set{Int})::Union{Nothing, Pair{Int, Int}} + parents = bfs_parents(digraph, start; dir=:out) + nodesReached = intersect(free, findall(v -> v > 0, parents)) + if isempty(nodesReached) + return nothing + end + + node = pop!(nodesReached) + currentNode = node + parent = parents[currentNode] + while currentNode != start + add_edge!(digraph, currentNode, parent) + rem_edge!(digraph, parent, currentNode) + currentNode, parent = parent, parents[parent] + end + return Pair(node, start) +end + +""" + buildDigraph!(digraph, graph, matching) + +Build a directed bipartite graph, from a graph and a matching solution. + +Copy the structure of `graph` into a pre-allocated `digraph` and orient the +edges using the matches contained in `matching`. +""" +function buildDigraph!(digraph::DiGraph{Int}, graph::Graph{Int}, match::Matching{Int}) + rem_edge!.([digraph], edges(digraph)) + for edge in edges(graph) + src, dst = edge.src > edge.dst ? (edge.src, edge.dst) : (edge.dst, edge.src) + add_edge!(digraph, src, dst) + end + for match in match.matches + src, dst = match + add_edge!(digraph, src, dst) + rem_edge!(digraph, dst, src) + end +end + +""" + maximizematching!(graph, digraph, lastfirst) + +Transform a directed graph, which encodes a matching, to encode a maximal matching. + +The problem must be encoded in the 2 bipartite graph, and `digraph` must already +encode a partial matching at least. Lastfirst is the index of the last node of +the first group. +""" +function maximizeMatching!(digraph::DiGraph{Int}, lastfirst::Int)::Matching{Int} + currentMatching = matchingFromDigraph(digraph, lastfirst) + stop = currentMatching.size == lastfirst + freeVariables = Set(filter(v -> outdegree(digraph, v) == 0, 1:lastfirst)) + freeValues = Set(filter(v -> indegree(digraph,v) == 0, lastfirst+1:nv(digraph))) + while !stop + start = pop!(freeValues) + augment = augmentMatching!(digraph, start, freeVariables) + if !isnothing(augment) + pop!(freeVariables, augment[1]) + end + stop = isempty(freeValues) || isempty(freeVariables) + end + return matchingFromDigraph(digraph, lastfirst) +end + +""" + maximumMatching!(graph, digraph, lastfirst)::Matching{Int} + +Compute a maximum matching from a given bipartite graph. + +From a variables-values problem encoded in `graph`, a pre-allocated `digraph` of +the same size, and the index of the last node of the first group, compute a maximum +matching and encode it in `digraph`. +""" +function maximumMatching!(graph::Graph{Int}, digraph::DiGraph{Int}, lastfirst::Int)::Matching{Int} + first = randomMatching(graph, lastfirst) + buildDigraph!(digraph, graph, first) + return maximizeMatching!(digraph, lastfirst) +end diff --git a/src/CP/constraints/alldifferent.jl b/src/CP/constraints/alldifferent.jl new file mode 100644 index 000000000..49855b46e --- /dev/null +++ b/src/CP/constraints/alldifferent.jl @@ -0,0 +1,318 @@ +include("algorithms/matching.jl") + +@enum State begin + used = 1 + unused = 2 + removed = 3 + vital = 4 +end + +""" + AllDifferent(x::Vector{SeaPearl.AbstractIntVar}, trailer) + +AllDifferent constraint, enforcing ∀ i ≠ j ∈ ⟦1, length(x)⟧, x[i] ≠ x[j]. + +The implementation of this contraint is inspired by: + https://www.researchgate.net/publication/200034395_A_Filtering_Algorithm_for_Constraints_of_Difference_in_CSPs +Many of the functions below relate to algorithms depicted in the paper, and their +documentation refer to parts of the overall algorithm. +""" +struct AllDifferent <: Constraint + x::Vector{SeaPearl.AbstractIntVar} + active::StateObject{Bool} + initialized::StateObject{Bool} + matching::Vector{StateObject{Pair{Int, Int}}} + edgesState::Dict{Edge{Int}, StateObject{State}} + nodesMin::Int + numberOfVars::Int + numberOfVals::Int + + function AllDifferent(x::Vector{SeaPearl.AbstractIntVar}, trailer)::AllDifferent + max = Base.maximum(var -> maximum(var.domain), x) + min = Base.minimum(var -> minimum(var.domain), x) + range = max - min + 1 + active = StateObject{Bool}(true, trailer) + initialized = StateObject{Bool}(false, trailer) + numberOfVars = length(x) + matching = Vector{StateObject{Pair{Int, Int}}}(undef, numberOfVars) + for i = 1:numberOfVars + matching[i] = StateObject{Pair{Int, Int}}(Pair(0, 0), trailer) + end + edgesState = Dict{Edge{Int}, StateObject{State}}() + constraint = new(x, + active, + initialized, + matching, + edgesState, + min, + numberOfVars, + range) + for (idx, var) in enumerate(x) + addOnDomainChange!(var, constraint) + for val in var.domain + dst = numberOfVars + val - min + 1 + constraint.edgesState[Edge(idx, dst)] = StateObject(unused, trailer) + end + end + return constraint + end +end + +""" + valToNode(constraint, value)::Int + +Return the node index of a value. +""" +function valToNode(con::AllDifferent, val::Int) + return con.numberOfVars + val - con.nodesMin + 1 +end + +""" + nodeToVal(constraint, node)::Int + +Return the underlying value of a node. +""" +function nodeToVal(con::AllDifferent, node::Int) + return node - con.numberOfVars + con.nodesMin - 1 +end + +""" + orderEdge(edge)::Edge + +Return the ordered version of an edge, i.e. with e.src ≤ e.dst. +""" +function orderEdge(e::Edge{Int})::Edge{Int} + src, dst = e.src < e.dst ? (e.src, e.dst) : (e.dst, e.src) + return Edge(src, dst) +end + +""" + initializeGraphs!(constraint) + +Return the graph and the empty direct graph of a variable-value problem. +""" +function initializeGraphs!(con::AllDifferent)::Pair{Graph{Int}, DiGraph{Int}} + graph = Graph(con.numberOfVars + con.numberOfVals) + digraph = DiGraph(nv(graph)) + for (e, status) in con.edgesState + if status.value != removed + add_edge!(graph, e.src, e.dst) + end + end + return Pair(graph, digraph) +end + +""" + getAllEdges(digraph, parents)::Set{Edge} + +Return all the edges visited by a BFS on `digraph` encoded in `parents`. +""" +function getAllEdges(digraph::DiGraph{Int}, parents::Vector{Int})::Set{Edge{Int}} + edgeSet = Set{Edge{Int}}() + for i = 1:nv(digraph) + if parents[i] > 0 && parents[i] != i + validneighbors = filter(v -> parents[v] > 0, inneighbors(digraph, i)) + validedges = map(v -> orderEdge(Edge(v, i)), validneighbors) + union!(edgeSet, validedges) + end + end + return edgeSet +end + +""" + getAllEdges(digraph, vars, vals) + +Return all the edges in a strongly connected component vars ∪ vars. +""" +function getAllEdges(digraph::DiGraph{Int}, vars::Vector{Int}, vals::Vector{Int})::Set{Edge{Int}} + edgeSet = Set{Edge{Int}}() + for var in vars + for val in intersect(union(inneighbors(digraph, var), outneighbors(digraph, var)), vals) + push!(edgeSet, orderEdge(Edge(var, val))) + end + end + return edgeSet +end + +""" + removeEdges!(constraint, prunedValue, graph, digraph) + +Remove all the unnecessary edges in graph and digraph as in the original paper. + +Update `constraint.edgesState` with the new status of each edge, remove some +edges from `graph` and `digraph` and push the removed values in `prunedValue`. +Following exactly the procedure in the function with the same name in the original +paper. +""" +function removeEdges!(constraint::AllDifferent, prunedValues::Vector{Vector{Int}}, graph::Graph{Int}, digraph::DiGraph{Int}) + for e in edges(graph) + if constraint.edgesState[orderEdge(e)] != removed + setValue!(constraint.edgesState[orderEdge(e)], unused) + end + end + + allValues = constraint.numberOfVars+1:nv(digraph) + freeValues = filter(v -> indegree(digraph,v) == 0, allValues) + + seen = fill(false, constraint.numberOfVals) + components = filter(comp -> length(comp)>1, strongly_connected_components(digraph)) + for component in components + variables = filter(v -> v <= constraint.numberOfVars, component) + values = filter(v -> v > constraint.numberOfVars, component) + edgeSet = getAllEdges(digraph, variables, values) + for e in edgeSet + setValue!(constraint.edgesState[e], used) + end + end + for node in freeValues + if seen[node - constraint.numberOfVars] + continue + end + parents = bfs_parents(digraph, node; dir=:out) + edgeSet = getAllEdges(digraph, parents) + for e in edgeSet + setValue!(constraint.edgesState[e], used) + end + reached = filter(v -> parents[v] > 0, allValues) + for val in reached + seen[val - constraint.numberOfVars] = true + end + end + + for pair in constraint.matching + var, val = pair.value + e = Edge(var, val) + if constraint.edgesState[e].value == unused + setValue!(constraint.edgesState[e], vital) + end + end + + rest = findall(state -> state.value == unused, constraint.edgesState) + for e in rest + rem_edge!(graph, e) + rem_edge!(digraph, Edge(e.dst, e.src)) + setValue!(constraint.edgesState[e], removed) + var, val = e.src < e.dst ? (e.src, e.dst) : (e.dst, e.src) + push!(prunedValues[var], nodeToVal(constraint, val)) + end +end + +""" + updateEdgesState!(constraint)::Set{Edge} + +Return all the pruned values not already encoded in the constraint state. +""" +function updateEdgesState!(constraint::AllDifferent) + modif = Set{Edge}() + for (edge, state) in constraint.edgesState + if state.value != removed && !(nodeToVal(constraint, edge.dst) in constraint.x[edge.src].domain) + push!(modif, edge) + end + end + return modif +end + +""" + propagate!(constraint::AllDifferent, toPropagate::Set{Constraint}, prunedDomains::CPModification) + +`AllDifferent` propagation function. Implement the full procedure of the paper. +""" +function propagate!(constraint::AllDifferent, toPropagate::Set{Constraint}, prunedDomains::CPModification) + if !constraint.active.value + return true + end + + # Variables Initialization + graph, digraph = initializeGraphs!(constraint) + # Run only once, when constraint is first propagated + if !constraint.initialized.value + matching = maximumMatching!(graph, digraph, constraint.numberOfVars) + if matching.size < constraint.numberOfVars + return false + end + for (idx, match) in enumerate(matching.matches) + setValue!(constraint.matching[idx], match) + end + setValue!(constraint.initialized, true) + # Otherwise just read the stored values + else + matching = Matching{Int}(length(constraint.matching), map(pair -> pair.value, constraint.matching)) + buildDigraph!(digraph, graph, matching) + end + + modifications = updateEdgesState!(constraint) + prunedValues = Vector{Vector{Int}}(undef, constraint.numberOfVars) + for i = 1:constraint.numberOfVars + prunedValues[i] = Int[] + end + + removeEdges!(constraint, prunedValues, graph, digraph) + needrematching = false + for e in modifications + rev_e = Edge(e.dst, e.src) + if e in edges(graph) + if constraint.edgesState[e].value == vital + return false + elseif e in edges(digraph) + needrematching = true + rem_edge!(digraph, e) + else + rem_edge!(digraph, rev_e) + end + rem_edge!(graph, e) + setValue!(constraint.edgesState[e], removed) + end + end + + if needrematching + matching = maximizeMatching!(digraph, constraint.numberOfVars) + if matching.size < constraint.numberOfVars + return false + end + for (idx, match) in enumerate(matching.matches) + setValue!(constraint.matching[idx], match) + end + end + removeEdges!(constraint, prunedValues, graph, digraph) + + for (prunedVar, var) in zip(prunedValues, constraint.x) + if !isempty(prunedVar) + for val in prunedVar + remove!(var.domain, val) + end + triggerDomainChange!(toPropagate, var) + addToPrunedDomains!(prunedDomains, var, prunedVar) + end + end + + if constraint in toPropagate + pop!(toPropagate, constraint) + end + + if all(var -> length(var.domain) <= 1, constraint.x) + setValue!(constraint.active, false) + end + + for var in constraint.x + if isempty(var.domain) + return false + end + end + return true +end + +AllDifferent(x::Vector{IntVar}, trailer) = AllDifferent(Vector{AbstractIntVar}(x), trailer) +AllDifferent(x::Vector{IntVarView}, trailer) = AllDifferent(Vector{AbstractIntVar}(x), trailer) + +variableArray(constraint::AllDifferent) = constraint.x + +function Base.show(io::IO, ::MIME"text/plain", con::AllDifferent) + println(io, string(typeof(con)), ": ", join([var.id for var in con.x], " != "), ", active = ", con.active) + for var in con.x + println(io, " ", var) + end +end + +function Base.show(io::IO, con::AllDifferent) + println(io, string(typeof(con)), ": ", join([var.id for var in con.x], " != ")) +end diff --git a/src/CP/constraints/constraints.jl b/src/CP/constraints/constraints.jl index c2540f1a7..ba8e6e114 100644 --- a/src/CP/constraints/constraints.jl +++ b/src/CP/constraints/constraints.jl @@ -1,4 +1,5 @@ +include("alldifferent.jl") include("equal.jl") include("notequal.jl") include("lessorequal.jl") diff --git a/test/CP/constraints/algorithms/matching.jl b/test/CP/constraints/algorithms/matching.jl new file mode 100644 index 000000000..0262174ab --- /dev/null +++ b/test/CP/constraints/algorithms/matching.jl @@ -0,0 +1,117 @@ +using LightGraphs + +@testset "matching.jl" begin + @testset "randomMatching(::Graph{Int}, ::Int)::Matching{Int}" begin + bipartite = Graph(10) + for i = 1:5 + add_edge!(bipartite, i, i+5) + end + matching = SeaPearl.randomMatching(bipartite, 5) + edgeset = [Edge(m[1], m[2]) for m in matching.matches] + + @test matching.size == 5 + @test length(intersect(edgeset, edges(bipartite))) == 5 + end + @testset "matchingFromDigraph(::DiGraph{Int}, ::Int)::Matching{Int}" begin + bipartite = DiGraph(6) + add_edge!(bipartite, 1, 4) + add_edge!(bipartite, 5, 1) + add_edge!(bipartite, 2, 5) + add_edge!(bipartite, 6, 3) + add_edge!(bipartite, 6, 2) + matching = SeaPearl.matchingFromDigraph(bipartite, 3) + + @test matching.size == 2 + @test Pair(1, 4) in matching.matches + @test Pair(2, 5) in matching.matches + end + @testset "augmentMatching!(::DiGraph, ::Int, ::Vector{Int})" begin + bipartite = DiGraph(6) + add_edge!(bipartite, 1, 4) + add_edge!(bipartite, 5, 1) + add_edge!(bipartite, 2, 5) + add_edge!(bipartite, 5, 3) + add_edge!(bipartite, 6, 2) + free = Set([3]) + res = SeaPearl.augmentMatching!(bipartite, 6, free) + + @test !isnothing(res) + @test res == Pair(3, 6) + end + @testset "buildDigraph!(::DiGraph{Int}, ::Graph{Int}, ::Matching{Int})" begin + graph = Graph(7) + digraph = DiGraph(7) + add_edge!(graph, 1, 4) + add_edge!(graph, 1, 5) + add_edge!(graph, 2, 4) + add_edge!(graph, 2, 7) + add_edge!(graph, 3, 4) + add_edge!(graph, 3, 6) + add_edge!(graph, 3, 7) + matching = SeaPearl.Matching(2, [Pair(1, 4), Pair(2, 7)]) + SeaPearl.buildDigraph!(digraph, graph, matching) + + @test Edge(1, 4) in edges(digraph) + @test Edge(5, 1) in edges(digraph) + @test Edge(4, 2) in edges(digraph) + @test Edge(2, 7) in edges(digraph) + @test Edge(4, 3) in edges(digraph) + @test Edge(6, 3) in edges(digraph) + @test Edge(7, 3) in edges(digraph) + end + @testset "maximizeMatching!(::Graph, ::DiGraph, ::Int)::Matching" begin + #Replays the example from the paper + graph = Graph(12) + digraph = DiGraph(12) + add_edge!(graph, 1, 7) + add_edge!(graph, 1, 8) + add_edge!(graph, 2, 8) + add_edge!(graph, 2, 9) + add_edge!(graph, 3, 9) + add_edge!(graph, 4, 8) + add_edge!(graph, 4, 10) + add_edge!(graph, 5, 9) + add_edge!(graph, 5, 10) + add_edge!(graph, 5, 11) + add_edge!(graph, 5, 12) + add_edge!(graph, 6, 12) + matching = SeaPearl.Matching(3, [Pair(1, 7), Pair(4, 8), Pair(5, 10)]) + SeaPearl.buildDigraph!(digraph, graph, matching) + + SeaPearl.maximizeMatching!(digraph, 6) + target = [Edge(1, 7), Edge(2, 8), Edge(3, 9), Edge(4, 10), Edge(5, 11), Edge(6, 12)] + @test all([e in edges(digraph) for e in target]) + @test all([outdegree(digraph, v) == 1 for v in 1:6]) + end + @testset "maximumMatching!(::Graph{Int}, ::DiGraph{Int}, ::Int)::Matching{Int}" begin + # This function and all its dependencies have been tested with an external library + # Thus the code written at that time is certified to work based on 3000 randomly generated + # matching cases with optimal matching detection. + + # Warning the testsets aren't as powerful as this external control and in case of a code + # modification I recommend to run external tests again. + + graph = Graph(13) + digraph = DiGraph(13) + add_edge!(graph, 1, 7) + add_edge!(graph, 1, 8) + add_edge!(graph, 2, 8) + add_edge!(graph, 2, 9) + add_edge!(graph, 3, 7) + add_edge!(graph, 3, 9) + add_edge!(graph, 4, 8) + add_edge!(graph, 4, 10) + add_edge!(graph, 5, 9) + add_edge!(graph, 5, 10) + add_edge!(graph, 5, 11) + add_edge!(graph, 6, 11) + add_edge!(graph, 6, 12) + matching = SeaPearl.maximumMatching!(graph, digraph, 6) + + @test matching.size == 6 + @test all(map(pair -> Edge(pair[1], pair[2]) in edges(graph), matching.matches)) + @test all(map(pair -> Edge(pair[1], pair[2]) in edges(digraph), matching.matches)) + @test all(map(e -> e in edges(graph), edges(digraph))) + @test ne(graph) == ne(digraph) + end +end \ No newline at end of file diff --git a/test/CP/constraints/alldifferent.jl b/test/CP/constraints/alldifferent.jl new file mode 100644 index 000000000..6467a3abc --- /dev/null +++ b/test/CP/constraints/alldifferent.jl @@ -0,0 +1,269 @@ +using LightGraphs + +@testset "alldifferent.jl" begin + @testset "AllDifferent(::Vector{AbstractIntVar}, ::Trailer)" begin + trailer = SeaPearl.Trailer() + x = SeaPearl.IntVar(1, 3, "x", trailer) + y = SeaPearl.IntVar(2, 3, "y", trailer) + z = SeaPearl.IntVar(2, 3, "Z", trailer) + vec = Vector{SeaPearl.AbstractIntVar}([x, y, z]) + + constraint = SeaPearl.AllDifferent(vec, trailer) + + @test constraint.active.value + @test !constraint.initialized.value + @test constraint.nodesMin == 1 + @test constraint.numberOfVals == 3 + @test constraint in x.onDomainChange + @test constraint in y.onDomainChange + @test constraint in z.onDomainChange + end + @testset "orderEdge(::Edge)::Edge" begin + @test SeaPearl.orderEdge(Edge(1, 2)) == Edge(1, 2) + @test SeaPearl.orderEdge(Edge(2, 1)) == Edge(1, 2) + end + @testset "initializeGraphs!(::AllDifferent)::Pair{Graph{Int}, DiGraph{Int}}" begin + trailer = SeaPearl.Trailer() + x = SeaPearl.IntVar(1, 3, "x", trailer) + y = SeaPearl.IntVar(2, 3, "y", trailer) + z = SeaPearl.IntVar(2, 3, "Z", trailer) + vec = Vector{SeaPearl.AbstractIntVar}([x, y, z]) + + constraint = SeaPearl.AllDifferent(vec, trailer) + SeaPearl.setValue!(constraint.edgesState[Edge(1, 6)], SeaPearl.removed) + graph, digraph = SeaPearl.initializeGraphs!(constraint) + + @test Edge(1, 4) in edges(graph) + @test Edge(1, 5) in edges(graph) + @test Edge(2, 5) in edges(graph) + @test Edge(2, 6) in edges(graph) + @test Edge(3, 5) in edges(graph) + @test Edge(3, 6) in edges(graph) + @test ne(graph) == 6 + end + @testset "getAllEdges(::DiGraph, ::Vector{Int})" begin + bipartite = DiGraph(7) + add_edge!(bipartite, 4, 1) + add_edge!(bipartite, 5, 1) + add_edge!(bipartite, 1, 6) + add_edge!(bipartite, 2, 5) + add_edge!(bipartite, 6, 2) + add_edge!(bipartite, 3, 7) + parents = bfs_parents(bipartite, 4; dir=:out) + edgeset = SeaPearl.getAllEdges(bipartite, parents) + + @test length(edgeset) == 5 + @test Edge(1, 4) in edgeset + @test Edge(1, 6) in edgeset + @test Edge(2, 5) in edgeset + @test Edge(2, 6) in edgeset + @test Edge(1, 5) in edgeset + end + @testset "getAllEdges(::DiGraph, ::Vector{Int}, ::Vector{Int})" begin + bipartite = DiGraph(7) + add_edge!(bipartite, 4, 1) + add_edge!(bipartite, 5, 1) + add_edge!(bipartite, 1, 6) + add_edge!(bipartite, 2, 5) + add_edge!(bipartite, 6, 2) + add_edge!(bipartite, 3, 7) + edgeset = SeaPearl.getAllEdges(bipartite, [1, 2], [5, 6]) + + @test length(edgeset) == 4 + @test Edge(1, 5) in edgeset + @test Edge(1, 6) in edgeset + @test Edge(2, 5) in edgeset + @test Edge(2, 6) in edgeset + end + @testset "removeEdges!(::AllDifferent, ::Vector{Vector{Int}}, ::Graph, ::DiGraph)" begin + + trailer = SeaPearl.Trailer() + x = SeaPearl.IntVar(1, 2, "x", trailer) + y = SeaPearl.IntVar(2, 3, "y", trailer) + z = SeaPearl.IntVar(1, 3, "z", trailer) + SeaPearl.remove!(z.domain, 2) + a = SeaPearl.IntVar(2, 4, "a", trailer) + SeaPearl.remove!(a.domain, 3) + b = SeaPearl.IntVar(3, 6, "b", trailer) + c = SeaPearl.IntVar(6, 7, "c", trailer) + vars = Vector{SeaPearl.AbstractIntVar}([x, y, z, a, b, c]) + constraint = SeaPearl.AllDifferent(vars, trailer) + + graph, digraph = SeaPearl.initializeGraphs!(constraint) + matching = SeaPearl.Matching(6, [Pair(1, 7), Pair(2, 8), Pair(3, 9), Pair(4, 10), Pair(5, 11), Pair(6, 12)]) + for (idx, match) in enumerate(matching.matches) + constraint.matching[idx] = SeaPearl.StateObject{Pair{Int, Int}}(match, trailer) + end + SeaPearl.setValue!(constraint.initialized, true) + SeaPearl.buildDigraph!(digraph, graph, matching) + prunedValues = Vector{Vector{Int}}(undef, constraint.numberOfVars) + for i = 1:constraint.numberOfVars + prunedValues[i] = Int[] + end + SeaPearl.removeEdges!(constraint, prunedValues, graph, digraph) + + @test isempty(prunedValues[1]) + @test isempty(prunedValues[2]) + @test isempty(prunedValues[3]) + @test prunedValues[4] == [2] + @test Set(prunedValues[5]) == Set([3, 4]) + @test isempty(prunedValues[6]) + + @test !(Edge(8, 4) in edges(digraph)) + @test !(Edge(9, 5) in edges(digraph)) + @test !(Edge(10, 5) in edges(digraph)) + + @test !(Edge(8, 4) in edges(graph)) + @test !(Edge(9, 5) in edges(graph)) + @test !(Edge(10, 5) in edges(graph)) + end + @testset "propagate!(::AllDifferent, ::Set{Constraint}, ::CPModification)" begin + trailer = SeaPearl.Trailer() + x = SeaPearl.IntVar(1, 2, "x", trailer) + y = SeaPearl.IntVar(2, 3, "y", trailer) + z = SeaPearl.IntVar(1, 3, "z", trailer) + SeaPearl.remove!(z.domain, 2) + a = SeaPearl.IntVar(2, 4, "a", trailer) + SeaPearl.remove!(a.domain, 3) + b = SeaPearl.IntVar(3, 6, "b", trailer) + c = SeaPearl.IntVar(6, 7, "c", trailer) + vars = Vector{SeaPearl.AbstractIntVar}([x, y, z, a, b, c]) + constraint = SeaPearl.AllDifferent(vars, trailer) + + toPropagate = Set{SeaPearl.Constraint}([constraint]) + modif = SeaPearl.CPModification(Dict("z" => [1])) + SeaPearl.remove!(z.domain, 1) + + res = SeaPearl.propagate!(constraint, toPropagate, modif) + + @test res + @test !(constraint in toPropagate) + @test modif["x"] == [2] + @test modif["y"] == [3] + @test modif["z"] == [1] + @test modif["a"] == [2] + @test 3 in modif["b"] && 4 in modif["b"] + @test !("c" in keys(modif)) + + @test length(x.domain) == 1 + @test length(y.domain) == 1 + @test length(z.domain) == 1 + @test length(a.domain) == 1 + @test length(b.domain) == 2 + @test length(c.domain) == 2 + + end + @testset "3 queens dummy" begin + trailer = SeaPearl.Trailer() + n = 3 + rows = Vector{SeaPearl.AbstractIntVar}(undef, n) + for i = 1:n + rows[i] = SeaPearl.IntVar(1, n, "row_"*string(i), trailer) + end + + rows_plus = Vector{SeaPearl.AbstractIntVar}(undef, n) + for i = 1:n + rows_plus[i] = SeaPearl.IntVarViewOffset(rows[i], i, rows[i].id*"+"*string(i)) + end + + rows_minus = Vector{SeaPearl.AbstractIntVar}(undef, n) + for i = 1:n + rows_minus[i] = SeaPearl.IntVarViewOffset(rows[i], -i, rows[i].id*"-"*string(i)) + end + + con1 = SeaPearl.AllDifferent(rows, trailer) + con2 = SeaPearl.AllDifferent(rows_plus, trailer) + con3 = SeaPearl.AllDifferent(rows_minus, trailer) + + modif = SeaPearl.CPModification() + toPropagate = Set{SeaPearl.Constraint}([con1, con2, con3]) + + @test SeaPearl.propagate!(con1, toPropagate, modif) + @test SeaPearl.propagate!(con3, toPropagate, modif) + @test SeaPearl.propagate!(con2, toPropagate, modif) + + SeaPearl.withNewState!(trailer) do + SeaPearl.assign!(rows[1].domain, 1) + toPropagate = Set{SeaPearl.Constraint}([con1, con2, con3]) + + @test SeaPearl.propagate!(con1, toPropagate, modif) + @test SeaPearl.propagate!(con3, toPropagate, modif) + @test !SeaPearl.propagate!(con2, toPropagate, modif) + end + modif = SeaPearl.CPModification() + SeaPearl.withNewState!(trailer) do + SeaPearl.assign!(rows[1].domain, 2) + toPropagate = Set{SeaPearl.Constraint}([con1, con2, con3]) + + @test SeaPearl.propagate!(con1, toPropagate, modif) + @test SeaPearl.propagate!(con3, toPropagate, modif) + @test !SeaPearl.propagate!(con2, toPropagate, modif) + end + end + @testset "5 queens full" begin + n=5 + trailer = SeaPearl.Trailer() + model = SeaPearl.CPModel(trailer) + + rows = Vector{SeaPearl.AbstractIntVar}(undef, n) + for i = 1:n + rows[i] = SeaPearl.IntVar(1, n, "row_"*string(i), trailer) + SeaPearl.addVariable!(model, rows[i]; branchable=true) + end + + rows_plus = Vector{SeaPearl.AbstractIntVar}(undef, n) + for i = 1:n + rows_plus[i] = SeaPearl.IntVarViewOffset(rows[i], i, rows[i].id*"+"*string(i)) + #SeaPearl.addVariable!(model, rows_plus[i]; branchable=false) + end + + rows_minus = Vector{SeaPearl.AbstractIntVar}(undef, n) + for i = 1:n + rows_minus[i] = SeaPearl.IntVarViewOffset(rows[i], -i, rows[i].id*"-"*string(i)) + #SeaPearl.addVariable!(model, rows_minus[i]; branchable=false) + end + + push!(model.constraints, SeaPearl.AllDifferent(rows, trailer)) + push!(model.constraints, SeaPearl.AllDifferent(rows_plus, trailer)) + push!(model.constraints, SeaPearl.AllDifferent(rows_minus, trailer)) + + variableSelection = SeaPearl.MinDomainVariableSelection{false}() + status = @time SeaPearl.solve!(model; variableHeuristic=variableSelection) + + @test status == :Optimal + @test length(model.solutions) == 10 + end + @testset "7 queens full" begin + n=7 + trailer = SeaPearl.Trailer() + model = SeaPearl.CPModel(trailer) + + rows = Vector{SeaPearl.AbstractIntVar}(undef, n) + for i = 1:n + rows[i] = SeaPearl.IntVar(1, n, "row_"*string(i), trailer) + SeaPearl.addVariable!(model, rows[i]; branchable=true) + end + + rows_plus = Vector{SeaPearl.AbstractIntVar}(undef, n) + for i = 1:n + rows_plus[i] = SeaPearl.IntVarViewOffset(rows[i], i, rows[i].id*"+"*string(i)) + #SeaPearl.addVariable!(model, rows_plus[i]; branchable=false) + end + + rows_minus = Vector{SeaPearl.AbstractIntVar}(undef, n) + for i = 1:n + rows_minus[i] = SeaPearl.IntVarViewOffset(rows[i], -i, rows[i].id*"-"*string(i)) + #SeaPearl.addVariable!(model, rows_minus[i]; branchable=false) + end + + push!(model.constraints, SeaPearl.AllDifferent(rows, trailer)) + push!(model.constraints, SeaPearl.AllDifferent(rows_plus, trailer)) + push!(model.constraints, SeaPearl.AllDifferent(rows_minus, trailer)) + + variableSelection = SeaPearl.MinDomainVariableSelection{false}() + status = @time SeaPearl.solve!(model; variableHeuristic=variableSelection) + + @test status == :Optimal + @test length(model.solutions) == 40 + end +end diff --git a/test/CP/constraints/constraints.jl b/test/CP/constraints/constraints.jl index 8bf440964..3dcf18a79 100644 --- a/test/CP/constraints/constraints.jl +++ b/test/CP/constraints/constraints.jl @@ -1,5 +1,7 @@ @testset "constraints" begin + include("algorithms/matching.jl") + include("alldifferent.jl") include("equal.jl") include("notequal.jl") include("lessorequal.jl") @@ -98,4 +100,4 @@ @test set_constraint in toPropagate end -end \ No newline at end of file +end