Skip to content

use LEMONGraphs.jl as a default algorithm for MWPM instead of BlossomV.jl #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 7, 2025
Merged
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
15 changes: 15 additions & 0 deletions .github/changelog-enforcer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: "Changelog Enforcer"
on:
pull_request:
# The specific activity types are listed here to include "labeled" and "unlabeled"
# (which are not included by default for the "pull_request" trigger).
# This is needed to allow skipping enforcement of the changelog in PRs with specific labels,
# as defined in the (optional) "skipLabels" property.
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]

jobs:
# Enforces the update of a changelog file on every pull request
changelog:
runs-on: ubuntu-latest
steps:
- uses: dangoslen/changelog-enforcer@v3
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ jobs:
- windows-latest
arch:
- x64
include:
- arch: aarch64
os: macos-latest
version: '1'
threads: '1'
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# News

## 0.2.1 - 2025-07-05

- Introduce LEMONGraphs as an option for running `minimum_weight_perfect_matching` and make it default.
- Move the BlossomV dispatch of `minimum_weight_perfect_matching` to a package extension as an optional dependency, thus making it possible to install GraphsMatching on systems where BlossomV does not install.

## pre 0.2.0

Not tracked
13 changes: 10 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
name = "GraphsMatching"
uuid = "c3af3a8c-b79e-4b01-bf44-c718d7e0e0d6"
authors = ["JuliaGraphs"]
version = "0.2.0"
version = "0.2.1"

[deps]
BlossomV = "6c721016-9dae-5d90-abf6-67daaccb2332"
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
Hungarian = "e91730f6-4275-51fb-a7a0-7064cfbd3b39"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
LEMONGraphs = "14b1564f-c77f-4800-9e89-efd961faef7c"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

[weakdeps]
BlossomV = "6c721016-9dae-5d90-abf6-67daaccb2332"

[extensions]
GraphsMatchingBlossomVExt = "BlossomV"

[compat]
BlossomV = "0.4"
Graphs = "1.4, 1.7"
Hungarian = "0.4, 0.6"
JuMP = "1"
LEMONGraphs = "0.1.0"
MathOptInterface = "1"
julia = "1"
julia = "1.10"

[extras]
Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
Expand Down
26 changes: 26 additions & 0 deletions ext/GraphsMatchingBlossomVExt/GraphsMatchingBlossomVExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module GraphsMatchingBlossomVExt

using Graphs
using GraphsMatching
import BlossomV

function GraphsMatching.minimum_weight_perfect_matching(g::Graph, w::Dict{E,U}, ::BlossomVAlgorithm) where {U<:Integer,E<:Edge}
m = BlossomV.Matching(nv(g))
for (e, c) in w
BlossomV.add_edge(m, src(e) - 1, dst(e) - 1, c)
end
BlossomV.solve(m)

mate = fill(-1, nv(g))
totweight = zero(U)
for i in 1:nv(g)
j = BlossomV.get_match(m, i - 1) + 1
mate[i] = j <= 0 ? -1 : j
if i < j
totweight += w[Edge(i, j)]
end
end
return MatchingResult(totweight, mate)
end

end
24 changes: 21 additions & 3 deletions src/GraphsMatching.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,28 @@ using SparseArrays: spzeros
using JuMP
using MathOptInterface
const MOI = MathOptInterface
import BlossomV # 'using BlossomV' leads to naming conflicts with JuMP
import LEMONGraphs
using Hungarian

export MatchingResult, maximum_weight_matching, maximum_weight_matching_reduction, maximum_weight_maximal_matching, minimum_weight_perfect_matching, HungarianAlgorithm, LPAlgorithm
export MatchingResult,
maximum_weight_matching,
maximum_weight_matching_reduction,
maximum_weight_maximal_matching,
minimum_weight_perfect_matching,
HungarianAlgorithm,
LPAlgorithm,
BlossomVAlgorithm,
LEMONMWPMAlgorithm

function __init__()
if isdefined(Base.Experimental, :register_error_hint)
Base.Experimental.register_error_hint(MethodError) do io, exc, argtypes, kwargs
if BlossomVAlgorithm in argtypes
print(io,"""\nPlease first import `BlossomV` to make BlossomV-based matching available.""")
end
end
end
end

"""
struct MatchingResult{U}
Expand All @@ -32,7 +50,7 @@ end

include("lp.jl")
include("maximum_weight_matching.jl")
include("blossomv.jl")
include("minimum_weight_perfect_matching.jl")
include("hungarian.jl")
include("maximum_weight_maximal_matching.jl")

Expand Down
75 changes: 0 additions & 75 deletions src/blossomv.jl

This file was deleted.

119 changes: 119 additions & 0 deletions src/minimum_weight_perfect_matching.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
minimum_weight_perfect_matching(g, w::Dict{Edge,Real}; tmaxscale)
minimum_weight_perfect_matching(g, w::Dict{Edge,Real}, cutoff; tmaxscale)
minimum_weight_perfect_matching(g, w::Dict{Edge,Real}, algorithm::AbstractMinimumWeightPerfectMatchingAlgorithm; tmaxscale)
minimum_weight_perfect_matching(g, w::Dict{Edge,Real}, cutoff, algorithm::AbstractMinimumWeightPerfectMatchingAlgorithm; tmaxscale)
Given a graph `g` and an edgemap `w` containing weights associated to edges,
returns a matching with the mimimum total weight among the ones containing
exactly `nv(g)/2` edges.
Edges in `g` not present in `w` will not be considered for the matching.
You can use the `algorithm` argument to specify the algorithm to use.
A `cutoff` argument can be given, to reduce the computational time by
excluding edges with weights higher than the cutoff (effective only for some algorithms,
not for the default `LEMONMWPMAlgorithm`).
When the weights are non-integer types, the keyword argument `tmaxscale` can be used to
scale the weights to integer values.
In case of error try to change `tmaxscale` (default is `tmaxscale=10`).
The scaling is as follows:
```
tmax = typemax(Int32) / tmaxscale
weight = round(Int32, (weight-minimum_weight) / max(maximum_weight-minimum_weight, 1) * tmax)
```
The returned object is of type [`MatchingResult`](@ref).
See also: [`BlossomVAlgorithm`](@ref)
"""
function minimum_weight_perfect_matching end

"""
AbstractMinimumWeightPerfectMatchingAlgorithm
Abstract type that allows users to pass in their preferred algorithm
See also: [`minimum_weight_perfect_matching`](@ref), [`BlossomVAlgorithm`](@ref)
"""
abstract type AbstractMinimumWeightPerfectMatchingAlgorithm end

"""
BlossomVAlgorithm()
Use the BlossomV algorithm to find the minimum weight perfect matching.
Depends on the BlossomV.jl package. You have to call `using BlossomV`
before using this algorithm.
This algorithm dispatches to the BlossomV library, a C library for finding
minimum weight perfect matchings in general graphs.
The BlossomV library is not open source, and thus we can not distribute a
a precompiled binary with GraphsMatching.jl. We attempt to build it on
installation, but unlike typical Julia packages, the build process is prone
to failure if you do not have all the dependencies installed.
If BlossomV.jl does not work on your system,
consider using the LEMONGraphs.jl algorithm instead (the default algorithm),
which we distribute precompiled on all platforms.
See also: [`minimum_weight_perfect_matching`](@ref), [`LEMONMWPMAlgorithm`](@ref)
"""
struct BlossomVAlgorithm <: AbstractMinimumWeightPerfectMatchingAlgorithm end

"""
LEMONMWPMAlgorithm()
Use the LEMON C++ implementation of minimum weight perfect matching.
See also: [`minimum_weight_perfect_matching`](@ref), [`BlossomVAlgorithm`](@ref)
"""
struct LEMONMWPMAlgorithm <: AbstractMinimumWeightPerfectMatchingAlgorithm end

function minimum_weight_perfect_matching(

Check warning on line 73 in src/minimum_weight_perfect_matching.jl

View check run for this annotation

Codecov / codecov/patch

src/minimum_weight_perfect_matching.jl#L73

Added line #L73 was not covered by tests
g::Graph, w::Dict{E,U}
) where {U<:Integer,E<:Edge}
return minimum_weight_perfect_matching(g, w, LEMONMWPMAlgorithm())

Check warning on line 76 in src/minimum_weight_perfect_matching.jl

View check run for this annotation

Codecov / codecov/patch

src/minimum_weight_perfect_matching.jl#L76

Added line #L76 was not covered by tests
end

function minimum_weight_perfect_matching(
g::Graph, w::Dict{E,U}, cutoff::Real, algorithm::AbstractMinimumWeightPerfectMatchingAlgorithm=LEMONMWPMAlgorithm(); kws...
) where {U<:Real,E<:Edge}
wnew = Dict{E,U}()
for (e, c) in w
if c <= cutoff
wnew[e] = c
end
end
return minimum_weight_perfect_matching(g, wnew, algorithm; kws...)
end

function minimum_weight_perfect_matching(
g::Graph, w::Dict{E,U}, algorithm::AbstractMinimumWeightPerfectMatchingAlgorithm=LEMONMWPMAlgorithm(); tmaxscale=10.0
) where {U<:AbstractFloat,E<:Edge}
wnew = Dict{E,Int32}()
cmax = maximum(values(w))
cmin = minimum(values(w))

tmax = typemax(Int32) / tmaxscale # /10 is kinda arbitrary,
# hopefully high enough to not occur in overflow problems
for (e, c) in w
wnew[e] = round(Int32, (c - cmin) / max(cmax - cmin, 1) * tmax)
end
match = minimum_weight_perfect_matching(g, wnew, algorithm)
weight = zero(U)
for i in 1:nv(g)
j = match.mate[i]
if j > i
weight += get(w, E(i, j), zero(U))
end
end
return MatchingResult(weight, match.mate)
end

function minimum_weight_perfect_matching(g::Graph, w::Dict{E,U}, ::LEMONMWPMAlgorithm) where {U<:Integer,E<:Edge}
max = 2*abs(maximum(values(w)))
weights = [-get(w, e, max) for e in edges(g)]
totweight, mate = LEMONGraphs.maxweightedperfectmatching(g, weights)
return MatchingResult(-totweight, mate)
end
11 changes: 11 additions & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[deps]
Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
Hungarian = "e91730f6-4275-51fb-a7a0-7064cfbd3b39"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
LEMONGraphs = "14b1564f-c77f-4800-9e89-efd961faef7c"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Loading
Loading