From 6897b0d1e7ec773239c3efea6fc23c1406ef3995 Mon Sep 17 00:00:00 2001 From: Maximilian HUEBL Date: Sat, 9 Aug 2025 05:32:39 +0200 Subject: [PATCH 01/11] rename densenauty tests --- test/densenautygraph.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/densenautygraph.jl b/test/densenautygraph.jl index 8d3d25d..52ef9eb 100644 --- a/test/densenautygraph.jl +++ b/test/densenautygraph.jl @@ -1,7 +1,7 @@ rng = Random.Random.MersenneTwister(0) # Use MersenneTwister for Julia 1.6 compat symmetrize_adjmx(A) = (A = convert(typeof(A), (A + A') .> 0); for i in axes(A, 1); A[i, i] = 0; end; A) -@testset "modify" begin +@testset "densenautygraph" begin nverts = [1, 2, 3, 4, 5, 10, 20, 31, 32, 33, 50, 63, 64, 65, 100, 122, 123, 124, 125, 126, 200, 500, 1000] As = [rand(rng, [0, 1], i, i) for i in nverts] @@ -95,9 +95,7 @@ symmetrize_adjmx(A) = (A = convert(typeof(A), (A + A') .> 0); for i in axes(A, 1 add_edge!(g_diloop, 1, 2) add_edge!(ng_diloop, 1, 2) @test ne(ng_diloop) == ne(g_diloop) -end -@testset "methods" begin empty_g = NautyGraph(0) @test nv(empty_g) == 0 @test ne(empty_g) == 0 From 51d6d5534deabd54e0eeb4584efcc0c880ebcfe7 Mon Sep 17 00:00:00 2001 From: Maximilian HUEBL Date: Sat, 9 Aug 2025 05:36:52 +0200 Subject: [PATCH 02/11] densenautygraph docstring --- src/densenautygraph.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/densenautygraph.jl b/src/densenautygraph.jl index 8c1ff1d..af479e1 100644 --- a/src/densenautygraph.jl +++ b/src/densenautygraph.jl @@ -3,7 +3,7 @@ Memory-efficient graph format compatible with nauty. Can be directed (`D = true`) or undirected (`D = false`). This graph format stores the adjacency matrix in bit vector form. `W` is the underlying -unsigned integer type that holds the individual bits (defaults to `UInt`). +unsigned integer type that holds the individual bits of the graph's adjacency matrix (defaults to `UInt`). """ mutable struct DenseNautyGraph{D,W<:Unsigned} <: AbstractNautyGraph{Int} graphset::Graphset{W} From 4f4b50e840ba63210061f5ab78246994b55bb6e3 Mon Sep 17 00:00:00 2001 From: Maximilian HUEBL Date: Wed, 10 Sep 2025 09:04:01 +0200 Subject: [PATCH 03/11] initial work on sparsenauty --- src/sparsegraphrep.jl | 106 +++++++++++++++++++++++++++++++++++++++++ test/sparsegraphrep.jl | 23 +++++++++ 2 files changed, 129 insertions(+) create mode 100644 src/sparsegraphrep.jl create mode 100644 test/sparsegraphrep.jl diff --git a/src/sparsegraphrep.jl b/src/sparsegraphrep.jl new file mode 100644 index 0000000..4d8f7d7 --- /dev/null +++ b/src/sparsegraphrep.jl @@ -0,0 +1,106 @@ +const NONEIGHBOR = -1 + +mutable struct SparseNautyGraph{D} <: AbstractNautyGraph{Cint} + nv::Int + nde::Int + v::Vector{Csize_t} + d::Vector{Cint} + e::Vector{Cint} + + function SparseNautyGraph{D}(n; ne=0) + v = ones(Csize_t, n) + d = zeros(Cint, n) + e = -ones(Cint, ne) + return new{D}(n, 0, v, d, e) + end +end + +mutable struct SparseGraphCStruct + nde::Csize_t + v::Ptr{Csize_t} #index into edges + nv::Cint + d::Ptr{Cint} #degrees + e::Ptr{Cint} #edges + w::Ptr{Cint} + vlen::Csize_t + dlen::Csize_t + elen::Csize_t + wlen::Csize_t +end +function Base.cconvert(::Type{Ref{SparseGraphCStruct}}, sref::Ref{SparseNautyGraph}) + s = sref[] + cstr = SparseGraphCStruct(s.nde, pointer(s.v), s.nv, pointer(s.d), pointer(s.e), C_NULL, length(s.v), length(s.d), length(s.e), 0) + return (s, cstr) +end +function Base.unsafe_convert(::Type{Ref{SparseGraphCStruct}}, x::Tuple{SparseNautyGraph,SparseGraphCStruct}) + _, cstr = x + return convert(Ptr{SparseGraphCStruct}, pointer_from_objref(cstr)) +end + +Graphs.is_directed(::SparseNautyGraph{D}) where {D} = D +Graphs.is_directed(::Type{SparseNautyGraph{D}}) where {D} = D + +Graphs.vertices(g::SparseNautyGraph) = Base.OneTo(g.nv) +Graphs.nv(g::SparseNautyGraph) = g.nv +Graphs.ne(g::SparseNautyGraph) = is_directed(g) ? g.nde : (g.nde + sum(has_edge(g, i, i) for i in vertices(g))) ÷ 2 + +function Graphs.has_edge(g::SparseNautyGraph, s::Integer, d::Integer) + for i in 0:g.d[s]-1 + g.e[g.v[s] + i] == d && return true + end + return false +end + +function Graphs.outdegree(g::SparseNautyGraph, v::Integer) + return g.d[v] +end +function Graphs.outneighbors(g::SparseNautyGraph, v::Integer) + return [g.e[v + i] for i in 0:g.d[v]-1] +end + +function Graphs.indegree(g::SparseNautyGraph, v::Integer) + return is_directed(g) ? sum(has_edge(g, i, v) for i in vertices(g)) : outdegree(g, v) +end +function Graphs.inneighbors(g::SparseNautyGraph, v::Integer) + return is_directed(g) ? findall(has_edge(g, i, v) for i in vertices(g)) : outneighbors(g, v) +end + +function Graphs.edges(g::SparseNautyGraph) + return SimpleEdgeIter(g) +end + + + +function Graphs.add_edge!(g::SparseNautyGraph, i::Integer, j::Integer) + has_vertex(g, i) && has_vertex(g, j) || return false + has_edge(g, i, j) && return false + + _add_directed_edge!(g, i, j) + if !is_directed(g) && i != j + _add_directed_edge!(g, j, i) + end + return true +end +function _add_directed_edge!(g::SparseNautyGraph, i::Integer, j::Integer) + idx = g.v[i] + g.d[i] + if idx in eachindex(g.e) && g.e[idx] == NONEIGHBOR + g.e[idx] = j + else + insert!(g.e, idx, j) + end + @views g.v[i+1:end] .+= 1 + g.d[i] += 1 + return +end + +function Graphs.add_vertices!(srep::SparseNautyGraph, n::Integer) + nnew = srep.nv + n + resize!(srep.v, nnew) + resize!(srep.d, nnew) + + srep.v[srep.nv+1:end] .= srep.v[srep.nv] + srep.d[srep.nv+1:end] .= 0 + srep.nv = nnew + return true +end + diff --git a/test/sparsegraphrep.jl b/test/sparsegraphrep.jl new file mode 100644 index 0000000..a2ea3b0 --- /dev/null +++ b/test/sparsegraphrep.jl @@ -0,0 +1,23 @@ +using NautyGraphs + +ll = NautyGraphs.nauty_jll.libnauty + +a = SparseGraphRep(3) +a.d = [2, 1, 1, 0, 0] +a.nv = 3 +a.nde = 4 +a.v = [0, 3, 5] +a.e = [2, 1, 0, 0, 0, 0, 0] + +b = SparseGraphRep(3) +b.d = [2, 1, 1, 0, 0] +b.nv = 3 +b.nde = 4 +b.v = [0, 4, 6] +b.e = [1, 2, 0, 0, 0, 0, 0, 0] + + +c = C_NULL + +@ccall ll.sortlists_sg(Ref(a)::Ref{SparseGraphCStruct})::Cvoid +@ccall ll.aresame_sg(Ref(a)::Ref{SparseGraphCStruct}, Ref(b)::Ref{SparseGraphCStruct})::Cint \ No newline at end of file From 6368990818a878ed5bb4a376053f584bfd738533 Mon Sep 17 00:00:00 2001 From: Maximilian HUEBL Date: Mon, 6 Oct 2025 15:10:46 +0200 Subject: [PATCH 04/11] sparse base methods --- src/NautyGraphs.jl | 10 +- src/nauty.jl | 51 ++++-- src/sparsegraphrep.jl | 106 ------------ src/sparsenautygraph.jl | 155 ++++++++++++++++++ test/densenautygraph.jl | 16 ++ ...{sparsegraphrep.jl => sparsenautygraph.jl} | 6 +- 6 files changed, 217 insertions(+), 127 deletions(-) delete mode 100644 src/sparsegraphrep.jl create mode 100644 src/sparsenautygraph.jl rename test/{sparsegraphrep.jl => sparsenautygraph.jl} (58%) diff --git a/src/NautyGraphs.jl b/src/NautyGraphs.jl index e84714d..44ae73a 100644 --- a/src/NautyGraphs.jl +++ b/src/NautyGraphs.jl @@ -12,16 +12,19 @@ abstract type AbstractNautyGraph{T} <: AbstractGraph{T} end include("utils.jl") include("graphset.jl") include("densenautygraph.jl") +include("sparsenautygraph.jl") include("nauty.jl") const NautyGraph = DenseNautyGraph{false} const NautyDiGraph = DenseNautyGraph{true} + function __init__() # global default options to nauty carry a pointer reference that needs to be initialized at runtime - DEFAULTOPTIONS16.dispatch = cglobal((:dispatch_graph, libnauty(UInt16)), Cvoid) - DEFAULTOPTIONS32.dispatch = cglobal((:dispatch_graph, libnauty(UInt32)), Cvoid) - DEFAULTOPTIONS64.dispatch = cglobal((:dispatch_graph, libnauty(UInt64)), Cvoid) + DEFAULTOPTIONS_DENSE16.dispatch = cglobal((:dispatch_graph, libnauty(UInt16)), Cvoid) + DEFAULTOPTIONS_DENSE32.dispatch = cglobal((:dispatch_graph, libnauty(UInt32)), Cvoid) + DEFAULTOPTIONS_DENSE64.dispatch = cglobal((:dispatch_graph, libnauty(UInt64)), Cvoid) + DEFAULTOPTIONS_SPARSE.dispatch = cglobal((:dispatch_sparse, libnauty(UInt64)), Cvoid) return end @@ -30,6 +33,7 @@ export NautyGraph, NautyDiGraph, DenseNautyGraph, + SparseNautyGraph, AutomorphismGroup, labels, nauty, diff --git a/src/nauty.jl b/src/nauty.jl index a8414df..a3e76eb 100644 --- a/src/nauty.jl +++ b/src/nauty.jl @@ -43,12 +43,15 @@ end return :(NautyOptions(cglobal((:dispatch_graph, $(libnauty(W))), Cvoid); digraph_or_loops, ignorelabels, groupinfo)) end -const DEFAULTOPTIONS16 = NautyOptions(C_NULL; digraph_or_loops=true, ignorelabels=false, groupinfo=false) -const DEFAULTOPTIONS32 = NautyOptions(C_NULL; digraph_or_loops=true, ignorelabels=false, groupinfo=false) -const DEFAULTOPTIONS64 = NautyOptions(C_NULL; digraph_or_loops=true, ignorelabels=false, groupinfo=false) -default_options(::DenseNautyGraph{D,UInt16}) where {D} = DEFAULTOPTIONS16 -default_options(::DenseNautyGraph{D,UInt32}) where {D} = DEFAULTOPTIONS32 -default_options(::DenseNautyGraph{D,UInt64}) where {D} = DEFAULTOPTIONS64 +const DEFAULTOPTIONS_DENSE16 = NautyOptions(C_NULL; digraph_or_loops=true, ignorelabels=false, groupinfo=false) +const DEFAULTOPTIONS_DENSE32 = NautyOptions(C_NULL; digraph_or_loops=true, ignorelabels=false, groupinfo=false) +const DEFAULTOPTIONS_DENSE64 = NautyOptions(C_NULL; digraph_or_loops=true, ignorelabels=false, groupinfo=false) +const DEFAULTOPTIONS_SPARSE = NautyOptions(C_NULL; digraph_or_loops=true, ignorelabels=false, groupinfo=false) + +default_options(::DenseNautyGraph{D,UInt16}) where {D} = DEFAULTOPTIONS_DENSE16 +default_options(::DenseNautyGraph{D,UInt32}) where {D} = DEFAULTOPTIONS_DENSE32 +default_options(::DenseNautyGraph{D,UInt64}) where {D} = DEFAULTOPTIONS_DENSE64 +default_options(::SparseNautyGraph) = DEFAULTOPTIONS_SPARSE mutable struct NautyStatistics grpsize1::Cdouble @@ -76,7 +79,7 @@ struct AutomorphismGroup # generators::Vector{Vector{Cint}} #TODO: not implemented end -function _densenauty(g::DenseNautyGraph{D,W}, options::NautyOptions=default_options(g), statistics::NautyStatistics=NautyStatistics()) where {D,W} +function _nauty(g::DenseNautyGraph{D,W}, options::NautyOptions=default_options(g), statistics::NautyStatistics=NautyStatistics()) where {D,W} # TODO: allow the user to pass pre-allocated arrays for lab, ptn, orbits, canong in a safe way. n, m = g.graphset.n, g.graphset.m @@ -84,13 +87,21 @@ function _densenauty(g::DenseNautyGraph{D,W}, options::NautyOptions=default_opti orbits = zeros(Cint, n) canong = Graphset{W}(n, m) - _ccall_densenauty(g, lab, ptn, orbits, options, statistics, canong) + _ccall_nauty(g, lab, ptn, orbits, options, statistics, canong) + canonperm = (lab .+= 1) + return canong, canonperm, orbits, statistics +end +function _nauty(g::SparseNautyGraph{D}, options::NautyOptions=default_options(g), statistics::NautyStatistics=NautyStatistics()) where {D} + lab, ptn = vertexlabels2labptn(g.labels) + orbits = zeros(Cint, nv(g)) + canong = SparseNautyGraph{D}(nv(g)) + _ccall_nauty(g, lab, ptn, orbits, options, statistics, canong) canonperm = (lab .+= 1) return canong, canonperm, orbits, statistics end -@generated function _ccall_densenauty(g::DenseNautyGraph{D,W}, lab, ptn, orbits, options, statistics, canong) where {D,W} +@generated function _ccall_nauty(g::DenseNautyGraph{D,W}, lab, ptn, orbits, options, statistics, canong) where {D,W} return quote @ccall $(libnauty(W)).densenauty( g.graphset.words::Ref{W}, lab::Ref{Cint}, @@ -102,6 +113,16 @@ end g.graphset.n::Cint, canong.words::Ref{W})::Cvoid end end +@generated function _ccall_nauty(g::SparseNautyGraph, lab, ptn, orbits, options, statistics, canong) + return quote @ccall $(libnauty(g)).sparsenauty( + Ref(g)::Ref{SparseGraphGraphRep}, + lab::Ref{Cint}, + ptn::Ref{Cint}, + orbits::Ref{Cint}, + Ref(options)::Ref{NautyOptions}, + Ref(statistics)::Ref{NautyStatistics}, + Ref(canong)::Ref{SparseGraphGraphRep})::Cvoid end +end function _sethash!(g::DenseNautyGraph, canong::Graphset, canonperm) # Base.hash skips elements in arrays of length >= 8192 @@ -136,7 +157,7 @@ function nauty(g::DenseNautyGraph, options::NautyOptions=default_options(g); can error("`options.getcanon` needs to be enabled.") end - canong, canonperm, orbits, statistics = _densenauty(g, options) + canong, canonperm, orbits, statistics = _nauty(g, options) # generators = Vector{Cint}[] # TODO: extract generators from nauty call autg = AutomorphismGroup(statistics.grpsize1 * 10^statistics.grpsize2, orbits) @@ -153,7 +174,7 @@ Reorder `g`'s vertices to be in canonical order. Returns the permutation `p` use function canonize!(::AbstractNautyGraph) end function canonize!(g::DenseNautyGraph) - canong, canonperm, _ = _densenauty(g) + canong, canonperm, _ = _nauty(g) _sethash!(g, canong, canonperm) _canonize!(g, canong, canonperm) return canonperm @@ -167,7 +188,7 @@ Return the permutation `p` needed to canonize `g`. This permutation satisfies `g function canonical_permutation(::AbstractNautyGraph) end function canonical_permutation(g::DenseNautyGraph) - _, canonperm, _ = _densenauty(g) + _, canonperm, _ = _nauty(g) return canonperm end @@ -179,8 +200,8 @@ Check whether two graphs `g` and `h` are isomorphic to each other by comparing t function is_isomorphic(::AbstractNautyGraph, ::AbstractNautyGraph) end function is_isomorphic(g::DenseNautyGraph, h::DenseNautyGraph) - canong, permg, _ = _densenauty(g) - canonh, permh, _ = _densenauty(h) + canong, permg, _ = _nauty(g) + canonh, permh, _ = _nauty(h) return canong == canonh && view(g.labels, permg) == view(h.labels, permh) end ≃(g::AbstractNautyGraph, h::AbstractNautyGraph) = is_isomorphic(g, h) @@ -199,7 +220,7 @@ function ghash(g::DenseNautyGraph) return g.hashval end - canong, canonperm, _ = _densenauty(g) + canong, canonperm, _ = _nauty(g) _sethash!(g, canong, canonperm) return g.hashval end \ No newline at end of file diff --git a/src/sparsegraphrep.jl b/src/sparsegraphrep.jl deleted file mode 100644 index 4d8f7d7..0000000 --- a/src/sparsegraphrep.jl +++ /dev/null @@ -1,106 +0,0 @@ -const NONEIGHBOR = -1 - -mutable struct SparseNautyGraph{D} <: AbstractNautyGraph{Cint} - nv::Int - nde::Int - v::Vector{Csize_t} - d::Vector{Cint} - e::Vector{Cint} - - function SparseNautyGraph{D}(n; ne=0) - v = ones(Csize_t, n) - d = zeros(Cint, n) - e = -ones(Cint, ne) - return new{D}(n, 0, v, d, e) - end -end - -mutable struct SparseGraphCStruct - nde::Csize_t - v::Ptr{Csize_t} #index into edges - nv::Cint - d::Ptr{Cint} #degrees - e::Ptr{Cint} #edges - w::Ptr{Cint} - vlen::Csize_t - dlen::Csize_t - elen::Csize_t - wlen::Csize_t -end -function Base.cconvert(::Type{Ref{SparseGraphCStruct}}, sref::Ref{SparseNautyGraph}) - s = sref[] - cstr = SparseGraphCStruct(s.nde, pointer(s.v), s.nv, pointer(s.d), pointer(s.e), C_NULL, length(s.v), length(s.d), length(s.e), 0) - return (s, cstr) -end -function Base.unsafe_convert(::Type{Ref{SparseGraphCStruct}}, x::Tuple{SparseNautyGraph,SparseGraphCStruct}) - _, cstr = x - return convert(Ptr{SparseGraphCStruct}, pointer_from_objref(cstr)) -end - -Graphs.is_directed(::SparseNautyGraph{D}) where {D} = D -Graphs.is_directed(::Type{SparseNautyGraph{D}}) where {D} = D - -Graphs.vertices(g::SparseNautyGraph) = Base.OneTo(g.nv) -Graphs.nv(g::SparseNautyGraph) = g.nv -Graphs.ne(g::SparseNautyGraph) = is_directed(g) ? g.nde : (g.nde + sum(has_edge(g, i, i) for i in vertices(g))) ÷ 2 - -function Graphs.has_edge(g::SparseNautyGraph, s::Integer, d::Integer) - for i in 0:g.d[s]-1 - g.e[g.v[s] + i] == d && return true - end - return false -end - -function Graphs.outdegree(g::SparseNautyGraph, v::Integer) - return g.d[v] -end -function Graphs.outneighbors(g::SparseNautyGraph, v::Integer) - return [g.e[v + i] for i in 0:g.d[v]-1] -end - -function Graphs.indegree(g::SparseNautyGraph, v::Integer) - return is_directed(g) ? sum(has_edge(g, i, v) for i in vertices(g)) : outdegree(g, v) -end -function Graphs.inneighbors(g::SparseNautyGraph, v::Integer) - return is_directed(g) ? findall(has_edge(g, i, v) for i in vertices(g)) : outneighbors(g, v) -end - -function Graphs.edges(g::SparseNautyGraph) - return SimpleEdgeIter(g) -end - - - -function Graphs.add_edge!(g::SparseNautyGraph, i::Integer, j::Integer) - has_vertex(g, i) && has_vertex(g, j) || return false - has_edge(g, i, j) && return false - - _add_directed_edge!(g, i, j) - if !is_directed(g) && i != j - _add_directed_edge!(g, j, i) - end - return true -end -function _add_directed_edge!(g::SparseNautyGraph, i::Integer, j::Integer) - idx = g.v[i] + g.d[i] - if idx in eachindex(g.e) && g.e[idx] == NONEIGHBOR - g.e[idx] = j - else - insert!(g.e, idx, j) - end - @views g.v[i+1:end] .+= 1 - g.d[i] += 1 - return -end - -function Graphs.add_vertices!(srep::SparseNautyGraph, n::Integer) - nnew = srep.nv + n - resize!(srep.v, nnew) - resize!(srep.d, nnew) - - srep.v[srep.nv+1:end] .= srep.v[srep.nv] - srep.d[srep.nv+1:end] .= 0 - srep.nv = nnew - return true -end - diff --git a/src/sparsenautygraph.jl b/src/sparsenautygraph.jl new file mode 100644 index 0000000..860c37a --- /dev/null +++ b/src/sparsenautygraph.jl @@ -0,0 +1,155 @@ +mutable struct SparseNautyGraph{D} <: AbstractNautyGraph{Int} + nv::Int # number of vertices + nde::Int # number of directed edges + v::Vector{Csize_t} # edgelist positions of vertices + d::Vector{Cint} # vertex degrees + e::Vector{Cint} # edgelist + labels::Vector{Int} # vertex labels +end +function SparseNautyGraph{D}(n; vertex_labels=nothing) where {D} + v = ones(Csize_t, n) + d = zeros(Cint, n) + e = -ones(Cint, 0) # encode unused values as -1 + if isnothing(vertex_labels) + vertex_labels = zeros(Int, n) + end + return SparseNautyGraph{D}(n, 0, v, d, e, vertex_labels) +end + +libnauty(::SparseNautyGraph) = nauty_jll.libnautyTL +libnauty(::Type{<:SparseNautyGraph}) = nauty_jll.libnautyTL + +# C-compatible representation of a sparsenautygraph +mutable struct SparseGraphGraphRep + nde::Csize_t + v::Ptr{Csize_t} + nv::Cint + d::Ptr{Cint} + e::Ptr{Cint} + w::Ptr{Cint} + vlen::Csize_t + dlen::Csize_t + elen::Csize_t + wlen::Csize_t +end +function Base.cconvert(::Type{Ref{SparseGraphGraphRep}}, sref::Ref{<:SparseNautyGraph}) + s = sref[] + cstr = SparseGraphGraphRep(s.nde, pointer(s.v), s.nv, pointer(s.d), pointer(s.e), C_NULL, length(s.v), length(s.d), length(s.e), 0) + return (s, cstr) +end +function Base.unsafe_convert(::Type{Ref{SparseGraphGraphRep}}, x::Tuple{<:SparseNautyGraph,SparseGraphGraphRep}) + _, cstr = x + return convert(Ptr{SparseGraphGraphRep}, pointer_from_objref(cstr)) +end +@generated function sortlists!(g::SparseNautyGraph) + # Sort the lists in the graph rep into some reference order + return quote + @ccall $(libnauty(g)).sortlists_sg(Ref(g)::Ref{SparseGraphGraphRep})::Cvoid + end +end + +Base.copy(g::G) where {G<:SparseNautyGraph} = G(g.nv, g.nde, copy(g.v), copy(g.d), copy(g.e), copy(g.labels)) +function Base.copy!(dest::G, src::G) where {G<:SparseNautyGraph} + copy!(dest.v, src.v) + copy!(dest.d, src.d) + copy!(dest.e, src.e) + copy!(dest.labels, src.labels) + + dest.ne = src.ne + dest.nde = src.nde + return dest +end + +Base.show(io::Core.IO, g::SparseNautyGraph{false}) = print(io, "{$(nv(g)), $(ne(g))} undirected SparseNautyGraph") +Base.show(io::Core.IO, g::SparseNautyGraph{true}) = print(io, "{$(nv(g)), $(ne(g))} directed SparseNautyGraph") + +function Base.hash(g::SparseNautyGraph, h::UInt) + # Reorder the edgelists into reference order before taking the hash + sortlists!(g) + return hash(g.labels, hash(g.v, hash(g.d, hash(g.e, h)))) +end + +function Base.:(==)(g::SparseNautyGraph{D1}, h::SparseNautyGraph{D2}) where {D1, D2} + return D1 == D2 && + labels(g) == labels(h) && + Bool(@ccall ll.aresame_sg(Ref(g)::Ref{SparseGraphGraphRep}, Ref(h)::Ref{SparseGraphGraphRep})::Cint) +end + +Graphs.nv(g::SparseNautyGraph) = g.nv +Graphs.ne(g::SparseNautyGraph) = is_directed(g) ? g.nde : (g.nde + sum(has_edge(g, i, i) for i in vertices(g))) ÷ 2 +Graphs.vertices(g::SparseNautyGraph) = Base.OneTo(g.nv) +Graphs.has_vertex(g::SparseNautyGraph, v::Integer) = v ∈ vertices(g) +function Graphs.has_edge(g::SparseNautyGraph, s::Integer, d::Integer) + (has_vertex(g, s) && has_vertex(g, d)) || return false + for i in outneighbors(g, s) + i == d && return true + end + return false +end + +@inline function Graphs.outdegree(g::SparseNautyGraph, v::Integer) + # following the Graph.jl implementation, there is no boundscheck here + return g.d[v] +end +@inline function Graphs.outneighbors(g::SparseNautyGraph, v::Integer) + # following the Graph.jl implementation, there is no boundscheck here + return (g.e[g.v[v] + i] for i in 0:g.d[v]-1) +end +@inline function Graphs.indegree(g::SparseNautyGraph, v::Integer) + # following the Graph.jl implementation, there is no boundscheck here + return is_directed(g) ? sum(has_edge(g, i, v) for i in vertices(g)) : outdegree(g, v) +end +@inline function Graphs.inneighbors(g::SparseNautyGraph, v::Integer) + # following the Graph.jl implementation, there is no boundscheck here + return is_directed(g) ? (i for i in vertices(g) if has_edge(g, i, v)) : outneighbors(g, v) +end + +function Graphs.edges(g::SparseNautyGraph) + return SimpleEdgeIter(g) +end + +Graphs.is_directed(::SparseNautyGraph{D}) where {D} = D +Graphs.is_directed(::Type{SparseNautyGraph{D}}) where {D} = D + +const NONEIGHBOR = -1 + +function Graphs.add_edge!(g::SparseNautyGraph, e::Edge) + has_vertex(g, e.src) && has_vertex(g, e.dst) || return false + has_edge(g, e.src, e.dst) && return false # TODO this checks has_vertex again + + _add_directed_edge!(g, e.src, e.dst) + if !is_directed(g) && e.src != e.dst + _add_directed_edge!(g, e.dst, e.src) + end + return true +end +function _add_directed_edge!(g::SparseNautyGraph, i::Integer, j::Integer) + idx = g.v[i] + g.d[i] + if idx in eachindex(g.e) && g.e[idx] == NONEIGHBOR + g.e[idx] = j + else + insert!(g.e, idx, j) + end + @views g.v[i+1:end] .+= 1 + g.d[i] += 1 + g.nde += 1 + return +end + +function Graphs.add_vertices!(g::SparseNautyGraph, n::Integer; vertex_labels=0) + vertex_labels isa Number || n != length(vertex_labels) && throw(ArgumentError("Incompatible length: trying to add `n=$n` vertices, but`vertex_labels` has length $(length(vertex_labels)).")) + + nold = g.nv + nnew = nold + n + resize!(g.v, nnew) + resize!(g.d, nnew) + resize!(g.labels, nnew) + + g.v[nold+1:end] .= g.v[nold] + g.d[nold+1:end] .= 0 + g.labels[nold+1:end] .= vertex_labels + + g.nv = nnew + return true +end + diff --git a/test/densenautygraph.jl b/test/densenautygraph.jl index 31bbce3..e57cd31 100644 --- a/test/densenautygraph.jl +++ b/test/densenautygraph.jl @@ -75,6 +75,14 @@ symmetrize_adjmx(A) = (A = convert(typeof(A), (A + A') .> 0); for i in axes(A, 1 add_edge!(ng_loop0, 1, 2) @test ne(ng_loop0) == ne(g_loop0) + rem_vertex!(g_loop0, 3) + rem_vertex!(ng_loop0, 3) + @test ne(ng_loop0) == ne(g_loop0) + + rem_edge!(g_loop0, 1, 2) + rem_edge!(ng_loop0, 1, 2) + @test ne(ng_loop0) == ne(g_loop0) + g_diloop0 = DiGraph([1 0 0; 0 1 0; 0 0 0]) ng_diloop0 = NautyDiGraph([1 0 0; 0 1 0; 0 0 0]) @@ -84,6 +92,14 @@ symmetrize_adjmx(A) = (A = convert(typeof(A), (A + A') .> 0); for i in axes(A, 1 add_edge!(ng_diloop0, 1, 2) @test ne(ng_diloop0) == ne(g_diloop0) + rem_vertex!(g_diloop0, 3) + rem_vertex!(ng_diloop0, 3) + @test ne(ng_diloop0) == ne(g_diloop0) + + rem_edge!(g_diloop0, 1, 2) + rem_edge!(ng_diloop0, 1, 2) + @test ne(ng_diloop0) == ne(g_diloop0) + g_loop = Graph(2) ng_loop = NautyGraph(2) diff --git a/test/sparsegraphrep.jl b/test/sparsenautygraph.jl similarity index 58% rename from test/sparsegraphrep.jl rename to test/sparsenautygraph.jl index a2ea3b0..b2730cf 100644 --- a/test/sparsegraphrep.jl +++ b/test/sparsenautygraph.jl @@ -2,7 +2,7 @@ using NautyGraphs ll = NautyGraphs.nauty_jll.libnauty -a = SparseGraphRep(3) +a = SparseNautyGraph{false}(3) a.d = [2, 1, 1, 0, 0] a.nv = 3 a.nde = 4 @@ -19,5 +19,5 @@ b.e = [1, 2, 0, 0, 0, 0, 0, 0] c = C_NULL -@ccall ll.sortlists_sg(Ref(a)::Ref{SparseGraphCStruct})::Cvoid -@ccall ll.aresame_sg(Ref(a)::Ref{SparseGraphCStruct}, Ref(b)::Ref{SparseGraphCStruct})::Cint \ No newline at end of file +@ccall ll.sortlists_sg(Ref(a)::Ref{SparseGraphGraphRep})::Cvoid +@ccall ll.aresame_sg(Ref(a)::Ref{SparseGraphGraphRep}, Ref(b)::Ref{SparseGraphGraphRep})::Cint \ No newline at end of file From 3a8629dec51c59b24298f0e290013c9b987b3334 Mon Sep 17 00:00:00 2001 From: Maximilian HUEBL Date: Wed, 8 Oct 2025 12:37:18 +0200 Subject: [PATCH 05/11] basic sparse modifiers --- src/densenautygraph.jl | 2 + src/nauty.jl | 17 ++- src/sparsenautygraph.jl | 232 ++++++++++++++++++++++++++++++++++++--- test/runtests.jl | 1 + test/sparsenautygraph.jl | 75 ++++++++++--- 5 files changed, 288 insertions(+), 39 deletions(-) diff --git a/src/densenautygraph.jl b/src/densenautygraph.jl index a913378..5cd0596 100644 --- a/src/densenautygraph.jl +++ b/src/densenautygraph.jl @@ -89,6 +89,8 @@ function DenseNautyGraph{D,W}(edge_list::Vector{<:AbstractEdge}; vertex_labels=n end DenseNautyGraph{D}(edge_list::Vector{<:AbstractEdge}; vertex_labels=nothing) where {D} = DenseNautyGraph{D,UInt}(edge_list; vertex_labels) +libnauty(::DenseNautyGraph{D,W}) where {D,W} = libnauty(W) +libnauty(::Type{DenseNautyGraph{D,W}}) where {D,W} = libnauty(W) Base.copy(g::G) where {G<:DenseNautyGraph} = G(copy(g.graphset), copy(g.labels), g.ne, g.hashval) function Base.copy!(dest::G, src::G) where {G<:DenseNautyGraph} diff --git a/src/nauty.jl b/src/nauty.jl index a3e76eb..e80e363 100644 --- a/src/nauty.jl +++ b/src/nauty.jl @@ -1,7 +1,6 @@ libnauty(::Type{UInt16}) = nauty_jll.libnautyTS libnauty(::Type{UInt32}) = nauty_jll.libnautyTW libnauty(::Type{UInt64}) = nauty_jll.libnautyTL -libnauty(::DenseNautyGraph{D,W}) where {D,W} = libnauty(W) mutable struct NautyOptions getcanon::Cint # Warning: setting getcanon to false means that nauty will NOT compute the canonical representative, which may lead to unexpected results. @@ -102,7 +101,7 @@ function _nauty(g::SparseNautyGraph{D}, options::NautyOptions=default_options(g) end @generated function _ccall_nauty(g::DenseNautyGraph{D,W}, lab, ptn, orbits, options, statistics, canong) where {D,W} - return quote @ccall $(libnauty(W)).densenauty( + return quote @ccall $(libnauty(g)).densenauty( g.graphset.words::Ref{W}, lab::Ref{Cint}, ptn::Ref{Cint}, @@ -139,16 +138,22 @@ function _canonize!(g::DenseNautyGraph, canong::Graphset, canonperm) permute!(g.labels, canonperm) return end - +function _sethash!(g::SparseNautyGraph, canong::Graphset, canonperm) + # TODO + return +end +function _canonize!(g::SparseNautyGraph, canong::SparseNautyGraph, canonperm) + copy!(g, canong) + permute!(g.labels, canonperm) + return +end """ nauty(g::AbstractNautyGraph, [options::NautyOptions]; [canonize=false]) Compute a graph `g`'s canonical form and automorphism group. """ -function nauty(::AbstractNautyGraph, ::NautyOptions; kwargs...) end - -function nauty(g::DenseNautyGraph, options::NautyOptions=default_options(g); canonize=false, compute_hash=true) +function nauty(g::AbstractNautyGraph, options::NautyOptions=default_options(g); canonize=false, compute_hash=true) if is_directed(g) && !isone(options.digraph) error("Nauty options need to match the directedness of the input graph. Make sure to instantiate options with `digraph=true` if the input graph is directed.") end diff --git a/src/sparsenautygraph.jl b/src/sparsenautygraph.jl index 860c37a..04ef406 100644 --- a/src/sparsenautygraph.jl +++ b/src/sparsenautygraph.jl @@ -1,21 +1,74 @@ mutable struct SparseNautyGraph{D} <: AbstractNautyGraph{Int} nv::Int # number of vertices nde::Int # number of directed edges - v::Vector{Csize_t} # edgelist positions of vertices + v::Vector{Csize_t} # edgelist positions of vertices (zero-based) d::Vector{Cint} # vertex degrees - e::Vector{Cint} # edgelist + e::Vector{Cint} # edgelist (zero-based) labels::Vector{Int} # vertex labels end -function SparseNautyGraph{D}(n; vertex_labels=nothing) where {D} - v = ones(Csize_t, n) + +""" + SparseNautyGraph{D}(n::Integer; [vertex_labels]) where {D} + +Construct a `SparseNautyGraph` on `n` vertices and 0 edges. +Can be directed (`D = true`) or undirected (`D = false`). +Vertex labels can optionally be specified. +""" +function SparseNautyGraph{D}(n::Integer; vertex_labels=nothing) where {D} + if !isnothing(vertex_labels) && n != length(vertex_labels) + throw(ArgumentError("The number of vertices is not compatible with the length of `vertex_labels`.")) + end + v = zeros(n) d = zeros(Cint, n) - e = -ones(Cint, 0) # encode unused values as -1 + e = -ones(Cint, n) # encode unused values as -1 if isnothing(vertex_labels) vertex_labels = zeros(Int, n) end return SparseNautyGraph{D}(n, 0, v, d, e, vertex_labels) end +""" + SparseNautyGraph{D}(A::AbstractMatrix; [vertex_labels]) where {D} + +Construct a `SparseNautyGraph{D}` from the adjacency matrix `A`. +If `A[i][j] != 0`, an edge `(i, j)` is inserted. `A` must be a square matrix. +The graph can be directed (`D = true`) or undirected (`D = false`). If `D = false`, `A` must be symmetric. +Vertex labels can optionally be specified. +""" +function SparseNautyGraph{D}(A::AbstractMatrix; vertex_labels=nothing) where {D} + n, m = size(A) + isequal(n, m) || throw(ArgumentError("Adjacency / distance matrices must be square")) + D || issymmetric(A) || throw(ArgumentError("Adjacency / distance matrices must be symmetric")) + + + g = SparseNautyGraph{D}(n; vertex_labels) + for i in axes(A, 1), j in axes(A, 2) + A[i, j] != 0 && _add_directed_edge!(g, i, j) + end + return g +end + +""" + SparseNautyGraph{D}(edge_list::Vector{<:AbstractEdge}; [vertex_labels]) where {D} + +Construct a `SparseNautyGraph` from a vector of edges. +The number of vertices is the highest that is used in an edge in `edge_list`. +The graph can be directed (`D = true`) or undirected (`D = false`). +Vertex labels can optionally be specified. +""" +function SparseNautyGraph{D}(edge_list::Vector{<:AbstractEdge}; vertex_labels=nothing) where {D} + nvg = 0 + for e in edge_list + nvg = max(nvg, src(e), dst(e)) + end + + g = SparseNautyGraph{D}(nvg; vertex_labels) + for edge in edge_list + add_edge!(g, edge) + end + return g +end + libnauty(::SparseNautyGraph) = nauty_jll.libnautyTL libnauty(::Type{<:SparseNautyGraph}) = nauty_jll.libnautyTL @@ -64,9 +117,7 @@ Base.show(io::Core.IO, g::SparseNautyGraph{false}) = print(io, "{$(nv(g)), $(ne( Base.show(io::Core.IO, g::SparseNautyGraph{true}) = print(io, "{$(nv(g)), $(ne(g))} directed SparseNautyGraph") function Base.hash(g::SparseNautyGraph, h::UInt) - # Reorder the edgelists into reference order before taking the hash - sortlists!(g) - return hash(g.labels, hash(g.v, hash(g.d, hash(g.e, h)))) + return hash(g.labels, hash(vertices(g), hash(edges(g), h))) end function Base.:(==)(g::SparseNautyGraph{D1}, h::SparseNautyGraph{D2}) where {D1, D2} @@ -76,7 +127,10 @@ function Base.:(==)(g::SparseNautyGraph{D1}, h::SparseNautyGraph{D2}) where {D1, end Graphs.nv(g::SparseNautyGraph) = g.nv -Graphs.ne(g::SparseNautyGraph) = is_directed(g) ? g.nde : (g.nde + sum(has_edge(g, i, i) for i in vertices(g))) ÷ 2 +function Graphs.ne(g::SparseNautyGraph) + nv(g) == 0 && return 0 + return is_directed(g) ? g.nde : (g.nde + sum(has_edge(g, i, i) for i in vertices(g))) ÷ 2 +end Graphs.vertices(g::SparseNautyGraph) = Base.OneTo(g.nv) Graphs.has_vertex(g::SparseNautyGraph, v::Integer) = v ∈ vertices(g) function Graphs.has_edge(g::SparseNautyGraph, s::Integer, d::Integer) @@ -93,7 +147,7 @@ end end @inline function Graphs.outneighbors(g::SparseNautyGraph, v::Integer) # following the Graph.jl implementation, there is no boundscheck here - return (g.e[g.v[v] + i] for i in 0:g.d[v]-1) + return (1 + g.e[i] for i in (1 + g.v[v]):(g.v[v] + g.d[v])) end @inline function Graphs.indegree(g::SparseNautyGraph, v::Integer) # following the Graph.jl implementation, there is no boundscheck here @@ -107,6 +161,70 @@ end function Graphs.edges(g::SparseNautyGraph) return SimpleEdgeIter(g) end +eltype(::Type{SimpleEdgeIter{<:SparseNautyGraph}}) = Graphs.SimpleGraphEdge{Int} +function Base.iterate(eit::SimpleEdgeIter{G}, state=(1, 1)) where {G<:SparseNautyGraph} + g = eit.g + n = nv(g) + v, nidx = state + + while nidx > g.d[v] + v += 1 + nidx = 1 + v > n && return nothing + end + + w = 1 + g.e[v + nidx - 1] + + if !is_directed(g) && w < v && has_edge(g, v, w) + return Base.iterate(eit, (v, nidx + 1)) + else + return Graphs.SimpleEdge{Int}(v, w), (v, nidx + 1) + end +end +# function Base.:(==)(e1::SimpleEdgeIter{<:SparseNautyGraph}, e2::SimpleEdgeIter{<:SparseNautyGraph}) +# g = e1.g +# h = e2.g +# ne(g) == ne(h) || return false +# m = min(nv(g), nv(h)) + +# g.graphset[1:m, 1:m] == h.graphset[1:m, 1:m] || return false +# nv(g) == nv(h) && return true + +# g.graphset[m+1:end, :] == 0 || return false +# is_directed(g) || g.graphset[m+1:end, 1:m] == 0 || return false + +# h.graphset[m+1:end, :] == 0 || return false +# is_directed(h) || h.graphset[m+1:end, 1:m] == 0 || return false +# return true +# end +# function Base.:(==)(e1::SimpleEdgeIter{<:DenseNautyGraph}, e2::SimpleEdgeIter{<:Graphs.SimpleGraphs.AbstractSimpleGraph}) +# g = e1.g +# h = e2.g +# ne(g) == ne(h) || return false +# is_directed(g) == is_directed(h) || return false + +# m = min(nv(g), nv(h)) +# for i in 1:m +# outneighbors(g, i) == Graphs.SimpleGraphs.fadj(h, i) || return false +# if is_directed(h) +# inneighbors(g, i) == Graphs.SimpleGraphs.badj(h, i) || return false +# end +# end +# nv(g) == nv(h) && return true + +# g.graphset[m+1:end, :] == 0 || return false +# is_directed(g) || g.graphset[m+1:end, 1:m] == 0 || return false + +# for i in (m + 1):nv(h) +# isempty(Graphs.SimpleGraphs.fadj(h, i)) || return false +# if is_directed(h) +# isempty(Graphs.SimpleGraphs.badj(h, i)) || return false +# end +# end +# return true +# end +# Base.:(==)(e1::SimpleEdgeIter{<:Graphs.SimpleGraphs.AbstractSimpleGraph}, e2::SimpleEdgeIter{<:DenseNautyGraph}) = e2 == e1 + Graphs.is_directed(::SparseNautyGraph{D}) where {D} = D Graphs.is_directed(::Type{SparseNautyGraph{D}}) where {D} = D @@ -124,16 +242,60 @@ function Graphs.add_edge!(g::SparseNautyGraph, e::Edge) return true end function _add_directed_edge!(g::SparseNautyGraph, i::Integer, j::Integer) - idx = g.v[i] + g.d[i] + idx = Int(1 + g.v[i] + g.d[i]) + + # If this is the first edge of vertex i + # find a free spot to start its neighborlist + if isone(idx) + idx = findfirst(==(NONEIGHBOR), g.e) + # If there is no free spot, we will append at the end + if isnothing(idx) + idx = length(g.e) + 1 + end + g.v[i] = idx - 1 + end + + # If there is a free spot at the end of the list, append j if idx in eachindex(g.e) && g.e[idx] == NONEIGHBOR - g.e[idx] = j + g.e[idx] = j - 1 + # otherwise insert j and shift the other indices else - insert!(g.e, idx, j) + insert!(g.e, idx, j - 1) + @views @. g.v[1:end != i] = ifelse(g.v[1:end != i] >= idx-1, g.v[1:end != i]+1, g.v[1:end != i]) end - @views g.v[i+1:end] .+= 1 g.d[i] += 1 g.nde += 1 - return + return true +end +function Graphs.rem_edge!(g::SparseNautyGraph, e::Edge) + has_vertex(g, e.src) && has_vertex(g, e.dst) || return false + has_edge(g, e.src, e.dst) || return false # TODO this checks has_vertex again + + _rem_directed_edge!(g, e.src, e.dst) + if !is_directed(g) && e.src != e.dst + _rem_directed_edge!(g, e.dst, e.src) + end + return true +end +function _rem_directed_edge!(g::SparseNautyGraph, i::Integer, j::Integer) + v, d = 1 + g.v[i], g.d[i] + idx = findfirst(==(j - 1), @view g.e[v:v+d-1]) + isnothing(idx) && return false + + vrem = v + idx - 1 + vlast = v + d - 1 + + if idx == d + g.e[vrem] = NONEIGHBOR + else + # Swap with the last edge and remove + elast = g.e[vlast] + g.e[vrem] = elast + g.e[vlast] = NONEIGHBOR + end + g.d[i] -= 1 + g.nde -= 1 + return true end function Graphs.add_vertices!(g::SparseNautyGraph, n::Integer; vertex_labels=0) @@ -145,11 +307,49 @@ function Graphs.add_vertices!(g::SparseNautyGraph, n::Integer; vertex_labels=0) resize!(g.d, nnew) resize!(g.labels, nnew) - g.v[nold+1:end] .= g.v[nold] + g.v[nold+1:end] .= 0 g.d[nold+1:end] .= 0 g.labels[nold+1:end] .= vertex_labels g.nv = nnew return true end +Graphs.add_vertex!(g::SparseNautyGraph; vertex_label::Integer=0) = Graphs.add_vertices!(g, 1; vertex_labels=vertex_label) > 0 + +function Graphs.rem_vertices!(g::SparseNautyGraph, inds) + isempty(inds) && return true + all(i->has_vertex(g, i), inds) || return false + + for i in vertices(g) + if i in inds + vstart, d = 1 + g.v[i], g.d[i] + d == 0 && continue + + vend = vstart + d - 1 + # Free memory for outneighbors + deleteat!(g.e, vstart:vend) + # TODO: this redundantly shifts indices that will be deleted below + @. g.v = ifelse(g.v > vend - 1, g.v - d, g.v) + else + # Keep memory for inneighbors + for j in inds + _rem_directed_edge!(g, i, j) + end + end + end + deleteat!(g.v, inds) + deleteat!(g.d, inds) + deleteat!(g.labels, inds) + + # shift vertices in edge list + for i in eachindex(g.e) + g.e[i] -= sum(<(1 + g.e[i]), inds) + end + + g.nv = length(g.v) + g.nde = sum(!=(NONEIGHBOR), g.e; init=0) + return true +end +Graphs.rem_vertex!(g::SparseNautyGraph, i::Integer) = rem_vertices!(g, (i,)) + diff --git a/test/runtests.jl b/test/runtests.jl index 1a29d37..16178be 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,6 +6,7 @@ using Base.Threads @testset verbose=true "NautyGraphs" begin include("densenautygraph.jl") + include("sparsenautygraph.jl") include("nauty.jl") include("graphset.jl") include("utils.jl") diff --git a/test/sparsenautygraph.jl b/test/sparsenautygraph.jl index b2730cf..f10f866 100644 --- a/test/sparsenautygraph.jl +++ b/test/sparsenautygraph.jl @@ -1,23 +1,64 @@ -using NautyGraphs +rng = Random.Random.MersenneTwister(0) # Use MersenneTwister for Julia 1.6 compat +symmetrize_adjmx(A) = (A = convert(typeof(A), (A + A') .> 0); for i in axes(A, 1); A[i, i] = 0; end; A) -ll = NautyGraphs.nauty_jll.libnauty +@testset "sparsenautygraph" begin + nverts = [1, 2, 3, 4, 5, 10, 20, 31, 32, 33, 50, 63, 64, 100, 200] + As = [rand(rng, [zeros(i ÷ 2); 1], i, i) for i in nverts] -a = SparseNautyGraph{false}(3) -a.d = [2, 1, 1, 0, 0] -a.nv = 3 -a.nde = 4 -a.v = [0, 3, 5] -a.e = [2, 1, 0, 0, 0, 0, 0] + gs = [] + ngs = [] + for A in As + Asym = symmetrize_adjmx(A) + push!(gs, Graph(Asym)) + push!(gs, DiGraph(A)) + push!(ngs, SparseNautyGraph{false}(Asym)) + push!(ngs, SparseNautyGraph{true}(A)) + end -b = SparseGraphRep(3) -b.d = [2, 1, 1, 0, 0] -b.nv = 3 -b.nde = 4 -b.v = [0, 4, 6] -b.e = [1, 2, 0, 0, 0, 0, 0, 0] + for (g, ng) in zip(gs, ngs) + g, ng = copy(g), copy(ng) + @test adjacency_matrix(g) == adjacency_matrix(ng) + # @test edges(ng) == edges(g) + # @test collect(edges(g)) == collect(edges(ng)) -c = C_NULL + rv = sort(unique(rand(rng, 1:nv(ng), 4))) -@ccall ll.sortlists_sg(Ref(a)::Ref{SparseGraphGraphRep})::Cvoid -@ccall ll.aresame_sg(Ref(a)::Ref{SparseGraphGraphRep}, Ref(b)::Ref{SparseGraphGraphRep})::Cint \ No newline at end of file + rem_vertices!(g, rv, keep_order=true) + rem_vertices!(ng, rv) + @test adjacency_matrix(g) == adjacency_matrix(ng) + end + + # for (g, ng) in zip(gs, ngs) + # g, ng = copy(g), copy(ng) + + # es = edges(g) + # if !isempty(es) + # edge = last(collect(es)) + + # rem_edge!(g, edge) + # rem_edge!(ng, edge) + # @test adjacency_matrix(g) == adjacency_matrix(ng) + # end + # end + + for (g, ng) in zip(gs, ngs) + g, ng = copy(g), copy(ng) + + add_vertex!(g) + add_vertex!(ng) + add_edge!(g, 1, nv(g)) + add_edge!(ng, 1, nv(ng)) + @test adjacency_matrix(g) == adjacency_matrix(ng) + end + + for (g, ng) in zip(gs, ngs) + g, ng = copy(g), copy(ng) + + add_vertices!(g, 500) + add_vertices!(ng, 500) + add_edge!(g, 1, 2) + add_edge!(ng, 1, 2) + @test adjacency_matrix(g) == adjacency_matrix(ng) + end +end \ No newline at end of file From 73cff05b59a97edc2f14f7dc2723562171fea47b Mon Sep 17 00:00:00 2001 From: Maximilian HUEBL Date: Wed, 8 Oct 2025 12:39:11 +0200 Subject: [PATCH 06/11] tiny test refact --- test/densenautygraph.jl | 3 --- test/runtests.jl | 3 +++ test/sparsenautygraph.jl | 3 --- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/test/densenautygraph.jl b/test/densenautygraph.jl index 5b790c7..17fac27 100644 --- a/test/densenautygraph.jl +++ b/test/densenautygraph.jl @@ -1,6 +1,3 @@ -rng = Random.Random.MersenneTwister(0) # Use MersenneTwister for Julia 1.6 compat -symmetrize_adjmx(A) = (A = convert(typeof(A), (A + A') .> 0); for i in axes(A, 1); end; A) - @testset "densenautygraph" begin nverts = [1, 2, 3, 4, 5, 10, 20, 31, 32, 33, 50, 63, 64, 65, 100, 122, 123, 124, 125, 126, 200, 500, 1000] diff --git a/test/runtests.jl b/test/runtests.jl index 16178be..68e38f1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,6 +4,9 @@ using Test using Random, LinearAlgebra using Base.Threads +rng = Random.Random.MersenneTwister(0) # Use MersenneTwister for Julia 1.6 compat +symmetrize_adjmx(A) = (A = convert(typeof(A), (A + A') .> 0); for i in axes(A, 1); end; A) + @testset verbose=true "NautyGraphs" begin include("densenautygraph.jl") include("sparsenautygraph.jl") diff --git a/test/sparsenautygraph.jl b/test/sparsenautygraph.jl index f10f866..ba6d6d6 100644 --- a/test/sparsenautygraph.jl +++ b/test/sparsenautygraph.jl @@ -1,6 +1,3 @@ -rng = Random.Random.MersenneTwister(0) # Use MersenneTwister for Julia 1.6 compat -symmetrize_adjmx(A) = (A = convert(typeof(A), (A + A') .> 0); for i in axes(A, 1); A[i, i] = 0; end; A) - @testset "sparsenautygraph" begin nverts = [1, 2, 3, 4, 5, 10, 20, 31, 32, 33, 50, 63, 64, 100, 200] As = [rand(rng, [zeros(i ÷ 2); 1], i, i) for i in nverts] From b99681c53b2701312956c10061fa68156e00a50e Mon Sep 17 00:00:00 2001 From: Maximilian HUEBL Date: Wed, 8 Oct 2025 18:25:36 +0200 Subject: [PATCH 07/11] basic methods working --- src/sparsenautygraph.jl | 129 +++++++++++++++++++++++---------------- test/sparsenautygraph.jl | 24 ++++---- 2 files changed, 89 insertions(+), 64 deletions(-) diff --git a/src/sparsenautygraph.jl b/src/sparsenautygraph.jl index 04ef406..7b23883 100644 --- a/src/sparsenautygraph.jl +++ b/src/sparsenautygraph.jl @@ -40,7 +40,6 @@ function SparseNautyGraph{D}(A::AbstractMatrix; vertex_labels=nothing) where {D} isequal(n, m) || throw(ArgumentError("Adjacency / distance matrices must be square")) D || issymmetric(A) || throw(ArgumentError("Adjacency / distance matrices must be symmetric")) - g = SparseNautyGraph{D}(n; vertex_labels) for i in axes(A, 1), j in axes(A, 2) A[i, j] != 0 && _add_directed_edge!(g, i, j) @@ -62,6 +61,9 @@ function SparseNautyGraph{D}(edge_list::Vector{<:AbstractEdge}; vertex_labels=no nvg = max(nvg, src(e), dst(e)) end + # sort the edgelist to optimize the neighborlist packing + edge_list = sort(edge_list) + g = SparseNautyGraph{D}(nvg; vertex_labels) for edge in edge_list add_edge!(g, edge) @@ -145,6 +147,9 @@ end # following the Graph.jl implementation, there is no boundscheck here return g.d[v] end +@inline function fadj(g::SparseNautyGraph, v::Integer) + return @view g.e[(1 + g.v[v]):(g.v[v] + g.d[v])] +end @inline function Graphs.outneighbors(g::SparseNautyGraph, v::Integer) # following the Graph.jl implementation, there is no boundscheck here return (1 + g.e[i] for i in (1 + g.v[v]):(g.v[v] + g.d[v])) @@ -162,73 +167,93 @@ function Graphs.edges(g::SparseNautyGraph) return SimpleEdgeIter(g) end eltype(::Type{SimpleEdgeIter{<:SparseNautyGraph}}) = Graphs.SimpleGraphEdge{Int} -function Base.iterate(eit::SimpleEdgeIter{G}, state=(1, 1)) where {G<:SparseNautyGraph} +function Base.iterate(eit::SimpleEdgeIter{G}) where {G<:SparseNautyGraph} + sortlists!(eit.g) + return Base.iterate(eit, (1, 1)) +end +function Base.iterate(eit::SimpleEdgeIter{G}, state) where {G<:SparseNautyGraph} g = eit.g n = nv(g) - v, nidx = state + i, nidx = state - while nidx > g.d[v] - v += 1 + while nidx > g.d[i] + i += 1 nidx = 1 - v > n && return nothing + i > n && return nothing end - w = 1 + g.e[v + nidx - 1] + w = 1 + g.e[g.v[i] + nidx] - if !is_directed(g) && w < v && has_edge(g, v, w) - return Base.iterate(eit, (v, nidx + 1)) + if !is_directed(g) && w < i && has_edge(g, i, w) + return Base.iterate(eit, (i, nidx + 1)) else - return Graphs.SimpleEdge{Int}(v, w), (v, nidx + 1) + return Graphs.SimpleEdge{Int}(i, w), (i, nidx + 1) + end +end +function Base.:(==)(e1::SimpleEdgeIter{<:SparseNautyGraph}, e2::SimpleEdgeIter{<:SparseNautyGraph}) + g = e1.g + h = e2.g + sortlists!(g) + sortlists!(h) + + ne(g) == ne(h) || return false + m = min(nv(g), nv(h)) + + for i in 1:m + fadj(g, i) == fadj(h, i) || return false + end + nv(g) == nv(h) && return true + for i in (m + 1):nv(g) + isempty(fadj(g, i)) || return false + end + for i in (m + 1):nv(h) + isempty(fadj(h, i)) || return false end + return true +end +function Base.:(==)(e1::SimpleEdgeIter{<:SparseNautyGraph}, e2::SimpleEdgeIter{<:Graphs.SimpleGraphs.AbstractSimpleGraph}) + g = e1.g + h = e2.g + sortlists!(g) + + ne(g) == ne(h) || return false + is_directed(g) == is_directed(h) || return false + + m = min(nv(g), nv(h)) + + for i in 1:m + neighs_g = NautyGraphs.fadj(g, i) + neighs_h = Graphs.SimpleGraphs.fadj(h, i) + length(neighs_g) == length(neighs_h) || return false + all(ngh -> 1 + ngh[1] == ngh[2], zip(neighs_g, neighs_h)) || return false + end + + nv(g) == nv(h) && return true + for i in (m + 1):nv(g) + isempty(NautyGraphs.fadj(g, i)) || return false + end + for i in (m + 1):nv(h) + isempty(Graphs.SimpleGraphs.fadj(h, i)) || return false + end + return true end -# function Base.:(==)(e1::SimpleEdgeIter{<:SparseNautyGraph}, e2::SimpleEdgeIter{<:SparseNautyGraph}) -# g = e1.g -# h = e2.g -# ne(g) == ne(h) || return false -# m = min(nv(g), nv(h)) - -# g.graphset[1:m, 1:m] == h.graphset[1:m, 1:m] || return false -# nv(g) == nv(h) && return true - -# g.graphset[m+1:end, :] == 0 || return false -# is_directed(g) || g.graphset[m+1:end, 1:m] == 0 || return false - -# h.graphset[m+1:end, :] == 0 || return false -# is_directed(h) || h.graphset[m+1:end, 1:m] == 0 || return false -# return true -# end -# function Base.:(==)(e1::SimpleEdgeIter{<:DenseNautyGraph}, e2::SimpleEdgeIter{<:Graphs.SimpleGraphs.AbstractSimpleGraph}) -# g = e1.g -# h = e2.g -# ne(g) == ne(h) || return false -# is_directed(g) == is_directed(h) || return false - -# m = min(nv(g), nv(h)) -# for i in 1:m -# outneighbors(g, i) == Graphs.SimpleGraphs.fadj(h, i) || return false -# if is_directed(h) -# inneighbors(g, i) == Graphs.SimpleGraphs.badj(h, i) || return false -# end -# end -# nv(g) == nv(h) && return true - -# g.graphset[m+1:end, :] == 0 || return false -# is_directed(g) || g.graphset[m+1:end, 1:m] == 0 || return false - -# for i in (m + 1):nv(h) -# isempty(Graphs.SimpleGraphs.fadj(h, i)) || return false -# if is_directed(h) -# isempty(Graphs.SimpleGraphs.badj(h, i)) || return false -# end -# end -# return true -# end # Base.:(==)(e1::SimpleEdgeIter{<:Graphs.SimpleGraphs.AbstractSimpleGraph}, e2::SimpleEdgeIter{<:DenseNautyGraph}) = e2 == e1 Graphs.is_directed(::SparseNautyGraph{D}) where {D} = D Graphs.is_directed(::Type{SparseNautyGraph{D}}) where {D} = D +function trim_edgelist!(g::SparseNautyGraph) + excess_length = 0 + + for i in Iterators.Reverse(g.e) + i != NONEIGHBOR && break + excess_length += 1 + end + resize!(g.e, length(g.e) - excess_length) + return excess_length +end + const NONEIGHBOR = -1 function Graphs.add_edge!(g::SparseNautyGraph, e::Edge) diff --git a/test/sparsenautygraph.jl b/test/sparsenautygraph.jl index ba6d6d6..5bfe8f3 100644 --- a/test/sparsenautygraph.jl +++ b/test/sparsenautygraph.jl @@ -16,8 +16,8 @@ g, ng = copy(g), copy(ng) @test adjacency_matrix(g) == adjacency_matrix(ng) - # @test edges(ng) == edges(g) - # @test collect(edges(g)) == collect(edges(ng)) + @test edges(ng) == edges(g) + @test collect(edges(g)) == collect(edges(ng)) rv = sort(unique(rand(rng, 1:nv(ng), 4))) @@ -26,18 +26,18 @@ @test adjacency_matrix(g) == adjacency_matrix(ng) end - # for (g, ng) in zip(gs, ngs) - # g, ng = copy(g), copy(ng) + for (g, ng) in zip(gs, ngs) + g, ng = copy(g), copy(ng) - # es = edges(g) - # if !isempty(es) - # edge = last(collect(es)) + es = edges(g) + if !isempty(es) + edge = last(collect(es)) - # rem_edge!(g, edge) - # rem_edge!(ng, edge) - # @test adjacency_matrix(g) == adjacency_matrix(ng) - # end - # end + rem_edge!(g, edge) + rem_edge!(ng, edge) + @test adjacency_matrix(g) == adjacency_matrix(ng) + end + end for (g, ng) in zip(gs, ngs) g, ng = copy(g), copy(ng) From fbd2d07dc9e2650804976205012a855e5dca52cc Mon Sep 17 00:00:00 2001 From: Maximilian HUEBL Date: Wed, 8 Oct 2025 18:27:20 +0200 Subject: [PATCH 08/11] fix to sparse edge equality --- src/sparsenautygraph.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sparsenautygraph.jl b/src/sparsenautygraph.jl index 7b23883..b68648e 100644 --- a/src/sparsenautygraph.jl +++ b/src/sparsenautygraph.jl @@ -237,7 +237,7 @@ function Base.:(==)(e1::SimpleEdgeIter{<:SparseNautyGraph}, e2::SimpleEdgeIter{< end return true end -# Base.:(==)(e1::SimpleEdgeIter{<:Graphs.SimpleGraphs.AbstractSimpleGraph}, e2::SimpleEdgeIter{<:DenseNautyGraph}) = e2 == e1 +Base.:(==)(e1::SimpleEdgeIter{<:Graphs.SimpleGraphs.AbstractSimpleGraph}, e2::SimpleEdgeIter{<:SparseNautyGraph}) = e2 == e1 Graphs.is_directed(::SparseNautyGraph{D}) where {D} = D From bda92635597248b4e5b65a86236fe9b0c23ef2ac Mon Sep 17 00:00:00 2001 From: Maximilian HUEBL Date: Fri, 17 Oct 2025 18:03:48 +0200 Subject: [PATCH 09/11] working nauty dispatch + more sparse graph methods --- src/nauty.jl | 18 ++++++---- src/sparsenautygraph.jl | 80 ++++++++++++++++++++++++++++++----------- 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/nauty.jl b/src/nauty.jl index e80e363..5ec7ae2 100644 --- a/src/nauty.jl +++ b/src/nauty.jl @@ -93,7 +93,7 @@ end function _nauty(g::SparseNautyGraph{D}, options::NautyOptions=default_options(g), statistics::NautyStatistics=NautyStatistics()) where {D} lab, ptn = vertexlabels2labptn(g.labels) orbits = zeros(Cint, nv(g)) - canong = SparseNautyGraph{D}(nv(g)) + canong = SparseGraphRep() _ccall_nauty(g, lab, ptn, orbits, options, statistics, canong) canonperm = (lab .+= 1) @@ -113,14 +113,15 @@ end canong.words::Ref{W})::Cvoid end end @generated function _ccall_nauty(g::SparseNautyGraph, lab, ptn, orbits, options, statistics, canong) - return quote @ccall $(libnauty(g)).sparsenauty( - Ref(g)::Ref{SparseGraphGraphRep}, + return quote + @ccall $(libnauty(g)).sparsenauty( + Ref(g)::Ref{SparseGraphRep}, lab::Ref{Cint}, ptn::Ref{Cint}, orbits::Ref{Cint}, Ref(options)::Ref{NautyOptions}, Ref(statistics)::Ref{NautyStatistics}, - Ref(canong)::Ref{SparseGraphGraphRep})::Cvoid end + Ref(canong)::Ref{SparseGraphRep})::Cvoid end end function _sethash!(g::DenseNautyGraph, canong::Graphset, canonperm) @@ -138,12 +139,12 @@ function _canonize!(g::DenseNautyGraph, canong::Graphset, canonperm) permute!(g.labels, canonperm) return end -function _sethash!(g::SparseNautyGraph, canong::Graphset, canonperm) +function _sethash!(g::SparseNautyGraph, canong::SparseGraphRep, canonperm) # TODO return end -function _canonize!(g::SparseNautyGraph, canong::SparseNautyGraph, canonperm) - copy!(g, canong) +function _canonize!(g::SparseNautyGraph, canong::SparseGraphRep, canonperm) + _unsafe_sparsegraphcopy!(g, canong) permute!(g.labels, canonperm) return end @@ -168,6 +169,9 @@ function nauty(g::AbstractNautyGraph, options::NautyOptions=default_options(g); compute_hash && _sethash!(g, canong, canonperm) canonize && _canonize!(g, canong, canonperm) + + # free memory allocated by nauty for sparse graphs + canong isa SparseGraphRep && _free_sparsegraph(canong) return canonperm, autg end diff --git a/src/sparsenautygraph.jl b/src/sparsenautygraph.jl index b68648e..4fd913d 100644 --- a/src/sparsenautygraph.jl +++ b/src/sparsenautygraph.jl @@ -8,19 +8,20 @@ mutable struct SparseNautyGraph{D} <: AbstractNautyGraph{Int} end """ - SparseNautyGraph{D}(n::Integer; [vertex_labels]) where {D} + SparseNautyGraph{D}(n::Integer; [vertex_labels, ne=n]) where {D} Construct a `SparseNautyGraph` on `n` vertices and 0 edges. Can be directed (`D = true`) or undirected (`D = false`). -Vertex labels can optionally be specified. +Vertex labels can optionally be specified. If `ne` is provided, enough +memory for `ne` optimally packed edges is allocated. """ -function SparseNautyGraph{D}(n::Integer; vertex_labels=nothing) where {D} +function SparseNautyGraph{D}(n::Integer; vertex_labels=nothing, ne=n) where {D} if !isnothing(vertex_labels) && n != length(vertex_labels) throw(ArgumentError("The number of vertices is not compatible with the length of `vertex_labels`.")) end v = zeros(n) d = zeros(Cint, n) - e = -ones(Cint, n) # encode unused values as -1 + e = -ones(Cint, ne) # encode unused values as -1 if isnothing(vertex_labels) vertex_labels = zeros(Int, n) end @@ -40,7 +41,7 @@ function SparseNautyGraph{D}(A::AbstractMatrix; vertex_labels=nothing) where {D} isequal(n, m) || throw(ArgumentError("Adjacency / distance matrices must be square")) D || issymmetric(A) || throw(ArgumentError("Adjacency / distance matrices must be symmetric")) - g = SparseNautyGraph{D}(n; vertex_labels) + g = SparseNautyGraph{D}(n; vertex_labels, ne=sum(isone, A)) for i in axes(A, 1), j in axes(A, 2) A[i, j] != 0 && _add_directed_edge!(g, i, j) end @@ -54,6 +55,7 @@ Construct a `SparseNautyGraph` from a vector of edges. The number of vertices is the highest that is used in an edge in `edge_list`. The graph can be directed (`D = true`) or undirected (`D = false`). Vertex labels can optionally be specified. +To achieve optimal memory efficiency, it is recommended to sort the edge list beforehand. """ function SparseNautyGraph{D}(edge_list::Vector{<:AbstractEdge}; vertex_labels=nothing) where {D} nvg = 0 @@ -61,21 +63,32 @@ function SparseNautyGraph{D}(edge_list::Vector{<:AbstractEdge}; vertex_labels=no nvg = max(nvg, src(e), dst(e)) end - # sort the edgelist to optimize the neighborlist packing - edge_list = sort(edge_list) + # sort edgelist to optimize neighborlist packing + # edge_list = sort(edge_list) - g = SparseNautyGraph{D}(nvg; vertex_labels) + g = SparseNautyGraph{D}(nvg; vertex_labels, ne=length(edge_list)) for edge in edge_list add_edge!(g, edge) end + trim_edgelist!(g) return g end +function (::Type{G})(g::AbstractGraph) where {G<:SparseNautyGraph} + ng = g isa AbstractNautyGraph ? G(nv(g); vertex_labels=labels(g), ne=ne(g)) : G(nv(g); ne=ne(g)) + for v in vertices(g) + for n in neighbors(g, v) + _add_directed_edge!(ng, v, n) + end + end + return ng +end + libnauty(::SparseNautyGraph) = nauty_jll.libnautyTL libnauty(::Type{<:SparseNautyGraph}) = nauty_jll.libnautyTL # C-compatible representation of a sparsenautygraph -mutable struct SparseGraphGraphRep +mutable struct SparseGraphRep nde::Csize_t v::Ptr{Csize_t} nv::Cint @@ -87,19 +100,24 @@ mutable struct SparseGraphGraphRep elen::Csize_t wlen::Csize_t end -function Base.cconvert(::Type{Ref{SparseGraphGraphRep}}, sref::Ref{<:SparseNautyGraph}) +function SparseGraphRep() + return SparseGraphRep(0, C_NULL, 0, C_NULL, C_NULL, C_NULL, 0, 0, 0, 0) +end + +function Base.cconvert(::Type{Ref{SparseGraphRep}}, sref::Ref{<:SparseNautyGraph}) s = sref[] - cstr = SparseGraphGraphRep(s.nde, pointer(s.v), s.nv, pointer(s.d), pointer(s.e), C_NULL, length(s.v), length(s.d), length(s.e), 0) + cstr = SparseGraphRep(s.nde, pointer(s.v), s.nv, pointer(s.d), pointer(s.e), C_NULL, length(s.v), length(s.d), length(s.e), 0) return (s, cstr) end -function Base.unsafe_convert(::Type{Ref{SparseGraphGraphRep}}, x::Tuple{<:SparseNautyGraph,SparseGraphGraphRep}) +function Base.unsafe_convert(::Type{Ref{SparseGraphRep}}, x::Tuple{<:SparseNautyGraph,SparseGraphRep}) _, cstr = x - return convert(Ptr{SparseGraphGraphRep}, pointer_from_objref(cstr)) + return convert(Ptr{SparseGraphRep}, pointer_from_objref(cstr)) end + @generated function sortlists!(g::SparseNautyGraph) # Sort the lists in the graph rep into some reference order return quote - @ccall $(libnauty(g)).sortlists_sg(Ref(g)::Ref{SparseGraphGraphRep})::Cvoid + @ccall $(libnauty(g)).sortlists_sg(Ref(g)::Ref{SparseGraphRep})::Cvoid end end @@ -122,16 +140,22 @@ function Base.hash(g::SparseNautyGraph, h::UInt) return hash(g.labels, hash(vertices(g), hash(edges(g), h))) end -function Base.:(==)(g::SparseNautyGraph{D1}, h::SparseNautyGraph{D2}) where {D1, D2} - return D1 == D2 && +@generated function Base.:(==)(g::SparseNautyGraph{D1}, h::SparseNautyGraph{D2}) where {D1, D2} + return quote D1 == D2 && labels(g) == labels(h) && - Bool(@ccall ll.aresame_sg(Ref(g)::Ref{SparseGraphGraphRep}, Ref(h)::Ref{SparseGraphGraphRep})::Cint) + Bool(@ccall $(libnauty(g)).aresame_sg(Ref(g)::Ref{SparseGraphRep}, Ref(h)::Ref{SparseGraphRep})::Cint) + end end Graphs.nv(g::SparseNautyGraph) = g.nv function Graphs.ne(g::SparseNautyGraph) - nv(g) == 0 && return 0 - return is_directed(g) ? g.nde : (g.nde + sum(has_edge(g, i, i) for i in vertices(g))) ÷ 2 + if nv(g) == 0 + return 0 + elseif is_directed(g) + return g.nde + else + return (g.nde + sum(has_edge(g, i, i) for i in vertices(g))) ÷ 2 + end end Graphs.vertices(g::SparseNautyGraph) = Base.OneTo(g.nv) Graphs.has_vertex(g::SparseNautyGraph, v::Integer) = v ∈ vertices(g) @@ -239,7 +263,6 @@ function Base.:(==)(e1::SimpleEdgeIter{<:SparseNautyGraph}, e2::SimpleEdgeIter{< end Base.:(==)(e1::SimpleEdgeIter{<:Graphs.SimpleGraphs.AbstractSimpleGraph}, e2::SimpleEdgeIter{<:SparseNautyGraph}) = e2 == e1 - Graphs.is_directed(::SparseNautyGraph{D}) where {D} = D Graphs.is_directed(::Type{SparseNautyGraph{D}}) where {D} = D @@ -378,3 +401,20 @@ end Graphs.rem_vertex!(g::SparseNautyGraph, i::Integer) = rem_vertices!(g, (i,)) +function _unsafe_sparsegraphcopy!(g::SparseNautyGraph, srep::SparseGraphRep) + copy!(g.e, unsafe_wrap(Array, srep.e, srep.elen)) + copy!(g.v, unsafe_wrap(Array, srep.v, srep.vlen)) + copy!(g.d, unsafe_wrap(Array, srep.d, srep.dlen)) + return +end +function _free_sparsegraph(srep::SparseGraphRep) + _sparsenautyfree(srep.e) + _sparsenautyfree(srep.v) + _sparsenautyfree(srep.d) + return +end +@generated function _sparsenautyfree(arr::Ptr{T}) where {T} + return quote + @ccall $(libnauty(SparseNautyGraph)).free(arr::Ptr{T})::Cvoid + end +end \ No newline at end of file From 8c8d5a3fc30ae71a9b10e612edd1ec2db8e2435e Mon Sep 17 00:00:00 2001 From: Maximilian HUEBL Date: Wed, 29 Oct 2025 11:43:16 +0100 Subject: [PATCH 10/11] rename of helper functions --- src/nauty.jl | 6 +++--- src/sparsenautygraph.jl | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/nauty.jl b/src/nauty.jl index 5ec7ae2..acf17aa 100644 --- a/src/nauty.jl +++ b/src/nauty.jl @@ -144,7 +144,7 @@ function _sethash!(g::SparseNautyGraph, canong::SparseGraphRep, canonperm) return end function _canonize!(g::SparseNautyGraph, canong::SparseGraphRep, canonperm) - _unsafe_sparsegraphcopy!(g, canong) + _unsafe_copyfromsparsegraphrep!(g, canong) permute!(g.labels, canonperm) return end @@ -171,7 +171,7 @@ function nauty(g::AbstractNautyGraph, options::NautyOptions=default_options(g); canonize && _canonize!(g, canong, canonperm) # free memory allocated by nauty for sparse graphs - canong isa SparseGraphRep && _free_sparsegraph(canong) + canong isa SparseGraphRep && _free_sparsegraphrep(canong) return canonperm, autg end @@ -192,7 +192,7 @@ end """ canonical_permutation(g::AbstractNautyGraph) -Return the permutation `p` needed to canonize `g`. This permutation satisfies `g[p] = canong`. +Return the permutation `p` needed to canonize `g`. This permutation satisfies `g[p] == canong`. """ function canonical_permutation(::AbstractNautyGraph) end diff --git a/src/sparsenautygraph.jl b/src/sparsenautygraph.jl index 4fd913d..dab7166 100644 --- a/src/sparsenautygraph.jl +++ b/src/sparsenautygraph.jl @@ -401,13 +401,13 @@ end Graphs.rem_vertex!(g::SparseNautyGraph, i::Integer) = rem_vertices!(g, (i,)) -function _unsafe_sparsegraphcopy!(g::SparseNautyGraph, srep::SparseGraphRep) +function _unsafe_copyfromsparsegraphrep!(g::SparseNautyGraph, srep::SparseGraphRep) copy!(g.e, unsafe_wrap(Array, srep.e, srep.elen)) copy!(g.v, unsafe_wrap(Array, srep.v, srep.vlen)) copy!(g.d, unsafe_wrap(Array, srep.d, srep.dlen)) return end -function _free_sparsegraph(srep::SparseGraphRep) +function _free_sparsegraphrep(srep::SparseGraphRep) _sparsenautyfree(srep.e) _sparsenautyfree(srep.v) _sparsenautyfree(srep.d) From 1d6193b3b945b2d9aa769ea2daac3c92fdb00ebd Mon Sep 17 00:00:00 2001 From: Maximilian HUEBL Date: Wed, 29 Oct 2025 11:43:38 +0100 Subject: [PATCH 11/11] graph constructor changes --- src/densenautygraph.jl | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/densenautygraph.jl b/src/densenautygraph.jl index 5c171d0..83644d3 100644 --- a/src/densenautygraph.jl +++ b/src/densenautygraph.jl @@ -47,25 +47,24 @@ The graph can be directed (`D = true`) or undirected (`D = false`). If `D = fals Vertex labels can optionally be specified. """ function DenseNautyGraph{D,W}(A::AbstractMatrix; vertex_labels=nothing) where {D,W<:Unsigned} + n, m = size(A) + isequal(n, m) || throw(ArgumentError("Adjacency / distance matrices must be square")) D || issymmetric(A) || throw(ArgumentError("Adjacency / distance matrices must be symmetric")) graphset = Graphset{W}(A) return DenseNautyGraph{D}(graphset; vertex_labels) end DenseNautyGraph{D}(A::AbstractMatrix; vertex_labels=nothing) where {D} = DenseNautyGraph{D,UInt}(A; vertex_labels) -function (::Type{G})(g::AbstractGraph) where {G<:AbstractNautyGraph} - ng = G(nv(g)) +function (::Type{G})(g::AbstractGraph) where {G<:DenseNautyGraph} + ng = g isa AbstractNautyGraph ? G(nv(g); vertex_labels=labels(g)) : G(nv(g)) for e in edges(g) add_edge!(ng, e) - !is_directed(g) && is_directed(ng) && add_edge!(ng, reverse(e)) + if !is_directed(g) && is_directed(ng) + add_edge!(ng, reverse(e)) + end end return ng end -function (::Type{G})(g::AbstractNautyGraph) where {G<:AbstractNautyGraph} - h = invoke(G, Tuple{AbstractGraph}, g) - @views h.labels .= g.labels - return h -end """ DenseNautyGraph{D}(edge_list::Vector{<:AbstractEdge}; [vertex_labels]) where {D}