Solver configuration tutorial#
Solver types#
quantum annealing
Advantagefor the D-Wave Advantage Solver (currently the default advantage_system5.4. is supported);CQMfor the D-Wave Constrained Quadratic Model Hybrid Solver;Note: for all the above solvers the D-Wave token is required.
gate-based
classical
Gurobifor the classical Gurobi Optimizer;Note: for larger problem instances Gurobi license is required.
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])
problem:
type: KnapsackProblem
max_weight: 2
item_weights: [1, 1, 1]
item_values: [2, 2, 1]
{
"problem": {
"type": "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:
- 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_weightis the maximum weight of the knapsack;\(c_i\) and \(w_i\) are the values and weights specified in
item_valuesanditem_weightslists 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)
solver:
category: quantum_annealing
platform: dwave
name: Advantage
penalty_weights: [1, 2.5, 2.5]
num_reads: 10
{
"solver": {
"category": "quantum_annealing",
"platform": "dwave",
"name": "Advantage",
"penalty_weights": [1, 2.5, 2.5],
"num_reads": 10
}
}
Adding a hyperoptimizer#
HyperOptimizer to search for the appropriate settings.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]}
)
solver:
category: quantum_annealing
platform: dwave
name: Advantage
hyper_optimizer:
optimizer:
type: GridSearch
penalty_weights:
min: [1, 1, 1]
max: [2.1, 2.1, 2.1]
step: [1, 1, 1]
{
"solver": {
"category": "quantum_annealing",
"platform": "dwave",
"name": "Advantage"
},
"hyper_optimizer": {
"optimizer": {
"type": "GridSearch"
},
"penalty_weights": {
"min": [1, 1, 1],
"max": [2.1, 2.1, 2.1],
"step": [1, 1, 1]
}
}
}
Configuring gate-based solvers: QAOA#
layers. The variational parameters gamma and beta are specified using OptimizationParameters.QmlGradientDescent optimizer (by default Adam gradient descent) with the default settings is used.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],
)
solver:
category: gate_based
platform: pennylane
name: QAOA
layers: 5
gamma:
init: [0.25, 0.25, 0.25, 0.25, 0.25]
beta:
init: [-0.5, -0.5, -0.5, -0.5, -0.5]
optimizer:
type: QmlGradientDescent
penalty_weights: [1, 2.5, 2.5]
{
"solver": {
"category": "gate_based",
"platform": "pennylane",
"name": "QAOA",
"layers": 5,
"gamma": {
"init": [0.25, 0.25, 0.25, 0.25, 0.25]
},
"beta": {
"init": [-0.5, -0.5, -0.5, -0.5, -0.5]
},
"optimizer": {
"type": "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],
)
solver:
category: gate_based
platform: pennylane
name: QAOA
layers: 5
gamma:
init: [0.25, 0.25, 0.25, 0.25, 0.25]
beta:
init: [-0.5, -0.5, -0.5, -0.5, -0.5]
optimizer:
type: QmlGradientDescent
backend: default.qubit
penalty_weights: [1, 2.5, 2.5]
{
"solver": {
"category": "gate_based",
"platform": "pennylane",
"name": "QAOA",
"layers": 5,
"gamma": {
"init": [0.25, 0.25, 0.25, 0.25, 0.25]
},
"beta": {
"init": [-0.5, -0.5, -0.5, -0.5, -0.5]
},
"optimizer": {
"type": "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]
)
solver:
category: gate_based
platform: pennylane
name: QAOA
layers: 5
gamma:
init: [0.25, 0.25, 0.25, 0.25, 0.25]
beta:
init: [-0.5, -0.5, -0.5, -0.5, -0.5]
optimizer:
type: QmlGradientDescent
name: adam
steps: 200
stepsize: 0.005
backend: default.qubit
penalty_weights: [1, 2.5, 2.5]
{
"solver": {
"category": "gate_based",
"platform": "pennylane",
"name": "QAOA",
"layers": 5,
"gamma": {
"init": [0.25, 0.25, 0.25, 0.25, 0.25]
},
"beta": {
"init": [-0.5, -0.5, -0.5, -0.5, -0.5]
},
"optimizer": {
"type": "QmlGradientDescent",
"name": "adam",
"steps": 200,
"stepsize": 0.005
},
"backend": "default.qubit",
"penalty_weights": [1, 2.5, 2.5]
}
}
Configuring a classical solver: Gurobi#
from QHyper.solvers.classical.gurobi import Gurobi
solver = Gurobi(problem)
solver:
category: classical
platform: gurobi
name: Gurobi
{
"solver": {
"category": "classical",
"platform": "gurobi",
"name": "Gurobi"
}
}
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]
}
)
solver:
category: gate_based
platform: pennylane
name: WF_QAOA
layers: 5
gamma:
min: [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:
min: [0, 0, 0, 0, 0]
init: [1., 1., 1., 1., 1.]
max: [6.28, 6.28, 6.28, 6.28, 6.28]
optimizer:
type: scipy
backend: default.qubit
hyper_optimizer:
optimizer:
type: cem
processes: 4
samples_per_epoch: 100
epochs: 5
penalty_weights:
min: [1, 1, 1]
max: [5, 5, 5]
init: [1, 2.5, 2.5]
{
"solver": {
"category": "gate_based",
"platform": "pennylane",
"name": "WF_QAOA",
"layers": 5,
"gamma": {
"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": {
"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": {
"type": "scipy"
},
"backend": "default.qubit"
},
"hyper_optimizer": {
"optimizer": {
"type": "cem",
"processes": 4,
"samples_per_epoch": 100,
"epochs": 5
},
"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).
QmlGradientDescent: customizable gradient descent set of optimizers from Pennylane (see below)Random: Random optimizer (see QHyper API doc)GridSearch: Grid search optimizer (see QHyper API doc)CEM: Cross Entropy Optimizer (see QHyper API doc)Dummy: Dummy optimizer (see QHyper API doc)
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()
'''
Note: the solver and problem configs should be
in a single <file_name>.yaml file.
---
solver:
...
problem:
...
'''
import yaml
from QHyper.solvers import solver_from_config
with open("<file_name>.yaml", "r") as file:
solver_config = yaml.safe_load(file)
solver = solver_from_config(solver_config)
solver.solve()
'''
Note: there are two ways to use the JSON config:
1. The solver and problem configs can be
read from a single <file_name>.json file.
{
"solver":
{ ... },
"problem":
{ ... }
}
2. Directly in Python code, JSON syntax
can be assigned to a variable for further use.
solver_config = {
"solver":
{ ... },
"problem":
{ ... }
}
'''
import json
from QHyper.solvers import solver_from_config
# Uncomment if reading data from a file
# with open("<file_name>.json", "r") as file:
# solver_config = json.load(file)
solver = solver_from_config(solver_config)
solver.solve()
Running a hyperoptimizer:
hyper_optimizer.solve()
hyper_optimizer.run_with_best_params()
'''
Note: the solver, problem, and hyper_optimizer configs
should be in the same <file_name>.yaml file.
---
solver:
...
problem:
...
hyper_optimizer:
...
'''
import yaml
from QHyper.solvers import solver_from_config
with open("<file_name>.yaml", "r") as file:
hyperoptimizer_config = yaml.safe_load(file)
hyper_optimizer = solver_from_config(hyperoptimizer_config)
hyper_optimizer.solve()
hyper_optimizer.run_with_best_params()
'''
Note: there are two ways to use the JSON config:
1. The solver, problem, and hyper optimizer configs
can be read from a single <file_name>.json file.
{
"solver":
{ ... },
"problem":
{ ... },
"hyper_optimizer":
{ ... }
}
2. Directly in Python code, JSON syntax
can be assigned to a variable for further use.
hyper_optimizer_config = {
"solver":
{ ... },
"problem":
{ ... },
"hyper_optimizer":
{ ... }
}
'''
import json
from QHyper.solvers import solver_from_config
# Uncomment if reading data from a file
# with open("<file_name>.json", "r") as file:
# hyper_optimizer_config = json.load(file)
hyper_optimizer = solver_from_config(hyper_optimizer_config)
hyper_optimizer.solve()
hyper_optimizer.run_with_best_params()
You can explore how to evaluate the results by visiting the Typical use cases demo.