Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Benchmarking (Python)

Maintained by the JuliaQUBO organization
SECQUOIA  ·  PSR Energy

Open In Colab

Setup

Google Colab

Click the badge above to open this notebook in Colab. The notebook installs or activates dependencies in the setup cells below.

Local installation

Run the following from the repository root before opening this notebook locally:

python -m pip install dimod dwave-neal matplotlib networkx numpy pandas pyomo scipy tqdm

The bundled cache avoids the long benchmark regeneration cells.

Learning objectives

By the end of this notebook you will be able to:

  1. Define solver, instance, schedule, and hyperparameter ensembles for benchmarking experiments.

  2. Compute and interpret time-to-solution, success probability, and performance-ratio summaries.

  3. Use bootstrap summaries to compare solver settings across repeated samples and instance ensembles.

  4. Explain the difference between default, tuned, ensemble-selected, and virtual-best parameter choices.

Prerequisites

Mathematical background: QUBO/Ising models, probability, medians, and confidence intervals.
Prior notebooks: Notebooks 2-4, especially QUBO construction and simulated/quantum annealing outputs.
Accounts required: None for the bundled cache; regenerating hardware data may require solver access.
Python version: Python 3.9+ with the repository’s benchmarking dependencies.

About this notebook

A solver is the combination of hardware, algorithm, software, parameter settings, and resource limits used to solve an optimization problem. No configuration is best for every problem family or computational budget.

Tuning every parameter on the target instance consumes the same time, memory, or energy budget that the final solve is meant to conserve, and it can overfit one instance. A more realistic workflow tunes configurations offline on a representative family of training instances, then evaluates the selected configuration on held-out or previously unseen instances.

This notebook develops metrics and experiments for comparing solver configurations across repeated samples and related problem instances.

Benchmarking example

For illustration purposes, we will use an example that you are already familiar with, which is an Ising model. As a solver, we will use a simulated annealing code provided by D-Wave Ocean Tools.

Ising model

We use D-Wave’s dimod and neal packages to define Ising models and solve them with simulated annealing.

Ising problem statement

We pose the Ising problem as the following optimization problem:

min⁡s∈{±1}nH(s)=min⁡s∈{±1}n∑(i,j)∈E(G)Ji,jsisj+∑i∈V(G)hisi+β\min_{s \in \{ \pm 1 \}^n} H(s) = \min_{s \in \{ \pm 1 \}^n} \sum_{(i, j) \in E(G)} J_{i,j}s_is_j + \sum_{i \in V(G)} h_is_i + \beta

where we optimize over spins s∈{±1}ns \in \{ \pm 1 \}^n, on a constrained graph G(V,E)G(V,E), where the quadratic coefficients are Ji,jJ_{i,j} and the linear coefficients are hih_i. We also include an arbitrary offset of the Ising model β\beta.

Small Ising example

Suppose we have an Ising model defined from

h=[145.0122.0122.0266.0266.0266.0242.5266.0386.5387.0386.5],J=[00024242424242424240002402424242424240000240242424242400002448242448484800000242448484848000000242448484800000002448484800000000484848000000000727200000000007200000000000] and cI=1319.5h = \begin{bmatrix} 145.0 \\ 122.0 \\ 122.0 \\ 266.0 \\ 266.0 \\ 266.0 \\ 242.5 \\ 266.0 \\ 386.5 \\ 387.0 \\ 386.5 \end{bmatrix}, J = \begin{bmatrix} 0 & 0 & 0 & 24 & 24 & 24 & 24 & 24 & 24 & 24 & 24\\ 0 & 0 & 0 & 24 & 0 & 24 & 24 & 24 & 24 & 24 & 24\\ 0 & 0 & 0 & 0 & 24 & 0 & 24 & 24 & 24 & 24 & 24\\ 0 & 0 & 0 & 0 & 24 & 48 & 24 & 24 & 48 & 48 & 48\\ 0 & 0 & 0 & 0 & 0 & 24 & 24 & 48 & 48 & 48 & 48\\ 0 & 0 & 0 & 0 & 0 & 0 & 24 & 24 & 48 & 48 & 48\\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 24 & 48 & 48 & 48\\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 48 & 48 & 48\\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 72 & 72\\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 72\\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\ \end{bmatrix} \text{ and } c_I = 1319.5

Let’s solve this problem

Notebook Cell
<python-environment>/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
Image produced in Jupyter

Since the problem is relatively small (11 variables, 211=20482^{11}=2048 combinations), we can afford to enumerate all the solutions.

runtime: 0.001972198486328125 seconds
minimum energy: 5.0
minimum energy: 5.0
<Axes: title={'center': 'Enumerate all solutions'}, xlabel='Energy', ylabel='Probabilities'>
Image produced in Jupyter
Image produced in Jupyter

We observe that the optimal solution of this problem is x10=1,0x_{10} = 1, 0 otherwise, leading to an objective of 5. Notice that this problem has a degenerate optimal solution given that x8=1,0x_8 = 1, 0 otherwise also leads to the same solution.

Let’s now solve this problem using Simulated Annealing

Simulated Annealing (SA)

SA is an optimization algorithm that works by navigating the input space and evaluating the objective function (energy) similarly to hill climbing. The main difference is that SA implements a strategy to avoid getting stuck in local optima.

The general idea is to allow moves to a worse position, but in a structured way. When a candidate solution is selected, the probability of actually moving to the new position is determined by the change in the objective function and by a metric called temperature. When the temperature is high, the chance of performing a jump to a worse state increases, and the opposite happens when the temperature is low. By continuously decreasing the temperature during the execution of the algorithm, it can escape local optima during the first stages (high temperature), but it settles into a position towards the end of the execution (low temperature).

Probability of Making a Jump

Case 1:

ΔE=Ecurrent−Enext>0\Delta E = E_{current} - E_{next} > 0

It is always advantageous to move to a state with a better objective function. In this case, the probability is P=1P = 1.

Case 2:

ΔE=Ecurrent−Enext<0\Delta E = E_{current} - E_{next} < 0

In this case, the probability of taking the step is determined by:

P=e−∣ΔE∣/TP = e^{-|\Delta E| / T}

Note: This formulation applies to a minimization problem. To solve a maximization problem, the only change is that ΔE=Enext−Ecurrent\Delta E = E_{next} - E_{current}.

Temperature Decrease

The way the temperature decreases during the algorithm’s execution varies between implementations. This is referred to as the temperature schedule, which is a function of time or the number of iterations.

Note: β=1/T\beta = 1 / T is also commonly used in the implementation of the algorithm.

minimum energy: 5.0
minimum energy: 5.0
<Axes: title={'center': 'Simulated annealing in default parameters'}, xlabel='Energy', ylabel='Probabilities'>
<Figure size 640x480 with 1 Axes>
<Figure size 640x480 with 1 Axes>

We are going to use the default limits of temperature given by the simulating annealing code. These are defined using the minimum and maximum nonzero coefficients in the Ising model. Then the range for beta is defined as

β∈[log⁡(2)max⁡{ΔE},log⁡(100)min⁡{ΔE}]\beta \in \left[ \frac{\log(2)}{\max \{ \Delta E \} },\frac{\log(100)}{\min \{ \Delta E \} } \right]

where

ΔE=min⁡i{hi}+∑jJij+Jji\Delta E = \min_{i} \{h_i \} + \sum_j J_{ij}+J_{ji}

Hot temperature: We want to scale hot_beta so that for the most unlikely qubit flip, we get at least 50% chance of flipping. (This means all other qubits will have > 50% chance of flipping initially). Most unlikely flip is when we go from a very low energy state to a high energy state, thus we calculate hot_beta based on max_delta_energy.

0.50=exp⁡(−β‾∗max⁡{ΔE})0.50 = \exp(-\overline{\beta} * \max \{ \Delta E \})

Cold temperature: Towards the end of the annealing schedule, we want to minimize the chance of flipping. Don’t want to be stuck between small energy tweaks. Hence, set cold_beta so that at minimum energy change, the chance of flipping is set to 1%.

0.01=exp⁡(−β‾∗min⁡{ΔE})0.01 = \exp(-\underline{\beta} * \min \{ \Delta E \})

By default, the schedule also follows a geometric series.

Image produced in Jupyter

Now let’s compute an expected time metric with respect to the number of sweeps in simulated annealing.

Image produced in Jupyter

These plots represent ofter contradictory metrics, on one hand you would like to obtain a large probability of finding a right solution (the deffinition of right comes from what you define as success). On the other hand, the time it takes to solve these cases should be as small as possible. This is why we are interested in a metric that combines both, and that is why we settle on the Time To Solution (TTS) which is defined as

TTS=log⁡1−slog⁡1−pTTS = \frac{\log{1-s}}{\log{1-p}}

where s is a success factor, usually takes as s=99%s = 99\%, and pp is the success probability, usually accounted as the observed success probability.

One usually reads this as the time to solution within 99% probability.

Image produced in Jupyter

As you can notice, the default parameters given by D-Wave (number of sweeps = 1000 and a geometric update of β\beta) are not optimal for our tiny example in terms of expected runtime. This is certainly a function of the problem, for such a small instance having two sweeps are more than enough and more sweeps are an overkill. This parameters choice might not generalize to any other problem, as seen below.

Random-instance benchmark

Let’s define a larger model, with 100 variables and random weights, to see how this performance changes.

Assume that we are interested in an instance with random weights hi,Ji,j∼U[−1,+1]h_{i}, J_{i, j} \sim U[-1, +1].

Image produced in Jupyter

For a problem of this size we cannot do a complete enumeration (2100≈1.2e302^{100} \approx 1.2e30) but we can randomly sample the distribution of energies to have a baseline for our later comparisons.

Average random energy:0.8893359888718223
minimum energy: -114.83700225525205
<Axes: title={'center': 'Random sampling'}, xlabel='solution', ylabel='Energy'>
<Figure size 640x480 with 1 Axes>
minimum energy: -424.47469301518714
runtime: 1.7792482376098633 seconds

Random-instance problem statement

We pose the Ising problem as the following optimization problem:

min⁡s∈{±1}nH(s)=min⁡s∈{±1}n∑(i,j)∈E(G)Ji,jsisj+∑i∈V(G)hisi+β\min_{s \in \{ \pm 1 \}^n} H(s) = \min_{s \in \{ \pm 1 \}^n} \sum_{(i, j) \in E(G)} J_{i,j}s_is_j + \sum_{i \in V(G)} h_is_i + \beta

where we optimize over spins s∈{±1}ns \in \{ \pm 1 \}^n, on a constrained graph G(V,E)G(V,E), where the quadratic coefficients are Ji,jJ_{i,j} and the linear coefficients are hih_i. We also include an arbitrary offset of the Ising model β\beta.

minimum energy: -424.47469301518714
minimum energy: -424.47469301518714
<Axes: title={'center': 'Simulated annealing with default parameters'}, xlabel='Energy', ylabel='Probabilities'>
<Figure size 640x480 with 1 Axes>
<Figure size 640x480 with 1 Axes>

Notice that the minimum energy coming from the random sampling and the one from the simulated annealing are very different. Moreover, the distributions that both lead to are extremely different too.

{'beta_range': [np.float64(0.006275211137529919), np.float64(32300.48293714536)], 'beta_schedule_type': 'geometric', 'timing': {'preprocessing_ns': 10681407, 'sampling_ns': 1768027756, 'postprocessing_ns': 405698}}
Image produced in Jupyter

We can solve this problem using IP such that we have guarantees that it is solved to optimality (this might be a great quiz for future lectures), but in this case let us define the “success” as getting an objective certain percentage of the best found solution in all cases (which we see it might not be even found with the default parameters). To get a scaled version of this success equivalent for all instances, we will define this success with respect to the metric:

found−randomminimum−random\frac{found - random}{minimum - random}

Where foundfound corresponds to the best found solution within our sampling, randomrandom is the mean of the random sampling shown above, and minimumminimum corresponds to the best found solution to our problem during the exploration. Consider that this minimum might not be the global minimum. The best possible performance ratio is 1, attained at the best-known value; a negative ratio means the method performs worse than random sampling. Success is now counted as being within a specified threshold of this value of 1. We will refer to this quality of solution metric as Performance Ratio.

Before figuring out if we have the right optimal parameters, we want to save some effort by loading previously computed results. Run the next cell to download the precomputed benchmark cache before continuing. Skipping this download, or changing overwrite_pickles to regenerate the data, can trigger approximately 3 hours of local computation. Otherwise, a zip file with precomputed results will be downloaded from GitHub.

Results zip file has been extracted to <notebook-directory>/results

Now either we have the pickled file or not, let us compute the statistics we are looking for.

After gathering all the results, we would like to see the progress of the Performance Ratio with respect to the increasing number of sweeps. To account for the stochasticity of this method, we are bootstrapping all of our results with different values of the bootstrapping sample, and each confidence interval corresponds to a standard deviation away from the mean.

[(0.8, 1.01)]
Image produced in Jupyter

Now, besides looking at the sweeps, which are our parameter, we want to see how the performance changes with respect to the number of shots, which in this case are proportional to the computational time/effort that it takes to solve the problem.

[(0.8, 1.01)]
Image produced in Jupyter
Image produced in Jupyter
Image produced in Jupyter
Image produced in Jupyter
A better solution of -424.47469301518714 was found for sweep 85
A better solution of -424.47469301518714 was found for sweep 1000
A better solution of -424.47469301518714 was found for sweep 10
A better solution of -424.47469301518714 was found for sweep 500
<Figure size 640x480 with 1 Axes>

Here see how using the optimal number of sweeps is better than using other values (including the default recommended by the solver) in terms of solving this problem. Obviously, we only know this after running the experiments and verifying it ourselves. This is not the usual case, so we want to see how well can we do if we solve similar (but no the same instances). Here we will generate 20 random instances from the same distribution and size but different random seed.

Warning - long-running computation The following cell can run 20 instances x 320 sweep values x 1000 reads when the bundled cache is unavailable or overwrite_pickles is set to True. Expected runtime is about 3 hours on a standard CPU runtime.

Before running it, use the precomputed cache cells above whenever possible. If you regenerate the data, keep the browser tab open; partial JSON results are saved after each instance.

Now we bootstrap our solutions with respect to the whole set of instances, or ensemble, and we use the median which represents the solution better than the mean.

minimum median TTS for geometric schedule = infs at sweep = 112
minimum TTS for instance 42 with geometric schedule = 3.3120138340301897s at sweep = 85
[Text(0.5, 0, 'Sweeps')]
<Figure size 640x480 with 2 Axes>
[(0.8, 1.01)]
<Figure size 640x480 with 1 Axes>
[(0.8, 1.01)]
<Figure size 640x480 with 1 Axes>
Image produced in Jupyter

Notice how much performance would we be losing if we had used the default value for all these instances, and how much we could eventually win if we knew the best for each.

A better solution of -424.47469301518714 was found for sweep 112
<Figure size 640x480 with 1 Axes>

This example shows that for the benchmark instance, using the mean of the best accross the ensemble is better than using the default, but not as good as if we knew from scratch what would have made the best case.

After figuring out what would be the best parameter for our instance of interest, it would be nice to see what the ensemble performance is. We have several choices, either going with the (arbitrary) default values, or using the mean of the best performance we have found up to that point. There is an unachievable goal, which would be the case where we knew the best solution of each case, which we call the virtual best. This helps us understand how much is at stake with the choice of parameters we make.

<Figure size 640x480 with 1 Axes>

As you can see, there is a gap between the case with the best mean performance and the virtual best. This difference is arguably small, but you can imagine that with a larger number of parameters, this difference can become larger and larger, making the search of good parameters more worthy and complicated. We are actively working on such parameter setting strategies, and expect to make progress in this area (keep tuned!).

Practice checkpoints

Use these checkpoints during the workshop to test the main ideas before moving on.

Notebook Cell
solver_results = new_sampler: best_energy=-12.6, time=2.1; baseline_sa: best_energy=-12.4, time=1.8
Notebook Cell
target_confidence = 0.99; tts_reads = 2900
Notebook Cell
performance_ratio = 0.75

Summary

In this notebook we:

  • Treated a solver as a combination of hardware, algorithm, software, and hyperparameters to make comparisons reproducible.

  • Loaded or generated benchmark samples and converted raw sampling results into success probabilities and time-to-solution metrics.

  • Used bootstrap summaries and performance ratios to compare schedules, sweeps, default choices, and virtual-best behavior.

  • Interpreted how tuning on one instance or ensemble can improve results while still leaving a gap to per-instance best choices.

Learning objectives met: You practiced setting up benchmark ensembles, computing TTS and performance ratios, using bootstrap summaries, and interpreting scaling and tuning tradeoffs.

Next steps: Proceed to Notebook 6: QCi to see another quantum-inspired solver workflow and compare its modeling interface with QUBO/BQM approaches.

Further reading:

Acknowledgments

This notebook was developed by:

References
  1. Rønnow, T. F., Wang, Z., Job, J., Boixo, S., Isakov, S. V., Wecker, D., Martinis, J. M., Lidar, D. A., & Troyer, M. (2014). Defining and detecting quantum speedup. Science, 345(6195), 420–424. 10.1126/science.1252319