One JuliaQUBO model interface, five examples, local simulated annealing, and an explicitly optional handoff to a cloud-hosted D-Wave quantum annealer.
This clean-room tutorial uses original Julia code and prose. It cites, but does not copy implementation material from, the Five Starter Problems paper and its companion repository.
Setup¶
The default path is local, credential-free, deterministic after dependency
installation, and covered by make verify-annealing-julia-local. It reads only
the committed cancer-genomics aggregate under notebooks_data/.
The optional QPU section is disabled unless
QUBONOTEBOOKS_ANNEALING_ENABLE_QPU=1. Set DWAVE_API_TOKEN in the process
environment or hosted secret store before opting in. Never paste a token into a
cell or save it in notebook output.
Local installation¶
From the repository root, instantiate the checked-in Julia project before opening Jupyter:
julia --project=notebooks_jl -e 'using Pkg; Pkg.instantiate()'Google Colab¶
Open the badge above, select a Julia runtime, and run the setup cells. The bootstrap clones this repository only when necessary and activates the same checked-in Julia project used by local verification.
Notebook Cell
function load_qubonotebooks_bootstrap()
candidates = (
joinpath(pwd(), "scripts", "notebook_bootstrap.jl"),
joinpath(pwd(), "..", "scripts", "notebook_bootstrap.jl"),
joinpath(pwd(), "QUBONotebooks", "scripts", "notebook_bootstrap.jl"),
joinpath("/content", "QUBONotebooks", "scripts", "notebook_bootstrap.jl"),
)
for candidate in candidates
if isfile(candidate)
include(candidate)
return nothing
end
end
in_colab = haskey(ENV, "COLAB_RELEASE_TAG") ||
haskey(ENV, "COLAB_JUPYTER_IP") ||
isdir(joinpath("/content", "sample_data"))
if in_colab
repo_dir = get(
ENV,
"QUBONOTEBOOKS_REPO_DIR",
joinpath(pwd(), "QUBONotebooks"),
)
if !isdir(repo_dir)
println("[bootstrap] Cloning JuliaQUBO/QUBONotebooks into $repo_dir")
run(`git clone --quiet --depth 1 https://github.com/JuliaQUBO/QUBONotebooks.git $repo_dir`)
end
include(joinpath(repo_dir, "scripts", "notebook_bootstrap.jl"))
return nothing
end
error("Could not locate scripts/notebook_bootstrap.jl from $(pwd()).")
end
load_qubonotebooks_bootstrap()
BOOTSTRAP = Base.invokelatest(QUBONotebooksBootstrap.bootstrap_notebook, "11-Annealing")
QUBONOTEBOOKS_REPO_DIR = BOOTSTRAP.repo_dir
JULIA_PROJECT_DIR = BOOTSTRAP.project_dir;Notebook Cell
import Pkg
python_warning_filter = "ignore:invalid escape sequence:SyntaxWarning"
python_warning_filters = String.(
filter(!isempty, split(get(ENV, "PYTHONWARNINGS", ""), ","))
)
if python_warning_filter ∉ python_warning_filters
push!(python_warning_filters, python_warning_filter)
ENV["PYTHONWARNINGS"] = join(python_warning_filters, ",")
end
if @isdefined(JULIA_PROJECT_DIR)
Pkg.activate(JULIA_PROJECT_DIR; io = devnull)
else
Pkg.activate(@__DIR__; io = devnull)
end
Pkg.instantiate(; io = devnull, allow_autoprecomp = false)Learning objectives¶
By the end of this notebook you will be able to:
Configure reproducible simulated annealing with explicit reads, sweeps, a schedule type, and a seed.
Send five independently decoded QUBOs through one DWave.jl/JuMP runner.
Compare sampled energies with exact optima where enumeration is appropriate.
Distinguish local effective algorithm time, user-observed wall time, and QPU access time.
Opt into a cloud-hosted D-Wave QPU without duplicating formulations or exposing secrets.
Prerequisites¶
Prior notebooks: QUBO objective conventions from Notebook 2; the three canonical formulations from Notebook 7; the corrected order-partitioning model from Notebook 8; and the aggregate-only cancer-genomics limitations from Notebook 9.
Julia 1.10+ and the checked-in notebooks_jl environment are required. No
account or network call is needed after dependencies are installed unless you
explicitly enable the QPU section.
Simulated annealing concepts¶
Simulated annealing starts at a comparatively high temperature, where an energy-increasing move of size may be accepted with Metropolis probability . As temperature falls (equivalently, inverse temperature rises), uphill moves become less likely.
A sweep proposes updates across all variables once.
A read is one complete annealing run and returns one sample.
Repeated samples are aggregated with occurrence counts.
The schedule determines how inverse temperature changes across sweeps.
The local wrapper reports an effective algorithm-call duration. We separately
measure user-observed wall time around optimize!. Neither should be labeled
as QPU access time. A live QPU result reports hardware timing in microseconds;
that field has different scope and must not be compared as if it were local
wall time.
# QUBONOTEBOOKS_COLAB_IMPORT_CELL
Base.invokelatest(
QUBONotebooksBootstrap.warm_notebook_packages!,
"11-Annealing",
);
@assert pkgversion(DWave) == v"0.7.6"
@assert DWave.OCEAN_SDK_VERSION == v"9.3.0"
println(
"Local annealing runtime ready: DWave $(pkgversion(DWave)), " *
"Ocean $(DWave.OCEAN_SDK_VERSION).",
)Local annealing runtime ready: DWave 0.7.6, Ocean 9.3.0.
One solver interface for five models¶
Each problem record owns only formulation and decoding behavior. Optimizer selection lives in a separate configuration record, so the local and QPU paths cannot drift into different models.
const LOCAL_SEED = 96096
const LOCAL_READS = 256
const LOCAL_SWEEPS = 1_000
local_config = (
label = "DWave.Neal",
optimizer = DWave.Neal.Optimizer,
attributes = Dict{String,Any}(
"num_reads" => LOCAL_READS,
"num_sweeps" => LOCAL_SWEEPS,
"seed" => LOCAL_SEED,
"beta_schedule_type" => "geometric",
),
)(label = "DWave.Neal", optimizer = DWave.Neal.Optimizer, attributes = Dict{String, Any}("num_sweeps" => 1000, "num_reads" => 256, "beta_schedule_type" => "geometric", "seed" => 96096))all_binary_states(n::Integer) = [
collect(state)
for state in Iterators.product(ntuple(_ -> (0, 1), n)...)
]
"""
exact_baseline(n, energy)
Enumerate a tutorial-sized binary model using its independent application
energy function.
"""
function exact_baseline(n::Integer, energy)
states = all_binary_states(n)
energies = energy.(states)
best_energy = minimum(energies)
optima = [
states[i]
for i in eachindex(states)
if isapprox(energies[i], best_energy; atol = 1e-9, rtol = 0)
]
return (
energy = Float64(best_energy),
optima = optima,
degeneracy = length(optima),
)
end
"""
quadratic_density(n, energy)
Recover the nonzero pair-interaction density from an independent quadratic
energy evaluator. The finite-difference coefficient is zero exactly when a
pair does not interact.
"""
function quadratic_density(n::Integer, energy)
n < 2 && return 0.0
zero_bits = zeros(Int, n)
zero_energy = energy(zero_bits)
singleton_energy = [
begin
bits = zeros(Int, n)
bits[i] = 1
energy(bits)
end
for i in 1:n
]
interactions = 0
for i in 1:(n - 1), j in (i + 1):n
bits = zeros(Int, n)
bits[i] = 1
bits[j] = 1
coefficient = energy(bits) - singleton_energy[i] -
singleton_energy[j] + zero_energy
interactions += !isapprox(coefficient, 0; atol = 1e-12, rtol = 0)
end
return interactions / binomial(n, 2)
endquadratic_densitypartition_weights = [1, 3, 4, 8]
weighted_edges = [
(1, 2, 2),
(1, 3, 1),
(2, 3, 2),
(2, 4, 1),
(3, 4, 3),
]
cover_edges = [(1, 2), (1, 3), (2, 3), (3, 4)]
cover_penalty = 2
order_values = [2, 3, 4, 5, 6, 8]
risk_exposures = [
1 1 2 5 4 5
4 -2 3 6 -3 0
]
value_weight = 2
risk_weight = 1
"""Build the number-partitioning QUBO from Notebook 7."""
function build_partition_model()
model = Model()
@variable(model, x[1:length(partition_weights)], Bin)
@objective(
model,
Min,
(
sum(
partition_weights[i] * (1 - 2 * x[i])
for i in eachindex(partition_weights)
)
)^2,
)
return model, x
end
"""Build the weighted Max-Cut QUBO from Notebook 7."""
function build_maxcut_model()
model = Model()
@variable(model, x[1:4], Bin)
@objective(
model,
Min,
-sum(
weight * (x[i] + x[j] - 2 * x[i] * x[j])
for (i, j, weight) in weighted_edges
),
)
return model, x
end
"""Build the penalized minimum-vertex-cover QUBO from Notebook 7."""
function build_cover_model()
model = Model()
@variable(model, x[1:4], Bin)
@objective(
model,
Min,
sum(x) + cover_penalty * sum(
(1 - x[i]) * (1 - x[j]) for (i, j) in cover_edges
),
)
return model, x
end
"""Build the grouped-square order-partitioning QUBO from Notebook 8."""
function build_order_model()
model = Model()
@variable(model, x[1:length(order_values)], Bin)
@objective(
model,
Min,
value_weight * (
sum(order_values) -
2 * sum(order_values[j] * x[j] for j in eachindex(order_values))
)^2 +
risk_weight * sum(
(
sum(
risk_exposures[i, j] * (2 * x[j] - 1)
for j in eachindex(order_values)
)
)^2
for i in axes(risk_exposures, 1)
),
)
return model, x
endbuild_order_modelpartition_energy(bits) = sum(
partition_weights[i] * (1 - 2 * bits[i])
for i in eachindex(partition_weights)
)^2
maxcut_weight(bits) = sum(
bits[i] == bits[j] ? 0 : weight
for (i, j, weight) in weighted_edges
)
maxcut_energy(bits) = -maxcut_weight(bits)
uncovered_cover_edges(bits) = [
(i, j) for (i, j) in cover_edges if bits[i] == 0 && bits[j] == 0
]
cover_energy(bits) = sum(bits) +
cover_penalty * length(uncovered_cover_edges(bits))
function order_imbalances(bits)
value = sum(
order_values[j] * (1 - 2 * bits[j])
for j in eachindex(order_values)
)
risk = [
sum(
risk_exposures[i, j] * (2 * bits[j] - 1)
for j in eachindex(order_values)
)
for i in axes(risk_exposures, 1)
]
return value, risk
end
function order_energy(bits)
value, risk = order_imbalances(bits)
return value_weight * value^2 + risk_weight * sum(abs2, risk)
end
decode_partition(bits) = (
application_score = "imbalance=$(abs(sum(partition_weights .* (1 .- 2 .* bits))))",
feasible = true,
details = "two complementary group labels",
)
decode_maxcut(bits) = (
application_score = "cut weight=$(maxcut_weight(bits))",
feasible = true,
details = "sides=$(findall(==(0), bits))/$(findall(==(1), bits))",
)
decode_cover(bits) = (
application_score = "cover size=$(sum(bits))",
feasible = isempty(uncovered_cover_edges(bits)),
details = "selected=$(findall(==(1), bits))",
)
function decode_order(bits)
value, risk = order_imbalances(bits)
return (
application_score = "value Δ=$(abs(value)); risk²=$(sum(abs2, risk))",
feasible = true,
details = "risk Δ=$(risk)",
)
enddecode_order (generic function with 1 method)split_csv_lines(path) = [split(line, ',') for line in readlines(path)]
"""
load_tcga_aml_aggregate(repo_root)
Load and validate the committed aggregate used by Notebook 9 without network
access or patient-level identifiers.
"""
function load_tcga_aml_aggregate(repo_root)
data_dir = joinpath(repo_root, "notebooks_data")
coverage_rows = split_csv_lines(
joinpath(data_dir, "9-CancerGenomics_coverage.csv")
)
matrix_rows = split_csv_lines(
joinpath(data_dir, "9-CancerGenomics_comutation.csv")
)
provenance = JSON.parsefile(
joinpath(data_dir, "9-CancerGenomics_provenance.json")
)
@assert coverage_rows[1] == ["gene", "patient_count"]
genes = String[row[1] for row in coverage_rows[2:end]]
coverage = parse.(Int, [row[2] for row in coverage_rows[2:end]])
@assert genes == String.(matrix_rows[1][2:end])
A = zeros(Int, length(genes), length(genes))
for (i, row) in enumerate(matrix_rows[2:end])
@assert row[1] == genes[i]
A[i, :] = parse.(Int, row[2:end])
end
patient_count = Int(provenance["aggregate"]["patient_count"])
@assert length(genes) == 33
@assert patient_count == 196
@assert A == A'
@assert iszero(diag(A))
@assert all(
A[i, j] <= min(coverage[i], coverage[j])
for i in eachindex(genes), j in eachindex(genes)
)
return (
genes = genes,
coverage = coverage,
A = A,
patient_count = patient_count,
)
end
const CANCER_ALPHA = 0.45
aml = load_tcga_aml_aggregate(QUBONOTEBOOKS_REPO_DIR)
function cancer_energy(bits)
return dot(bits, aml.A * bits) -
CANCER_ALPHA * dot(aml.coverage, bits)
end
"""Build the aggregate-only cancer-genomics QUBO from Notebook 9."""
function build_cancer_model()
model = Model()
@variable(model, x[1:length(aml.genes)], Bin)
@objective(
model,
Min,
sum(
aml.A[i, j] * x[i] * x[j]
for i in eachindex(aml.genes), j in eachindex(aml.genes)
) - CANCER_ALPHA * sum(
aml.coverage[i] * x[i] for i in eachindex(aml.genes)
),
)
return model, x
end
function distinct_patient_bounds(bits)
selected = findall(==(1), bits)
isempty(selected) && return (lower = 0, upper = 0)
coverage_sum = sum(aml.coverage[selected])
pair_overlap = sum(
(aml.A[i, j] for i in selected for j in selected if i < j);
init = 0,
)
return (
lower = max(maximum(aml.coverage[selected]), coverage_sum - pair_overlap),
upper = min(aml.patient_count, coverage_sum),
)
end
function decode_cancer(bits)
selected = findall(==(1), bits)
pair_overlap = sum(
(aml.A[i, j] for i in selected for j in selected if i < j);
init = 0,
)
bounds = distinct_patient_bounds(bits)
return (
application_score = "coverage bounds=$(bounds.lower)–$(bounds.upper)",
feasible = true,
details = "size=$(length(selected)); pair co-mutation=$pair_overlap; aggregate-only",
)
enddecode_cancer (generic function with 1 method)partition_baseline = exact_baseline(4, partition_energy)
maxcut_baseline = exact_baseline(4, maxcut_energy)
cover_baseline = exact_baseline(4, cover_energy)
order_baseline = exact_baseline(6, order_energy)
@assert partition_baseline.energy == 0 && partition_baseline.degeneracy == 2
@assert maxcut_baseline.energy == -7 && maxcut_baseline.degeneracy == 4
@assert cover_baseline.energy == 2 && cover_baseline.degeneracy == 2
@assert order_baseline.energy == 28 && order_baseline.degeneracy == 2
partition_problem = (
name = "number partitioning",
n = 4,
build_model = build_partition_model,
energy = partition_energy,
decode = decode_partition,
exact_optimum_known = true,
exact = partition_baseline,
)
maxcut_problem = (
name = "weighted Max-Cut",
n = 4,
build_model = build_maxcut_model,
energy = maxcut_energy,
decode = decode_maxcut,
exact_optimum_known = true,
exact = maxcut_baseline,
)
cover_problem = (
name = "minimum vertex cover",
n = 4,
build_model = build_cover_model,
energy = cover_energy,
decode = decode_cover,
exact_optimum_known = true,
exact = cover_baseline,
)
order_problem = (
name = "order partitioning",
n = 6,
build_model = build_order_model,
energy = order_energy,
decode = decode_order,
exact_optimum_known = true,
exact = order_baseline,
)
cancer_problem = (
name = "cancer-genomics aggregate",
n = length(aml.genes),
build_model = build_cancer_model,
energy = cancer_energy,
decode = decode_cancer,
exact_optimum_known = false,
exact = nothing,
)
annealing_problems = (
partition_problem,
maxcut_problem,
cover_problem,
order_problem,
cancer_problem,
)
println("Prepared five solver-interchangeable models.")Prepared five solver-interchangeable models.
"""
run_annealing(problem, config)
Build a problem once through its JuMP formulation, attach the selected
DWave.jl optimizer, validate every returned energy against the independent
application evaluator, and preserve occurrence counts and timing metadata.
"""
function run_annealing(problem, config)
model, variables = problem.build_model()
set_optimizer(model, config.optimizer)
for (name, value) in config.attributes
set_optimizer_attribute(model, name, value)
end
user_wall_seconds = @elapsed optimize!(model)
@assert termination_status(model) == JuMP.MOI.LOCALLY_SOLVED
sampleset = DWave.QUBOTools.solution(DWave.QUBOTools.backend(model))
sample_reads = DWave.QUBOTools.reads.(sampleset)
metadata = DWave.QUBOTools.metadata(sampleset)
execution_mode = metadata["execution"]["mode"]
dwave_timing = get(
get(metadata, "dwave_info", Dict{String,Any}()),
"timing",
Dict{String,Any}(),
)
qpu_access_time_microseconds = execution_mode == "qpu" ?
get(dwave_timing, "qpu_access_time", missing) : missing
@assert length(sample_reads) == result_count(model)
rows = [
begin
bits = round.(Int, value.(variables; result = result))
reported_energy = objective_value(model; result = result)
recomputed_energy = problem.energy(bits)
@assert isapprox(
reported_energy,
recomputed_energy;
atol = 1e-8,
rtol = 0,
)
decoded = problem.decode(bits)
@assert decoded.feasible isa Bool
(
bits = bits,
reported_energy = reported_energy,
recomputed_energy = recomputed_energy,
reads = sample_reads[result],
decoded = decoded,
)
end
for result in 1:result_count(model)
]
sort!(rows; by = row -> (row.reported_energy, Tuple(row.bits)))
best_raw_energy = first(rows).recomputed_energy
total_reads = sum(row.reads for row in rows)
@assert total_reads == config.attributes["num_reads"]
success_count = if problem.exact_optimum_known
sum(
row.reads
for row in rows
if isapprox(
row.recomputed_energy,
problem.exact.energy;
atol = 1e-8,
rtol = 0,
)
)
else
missing
end
success_probability = ismissing(success_count) ?
missing : success_count / total_reads
return (
problem = problem.name,
solver = config.label,
model_size = problem.n,
density = quadratic_density(problem.n, problem.energy),
requested_reads = config.attributes["num_reads"],
returned_states = length(rows),
validated_states = length(rows),
best_raw_energy = best_raw_energy,
decoded = first(rows).decoded,
feasibility = first(rows).decoded.feasible,
success_count = success_count,
success_probability = success_probability,
algorithm_effective_seconds = metadata["time"]["effective"],
user_wall_seconds = user_wall_seconds,
execution_mode = execution_mode,
qpu_access_time_microseconds = qpu_access_time_microseconds,
rows = rows,
metadata = metadata,
)
endrun_annealingSeeded local comparison¶
All five models now pass through the same runner. The four small examples use independent exhaustive baselines. For the 33-variable aggregate, every returned state is checked for energy and decoded-metric consistency, but the sampled best is not a proof of global optimality.
local_results = [
run_annealing(problem, local_config)
for problem in annealing_problems
]
@assert all(
result.returned_states == result.validated_states
for result in local_results
)
@assert all(result.success_count > 0 for result in local_results[1:4])
@assert all(
isapprox(
row.reported_energy,
row.recomputed_energy;
atol = 1e-8,
rtol = 0,
)
for result in local_results for row in result.rows
)
println(
"Seeded DWave.Neal comparison: seed=$LOCAL_SEED, " *
"reads=$LOCAL_READS, sweeps=$LOCAL_SWEEPS.",
)
println("Validated every returned state for all five models.")Seeded DWave.Neal comparison: seed=96096, reads=256, sweeps=1000.
Validated every returned state for all five models.
function format_comparison_row(result)
success = ismissing(result.success_count) ?
"n/a (exact optimum not claimed)" :
@sprintf(
"%d/%.3f",
result.success_count,
result.success_probability,
)
local_effective = result.execution_mode == "qpu" ?
"n/a" : @sprintf("%.6f", result.algorithm_effective_seconds)
qpu_access = ismissing(result.qpu_access_time_microseconds) ?
"n/a" : @sprintf("%.0f", result.qpu_access_time_microseconds)
return @sprintf(
"%s | %s | %d | %.3f | %d | %.3f | %s | %s | %s | %s | %.6f | %s",
result.solver,
result.problem,
result.model_size,
result.density,
result.requested_reads,
result.best_raw_energy,
result.decoded.application_score,
string(result.feasibility),
success,
local_effective,
result.user_wall_seconds,
qpu_access,
)
end
function print_comparison_table(results)
println(
"solver | problem | n | density | reads | best raw energy | " *
"decoded score | feasible | exact successes/probability | " *
"local effective s | user wall s | qpu access μs",
)
foreach(result -> println(format_comparison_row(result)), results)
return nothing
end
# Credential-free regression check for the live-QPU reporting branch.
synthetic_qpu_result = (
solver = "D-Wave QPU (synthetic)",
problem = "weighted Max-Cut",
model_size = 4,
density = 5 / 6,
requested_reads = 100,
best_raw_energy = -7.0,
decoded = (application_score = "cut weight=7",),
feasibility = true,
success_count = 80,
success_probability = 0.8,
algorithm_effective_seconds = 0.001234,
user_wall_seconds = 0.25,
execution_mode = "qpu",
qpu_access_time_microseconds = 1_234,
)
synthetic_qpu_row = format_comparison_row(synthetic_qpu_result)
@assert synthetic_qpu_row == "D-Wave QPU (synthetic) | weighted Max-Cut | " *
"4 | 0.833 | 100 | -7.000 | cut weight=7 | true | 80/0.800 | " *
"n/a | 0.250000 | 1234"
print_comparison_table(local_results)solver | problem | n | density | reads | best raw energy | decoded score | feasible | exact successes/probability | local effective s | user wall s | qpu access μs
DWave.Neal | number partitioning | 4 | 1.000 | 256 | 0.000 | imbalance=0 | true | 214/0.836 | 0.625441 | 8.217921 | n/a
DWave.Neal | weighted Max-Cut | 4 | 0.833 | 256 | -7.000 | cut weight=7 | true | 255/0.996 | 0.014620 | 0.016497 | n/a
DWave.Neal | minimum vertex cover | 4 | 0.667 | 256 | 2.000 | cover size=2 | true | 253/0.988 | 0.014708 | 0.016633 | n/a
DWave.Neal | order partitioning | 6 | 1.000 | 256 | 28.000 | value Δ=2; risk²=20 | true | 92/0.359 | 0.023844 | 0.025673 | n/a
DWave.Neal | cancer-genomics aggregate | 33 | 0.360 | 256 | -43.050 | coverage bounds=106–109 | true | n/a (exact optimum not claimed) | 0.152059 | 0.155185 | n/a
The exact-success columns answer a narrow reproducibility question: how many
reads reached an independently enumerated optimum on a small instance.
They do not establish solver superiority. The cancer row deliberately reports
n/a for exact success; its aggregate supports exact QUBO energy and rigorous
distinct-patient bounds, not a global-optimality or clinical claim.
Local effective seconds and user wall seconds are both local measurements with different overhead. They are not QPU access time and are not evidence of quantum advantage.
Optional D-Wave QPU path¶
Where the quantum annealing runs: in the cloud, not on this machine. The
simulated annealing above is local, but there is no local quantum annealer:
DWave.Optimizer submits the problem over the network to a physical quantum
processing unit hosted in D-Wave’s Leap cloud service. Enabling this section
therefore needs network access and a Leap account, adds queue wait to the
elapsed time, and draws on that account’s solver quota.
This section uses the same maxcut_problem record as the local run. Opt-in
changes only the optimizer/configuration. DWave.Optimizer discovers an
available QPU through the configured Leap account; no device identifier is
hard-coded. The initial DWave.jl import temporarily masks any ambient token,
so solver discovery cannot occur on the default path. Missing credentials
fail before optimize! can submit work.
Live QPU execution is a manual, credentialed target and its outputs are not committed. The default notebook run prints a disabled message.
function qpu_opt_in_requested()
raw_value = get(ENV, "QUBONOTEBOOKS_ANNEALING_ENABLE_QPU", "0")
raw_value in ("0", "1") || error(
"Set QUBONOTEBOOKS_ANNEALING_ENABLE_QPU to 0 or 1.",
)
return raw_value == "1"
end
function require_qpu_credentials()
token = get(ENV, "DWAVE_API_TOKEN", "")
isempty(strip(token)) && error(
"QPU execution was requested, but DWAVE_API_TOKEN is missing. " *
"Set it in the process environment or hosted secret store, then rerun.",
)
return nothing
end
"""
sanitized_qpu_metadata(metadata)
Return a small allowlist of reviewable solver, timing, and embedding fields.
Opaque problem identifiers and credentials are intentionally omitted.
"""
function sanitized_qpu_metadata(metadata)
dwave_info = get(metadata, "dwave_info", Dict{String,Any}())
timing = get(dwave_info, "timing", Dict{String,Any}())
chip = get(dwave_info, "chip_info", Dict{String,Any}())
context = get(dwave_info, "embedding_context", Dict{String,Any}())
embedding = DWave.embedding(metadata)
return Dict{String,Any}(
"solver_name" => get(chip, "solver_name", "unavailable"),
"topology" => get(chip, "topology", "unavailable"),
"num_qubits" => get(chip, "num_qubits", "unavailable"),
"qpu_access_time" => get(timing, "qpu_access_time", "unavailable"),
"qpu_access_time_unit" => "microseconds",
"chain_strength" => get(context, "chain_strength", "unavailable"),
"embedding_parameters" => get(
context,
"embedding_parameters",
"unavailable",
),
"embedded_variable_count" => isnothing(embedding) ?
0 : length(embedding),
)
endsanitized_qpu_metadataqpu_requested = qpu_opt_in_requested()
qpu_hardware_required = get(
ENV,
"QUBONOTEBOOKS_ANNEALING_REQUIRE_QPU",
"0",
) == "1"
qpu_hardware_submitted = false
qpu_result = nothing
qpu_summary = nothing
if qpu_requested
require_qpu_credentials()
qpu_config = (
label = "D-Wave QPU",
optimizer = DWave.Optimizer,
attributes = Dict{String,Any}(
"num_reads" => 100,
"annealing_time" => 20.0,
"return_embedding" => true,
),
)
qpu_result = run_annealing(maxcut_problem, qpu_config)
qpu_hardware_submitted = qpu_result.execution_mode == "qpu"
qpu_summary = sanitized_qpu_metadata(qpu_result.metadata)
println("Neal/QPU comparison for weighted Max-Cut:")
print_comparison_table((local_results[2], qpu_result))
println("Sanitized QPU metadata:")
display(qpu_summary)
else
println(
"D-Wave QPU execution is disabled. Set " *
"QUBONOTEBOOKS_ANNEALING_ENABLE_QPU=1 and provide " *
"DWAVE_API_TOKEN through a secret store to opt in.",
)
end
if qpu_hardware_required && !qpu_hardware_submitted
error("D-Wave QPU verification was required, but no job was submitted.")
endD-Wave QPU execution is disabled. Set QUBONOTEBOOKS_ANNEALING_ENABLE_QPU=1 and provide DWAVE_API_TOKEN through a secret store to opt in.
if isnothing(qpu_result)
println("Topology and embedding plots skipped because QPU execution is disabled.")
else
working_graph = DWave.WorkingGraph(qpu_result.metadata)
display(DWave.draw_topology(working_graph; node_size = 1, with_labels = false))
display(DWave.draw_embedding(qpu_result.metadata; node_size = 2))
endTopology and embedding plots skipped because QPU execution is disabled.
Reading a live result responsibly¶
If you opt in, record the discovered solver/topology, reads, annealing time,
returned chain strength and embedding parameters when exposed, and
qpu_access_time with its microsecond unit. QPU access time covers QPU
programming and sampling components defined by D-Wave; it is not the elapsed
time a notebook user sees, and it is not comparable to the local Neal timing
columns as though they measure the same work.
Tutorial-scale success counts can demonstrate interface behavior. They cannot support claims of production performance, solver superiority, or quantum advantage.
Practice checkpoints¶
Use these small checks to connect the configuration and reporting choices to the concepts above.
# EXERCISE 1: Compare the Metropolis acceptance probability for ΔE=2 at
# temperatures T=4 and T=0.5. Which run explores uphill moves more readily?
nothingNotebook Cell
# SOLUTION (hidden in workshop version):
delta_energy = 2.0
hot_probability = exp(-delta_energy / 4.0)
cold_probability = exp(-delta_energy / 0.5)
@assert hot_probability > cold_probability
@printf(
"At T=4, uphill acceptance is %.4f; at T=0.5, it is %.4f.\n",
hot_probability,
cold_probability,
)At T=4, uphill acceptance is 0.6065; at T=0.5, it is 0.0183.
# EXERCISE 2: For the four exact-checked models, print the number of successful
# reads and verify that probability equals successes divided by total reads.
nothingNotebook Cell
# SOLUTION (hidden in workshop version):
for result in local_results[1:4]
@assert isapprox(
result.success_probability,
result.success_count / result.requested_reads;
atol = 1e-12,
rtol = 0,
)
println(
"$(result.problem): $(result.success_count)/$(result.requested_reads) " *
"exact-optimum reads",
)
endnumber partitioning: 214/256 exact-optimum reads
weighted Max-Cut: 255/256 exact-optimum reads
minimum vertex cover: 253/256 exact-optimum reads
order partitioning: 92/256 exact-optimum reads
# EXERCISE 3: Name the three timing fields in this notebook and state why the
# local and QPU values should not be compared as equivalent runtime.
nothingNotebook Cell
# SOLUTION (hidden in workshop version):
println(
"Local effective algorithm time excludes some notebook overhead; " *
"user wall time surrounds optimize!; QPU access time measures hardware " *
"programming/sampling in microseconds. Their scopes differ.",
)Local effective algorithm time excludes some notebook overhead; user wall time surrounds optimize!; QPU access time measures hardware programming/sampling in microseconds. Their scopes differ.
Summary¶
One shared JuMP/DWave.jl runner handled number partitioning, weighted Max-Cut, minimum vertex cover, corrected order partitioning, and the bounded cancer-genomics aggregate.
The default Neal path fixed reads, sweeps, schedule type, and seed and independently recomputed every returned energy.
Four small examples were compared with exhaustive optima; the larger aggregate was decoded without claiming global optimality.
The optional QPU path fails closed on missing credentials, discovers a solver through DWave.jl, sanitizes metadata, and uses public topology/embedding APIs.
Timing fields remain explicitly separated, and no quantum-advantage claim is made.
Learning objectives met: You configured a deterministic local annealer, validated five formulations through one interface, separated timing scopes, and audited a fail-closed QPU handoff.
Next steps: Revisit Notebook 10 to compare the QAOA interface boundary, or run the QPU section manually with an approved Leap secret and record only the sanitized fields defined above.
Further reading:
References¶
A. Mazumder and S. Tayur, “Five Starter Problems: Solving Quadratic Unconstrained Binary Optimization Models on Quantum Computers”, 2025.
Companion repository (ideas and reference context only; no implementation material copied).
DWave.jl, the maintained JuliaQUBO optimizer interface used here.
D-Wave, SimulatedAnnealingSampler reference.
D-Wave, EmbeddingComposite reference.
D-Wave, Operation and timing.
D-Wave Quantum Computing Products documentation: https://
docs .dwavequantum .com/.
- Mazumder, A. R., & Tayur, S. (2025). Five Starter Problems: Solving Quadratic Unconstrained Binary Optimization Models on Quantum Computers. In Tutorials in Operations Research: Advances in Analytics and Operations Research: Improving Decisions to Secure the Future (pp. 145–183). INFORMS. 10.1287/educ.2025.0288