Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

- Updated compatibility to QUBODrivers 0.6.1 and QUBOTools 0.13.
- Added QUBODrivers benchmark metadata, final-read, and time-limit conformance
declarations for the MQLib optimizer.

## v0.6.0 - 2026-06-06

- Added a Julia `ccall` solve path that uses `MQLib_jll.libmqlib_c_api`.
Expand Down
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ QUBOTools = "60eb5b62-0a39-4ddc-84c5-97d2adff9319"
MQLib_jll = "0.1.2"
MathOptInterface = "1"
Printf = "1"
QUBOTools = "0.12, 0.13"
QUBODrivers = "0.4, 0.5, 0.6"
QUBOTools = "0.13"
QUBODrivers = "0.6.1"
julia = "1.10"
24 changes: 17 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ optimize!(model)

## Selecting Heuristics

This wrapper allows one to access all 39 QUBO and Max-Cut Heuristics provided by [MQLib](https://github.com/MQLib/MQLib).
Selecting the method to be used can be achieved via JuMP's attribute interface:
This wrapper allows one to access the QUBO and Max-Cut heuristics provided by [MQLib](https://github.com/MQLib/MQLib).
Selecting the method to be used can be achieved via JuMP's attribute interface:

```julia
JuMP.set_optimizer_attribute(model, "heuristic", "ALKHAMIS1998")
Expand All @@ -49,8 +49,18 @@ or by calling MQLib helper functions:
MQLib.set_heuristic(model, "ALKHAMIS1998")
```

To list available heuristics and their descriptions, run:

```julia
MQLib.show_heuristics()
```
To list available heuristics and their descriptions, run:

```julia
MQLib.show_heuristics()
```

## Reads, Seeds, and Time Limits

MQLib.jl supports the standard `QUBODrivers.RandomSeed()` attribute through the
raw `"seed"` optimizer attribute. `QUBODrivers.FinalNumberOfReads()` controls
the number of emitted samples; when it is unset, it falls back to `"num_reads"`.

`MOI.TimeLimitSec()` is divided evenly across the emitted reads and passed to
MQLib as the per-heuristic runtime limit. If no time limit is set, each read
uses a one-second total default split across the requested reads.
196 changes: 132 additions & 64 deletions src/MQLib.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,88 +55,156 @@ function __init__()
return nothing
end

QUBODrivers.@setup Optimizer begin
name = "MQLib"
version = __VERSION__
attributes = begin
RandomSeed["seed"]::Union{Integer,Nothing} = nothing
QUBODrivers.@setup Optimizer begin
name = "MQLib"
version = __VERSION__
attributes = begin
RandomSeed["seed"]::Union{Integer,Nothing} = nothing
NumberOfReads["num_reads"]::Integer = 1
Heuristic["heuristic"]::Union{String,Nothing} = nothing
end
end

function QUBODrivers.sample(sampler::Optimizer{T}) where {T}
n, L, Q, α, β = QUBOTools.qubo(sampler, :dict; sense = :max, domain = :bool)

num_reads = MOI.get(sampler, MQLib.NumberOfReads())
silent = MOI.get(sampler, MOI.Silent())
heuristic = MOI.get(sampler, MQLib.Heuristic())
random_seed = MOI.get(sampler, MQLib.RandomSeed())
time_limit_sec = MOI.get(sampler, MOI.TimeLimitSec())

if num_reads <= 0
error("Number of reads must be a positive integer")
end

if !isnothing(heuristic) && !haskey(_HEURISTICS, heuristic)
error("Invalid QUBO Heuristic code '$heuristic'")
end

end
end

QUBODrivers.honors_final_reads(::Type{<:Optimizer}) = true
QUBODrivers.enforces_time_limit(::Type{<:Optimizer}) = true

function QUBODrivers.sample(sampler::Optimizer{T}) where {T}
n, L, Q, α, β = QUBOTools.qubo(sampler, :dict; sense = :max, domain = :bool)

num_reads = MOI.get(sampler, MQLib.NumberOfReads())
final_num_reads = _mqlib_final_number_of_reads(sampler, num_reads)
silent = MOI.get(sampler, MOI.Silent())
heuristic = MOI.get(sampler, MQLib.Heuristic())
random_seed = MOI.get(sampler, MQLib.RandomSeed())
metadata_seed = random_seed
time_limit_sec = MOI.get(sampler, MOI.TimeLimitSec())

if num_reads <= 0
error("Number of reads must be a positive integer")
end

if isnothing(final_num_reads) || final_num_reads <= 0
error("Final number of reads must be a positive integer")
end

if !isnothing(heuristic) && !haskey(_HEURISTICS, heuristic)
error("Invalid QUBO Heuristic code '$heuristic'")
end

if !isnothing(random_seed)
random_seed %= 65_536
end

run_time_limit = if isnothing(time_limit_sec)
1.0 / num_reads
else
time_limit_sec / num_reads
end

metadata = Dict{String,Any}(
"time" => Dict{String,Any}(),
"origin" => Dict{String,Any}(
"name" => "MQLib",
"version" => __VERSION__,
"heuristic" => heuristic,
),
)


run_time_limit = if isnothing(time_limit_sec)
1.0 / final_num_reads
else
time_limit_sec / final_num_reads
end

samples, effective_time = if _mqlib_can_use_c_api(heuristic)
_sample_with_c_api(
T,
n,
L,
Q,
α,
β;
num_reads,
silent,
heuristic,
random_seed,
run_time_limit,
α,
β;
num_reads = final_num_reads,
silent,
heuristic,
random_seed,
run_time_limit,
)
else
_sample_with_executable(
T,
n,
L,
Q,
α,
β;
num_reads,
silent,
heuristic,
random_seed,
run_time_limit,
)
end

metadata["time"]["effective"] = effective_time

return QUBOTools.SampleSet{T}(samples, metadata; sense = :max, domain = :bool)
end

function _sample_with_executable(
α,
β;
num_reads = final_num_reads,
silent,
heuristic,
random_seed,
run_time_limit,
)
end

metadata = _mqlib_metadata(
heuristic;
final_num_reads,
sample_count = length(samples),
effective_time,
random_seed = metadata_seed,
)

return QUBOTools.SampleSet{T}(samples, metadata; sense = :max, domain = :bool)
end

function _mqlib_final_number_of_reads(sampler::Optimizer, num_reads::Integer)
if isdefined(QUBODrivers, :final_number_of_reads)
return QUBODrivers.final_number_of_reads(sampler)
else
return num_reads
end
end

function _mqlib_metadata(
heuristic::Union{String,Nothing};
final_num_reads::Integer,
sample_count::Integer,
effective_time::Real,
random_seed::Union{Integer,Nothing},
)
if isdefined(QUBODrivers, :_sampler_metadata)
metadata = QUBODrivers._sampler_metadata(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: with the currently allowed registered QUBODrivers v0.6.0, this helper exists but QUBODrivers does not post-stamp the configured seed into metadata. Because _mqlib_metadata does not pass a seeds dictionary, solving with MQLib.RandomSeed() set emits metadata["seeds"] == Dict() under the dependency version this PR allows. That violates issue #26's requirement to record seeds under metadata["seeds"]. Please pass the configured seed into this helper and initialize seeds = Dict("sampler" => seed) when it is non-nothing, or tighten compat to a QUBODrivers release that reliably stamps the seed and make CI exercise that release.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 1768fc2. _mqlib_metadata now receives the configured RandomSeed and sets metadata["seeds"]["sampler"] for both metadata paths. test_public_c_api_bool_max_objectives now asserts this under released dependencies. Tests: Pkg.test, QUBODrivers #52 conformance, QUBO v0.6 coexistence, native C ABI smoke.

origin = "MQLib.jl",
algorithm_name = _mqlib_algorithm_name(heuristic),
backend_name = "MQLib",
backend_version = __VERSION__,
execution_mode = _mqlib_execution_mode(heuristic),
optimizer_evaluations = final_num_reads,
number_of_reads = final_num_reads,
final_number_of_reads = sample_count,
status = "locally_solved",
termination_status = MOI.LOCALLY_SOLVED,
)
metadata["seeds"] = _mqlib_seed_metadata(random_seed)
metadata["time"] = Dict{String,Any}("effective" => effective_time)

return metadata
else
return Dict{String,Any}(
"time" => Dict{String,Any}("effective" => effective_time),
"seeds" => _mqlib_seed_metadata(random_seed),
"origin" => Dict{String,Any}(
"name" => "MQLib",
"version" => __VERSION__,
"heuristic" => heuristic,
),
)
end
end

function _mqlib_seed_metadata(random_seed::Union{Integer,Nothing})
seeds = Dict{String,Any}()
if !isnothing(random_seed)
seeds["sampler"] = random_seed
end

return seeds
end

function _mqlib_algorithm_name(heuristic::Union{String,Nothing})
return something(heuristic, "Hyper-Heuristic")
end

function _mqlib_execution_mode(heuristic::Union{String,Nothing})
return isnothing(heuristic) ? "hyperheuristic" : "heuristic"
end

function _sample_with_executable(
::Type{T},
n::Integer,
L,
Expand Down
30 changes: 27 additions & 3 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import TOML
import MQLib
import MQLib: MOI, QUBODrivers

const QUBOTools = MQLib.QUBOTools

function first_available_tool(names::Vector{String})
for name in names
path = Sys.which(name)
Expand Down Expand Up @@ -33,6 +35,12 @@ function configure_public_default_hyperheuristic_smoke!(model)
return model
end

function solution_metadata(model)
raw = MOI.get(model, MOI.RawSolver())

return QUBOTools.metadata(QUBOTools.solution(raw))
end

function test_public_default_hyperheuristic_succeeds()
T = Float64
n = 3
Expand Down Expand Up @@ -111,6 +119,18 @@ function test_public_c_api_bool_max_objectives()
)
Test.@test total_reads == 2

metadata = solution_metadata(model)
Test.@test metadata["seeds"]["sampler"] == 1234

Test.@test isempty(QUBODrivers.validate_metadata(metadata))
Test.@test metadata["origin"] == "MQLib.jl"
Test.@test metadata["algorithm"]["name"] == "ALKHAMIS1998"
Test.@test metadata["backend"]["name"] == "MQLib"
Test.@test metadata["backend"]["version"] == MQLib.__VERSION__
Test.@test metadata["reads"]["number_of_reads"] == 2
Test.@test metadata["reads"]["final_number_of_reads"] == 2
Test.@test metadata["time"]["effective"] > 0.0

return nothing
end

Expand Down Expand Up @@ -159,16 +179,20 @@ Test.@testset "Compatibility metadata" begin

Test.@test compat["julia"] == "1.10"
Test.@test compat["MQLib_jll"] == "0.1.2"
Test.@test compat["QUBODrivers"] == "0.4, 0.5, 0.6"
Test.@test compat["QUBOTools"] == "0.12, 0.13"
Test.@test compat["QUBODrivers"] == "0.6.1"
Test.@test compat["QUBOTools"] == "0.13"

ci = read(joinpath(root, ".github", "workflows", "ci.yml"), String)
Test.@test occursin(r"version:\s*'1\.10'", ci)
Test.@test occursin(r"version:\s*'1'", ci)
end

Test.@testset "QUBODrivers" begin
QUBODrivers.test(MQLib.Optimizer) do model
Test.@test QUBODrivers.supports_seed(MQLib.Optimizer)
Test.@test QUBODrivers.honors_final_reads(MQLib.Optimizer)
Test.@test QUBODrivers.enforces_time_limit(MQLib.Optimizer)

QUBODrivers.test(MQLib.Optimizer; benchmark_conformance = true) do model
MOI.set(model, MOI.Silent(), true)
MOI.set(model, MQLib.Heuristic(), first(MQLib.heuristics()))
end
Expand Down
Loading