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)Every generated optimizer also supports QUBODrivers.FinalNumberOfReads() and the raw key "final_num_reads". "num_reads" controls reads used by a sampler's internal search or optimizer evaluations. "final_num_reads" controls the reads used to build the returned SampleSet. If "final_num_reads" is unset, call QUBODrivers.final_number_of_reads(sampler) to get the effective value; it defaults to "num_reads" when that attribute is supported.
Declare raw "seed" to opt into the standard QUBODrivers.RandomSeed() attribute:
QUBODrivers.@setup Optimizer begin
name = "Seeded Sampler"
attributes = begin
"seed"::Union{Integer,Nothing} = nothing
end
endWhen a seed is configured, QUBODrivers records it under metadata["seeds"]["sampler"] after sampling.
Generated optimizers also support a generic post-sampling hook through QUBODrivers.PostSampleCallback() and the raw key "post_sample_callback". The callback is called after QUBODrivers.sample returns and before the final SampleSet is attached:
callback = function (sampleset, sampler)
QUBOTools.metadata(sampleset)["postprocess_note"] = "annotated"
return nothing
end
MOI.set(sampler, QUBODrivers.PostSampleCallback(), callback)The callback receives a copy of the raw backend SampleSet and the sampler context. Metadata-only annotations are allowed by default. If the callback changes sample states, objective values, reads, sense, or domain, also set QUBODrivers.PostSampleTransform() or raw "post_sample_transform" to true and return the transformed SampleSet. Replacement SampleSets should carry over or intentionally rebuild the original metadata; otherwise backend origin, status, reads, timing, and diagnostics metadata from the raw output are lost. When transformed samples are emitted, QUBODrivers records postprocess metadata and preserves the raw sample frame and records in the emitted solution metadata.
Use this hook for driver-level sample annotation, objective bookkeeping, or controlled repair of emitted samples. For objective bookkeeping, prefer the QUBOTools convention metadata(sampleset)["objectives"][label]. If your QUBOTools version includes objective-bookkeeping helpers, prefer QUBOTools.annotate_objectives! and QUBOTools.verify_objective_values for this. Problem-specific repair algorithms and the reformulation metadata needed to implement them should live outside QUBODrivers. ToQUBO versions that expose reformulation metadata record it under metadata(QUBOTools.backend(model))["toqubo"]["reformulation"] and expose helpers such as ToQUBO.project_original_state; QUBODrivers only provides the sampler-interface hook.
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. It then validates the benchmarking metadata schema and emits a warning, not an error, for missing or malformed fields.
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. See the Metadata Schema for the normative benchmarking contract.
QUBODrivers requires these top-level metadata keys for benchmark-ready driver results:
"origin": human-readable driver or backend origin;"algorithm": dictionary with at least"name";"backend": dictionary with backend"name"and"version"when known;"execution": dictionary with"mode";"optimizer": dictionary with"iterations"and objective/backend"evaluations"actually performed, when known;"reads": dictionary with"number_of_reads"and"final_number_of_reads"counts actually consumed by internal search and final sample-set construction, respectively; if there is no separate internal phase, both values should be the same;"seeds": dictionary of sampler, model, optimizer, or backend seeds;"status"and"termination_status": raw and structured termination status.
When a post-sampling callback runs, QUBODrivers records callback metadata under "postprocess" => "callback". If the callback transforms emitted samples, the raw sample frame and records are stored under "postprocess" => "raw_samples".
Sampler checklist
Before releasing a driver wrapper, confirm that:
QUBODrivers.validate_metadata(sampleset)returns an empty vector for normal results; see the Metadata Schema;- stochastic samplers expose raw
"seed"and supportQUBODrivers.RandomSeed()when reproducibility is part of their contract; QUBODrivers.honors_final_readsandQUBODrivers.enforces_time_limitreturntrueonly for behavior the driver actually guarantees;MOI.get(sampler, MOI.SolveTimeSec())reports effective backend time, while wall-clock time is available throughQUBODrivers.total_time.
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())
final_reads = MOI.get(sampler, QUBODrivers.FinalNumberOfReads())
super_attr = MOI.get(sampler, SuperAttribute())
# ~ Do some sampling ~ #
samples = QUBOTools.Sample{T,Int}[]
clock = @timed for _ = 1:final_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}(
"origin" => "Super Sampler @ SuperBackend",
"algorithm" => Dict{String,Any}("name" => "Super Sampler"),
"backend" => Dict{String,Any}("name" => "SuperBackend", "version" => nothing),
"execution" => Dict{String,Any}("mode" => "sampling"),
"optimizer" => Dict{String,Any}("iterations" => nothing, "evaluations" => final_reads),
"reads" => Dict{String,Any}(
"number_of_reads" => final_reads,
"final_number_of_reads" => final_reads,
),
"seeds" => Dict{String,Any}(),
"status" => "locally_solved",
"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