Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 and QUBOTools 0.13.

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.

Nonblocking: This says QUBODrivers 0.6, but the PR now requires QUBODrivers = "0.6.1". Please update the entry to name 0.6.1 so the release notes do not imply v0.6.0 remains acceptable.

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 c419e68. The changelog now names QUBODrivers 0.6.1 to match the compat bound. Verified with a compat/changelog assertion, Pkg.test, native C ABI smoke, and git diff --check.

- 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"

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: this compat still allows the only registered QUBODrivers 0.6 tag, v0.6.0, but that tag predates the conformance API from JuliaQUBO/QUBODrivers.jl#52. Under the PR's normal dependency resolution and CI, validate_metadata, honors_final_reads, and enforces_time_limit are absent, so has_benchmark_metadata_api() is false and the test suite silently runs the legacy QUBODrivers.test path instead of benchmark_conformance = true. That misses issue #26's core acceptance criterion that CI runs the upgraded conformance suite. Please wait for/register a QUBODrivers release containing #52 and constrain compat to that release, or otherwise make CI intentionally test the merged conformance API and fail when it is unavailable.

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.

Not addressed. I verified there is still no QUBODrivers tag newer than v0.6.0, so there is no released build containing JuliaQUBO/QUBODrivers.jl#52 to constrain compat to. I did not pin CI to an unreleased source or modify workflow files. Leaving this Blocking thread unresolved pending maintainer review.

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 47050a8. Compat now requires QUBODrivers = "0.6.1", the trait methods are required at load time, and the test suite always runs QUBODrivers.test(...; benchmark_conformance = true) with no legacy fallback. Verified against released deps: QUBODrivers v0.6.1, Benchmark Conformance 14/14.

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.
189 changes: 125 additions & 64 deletions src/MQLib.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,88 +55,149 @@ 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 v0.6.0 was tagged before these benchmark traits landed. Keep the
# released 0.6 line loadable while activating the traits for newer 0.6 builds.
if isdefined(QUBODrivers, :honors_final_reads)
QUBODrivers.honors_final_reads(::Type{<:Optimizer}) = true
end

if isdefined(QUBODrivers, :enforces_time_limit)
QUBODrivers.enforces_time_limit(::Type{<:Optimizer}) = true
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())
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())
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,
)

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,
)
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["time"] = Dict{String,Any}("effective" => effective_time)

return metadata
else
return Dict{String,Any}(
"time" => Dict{String,Any}("effective" => effective_time),
"origin" => Dict{String,Any}(
"name" => "MQLib",
"version" => __VERSION__,
"heuristic" => heuristic,
),
)
end
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
48 changes: 43 additions & 5 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,18 @@ 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 has_benchmark_metadata_api()
return isdefined(QUBODrivers, :validate_metadata) &&
isdefined(QUBODrivers, :honors_final_reads) &&
isdefined(QUBODrivers, :enforces_time_limit)
end

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

if has_benchmark_metadata_api()
metadata = solution_metadata(model)
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["seeds"]["sampler"] == 1234
Test.@test metadata["time"]["effective"] > 0.0
end

return nothing
end

Expand Down Expand Up @@ -159,18 +186,29 @@ 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"
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
MOI.set(model, MOI.Silent(), true)
MOI.set(model, MQLib.Heuristic(), first(MQLib.heuristics()))
if has_benchmark_metadata_api()
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
else
QUBODrivers.test(MQLib.Optimizer) do model
MOI.set(model, MOI.Silent(), true)
MOI.set(model, MQLib.Heuristic(), first(MQLib.heuristics()))
end
end
end

Expand Down
Loading