Implementing a Sampler

This walkthrough builds a minimal sampler that works through the same public interfaces used by package wrappers. It uses exhaustive search so the example is self-contained; a production wrapper can replace that inner loop with a call to an external library, service, or device.

Minimal module

A QUBODrivers sampler needs an optimizer type and a QUBODrivers.sample method. The QUBODrivers.@setup macro creates the optimizer type, standard MOI methods, and attribute storage.

module DemoSampler

import QUBODrivers
import QUBODrivers: MOI, QUBOTools, Sample, SampleSet

QUBODrivers.@setup Optimizer begin
    name    = "Demo Sampler"
    version = v"0.1.0"
    attributes = begin
        NumberOfReads["num_reads"]::Integer = 1
    end
end

function QUBODrivers.sample(sampler::Optimizer{T}) where {T}
    n, linear, quadratic, scale, offset = QUBOTools.qubo(
        sampler,
        :dict;
        sense = :min,
    )

    num_reads = MOI.get(sampler, NumberOfReads())
    final_reads = MOI.get(sampler, QUBODrivers.FinalNumberOfReads())
    if num_reads < 1
        error("'num_reads' must be a positive integer")
    end
    if final_reads < 1
        error("'final_num_reads' must be a positive integer")
    end

    best_state = Vector{Int}()
    best_value = nothing

    for state in Iterators.product(ntuple(_ -> (0, 1), n)...)
        candidate_state = collect(state)
        candidate_value = QUBOTools.value(
            candidate_state,
            linear,
            quadratic,
            scale,
            offset,
        )

        if isnothing(best_value) || candidate_value < best_value
            best_state = candidate_state
            best_value = candidate_value
        end
    end

    samples = [
        Sample{T,Int}(copy(best_state), best_value)
        for _ in 1:final_reads
    ]

    metadata = Dict{String,Any}(
        "origin"    => "Demo Sampler",
        "algorithm" => Dict{String,Any}("name" => "Demo Sampler"),
        "backend"   => Dict{String,Any}("name" => "Demo Sampler", "version" => v"0.1.0"),
        "execution" => Dict{String,Any}("mode" => "exhaustive_search"),
        "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}(),
        "time"      => Dict{String,Any}("effective" => 0.0),
        "status"    => "optimal",
    )

    return SampleSet{T}(samples, metadata; sense = :min, domain = :bool)
end

end

nothing

Use it from JuMP

The generated Optimizer type is an MOI.AbstractOptimizer, so it can be passed directly to JuMP.Model.

using JuMP

model = Model(DemoSampler.Optimizer)
set_optimizer_attribute(model, "num_reads", 2)

@variable(model, x[1:2], Bin)
@objective(model, Min, -x[1] - 2x[2] + 3x[1] * x[2])

optimize!(model)

(objective_value(model), round.(Int, value.(x)), result_count(model))
(-2.0, [0, 1], 1)

Implementation checklist

When adapting the example for a real sampler:

  • choose the QUBOTools representation that matches the backend, such as QUBOTools.qubo(sampler, :dict; sense = :min) or QUBOTools.ising(sampler, :dense; sense = :max);
  • read user-facing options through MOI attributes generated by @setup;
  • use QUBODrivers.FinalNumberOfReads() for the returned sample count when a backend has separate search and final sampling phases;
  • convert backend outputs into Sample{T,Int} entries;
  • return a SampleSet{T} with the correct sense and domain;
  • include useful metadata such as timing, backend status, and backend version;
  • leave generic post-sampling annotation or repair to QUBODrivers.PostSampleCallback() when users need a configurable hook;
  • use QUBOTools objective metadata and ToQUBO reformulation metadata for objective bookkeeping, original-variable projection, and encoding-specific auxiliary handling;
  • run QUBODrivers.test(YourSampler.Optimizer) in the package test suite.

The built-in RandomSampler and ExactSampler implementations are compact references for these same steps.

Package a sampler interface

Once the sampler module works, package it like a normal Julia interface package. The smallest useful repository usually has src/, test/, Project.toml, and GitHub automation for tests and releases. Prefer this checklist over a generated template: QUBODrivers interfaces vary from single-file wrappers to multi-module hardware clients and Python-backed packages.

Use src/library/drivers/ExactSampler.jl as the canonical copy-from skeleton for the optimizer declaration, the QUBODrivers.sample method, and metadata shape. Then compare against real interfaces such as DWave.jl and JuliQAOAOpt.jl for package layouts that include external backends.

Project.toml

A standalone interface package should depend on QUBODrivers, QUBOTools, and MathOptInterface. Add backend-specific dependencies only when the sampler calls them directly.

name = "MySampler"
uuid = "00000000-0000-0000-0000-000000000000"
authors = ["Your Name <you@example.com>"]
version = "0.1.0"

[deps]
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
QUBODrivers = "a3f166f7-2cd3-47b6-9e1e-6fbfe0449eb0"
QUBOTools = "60eb5b62-0a39-4ddc-84c5-97d2adff9319"

[compat]
MathOptInterface = "1"
QUBODrivers = "0.6"
QUBOTools = "0.10, 0.11, 0.12, 0.13, 0.14"
julia = "1.10"

Keep [compat] bounds explicit. They define what users and CI can install, and they are also what registry review checks before accepting a release.

Replace the placeholder uuid with a real value generated in Julia, for example using UUIDs; uuid4().

Tests

Put the public interface checks in test/runtests.jl. The QUBODrivers.test extension loads when both QUBODrivers and Julia's Test standard library are available.

using Test
using MySampler
using QUBODrivers

@testset "MySampler" begin
    QUBODrivers.test(MySampler.Optimizer)
end

For samplers that need deterministic options or credentials-free operation, pass a setup function or disable example problems as shown in Test Suite. Keep backend integration tests separate when they require secrets, paid hardware, or long runtimes.

Continuous integration and maintenance

Use CI to exercise the package on the Julia versions in [compat]. A typical GitHub Actions workflow checks out the repository, installs Julia with julia-actions/setup-julia, enables julia-actions/cache, runs julia-actions/julia-buildpkg, and runs julia-actions/julia-runtest.

Add Dependabot for the package environments that exist in the repository. Common entries are:

  • the root Julia package environment at /;
  • /docs if the package builds Documenter docs;
  • /test if tests use a separate test environment;
  • GitHub Actions updates at /.

If the package is registered and should publish Git tags automatically, add TagBot with JuliaRegistries/TagBot. Registration is optional for sampler interfaces, and TagBot is only useful once releases are driven by registry events.

Installation and registration

Unregistered sampler packages can be installed directly from their repository URL. This is the default fallback path while an interface is private, experimental, or too specialized for registration.

import Pkg
Pkg.add(url = "https://github.com/Owner/MySampler.jl")

When the package is stable enough for wider use, register it with Julia's General registry using Registrator. Before registering, confirm the package has a unique UUID, an incremented version, explicit [compat] bounds, passing CI, and documentation that points users back to this QUBODrivers interface guide.

Registering the package with QUBODrivers docs

If the sampler package is public and current in the JuliaQUBO or SECQUOIA organizations, open a QUBODrivers documentation PR that adds it to the external sampler table in docs/src/manual/3-samplers.md. That table is maintained as the reviewed source of truth rather than generated from GitHub search.

Include:

  • the package repository URL;
  • the public solver type, such as Package.Optimizer or Package.Submodule.Optimizer;
  • the source file path that defines the optimizer;
  • a note when the package is deprecated, renamed, experimental, private, or a compatibility shim.

For documentation-only registration PRs, run:

julia --project=docs/ docs/make.jl --skip-deploy