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
11 changes: 11 additions & 0 deletions benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ boolean `MOI.ModelLike` parsing through `QUBOTools.Model(moi_model)` so that
performance work such as issue `#56` is exercised by the benchmark harness on
the reported QUBO conversion path.

It also includes a dense TSP-style constructor fixture (`tsp/cities=36`) that
exercises the same public MOI extraction path used by ToQUBO integrations:

```julia
QUBOTools.Model(moi_model)
```

The default fixture is sized for pull-request benchmark runtime. To add the
10,000-variable TSP extraction case (`tsp/cities=100`) to the constructor suite,
set `QUBOTOOLS_SCALE_BENCHMARKS=true` when running the benchmark commands.

To smoke-test the benchmark harness itself without running the full benchmark,
use:

Expand Down
93 changes: 93 additions & 0 deletions benchmark/suites/fixtures.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,76 @@ function benchmark_qubo_data(label::String, n::Int; quadratic_density::Float64)
return (; rng, variables, linear, quadratic)
end

function _tsp_variable(variables::Vector{Symbol}, cities::Int, city::Int, step::Int)
return variables[(city - 1) * cities + step]
end

function _add_tsp_quadratic!(
quadratic::Dict{Tuple{Symbol,Symbol},Float64},
order::Dict{Symbol,Int},
u::Symbol,
v::Symbol,
value::Float64,
)
key = order[u] < order[v] ? (u, v) : (v, u)

quadratic[key] = get(quadratic, key, 0.0) + value

return nothing
end

function benchmark_dense_tsp_qubo_data(label::String, cities::Int; penalty::Float64 = 10.0)
cities >= 2 || throw(ArgumentError("cities must be at least 2"))

rng = MersenneTwister(benchmark_seed(label, cities; quadratic_density = 1.0))
variables = [
Symbol("x", city, "_", step) for city in 1:cities for step in 1:cities
]
order = Dict{Symbol,Int}(variable => i for (i, variable) in enumerate(variables))
linear = Dict{Symbol,Float64}(variable => -2.0 * penalty for variable in variables)
quadratic = Dict{Tuple{Symbol,Symbol},Float64}()

sizehint!(quadratic, 2 * cities * cities * (cities - 1))

for city in 1:cities, step_a in 1:(cities - 1), step_b in (step_a + 1):cities
_add_tsp_quadratic!(
quadratic,
order,
_tsp_variable(variables, cities, city, step_a),
_tsp_variable(variables, cities, city, step_b),
2.0 * penalty,
)
end

for step in 1:cities, city_a in 1:(cities - 1), city_b in (city_a + 1):cities
_add_tsp_quadratic!(
quadratic,
order,
_tsp_variable(variables, cities, city_a, step),
_tsp_variable(variables, cities, city_b, step),
2.0 * penalty,
)
end

for step in 1:cities
next_step = step == cities ? 1 : step + 1

for city_a in 1:cities, city_b in 1:cities
city_a == city_b && continue

_add_tsp_quadratic!(
quadratic,
order,
_tsp_variable(variables, cities, city_a, step),
_tsp_variable(variables, cities, city_b, next_step),
1.0 + rand(rng),
)
end
end

return (; rng, variables, linear, quadratic)
end

function benchmark_fixture(label::String, n::Int; quadratic_density::Float64)
data = benchmark_qubo_data(label, n; quadratic_density)

Expand Down Expand Up @@ -114,6 +184,19 @@ function benchmark_constructor_fixture(label::String, n::Int; quadratic_density:
)
end

function benchmark_dense_tsp_constructor_fixture(label::String, cities::Int)
data = benchmark_dense_tsp_qubo_data(label, cities)
bool_moi_model = benchmark_bool_moi_model(data.variables, data.linear, data.quadratic)

return (
label = label,
cities = cities,
linear = data.linear,
quadratic = data.quadratic,
bool_moi_model = bool_moi_model,
)
end

function repeat_last(f::F, repeats::Int) where {F}
result = f()

Expand All @@ -134,6 +217,14 @@ function repeat_sum(f::F, repeats::Int) where {F}
return total
end

function benchmark_scale_constructor_fixtures()
if get(ENV, "QUBOTOOLS_SCALE_BENCHMARKS", "false") == "true"
return (benchmark_dense_tsp_constructor_fixture("tsp/cities=100", 100),)
else
return ()
end
end

function benchmark_fixtures()
return (
benchmark_fixture("n=128", 128; quadratic_density = 0.08),
Expand All @@ -146,5 +237,7 @@ function benchmark_constructor_fixtures()
benchmark_constructor_fixture("n=128", 128; quadratic_density = 0.08),
benchmark_constructor_fixture("n=384", 384; quadratic_density = 0.03),
benchmark_constructor_fixture("n=2048", 2048; quadratic_density = 0.01),
benchmark_dense_tsp_constructor_fixture("tsp/cities=36", 36),
benchmark_scale_constructor_fixtures()...,
)
end
15 changes: 15 additions & 0 deletions benchmark/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ include("../suites/constructors.jl")
include("../suites/conversions.jl")
include("../suites/evaluation.jl")

const TSP_EXTRACTION_ALLOCATION_BUDGET = 32 * 1024 * 1024

@testset "Benchmark Fixtures" begin
@test benchmark_seed("n=128", 128; quadratic_density = 0.08) == 0xe5c9c566

Expand All @@ -41,6 +43,17 @@ include("../suites/evaluation.jl")
@test QUBOTools.sense(parsed_model) === QUBOTools.Min
@test QUBOTools.domain(parsed_model) === QUBOTools.BoolDomain
@test QUBOTools.value(parsed_model, psi) ≈ QUBOTools.value(dict_model, psi)

tsp_fixture = benchmark_dense_tsp_constructor_fixture("tsp/cities=12", 12)
tsp_model = QUBOTools.Model(tsp_fixture.bool_moi_model)
tsp_allocated = @allocated QUBOTools.Model(tsp_fixture.bool_moi_model)

@test length(tsp_fixture.linear) == tsp_fixture.cities^2
@test length(tsp_fixture.quadratic) == 2 * tsp_fixture.cities^2 * (tsp_fixture.cities - 1)
@test QUBOTools.dimension(tsp_model) == tsp_fixture.cities^2
@test QUBOTools.linear_size(tsp_model) == tsp_fixture.cities^2
@test QUBOTools.quadratic_size(tsp_model) == length(tsp_fixture.quadratic)
@test tsp_allocated <= TSP_EXTRACTION_ALLOCATION_BUDGET
end

@testset "Benchmark Suites" begin
Expand All @@ -58,6 +71,8 @@ end

@test haskey(suite["constructors"], "n=2048")
@test haskey(suite["constructors"]["n=2048"], "Model/MOI/bool")
@test haskey(suite["constructors"], "tsp/cities=36")
@test haskey(suite["constructors"]["tsp/cities=36"], "Model/MOI/bool")

for fixture in fixtures
conversion_group = suite["conversions"][fixture.label]
Expand Down
12 changes: 12 additions & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@

When extending `QUBOTools`, one might want to implement a method for [`QUBOTools.backend`](@ref).

For MathOptInterface/JuMP integrations, including ToQUBO workflows that expose
an `MOI.ModelLike` object, the supported public materialization path is:

```julia
qt_model = QUBOTools.Model(moi_model)
```

Use `backend` for wrapper types that already own or can return a
`QUBOTools.AbstractModel`; use `QUBOTools.Model(moi_model)` when the source is an
MOI model that needs to be converted into QUBOTools' sparse in-memory
representation.

```@docs
QUBOTools.backend
```
Expand Down
4 changes: 3 additions & 1 deletion ext/QUBOTools_MOI/QUBOTools_MOI.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module QUBOTools_MOI
import QUBOTools
import MathOptInterface as MOI

using SparseArrays: dropzeros!, sparse, sparsevec

const MOIU = MOI.Utilities
const VI = MOI.VariableIndex
const CI{F,S} = MOI.ConstraintIndex{F,S}
Expand All @@ -19,4 +21,4 @@ include("sense.jl")
include("model_parser.jl")
include("num_reads.jl")

end
end
Loading
Loading