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
nothingUse 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)orQUBOTools.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 correctsenseanddomain; - 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)
endFor 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
/; /docsif the package builds Documenter docs;/testif 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.OptimizerorPackage.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