Browse Source

Merge pull request #421 from epatters/benchmark-graph-homs

Benchmark counting triangles in a graph
pull/424/head
Evan Patterson 2 weeks ago
committed by GitHub
parent
commit
3fc4ccfda3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 55
      benchmark/Graphs.jl
  2. 106
      src/categorical_algebra/CSets.jl
  3. 15
      src/graphs/GraphGenerators.jl
  4. 24
      src/wiring_diagrams/Algebras.jl
  5. 15
      test/graphs/GraphGenerators.jl
  6. 6
      test/wiring_diagrams/Algebras.jl

55
benchmark/Graphs.jl

@ -8,6 +8,8 @@ const LG, MG = LightGraphs, MetaGraphs
using Catlab, Catlab.CategoricalAlgebra, Catlab.Graphs
using Catlab.Graphs.BasicGraphs: TheoryGraph
using Catlab.WiringDiagrams: query
using Catlab.Programs: @relation
# Helpers
#########
@ -60,11 +62,35 @@ end
@inline Graphs.has_edge(g::LG.AbstractGraph, args...) = LG.has_edge(g, args...)
@inline Graphs.neighbors(g::LG.AbstractGraph, args...) = LG.neighbors(g, args...)
function lg_connected_components_projection(g)
function Graphs.connected_component_projection(g::LG.AbstractGraph)
label = Vector{Int}(undef, LG.nv(g))
LG.connected_components!(label, g)
end
abstract type FindTrianglesAlgorithm end
struct TriangleHomomorphism <: FindTrianglesAlgorithm end
struct TriangleQuery <: FindTrianglesAlgorithm end
""" Number of triangles in a graph.
"""
function ntriangles(g::T, ::TriangleHomomorphism) where T
triangle = T(3)
add_edges!(triangle, [1,2,1], [2,3,3])
count = 0
homomorphisms(triangle, g) do α;
count += 1; return false
end
count
end
function ntriangles(g, ::TriangleQuery)
length(query(g, ntriangles_query))
end
const ntriangles_query = @relation (;) begin
E(src=v1, tgt=v2)
E(src=v2, tgt=v3)
E(src=v1, tgt=v3)
end
# Graphs
########
@ -75,7 +101,6 @@ lgbench = bench["LightGraphs"] = BenchmarkGroup()
n = 10000
clbench["make-path"] = @benchmarkable path_graph(Graph,n)
lgbench["make-path"] = @benchmarkable begin
g = LG.DiGraph()
LG.add_vertices!(g, n)
@ -102,7 +127,7 @@ lg = LG.DiGraph(g)
clbench["path-graph-components"] =
@benchmarkable connected_component_projection($g)
lgbench["path-graph-components"] =
@benchmarkable lg_connected_components_projection($lg)
@benchmarkable connected_component_projection($lg)
g₀ = star_graph(Graph, n₀)
g = ob(coproduct(fill(g₀, 5)))
@ -110,7 +135,15 @@ lg = LG.DiGraph(g)
clbench["star-graph-components"] =
@benchmarkable connected_component_projection($g)
lgbench["star-graph-components"] =
@benchmarkable lg_connected_components_projection($lg)
@benchmarkable connected_component_projection($lg)
n = 100
g = wheel_graph(Graph, n)
lg = LG.DiGraph(g)
clbench["wheel-graph-triangles-hom"] =
@benchmarkable ntriangles($g, TriangleHomomorphism())
clbench["wheel-graph-triangles-query"] =
@benchmarkable ntriangles($g, TriangleQuery())
# Symmetric graphs
##################
@ -122,7 +155,6 @@ lgbench = bench["LightGraphs"] = BenchmarkGroup()
n = 10000
clbench["make-path"] = @benchmarkable path_graph(SymmetricGraph, n)
lgbench["make-path"] = @benchmarkable begin
g = LG.Graph()
LG.add_vertices!(g, n)
@ -149,7 +181,7 @@ lg = LG.Graph(g)
clbench["path-graph-components"] =
@benchmarkable connected_component_projection($g)
lgbench["path-graph-components"] =
@benchmarkable lg_connected_components_projection($lg)
@benchmarkable connected_component_projection($lg)
g₀ = star_graph(SymmetricGraph, n₀)
g = ob(coproduct(fill(g₀, 5)))
@ -157,7 +189,16 @@ lg = LG.Graph(g)
clbench["star-graph-components"] =
@benchmarkable connected_component_projection($g)
lgbench["star-graph-components"] =
@benchmarkable lg_connected_components_projection($lg)
@benchmarkable connected_component_projection($lg)
n = 100
g = wheel_graph(SymmetricGraph, n)
lg = LG.Graph(g)
clbench["wheel-graph-triangles-hom"] =
@benchmarkable ntriangles($g, TriangleHomomorphism())
clbench["wheel-graph-triangles-query"] =
@benchmarkable ntriangles($g, TriangleQuery())
lgbench["wheel-graph-triangles"] = @benchmarkable sum(LG.triangles($lg))
# Weighted graphs
#################

106
src/categorical_algebra/CSets.jl

@ -6,6 +6,7 @@ export ACSetTransformation, CSetTransformation, components, force, is_natural,
isomorphism, isomorphisms, is_isomorphic, migrate!,
generate_json_acset, parse_json_acset, read_json_acset, write_json_acset
using Base.Meta: quot
using AutoHashEquals
using JSON
using Reexport
@ -297,10 +298,10 @@ function backtracking_search(f, state::BacktrackingState, depth::Int)
# Attempt all assignments of the chosen element.
Y = state.codom
for y in parts(Y, c)
assign_elem!(state, depth, c, x, y) &&
assign_elem!(state, depth, Val{c}, x, y) &&
backtracking_search(f, state, depth + 1) &&
return true
unassign_elem!(state, depth, c, x)
unassign_elem!(state, depth, Val{c}, x)
end
return false
end
@ -312,7 +313,7 @@ function find_mrv_elem(state::BacktrackingState{CD}, depth) where CD
Y = state.codom
for c in ob(CD), (x, y) in enumerate(state.assignment[c])
y == 0 || continue
n = count(can_assign_elem(state, depth, c, x, y) for y in parts(Y, c))
n = count(can_assign_elem(state, depth, Val{c}, x, y) for y in parts(Y, c))
if n < mrv
mrv, mrv_elem = n, (c, x)
end
@ -322,15 +323,16 @@ end
""" Check whether element (c,x) can be assigned to (c,y) in current assignment.
"""
function can_assign_elem(state::BacktrackingState, depth, c, x, y)
function can_assign_elem(state::BacktrackingState, depth,
::Type{Val{c}}, x, y) where c
# Although this method is nonmutating overall, we must temporarily mutate the
# backtracking state, for several reasons. First, an assignment can be a
# consistent at each individual subpart but not consistent for all subparts
# simultaneously (consider trying to assign a self-loop to an edge with
# distinct vertices). Moreover, in schemas with non-trivial endomorphisms, we
# must keep track of which elements we have visited to avoid looping forever.
ok = assign_elem!(state, depth, c, x, y)
unassign_elem!(state, depth, c, x)
ok = assign_elem!(state, depth, Val{c}, x, y)
unassign_elem!(state, depth, Val{c}, x)
return ok
end
@ -339,62 +341,64 @@ end
Returns whether the assignment succeeded. Note that the backtracking state can
be mutated even when the assignment fails.
"""
function assign_elem!(state::BacktrackingState{CD,AD}, depth, c, x, y) where {CD, AD}
y′ = state.assignment[c][x]
y′ == y && return true # If x is already assigned to y, return immediately.
y′ == 0 || return false # Otherwise, x must be unassigned.
if !isnothing(state.inv_assignment) && state.inv_assignment[c][y] != 0
# Also, y must unassigned in the inverse assignment.
return false
end
@generated function assign_elem!(state::BacktrackingState{CD,AD}, depth,
::Type{Val{c}}, x, y) where {CD, AD, c}
out_hom = [ f => codom(CD, f) for f in hom(CD) if dom(CD, f) == c ]
out_attr = [ f for f in attr(AD) if dom(AD, f) == c ]
quote
y′ = state.assignment.$c[x]
y′ == y && return true # If x is already assigned to y, return immediately.
y′ == 0 || return false # Otherwise, x must be unassigned.
if !isnothing(state.inv_assignment) && state.inv_assignment.$c[y] != 0
# Also, y must unassigned in the inverse assignment.
return false
end
# Check attributes first to fail as quickly as possible.
X, Y = state.dom, state.codom
for f in out_attr(AD, Val{c})
subpart(X,x,f) == subpart(Y,y,f) || return false
end
# Check attributes first to fail as quickly as possible.
X, Y = state.dom, state.codom
$(map(out_attr) do f
:(subpart(X,x,$(quot(f))) == subpart(Y,y,$(quot(f))) || return false)
end...)
# Make the assignment and recursively assign subparts.
state.assignment[c][x] = y
state.assignment_depth[c][x] = depth
if !isnothing(state.inv_assignment)
state.inv_assignment[c][y] = x
end
for (f, d) in out_hom(CD, Val{c})
assign_elem!(state, depth, d,
subpart(X,x,f), subpart(Y,y,f)) || return false
# Make the assignment and recursively assign subparts.
state.assignment.$c[x] = y
state.assignment_depth.$c[x] = depth
if !isnothing(state.inv_assignment)
state.inv_assignment.$c[y] = x
end
$(map(out_hom) do (f, d)
:(assign_elem!(state, depth, Val{$(quot(d))}, subpart(X,x,$(quot(f))),
subpart(Y,y,$(quot(f)))) || return false)
end...)
return true
end
return true
end
""" Unassign the element (c,x) in the current assignment.
"""
function unassign_elem!(state::BacktrackingState{CD}, depth, c, x) where CD
state.assignment[c][x] == 0 && return
assign_depth = state.assignment_depth[c][x]
@assert assign_depth <= depth
if assign_depth == depth
X = state.dom
if !isnothing(state.inv_assignment)
y = state.assignment[c][x]
state.inv_assignment[c][y] = 0
end
state.assignment[c][x] = 0
state.assignment_depth[c][x] = 0
for (f, d) in out_hom(CD, Val{c})
unassign_elem!(state, depth, d, subpart(X,x,f))
@generated function unassign_elem!(state::BacktrackingState{CD}, depth,
::Type{Val{c}}, x) where {CD, c}
out_hom = [ f => codom(CD, f) for f in hom(CD) if dom(CD, f) == c ]
quote
state.assignment.$c[x] == 0 && return
assign_depth = state.assignment_depth.$c[x]
@assert assign_depth <= depth
if assign_depth == depth
X = state.dom
if !isnothing(state.inv_assignment)
y = state.assignment.$c[x]
state.inv_assignment.$c[y] = 0
end
state.assignment.$c[x] = 0
state.assignment_depth.$c[x] = 0
$(map(out_hom) do (f, d)
:(unassign_elem!(state, depth, Val{$(quot(d))},
subpart(X,x,$(quot(f)))))
end...)
end
end
end
@generated function out_hom(::Type{CD}, ::Type{Val{c}}) where {CD<:CatDesc, c}
Expr(:tuple, (:($(QuoteNode(f)) => $(QuoteNode(codom(CD, f))))
for f in hom(CD) if dom(CD, f) == c)...)
end
@generated function out_attr(::Type{AD}, ::Type{Val{c}}) where {AD<:AttrDesc, c}
Expr(:tuple, (QuoteNode(f) for f in attr(AD) if dom(AD, f) == c)...)
end
# Category of C-sets
####################

15
src/graphs/GraphGenerators.jl

@ -1,5 +1,5 @@
module GraphGenerators
export path_graph, cycle_graph, complete_graph, star_graph
export path_graph, cycle_graph, complete_graph, star_graph, wheel_graph
using ...CSetDataStructures, ..BasicGraphs
using ...CSetDataStructures: hom
@ -38,6 +38,8 @@ function complete_graph(::Type{T}, n::Int; V=(;)) where T <: AbstractACSet
end
""" Star graph on ``n`` vertices.
In the directed case, the edges point outward.
"""
function star_graph(::Type{T}, n::Int) where T <: AbstractACSet
g = T()
@ -46,6 +48,17 @@ function star_graph(::Type{T}, n::Int) where T <: AbstractACSet
g
end
""" Wheel graph on ``n`` vertices.
In the directed case, the outer cycle is directed and the spokes point outward.
"""
function wheel_graph(::Type{T}, n::Int) where T <: AbstractACSet
g = cycle_graph(T, n-1)
add_vertex!(g)
add_edges!(g, fill(n,n-1), 1:(n-1))
g
end
# Should this be exported from `BasicGraphs`?
@generated is_directed(::Type{T}) where {CD, T<:AbstractACSet{CD}} =
!(:inv in hom(CD))

24
src/wiring_diagrams/Algebras.jl

@ -127,15 +127,19 @@ end
""" Evaluate a conjunctive query on an attributed C-set.
The query is a undirected wiring diagram whose boxes and ports are assumed to be
named through attributes `:name` and `:port_name`/`:outer_port_name`. To define
such a diagram, use the named form of the [`@relation`](@ref) macro.
The query is a undirected wiring diagram (UWD) whose boxes and ports are assumed
to be named through attributes `:name` and `:port_name`/`:outer_port_name`. To
define such a diagram, use the named form of the [`@relation`](@ref) macro.
This function straightforwardly wraps the [`oapply`](@ref) method for
multispans, which implements the UWD algebra of multispans.
The result is a table, by default a `TypedTable`, whose columns correspond to
the outer ports of the UWD. If the UWD has no outer ports, the query is a
counting query and the result is a vector whose length is the number of results.
For its implementation, this function wraps the [`oapply`](@ref) method for
multispans, which defines the UWD algebra of multispans.
"""
function query(X::AbstractACSet, diagram::UndirectedWiringDiagram,
params=NamedTuple(); table_type::Type=TypedTables.Table)
params=(;); table_type::Type=TypedTables.Table)
# For each box in the diagram, extract span from ACSet.
spans = map(boxes(diagram), subpart(diagram, :name)) do b, name
apex = FinSet(nparts(X, name))
@ -159,8 +163,12 @@ function query(X::AbstractACSet, diagram::UndirectedWiringDiagram,
# Call `oapply` and make a table out of the resulting span.
outer_names = subpart(diagram, :outer_port_name)
outer_span = oapply(diagram, spans, Ob=SetOb, Hom=FinDomFunction{Int})
table = NamedTuple{Tuple(outer_names)}(Tuple(map(collect, outer_span)))
make_table(table_type, table)
if isempty(outer_names)
fill((;), length(apex(outer_span)))
else
table = NamedTuple{Tuple(outer_names)}(Tuple(map(collect, outer_span)))
make_table(table_type, table)
end
end
end

15
test/graphs/GraphGenerators.jl

@ -2,6 +2,7 @@ module TestGraphGenerators
using Test
using Catlab.Graphs.BasicGraphs, Catlab.Graphs.GraphGenerators
using Catlab.CategoricalAlgebra: homomorphisms
# Path graphs
#------------
@ -41,4 +42,18 @@ g = star_graph(Graph, n)
g = star_graph(SymmetricGraph, n)
@test (nv(g), ne(g)) == (n, 2(n-1))
# Wheel graphs
#-------------
g = wheel_graph(Graph, n)
@test (nv(g), ne(g)) == (n, 2(n-1))
triangle = Graph(3)
add_edges!(triangle, [1,2,1], [2,3,3])
@test length(homomorphisms(triangle, g)) == n-1
g = wheel_graph(SymmetricGraph, n)
@test (nv(g), ne(g)) == (n, 4(n-1))
triangle = cycle_graph(SymmetricGraph, 3)
@test length(homomorphisms(triangle, g)) == 6(n-1) # == 3! * (n-1)
end

6
test/wiring_diagrams/Algebras.jl

@ -62,6 +62,10 @@ paths2 = @relation (start=start, stop=stop) begin
E(src=start, tgt=mid)
E(src=mid, tgt=stop)
end
count_paths2 = @relation (;) begin
E(src=start, tgt=mid)
E(src=mid, tgt=stop)
end
# Graph underlying a commutative squares.
square = Graph(4)
@ -75,11 +79,13 @@ add_vertices!(squares2, 2)
add_edges!(squares2, [2,4,5], [5,6,6])
result = query(squares2, paths2)
@test tuples(columns(result)...) == [(1,4), (1,4), (1,5), (2,6), (2,6), (3,6)]
@test length(query(squares2, count_paths2)) == 6
result = query(squares2, paths2, (start=1,))
@test tuples(columns(result)...) == [(1,4), (1,4), (1,5)]
result = query(squares2, paths2, (start=1, stop=4))
@test result == Table((start=[1,1], stop=[4,4]))
@test length(query(squares2, count_paths2, (start=1, stop=4))) == 2
cycles3 = @relation (edge1=e, edge2=f, edge3=g) where (e,f,g,u,v,w) begin
E(_id=e, src=u, tgt=v)

Loading…
Cancel
Save