Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require homomorphism to be unique #926

Merged
merged 6 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name = "Catlab"
uuid = "134e5e36-593f-5add-ad60-77f754baafbe"
license = "MIT"
authors = ["Evan Patterson <[email protected]>"]
version = "0.16.15"
version = "0.16.16"

[deps]
ACSets = "227ef7b5-1206-438b-ac65-934d6da304b8"
Expand Down
2 changes: 1 addition & 1 deletion docs/literate/graphics/graphviz_graphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ to_graphviz(g, node_attrs=Dict(:color => "cornflowerblue"),

using Catlab.CategoricalAlgebra

f = homomorphism(cycle_graph(Graph, 4), complete_graph(Graph, 2))
f = homomorphisms(cycle_graph(Graph, 4), complete_graph(Graph, 2)) |> first

# By default, the domain and codomain graph are both drawn, as well the vertex
# mapping between them.
Expand Down
2 changes: 1 addition & 1 deletion docs/literate/graphs/graphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ draw(id(K₃))
length(homomorphisms(T, esym))

# but we can use 3 colors to color T.
draw(homomorphism(T, K₃))
draw(homomorphism(T, K₃; any=true))

# ### Exercise:
# 1. Find a graph that is not 3-colorable
Expand Down
8 changes: 6 additions & 2 deletions src/categorical_algebra/CSets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,12 @@ end

function coerce_component(ob::Symbol, f::FinFunction{Int,Int},
dom_size::Int, codom_size::Int; kw...)
length(dom(f)) == dom_size || error("Domain error in component $ob")
# length(codom(f)) == codom_size || error("Codomain error in component $ob") # codom size is now Maxpart not nparts
if haskey(kw, :dom_parts)
!any(i -> f(i) == 0, kw[:dom_parts]) # check domain of mark as deleted
else
length(dom(f)) == dom_size # check domain of dense parts
end || error("Domain error in component $ob")
# length(codom(f)) == codom_size || error("Codomain error in component $ob")
kris-brown marked this conversation as resolved.
Show resolved Hide resolved
return f
end

Expand Down
2 changes: 1 addition & 1 deletion src/categorical_algebra/CatElements.jl
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function elements(f::ACSetTransformation)
end
pts = vcat([collect(f[o]).+off for (o, off) in zip(ob(S), offs)]...)
# *strict* ACSet transformation uniquely determined by its action on vertices
return only(homomorphisms(X, Y; initial=Dict([:El=>pts])))
return homomorphism(X, Y; initial=Dict([:El=>pts]))
end


Expand Down
129 changes: 84 additions & 45 deletions src/categorical_algebra/HomSearch.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ to infinite ``C``-sets when ``C`` is infinite (but possibly finitely presented).
"""
struct HomomorphismQuery <: ACSetHomomorphismAlgorithm end

""" Find a homomorphism between two attributed ``C``-sets.
""" Find a unique homomorphism between two attributed ``C``-sets (subject to a
variety of constraints), if one exists.

Returns `nothing` if no homomorphism exists. For many categories ``C``, the
``C``-set homomorphism problem is NP-complete and thus this procedure generally
Expand Down Expand Up @@ -94,17 +95,17 @@ In both of these cases, it's possible to compute homomorphisms when there are
the domain), as each such variable has a finite number of possibilities for it
to be mapped to.

Setting `any=true` relaxes the constraint that the returned homomorphism is
unique.

See also: [`homomorphisms`](@ref), [`isomorphism`](@ref).
"""
homomorphism(X::ACSet, Y::ACSet; alg=BacktrackingSearch(), kw...) =
homomorphism(X, Y, alg; kw...)

function homomorphism(X::ACSet, Y::ACSet, alg::BacktrackingSearch; kw...)
result = nothing
backtracking_search(X, Y; kw...) do α
result = α; return true
end
result
function homomorphism(X::ACSet, Y::ACSet, alg::BacktrackingSearch; any=false, kw...)
res = homomorphisms(X, Y, alg; Dict((any ? :take : :max) => 1)..., kw...)
isempty(res) ? nothing : only(res)
end

""" Find all homomorphisms between two attributed ``C``-sets.
Expand All @@ -115,10 +116,29 @@ homomorphisms exist, it is exactly as expensive.
homomorphisms(X::ACSet, Y::ACSet; alg=BacktrackingSearch(), kw...) =
homomorphisms(X, Y, alg; kw...)

function homomorphisms(X::ACSet, Y::ACSet, alg::BacktrackingSearch; kw...)
"""
kris-brown marked this conversation as resolved.
Show resolved Hide resolved
take = number of homomorphisms requested (stop the search process early if this
number is reached)
max = throw an error if we take more than this many morphisms (e.g. set max=1 if
one expects 0 or 1 morphism)
filter = only consider morphisms which meet some criteria, expressed as a Julia
function of type ACSetTransformation -> Bool

It does not make sense to specify both `take` and `max`.
"""
function homomorphisms(X::ACSet, Y::ACSet, alg::BacktrackingSearch;
take=nothing, max=nothing, filter=nothing, kw...)
results = []
backtracking_search(X, Y; kw...) do α
push!(results, map_components(deepcopy, α)); return false
isnothing(take) || isnothing(max) || error(
"Cannot set both `take`=$take and `max`=$max for `homomorphisms`")
backtracking_search(X, Y; kw...) do αs
for α in αs
isnothing(filter) || filter(α) || continue
length(results) == max && error("Exceeded $max: $([results; α])")
push!(results, map_components(deepcopy, α));
length(results) == take && return true
end
return false
end
results
end
Expand All @@ -132,7 +152,7 @@ is_homomorphic(X::ACSet, Y::ACSet; alg=BacktrackingSearch(), kw...) =
is_homomorphic(X, Y, alg; kw...)

is_homomorphic(X::ACSet, Y::ACSet, alg::BacktrackingSearch; kw...) =
!isnothing(homomorphism(X, Y, alg; kw...))
!isempty(homomorphisms(X, Y, alg; take=1, kw...))

""" Find an isomorphism between two attributed ``C``-sets, if one exists.

Expand All @@ -152,8 +172,8 @@ homomorphisms exist, it is exactly as expensive.
isomorphisms(X::ACSet, Y::ACSet; alg=BacktrackingSearch(), kw...) =
isomorphisms(X, Y, alg; kw...)

isomorphisms(X::ACSet, Y::ACSet, alg::BacktrackingSearch; initial=(;)) =
homomorphisms(X, Y, alg; iso=true, initial=initial)
isomorphisms(X::ACSet, Y::ACSet, alg::BacktrackingSearch; initial=(;), kw...) =
homomorphisms(X, Y, alg; iso=true, initial=initial, kw...)

""" Are the two attributed ``C``-sets isomorphic?

Expand All @@ -164,7 +184,7 @@ is_isomorphic(X::ACSet, Y::ACSet; alg=BacktrackingSearch(), kw...) =
is_isomorphic(X, Y, alg; kw...)

is_isomorphic(X::ACSet, Y::ACSet, alg::BacktrackingSearch; kw...) =
!isnothing(isomorphism(X, Y, alg; kw...))
!isempty(isomorphisms(X, Y, alg; take=1, kw...))

# Backtracking search
#--------------------
Expand Down Expand Up @@ -198,7 +218,6 @@ struct BacktrackingState{
predicates::Predicates
image::Image # Negative of image for epic components or if finding an epimorphism
unassigned::Unassign # "# of unassigned elems in domain of a component

end

function backtracking_search(f, X::ACSet, Y::ACSet;
Expand Down Expand Up @@ -304,44 +323,22 @@ function backtracking_search(f, X::ACSet, Y::ACSet;
backtracking_search(f, state, 1; random=random)
end

"""
Note: a successful search returns an *iterator* of solutions, rather than
a single solution. See `postprocess_res`.
"""
function backtracking_search(f, state::BacktrackingState, depth::Int;
random=false)
# Choose the next unassigned element.
mrv, mrv_elem = find_mrv_elem(state, depth)
if isnothing(mrv_elem)
# No unassigned elements remain, so we have a complete assignment.
if any(!=(identity), state.type_components)
return f(LooseACSetTransformation(
state.assignment, state.type_components, state.dom, state.codom))
return f([LooseACSetTransformation(
state.assignment, state.type_components, state.dom, state.codom)])
else
S = acset_schema(state.dom)
od = Dict{Symbol,Vector{Int}}(k=>(state.assignment[k]) for k in objects(S))

# Compute possible assignments for all free variables
free_data = map(attrtypes(S)) do k
monic = !isnothing(state.inv_assignment[k])
assigned = [v.val for (_, v) in state.assignment[k] if v isa AttrVar]
valid_targets = setdiff(parts(state.codom, k), monic ? assigned : [])
free_vars = findall(==(AttrVar(0)), last.(state.assignment[k]))
N = length(free_vars)
prod_iter = Iterators.product(fill(valid_targets, N)...)
if monic
prod_iter = Iterators.filter(x->length(x)==length(unique(x)), prod_iter)
end
(free_vars, prod_iter) # prod_iter = valid assignments for this attrtype
end

# Homomorphism for each element in the product of the prod_iters
for combo in Iterators.product(last.(free_data)...)
ad = Dict(map(zip(attrtypes(S), first.(free_data), combo)) do (k, xs, vs)
vec = last.(state.assignment[k])
vec[xs] = AttrVar.(collect(vs))
k => vec
end)
comps = merge(NamedTuple(od),NamedTuple(ad))
f(ACSetTransformation(comps, state.dom, state.codom))
end
return false
m = Dict(k=>!isnothing(v) for (k,v) in pairs(state.inv_assignment))
return f(postprocess_res(state.dom, state.codom, state.assignment, m))
end
elseif mrv == 0
# An element has no allowable assignment, so we must backtrack.
Expand Down Expand Up @@ -509,6 +506,48 @@ unassign_elem!(state::BacktrackingState{<:DynamicACSet}, depth, c, x) =
end
end

"""
A hom search result might not have all the data for an ACSetTransformation
explicitly specified. For example, if there is a cartesian product of possible
assignments which could not possibly constrain each other, then we should
iterate through this product at the very end rather than having the backtracking
search navigate the product space. Currently, this is only done with assignments
for floating attribute variables, but in principle this could be applied in the
future to, e.g., free-floating vertices of a graph or other coproducts of
representables.

This function takes a result assignment from backtracking search and returns an
iterator of the implicit set of homomorphisms that it specifies.
"""
function postprocess_res(dom, codom, assgn, monic)
kris-brown marked this conversation as resolved.
Show resolved Hide resolved
S = acset_schema(dom)
od = Dict{Symbol,Vector{Int}}(k=>(assgn[k]) for k in objects(S))

# Compute possible assignments for all free variables
free_data = map(attrtypes(S)) do k
assigned = [v.val for (_, v) in assgn[k] if v isa AttrVar]
valid_targets = setdiff(parts(codom, k), monic[k] ? assigned : [])
free_vars = findall(==(AttrVar(0)), last.(assgn[k]))
N = length(free_vars)
prod_iter = Iterators.product(fill(valid_targets, N)...)
if monic[k]
prod_iter = Iterators.filter(x->length(x)==length(unique(x)), prod_iter)
end
(free_vars, prod_iter) # prod_iter = valid assignments for this attrtype
end

# Homomorphism for each element in the product of the prod_iters
return Iterators.map(Iterators.product(last.(free_data)...) ) do combo
ad = Dict(map(zip(attrtypes(S), first.(free_data), combo)) do (k, xs, vs)
vec = last.(assgn[k])
vec[xs] = AttrVar.(collect(vs))
k => vec
end)
comps = merge(NamedTuple(od),NamedTuple(ad))
ACSetTransformation(comps, dom, codom)
end
end

# Macros
########

Expand Down
10 changes: 5 additions & 5 deletions test/categorical_algebra/CSets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ d = naturality_failures(β)
G = @acset Graph begin V=2; E=1; src=1; tgt=2 end
H = @acset Graph begin V=2; E=2; src=1; tgt=2 end
I = @acset Graph begin V=2; E=2; src=[1,2]; tgt=[1,2] end
f_ = homomorphism(G, H; monic=true)
f_ = homomorphism(G, H; monic=true, any=true)
g_ = homomorphism(H, G)
h_ = homomorphism(G, I)
h_ = homomorphism(G, I; initial=(V=[1,1],))
@test is_monic(f_)
@test !is_epic(f_)
@test !is_monic(g_)
Expand Down Expand Up @@ -689,7 +689,7 @@ rem_part!(X, :E, 2)
A = @acset WG{Symbol} begin V=1;E=2;Weight=1;src=1;tgt=1;weight=[AttrVar(1),:X] end
B = @acset WG{Symbol} begin V=1;E=2;Weight=1;src=1;tgt=1;weight=[:X, :Y] end
C = B ⊕ @acset WG{Symbol} begin V=1 end
AC = homomorphism(A,C)
AC = homomorphism(A,C; initial=(E=[1,1],))
BC = CSetTransformation(B,C; V=[1],E=[1,2], Weight=[:X])
@test all(is_natural,[AC,BC])
p1, p2 = product(A,A; cset=true);
Expand All @@ -701,8 +701,8 @@ g0, g1, g2 = WG{Symbol}.([2,3,2])
add_edges!(g0, [1,1,2], [1,2,2]; weight=[:X,:Y,:Z])
add_edges!(g1, [1,2,3], [2,3,3]; weight=[:Y,:Z,AttrVar(add_part!(g1,:Weight))])
add_edges!(g2, [1,2,2], [1,2,2]; weight=[AttrVar(add_part!(g2,:Weight)), :Z,:Z])
ϕ = only(homomorphisms(g1, g0)) |> CSetTransformation
ψ = only(homomorphisms(g2, g0; initial=(V=[1,2],))) |> CSetTransformation
ϕ = homomorphism(g1, g0) |> CSetTransformation
ψ = homomorphism(g2, g0; initial=(V=[1,2],)) |> CSetTransformation
@test is_natural(ϕ) && is_natural(ψ)
lim = pullback(ϕ, ψ)
@test nv(ob(lim)) == 3
Expand Down
4 changes: 2 additions & 2 deletions test/categorical_algebra/Chase.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ using Catlab.CategoricalAlgebra.Chase: egd, tgd, crel_type, pres_to_eds,
# ACSetTransformations as ACSets on the collage
###############################################

h = homomorphism(path_graph(Graph, 2), path_graph(Graph, 3))
h = homomorphism(path_graph(Graph, 2), path_graph(Graph, 3); initial=(V=[1,2],))
_, col = collage(h)
col[Symbol("α_V")] == h[:V] |> collect
col[Symbol("α_E")] == h[:E] |> collect
Expand Down Expand Up @@ -115,7 +115,7 @@ add_parts!(unique_l,:X,3);
add_parts!(unique_l,:x,2;src_x=[1,1],tgt_x=[2,3]);
add_parts!(unique_r,:X,2);
add_part!(unique_r,:x;src_x=1,tgt_x=2);
ED_unique = only(homomorphisms(unique_l, unique_r))
ED_unique = homomorphism(unique_l, unique_r)
add_part!(total_l,:X)
ED_total = ACSetTransformation(total_l, unique_r; X=[1])
# x-path of length 3 = x-path of length 2
Expand Down
29 changes: 22 additions & 7 deletions test/categorical_algebra/HomSearch.jl
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ set_subpart!(s3, :f, [20,10])
#Backtracking with monic and iso failure objects
g1, g2 = path_graph(Graph, 3), path_graph(Graph, 2)
rem_part!(g1,:E,2)
@test_throws ErrorException homomorphism(g1,g2;monic=true,error_failures=true)
@test_throws ErrorException homomorphism(g1, g2; monic=true, error_failures=true)

# Epic constraint
g0, g1, g2 = Graph(2), Graph(2), Graph(2)
Expand All @@ -95,6 +95,18 @@ add_edges!(g3,[1,3],[1,3]) # g3: ↻•→•→• ↺

@test length(homomorphisms(Graph(4),Graph(2); epic=true)) == 14 # 2^4 - 2

# taking a particular number of morphisms
@test length(homomorphisms(Graph(4),Graph(2); epic=true, take=7)) == 7

# throwing an error if max is exceeded
@test_throws ErrorException homomorphism(Graph(1), Graph(2))
@test_throws ErrorException length(homomorphisms(Graph(4),Graph(2); epic=true, max=6))
@test length(homomorphisms(Graph(4),Graph(2); epic=true, max=16)) == 14

# filtering morphisms
@test (length(homomorphisms(Graph(3),Graph(5); filter=is_monic))
== length(homomorphisms(Graph(3),Graph(5); monic=true)))

# Symmetric graphs
#-----------------

Expand All @@ -114,9 +126,9 @@ K₂, K₃ = complete_graph(SymmetricGraph, 2), complete_graph(SymmetricGraph, 3
C₅, C₆ = cycle_graph(SymmetricGraph, 5), cycle_graph(SymmetricGraph, 6)
@test !is_homomorphic(C₅, K₂)
@test is_homomorphic(C₅, K₃)
@test is_natural(homomorphism(C₅, K₃))
@test is_natural(homomorphism(C₅, K₃; any=true))
@test is_homomorphic(C₆, K₂)
@test is_natural(homomorphism(C₆, K₂))
@test is_natural(homomorphism(C₆, K₂; any=true))

# Labeled graphs
#---------------
Expand All @@ -140,7 +152,10 @@ hs = homomorphisms(K₆,K₆)
rand_hs = homomorphisms(K₆,K₆; random=true)
@test sort(hs,by=comps) == sort(rand_hs,by=comps) # equal up to order
@test hs != rand_hs # not equal given order
@test homomorphism(K₆,K₆) != homomorphism(K₆,K₆;random=true)

# This is very probably true
@test (homomorphism(K₆, K₆, any=true)
!= homomorphism(K₆ ,K₆; any=true, random=true))

# AttrVar constraints (monic and no_bind)
#----------------------------------------
Expand Down Expand Up @@ -358,10 +373,10 @@ exp = @acset WG begin V=3; E=1; src=1; tgt=2; weight=[false] end
const MADGraph = AbsMADGraph{Symbol}

v1, v2 = MADGraph.(1:2)
@test !is_isomorphic(v1,v2)
@test !is_isomorphic(v1, v2)
rem_part!(v2, :V, 1)
@test is_isomorphic(v1,v2)
@test is_isomorphic(v2,v1)
@test is_isomorphic(v1, v2)
@test is_isomorphic(v2, v1)


end # module
Loading