Skip to content

Commit

Permalink
Merge pull request #926 from AlgebraicJulia/unique_hom
Browse files Browse the repository at this point in the history
Require `homomorphism` to be unique
  • Loading branch information
epatters authored Jul 17, 2024
2 parents c59feeb + 940ea04 commit cd11d31
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 92 deletions.
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
17 changes: 11 additions & 6 deletions src/categorical_algebra/CSets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -451,10 +451,14 @@ function coerce_components(S, components, X::ACSet{<:PT}, Y) where PT
return merge(ocomps, acomps)
end

# Enforces that function has a valid domain (but not necessarily codomain)
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")
return f
end

Expand All @@ -472,8 +476,8 @@ end
function coerce_attrvar_component(
ob::Symbol, f::VarFunction,::TypeSet{T},::TypeSet{T},
dom_size::Int, codom_size::Int; kw...) where {T}
# length(dom(f.fun)) == dom_size || error("Domain error in component $ob: $(dom(f.fun))!=$dom_size")
length(f.codom) == codom_size || error("Codomain error in component $ob: $(f.fun.codom)!=$codom_size")
length(f.codom) == codom_size || error(
"Codomain error in component $ob: $(f.fun.codom)!=$codom_size")
return f
end

Expand Down Expand Up @@ -1119,9 +1123,10 @@ end
const SubCSet{S} = Subobject{<:StructCSet{S}}
const SubACSet{S} = Subobject{<:StructACSet{S}}

# Componentwise subobjects
# Componentwise subobjects: coerce VarFunctions to FinFunctions
components(A::SubACSet{S}) where S =
NamedTuple(k => Subobject(vs) for (k,vs) in pairs(components(hom(A)))
NamedTuple(k => Subobject(k ob(S) ? vs : FinFunction(vs)) for (k,vs) in
pairs(components(hom(A)))
)

force(A::SubACSet) = Subobject(force(hom(A)))
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
2 changes: 1 addition & 1 deletion src/categorical_algebra/FinSets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ VarFunction(f::AbstractVector{Int},cod::Int) = VarFunction(FinFunction(f,cod))
VarFunction(f::FinDomFunction) = VarFunction{Union{}}(AttrVar.(collect(f)),codom(f))
VarFunction{T}(f::FinDomFunction,cod::FinSet) where T = VarFunction{T}(collect(f),cod)
FinFunction(f::VarFunction{T}) where T = FinFunction(
[f.fun(i) isa AttrVar ? f.fun(i).val : error("Cannot cast to FinFunction")
[f(i) isa AttrVar ? f(i).val : error("Cannot cast to FinFunction")
for i in dom(f)], f.codom)
FinDomFunction(f::VarFunction{T}) where T = f.fun
Base.length(f::AbsVarFunction{T}) where T = length(collect(f.fun))
Expand Down
130 changes: 85 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,30 @@ 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...)
""" Find all homomorphisms between two attributed ``C``-sets via BackTracking Search.
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 +153,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 +173,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 +185,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 +219,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 +324,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_search_results`.
"""
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_search_results(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 +507,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_search_results(dom, codom, assgn, monic)
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
60 changes: 33 additions & 27 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 Expand Up @@ -733,29 +733,35 @@ X = @acset VES begin V=6; E=5; Label=5
src=[1,2,3,4,4]; tgt=[3,3,4,5,6];
vlabel=[:a,:b,:c,:d,:e,:f]; elabel=AttrVar.(1:5)
end
A, B = Subobject(X, V=1:4, E=1:3, Label=1:3), Subobject(X, V=3:6, E=3:5, Label=3:5)
@test A B |> force == Subobject(X, V=3:4, E=3:3, Label=3:3) |> force
expected = @acset VES begin V=2; E=1; Label=1;
src=1; tgt=2; vlabel=[:c,:d]; elabel=[AttrVar(1)]
end
@test is_isomorphic(dom(hom(A B )), expected)
@test A B |> force == Subobject(X, V=1:6, E=1:5, Label=1:5) |> force
@test (X) |> force == A B |> force
@test (X) |> force == Subobject(X, V=1:0, E=1:0, Label=1:0) |> force
@test force(implies(A, B)) == force(¬(A) B)
@test ¬(A B) == ¬(A) ¬(B)
@test ¬(A B) != ¬(A) B
@test (A implies(A,B)) == B (A implies(A,B))
@test (B implies(B,A)) == A (B implies(B,A))
@test ¬(A (¬B)) == ¬(A) ¬(¬(B))
@test ¬(A (¬B)) == ¬(A) B
@test A ¬(¬(A)) == ¬(¬(A))
@test implies((AB), A) == AB
@test dom(hom(subtract(A,B))) == @acset VES begin V=3; E=2; Label=2
src=[1,2]; tgt=3; vlabel=[:a,:b,:c]; elabel=AttrVar.(1:2)
end

@test nv(dom(hom(~A))) == 3
A′ = Subobject(X, V=1:4, E=1:3, Label=1:3) # component-wise representation
B′ = Subobject(X, V=3:6, E=3:5, Label=3:5)
A′′, B′′ = Subobject.(hom.([A′,B′])) # hom representation

for (A,B) in [A′=>B′, A′′ =>B′′]
@test A B |> force == Subobject(X, V=3:4, E=3:3, Label=3:3) |> force
expected = @acset VES begin V=2; E=1; Label=1;
src=1; tgt=2; vlabel=[:c,:d]; elabel=[AttrVar(1)]
end
@test is_isomorphic(dom(hom(A B )), expected)
@test A B |> force == Subobject(X, V=1:6, E=1:5, Label=1:5) |> force
@test (X) |> force == A B |> force
@test (X) |> force == Subobject(X, V=1:0, E=1:0, Label=1:0) |> force
@test force(implies(A, B)) == force(¬(A) B)
@test ¬(A B) == ¬(A) ¬(B)
@test ¬(A B) != ¬(A) B
@test (A implies(A,B)) == B (A implies(A,B))
@test (B implies(B,A)) == A (B implies(B,A))
@test ¬(A (¬B)) == ¬(A) ¬(¬(B))
@test ¬(A (¬B)) == ¬(A) B
@test A ¬(¬(A)) == ¬(¬(A))
@test implies((AB), A) == AB
@test dom(hom(subtract(A,B))) == @acset VES begin V=3; E=2; Label=2
src=[1,2]; tgt=3; vlabel=[:a,:b,:c]; elabel=AttrVar.(1:2)
end

@test nv(dom(hom(~A))) == 3
end

# Limits of CSetTransformations between ACSets
#---------------------------------------------
Expand Down
Loading

0 comments on commit cd11d31

Please sign in to comment.