Drive Shaping Methods
A quantum program in the Rydberg analog model is defined as a time-dependent drive Hamiltonian that is imposed on the qubits (in addition to the interaction Hamiltonian). The drive shaping configuration part (the drive_shaping field of SolverConfig) defines how the drive parameters are constructed.
In this notebook, we show how to use different drive shaping methods:
ADIABATIC(Has no parameters to be customized).OPTIMIZED(Has seven parameters that can be customized).
We choose the method when defining the SolverConfig configuration, with drive_shaping_method = DriveType.(method)
Default SolverConfig parameters to run a quantum approach:
- use_quantum: bool = False (for using the drive shaping methods, we have to set it to
True.) - backend: LocalEmulator | RemoteEmulator | QPU (by default, use a LocalEmulator based on qutip)
- device: Device = DigitalAnalogDevice() (also available:
AnalogDevice()) - embedding_method: str | EmbedderType | None = EmbedderType.GREEDY (also available:
BLADE) - drive_shaping_method: str | DriveType | None = DriveType.ADIABATIC (also available:
OPTIMIZED)
import torch
from qubosolver.qubo_instance import QUBOInstancefrom qubosolver.config import SolverConfig, DriveShapingConfigfrom qubosolver.qubo_types import DriveTypefrom qubosolver.solver import QuboSolver
import matplotlib.pyplot as pltplt.rcParams["animation.html"] = "jshtml"
%matplotlib inline** Create the QUBOInstance **
Here, we have a 3x3 QUBO matrix with negative diagonal and positive off-diagonal terms.
coefficients = torch.tensor([[-1.0, 0.5, 0.2], [0.5, -2.0, 0.3], [0.2, 0.3, -3.0]])instance = QUBOInstance(coefficients)Standard Adiabatic
Section titled “Standard Adiabatic”Default method
default_config = SolverConfig.from_kwargs( use_quantum=True, drive_shaping=DriveShapingConfig(drive_shaping_method=DriveType.ADIABATIC),)solver = QuboSolver(instance, default_config)
solution = solver.solve()print(solution)QUBOSolution(bitstrings=tensor([[0., 0., 1.],
[0., 1., 0.],
[1., 0., 0.],
[0., 0., 0.]]), costs=tensor([-3., -2., -1., 0.]), counts=tensor([ 4, 7, 8, 81]), probabilities=tensor([0.0400, 0.0700, 0.0800, 0.8100]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
If needed, one can access the drive used and draw the sequence used by the quantum solver as follows:
embedding = solver.embedding()drive = solver.drive(embedding)[0]solver.draw_sequence(drive, embedding)Optimized Drive shaping
Section titled “Optimized Drive shaping”Parameters to customize:
Section titled “Parameters to customize:”For the OPTIMIZED drive shaping, we have the following parameters:
- optimized_n_calls: Number of calls for the optimization process.
- optimized_re_execute_opt_drive: Whether to re-run the optimal drive sequence after optimization.
- optimized_initial_omega_parameters: Default initial omega parameters for the drive. Defaults to (5, 10, 5).
- optimized_initial_detuning_parameters: Default initial detuning parameters for the drive. Defaults to (-10, 0, 10).
Default configuration
Section titled “Default configuration”default_config = SolverConfig.from_kwargs( use_quantum=True, drive_shaping=DriveShapingConfig(drive_shaping_method=DriveType.OPTIMIZED),)solver = QuboSolver(instance, default_config)
solution = solver.solve()print(solution)QUBOSolution(bitstrings=tensor([[0., 0., 1.],
[0., 1., 0.],
[1., 0., 0.],
[0., 0., 0.]]), costs=tensor([-3., -2., -1., 0.]), counts=tensor([28, 27, 41, 4], dtype=torch.int32), probabilities=tensor([0.2800, 0.2700, 0.4100, 0.0400]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Changing optimized_n_calls
Section titled “Changing optimized_n_calls”default_config = SolverConfig.from_kwargs( use_quantum=True, drive_shaping=DriveShapingConfig(drive_shaping_method=DriveType.OPTIMIZED, optimized_n_calls=13),)solver = QuboSolver(instance, default_config)
solution = solver.solve()print(solution)QUBOSolution(bitstrings=tensor([[0., 0., 1.],
[0., 1., 0.],
[1., 0., 0.],
[0., 0., 0.]]), costs=tensor([-3., -2., -1., 0.]), counts=tensor([20, 29, 35, 16], dtype=torch.int32), probabilities=tensor([0.2000, 0.2900, 0.3500, 0.1600]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Changing the initial parameters
Section titled “Changing the initial parameters”Initial parameters of the optimization procedure can be changed as optimized_initial_omega_parameters and optimized_initial_detuning_parameters
default_config = SolverConfig.from_kwargs( use_quantum=True, drive_shaping=DriveShapingConfig(drive_shaping_method=DriveType.OPTIMIZED, optimized_initial_omega_parameters=[2.0, 15.0, 5.0,], optimized_initial_detuning_parameters=[-45.0, 0.0, 25.0]),)solver = QuboSolver(instance, default_config)
solution = solver.solve()print(solution)QUBOSolution(bitstrings=tensor([[0., 0., 1.],
[0., 1., 0.],
[1., 0., 0.],
[0., 0., 0.]]), costs=tensor([-3., -2., -1., 0.]), counts=tensor([ 7, 17, 11, 65], dtype=torch.int32), probabilities=tensor([0.0700, 0.1700, 0.1100, 0.6500]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Changing optimized_re_execute_opt_drive
Section titled “Changing optimized_re_execute_opt_drive”default_config = SolverConfig.from_kwargs( use_quantum=True, drive_shaping=DriveShapingConfig(drive_shaping_method=DriveType.OPTIMIZED, optimized_re_execute_opt_drive=True),)solver = QuboSolver(instance, default_config)
solution = solver.solve()print(solution)QUBOSolution(bitstrings=tensor([[0., 0., 1.],
[0., 1., 0.],
[1., 0., 0.],
[0., 0., 0.]]), costs=tensor([-3., -2., -1., 0.]), counts=tensor([33, 30, 28, 9]), probabilities=tensor([0.3300, 0.3000, 0.2800, 0.0900]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Adding custom functions
Section titled “Adding custom functions”One can change the drive shaping method by incorporating custom functions for:
- Evaluating a candidate bitstring and QUBO via
optimized_custom_qubo_cost - Performing optimization with a different objective than the best cost via
optimized_custom_objective - Adding callback functions via
optimized_callback_objective.
from qubosolver.utils.qubo_eval import calculate_qubo_cost
# example of penalizationdef penalized_qubo(bitstring: str, QUBO: torch.Tensor) -> float: return calculate_qubo_cost(bitstring, QUBO) + 2 * bitstring.count("0")
# example of saving intermediate resultsopt_results = list()def callback(d: dict) -> None: opt_results.append(d)
# example of using an average costdef average_ojective( bitstrings: list, counts: list, probabilities: list, costs: list, best_cost: float, best_bitstring: str,) -> float: return sum([p * c for p, c in zip(probabilities, costs)])
drive_shaping=DriveShapingConfig(drive_shaping_method=DriveType.OPTIMIZED, optimized_re_execute_opt_drive=True, optimized_custom_qubo_cost=penalized_qubo, optimized_callback_objective=callback, optimized_custom_objective = average_ojective,)
config = SolverConfig( use_quantum=True, drive_shaping=drive_shaping,)solver = QuboSolver(instance, config)solution = solver.solve()len(opt_results), opt_results[-1](20,
{'x': [5.008864997401849,
9.962027508268053,
7.097581038790153,
-34.612625398428435,
2.2551049415033475,
63.66932478277556],
'cost_eval': 3.3})
solutionQUBOSolution(bitstrings=tensor([[0., 0., 1.],
[0., 1., 0.],
[1., 0., 0.],
[0., 0., 0.]]), costs=tensor([-3., -2., -1., 0.]), counts=tensor([35, 24, 34, 7]), probabilities=tensor([0.3500, 0.2400, 0.3400, 0.0700]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)