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:¶
- 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_MPSor 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:¶
- 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 - optimized_n_calls: Number of optimization rounds; default is 20 and minimum is 12.
- optimized_initial_omega_parameters: [5.0, 10.0, 5.0,] List with initial values for Amplitude (5.0, 10, 5.0) when using Optimized Pulse
- optimized_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 [ ]:
Copied!
import torch
from qubosolver.qubo_instance import QUBOInstance
from qubosolver.config import SolverConfig, PulseShapingConfig
from qubosolver.qubo_types import PulseType
from qubosolver.solver import QuboSolver
import matplotlib.pyplot as plt
plt.rcParams["animation.html"] = "jshtml"
%matplotlib inline
import torch
from qubosolver.qubo_instance import QUBOInstance
from qubosolver.config import SolverConfig, PulseShapingConfig
from qubosolver.qubo_types import PulseType
from qubosolver.solver import QuboSolver
import matplotlib.pyplot as plt
plt.rcParams["animation.html"] = "jshtml"
%matplotlib inline
Load the instance as a QUBOInstance object¶
Here, we have a 3x3 QUBO matrix with negative diagonal and positive off-diagonal terms.
In [ ]:
Copied!
coefficients = torch.tensor([[-1.0, 0.5, 0.2], [0.5, -2.0, 0.3], [0.2, 0.3, -3.0]])
instance = QUBOInstance(coefficients)
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¶
Default method
In [ ]:
Copied!
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)
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([117, 54, 21, 308], dtype=torch.int32), probabilities=tensor([0.2340, 0.1080, 0.0420, 0.6160]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
If needed, one can access the pulse used and draw the sequence of pulses used by the quantum solver as follows:
In [ ]:
Copied!
embedding = solver.embedding()
pulse = solver.pulse(embedding)[0]
solver.draw_sequence(pulse, embedding)
embedding = solver.embedding()
pulse = solver.pulse(embedding)[0]
solver.draw_sequence(pulse, embedding)
Default configuration¶
In [ ]:
Copied!
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)
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, 14, 15, 57], dtype=torch.int32), probabilities=tensor([0.1400, 0.1400, 0.1500, 0.5700]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Changing optimized_n_calls¶
In [ ]:
Copied!
default_config = SolverConfig.from_kwargs(
use_quantum=True, pulse_shaping=PulseShapingConfig(pulse_shaping_method=PulseType.OPTIMIZED, optimized_n_calls=13),
)
solver = QuboSolver(instance, default_config)
solution = solver.solve()
print(solution)
default_config = SolverConfig.from_kwargs(
use_quantum=True, pulse_shaping=PulseShapingConfig(pulse_shaping_method=PulseType.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([16, 11, 14, 59], dtype=torch.int32), probabilities=tensor([0.1600, 0.1100, 0.1400, 0.5900]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Changing optimized_initial_omega_parameters and optimized_initial_detuning_parameters¶
In [ ]:
Copied!
default_config = SolverConfig.from_kwargs(
use_quantum=True, pulse_shaping=PulseShapingConfig(pulse_shaping_method=PulseType.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)
default_config = SolverConfig.from_kwargs(
use_quantum=True, pulse_shaping=PulseShapingConfig(pulse_shaping_method=PulseType.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([17, 11, 10, 62], dtype=torch.int32), probabilities=tensor([0.1700, 0.1100, 0.1000, 0.6200]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Changing re_execute_opt_pulse to True¶
In [ ]:
Copied!
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)
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, 70, 70, 289], dtype=torch.int32), probabilities=tensor([0.1420, 0.1400, 0.1400, 0.5780]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Adding custom functions¶
One can change the pulse 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.
In [ ]:
Copied!
from qubosolver.utils.qubo_eval import calculate_qubo_cost
# example of penalization
def penalized_qubo(bitstring: str, QUBO: torch.Tensor) -> float:
return calculate_qubo_cost(bitstring, QUBO) + 2 * bitstring.count("0")
# example of saving intermediate results
opt_results = list()
def callback(d: dict) -> None:
opt_results.append(d)
# example of using an average cost
def 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,
optimized_custom_qubo_cost=penalized_qubo,
optimized_callback_objective=callback,
optimized_custom_objective = average_ojective,
)
config = SolverConfig(
use_quantum=True,
pulse_shaping=pulse_shaping,
)
from qubosolver.utils.qubo_eval import calculate_qubo_cost
# example of penalization
def penalized_qubo(bitstring: str, QUBO: torch.Tensor) -> float:
return calculate_qubo_cost(bitstring, QUBO) + 2 * bitstring.count("0")
# example of saving intermediate results
opt_results = list()
def callback(d: dict) -> None:
opt_results.append(d)
# example of using an average cost
def 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,
optimized_custom_qubo_cost=penalized_qubo,
optimized_callback_objective=callback,
optimized_custom_objective = average_ojective,
)
config = SolverConfig(
use_quantum=True,
pulse_shaping=pulse_shaping,
)
In [ ]:
Copied!
solver = QuboSolver(instance, config)
solution = solver.solve()
len(opt_results), opt_results[-1]
solver = QuboSolver(instance, config)
solution = solver.solve()
len(opt_results), opt_results[-1]
Out[ ]:
(20,
{'x': [12.933030340005581,
1.0,
15.460306081212382,
0.0,
49.026100047935614,
6.699675820531098],
'cost_eval': 4.46})
In [ ]:
Copied!
solution
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([160, 146, 162, 32], dtype=torch.int32), probabilities=tensor([0.3200, 0.2920, 0.3240, 0.0640]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)