Code
qcr:2606.27930.1

Minimum Eigen Optimizer

This Qiskit Optimization tutorial introduces the MinimumEigenOptimizer, the bridge that lets any of Qiskit's minimum-eigensolvers (such as VQE, QAOA, or the classical NumPyMinimumEigensolver) solve a combinatorial optimization problem. The key insight is that a quadratic unconstrained binary optimization (QUBO) problem maps exactly onto finding the ground state of an Ising Hamiltonian, so solving the optimization is equivalent to finding that Hamiltonian's minimum eigenvalue. The optimizer automates this: it takes a QuadraticProgram, converts it to a QUBO and then to an Ising operator, hands that operator to the chosen minimum-eigensolver, and translates the resulting ground-state measurement back into values of the original decision variables. The tutorial shows how to wrap QAOA and VQE in the MinimumEigenOptimizer, solve a sample problem, and compare the quantum result against an exact classical eigensolver for validation. It explains how this design cleanly separates the problem (the QuadraticProgram) from the solver (any eigensolver), making it easy to swap algorithms. It is the central pattern for quantum combinatorial optimization in Qiskit.
Optimization
Qubit
Circuit-based
Uploaded 4 days ago
12
Views
Citing this entry? Use this QCR ID
Uploaded by
QL
QCR Librarian

Overview

qiskit-community/qiskit-optimization
282149
In [ ]:
# --- Setup cell added by QCR (not part of the original tutorial) ---
# Source: qiskit-community/qiskit-optimization @ 0.7.0, Apache License 2.0.
# Installs the example's dependencies. If a later cell still reports a missing
# package, restart the runtime/kernel and run again from the top.
%pip install -q qiskit-optimization==0.7.0 qiskit-aer

Minimum Eigen Optimizer

Introduction

An interesting class of optimization problems to be addressed by quantum computing are Quadratic Unconstrained Binary Optimization (QUBO) problems. Finding the solution to a QUBO is equivalent to finding the ground state of a corresponding Ising Hamiltonian, which is an important problem not only in optimization, but also in quantum chemistry and physics. For this translation, the binary variables taking values in are replaced by spin variables taking values in , which allows one to replace the resulting spin variables by Pauli Z matrices, and thus, an Ising Hamiltonian. For more details on this mapping we refer to [1].

Qiskit optimization provides automatic conversion from a suitable QuadraticProgram to an Ising Hamiltonian, which then allows leveraging all the SamplingMinimumEigensolver implementations, such as

  • SamplingVQE,
  • QAOA, or
  • NumpyMinimumEigensolver (classical exact method).

Note 1: MinimumEigenOptimizer does not support VQE. But SamplingVQE can be used instead.

Note 2: MinimumEigenOptimizer can use NumpyMinimumEigensolver as an exception case though it inherits MinimumEigensolver (not SamplingMinimumEigensolver).

Qiskit optimization provides a the MinimumEigenOptimizer class, which wraps the translation to an Ising Hamiltonian (in Qiskit Terra also called SparsePauliOp), the call to a MinimumEigensolver, and the translation of the results back to an OptimizationResult.

In the following we first illustrate the conversion from a QuadraticProgram to a SparsePauliOp and then show how to use the MinimumEigenOptimizer with different MinimumEigensolvers to solve a given QuadraticProgram. The algorithms in Qiskit optimization automatically try to convert a given problem to the supported problem class if possible, for instance, the MinimumEigenOptimizer will automatically translate integer variables to binary variables or add linear equality constraints as a quadratic penalty term to the objective. It should be mentioned that a QiskitOptimizationError will be thrown if conversion of a quadratic program with integer variables is attempted.

The circuit depth of QAOA potentially has to be increased with the problem size, which might be prohibitive for near-term quantum devices. A possible workaround is Recursive QAOA, as introduced in [2]. Qiskit optimization generalizes this concept to the RecursiveMinimumEigenOptimizer, which is introduced at the end of this tutorial.

References

[1] A. Lucas, Ising formulations of many NP problems, Front. Phys., 12 (2014).

[2] S. Bravyi, A. Kliesch, R. Koenig, E. Tang, Obstacles to State Preparation and Variational Optimization from Symmetry Protection, arXiv preprint arXiv:1910.08980 (2019).

Converting a QUBO to a SparsePauliOp

In [1]:
import numpy as np
from qiskit.primitives import StatevectorSampler
from qiskit.visualization import plot_histogram
from qiskit_optimization import QuadraticProgram
from qiskit_optimization.algorithms import (
    MinimumEigenOptimizer,
    OptimizationResultStatus,
    RecursiveMinimumEigenOptimizer,
    SolutionSample,
)
from qiskit_optimization.minimum_eigensolvers import QAOA, NumPyMinimumEigensolver
from qiskit_optimization.optimizers import COBYLA
from qiskit_optimization.utils import algorithm_globals
In [2]:
# create a QUBO
qubo = QuadraticProgram()
qubo.binary_var("x")
qubo.binary_var("y")
qubo.binary_var("z")
qubo.minimize(linear=[1, -2, 3], quadratic={("x", "y"): 1, ("x", "z"): -1, ("y", "z"): 2})
print(qubo.prettyprint())
Problem name: 

Minimize
  x*y - x*z + 2*y*z + x - 2*y + 3*z

Subject to
  No constraints

  Binary variables (3)
    x y z

Next we translate this QUBO into an Ising operator. This results not only in a SparsePauliOp but also in a constant offset to be taken into account to shift the resulting value.

In [3]:
op, offset = qubo.to_ising()
print("offset: {}".format(offset))
print("operator:")
print(op)
offset: 1.5
operator:
SparsePauliOp(['IIZ', 'IZI', 'ZII', 'IZZ', 'ZIZ', 'ZZI'],
              coeffs=[-0.5 +0.j,  0.25+0.j, -1.75+0.j,  0.25+0.j, -0.25+0.j,  0.5 +0.j])

Sometimes a QuadraticProgram might also directly be given in the form of a SparsePauliOp. For such cases, Qiskit optimization also provides a translator from a SparsePauliOp back to a QuadraticProgram, which we illustrate in the following.

In [4]:
qp = QuadraticProgram()
qp.from_ising(op, offset, linear=True)
print(qp.prettyprint())
Problem name: 

Minimize
  x0*x1 - x0*x2 + 2*x1*x2 + x0 - 2*x1 + 3*x2

Subject to
  No constraints

  Binary variables (3)
    x0 x1 x2

This translator allows, for instance, one to translate a SparsePauliOp to a QuadraticProgram and then solve the problem with other algorithms that are not based on the Ising Hamiltonian representation, such as the GroverOptimizer.

Solving a QUBO with the MinimumEigenOptimizer

We start by initializing the MinimumEigensolver we want to use.

In [5]:
algorithm_globals.random_seed = 10598
qaoa_mes = QAOA(sampler=StatevectorSampler(seed=123), optimizer=COBYLA(), initial_point=[1, 1])
exact_mes = NumPyMinimumEigensolver()

Then, we use the MinimumEigensolver to create MinimumEigenOptimizer.

In [6]:
qaoa = MinimumEigenOptimizer(qaoa_mes)  # using QAOA
exact = MinimumEigenOptimizer(exact_mes)  # using the exact classical numpy minimum eigen solver

We first use the MinimumEigenOptimizer based on the classical exact NumPyMinimumEigensolver to get the optimal benchmark solution for this small example.

In [7]:
exact_result = exact.solve(qubo)
print(exact_result.prettyprint())
objective function value: -2.0
variable values: x=0.0, y=1.0, z=0.0
status: SUCCESS

Next we apply the MinimumEigenOptimizer based on QAOA to the same problem.

In [8]:
qaoa_result = qaoa.solve(qubo)
print(qaoa_result.prettyprint())
objective function value: -2.0
variable values: x=0.0, y=1.0, z=0.0
status: SUCCESS

Analysis of Samples

OptimizationResult provides useful information in the form of SolutionSamples (here denoted as samples). Each SolutionSample contains information about the input values (x), the corresponding objective function value (fval), the fraction of samples corresponding to that input (probability), and the solution status (SUCCESS, FAILURE, INFEASIBLE). Multiple samples corresponding to the same input are consolidated into a single SolutionSample (with its probability attribute being the aggregate fraction of samples represented by that SolutionSample).

In [9]:
print("variable order:", [var.name for var in qaoa_result.variables])
for s in qaoa_result.samples:
    print(s)
variable order: ['x', 'y', 'z']
SolutionSample(x=array([0., 1., 0.]), fval=np.float64(-2.0), probability=0.4501953125, status=<OptimizationResultStatus.SUCCESS: 0>)
SolutionSample(x=array([0., 0., 0.]), fval=np.float64(0.0), probability=0.232421875, status=<OptimizationResultStatus.SUCCESS: 0>)
SolutionSample(x=array([1., 1., 0.]), fval=np.float64(0.0), probability=0.12890625, status=<OptimizationResultStatus.SUCCESS: 0>)
SolutionSample(x=array([1., 0., 0.]), fval=np.float64(1.0), probability=0.119140625, status=<OptimizationResultStatus.SUCCESS: 0>)
SolutionSample(x=array([0., 0., 1.]), fval=np.float64(3.0), probability=0.021484375, status=<OptimizationResultStatus.SUCCESS: 0>)
SolutionSample(x=array([1., 0., 1.]), fval=np.float64(3.0), probability=0.025390625, status=<OptimizationResultStatus.SUCCESS: 0>)
SolutionSample(x=array([0., 1., 1.]), fval=np.float64(3.0), probability=0.0224609375, status=<OptimizationResultStatus.SUCCESS: 0>)

We may also want to filter samples according to their status or probabilities.

In [10]:
def get_filtered_samples(
    samples: list[SolutionSample],
    threshold: float = 0,
    allowed_status: tuple[OptimizationResultStatus] = (OptimizationResultStatus.SUCCESS,),
):
    res = []
    for s in samples:
        if s.status in allowed_status and s.probability > threshold:
            res.append(s)

    return res
In [11]:
filtered_samples = get_filtered_samples(
    qaoa_result.samples, threshold=0.005, allowed_status=(OptimizationResultStatus.SUCCESS,)
)
for s in filtered_samples:
    print(s)
SolutionSample(x=array([0., 1., 0.]), fval=np.float64(-2.0), probability=0.4501953125, status=<OptimizationResultStatus.SUCCESS: 0>)
SolutionSample(x=array([0., 0., 0.]), fval=np.float64(0.0), probability=0.232421875, status=<OptimizationResultStatus.SUCCESS: 0>)
SolutionSample(x=array([1., 1., 0.]), fval=np.float64(0.0), probability=0.12890625, status=<OptimizationResultStatus.SUCCESS: 0>)
SolutionSample(x=array([1., 0., 0.]), fval=np.float64(1.0), probability=0.119140625, status=<OptimizationResultStatus.SUCCESS: 0>)
SolutionSample(x=array([0., 0., 1.]), fval=np.float64(3.0), probability=0.021484375, status=<OptimizationResultStatus.SUCCESS: 0>)
SolutionSample(x=array([1., 0., 1.]), fval=np.float64(3.0), probability=0.025390625, status=<OptimizationResultStatus.SUCCESS: 0>)
SolutionSample(x=array([0., 1., 1.]), fval=np.float64(3.0), probability=0.0224609375, status=<OptimizationResultStatus.SUCCESS: 0>)

If we want to obtain a better perspective of the results, statistics is very helpful, both with respect to the objective function values and their respective probabilities. Thus, mean and standard deviation are the very basics for understanding the results.

In [12]:
fvals = [s.fval for s in qaoa_result.samples]
probabilities = [s.probability for s in qaoa_result.samples]
In [13]:
np.mean(fvals)
np.float64(1.1428571428571428)
In [14]:
np.std(fvals)
np.float64(1.8070158058105026)

Finally, despite all the number-crunching, visualization is usually the best early-analysis approach.

In [15]:
samples_for_plot = {
    " ".join(f"{qaoa_result.variables[i].name}={int(v)}" for i, v in enumerate(s.x)): s.probability
    for s in filtered_samples
}
samples_for_plot
{'x=0 y=1 z=0': 0.4501953125,
 'x=0 y=0 z=0': 0.232421875,
 'x=1 y=1 z=0': 0.12890625,
 'x=1 y=0 z=0': 0.119140625,
 'x=0 y=0 z=1': 0.021484375,
 'x=1 y=0 z=1': 0.025390625,
 'x=0 y=1 z=1': 0.0224609375}
In [16]:
plot_histogram(samples_for_plot)

Execution with V2 Sampler

If you want to run it with V2 Sampler except StatevectorSampler, you are required to set pass_manager argument of QAOA initialization.

In [17]:
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_aer import AerSimulator
from qiskit_aer.primitives import SamplerV2
In [18]:
pass_manager = generate_preset_pass_manager(optimization_level=2, backend=AerSimulator())
qaoa_noisy = QAOA(
    sampler=SamplerV2(seed=123, default_shots=1000),
    optimizer=COBYLA(),
    initial_point=[1, 1],
    pass_manager=pass_manager,
)
In [19]:
meo_noisy = MinimumEigenOptimizer(qaoa_noisy)
qaoa_result = meo_noisy.solve(qubo)
print(qaoa_result.prettyprint())
filtered_samples = get_filtered_samples(
    qaoa_result.samples, threshold=0.005, allowed_status=(OptimizationResultStatus.SUCCESS,)
)
samples_for_plot = {
    " ".join(f"{qaoa_result.variables[i].name}={int(v)}" for i, v in enumerate(s.x)): s.probability
    for s in filtered_samples
}
plot_histogram(samples_for_plot)
objective function value: -2.0
variable values: x=0.0, y=1.0, z=0.0
status: SUCCESS

RecursiveMinimumEigenOptimizer

The RecursiveMinimumEigenOptimizer takes a MinimumEigenOptimizer as input and applies the recursive optimization scheme to reduce the size of the problem one variable at a time. Once the size of the generated intermediate problem is below a given threshold (min_num_vars), the RecursiveMinimumEigenOptimizer uses another solver (min_num_vars_optimizer), e.g., an exact classical solver such as CPLEX or the MinimumEigenOptimizer based on the NumPyMinimumEigensolver.

In the following, we show how to use the RecursiveMinimumEigenOptimizer using the two MinimumEigenOptimizers introduced before.

First, we construct the RecursiveMinimumEigenOptimizer such that it reduces the problem size from 3 variables to 1 variable and then uses the exact solver for the last variable. Then we call solve to optimize the considered problem.

In [20]:
rqaoa = RecursiveMinimumEigenOptimizer(qaoa, min_num_vars=1, min_num_vars_optimizer=exact)
In [21]:
rqaoa_result = rqaoa.solve(qubo)
print(rqaoa_result.prettyprint())
objective function value: -2.0
variable values: x=0.0, y=1.0, z=0.0
status: SUCCESS
In [22]:
filtered_samples = get_filtered_samples(
    rqaoa_result.samples, threshold=0.005, allowed_status=(OptimizationResultStatus.SUCCESS,)
)
In [23]:
samples_for_plot = {
    " ".join(f"{rqaoa_result.variables[i].name}={int(v)}" for i, v in enumerate(s.x)): s.probability
    for s in filtered_samples
}
samples_for_plot
{'x=0 y=1 z=0': 1.0}
In [24]:
plot_histogram(samples_for_plot)
In [25]:
import tutorial_magics

%qiskit_version_table
%qiskit_copyright

Version Information

SoftwareVersion
qiskit2.1.1
qiskit_ibm_runtime0.41.0
qiskit_aer0.17.1
qiskit_optimization0.7.0
System information
Python version3.11.13
OSDarwin
Wed Aug 13 22:59:45 2025 JST

This code is a part of a Qiskit project

© Copyright IBM 2017, 2025.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.

Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.

In [ ]:

Join the Discussion

Comments (0)

No comments yet. Be the first to share your thoughts!

Indexed by QCR Librarian

This entry was created automatically from publicly available records. QCR links to public sources and only stores repository content where the license permits redistribution.

Versions

v1 Latest
Jun 14, 2026
qcr:2606.27930.1

Cite all versions? Use the base QCR ID to always reference the latest version of this entry.

Tools used

Qiskit

Keywords

minimum-eigen-optimizer
qubo
ising
optimization
qiskit

You may also like5