From fcd3de00eb17ab4d15b02b5c376843561127bb97 Mon Sep 17 00:00:00 2001 From: Kris Brown Date: Mon, 15 Jul 2024 14:33:41 -0700 Subject: [PATCH 1/6] refactor backtracking_search interface, `homomorphism` must be unique --- src/categorical_algebra/CSets.jl | 8 +- src/categorical_algebra/CatElements.jl | 2 +- src/categorical_algebra/HomSearch.jl | 129 ++++++++++++++++--------- test/categorical_algebra/CSets.jl | 10 +- test/categorical_algebra/Chase.jl | 4 +- test/categorical_algebra/HomSearch.jl | 29 ++++-- 6 files changed, 120 insertions(+), 62 deletions(-) diff --git a/src/categorical_algebra/CSets.jl b/src/categorical_algebra/CSets.jl index 6793fb89e..bda5fd750 100644 --- a/src/categorical_algebra/CSets.jl +++ b/src/categorical_algebra/CSets.jl @@ -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.func[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") return f end diff --git a/src/categorical_algebra/CatElements.jl b/src/categorical_algebra/CatElements.jl index adb7a53d1..6d8aa3b96 100644 --- a/src/categorical_algebra/CatElements.jl +++ b/src/categorical_algebra/CatElements.jl @@ -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 diff --git a/src/categorical_algebra/HomSearch.jl b/src/categorical_algebra/HomSearch.jl index d530474ba..53307333a 100644 --- a/src/categorical_algebra/HomSearch.jl +++ b/src/categorical_algebra/HomSearch.jl @@ -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 @@ -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. @@ -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...) +""" +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=-1, max=-1, filter=nothing, kw...) results = [] - backtracking_search(X, Y; kw...) do α - push!(results, map_components(deepcopy, α)); return false + take == -1 || max == -1 || 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 @@ -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. @@ -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? @@ -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 #-------------------- @@ -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; @@ -304,6 +323,10 @@ 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. @@ -311,37 +334,11 @@ function backtracking_search(f, state::BacktrackingState, depth::Int; 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. @@ -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) + 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 ######## diff --git a/test/categorical_algebra/CSets.jl b/test/categorical_algebra/CSets.jl index ccbe2e3c3..8fb299268 100644 --- a/test/categorical_algebra/CSets.jl +++ b/test/categorical_algebra/CSets.jl @@ -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_) @@ -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); @@ -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 diff --git a/test/categorical_algebra/Chase.jl b/test/categorical_algebra/Chase.jl index 76d1ad4da..f0f78ac0c 100644 --- a/test/categorical_algebra/Chase.jl +++ b/test/categorical_algebra/Chase.jl @@ -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 @@ -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 diff --git a/test/categorical_algebra/HomSearch.jl b/test/categorical_algebra/HomSearch.jl index 9483fa950..f3df1b507 100644 --- a/test/categorical_algebra/HomSearch.jl +++ b/test/categorical_algebra/HomSearch.jl @@ -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) @@ -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 #----------------- @@ -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 #--------------- @@ -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) #---------------------------------------- @@ -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 From 4e7392961576bd9c38883c871d1e5179013b0fbb Mon Sep 17 00:00:00 2001 From: Kris Brown Date: Mon, 15 Jul 2024 15:06:00 -0700 Subject: [PATCH 2/6] make `nothing` the default value --- docs/literate/graphics/graphviz_graphs.jl | 2 +- src/categorical_algebra/HomSearch.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/literate/graphics/graphviz_graphs.jl b/docs/literate/graphics/graphviz_graphs.jl index 6f46b58da..931d44154 100644 --- a/docs/literate/graphics/graphviz_graphs.jl +++ b/docs/literate/graphics/graphviz_graphs.jl @@ -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. diff --git a/src/categorical_algebra/HomSearch.jl b/src/categorical_algebra/HomSearch.jl index 53307333a..ea3a08154 100644 --- a/src/categorical_algebra/HomSearch.jl +++ b/src/categorical_algebra/HomSearch.jl @@ -127,9 +127,9 @@ filter = only consider morphisms which meet some criteria, expressed as a Julia It does not make sense to specify both `take` and `max`. """ function homomorphisms(X::ACSet, Y::ACSet, alg::BacktrackingSearch; - take=-1, max=-1, filter=nothing, kw...) + take=nothing, max=nothing, filter=nothing, kw...) results = [] - take == -1 || max == -1 || error( + 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 From c2ff9426bb75f41d4db76e69988412eaf3a9cd27 Mon Sep 17 00:00:00 2001 From: Kris Brown Date: Mon, 15 Jul 2024 15:15:28 -0700 Subject: [PATCH 3/6] any=true for docs example --- docs/literate/graphs/graphs.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate/graphs/graphs.jl b/docs/literate/graphs/graphs.jl index 39cccf345..155698b1b 100644 --- a/docs/literate/graphs/graphs.jl +++ b/docs/literate/graphs/graphs.jl @@ -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 From da6b62c7495007de8be3577319a41b258946ecce Mon Sep 17 00:00:00 2001 From: Kris Brown Date: Wed, 17 Jul 2024 01:08:17 -0700 Subject: [PATCH 4/6] more robust check of function value in coerce_component --- Project.toml | 2 +- src/categorical_algebra/CSets.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 34dfbd934..3a9fcdc7d 100644 --- a/Project.toml +++ b/Project.toml @@ -2,7 +2,7 @@ name = "Catlab" uuid = "134e5e36-593f-5add-ad60-77f754baafbe" license = "MIT" authors = ["Evan Patterson "] -version = "0.16.15" +version = "0.16.16" [deps] ACSets = "227ef7b5-1206-438b-ac65-934d6da304b8" diff --git a/src/categorical_algebra/CSets.jl b/src/categorical_algebra/CSets.jl index bda5fd750..05588b158 100644 --- a/src/categorical_algebra/CSets.jl +++ b/src/categorical_algebra/CSets.jl @@ -454,7 +454,7 @@ end function coerce_component(ob::Symbol, f::FinFunction{Int,Int}, dom_size::Int, codom_size::Int; kw...) if haskey(kw, :dom_parts) - !any(i -> f.func[i] == 0, kw[:dom_parts]) # check domain of mark as deleted + !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") From 66f1f40583efeaafd8a03ea50bf189257d719166 Mon Sep 17 00:00:00 2001 From: Kris Brown Date: Wed, 17 Jul 2024 09:44:56 -0700 Subject: [PATCH 5/6] coerce VarFuns to FinFuns for SubobjectHom as well --- src/categorical_algebra/CSets.jl | 5 +-- src/categorical_algebra/FinSets.jl | 2 +- test/categorical_algebra/CSets.jl | 50 +++++++++++++++++------------- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/categorical_algebra/CSets.jl b/src/categorical_algebra/CSets.jl index 05588b158..7eac16548 100644 --- a/src/categorical_algebra/CSets.jl +++ b/src/categorical_algebra/CSets.jl @@ -1123,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))) diff --git a/src/categorical_algebra/FinSets.jl b/src/categorical_algebra/FinSets.jl index a5d9866ec..68edf710f 100644 --- a/src/categorical_algebra/FinSets.jl +++ b/src/categorical_algebra/FinSets.jl @@ -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)) diff --git a/test/categorical_algebra/CSets.jl b/test/categorical_algebra/CSets.jl index 8fb299268..19db87963 100644 --- a/test/categorical_algebra/CSets.jl +++ b/test/categorical_algebra/CSets.jl @@ -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 #--------------------------------------------- From 940ea04712f339da7388284b267ee3957778c467 Mon Sep 17 00:00:00 2001 From: Kris Brown Date: Wed, 17 Jul 2024 11:08:30 -0700 Subject: [PATCH 6/6] more verbose names + comments --- src/categorical_algebra/CSets.jl | 6 +++--- src/categorical_algebra/HomSearch.jl | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/categorical_algebra/CSets.jl b/src/categorical_algebra/CSets.jl index 7eac16548..d26894a90 100644 --- a/src/categorical_algebra/CSets.jl +++ b/src/categorical_algebra/CSets.jl @@ -451,6 +451,7 @@ 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...) if haskey(kw, :dom_parts) @@ -458,7 +459,6 @@ function coerce_component(ob::Symbol, f::FinFunction{Int,Int}, 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") return f end @@ -476,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 diff --git a/src/categorical_algebra/HomSearch.jl b/src/categorical_algebra/HomSearch.jl index ea3a08154..35737c432 100644 --- a/src/categorical_algebra/HomSearch.jl +++ b/src/categorical_algebra/HomSearch.jl @@ -116,7 +116,8 @@ homomorphisms exist, it is exactly as expensive. homomorphisms(X::ACSet, Y::ACSet; alg=BacktrackingSearch(), kw...) = homomorphisms(X, Y, alg; 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 @@ -325,7 +326,7 @@ end """ Note: a successful search returns an *iterator* of solutions, rather than -a single solution. See `postprocess_res`. +a single solution. See `postprocess_search_results`. """ function backtracking_search(f, state::BacktrackingState, depth::Int; random=false) @@ -338,7 +339,7 @@ function backtracking_search(f, state::BacktrackingState, depth::Int; state.assignment, state.type_components, state.dom, state.codom)]) else m = Dict(k=>!isnothing(v) for (k,v) in pairs(state.inv_assignment)) - return f(postprocess_res(state.dom, state.codom, state.assignment, m)) + 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. @@ -519,7 +520,7 @@ 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) +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))