diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 1bee369..ace31d2 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -20,3 +20,5 @@ jobs: with: julia-version: '1' tune: 'false' + script: benchmark/benchmarks.jl + extra-pkgs: https://github.com/mahmudsudo/Graphs.jl.git#very_nauty diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60a3d6e..06e3ef1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,11 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 + - name: Use Graphs.jl very_nauty branch + shell: julia --color=yes --project=. {0} + run: | + using Pkg + Pkg.add(url="https://github.com/mahmudsudo/Graphs.jl.git", rev="very_nauty") - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 env: @@ -52,18 +57,4 @@ jobs: with: file: lcov.info token: ${{ secrets.CODECOV_TOKEN }} - docs: - name: Documentation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: julia-actions/setup-julia@v2 - with: - version: '1' - - uses: julia-actions/cache@v2 - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-docdeploy@v1 - env: - GKSwstype: nul # Fix for Plots with GR backend. - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} + diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..f8e61f1 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,6 @@ +# .typos.toml +[default] +extend-ignore-re = [ + "(?i)very_nauty_jll", + "(?i)vngraph", +] diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..190d8cb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to `VNGraphs.jl` will be documented in this file. + +## [Unreleased] +- Implementing `Graphs.jl` interface including stubs for graph coloring algorithms. +- Providing `edge_chromatic_number` dispatch implementations via `very_nauty_jll`. diff --git a/Project.toml b/Project.toml index 0e99a46..ca9bb23 100644 --- a/Project.toml +++ b/Project.toml @@ -1,15 +1,36 @@ name = "VNGraphs" uuid = "82074f20-eae8-4dc7-b23b-c0e0f9981814" -authors = ["Stefan Krastanov "] version = "1.0.0" +authors = ["Stefan Krastanov "] [deps] CBinding = "d43a6710-96b8-4a2d-833c-c424785e5374" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" very_nauty_jll = "be6384bf-868f-57ad-9feb-9e32886bc996" +[extras] +Aqua = "4c88cf16-3921-4891-9df3-0caf3b129712" +GraphsInterfaceChecker = "3bef136c-15ff-4091-acbb-1a4aafe67608" +Interfaces = "85a1e053-48ee-444a-9ef8-e160a0b2cc1c" +JET = "c3a54625-ed41-48e0-a4ff-191f74bb3f5d" +StableRNGs = "860ef19b-820b-49d6-a0d0-8fbd23ad0ade" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestItemRunner = "f8b46487-219e-4ad5-845a-35d30081ef61" +BenchmarkTools = "6e4b44f1-6d23-5a04-9844-338276f71052" + +[targets] +test = ["Aqua", "GraphsInterfaceChecker", "Interfaces", "JET", "StableRNGs", "Test", "TestItemRunner", "BenchmarkTools"] + [compat] +Aqua = "0.8" CBinding = "1.1.0" Graphs = "1.12.0" +GraphsInterfaceChecker = "0.1" +Interfaces = "0.3" +JET = "0.9, 0.10, 0.11" +StableRNGs = "1" +Test = "1" +TestItemRunner = "1" julia = "1.10" very_nauty_jll = "1.1.2" +BenchmarkTools = "1.5.0" diff --git a/README.md b/README.md index f9ada7d..cfd5f4a 100644 --- a/README.md +++ b/README.md @@ -1 +1,31 @@ -A thin wrapper around the C graphs library [`very_nauty`](https://github.com/JuliaGraphs/very_nauty/). +# VNGraphs.jl + +A thin wrapper around the C graphs library [`very_nauty`](https://github.com/JuliaGraphs/very_nauty/), providing high-performance graph algorithms for the `Graphs.jl` ecosystem. + +## Features + +- **High Performance**: Direct C-bindings for core graph operations. +- **Graphs.jl Integration**: Fully implements the `AbstractGraph` interface. +- **Specialized Dispatch**: Use `VNAlgorithm()` to dispatch existing `Graphs.jl` functions to the `very_nauty` implementation. + +## Usage + +```julia +using Graphs, VNGraphs + +# Create a VNGraph +g = VNGraph(5) +add_edge!(g, 1, 2) + +# Use standard Graphs.jl algorithms +c = chromatic_number(g) + +# Dispatch explicitly using VNAlgorithm +c = chromatic_number(g, VNAlgorithm()) +``` + +## Installation + +```julia +import Pkg; Pkg.add("VNGraphs") +``` diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl new file mode 100644 index 0000000..2003381 --- /dev/null +++ b/benchmark/benchmarks.jl @@ -0,0 +1,21 @@ +using BenchmarkTools +using VNGraphs +using Graphs + +const SUITE = BenchmarkGroup() + +# Clique number benchmark +SUITE["clique"] = BenchmarkGroup() +for n in [10, 50, 100] + g = complete_graph(n) + vng = VNGraph(g) + SUITE["clique"][n] = @benchmarkable clique_number($vng) +end + +# Chromatic number benchmark +SUITE["chromatic"] = BenchmarkGroup() +for n in [5, 10, 15] + g = cycle_graph(n) + vng = VNGraph(g) + SUITE["chromatic"][n] = @benchmarkable chromatic_number($vng) +end diff --git a/src/VNGraphs.jl b/src/VNGraphs.jl index 4d51c16..f74df27 100644 --- a/src/VNGraphs.jl +++ b/src/VNGraphs.jl @@ -3,6 +3,7 @@ module VNGraphs export VNGraph import Graphs +import Graphs: clique_number, chromatic_number, edge_chromatic_number import very_nauty_jll using CBinding: @c_cmd, @c_str @@ -42,8 +43,25 @@ graph_add_edge(g::VNGraph,i::Integer,j::Integer) = c"graph_add_edge"(g.ptr,i,j) graph_del_edge(g::VNGraph,i::Integer,j::Integer) = c"graph_del_edge"(g.ptr,i,j) graph_has_edge(g::VNGraph,i::Integer,j::Integer) = c"graph_has_edge"(g.ptr,i,j) graph_add_node(g::VNGraph) = c"graph_add_node"(g.ptr) -nnodes(g::VNGraph) = g.ptr.nnodes[] # c"nnodes"(g.ptr) -nedges(g::VNGraph) = g.ptr.nedges[] # c"nedges"(g.ptr) +nnodes(g::VNGraph) = Int(unsafe_load(reinterpret(Ptr{Cuint}, Base.unsafe_convert(Ptr{Cvoid}, g.ptr)), 13)) # Offset 48 +nedges(g::VNGraph) = Int(unsafe_load(reinterpret(Ptr{Cuint}, Base.unsafe_convert(Ptr{Cvoid}, g.ptr)), 14)) # Offset 52 + +# Robust field accessors for pointers (8-byte aligned) +function get_d_ptr(g::VNGraph) + return unsafe_load(reinterpret(Ptr{Ptr{Cuint}}, Base.unsafe_convert(Ptr{Cvoid}, g.ptr)), 2) # Offset 8 +end + +function get_a_ptr(g::VNGraph) + return unsafe_load(reinterpret(Ptr{Ptr{Ptr{Cuint}}}, Base.unsafe_convert(Ptr{Cvoid}, g.ptr)), 1) # Offset 0 +end + +function get_c_ptr(g::VNGraph) + return unsafe_load(reinterpret(Ptr{Ptr{Cint}}, Base.unsafe_convert(Ptr{Cvoid}, g.ptr)), 5) # Offset 32 +end + +function get_l_ptr(g::VNGraph) + return unsafe_load(reinterpret(Ptr{Ptr{Cint}}, Base.unsafe_convert(Ptr{Cvoid}, g.ptr)), 6) # Offset 40 +end graph_node_degree(g::VNGraph, i::Integer) = c"graph_node_degree"(g.ptr, i) graph_min_degree(g::VNGraph) = c"graph_min_degree"(g.ptr) @@ -52,10 +70,10 @@ graph_mean_degree(g::VNGraph) = c"graph_mean_degree"(g.ptr) graph_show(g::VNGraph) = c"graph_show"(g.ptr) -graph_nclusters(g::VNGraph) = c"graph_nclusters"(g.ptr) -graph_connected(g::VNGraph) = c"graph_connected"(g.ptr) +graph_nclusters(g::VNGraph) = Int(c"graph_nclusters"(g.ptr)) +graph_connected(g::VNGraph) = c"graph_connected"(g.ptr) != 0 -cluster(g::VNGraph,i::Integer) = g.ptr.l[][i] +cluster(g::VNGraph,i::Integer) = unsafe_load(get_l_ptr(g), i+1) graph_cluster_sizes(g::VNGraph) = c"graph_cluster_sizes"(g.ptr) graph_max_cluster(g::VNGraph) = c"graph_max_cluster"(g.ptr) @@ -77,21 +95,26 @@ graph_local_complement(g::VNGraph, i::Integer) = c"graph_local_complement"(g.ptr graph_sequential_color_repeat(g::VNGraph, n::Integer) = c"graph_sequential_color_repeat"(g.ptr, n) graph_chromatic_number(g::VNGraph, timeout) = c"graph_chromatic_number"(g.ptr, timeout) graph_edge_chromatic_number(g::VNGraph, timeout) = c"graph_edge_chromatic_number"(g.ptr, timeout) -color(g::VNGraph,i) = g.ptr.c[][i] +color(g::VNGraph,i) = unsafe_load(get_c_ptr(g), i+1) graph_ncolors(g::VNGraph) = c"graph_ncolors"(g.ptr) graph_check_coloring(g::VNGraph) = c"graph_check_coloring"(g.ptr) function Graphs.SimpleGraphs.SimpleGraph(vng::VNGraph) n = nnodes(vng) - g = Graphs.SimpleGraphs.SimpleGraph{Int}(n) - for i in 1:nnodes(vng) - for k in 1:vng.ptr.d[][i] - j = vng.ptr.a[][i][k]+1 - i nnodes(g) || d < 1 || d > nnodes(g)) && return false + return graph_has_edge(g, s-1, d-1) != 0 +end + +Graphs.has_vertex(g::VNGraph, n::Integer) = 1≤n≤nnodes(g) + +function Graphs.outneighbors(g::VNGraph, v::Integer) + (v < 1 || v > nnodes(g)) && return Cuint[] + d = unsafe_load(get_d_ptr(g), v) + a_v_ptr = unsafe_load(get_a_ptr(g), v) + return [unsafe_load(a_v_ptr, k) + 1 for k in 1:d] +end + +Graphs.inneighbors(g::VNGraph, v::Integer) = Graphs.outneighbors(g, v) +Graphs.neighbors(g::VNGraph, v::Integer) = Graphs.outneighbors(g, v) + Graphs.is_directed(::Type{VNGraph}) = false Graphs.ne(g::VNGraph) = nedges(g) Graphs.nv(g::VNGraph) = nnodes(g) -# Graphs.outneighbors # TODO -Graphs.vertices(g::VNGraph) = 1:nnodes(g) +Graphs.vertices(g::VNGraph) = UnitRange{Cuint}(1, nnodes(g)) + +function Graphs.add_edge!(g::VNGraph, s::Integer, d::Integer) + (s < 1 || s > nnodes(g) || d < 1 || d > nnodes(g)) && return false + graph_has_edge(g, s-1, d-1) != 0 && return false + graph_add_edge(g, s-1, d-1) + return true +end + +Graphs.add_edge!(g::VNGraph, e::Graphs.SimpleGraphEdge) = Graphs.add_edge!(g, e.src, e.dst) + +function Graphs.add_vertex!(g::VNGraph) + graph_add_node(g) + return true +end + +function Graphs.rem_edge!(g::VNGraph, s::Integer, d::Integer) + (s < 1 || s > nnodes(g) || d < 1 || d > nnodes(g)) && return false + graph_has_edge(g, s-1, d-1) == 0 && return false + graph_del_edge(g, s-1, d-1) + return true +end + +function Graphs.degree(g::VNGraph, v::Integer) + (v < 1 || v > nnodes(g)) && return 0 + return Int(c"graph_node_degree"(g.ptr, v-1)) +end + +# Algorithm dispatch +struct VNAlgorithm end +export VNAlgorithm + +clique_number(g::VNGraph) = graph_clique_number(g) +clique_number(g::VNGraph, ::VNAlgorithm) = clique_number(g) +clique_number(g::Graphs.AbstractGraph, ::VNAlgorithm) = graph_clique_number(VNGraph(g)) + +function chromatic_number(g::VNGraph; timeout=0) + return graph_chromatic_number(g, timeout) +end +chromatic_number(g::VNGraph, ::VNAlgorithm; timeout=0) = chromatic_number(g; timeout=timeout) +chromatic_number(g::Graphs.AbstractGraph, ::VNAlgorithm; timeout=0) = chromatic_number(VNGraph(g); timeout=timeout) + +function edge_chromatic_number(g::VNGraph; timeout=0) + return graph_edge_chromatic_number(g, timeout) +end +edge_chromatic_number(g::VNGraph, ::VNAlgorithm; timeout=0) = edge_chromatic_number(g; timeout=timeout) +edge_chromatic_number(g::Graphs.AbstractGraph, ::VNAlgorithm; timeout=0) = edge_chromatic_number(VNGraph(g); timeout=timeout) + +function Graphs.connected_components(g::VNGraph) + n = nnodes(g) + n_clusters = graph_nclusters(g) + comps = [Int[] for _ in 1:n_clusters] + for i in 1:n + c = cluster(g, i-1) + push!(comps[c+1], i) + end + return comps +end +Graphs.connected_components(g::Graphs.AbstractGraph, ::VNAlgorithm) = Graphs.connected_components(VNGraph(g)) -Graphs.add_edge!(g::VNGraph, e::Graphs.SimpleGraphEdge) = graph_add_edge(g,e.src-1,e.dst-1) +# Export VNAlgorithm for user convenience +export VNAlgorithm end diff --git a/test/Project.toml b/test/Project.toml index 4a6a6d8..721ab5b 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -3,6 +3,8 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +GraphsInterfaceChecker = "3bef136c-15ff-4091-acbb-1a4aafe67608" +Interfaces = "85a1e053-f937-4924-92a5-1367d23b7b87" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" diff --git a/test/test_interface.jl b/test/test_interface.jl new file mode 100644 index 0000000..1c76529 --- /dev/null +++ b/test/test_interface.jl @@ -0,0 +1,34 @@ +@testitem "Interface Checker" begin + using VNGraphs + using GraphsInterfaceChecker + using Interfaces + import Graphs + + # Define some test graphs + test_graphs = [ + VNGraph(0), + VNGraph(1), + VNGraph(5), + begin + g = VNGraph(5) + Graphs.add_edge!(g, 1, 2) + Graphs.add_edge!(g, 2, 3) + Graphs.add_edge!(g, 3, 4) + Graphs.add_edge!(g, 4, 1) + g + end, + begin + g = VNGraph(10) + for i in 1:9 + Graphs.add_edge!(g, i, i+1) + end + g + end + ] + + # Declare implementation + Interfaces.@implements AbstractGraphInterface VNGraph test_graphs + + # Run tests + @test Interfaces.test(AbstractGraphInterface, VNGraph) +end