Skip to content

Commit

Permalink
basic csetautomorphisms code
Browse files Browse the repository at this point in the history
  • Loading branch information
Kris Brown committed Mar 8, 2024
1 parent ca8354c commit e9349af
Show file tree
Hide file tree
Showing 9 changed files with 468 additions and 10 deletions.
8 changes: 6 additions & 2 deletions ext/NautyACSetsExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ using ACSets
using nauty_jll

"""Compute CSetNautyRes from an ACSet."""
function ACSets.call_nauty(g::ACSet)::CSetNautyRes
ACSets.NautyInterface.parse_res(nauty_res(g), g)
function ACSets.call_nauty(g::ACSet; use_nauty=true)::CSetNautyRes
if Sys.iswindows() || !use_nauty
CSetAutomorphisms.to_nauty_res(g)

Check warning on line 9 in ext/NautyACSetsExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/NautyACSetsExt.jl#L9

Added line #L9 was not covered by tests
else
ACSets.NautyInterface.parse_res(nauty_res(g), g)
end
end

"""Make shell command to dreadnaut (nauty) and collect stdout text."""
Expand Down
4 changes: 2 additions & 2 deletions src/ACSets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ include("DenseACSets.jl")
include("intertypes/InterTypes.jl")
include("serialization/Serialization.jl")
include("ADTs.jl")
include("NautyInterface.jl")
include("nauty/Nauty.jl")

@reexport using .ColumnImplementations: AttrVar
@reexport using .Schemas
Expand All @@ -23,6 +23,6 @@ include("NautyInterface.jl")
@reexport using .InterTypes
@reexport using .ACSetSerialization
using .ADTs
@reexport using .NautyInterface
@reexport using .Nauty

end
275 changes: 275 additions & 0 deletions src/nauty/CSetAutomorphisms.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
"""
This is code which is required because nauty_jll is not supported on Windows:
thus we need a makeshift implementation of Nauty within Julia. There is
potential to do this in a much cleaner and more efficient way with a virtual
machine, but for now performance is not a priority.
"""
module CSetAutomorphisms

using ...ACSetInterface, ...DenseACSets, ...Schemas
using ..NautyInterface
using Permutations


# Color assigned to each elem of each component
const CDict = Dict{Symbol, Vector{Int}}

"""Construct permutation σ⁻¹ such that σσ⁻¹=id"""
invert_perms(x::CDict) = Dict([k=>Base.invperm(v) for (k, v) in collect(x)])

Check warning on line 18 in src/nauty/CSetAutomorphisms.jl

View check run for this annotation

Codecov / codecov/patch

src/nauty/CSetAutomorphisms.jl#L18

Added line #L18 was not covered by tests

check_auto(x::CDict)::Bool = all(Base.isperm, values(x))

# Sequence of keys, to index a Tree
const VPSI = Vector{Pair{Symbol, Int}}

max0(x::Vector{Int})::Int = isempty(x) ? 0 : maximum(x)

include("ColorRefine.jl")

# CSets with attributes replaced w/ combinatorial representatives
#################################################################

"""
To compute automorphisms of Attributed CSets, we create a pseudo CSet which has
additional components for each data type.
This is inefficient for attributes which have a total order on them
(e.g. integers/strings) since we solve for a canonical permutation of the
attributes. Future work could address this by initializing the coloring with
the 'correct' canonical order.
"""
function pseudo_cset(g::ACSet)::Tuple{ACSet, Dict{Symbol,Vector{Any}}}
# Create copy of schema (+ an extra component for each datatype)
S = acset_schema(g)
pres = deepcopy(S)
append!(pres.obs, pres.attrtypes)
append!(pres.homs, pres.attrs)
empty!.([pres.attrtypes, pres.attrs])

# Use Julia ordering to give each value an index
attrvals = Dict(map(attrtypes(S)) do at
vals = Set{Any}()
[union!(vals, g[a]) for a in attrs(S; just_names=true, to=at)]
at => vcat(filter(x->!(x isa AttrVar), vals) |> collect |> sort,
AttrVar.(parts(g, at)))
end)

# Create and populate pseudo-cset
res = AnonACSet(pres, index=arrows(S; just_names=true))

copy_parts!(res, g)

# Replace data value with an index for each attribute
for t in attrtypes(S)
add_parts!(res, t, length(attrvals[t]) - nparts(g, t))
for (a,d,_) in attrs(S; to=t)
for p in parts(g, d)
res[p, a] = findfirst(==(g[p, a]), attrvals[t])
end
end
end

(res, attrvals)
end

"""
Inverse of pseudo_cset. Requires mapping (generated by `pseudo_cset`) of indices
for each Data to the actual data values.
"""
function pseudo_cset_inv(g::ACSet, orig::ACSet, attrvals::AbstractDict)
S = acset_schema(orig)
orig = deepcopy(orig)
for arr in hom(S)
orig[arr] = g[arr]
end
for (darr,_,tgt) in attrs(S)
orig[darr] = attrvals[tgt][g[darr]]
end
orig

Check warning on line 88 in src/nauty/CSetAutomorphisms.jl

View check run for this annotation

Codecov / codecov/patch

src/nauty/CSetAutomorphisms.jl#L88

Added line #L88 was not covered by tests
end

# Results
#########

"""Apply a coloring to a C-set to get an isomorphic cset"""
function apply_automorphism(c::ACSet, d::CDict)
check_auto(d) || error("received coloring that is not an automorphism: $d")
new = deepcopy(c)
for (arr, src, tgt) in homs(acset_schema(c))
new[d[src], arr] = d[tgt][c[arr]]
end
for (arr, src, _) in attrs(acset_schema(c))
new[d[src], arr] = c[arr]
end
new

Check warning on line 104 in src/nauty/CSetAutomorphisms.jl

View check run for this annotation

Codecov / codecov/patch

src/nauty/CSetAutomorphisms.jl#L102-L104

Added lines #L102 - L104 were not covered by tests
end

function to_nauty_res(g::ACSet)
p, avals = pseudo_cset(g)
c, m = [pseudo_cset_inv(apply_automorphism(p, Dict(a)), g, avals) => a
for a in autos(p)[1]] |> sort |> first
strhsh = string(c)
orbits = Dict{Symbol, Vector{Int}}() # todo
generators = Pair{Int, Vector{Permutation}}[] # todo
CSetNautyRes(strhsh, orbits, generators, 0, m, c)
end

# Trees
#######

"""Find index at which two vectors diverge (used in `search_tree`)"""
function common(v1::Vector{T}, v2::Vector{T})::Int where {T}
for (i, (x, y)) in enumerate(zip(v1, v2))
x == y || return i - 1
end
min(length(v1), length(v2))

Check warning on line 125 in src/nauty/CSetAutomorphisms.jl

View check run for this annotation

Codecov / codecov/patch

src/nauty/CSetAutomorphisms.jl#L121-L125

Added lines #L121 - L125 were not covered by tests
end

"""
Search tree explored by Nauty. Each node has an input coloring, a refined
coloring, and a set of children indexed by which element (in the smallest
nontrivial orbit) has its symmetry artificially broken.
"""
struct Tree
coloring::CDict
saturated::CDict
children::Dict{Pair{Symbol, Int}, Tree}
Tree() = new(CDict(), CDict(), Dict{Pair{Symbol, Int}, Tree}())
end

"""Get a node via a sequence of edges from the root"""
function Base.getindex(t::Tree, pth::VPSI)::Tree
ptr = t
for p in pth
ptr = ptr.children[p]
end
ptr
end

"""Automorphism based pruning when we've found a new leaf node (τ @ t)"""
function compute_auto_prune(tree::Tree, t::VPSI, τ::CDict, leafnodes::Set{VPSI})::Set{VPSI}
skip = Set{VPSI}()
for p in filter(!=(t), leafnodes)
π = tree[p].saturated
i = common(p, t)
_, _, c = abc = p[1:i], p[1:i+1], t[1:i+1] # == t[1:i]
a_, b_, c_ = [tree[x].saturated for x in abc]
γ = compose_perms(π, invert_perms(τ))
if (compose_perms(γ, a_) == a_ && compose_perms(γ, b_) == c_)

Check warning on line 158 in src/nauty/CSetAutomorphisms.jl

View check run for this annotation

Codecov / codecov/patch

src/nauty/CSetAutomorphisms.jl#L153-L158

Added lines #L153 - L158 were not covered by tests
# skip everything from c to a
for i in length(c):length(t)
push!(skip, t[1:i])
end
break

Check warning on line 163 in src/nauty/CSetAutomorphisms.jl

View check run for this annotation

Codecov / codecov/patch

src/nauty/CSetAutomorphisms.jl#L160-L163

Added lines #L160 - L163 were not covered by tests
end
end

Check warning on line 165 in src/nauty/CSetAutomorphisms.jl

View check run for this annotation

Codecov / codecov/patch

src/nauty/CSetAutomorphisms.jl#L165

Added line #L165 was not covered by tests
filter(!=(t), skip) # has something gone wrong if we need to do this?
end

"""
Get vector listing nontrivial colors (which component and which color index) as
well as how many elements have that color. E.g. for (V=[1,1,2], E=[1,2,2,2,3,3])
we would get `[2=>(:V,1), 3=>(:E,2), 2=>(:E, 3)]`
"""
function get_colors_by_size(coloring::CDict)::Vector{Pair{Int,Tuple{Symbol, Int}}}
res = []
for (k, v) in coloring
for color in 1:max0(v)
n_c = count(==(color), v)
n_c > 1 && push!(res, n_c => (k, color)) # Store which table and which color
end
end
res
end


"""To reduce branching factor, split on the SMALLEST nontrivial partition"""
function split_data(coloring::CDict)::Tuple{Symbol, Int, Vector{Int}}
colors_by_size = sort(get_colors_by_size(coloring), rev=false)
isempty(colors_by_size) && return :_nothing, 0, []
split_tab, split_color = colors_by_size[1][2]
colors = coloring[split_tab]
split_inds = findall(==(split_color), colors)
(split_tab, split_color, split_inds)
end

"""
DFS tree of colorings, with edges being choices in how to break symmetry
Goal is to acquire all leaf nodes.
Algorithm from "McKay’s Canonical Graph Labeling Algorithm" by Hartke and
Radcliffe (2009).
McKay's "Practical Graph Isomorphism" (Section 2.29: "storage of identity
nodes") warns that it's not a good idea to check for every possible automorphism
pruning (for memory and time concerns). To do: look into doing this in a more
balanced way. Profiling code will probably reveal that checking for automorphism
pruning is a bottleneck.
Inputs:
- g: our structure that we are computing automorphisms for
- res: all automorphisms found so far
- split_seq: sequence of edges (our current location in the tree)
- tree: all information known so far - this gets modified
- leafnodes: coordinates of all automorphisms found so far
- skip: flagged coordinates which have been pruned
"""
function search_tree!(g::ACSet, init_coloring::CDict, split_seq::VPSI,
tree::Tree, leafnodes::Set{VPSI}, skip::Set{VPSI})
curr_tree = tree[split_seq]
# Perform color saturation
coloring = color_saturate(g; init_color=init_coloring)
for (k, v) in pairs(coloring)
curr_tree.coloring[k] = init_coloring[k]
curr_tree.saturated[k] = v
end

split_tab, _, split_inds = split_data(coloring)

# Check if we are now at a leaf node
if isempty(split_inds)
# Add result to list of results
push!(leafnodes, split_seq)
check_auto(coloring) # fail if not a perm

# Prune with automorphisms
pruned = compute_auto_prune(tree, split_seq, coloring, leafnodes)
isempty(pruned) || union!(skip, pruned)
else
# Branch on this leaf
for split_ind in split_inds
if split_ind == split_inds[1]
# Construct arguments for recursive call to child
new_coloring = deepcopy(coloring)
new_seq = vcat(split_seq, [split_tab => split_ind])
new_coloring[split_tab][split_ind] = maximum(coloring[split_tab]) + 1
curr_tree.children[split_tab => split_ind] = Tree()
search_tree!(g, new_coloring, new_seq, tree, leafnodes, skip)
end
end
end
end

"""Get coordinates of all nodes in a tree that have no children"""
function get_leaves(t::Tree)::Vector{VPSI}
if isempty(t.children)
[Pair{Symbol,Int}[]]

Check warning on line 256 in src/nauty/CSetAutomorphisms.jl

View check run for this annotation

Codecov / codecov/patch

src/nauty/CSetAutomorphisms.jl#L254-L256

Added lines #L254 - L256 were not covered by tests
else
res = []
for (kv, c) in t.children
for pth in get_leaves(c)
push!(res, vcat([kv],pth))
end
end
res

Check warning on line 264 in src/nauty/CSetAutomorphisms.jl

View check run for this annotation

Codecov / codecov/patch

src/nauty/CSetAutomorphisms.jl#L258-L264

Added lines #L258 - L264 were not covered by tests
end
end

"""Compute the automorphisms of a CSet"""
function autos(g::ACSet)::Tuple{Set{CDict}, Tree}
tree, leafnodes = Tree(), Set{VPSI}()
search_tree!(g, nocolor(g), VPSI(), tree, leafnodes,Set{VPSI}())
Set([tree[ln].saturated for ln in leafnodes]), tree
end

end # module
Loading

0 comments on commit e9349af

Please sign in to comment.