Sampler Setup

This guide explains the pieces needed to define a sampler interface with QUBODrivers. The smallest useful wrapper has two parts:

  • a QUBODrivers.@setup macro call that declares the optimizer type and attributes;
  • a QUBODrivers.sample method that reads the internal model, calls the backend, and returns a QUBOTools.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 MOI

The QUBODrivers.@setup macro

QUBODrivers.@setupMacro
QUBODrivers.@setup Optimizer begin
    name = "Solver Name"
    version = v"1.0.0"
    attributes = begin
        NumberOfReads["num_reads"]::Integer = 1_000
    end
end

Declare 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 by MOI.SolverName;
  • version: optional VersionNumber, 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_000
  • NumberOfReads = 1_000
  • NumberOfReads::Integer = 1_000
  • NumberOfReads["num_reads"] = 1_000
  • NumberOfReads["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
end

After setup, implement QUBODrivers.sample for the generated optimizer.

source

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

Note

If missing, the version parameter matches the current version of QUBODrivers.jl.

A simple yet valid @setup call would look like this:

QUBODrivers.@setup Optimizer begin
    name    = "Super Sampler"
    version = v"1.0.2"
end

The 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
end

In 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.sampleFunction
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.

source

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