Demo - Pulse Shaping Methods¶
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_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:¶
- 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 [ ]:
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 torch
from qubosolver.qubo_instance import QUBOInstance
from qubosolver.config import SolverConfig, PulseShapingConfig
from qubosolver.qubo_types import PulseType
from qubosolver.solver import QuboSolver
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([ 83, 111, 120, 186], dtype=torch.int32), probabilities=tensor([0.1660, 0.2220, 0.2400, 0.3720]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
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, 18, 12, 56], dtype=torch.int32), probabilities=tensor([0.1400, 0.1800, 0.1200, 0.5600]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)
Changing n_calls¶
In [ ]:
Copied!
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)
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¶
In [ ]:
Copied!
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)
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¶
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, 64, 89, 276], dtype=torch.int32), probabilities=tensor([0.1420, 0.1280, 0.1780, 0.5520]), 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
custom_qubo_cost
- Performing optimization with a different objective than the best cost via
custom_objective
- Adding callback functions via
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,
custom_qubo_cost=penalized_qubo,
callback_objective=callback,
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,
custom_qubo_cost=penalized_qubo,
callback_objective=callback,
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': [1.784928590509922, 8.110880628177934, 7.507619225411378, -121.60698017231257, -1.6642490558873106, -125.66370514359173], 'cost_eval': 4.83})
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([162, 178, 156, 4], dtype=torch.int32), probabilities=tensor([0.3240, 0.3560, 0.3120, 0.0080]), solution_status=<SolutionStatusType.UNPROCESSED: 'unprocessed'>)