Skip to content

Commit 7e5e1ff

Browse files
author
Kris Brown
committed
debug homomorphisms
1 parent 3bed069 commit 7e5e1ff

3 files changed

Lines changed: 153 additions & 24 deletions

File tree

src/categorical_algebra/CSets.jl

Lines changed: 91 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export ACSetTransformation, CSetTransformation, StructACSetTransformation,
77
ACSetHomomorphismAlgorithm, BacktrackingSearch, HomomorphismQuery,
88
components, type_components, force,
99
naturality_failures, show_naturality_failures, is_natural,
10-
homomorphism, homomorphisms, is_homomorphic,
10+
homomorphism, homomorphisms, debug_homomorphisms, is_homomorphic,
1111
isomorphism, isomorphisms, is_isomorphic,
1212
@acset_transformation, @acset_transformations,
1313
subobject_graph, partial_overlaps, maximum_common_subobject
@@ -766,7 +766,7 @@ homomorphism(X::ACSet, Y::ACSet; alg=BacktrackingSearch(), kw...) =
766766
function homomorphism(X::ACSet, Y::ACSet, alg::BacktrackingSearch; kw...)
767767
result = nothing
768768
backtracking_search(X, Y; kw...) do α
769-
result = α; return true
769+
result = get_hom(α); return true
770770
end
771771
result
772772
end
@@ -782,11 +782,19 @@ homomorphisms(X::ACSet, Y::ACSet; alg=BacktrackingSearch(), kw...) =
782782
function homomorphisms(X::ACSet, Y::ACSet, alg::BacktrackingSearch; kw...)
783783
results = []
784784
backtracking_search(X, Y; kw...) do α
785-
push!(results, map_components(deepcopy, α)); return false
785+
push!(results, map_components(deepcopy, get_hom(α))); return false
786786
end
787787
results
788788
end
789789

790+
function debug_homomorphisms(X::ACSet, Y::ACSet; kw...)
791+
results = []
792+
m = backtracking_search(X, Y; debug=true, kw...) do α
793+
push!(results, map_components(deepcopy, get_hom(α))); return false
794+
end
795+
results => m.debug
796+
end
797+
790798
""" Is the first attributed ``C``-set homomorphic to the second?
791799
792800
This function generally reduces to [`homomorphism`](@ref) but certain algorithms
@@ -844,6 +852,53 @@ partial_assignments(x::AbstractVector; is_attr=false) =
844852
in_hom(S, c) = [dom(S,f) => f for f in hom(S) if codom(S,f) == c]
845853
out_hom(S, c) = [f => codom(S,f) for f in hom(S) if dom(S,f) == c]
846854

855+
"""Keep track of progress through backtracking homomorphism search."""
856+
mutable struct BacktrackingTree
857+
node::Union{Nothing,Pair{Symbol,Int}}
858+
success::Bool
859+
asgn::NamedTuple
860+
children::OrderedDict{Int,BacktrackingTree}
861+
BacktrackingTree() = new(nothing, false, (;), OrderedDict{Int,BacktrackingTree}())
862+
end
863+
"""A backtracking tree plus a pointer to a node in the tree"""
864+
struct BacktrackingTreePt
865+
t::BacktrackingTree
866+
curr::Vector{Int}
867+
BacktrackingTreePt() = new(BacktrackingTree(),Int[])
868+
end
869+
function Base.push!(tc::BacktrackingTreePt, c::Symbol, x::Int, y::Int, asgn)
870+
t = tc.t[tc.curr]
871+
t.node = c => x
872+
t.children[y] = BacktrackingTree()
873+
t.children[y].asgn = deepcopy(asgn)
874+
push!(tc.curr, y)
875+
return true
876+
end
877+
function Base.delete!(tc::BacktrackingTreePt, c::Symbol, x::Int, y::Int)
878+
t = tc.t[tc.curr[1:end-1]]
879+
t.node == (c=>x) || error("Bad remove $c#$x->$y")
880+
pop!(tc.curr)
881+
end
882+
function success(tc::BacktrackingTreePt)
883+
tc.t[tc.curr].success = true
884+
end
885+
function Base.show(io::IO, t::BacktrackingTree)
886+
if !isnothing(t.node)
887+
print(io,"{"); print(io, t.node[1]); print(io, t.node[2]); print(io,"}");
888+
end
889+
print(io, "[")
890+
for (k,v) in collect(t.children)
891+
print(io, k); print(io, v); print(io, ",")
892+
end
893+
if !isempty(t.children) print(io,"\b") end
894+
print(io,"]")
895+
end
896+
function Base.getindex(t::BacktrackingTree, curr::Vector{Int})
897+
for c in curr
898+
t = t.children[c]
899+
end
900+
t
901+
end
847902

848903
""" Internal state for backtracking search for ACSet homomorphisms.
849904
@@ -862,11 +917,27 @@ struct BacktrackingState{
862917
dom::Dom
863918
codom::Codom
864919
type_components::LooseFun
920+
debug::Union{Nothing,BacktrackingTreePt}
921+
end
922+
923+
"""Extract an ACSetTransformation from BacktrackingState"""
924+
function get_hom(state::BacktrackingState)
925+
if any(!=(identity), state.type_components)
926+
return LooseACSetTransformation(
927+
state.assignment, state.type_components, state.dom, state.codom)
928+
else
929+
S = acset_schema(state.dom)
930+
od = Dict{Symbol,Vector{Int}}(k=>(state.assignment[k]) for k in objects(S))
931+
ad = Dict(k=>last.(state.assignment[k]) for k in attrtypes(S))
932+
comps = merge(NamedTuple(od),NamedTuple(ad))
933+
return ACSetTransformation(comps, state.dom, state.codom)
934+
end
865935
end
866936

937+
867938
function backtracking_search(f, X::ACSet, Y::ACSet;
868939
monic=false, iso=false, random=false,
869-
type_components=(;), initial=(;), error_failures=false)
940+
type_components=(;), initial=(;), error_failures=false, debug=false)
870941
S, Sy = acset_schema.([X,Y])
871942
S == Sy || error("Schemas must match for morphism search")
872943
Ob = Tuple(objects(S))
@@ -922,7 +993,8 @@ function backtracking_search(f, X::ACSet, Y::ACSet;
922993
loosefuns = NamedTuple{Attr}(
923994
isnothing(type_components) ? identity : get(type_components, c, identity) for c in Attr)
924995
state = BacktrackingState(assignment, assignment_depth,
925-
inv_assignment, X, Y, loosefuns)
996+
inv_assignment, X, Y, loosefuns,
997+
debug ? BacktrackingTreePt() : nothing)
926998

927999
# Make any initial assignments, failing immediately if inconsistent.
9281000
for (c, c_assignments) in pairs(initial)
@@ -937,39 +1009,31 @@ function backtracking_search(f, X::ACSet, Y::ACSet;
9371009
end
9381010
end
9391011
# Start the main recursion for backtracking search.
940-
backtracking_search(f, state, 1; random=random)
1012+
backtracking_search(f, state, 1; random=random, toplevel=true)
9411013
end
9421014

9431015
function backtracking_search(f, state::BacktrackingState, depth::Int;
944-
random=false)
1016+
random=false, toplevel=false)
9451017
# Choose the next unassigned element.
9461018
mrv, mrv_elem = find_mrv_elem(state, depth)
9471019
if isnothing(mrv_elem)
948-
# No unassigned elements remain, so we have a complete assignment.
949-
if any(!=(identity), state.type_components)
950-
return f(LooseACSetTransformation(
951-
state.assignment, state.type_components, state.dom, state.codom))
952-
else
953-
S = acset_schema(state.dom)
954-
od = Dict{Symbol,Vector{Int}}(k=>(state.assignment[k]) for k in objects(S))
955-
ad = Dict(k=>last.(state.assignment[k]) for k in attrtypes(S))
956-
comps = merge(NamedTuple(od),NamedTuple(ad))
957-
return f(ACSetTransformation(comps, state.dom, state.codom))
958-
end
1020+
isnothing(state.debug) || success(state.debug)
1021+
return f(state)
9591022
elseif mrv == 0
9601023
# An element has no allowable assignment, so we must backtrack.
9611024
return false
9621025
end
963-
c, x = mrv_elem
1026+
c, x, ys = mrv_elem
9641027

9651028
# Attempt all assignments of the chosen element.
966-
Y = state.codom
967-
for y in (random ? shuffle : identity)(parts(Y, c))
1029+
for y in (random ? shuffle : identity)(ys)
9681030
(assign_elem!(state, depth, c, x, y)
1031+
&& (isnothing(state.debug) ? true : push!(state.debug, c, x, y, state.assignment))
9691032
&& backtracking_search(f, state, depth + 1)) && return true
9701033
unassign_elem!(state, depth, c, x)
1034+
isnothing(state.debug) || delete!(state.debug, c, x, state.assignment[c][x])
9711035
end
972-
return false
1036+
return toplevel ? state : false # return state to recover debug tree
9731037
end
9741038

9751039
""" Find an unassigned element having the minimum remaining values (MRV).
@@ -980,9 +1044,12 @@ function find_mrv_elem(state::BacktrackingState, depth)
9801044
Y = state.codom
9811045
for c in ob(S), (x, y) in enumerate(state.assignment[c])
9821046
y == 0 || continue
983-
n = count(can_assign_elem(state, depth, c, x, y) for y in parts(Y, c))
1047+
ys = filter(parts(Y,c)) do y
1048+
can_assign_elem(state, depth, c, x, y)
1049+
end
1050+
n = length(ys)
9841051
if n < mrv
985-
mrv, mrv_elem = n, (c, x)
1052+
mrv, mrv_elem = n, (c, x, ys)
9861053
end
9871054
end
9881055
(mrv, mrv_elem)

src/graphics/GraphvizCategories.jl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module GraphvizCategories
44
export to_graphviz, to_graphviz_property_graph
55

66
using ...GATs, ...Theories, ...CategoricalAlgebra, ...Graphs, ..GraphvizGraphs
7+
using ...CategoricalAlgebra.CSets: BacktrackingTree, BacktrackingTreePt
78
import ..Graphviz
89
import ..GraphvizGraphs: to_graphviz, to_graphviz_property_graph
910

@@ -142,4 +143,37 @@ function to_graphviz(f::FinFunction{Int,Int}; kw...)
142143
to_graphviz(g; kw...)
143144
end
144145

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

test/categorical_algebra/CSets.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,34 @@ end
551551
@test length(@acset_transformations x x begin V = Dict(1=>1) end monic = [:E]) == 2
552552
@test_throws ErrorException @acset_transformation g h begin V = [4,3,2,1]; E = [1,2,3,4] end
553553

554+
# Debug graph
555+
#------------
556+
@present SchTri <: SchGraph begin
557+
T::Ob
558+
(t1,t2,t3)::Hom(T,E)
559+
t1 src == t2 src
560+
t1 tgt == t3 tgt
561+
t2 src == t3 src
562+
end
563+
@acset_type Tri(SchTri)
564+
""" e₃
565+
2 ← 4
566+
e₁↑ ↖ ↓ e₄
567+
1 → 3
568+
e₂
569+
"""
570+
quad = @acset Tri begin V=4; E=5; T=2;
571+
src=[1,1,4,4,3]; tgt=[2,3,2,3,2];
572+
t1=[1,3]; t2=[2,4]; t3=[5,5]
573+
end
574+
term = apex(terminal(Tri))
575+
tri5 = @acset Tri begin V=2; E=3; T=5; src=[1,1,2]; tgt=[2,2,2]; t1=1; t2=2; t3=3 end
576+
tri = @acset Tri begin V=3; E=3; T=1; src=[1,1,2]; tgt=[3,2,3]; t1=1; t2=2; t3=3 end
577+
homomorphisms(tri,quadtri5)
578+
579+
hs, t = debug_homomorphisms(tri,quadtri5; monic=false)
580+
@test length(hs) == length(homomorphisms(tri,quadtri5))
581+
554582
# Sub-C-sets
555583
############
556584

0 commit comments

Comments
 (0)