Solver configuration tutorial#

Solver types#

The basic solver definition requires the specification of its type.
Currently supported solver types are:

Problem definition#

This tutorial assumes the following sample optimization problem definition:

from QHyper.problems.knapsack import KnapsackProblem
problem = KnapsackProblem(max_weight=2,
                          item_weights=[1, 1, 1],
                          item_values=[2, 2, 1])

This specifies the Knapsack Problem: fill a knapsack with three items, each characterized with a weight and cost, to maximize the total value without exceeding max_weight.

Configuring quantum annealing solvers: D-Wave#

The initial penalty weights for constrained problems#

Some solvers, such as the D-Wave Advantage hybrid solver, require the problem definition in the Quadratic Unconstrained Binary Optimization (QUBO) form. QHyper automatically creates the QUBO, e.g., for the Knapsack Problem:

\[f(\boldsymbol{x}, \boldsymbol{y}) = - \alpha_0 \underbrace{\sum_{i = 1}^N c_i x_i}_{\text{cost function}} + \alpha_1 \underbrace{(1 - \sum_{i=1}^W y_i)^2}_{\text{constraint encoding}} + \alpha_2 \underbrace{(\sum_{i=1}^W iy_i - \sum_{i=1}^N w_ix_i)^2}_{\text{weight constraint}},\]
where
  • \(\alpha_j\) are the penalty weights (i.e. Lagrangian multipliers, hyperparameters of the optimized function);

  • \(N=3\) is the number of available items;

  • \(W=\) max_weight is the maximum weight of the knapsack;

  • \(c_i\) and \(w_i\) are the values and weights specified in item_values and item_weights lists of the configuration;

  • The goal is to optimize \(\boldsymbol{x} = [x_i]_N\) which is a Boolean vector, where \(x_i = 1\) if and only if the item \(i\) was selected to be inserted into the knapsack;

  • \(\boldsymbol{y} = [y_i]_W\) is a one-hot vector where \(y_i = 1\) if and only if the weight of the knapsack is equal to \(i\).

To define the function properly, you need to set three penalty terms \(\alpha_j\), which act as hyperparameters. These penalties are used to combine the cost function and constraints. The first constraint ensure that the problem encoding is correct, and the second that the total weight in the knapsack does not exceed the max_weight limit.

D-Wave Advantage solver#

In the example below, the solver used is the D-Wave Advantage quantum annealing system and the constraint penalties (\(\alpha_j\)) are set using the penalty_weights keyword argument. The num_reads argument is the amount of samples.

from QHyper.solvers.quantum_annealing.dwave import Advantage

solver = Advantage(problem,
                   penalty_weights=[1, 2.5, 2.5],
                   num_reads=10)

Adding a hyperoptimizer#

Since guessing the correct penalty weights is often a difficult task, there is also an option to define a HyperOptimizer to search for the appropriate settings.
In the example below, GridSearch optimizer is applied to find the proper penalty weights for the knapsack QUBO formulation. The penalty weights are searched within specified bounds (min, max) and incremented by a specified step size.
from QHyper.solvers.hyper_optimizer import HyperOptimizer
from QHyper.optimizers.grid_search import GridSearch
from QHyper.solvers.quantum_annealing.dwave import Advantage

hyper_optimizer = HyperOptimizer(
    optimizer=GridSearch(),
    solver=Advantage(problem),
    penalty_weights={"min": [1, 1, 1], "max": [2.1, 2.1, 2.1], "step": [1, 1, 1]}
)

Configuring gate-based solvers: QAOA#

A typical example of the QAOA configuration is presented below.
The quantum circuit consists of 5 layers. The variational parameters gamma and beta are specified using OptimizationParameters.
A local QmlGradientDescent optimizer (by default Adam gradient descent) with the default settings is used.
Problem’s penalty weights are defined in penalty_weights.
from QHyper.solvers.gate_based.pennylane import QAOA
from QHyper.optimizers import OptimizationParameter
from QHyper.optimizers.qml_gradient_descent import QmlGradientDescent

solver = QAOA(problem,
    layers=5,
    gamma=OptimizationParameter(init=[0.25, 0.25, 0.25, 0.25, 0.25]),
    beta=OptimizationParameter(init=[-0.5, -0.5, -0.5, -0.5, -0.5]),
    optimizer=QmlGradientDescent(),
    penalty_weights=[1, 2.5, 2.5],
)

It is possible to further customize the QAOA with additional keyword arguments (see the QHyper API documentation). Below is presented an example of setting the Pennylane simulator type using the backend keyword.

from QHyper.solvers.gate_based.pennylane import QAOA
from QHyper.optimizers import OptimizationParameter
from QHyper.optimizers.qml_gradient_descent import QmlGradientDescent

solver = QAOA(problem,
    layers=5,
    gamma=OptimizationParameter(init=[0.25, 0.25, 0.25, 0.25, 0.25]),
    beta=OptimizationParameter(init=[-0.5, -0.5, -0.5, -0.5, -0.5]),
    optimizer=QmlGradientDescent(),
    backend="default.qubit",
    penalty_weights=[1, 2.5, 2.5],
)

Customizing optimizers#

Customizing the optimizer settings is also possible. Below, a more detailed sample configuration is shown. Please note that adding all native function options is possible (e.g., stepsize in this example is native from Adam gradient descent).

from QHyper.solvers.gate_based.pennylane import QAOA
from QHyper.optimizers import OptimizationParameter
from QHyper.optimizers.qml_gradient_descent import QmlGradientDescent

solver = QAOA(problem,
    layers=5,
    gamma=OptimizationParameter(init=[0.25, 0.25, 0.25, 0.25, 0.25]),
    beta=OptimizationParameter(init=[-0.5, -0.5, -0.5, -0.5, -0.5]),
    optimizer=QmlGradientDescent(name='adam',
                                steps=200,
                                stepsize=0.005),
    penalty_weights=[1, 2.5, 2.5]
)

Configuring a classical solver: Gurobi#

from QHyper.solvers.classical.gurobi import Gurobi

solver = Gurobi(problem)

Combining optimizers and hyperoptimizers#

It is also possible to make use of both the optimizer and the HyperOptimizer functionalities. The example below is similar to that in Customizing optimizers. However, as in Adding a hyperoptimizer, penalty weights are searched by the HyperOptimizer within specified bounds. In this example it is done using the Cross Entropy Search method (defined as cem). processes, samples_per_epoch, and epochs are parameters specific for CEM.

Note

The CEM method is computationally expensive and may require a significant amount of time to complete (~5 min).

from QHyper.solvers.gate_based.pennylane import WF_QAOA
from QHyper.optimizers import OptimizationParameter
from QHyper.optimizers.scipy_minimizer import ScipyOptimizer
from QHyper.solvers.hyper_optimizer import HyperOptimizer
from QHyper.optimizers.cem import CEM

solver = WF_QAOA(problem,
    layers=5,
    gamma=OptimizationParameter(min=[0.0, 0.0, 0.0, 0.0, 0.0],
                                init=[0.5, 0.5, 0.5, 0.5, 0.5],
                                max=[6.28, 6.28, 6.28, 6.28, 6.28]),
    beta=OptimizationParameter(min=[0.0, 0.0, 0.0, 0.0, 0.0],
                            init=[1.0, 1.0, 1.0, 1.0, 1.0],
                            max=[6.28, 6.28, 6.28, 6.28, 6.28]),
    optimizer=ScipyOptimizer(),
    backend="default.qubit",
    penalty_weights=[1, 2.5, 2.5],
)

hyper_optimizer = HyperOptimizer(
    optimizer=CEM(processes=4,
                samples_per_epoch=100,
                epochs=5),
    solver=solver,
    penalty_weights={
        "min": [1, 1, 1],
        "max": [5, 5, 5],
        "init": [1, 2.5, 2.5]
    }
)

Supported optimizers#

A variety of (hyper)optimizers is supported. In QHyper the optimizer (both in a solver and in a hyperoptimizer) can be set up using keyword arguments given below.

Note

Please note that additional keyword arguments for each optimizer configuration can be taken directly from the native function definition (refer to the indicated API documentation).

Additionally, the QmlGradientDescent set of optimizers can be further specified (e.g. adam configuration was shown in point 6 above) using following keyword arguments (for details see Pennylane documentation ):

  • adam: qml.AdamOptimizer;

  • adagrad: qml.AdagradOptimizer;

  • rmsprop: qml.RMSPropOptimizer;

  • momentum: qml.MomentumOptimizer;

  • nesterov_momentum: qml.NesterovMomentumOptimizer;

  • sgd: qml.GradientDescentOptimizer;

  • qng: qml.QNGOptimizer.

Running solvers and hyperoptimizers#

Running a pure solver:

solver.solve()

Running a hyperoptimizer:

hyper_optimizer.solve()
hyper_optimizer.run_with_best_params()

You can explore how to evaluate the results by visiting the Typical use cases demo.