A reproducible Julia tutorial for local optimization, circuit auditing, and optional execution on IBM quantum hardware.
Setup¶
Local installation¶
From the repository root, instantiate the shared Julia environment before opening this notebook:
julia --project=notebooks_jl -e 'using Pkg; Pkg.instantiate()'QiskitOpt.jl manages its compatible local Qiskit stack through PythonCall and CondaPkg. The default path below is credential-free: it uses a local Aer simulator and does not contact IBM Runtime.
Google Colab¶
Open the badge above, select a Julia runtime, and run the setup cells. The bootstrap clones this repository only when Colab does not already have it, then activates the same checked-in notebook project used locally.
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(Cmd(["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, "10-QAOA")
QUBONOTEBOOKS_REPO_DIR = BOOTSTRAP.repo_dir
JULIA_NOTEBOOKS_DIR = BOOTSTRAP.notebooks_dir
JULIA_PROJECT_DIR = BOOTSTRAP.project_dir
IN_COLAB = BOOTSTRAP.in_colab;
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:
map a minimization QUBO to a cost Hamiltonian with an explicit bit/spin convention;
configure a bounded, reproducible local QAOA run through
QiskitOpt.jl;distinguish raw QUBO energy, application score, sample probability, and the most-frequent sample;
decode number-partitioning, Max-Cut, and minimum-vertex-cover samples and compare them with exact baselines;
audit parameter order, bit order, circuit resources, objective metadata, and the effect of QAOA depth
p; andsubmit a fixed QAOA circuit to IBM quantum hardware through an explicit, secret-safe opt-in.
Prerequisites¶
Prior notebooks: Notebook 2 introduces QUBO modeling, and Notebook 7 derives and independently checks the three canonical models used here.
Mathematical background: Binary variables, Pauli operators, expectation values, and basic probability.
Software: Julia 1.10+ with the shared notebook project instantiated.
Accounts required: None for the default path. An IBM Quantum account is required only for the explicitly enabled hardware-submission cell.
# QUBONOTEBOOKS_COLAB_IMPORT_CELL
Base.invokelatest(
QUBONotebooksBootstrap.warm_notebook_packages!,
"10-QAOA",
);
runtime_diagnostics = QiskitOpt.check_runtime(; local_backend=true, ibm=false)
@assert runtime_diagnostics.ok
@assert runtime_diagnostics.local_backend !== nothing
@assert runtime_diagnostics.local_backend.ok
println(
"Local runtime ready: QiskitOpt ",
runtime_diagnostics.julia.version,
", qiskit ",
runtime_diagnostics.packages["qiskit"].version,
", qiskit-aer ",
runtime_diagnostics.packages["qiskit_aer"].version,
)
QiskitOpt runtime diagnostics: OK
Julia package: OK (0.7.1)
Julia version: 1.10.11
PythonCall: OK (0.9.35)
Python executable: <notebook-environment>/bin/python
numpy: OK (2.4.6)
Python executable: <notebook-environment>/bin/python
qiskit: OK (2.3.1)
Python executable: <notebook-environment>/bin/python
qiskit_aer: OK (0.17.2)
Python executable: <notebook-environment>/bin/python
qiskit_optimization: OK (0.7.0)
Python executable: <notebook-environment>/bin/python
scipy: OK (1.15.3)
Python executable: <notebook-environment>/bin/python
Local backend: OK
Usable backend: aer_simulator_matrix_product_state. QAOA/VQE local solves still use qiskit_ibm_runtime EstimatorV2/SamplerV2; pass ibm=true to verify those primitives.
Local runtime ready: QiskitOpt 0.7.1, qiskit 2.3.1, qiskit-aer 0.17.2
QAOA in one convention¶
Write a minimization QUBO as
We use , or equivalently . Replacing each by the Pauli- operator gives a diagonal cost Hamiltonian whose computational-basis eigenvalue is the raw QUBO energy. QiskitOpt performs this conversion and records the objective sense, scale, sign, and offset so we can audit the convention instead of recreating the bridge.
QAOA starts from the uniform state . At depth , it alternates a cost unitary with a mixer unitary , where the default mixer is based on Pauli . A classical optimizer changes the angles to reduce an estimated expected energy. Qiskit/QiskitOpt bind the vector as all beta angles followed by all gamma angles:
The optimizer uses finite-shot expectation estimates; a separate final sampling stage draws bit strings from the optimized circuit. More shots reduce sampling noise but do not turn QAOA into an exact algorithm.
const QAOA_STANDARD_SEED = 73001
const QAOA_LAYERS = 1
const QAOA_OPTIMIZER_SHOTS = 256
const QAOA_FINAL_SHOTS = 512
const QAOA_MAX_ITERATIONS = 8
function configured_qaoa_model()
model = Model(QiskitOpt.QAOA.Optimizer)
set_silent(model)
set_attribute(model, QiskitOpt.QAOA.NumberOfLayers(), QAOA_LAYERS)
set_attribute(model, QiskitOpt.QAOA.NumberOfReads(), QAOA_OPTIMIZER_SHOTS)
set_attribute(
model,
QiskitOpt.QUBODrivers.FinalNumberOfReads(),
QAOA_FINAL_SHOTS,
)
set_attribute(model, QiskitOpt.QAOA.MaximumIterations(), QAOA_MAX_ITERATIONS)
set_attribute(
model,
QiskitOpt.QUBODrivers.RandomSeed(),
QAOA_STANDARD_SEED,
)
# Restate QiskitOpt's documented local default so the backend choice is explicit and auditable.
set_attribute(
model,
QiskitOpt.QAOA.AerBackendMethod(),
"matrix_product_state",
)
set_attribute(
model,
QiskitOpt.QAOA.AerSeedSimulator(),
QAOA_STANDARD_SEED,
)
set_attribute(
model,
QiskitOpt.QAOA.TranspilerSeed(),
QAOA_STANDARD_SEED,
)
initial_parameters = QiskitOpt.QAOA.random_initial_parameters(
number_of_layers=QAOA_LAYERS,
seed=QAOA_STANDARD_SEED,
)
set_attribute(
model,
QiskitOpt.QAOA.InitialParameters(),
initial_parameters,
)
set_attribute(
model,
QiskitOpt.QAOA.InitialParameterSource(),
"random_seed_$(QAOA_STANDARD_SEED)",
)
return model
end
println(
"Default budget: p=$QAOA_LAYERS, optimizer shots=$QAOA_OPTIMIZER_SHOTS, ",
"final shots=$QAOA_FINAL_SHOTS, maximum evaluations=$QAOA_MAX_ITERATIONS",
)
Default budget: p=1, optimizer shots=256, final shots=512, maximum evaluations=8
Four quantities that should not be conflated¶
Raw QUBO energy is the minimized polynomial value returned with a sample.
Application score is the decoded quantity we care about: partition imbalance, cut weight, or cover size.
Probability is the sample’s read count divided by the final shot count.
Most-frequent sample has the largest observed probability. It is not automatically the minimum-energy sample.
The exact baseline below is computed by independent enumeration. The stochastic assertions require the sampled set to contain an exact-energy feasible state, but they never pin one complete histogram.
function all_binary_states(n::Integer)
states = Vector{Vector{Int}}()
for state in Iterators.product(ntuple(_ -> (0, 1), n)...)
push!(states, collect(state))
end
return states
end
function exact_baseline(n_variables::Integer, raw_energy)
states = all_binary_states(n_variables)
energies = Float64[raw_energy(bits) for bits in states]
best_energy = minimum(energies)
optima = [
states[index]
for index in eachindex(states)
if isapprox(energies[index], best_energy; atol=1e-9, rtol=0)
]
return (energy=best_energy, optima=optima)
end
function qaoa_sampleset(model::Model)
raw_solver = JuMP.MOI.get(JuMP.backend(model), JuMP.MOI.RawSolver())
return QiskitOpt.QUBOTools.solution(raw_solver)
end
function sample_rows(sampleset)
total_reads = QiskitOpt.QUBOTools.reads(sampleset)
return [
(
bits=QiskitOpt.QUBOTools.state(sample),
raw_energy=QiskitOpt.QUBOTools.value(sample),
reads=QiskitOpt.QUBOTools.reads(sample),
probability=QiskitOpt.QUBOTools.reads(sample) / total_reads,
)
for sample in sampleset
]
end
function run_and_check_qaoa!(
name,
model,
variables,
raw_energy,
application_score,
application_label,
is_feasible,
decode,
)
baseline = exact_baseline(length(variables), raw_energy)
optimize!(model)
@assert termination_status(model) == JuMP.MOI.LOCALLY_SOLVED
sampleset = qaoa_sampleset(model)
rows = sample_rows(sampleset)
best = rows[argmin([row.raw_energy for row in rows])]
most_frequent = rows[argmax([row.reads for row in rows])]
metadata = QiskitOpt.QUBOTools.metadata(sampleset)
@assert sum(row.reads for row in rows) == QAOA_FINAL_SHOTS
@assert isapprox(best.raw_energy, baseline.energy; atol=1e-9, rtol=0)
@assert is_feasible(best.bits)
@assert metadata["optimizer"]["number_of_reads"] == QAOA_OPTIMIZER_SHOTS
@assert metadata["reads"]["final_number_of_reads"] == QAOA_FINAL_SHOTS
@assert metadata["seeds"]["sampler"] == QAOA_STANDARD_SEED
@assert metadata["seeds"]["simulator"] == QAOA_STANDARD_SEED
@assert metadata["seeds"]["transpiler"] == QAOA_STANDARD_SEED
@assert startswith(metadata["backend"]["name"], "aer_simulator")
println(name)
println(" exact optimum raw energy = $(baseline.energy)")
println(
" sampled best: bits=$(best.bits), raw energy=$(best.raw_energy), ",
"$application_label=$(application_score(best.bits)), ",
"probability=$(round(best.probability; digits=3))",
)
println(
" most frequent: bits=$(most_frequent.bits), ",
"raw energy=$(most_frequent.raw_energy), ",
"probability=$(round(most_frequent.probability; digits=3))",
)
println(" decoded best = $(decode(best.bits))")
return (
name=name,
baseline=baseline,
sampleset=sampleset,
rows=rows,
best=best,
most_frequent=most_frequent,
metadata=metadata,
decoded=decode(best.bits),
)
end
Three canonical models¶
Notebook 7 gives the full derivations. Here we rebuild the same four-variable tutorial instances directly in QiskitOpt-backed JuMP models. Each application score is independent of the model expression used by the solver.
partition_weights = [1, 3, 4, 8]
partition_signed_imbalance(bits) =
sum(partition_weights[i] * (1 - 2 * bits[i]) for i in eachindex(bits))
partition_energy(bits) = partition_signed_imbalance(bits)^2
partition_imbalance(bits) = abs(partition_signed_imbalance(bits))
decode_partition(bits) = (
group_zero=partition_weights[findall(==(0), bits)],
group_one=partition_weights[findall(==(1), bits)],
)
weighted_edges = [
(1, 2, 2),
(1, 3, 1),
(2, 3, 2),
(2, 4, 1),
(3, 4, 3),
]
maxcut_weight(bits) =
sum(bits[i] == bits[j] ? 0 : weight for (i, j, weight) in weighted_edges)
maxcut_energy(bits) = -maxcut_weight(bits)
decode_cut(bits) = (
side_zero=findall(==(0), bits),
side_one=findall(==(1), bits),
)
cover_edges = [(1, 2), (1, 3), (2, 3), (3, 4)]
cover_penalty = 2
uncovered_cover_edges(bits) =
[(i, j) for (i, j) in cover_edges if bits[i] == 0 && bits[j] == 0]
cover_size(bits) = sum(bits)
cover_energy(bits) =
cover_size(bits) + cover_penalty * length(uncovered_cover_edges(bits))
cover_feasible(bits) = isempty(uncovered_cover_edges(bits))
decode_cover(bits) = (
selected=findall(==(1), bits),
uncovered=uncovered_cover_edges(bits),
)
Number partitioning¶
With and naming the two groups, the raw energy is the squared signed imbalance. The decoded application score is the absolute imbalance, so a raw energy of 0 means a perfect partition.
partition_model = configured_qaoa_model()
@variable(partition_model, partition_x[1:length(partition_weights)], Bin)
@objective(
partition_model,
Min,
sum(
partition_weights[i] * (1 - 2 * partition_x[i])
for i in eachindex(partition_weights)
)^2,
)
partition_result = run_and_check_qaoa!(
"Number partitioning",
partition_model,
partition_x,
partition_energy,
partition_imbalance,
"absolute imbalance",
_ -> true,
decode_partition,
)
@assert partition_result.baseline.energy == 0
@assert length(partition_result.baseline.optima) == 2
Number partitioning
exact optimum raw energy = 0.0
sampled best: bits=[0, 0, 0, 1], raw energy=0.0, absolute imbalance=0, probability=0.135
most frequent: bits=[0, 0, 0, 1], raw energy=0.0, probability=0.135
decoded best = (group_zero = [1, 3, 4], group_one = [8])
Weighted Max-Cut¶
The application maximizes cut weight, while the QUBO minimizes its negative. Therefore a raw energy of -7 corresponds to the exact cut weight 7.
maxcut_model = configured_qaoa_model()
@variable(maxcut_model, maxcut_x[1:4], Bin)
@objective(
maxcut_model,
Min,
-sum(
weight * (
maxcut_x[i] + maxcut_x[j] - 2 * maxcut_x[i] * maxcut_x[j]
)
for (i, j, weight) in weighted_edges
),
)
maxcut_result = run_and_check_qaoa!(
"Weighted Max-Cut",
maxcut_model,
maxcut_x,
maxcut_energy,
maxcut_weight,
"cut weight",
_ -> true,
decode_cut,
)
@assert maxcut_result.baseline.energy == -7
@assert length(maxcut_result.baseline.optima) == 4
Weighted Max-Cut
exact optimum raw energy = -7.0
sampled best: bits=[0, 1, 1, 0], raw energy=-7.0, cut weight=7, probability=0.17
most frequent: bits=[0, 1, 1, 0], raw energy=-7.0, probability=0.17
decoded best = (side_zero = [1, 4], side_one = [2, 3])
Minimum vertex cover¶
The raw energy adds the selected-vertex count to a penalty for each uncovered edge. With penalty 2 on this instance, every exact optimum is feasible and selects two vertices.
cover_model = configured_qaoa_model()
@variable(cover_model, cover_x[1:4], Bin)
@objective(
cover_model,
Min,
sum(cover_x) + cover_penalty * sum(
(1 - cover_x[i]) * (1 - cover_x[j])
for (i, j) in cover_edges
),
)
cover_result = run_and_check_qaoa!(
"Minimum vertex cover",
cover_model,
cover_x,
cover_energy,
cover_size,
"cover size",
cover_feasible,
decode_cover,
)
@assert cover_result.baseline.energy == 2
@assert length(cover_result.baseline.optima) == 2
@assert cover_feasible(cover_result.best.bits)
Minimum vertex cover
exact optimum raw energy = 2.0
sampled best: bits=[0, 1, 1, 0], raw energy=2.0, cover size=2, probability=0.178
most frequent: bits=[1, 1, 0, 1], raw energy=3.0, probability=0.195
decoded best = (selected = [2, 3], uncovered = Tuple{Int64, Int64}[])
println("Exact-versus-sampled energy summary")
for result in (partition_result, maxcut_result, cover_result)
println(
" ",
rpad(result.name, 24),
" exact=",
result.baseline.energy,
", sampled best=",
result.best.raw_energy,
", most-frequent energy=",
result.most_frequent.raw_energy,
)
end
Exact-versus-sampled energy summary
Number partitioning exact=0.0, sampled best=0.0, most-frequent energy=0.0
Weighted Max-Cut exact=-7.0, sampled best=-7.0, most-frequent energy=-7.0
Minimum vertex cover exact=2.0, sampled best=2.0, most-frequent energy=3.0
The exact and sampled-best energies agree for all three small instances. This says the finite sample included an optimum; it does not prove the optimized quantum state assigns the optimum the highest probability, and it does not justify a performance or quantum-advantage claim. Inspect the separately reported most-frequent row to see why energy and frequency are different questions.
Fixed-parameter circuits and resource audit¶
The trained Max-Cut angles give a representative fixed circuit. We also construct a deterministic circuit for the same QUBO without running another optimizer. fixed_parameter_circuit records the binding convention and objective metadata; resource_audit transpiles locally and reports depth, size, operation counts, and two-qubit counts without submitting a job.
p1_parameters = maxcut_result.metadata["optimized_parameters"]["values"]
p1_circuit, p1_fixed_metadata = QiskitOpt.QAOA.fixed_parameter_circuit(
JuMP.backend(maxcut_model);
parameters=p1_parameters,
reps=1,
parameter_order=:beta_then_gamma,
measure=true,
)
p2_parameters = QiskitOpt.QAOA.random_initial_parameters(
number_of_layers=2,
seed=QAOA_STANDARD_SEED + 2,
)
p2_circuit, p2_fixed_metadata = QiskitOpt.QAOA.fixed_parameter_circuit(
JuMP.backend(maxcut_model);
parameters=p2_parameters,
reps=2,
parameter_order=:beta_then_gamma,
measure=true,
)
@assert p1_fixed_metadata["parameters"]["parameter_names"] == ["β[0]", "γ[0]"]
@assert p2_fixed_metadata["parameters"]["parameter_names"] == [
"β[0]",
"β[1]",
"γ[0]",
"γ[1]",
]
@assert p1_fixed_metadata["parameters"]["values"] == p1_parameters
@assert p1_fixed_metadata["variables"]["order"] == ["1", "2", "3", "4"]
@assert p1_fixed_metadata["measurement"]["enabled"]
@assert p1_fixed_metadata["objective"]["sense"] == "min"
@assert p1_fixed_metadata["objective"]["scale"] == 1.0
@assert p1_fixed_metadata["objective"]["offset"] == 0.0
@assert p1_fixed_metadata["objective"]["qiskit_minimization_sign"] == 1
println("p=1 parameter order: ", p1_fixed_metadata["parameters"]["parameter_names"])
println("p=2 parameter order: ", p2_fixed_metadata["parameters"]["parameter_names"])
println("Variable order: ", p1_fixed_metadata["variables"]["order"])
println("Measurement convention: ", p1_fixed_metadata["measurement"]["variable_bit_order"])
p=1 parameter order: ["β[0]", "γ[0]"]
p=2 parameter order: ["β[0]", "β[1]", "γ[0]", "γ[1]"]
Variable order: ["1", "2", "3", "4"]
Measurement convention: QAOA.count_key_bits(key) returns bits in variable order
p1_audit = QiskitOpt.QAOA.resource_audit(
p1_circuit;
fixed_metadata=p1_fixed_metadata,
optimization_level=1,
transpiler_seed=QAOA_STANDARD_SEED,
return_transpiled_circuit=true,
)
p2_audit = QiskitOpt.QAOA.resource_audit(
p2_circuit;
fixed_metadata=p2_fixed_metadata,
optimization_level=1,
transpiler_seed=QAOA_STANDARD_SEED,
return_transpiled_circuit=true,
)
@assert p1_audit.metadata["status"] == "success"
@assert p2_audit.metadata["status"] == "success"
@assert p1_audit.metadata["transpilation"]["transpiler_seed"] == QAOA_STANDARD_SEED
@assert p2_audit.metadata["transpilation"]["transpiler_seed"] == QAOA_STANDARD_SEED
p1_resources = p1_audit.metadata["transpiled_circuit"]
p2_resources = p2_audit.metadata["transpiled_circuit"]
@assert p2_resources["depth"] > p1_resources["depth"]
@assert p2_resources["size"] > p1_resources["size"]
@assert p2_resources["two_qubit_operation_count"] > p1_resources["two_qubit_operation_count"]
for (p, resources) in ((1, p1_resources), (2, p2_resources))
println(
"p=$p resources: depth=$(resources["depth"]), size=$(resources["size"]), ",
"two-qubit operations=$(resources["two_qubit_operation_count"]), ",
"operations=$(resources["operations"])",
)
end
p=1 resources: depth=8, size=17, two-qubit operations=5, operations=Dict("barrier" => 1, "rzz" => 5, "measure" => 4, "rx" => 4, "h" => 4)
p=2 resources: depth=13, size=26, two-qubit operations=10, operations=Dict("barrier" => 1, "rzz" => 10, "measure" => 4, "rx" => 8, "h" => 4)
rendered_qiskit_key = "0101"
decoded_variable_bits = QiskitOpt.QAOA.count_key_bits(rendered_qiskit_key)
@assert decoded_variable_bits == [1, 0, 1, 0]
println(
"Qiskit key $rendered_qiskit_key is printed highest classical bit first; ",
"QAOA.count_key_bits returns variable order $decoded_variable_bits.",
)
Qiskit key 0101 is printed highest classical bit first; QAOA.count_key_bits returns variable order [1, 0, 1, 0].
Run on IBM quantum hardware¶
The next two cells follow the same local-fallback pattern as the D-Wave notebook: first prepare and inspect a dry run, then optionally submit the measured Max-Cut circuit to a real IBM backend. The live path calls ibm_runtime_handoff(...; dry_run=false), which resolves the configured backend, transpiles for that hardware, and submits a SamplerV2 job. On success, ibm_hardware_job is the real Runtime job handle.
Before starting Jupyter, choose a currently available backend in your IBM Quantum account and set these values outside the notebook:
QUBONOTEBOOKS_QAOA_ENABLE_IBM=1explicitly requests a hardware job;QUBONOTEBOOKS_QAOA_IBM_BACKENDnames the backend selected for this run;QISKIT_IBM_TOKENcomes from an environment variable or secret store;optionally
QISKIT_IBM_INSTANCEandQISKIT_IBM_CHANNEL
After a successful submission, use ibm_hardware_job.status() to monitor it and ibm_hardware_job.result() after completion. Decode returned Qiskit count keys with QiskitOpt.QAOA.count_key_bits before evaluating the application score.
With the opt-in disabled, configuration missing, or IBM Runtime unavailable, no hardware job is submitted and the local Aer results above remain the fallback. The notebook never calls save_account, writes credentials, prints the token, or embeds a live backend name.
ibm_hardware_requested = get(ENV, "QUBONOTEBOOKS_QAOA_ENABLE_IBM", "0") == "1"
ibm_backend = strip(get(ENV, "QUBONOTEBOOKS_QAOA_IBM_BACKEND", ""))
ibm_channel_value = strip(get(ENV, "QISKIT_IBM_CHANNEL", ""))
ibm_instance_value = strip(get(ENV, "QISKIT_IBM_INSTANCE", ""))
ibm_token_is_configured = !isempty(strip(get(ENV, "QISKIT_IBM_TOKEN", "")))
ibm_channel = isempty(ibm_channel_value) ? nothing : ibm_channel_value
ibm_instance = isempty(ibm_instance_value) ? nothing : ibm_instance_value
dry_run_backend = isempty(ibm_backend) ? "not-configured" : ibm_backend
ibm_dry_run = QiskitOpt.QAOA.ibm_runtime_handoff(
p1_circuit;
fixed_metadata=p1_fixed_metadata,
backend=dry_run_backend,
shots=QAOA_FINAL_SHOTS,
transpiler_seed=QAOA_STANDARD_SEED,
channel=ibm_channel,
instance=ibm_instance,
dry_run=true,
)
@assert ibm_dry_run.job === nothing
@assert ibm_dry_run.metadata["runtime_handoff"]["mode"] == "dry_run"
@assert !ibm_dry_run.metadata["runtime_handoff"]["credentials_recorded"]
println(
"IBM handoff dry run: backend configured=",
!isempty(ibm_backend),
", shots=",
ibm_dry_run.metadata["runtime_handoff"]["shots"],
", credentials recorded=false",
)
IBM handoff dry run: backend configured=false, shots=512, credentials recorded=false
function submit_ibm_hardware_if_requested(
requested,
backend,
token_is_configured,
circuit,
fixed_metadata;
channel=nothing,
instance=nothing,
)
if !requested
println(
"IBM quantum hardware submission is disabled. Set ",
"QUBONOTEBOOKS_QAOA_ENABLE_IBM=1 with a backend and token to submit. ",
"The local Aer results above remain available.",
)
return (submitted=false, run=nothing, failure=nothing, runtime=nothing)
end
missing_configuration = String[]
isempty(backend) && push!(missing_configuration, "QUBONOTEBOOKS_QAOA_IBM_BACKEND")
!token_is_configured && push!(missing_configuration, "QISKIT_IBM_TOKEN")
if !isempty(missing_configuration)
println(
"IBM quantum hardware was requested, but ",
join(missing_configuration, " and "),
" is missing. No job was submitted; the local Aer results above remain available.",
)
return (submitted=false, run=nothing, failure=nothing, runtime=nothing)
end
runtime = QiskitOpt.check_runtime(; local_backend=false, ibm=true, verbose=false)
if !runtime.ok || isnothing(runtime.ibm_runtime) || !runtime.ibm_runtime.ok
println(
"IBM Runtime packages are unavailable. No job was submitted; ",
"the local Aer results above remain available.",
)
return (submitted=false, run=nothing, failure=nothing, runtime=runtime)
end
try
hardware_run = QiskitOpt.QAOA.ibm_runtime_handoff(
circuit;
fixed_metadata=fixed_metadata,
backend=backend,
shots=QAOA_FINAL_SHOTS,
transpiler_seed=QAOA_STANDARD_SEED,
channel=channel,
instance=instance,
dry_run=false,
)
println(
"IBM quantum hardware job submitted to the configured backend. Job ID: ",
hardware_run.metadata["runtime_handoff"]["job"]["id"],
)
return (submitted=true, run=hardware_run, failure=nothing, runtime=runtime)
catch err
if err isa QiskitOpt.QAOA.RuntimeHandoffError
handoff = err.metadata["runtime_handoff"]
println(
"IBM quantum hardware submission failed before a job was returned. ",
"The local Aer results above remain available.",
)
println("Sanitized handoff status: ", get(handoff, "status", "unknown"))
return (submitted=false, run=nothing, failure=err.metadata, runtime=runtime)
end
rethrow()
end
end
ibm_hardware = submit_ibm_hardware_if_requested(
ibm_hardware_requested,
ibm_backend,
ibm_token_is_configured,
p1_circuit,
p1_fixed_metadata;
channel=ibm_channel,
instance=ibm_instance,
)
ibm_hardware_submitted = ibm_hardware.submitted
ibm_hardware_run = ibm_hardware.run
ibm_hardware_failure = ibm_hardware.failure
ibm_hardware_job = ibm_hardware_submitted ? ibm_hardware_run.job : nothing
ibm_hardware_required = get(ENV, "QUBONOTEBOOKS_QAOA_REQUIRE_IBM", "0") == "1"
if ibm_hardware_required && !ibm_hardware_submitted
error("IBM hardware verification was required, but no job was submitted.")
end
IBM quantum hardware submission is disabled. Set QUBONOTEBOOKS_QAOA_ENABLE_IBM=1 with a backend and token to submit. The local Aer results above remain available.
Practice checkpoints¶
Find a problem above where the most-frequent sample differs from the first minimum-energy sample. Why does that not invalidate the exact-energy check?
Compare the transpiled and resource dictionaries. Which counts scale directly with the number of repeated cost layers on this instance?
Convert the rendered Qiskit key
1100to variable order before evaluating an application score.
# EXERCISE 1: Inspect each result's best and most_frequent rows.
nothing
Notebook Cell
# SOLUTION (hidden in workshop version):
frequency_differences = [
result.name
for result in (partition_result, maxcut_result, cover_result)
if result.best.bits != result.most_frequent.bits
]
println("Problems whose selected minimum-energy row differs from the most-frequent row: $frequency_differences")
Problems whose selected minimum-energy row differs from the most-frequent row: ["Minimum vertex cover"]
# EXERCISE 2: Compare depth, size, and two-qubit counts between p=1 and p=2.
nothing
Notebook Cell
# SOLUTION (hidden in workshop version):
println(
"Increasing p from 1 to 2 changes depth $(p1_resources["depth"])→$(p2_resources["depth"]), ",
"size $(p1_resources["size"])→$(p2_resources["size"]), and two-qubit operations ",
"$(p1_resources["two_qubit_operation_count"])→$(p2_resources["two_qubit_operation_count"]).",
)
Increasing p from 1 to 2 changes depth 8→13, size 17→26, and two-qubit operations 5→10.
# EXERCISE 3: Decode rendered Qiskit key "1100" into [x1, x2, x3, x4].
nothing
Notebook Cell
# SOLUTION (hidden in workshop version):
practice_decoded_bits = QiskitOpt.QAOA.count_key_bits("1100")
@assert practice_decoded_bits == [0, 0, 1, 1]
println("Rendered key 1100 becomes variable-order bits $practice_decoded_bits.")
Rendered key 1100 becomes variable-order bits [0, 0, 1, 1].
Summary¶
Learning objectives met:
The convention turns a minimization QUBO into a diagonal cost Hamiltonian whose computational-basis values are raw QUBO energies.
A local Aer backend, bounded /shot/iteration budget, and explicit sampler/simulator/transpiler seeds make the tutorial path reproducible and credential-free.
Independent enumeration verifies the sampled best energy and decoded feasibility for number partitioning, weighted Max-Cut, and minimum vertex cover.
Sample probability and most-frequent sample are reported separately from raw energy and application score.
Public QiskitOpt APIs expose beta-then-gamma binding, variable/count-key order, objective scale/sign/offset, and transpiled circuit resources.
The IBM section performs a safe dry run by default and can submit the fixed circuit to real quantum hardware when the backend, secret, and explicit environment opt-in are configured.
Expected runtime class: after the Julia and Python environments are instantiated, the three four-qubit local solves and two circuit audits are a short tutorial run (normally well under two minutes on a laptop-class CPU). First-time package installation is separate.
Next steps: Increase one budget dimension at a time, retain the exact baseline, and compare distributions across several seeds before drawing statistical conclusions.
Further reading:
The original QAOA paper defines the alternating cost/mixer construction and its depth parameter.
The QiskitOpt.jl best-practices guide documents the maintained local-Aer, fixed-circuit, resource-audit, and Runtime-handoff APIs used here.
References¶
E. Farhi, J. Goldstone, and S. Gutmann, A Quantum Approximate Optimization Algorithm, arXiv:1411.4028 (2014), https://
arxiv .org /abs /1411 .4028. A. R. Mazumder and S. Tayur, Five Starter Problems: Solving Quadratic Unconstrained Binary Optimization Models on Quantum Computers, TutORials in Operations Research (2025), pp. 145–183, Mazumder & Tayur (2025).
QiskitOpt.jl public QAOA interface and best-practices guide: https://
github .com /JuliaQUBO /QiskitOpt .jl. Companion materials cited for pedagogical context: https://
github .com /arulrhikm /Solving -QUBOs -on -Quantum -Computers.
This notebook contains original Julia code and prose. The companion repository is cited as context; no source cells, prose, saved output, or assets were copied from it.
- 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