Sampler Setup
This guide explains the pieces needed to define a sampler interface with QUBODrivers. The smallest useful wrapper has two parts:
- a
QUBODrivers.@setupmacro call that declares the optimizer type and attributes; - a
QUBODrivers.samplemethod that reads the internal model, calls the backend, and returns aQUBOTools.SampleSet.
Imports
Import QUBODrivers, MathOptInterface, and QUBOTools. QUBOTools is available through the QUBODrivers module and provides model conversion, objective evaluation, sample containers, and metadata helpers.
import QUBODrivers
import QUBODrivers: QUBOTools
import MathOptInterface as MOIThe QUBODrivers.@setup macro
QUBODrivers.@setup — Macro
QUBODrivers.@setup Optimizer begin
name = "Solver Name"
version = v"1.0.0"
attributes = begin
NumberOfReads["num_reads"]::Integer = 1_000
end
endDeclare a QUBODrivers sampler optimizer type.
The macro creates a mutable Optimizer{T} <: QUBODrivers.AbstractSampler{T}, QUBOTools model storage, MOI optimizer metadata, raw attribute storage, and MOI.get/MOI.set/MOI.supports methods for declared attributes.
The setup block accepts:
name: required solver name returned byMOI.SolverName;version: optionalVersionNumber, defaulting to the QUBODrivers package version;attributes: optional block of solver attributes.
Attributes are assignments to default values. They may be typed with ::T, exposed as typed MOI attributes, exposed as raw string attributes, or exposed as both:
"num_reads" = 1_000"num_reads"::Integer = 1_000NumberOfReads = 1_000NumberOfReads::Integer = 1_000NumberOfReads["num_reads"] = 1_000NumberOfReads["num_reads"]::Integer = 1_000
Example
QUBODrivers.@setup Optimizer begin
name = "Super Sampler"
version = v"1.0.2"
attributes = begin
NumberOfReads["num_reads"]::Integer = 1_000
SuperAttribute["super_attr"] = nothing
MegaAttribute::Union{String,Nothing} = "mega"
end
endAfter setup, implement QUBODrivers.sample for the generated optimizer.
This macro usually takes two arguments: the identifier of the sampler's struct (usually Optimizer) and a begin ... end block containing configuration parameters as key-value pairs.
The first parameter of the configuration block is the sampler's name, which will be used to identify it in the MOI.SolverName attribute.
The next entry is the version assignment, which is accessed by the MOI.SolverVersion attribute. In order to consistently support semantic versioning it is required that the version number comes as a v-string e.g. v"major.minor.patch".
A simple yet valid @setup call would look like this:
QUBODrivers.@setup Optimizer begin
name = "Super Sampler"
version = v"1.0.2"
endThe generated optimizer has storage for the current QUBOTools model, raw attributes, the original MOI variable order, and fixed-variable metadata. If a sampler needs additional fields, define the optimizer type manually and implement the same methods described in the API Reference.
Attributes
The attributes parameter is also a begin ... end block. Each entry declares a default value and optional type for a sampler option. Attributes are accessed with MOI.get, MOI.set, MOI.RawOptimizerAttribute, JuMP's set_optimizer_attribute, or the generated typed attribute.
QUBODrivers.@setup Optimizer begin
name = "Super Sampler"
version = v"1.0.2"
attributes = begin
NumberOfReads["num_reads"]::Integer = 1_000
SuperAttribute::String = "super"
end
endIn the example above, users can write either:
MOI.set(sampler, NumberOfReads(), 2_000)
MOI.set(sampler, MOI.RawOptimizerAttribute("num_reads"), 2_000)The QUBODrivers.sample method
QUBODrivers.sample — Function
sample(::AbstractSampler{T})::SampleSet{T} where {T}Run the backend sampler and return a QUBOTools.SampleSet.
Sampler packages implement this method for their optimizer type. The method should read the model from the sampler, read any MOI or raw optimizer attributes it needs, call the backend, and return a SampleSet{T} whose samples use the same sense and domain as the backend output.
MOI.optimize! calls this method and attaches the returned sample set to the optimizer. If the returned metadata does not include a "time" dictionary with a "total" entry, or does not include "status", QUBODrivers fills those fields with default values.
The SampleSet collection
The QUBODrivers.sample method must return a QUBOTools.SampleSet{T}. A SampleSet collects QUBOTools.Sample entries together with metadata about the sampling run.
Build a SampleSet from a vector of samples and an optional metadata dictionary:
samples = QUBOTools.Sample{T,Int}[
QUBOTools.Sample{T,Int}(ψ, λ) # state vector ψ, objective value λ
for (ψ, λ) in zip(states, values)
]
metadata = Dict{String,Any}(
"time" => Dict{String,Any}("total" => elapsed),
)
return QUBOTools.SampleSet(samples, metadata; sense = :min, domain = :bool)The sense keyword (:min or :max) and domain (:bool or :spin) tell QUBOTools how to interpret the samples.
The metadata dictionary is the place to record backend status, timing, and diagnostics. QUBODrivers will add a total time and empty status string if they are missing, but backend-specific wrappers should provide as much useful metadata as their solver exposes.
A complete example
module SuperSampler
import QUBODrivers
import QUBODrivers: QUBOTools
import MathOptInterface as MOI
@doc raw"""
SuperSampler.Optimizer
This sampler is super!
"""
QUBODrivers.@setup Optimizer begin
name = "Super Sampler"
version = v"1.0.2"
attributes = begin
NumberOfReads["num_reads"]::Integer = 1_000
SuperAttribute::String = "super"
end
end
function QUBODrivers.sample(sampler::Optimizer{T}) where {T}
# ~ Is your annealer running on the Ising Model? Have this:
n, h, J, α, β = QUBOTools.ising(
sampler,
:dense; # Here we opt for a dense matrix representation
sense = :max,
)
# ~ Retrieve Attributes using MathOptInterface ~ #
num_reads = MOI.get(sampler, NumberOfReads())
super_attr = MOI.get(sampler, SuperAttribute())
# ~ Do some sampling ~ #
samples = QUBOTools.Sample{T,Int}[]
clock = @timed for _ = 1:num_reads
ψ = super_sample(n, h, J, super_attr)
λ = QUBOTools.value(ψ, h, J, α, β)
s = QUBOTools.Sample{T,Int}(ψ, λ)
push!(samples, s)
end
# ~ Store some metadata ~ #
metadata = Dict{String,Any}(
"num_reads" => num_reads,
"super_attr" => super_attr,
"time" => Dict{String,Any}("effective" => clock.time),
)
# ~ Return a SampleSet ~ #
return QUBOTools.SampleSet(samples, metadata; sense=:max, domain=:spin)
end
function super_sample(n, h, J, super_attr)
# ~ Do some super sampling (using C/C++) ~ #
ψ = ccall(
:super_sample,
Vector{Int},
(
Cint,
Ptr{Float64},
Ptr{Ptr{Float64}},
Cstring
),
n,
h,
J,
super_attr,
)
return ψ
end
end # module