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 all 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
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((A∧B), A) == A∨B
@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((A∧B), A) == A∨B
@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
Loading