Pulse Shaping Methods
Quantum devices can be programmed by specifying a sequence of pulses. The pulse shaping configuration part (the pulse_shaping
field of SolverConfig
) defines how the pulse parameters are constructed.
In this notebook, we show how to use different pulse shaping methods.
Here, the two available pulse shaping methods are shown:
ADIABATIC
(Has no parameters to be customized).OPTIMIZED
(Has three parameters that can be customized).
We choose the method when defining the configurations, with pulse_shaping_method = PulseType.(method)
Default config parameters:
Section titled “Default config parameters:”- use_quantum: bool | None = False (for using the pulse shaping methods, we have to set it to
True
.) - backend: str | BackendType = BackendType.QUTIP (possibly can be replace by
BackendType.EMU_MPS
or any value inBackendType
) - device: str | DeviceType | None = DeviceType.DIGITAL_ANALOG_DEVICE (also available:
ANALOG_DEVICE
) - embedding_method: str | EmbedderType | None = EmbedderType.GREEDY (also available:
BLADE
) - pulse_shaping_method: str | PulseType | None = PulseType.ADIABATIC (also available:
OPTIMIZED
)
OPTIMIZED pulse shaping parameters:
Section titled “OPTIMIZED pulse shaping parameters:”- re_execute_opt_pulse: bool = False (
True
) Whether we take the last pulse and make another optimization round following the pipeline (execute) or just take the results of the last one - n_calls: Number of optimization rounds; default is 20 and minimum is 12.
- initial_omega_parameters: [5.0, 10.0, 5.0,] List with initial values for Amplitude (5.0, 10, 5.0) when using Optimized Pulse
- initial_detuning_parameters: [-10.0, 0.0, 10.0] List with initial values for Detuning (-10.0, 0.0, 10.0) when using Optimized Pulse
In [ ]:
import torch
from qubosolver.qubo_instance import QUBOInstancefrom qubosolver.config import SolverConfig, PulseShapingConfigfrom qubosolver.qubo_types import PulseTypefrom qubosolver.solver import QuboSolver
Load the instance as a QUBOInstance
object
Section titled “Load the instance as a QUBOInstance object”Here, we have a 3x3 QUBO matrix with negative diagonal and positive off-diagonal terms.
In [ ]:
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
In [ ]:
default_config = SolverConfig.from_kwargs( use_quantum=True, pulse_shaping=PulseShapingConfig(pulse_shaping_method=PulseType.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([ 83, 111, 120, 186], dtype=torch.int32), probabilities=tensor([0.1660, 0.2220, 0.2400, 0.3720]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Optimized Pulse shaping
Section titled “Optimized Pulse shaping”Parameters to customize:
Section titled “Parameters to customize:”For the OPTIMIZED pulse shaping, we have the following parameters:
- n_calls
- re_execute_opt_pulse
- initial_omega_parameters
- initial_detuning_parameters
Default configuration
Section titled “Default configuration”In [ ]:
default_config = SolverConfig.from_kwargs( use_quantum=True, pulse_shaping=PulseShapingConfig(pulse_shaping_method=PulseType.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([14, 18, 12, 56], dtype=torch.int32), probabilities=tensor([0.1400, 0.1800, 0.1200, 0.5600]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Changing n_calls
Section titled “Changing n_calls”In [ ]:
default_config = SolverConfig.from_kwargs( use_quantum=True, pulse_shaping=PulseShapingConfig(pulse_shaping_method=PulseType.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([14, 16, 17, 53], dtype=torch.int32), probabilities=tensor([0.1400, 0.1600, 0.1700, 0.5300]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Changing initial_omega_parameters and initial_detuning_parameters
Section titled “Changing initial_omega_parameters and initial_detuning_parameters”In [ ]:
default_config = SolverConfig.from_kwargs( use_quantum=True, pulse_shaping=PulseShapingConfig(pulse_shaping_method=PulseType.OPTIMIZED, initial_omega_parameters=[2.0, 15.0, 5.0,], 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([12, 16, 15, 57], dtype=torch.int32), probabilities=tensor([0.1200, 0.1600, 0.1500, 0.5700]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Changing re_execute_opt_pulse to True
Section titled “Changing re_execute_opt_pulse to True”In [ ]:
default_config = SolverConfig.from_kwargs( use_quantum=True, pulse_shaping=PulseShapingConfig(pulse_shaping_method=PulseType.OPTIMIZED, re_execute_opt_pulse=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([ 71, 64, 89, 276], dtype=torch.int32), probabilities=tensor([0.1420, 0.1280, 0.1780, 0.5520]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Adding custom functions
Section titled “Adding custom functions”One can change the pulse shaping method by incorporating custom functions for:
- Evaluating a candidate bitstring and QUBO via
custom_qubo_cost
- Performing optimization with a different objective than the best cost via
custom_objective
- Adding callback functions via
callback_objective
.
In [ ]:
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)])
pulse_shaping=PulseShapingConfig(pulse_shaping_method=PulseType.OPTIMIZED, re_execute_opt_pulse=True, custom_qubo_cost=penalized_qubo, callback_objective=callback, custom_objective = average_ojective,)
config = SolverConfig( use_quantum=True, pulse_shaping=pulse_shaping,)
In [ ]:
solver = QuboSolver(instance, config)solution = solver.solve()len(opt_results), opt_results[-1]
Out[ ]:
(20, {'x': [1.784928590509922, 8.110880628177934, 7.507619225411378, -121.60698017231257, -1.6642490558873106, -125.66370514359173], 'cost_eval': 4.83})
In [ ]:
solution
Out[ ]:
QUBOSolution(bitstrings=tensor([[0., 0., 1.], [0., 1., 0.], [1., 0., 0.], [0., 0., 0.]]), costs=tensor([-3., -2., -1., 0.]), counts=tensor([162, 178, 156, 4], dtype=torch.int32), probabilities=tensor([0.3240, 0.3560, 0.3120, 0.0080]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)