Skip to content

Commit 5ca77d9

Browse files
author
Kris Brown
committed
Add BacktrackingTree and debug keyword
1 parent ff10eff commit 5ca77d9

File tree

3 files changed

+172
-26
lines changed

3 files changed

+172
-26
lines changed

src/categorical_algebra/HomSearch.jl

Lines changed: 102 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export ACSetHomomorphismAlgorithm, BacktrackingSearch, HomomorphismQuery,
1010
homomorphism, homomorphisms, is_homomorphic,
1111
isomorphism, isomorphisms, is_isomorphic,
1212
@acset_transformation, @acset_transformations,
13-
subobject_graph, partial_overlaps, maximum_common_subobject
13+
subobject_graph, partial_overlaps, maximum_common_subobject,
14+
debug_homomorphisms
1415

1516
using ...Theories, ..CSets, ..FinSets, ..FreeDiagrams, ..Subobjects
1617
using ...Graphs.BasicGraphs
@@ -20,7 +21,7 @@ using ACSets.DenseACSets: attrtype_type, delete_subobj!
2021
using Random
2122
using CompTime
2223
using MLStyle: @match
23-
using DataStructures: BinaryHeap, DefaultDict
24+
using DataStructures: BinaryHeap, DefaultDict, OrderedDict
2425

2526
# Finding C-set transformations
2627
###############################
@@ -85,7 +86,7 @@ homomorphism(X::ACSet, Y::ACSet; alg=BacktrackingSearch(), kw...) =
8586
function homomorphism(X::ACSet, Y::ACSet, alg::BacktrackingSearch; kw...)
8687
result = nothing
8788
backtracking_search(X, Y; kw...) do α
88-
result = α; return true
89+
result = get_hom(α); return true
8990
end
9091
result
9192
end
@@ -101,11 +102,19 @@ homomorphisms(X::ACSet, Y::ACSet; alg=BacktrackingSearch(), kw...) =
101102
function homomorphisms(X::ACSet, Y::ACSet, alg::BacktrackingSearch; kw...)
102103
results = []
103104
backtracking_search(X, Y; kw...) do α
104-
push!(results, map_components(deepcopy, α)); return false
105+
push!(results, map_components(deepcopy, get_hom(α))); return false
105106
end
106107
results
107108
end
108109

110+
function debug_homomorphisms(X::ACSet, Y::ACSet; kw...)
111+
results = []
112+
m = backtracking_search(X, Y; debug=true, kw...) do α
113+
push!(results, map_components(deepcopy, get_hom(α))); return false
114+
end
115+
results => m.debug
116+
end
117+
109118
""" Is the first attributed ``C``-set homomorphic to the second?
110119
111120
This function generally reduces to [`homomorphism`](@ref) but certain algorithms
@@ -152,6 +161,60 @@ is_isomorphic(X::ACSet, Y::ACSet, alg::BacktrackingSearch; kw...) =
152161
# Backtracking search
153162
#--------------------
154163

164+
"""Keep track of progress through backtracking homomorphism search."""
165+
mutable struct BacktrackingTree
166+
node::Union{Nothing,Pair{Symbol,Int}}
167+
success::Bool
168+
asgn::NamedTuple
169+
children::OrderedDict{Int,BacktrackingTree}
170+
BacktrackingTree() = new(nothing, false, (;), OrderedDict{Int,BacktrackingTree}())
171+
end
172+
173+
"""A backtracking tree plus a pointer to a node in the tree"""
174+
struct BacktrackingTreePt
175+
t::BacktrackingTree
176+
curr::Vector{Int}
177+
BacktrackingTreePt() = new(BacktrackingTree(),Int[])
178+
end
179+
180+
function Base.push!(tc::BacktrackingTreePt, c::Symbol, x::Int, y::Int, asgn)
181+
t = tc.t[tc.curr]
182+
t.node = c => x
183+
t.children[y] = BacktrackingTree()
184+
t.children[y].asgn = deepcopy(asgn)
185+
push!(tc.curr, y)
186+
return true
187+
end
188+
189+
function Base.delete!(tc::BacktrackingTreePt, c::Symbol, x::Int, y::Int)
190+
t = tc.t[tc.curr[1:end-1]]
191+
t.node == (c=>x) || error("Bad remove $c#$x->$y")
192+
pop!(tc.curr)
193+
end
194+
195+
function success(tc::BacktrackingTreePt)
196+
tc.t[tc.curr].success = true
197+
end
198+
199+
function Base.show(io::IO, t::BacktrackingTree)
200+
if !isnothing(t.node)
201+
print(io,"{"); print(io, t.node[1]); print(io, t.node[2]); print(io,"}");
202+
end
203+
print(io, "[")
204+
for (k,v) in collect(t.children)
205+
print(io, k); print(io, v); print(io, ",")
206+
end
207+
if !isempty(t.children) print(io,"\b") end
208+
print(io,"]")
209+
end
210+
211+
function Base.getindex(t::BacktrackingTree, curr::Vector{Int})
212+
for c in curr
213+
t = t.children[c]
214+
end
215+
t
216+
end
217+
155218
""" Get assignment pairs from partially specified component of C-set morphism.
156219
"""
157220
partial_assignments(x::FinFunction; is_attr=false) = partial_assignments(collect(x))
@@ -177,10 +240,25 @@ struct BacktrackingState{
177240
dom::Dom
178241
codom::Codom
179242
type_components::LooseFun
243+
debug::Union{Nothing,BacktrackingTreePt}
244+
end
245+
246+
"""Extract an ACSetTransformation from BacktrackingState"""
247+
function get_hom(state::BacktrackingState)
248+
if any(!=(identity), state.type_components)
249+
return LooseACSetTransformation(
250+
state.assignment, state.type_components, state.dom, state.codom)
251+
else
252+
S = acset_schema(state.dom)
253+
od = Dict{Symbol,Vector{Int}}(k=>(state.assignment[k]) for k in objects(S))
254+
ad = Dict(k=>last.(state.assignment[k]) for k in attrtypes(S))
255+
comps = merge(NamedTuple(od),NamedTuple(ad))
256+
return ACSetTransformation(comps, state.dom, state.codom)
257+
end
180258
end
181259

182260
function backtracking_search(f, X::ACSet, Y::ACSet;
183-
monic=false, iso=false, random=false,
261+
monic=false, iso=false, random=false, debug=false,
184262
type_components=(;), initial=(;), error_failures=false)
185263
S, Sy = acset_schema.([X,Y])
186264
S == Sy || error("Schemas must match for morphism search")
@@ -235,9 +313,11 @@ function backtracking_search(f, X::ACSet, Y::ACSet;
235313
inv_assignment = NamedTuple{ObAttr}(
236314
(c in monic ? zeros(Int, nparts(Y, c)) : nothing) for c in ObAttr)
237315
loosefuns = NamedTuple{Attr}(
238-
isnothing(type_components) ? identity : get(type_components, c, identity) for c in Attr)
239-
state = BacktrackingState(assignment, assignment_depth,
240-
inv_assignment, X, Y, loosefuns)
316+
isnothing(type_components) ? identity : get(type_components, c, identity)
317+
for c in Attr)
318+
319+
state = BacktrackingState(assignment, assignment_depth, inv_assignment, X, Y,
320+
loosefuns, debug ? BacktrackingTreePt() : nothing)
241321

242322
# Make any initial assignments, failing immediately if inconsistent.
243323
for (c, c_assignments) in pairs(initial)
@@ -252,39 +332,32 @@ function backtracking_search(f, X::ACSet, Y::ACSet;
252332
end
253333
end
254334
# Start the main recursion for backtracking search.
255-
backtracking_search(f, state, 1; random=random)
335+
backtracking_search(f, state, 1; random=random, toplevel=true)
256336
end
257337

258338
function backtracking_search(f, state::BacktrackingState, depth::Int;
259-
random=false)
339+
random=false, toplevel=false)
260340
# Choose the next unassigned element.
261341
mrv, mrv_elem = find_mrv_elem(state, depth)
262342
if isnothing(mrv_elem)
263-
# No unassigned elements remain, so we have a complete assignment.
264-
if any(!=(identity), state.type_components)
265-
return f(LooseACSetTransformation(
266-
state.assignment, state.type_components, state.dom, state.codom))
267-
else
268-
S = acset_schema(state.dom)
269-
od = Dict{Symbol,Vector{Int}}(k=>(state.assignment[k]) for k in objects(S))
270-
ad = Dict(k=>last.(state.assignment[k]) for k in attrtypes(S))
271-
comps = merge(NamedTuple(od),NamedTuple(ad))
272-
return f(ACSetTransformation(comps, state.dom, state.codom))
273-
end
343+
isnothing(state.debug) || success(state.debug)
344+
return f(state)
274345
elseif mrv == 0
275346
# An element has no allowable assignment, so we must backtrack.
276347
return false
277348
end
278-
c, x = mrv_elem
349+
c, x, ys = mrv_elem
279350

280351
# Attempt all assignments of the chosen element.
281352
Y = state.codom
282-
for y in (random ? shuffle : identity)(parts(Y, c))
353+
for y in (random ? shuffle : identity)(ys)
283354
(assign_elem!(state, depth, c, x, y)
355+
&& (isnothing(state.debug) ? true : push!(state.debug, c, x, y, state.assignment))
284356
&& backtracking_search(f, state, depth + 1)) && return true
285357
unassign_elem!(state, depth, c, x)
358+
isnothing(state.debug) || delete!(state.debug, c, x, state.assignment[c][x])
286359
end
287-
return false
360+
return toplevel ? state : false # return state to recover debug tree
288361
end
289362

290363
""" Find an unassigned element having the minimum remaining values (MRV).
@@ -295,9 +368,12 @@ function find_mrv_elem(state::BacktrackingState, depth)
295368
Y = state.codom
296369
for c in ob(S), (x, y) in enumerate(state.assignment[c])
297370
y == 0 || continue
298-
n = count(can_assign_elem(state, depth, c, x, y) for y in parts(Y, c))
371+
ys = filter(parts(Y,c)) do y
372+
can_assign_elem(state, depth, c, x, y)
373+
end
374+
n = length(ys)
299375
if n < mrv
300-
mrv, mrv_elem = n, (c, x)
376+
mrv, mrv_elem = n, (c, x, ys)
301377
end
302378
end
303379
(mrv, mrv_elem)

src/graphics/GraphvizCategories.jl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export to_graphviz, to_graphviz_property_graph
66
using ...GATs, ...Theories, ...CategoricalAlgebra, ...Graphs, ..GraphvizGraphs
77
import ..Graphviz
88
import ..GraphvizGraphs: to_graphviz, to_graphviz_property_graph
9+
using ...CategoricalAlgebra.HomSearch: BacktrackingTree, BacktrackingTreePt
910

1011
# Presentations
1112
###############
@@ -143,4 +144,37 @@ function to_graphviz(f::FinFunction{Int,Int}; kw...)
143144
to_graphviz(g; kw...)
144145
end
145146

147+
# Search trees
148+
###############
149+
to_graphviz(t::BacktrackingTreePt) = to_graphviz(t.t)
150+
151+
function to_graphviz(t::BacktrackingTree)
152+
pg = PropertyGraph{Any}(;
153+
prog = "dot",
154+
graph = Dict(),
155+
node = merge!(Dict(:shape => "box", :width => ".1", :height => ".1",
156+
:margin => "0.025", :style=>"filled")),
157+
edge = Dict())
158+
kwargs(tr::BacktrackingTree) = (
159+
fillcolor=tr.success ? "green" : "red",
160+
tooltip=isempty(tr.asgn) ? "" : string(tr.asgn),
161+
label = isnothing(tr.node) ? "" : join(string.([tr.node...])))
162+
add_vertex!(pg; kwargs(t)...)
163+
queue = [Int[]]
164+
paths = Dict([Int[]=>1]) # path to vertex
165+
while !isempty(queue)
166+
curr = popfirst!(queue)
167+
subt = t[curr]
168+
# We ought print the index too, but graphviz renders edges in right order
169+
for (_,e) in enumerate(keys(subt.children))
170+
new_pth = [curr...,e]
171+
v = add_vertex!(pg; kwargs(t[new_pth])...)
172+
paths[new_pth] = v
173+
add_edge!(pg, paths[curr], v; label=string("$e"))
174+
push!(queue, new_pth)
175+
end
176+
end
177+
to_graphviz(pg)
178+
end
179+
146180
end

test/categorical_algebra/HomSearch.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,42 @@ end
147147
@test length(@acset_transformations x x begin V = Dict(1=>1) end monic = [:E]) == 2
148148
@test_throws ErrorException @acset_transformation g h begin V = [4,3,2,1]; E = [1,2,3,4] end
149149

150+
# Debug graph
151+
#------------
152+
@present SchTri <: SchGraph begin
153+
T::Ob
154+
(t1,t2,t3)::Hom(T,E)
155+
t1 src == t2 src
156+
t1 tgt == t3 tgt
157+
t2 src == t3 src
158+
end
159+
160+
@acset_type Tri(SchTri)
161+
162+
""" e₃
163+
2 ← 4
164+
e₁↑ ↖ ↓ e₄
165+
1 → 3
166+
e₂
167+
"""
168+
quad = @acset Tri begin V=4; E=5; T=2;
169+
src=[1,1,4,4,3]; tgt=[2,3,2,3,2];
170+
t1=[1,3]; t2=[2,4]; t3=[5,5]
171+
end
172+
173+
term = apex(terminal(Tri))
174+
175+
tri5 = @acset Tri begin
176+
V=2; E=3; T=5; src=[1,1,2]; tgt=[2,2,2]; t1=1; t2=2; t3=3
177+
end
178+
179+
tri = @acset Tri begin
180+
V=3; E=3; T=1; src=[1,1,2]; tgt=[3,2,3]; t1=1; t2=2; t3=3
181+
end
182+
183+
hs, t = debug_homomorphisms(tri, quad tri5; monic=false)
184+
@test length(hs) == length(homomorphisms(tri, quad tri5))
185+
# to_graphviz(t)
150186

151187
# Enumeration of subobjects
152188
###########################

0 commit comments

Comments
 (0)