Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ scratch/
.vscode
test/.CondaPkg/
docs/.CondaPkg/
docs/build/
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,56 @@
# News

## unreleased

- Require `LEMON_jll` 1.3.6 and move to its parameterized CxxWrap bindings
(`ListGraphEdgeMap{T}`, `ListDigraphArcMap{T}`, `NetworkSimplex{V,C}`, ...),
replacing the `…Int`-suffixed names of earlier releases
- `LEMONGraph`/`LEMONDiGraph` now cache edge endpoints and adjacency lists, so
`has_edge`, `inneighbors` and `outneighbors` are O(1)-ish instead of scanning
every edge through the C++ boundary
- `edges` returns a lazy edge iterator, and `edgetype` returns the concrete
`Edge{Int}`; undirected edges are normalized to `src <= dst`, as in
`SimpleGraph`
- Complete the `AbstractGraph` interface: `LEMONGraph(n)`/`LEMONDiGraph(n)`,
`zero`, `copy`, `add_vertex!`, `add_edge!` and `reverse`, so that generic
algorithms such as `induced_subgraph` and `egonet` work on the wrappers.
`rem_edge!`/`rem_vertex!` return `false` because `LEMON_jll` exposes no
`ListGraph::erase`. Both the mandatory and the optional `mutation`
components of `AbstractGraphInterface` now pass
- Add `Graph(::LEMONGraph)` / `DiGraph(::LEMONDiGraph)` conversions back to
Graphs.jl, plus the matching `convert` methods
- LEMON `dijkstra_shortest_paths` is now a drop-in replacement for the
Graphs.jl one: multiple sources, `allpaths`, `trackvertices` and `maxdist`
are supported, and `pathcounts`/`predecessors` are computed exactly
(including `Inf` counts behind zero-weight cycles)
- Validate Dijkstra inputs: non-negative integer weights that fit a C++ `int`
- Implement `GraphsOptim.min_cost_flow` on LEMON's minimum cost flow solvers,
selectable with `solver = :network_simplex` (default), `:capacity_scaling`,
`:cost_scaling` or `:cycle_canceling`; infinite upper capacities are accepted
- Implement `GraphsOptim.min_cost_assignment` on top of the same solvers,
replacing the previous stub
- Register an error hint so that unsupported `LEMONAlgorithm()` calls explain
the integer-only restriction instead of raising a bare `MethodError`
- Raise the `GraphsMatching` compat bound to 0.2.1, which removes a method
ambiguity with its `cutoff` argument
- Run the GraphsInterfaceChecker suite as a `@testitem` (it was previously
never collected), and cross-check Dijkstra, minimum cost flow and assignment
against Graphs.jl and the JuMP-based GraphsOptim solvers
- Expand the documentation: graph types, algorithm dispatch, and a guide for
adding further LEMON algorithms
- Track arc endpoints on the Julia side in the Dijkstra wrapper instead of
asking LEMON for them arc by arc; this makes it ~2x faster from a
`SimpleGraph` and ~3x faster from a pre-built `LEMONDiGraph`
- Benchmark conversions, the `AbstractGraph` API and Dijkstra, not just
maximum weight perfect matching
- Add the `LEMONAlgorithm` marker type, which picks the LEMON implementation
when passed as the last positional argument
- Add a GraphsMatching.jl extension implementing
`minimum_weight_perfect_matching` on LEMON, for integer and floating point
weights
- Add a GraphsOptim.jl extension implementing `shortest_path` on LEMON's
Dijkstra

## v0.1.2 - 2026-09-09

- Fix maximum-weight perfect matching broken by a change to the parametric map types in LEMON_jll 1.3.6.
Expand Down
14 changes: 13 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
name = "LEMONGraphs"
uuid = "14b1564f-c77f-4800-9e89-efd961faef7c"
authors = ["Stefan Krastanov <stefan@krastanov.org>"]
version = "0.1.2"
version = "0.1.3"

[deps]
CxxWrap = "1f15a43c-97ca-5a2a-ae31-89f07a497df4"
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
LEMON_jll = "9f9b04fa-cfb6-5331-975f-45512019a816"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

[weakdeps]
GraphsMatching = "c3af3a8c-b79e-4b01-bf44-c718d7e0e0d6"
GraphsOptim = "e79ef3ae-79c8-4b97-b165-63f338db30c2"

[extensions]
LEMONGraphsGraphsMatchingExt = "GraphsMatching"
LEMONGraphsGraphsOptimExt = "GraphsOptim"

[compat]
CxxWrap = "0.17.1"
Graphs = "1.12.0"
LEMON_jll = "1.3.6"
GraphsMatching = "0.2.1"
GraphsOptim = "0.1"
SparseArrays = "1"
julia = "1.10"
121 changes: 116 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,120 @@
A very thin Julia wrapper around the C++ graphs library [`LEMON`](http://lemon.cs.elte.hu/).
# LEMONGraphs.jl

Currently used mainly for its Min/Max Weight Perfect Matching algorithm,
wrapped into a more user-friendly API in GraphsMatching.jl,
but additional bindings can be added on request.
A Julia wrapper for the C++ graph algorithms library [LEMON](http://lemon.cs.elte.hu/).

No public API is provided yet.
It provides `LEMONGraph` and `LEMONDiGraph`, which implement the Graphs.jl
`AbstractGraph` interface on top of LEMON's `ListGraph` and `ListDigraph`,
conversions in both directions with `O(1)` reuse, and LEMON implementations of
matching, shortest paths, minimum cost flow and assignment. The interface is
checked in the test suite with GraphsInterfaceChecker.jl.

Full documentation: <https://juliagraphs.org/LEMONGraphs.jl/>.

## Installation

```julia
using Pkg
Pkg.add("LEMONGraphs")
```

Depends on `LEMON_jll`, which is compiled and packaged for all platforms supported by Julia.

## Quick Start

```julia
using Graphs, LEMONGraphs

# Convert a Graphs.jl graph to LEMON
g = path_graph(5)
lg = LEMONGraph(g)

# Use LEMON graph like any AbstractGraph
@assert nv(lg) == 5
@assert ne(lg) == 4
@assert Graph(lg) == g

# Use LEMON-backed algorithms by passing LEMONAlgorithm() last
distmx = [abs(i - j) for i in 1:5, j in 1:5]
state = dijkstra_shortest_paths(g, 1, distmx, LEMONAlgorithm())
@assert state.dists == [0, 1, 2, 3, 4]
```

## Usage

### Graph types

```julia
# Undirected
g = complete_graph(4)
lg = LEMONGraph(g)

# Directed
dg = SimpleDiGraph(4)
ldg = LEMONDiGraph(dg)

# All standard Graphs.jl methods work:
nv(lg), ne(lg), vertices(lg), edges(lg)
has_vertex(lg, 1), has_edge(lg, 1, 2)
inneighbors(lg, 1), outneighbors(lg, 1)

# ... and so do generic Graphs.jl algorithms:
connected_components(lg), kruskal_mst(lg), is_strongly_connected(ldg)
```

Adjacency is cached on the Julia side when the wrapper is built, so neighbour
and edge queries are as fast as on a `SimpleGraph` and never cross into C++.
Every component of the `AbstractGraph` interface, the optional `mutation` one
included, is checked in the test suite with GraphsInterfaceChecker.jl.

`add_vertex!` and `add_edge!` grow the underlying LEMON structure. Removal is
the one gap: `LEMON_jll` does not expose `ListGraph::erase`, so `rem_edge!` and
`rem_vertex!` decline by returning `false`.

### Reusing a wrapper

A LEMON-backed algorithm handed an existing `LEMONGraph` or `LEMONDiGraph`
reuses it in `O(1)` rather than converting again:

```julia
g = complete_graph(100)
lg1 = LEMONGraph(g)
w, mate = maxweightedperfectmatching(lg1, ones(Int, ne(lg1))) # no reconversion
```

### Maximum weight perfect matching

```julia
using Graphs, LEMONGraphs

g = complete_graph(6)
weights = Dict(e => rand(-100:100) for e in edges(g))
w, spouse_map = maxweightedperfectmatching(g, weights, LEMONAlgorithm())
```

### Package extensions

- GraphsMatching.jl: `minimum_weight_perfect_matching(g, weights, LEMONAlgorithm())`, for
integer or floating point weight dictionaries.
- GraphsOptim.jl: `shortest_path` (LEMON Dijkstra), `min_cost_flow` (LEMON
`NetworkSimplex`, with `:capacity_scaling`, `:cost_scaling` and
`:cycle_canceling` also available through the `solver` keyword), and
`min_cost_assignment` (square integer cost matrices). `min_vertex_cover` and
`maximum_weight_clique` accept `LEMONAlgorithm()` only to report that LEMON
has no such solver.

All LEMON solvers exposed by `LEMON_jll` are integer-valued, so weights, costs
and capacities must be `Integer`s; a floating point input raises an
`ArgumentError` instead of being silently rounded.

## Testing

```julia
using Pkg
Pkg.test("LEMONGraphs")
```

## References

- LEMON documentation: http://lemon.cs.elte.hu/
- Graphs.jl: https://github.com/JuliaGraphs/Graphs.jl
- Issue #447: [A reliable idiomatic wrapper for the C++ library LEMON](https://github.com/JuliaGraphs/Graphs.jl/issues/447)
1 change: 1 addition & 0 deletions benchmark/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ LEMONGraphs = "14b1564f-c77f-4800-9e89-efd961faef7c"

[compat]
BenchmarkTools = "1"
Graphs = "1.12"
93 changes: 93 additions & 0 deletions benchmark/benchmarks.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
using BenchmarkTools
using Graphs
using LEMONGraphs
using LEMONGraphs: maxweightedperfectmatching

const SUITE = BenchmarkGroup()

# ---------------------------------------------------------------------------
# Maximum weight perfect matching (the original benchmark, kept unchanged so
# that historical comparisons stay meaningful)
# ---------------------------------------------------------------------------

function mwpm_benchmark_input()
g = Graph(6)
add_edge!(g, 1, 2)
Expand All @@ -23,3 +29,90 @@ SUITE["maxweightedperfectmatching"] = @benchmarkable maxweightedperfectmatching(
$MWPM_GRAPH,
$MWPM_WEIGHTS,
)

# ---------------------------------------------------------------------------
# Conversions between Graphs.jl and the LEMON C++ structures
# ---------------------------------------------------------------------------

const CONV_GRAPH = complete_graph(100)
const CONV_DIGRAPH = complete_digraph(70)
const CONV_LEMON = LEMONGraph(CONV_GRAPH)
const CONV_LEMON_DI = LEMONDiGraph(CONV_DIGRAPH)

SUITE["conversion"] = BenchmarkGroup()
SUITE["conversion"]["to LEMONGraph"] = @benchmarkable LEMONGraph($CONV_GRAPH)
SUITE["conversion"]["to LEMONDiGraph"] = @benchmarkable LEMONDiGraph($CONV_DIGRAPH)
SUITE["conversion"]["back to Graph"] = @benchmarkable Graph($CONV_LEMON)
SUITE["conversion"]["back to DiGraph"] = @benchmarkable DiGraph($CONV_LEMON_DI)
# O(1) reuse: handing a wrapper to a LEMON algorithm must not re-convert it
SUITE["conversion"]["reuse a wrapper"] = @benchmarkable LEMONGraphs.to_list_graph(
$CONV_LEMON
)

# ---------------------------------------------------------------------------
# AbstractGraph API, the inner loop of every generic Graphs.jl algorithm
# ---------------------------------------------------------------------------

function neighbor_sweep(g)
total = 0
for v in vertices(g), u in outneighbors(g, v)
total += u
end
return total
end

function edge_sweep(g)
total = 0
for e in edges(g)
total += src(e) + dst(e)
end
return total
end

function has_edge_sweep(g, n)
total = 0
for u in 1:n, v in 1:n
total += has_edge(g, u, v)
end
return total
end

const API_GRAPH = grid([25, 25])
const API_LEMON = LEMONGraph(API_GRAPH)

SUITE["api"] = BenchmarkGroup()
for (name, g) in ("LEMONGraph" => API_LEMON, "SimpleGraph" => API_GRAPH)
SUITE["api"]["outneighbors ($name)"] = @benchmarkable neighbor_sweep($g)
SUITE["api"]["edges ($name)"] = @benchmarkable edge_sweep($g)
SUITE["api"]["has_edge ($name)"] = @benchmarkable has_edge_sweep($g, 60)
end
# a generic Graphs.jl algorithm driven entirely through the interface
SUITE["api"]["connected_components (LEMONGraph)"] = @benchmarkable connected_components(
$API_LEMON
)

# ---------------------------------------------------------------------------
# Dijkstra: the LEMON backend against the native Graphs.jl one
# ---------------------------------------------------------------------------

const DIJKSTRA_GRAPH = grid([20, 20])
const DIJKSTRA_LEMON = LEMONDiGraph(DIJKSTRA_GRAPH)
const DIJKSTRA_WEIGHTS = [
1 + ((i + j) % 7) for i in 1:nv(DIJKSTRA_GRAPH), j in 1:nv(DIJKSTRA_GRAPH)
]

SUITE["dijkstra"] = BenchmarkGroup()
SUITE["dijkstra"]["Graphs.jl"] = @benchmarkable dijkstra_shortest_paths(
$DIJKSTRA_GRAPH, 1, $DIJKSTRA_WEIGHTS
)
# from a SimpleGraph the LEMON backend has to build the C++ digraph first ...
SUITE["dijkstra"]["LEMON (converting)"] = @benchmarkable dijkstra_shortest_paths(
$DIJKSTRA_GRAPH, 1, $DIJKSTRA_WEIGHTS, LEMONAlgorithm()
)
# ... whereas a wrapper it already holds is reused as is
SUITE["dijkstra"]["LEMON (prebuilt wrapper)"] = @benchmarkable dijkstra_shortest_paths(
$DIJKSTRA_LEMON, 1, $DIJKSTRA_WEIGHTS, LEMONAlgorithm()
)
SUITE["dijkstra"]["LEMON (multi-source)"] = @benchmarkable dijkstra_shortest_paths(
$DIJKSTRA_GRAPH, [1, 5], $DIJKSTRA_WEIGHTS, LEMONAlgorithm()
)
7 changes: 7 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ using LEMONGraphs
makedocs(
sitename = "LEMONGraphs.jl",
modules = [LEMONGraphs],
# internal helpers carry docstrings for the sake of contributors without
# being part of the manual
checkdocs = :exports,
repo = "https://github.com/JuliaGraphs/LEMONGraphs.jl/blob/{commit}{path}#L{line}",
format = Documenter.HTML(
repolink = "https://github.com/JuliaGraphs/LEMONGraphs.jl",
),
pages = [
"Home" => "index.md",
"Graph types and conversions" => "graphs.md",
"LEMON-backed algorithms" => "algorithms.md",
"Adding another LEMON algorithm" => "extending.md",
"API reference" => "api.md",
],
)

Expand Down
Loading
Loading