Quantum-related components of a QUBO solver
Pulse Shapping
Section titled “Pulse Shapping”
AdiabaticPulseShaper(instance, config, backend)
Section titled “
AdiabaticPulseShaper(instance, config, backend)
”
Bases: BasePulseShaper
A Standard Adiabatic Pulse shaper.
Source code in qubosolver/pipeline/pulse.py
def __init__(self, instance: QUBOInstance, config: SolverConfig, backend: BaseBackend): """ Initialize the pulse shaping module with a QUBO instance.
Args: instance (QUBOInstance): The QUBO problem instance. config (SolverConfig): The solver configuration. backend (BaseBackend): Backend to use. """ self.instance: QUBOInstance = instance self.config: SolverConfig = config self.pulse: Pulse | None = None self.backend = backend self.device = backend.device()
generate(register, instance)
Section titled “
generate(register, instance)
”Generate an adiabatic pulse based on the QUBO instance and physical register.
PARAMETER | DESCRIPTION |
---|---|
register
|
The physical register layout for the quantum system.
TYPE:
|
instance
|
The QUBO instance.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
tuple[Pulse, QUBOSolution]
|
tuple[Pulse, QUBOSolution | None]: - Pulse: A generated pulse object wrapping a Pulser pulse. - QUBOSolution: An instance of the qubo solution - str | None: The bitstring (solution) -> Not computed - float | None: The cost (energy value) -> Not computed - float | None: The probabilities for each bitstring -> Not computed - float | None: The counts of each bitstring -> Not computed |
Source code in qubosolver/pipeline/pulse.py
def generate( self, register: Register, instance: QUBOInstance,) -> tuple[Pulse, QUBOSolution]: """ Generate an adiabatic pulse based on the QUBO instance and physical register.
Args: register (Register): The physical register layout for the quantum system. instance (QUBOInstance): The QUBO instance.
Returns: tuple[Pulse, QUBOSolution | None]: - Pulse: A generated pulse object wrapping a Pulser pulse. - QUBOSolution: An instance of the qubo solution - str | None: The bitstring (solution) -> Not computed - float | None: The cost (energy value) -> Not computed - float | None: The probabilities for each bitstring -> Not computed - float | None: The counts of each bitstring -> Not computed """
QUBO = instance.coefficients weights_list = torch.abs(torch.diag(QUBO)).tolist() max_node_weight = max(weights_list) norm_weights_list = [1 - (w / max_node_weight) for w in weights_list]
T = 4000 off_diag = QUBO[ ~torch.eye(QUBO.shape[0], dtype=bool) ] # Selecting off-diagonal terms of the Qubo with a mask rydberg_global = self.device.channels["rydberg_global"] assert rydberg_global.max_amp is not None # FIXME: Document why Omega = min( torch.max(off_diag).item(), rydberg_global.max_amp - 1e-9, )
delta_0 = torch.min(torch.diag(QUBO)).item() delta_f = -delta_0
amp_wave = InterpolatedWaveform(T, [1e-9, Omega, 1e-9]) det_wave = InterpolatedWaveform(T, [delta_0, 0, delta_f])
pulser_pulse = PulserPulse(amp_wave, det_wave, 0) # PulserPulse has some magic that ensures its constructor does not always return # an instance of PulserPulse. Let's make sure (and help mypy realize) that we # are building an instance of PulserPulse. assert isinstance(pulser_pulse, PulserPulse)
shaped_pulse = Pulse(pulse=pulser_pulse) shaped_pulse.norm_weights = norm_weights_list shaped_pulse.duration = T
self.pulse = shaped_pulse solution = QUBOSolution(torch.Tensor(), torch.Tensor())
return self.pulse, solution
BasePulseShaper(instance, config, backend)
Section titled “
BasePulseShaper(instance, config, backend)
”
Bases: ABC
Abstract base class for generating pulse schedules based on a QUBO problem.
This class transforms the structure of a QUBOInstance into a quantum pulse sequence that can be applied to a physical register. The register is passed at the time of pulse generation, not during initialization.
ATTRIBUTE | DESCRIPTION |
---|---|
instance |
The QUBO problem instance.
TYPE:
|
config |
The solver configuration.
TYPE:
|
pulse |
A saved current pulse obtained by
TYPE:
|
backend |
Backend to use.
TYPE:
|
device |
Device from backend.
TYPE:
|
Initialize the pulse shaping module with a QUBO instance.
PARAMETER | DESCRIPTION |
---|---|
instance
|
The QUBO problem instance.
TYPE:
|
config
|
The solver configuration.
TYPE:
|
backend
|
Backend to use.
TYPE:
|
Source code in qubosolver/pipeline/pulse.py
def __init__(self, instance: QUBOInstance, config: SolverConfig, backend: BaseBackend): """ Initialize the pulse shaping module with a QUBO instance.
Args: instance (QUBOInstance): The QUBO problem instance. config (SolverConfig): The solver configuration. backend (BaseBackend): Backend to use. """ self.instance: QUBOInstance = instance self.config: SolverConfig = config self.pulse: Pulse | None = None self.backend = backend self.device = backend.device()
generate(register, instance)
abstractmethod
Section titled “
generate(register, instance)
abstractmethod
”Generate a pulse based on the problem and the provided register.
PARAMETER | DESCRIPTION |
---|---|
register
|
The physical register layout.
TYPE:
|
instance
|
The QUBO instance.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
Pulse
|
A generated pulse object wrapping a Pulser pulse.
TYPE:
|
QUBOSolution
|
An instance of the qubo solution
TYPE:
|
Source code in qubosolver/pipeline/pulse.py
@abstractmethoddef generate( self, register: Register, instance: QUBOInstance,) -> tuple[Pulse, QUBOSolution]: """ Generate a pulse based on the problem and the provided register.
Args: register (Register): The physical register layout. instance (QUBOInstance): The QUBO instance.
Returns: Pulse: A generated pulse object wrapping a Pulser pulse. QUBOSolution: An instance of the qubo solution """ pass
OptimizedPulseShaper(instance, config, backend)
Section titled “
OptimizedPulseShaper(instance, config, backend)
”
Bases: BasePulseShaper
Pulse shaper that uses optimization to find the best pulse parameters for solving QUBOs. Returns an optimized pulse, the bitstrings, their counts, probabilities, and costs.
ATTRIBUTE | DESCRIPTION |
---|---|
pulse |
current pulse.
TYPE:
|
best_cost |
Current best cost.
TYPE:
|
best_bitstring |
Current best bitstring.
TYPE:
|
bitstrings |
List of current bitstrings obtained.
TYPE:
|
counts |
Frequencies of bitstrings.
TYPE:
|
probabilities |
Probabilities of bitstrings.
TYPE:
|
costs |
Qubo cost.
TYPE:
|
custom_qubo_cost |
Apply a different qubo cost evaluation during optimization.
Must be defined as:
TYPE:
|
custom_objective_fn |
For bayesian optimization, one can change the output of
TYPE:
|
callback_objective |
Apply a callback
during bayesian optimization. Only accepts one input dictionary
created during optimization
TYPE:
|
Instantiate an OptimizedPulseShaper
.
PARAMETER | DESCRIPTION |
---|---|
instance
|
Qubo instance.
TYPE:
|
config
|
Configuration for solving.
TYPE:
|
backend
|
Backend to use during optimization.
TYPE:
|
Source code in qubosolver/pipeline/pulse.py
def __init__( self, instance: QUBOInstance, config: SolverConfig, backend: BaseBackend,): """Instantiate an `OptimizedPulseShaper`.
Args: instance (QUBOInstance): Qubo instance. config (SolverConfig): Configuration for solving. backend (BaseBackend): Backend to use during optimization.
""" super().__init__(instance, config, backend)
self.pulse = None self.best_cost = None self.best_bitstring = None self.best_params = None self.bitstrings = None self.counts = None self.probabilities = None self.costs = None self.custom_qubo_cost = self.config.pulse_shaping.custom_qubo_cost self.custom_objective_fn = self.config.pulse_shaping.custom_objective self.callback_objective = self.config.pulse_shaping.callback_objective
build_pulse(params)
Section titled “
build_pulse(params)
”Build the pulse from a list of parameters for the objective.
PARAMETER | DESCRIPTION |
---|---|
params
|
List of parameters.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
Pulse
|
pulse sequence.
TYPE:
|
Source code in qubosolver/pipeline/pulse.py
def build_pulse(self, params: list) -> Pulse: """Build the pulse from a list of parameters for the objective.
Args: params (list): List of parameters.
Returns: Pulse: pulse sequence. """ amp = InterpolatedWaveform(5000, [1e-9] + list(params[:3]) + [1e-9]) det = InterpolatedWaveform(5000, [params[3]] + list(params[4:]) + [params[3]]) pulser_pulse = PulserPulse(amp, det, 0) # PulserPulse has some magic that ensures its constructor does not always return # an instance of PulserPulse. Let's make sure (and help mypy realize) that we # are building an instance of PulserPulse. assert isinstance(pulser_pulse, PulserPulse)
pulse = Pulse( pulse=pulser_pulse, norm_weights=self.norm_weights_list, duration=5000, ) # pulse.pulse.norm_weights = self.norm_weights_list # pulse.pulse.duration = 5000 return pulse
compute_qubo_cost(bitstring, QUBO)
Section titled “
compute_qubo_cost(bitstring, QUBO)
”The qubo cost for a single bitstring to apply during optimization.
PARAMETER | DESCRIPTION |
---|---|
bitstring
|
candidate bitstring.
TYPE:
|
QUBO
|
qubo coefficients.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
float
|
respective cost of bitstring.
TYPE:
|
Source code in qubosolver/pipeline/pulse.py
def compute_qubo_cost(self, bitstring: str, QUBO: torch.Tensor) -> float: """The qubo cost for a single bitstring to apply during optimization.
Args: bitstring (str): candidate bitstring. QUBO (torch.Tensor): qubo coefficients.
Returns: float: respective cost of bitstring. """ if self.custom_qubo_cost is None: return calculate_qubo_cost(bitstring, QUBO)
return cast(float, self.custom_qubo_cost(bitstring, QUBO))
generate(register, instance)
Section titled “
generate(register, instance)
”Generate a pulse via optimization.
PARAMETER | DESCRIPTION |
---|---|
register
|
The physical register layout.
TYPE:
|
instance
|
The QUBO instance.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
Pulse
|
A generated pulse object wrapping a Pulser pulse.
TYPE:
|
QUBOSolution
|
An instance of the qubo solution
TYPE:
|
Source code in qubosolver/pipeline/pulse.py
def generate( self, register: Register, instance: QUBOInstance,) -> tuple[Pulse, QUBOSolution]: """ Generate a pulse via optimization.
Args: register (Register): The physical register layout. instance (QUBOInstance): The QUBO instance.
Returns: Pulse: A generated pulse object wrapping a Pulser pulse. QUBOSolution: An instance of the qubo solution """ # TODO: Harmonize the output of the pulse_shaper generate QUBO = instance.coefficients self.register = register self.norm_weights_list = self._compute_norm_weights(QUBO)
n_amp = 3 n_det = 3 max_amp = self.device.channels["rydberg_global"].max_amp assert max_amp is not None max_amp = max_amp - 1e-6 # added to avoid rouding errors that make the simulation fail (overcoming max_amp)
max_det = self.device.channels["rydberg_global"].max_abs_detuning assert max_det is not None max_det -= 1e-6 # same
bounds = [(1, max_amp)] * n_amp + [(-max_det, 0)] + [(-max_det, max_det)] * (n_det - 1) x0 = ( self.config.pulse_shaping.initial_omega_parameters + self.config.pulse_shaping.initial_detuning_parameters )
def objective(x: list[float]) -> float: pulse = self.build_pulse(x)
try: bitstrings, counts, probabilities, costs, cost_eval, best_bitstring = ( self.run_simulation( self.register, pulse, QUBO, convert_to_tensor=False, ) ) if self.custom_objective_fn is not None: cost_eval = self.custom_objective_fn( bitstrings, counts, probabilities, costs, cost_eval, best_bitstring, ) if not np.isfinite(cost_eval): print(f"[Warning] Non-finite cost encountered: {cost_eval} at x={x}") cost_eval = 1e4
except Exception as e: print(f"[Exception] Error during simulation at x={x}: {e}") cost_eval = 1e4
if self.callback_objective is not None: self.callback_objective({"x": x, "cost_eval": cost_eval}) return float(cost_eval)
opt_result = gp_minimize(objective, bounds, x0=x0, n_calls=self.config.n_calls)
if opt_result and opt_result.x: self.best_params = opt_result.x self.pulse = self.build_pulse(self.best_params) # type: ignore[arg-type]
( self.bitstrings, self.counts, self.probabilities, self.costs, self.best_cost, self.best_bitstring, ) = self.run_simulation(self.register, self.pulse, QUBO, convert_to_tensor=True)
if self.bitstrings is None or self.counts is None: # TODO: what needs to be returned here? # the generate function should always return a pulse - even if it is not good. # we need to return a pulse (self.pulse) - which is none here. return self.pulse, QUBOSolution(None, None) # type: ignore[return-value]
assert self.costs is not None solution = QUBOSolution( bitstrings=self.bitstrings, counts=self.counts, probabilities=self.probabilities, costs=self.costs, ) assert self.pulse is not None return self.pulse, solution
run_simulation(register, pulse, QUBO, convert_to_tensor=True)
Section titled “
run_simulation(register, pulse, QUBO, convert_to_tensor=True)
”Run a quantum program using backend and returns a tuple of (bitstrings, counts, probabilities, costs, best cost, best bitstring).
PARAMETER | DESCRIPTION |
---|---|
register
|
register of quantum program.
TYPE:
|
pulse
|
pulse sequence to run on backend.
TYPE:
|
QUBO
|
Qubo coefficients.
TYPE:
|
convert_to_tensor
|
Convert tuple components to tensors. Defaults to True.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
tuple
|
tuple of (bitstrings, counts, probabilities, costs, best cost, best bitstring)
TYPE:
|
Source code in qubosolver/pipeline/pulse.py
def run_simulation( self, register: Register, pulse: Pulse, QUBO: torch.Tensor, convert_to_tensor: bool = True,) -> tuple: """Run a quantum program using backend and returns a tuple of (bitstrings, counts, probabilities, costs, best cost, best bitstring).
Args: register (Register): register of quantum program. pulse (Pulse): pulse sequence to run on backend. QUBO (torch.Tensor): Qubo coefficients. convert_to_tensor (bool, optional): Convert tuple components to tensors. Defaults to True.
Returns: tuple: tuple of (bitstrings, counts, probabilities, costs, best cost, best bitstring) """ try: program = QuantumProgram( register=register.register, pulse=pulse.pulse, device=self.device ) bitstring_counts = self.backend.run(program).counts
cost_dict = {b: self.compute_qubo_cost(b, QUBO) for b in bitstring_counts.keys()}
best_bitstring = min(cost_dict, key=cost_dict.get) # type: ignore[arg-type] best_cost = cost_dict[best_bitstring]
if convert_to_tensor: keys = list(bitstring_counts.keys()) values = list(bitstring_counts.values())
bitstrings_tensor = torch.tensor( [[int(b) for b in bitstr] for bitstr in keys], dtype=torch.int32 ) counts_tensor = torch.tensor(values, dtype=torch.int32) probabilities_tensor = counts_tensor.float() / counts_tensor.sum()
costs_tensor = torch.tensor( [self.compute_qubo_cost(b, QUBO) for b in keys], dtype=torch.float32 )
return ( bitstrings_tensor, counts_tensor, probabilities_tensor, costs_tensor, best_cost, best_bitstring, ) else: counts = list(bitstring_counts.values()) nsamples = float(sum(counts)) return ( list(bitstring_counts.keys()), counts, [c / nsamples for c in counts], list(cost_dict.values()), best_cost, best_bitstring, )
except Exception as e: print(f"Simulation failed: {e}") return ( torch.tensor([]), torch.tensor([]), torch.tensor([]), torch.tensor([]), float("inf"), None, )
get_pulse_shaper(instance, config, backend)
Section titled “
get_pulse_shaper(instance, config, backend)
”Method that returns the correct PulseShaper based on configuration. The correct pulse shaping method can be identified using the config, and an object of this pulseshaper can be returned using this function.
PARAMETER | DESCRIPTION |
---|---|
instance
|
The QUBO problem to embed.
TYPE:
|
config
|
The solver configuration used.
TYPE:
|
backend
|
Backend to extract device from or to use during pulse shaping.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
BasePulseShaper
|
The representative Pulse Shaper object. |
Source code in qubosolver/pipeline/pulse.py
def get_pulse_shaper( instance: QUBOInstance, config: SolverConfig, backend: BaseBackend,) -> BasePulseShaper: """ Method that returns the correct PulseShaper based on configuration. The correct pulse shaping method can be identified using the config, and an object of this pulseshaper can be returned using this function.
Args: instance (QUBOInstance): The QUBO problem to embed. config (SolverConfig): The solver configuration used. backend (BaseBackend): Backend to extract device from or to use during pulse shaping.
Returns: (BasePulseShaper): The representative Pulse Shaper object. """ if config.pulse_shaping.pulse_shaping_method == PulseType.ADIABATIC: return AdiabaticPulseShaper(instance, config, backend) elif config.pulse_shaping.pulse_shaping_method == PulseType.OPTIMIZED: return OptimizedPulseShaper(instance, config, backend) elif issubclass(config.pulse_shaping.pulse_shaping_method, BasePulseShaper): return cast( BasePulseShaper, config.pulse_shaping.pulse_shaping_method(instance, config, backend), ) else: raise NotImplementedError
Embedding
Section titled “Embedding”
BLaDEmbedder(instance, config, backend)
Section titled “
BLaDEmbedder(instance, config, backend)
”
Bases: BaseEmbedder
BLaDE (Balanced Latently Dimensional Embedder)
Computes positions for nodes so that their interactions according to a device approach the desired values at best. The result can be used as an embedding. Its prior target is on interaction matrices or QUBOs, but it can also be used for MIS with limitations if the adjacency matrix is converted into a QUBO. The general principle is based on the Fruchterman-Reingold algorithm.
Source code in qubosolver/pipeline/embedder.py
def __init__(self, instance: QUBOInstance, config: SolverConfig, backend: BaseBackend): """ Args: instance (QUBOInstance): The QUBO problem to embed. config (SolverConfig): The Solver Configuration. """ self.instance: QUBOInstance = instance self.config: SolverConfig = config self.register: TargetRegister | None = None self.backend = backend
BaseEmbedder(instance, config, backend)
Section titled “
BaseEmbedder(instance, config, backend)
”
Bases: ABC
Abstract base class for all embedders.
Prepares the geometry (register) of atoms based on the QUBO instance. Returns a Register compatible with Pasqal/Pulser devices.
PARAMETER | DESCRIPTION |
---|---|
instance
|
The QUBO problem to embed.
TYPE:
|
config
|
The Solver Configuration.
TYPE:
|
Source code in qubosolver/pipeline/embedder.py
def __init__(self, instance: QUBOInstance, config: SolverConfig, backend: BaseBackend): """ Args: instance (QUBOInstance): The QUBO problem to embed. config (SolverConfig): The Solver Configuration. """ self.instance: QUBOInstance = instance self.config: SolverConfig = config self.register: TargetRegister | None = None self.backend = backend
embed()
abstractmethod
Section titled “
embed()
abstractmethod
”Creates a layout of atoms as the register.
RETURNS | DESCRIPTION |
---|---|
Register
|
The register.
TYPE:
|
Source code in qubosolver/pipeline/embedder.py
@abstractmethoddef embed(self) -> TargetRegister: """ Creates a layout of atoms as the register.
Returns: Register: The register. """
GreedyEmbedder(instance, config, backend)
Section titled “
GreedyEmbedder(instance, config, backend)
”
Bases: BaseEmbedder
Create an embedding in a greedy fashion.
At each step, place one logical node onto one trap to minimize the incremental mismatch between the logical QUBO matrix Q and the physical interaction matrix U (approx. C / ||r_i - r_j||^6).
Source code in qubosolver/pipeline/embedder.py
def __init__(self, instance: QUBOInstance, config: SolverConfig, backend: BaseBackend): """ Args: instance (QUBOInstance): The QUBO problem to embed. config (SolverConfig): The Solver Configuration. """ self.instance: QUBOInstance = instance self.config: SolverConfig = config self.register: TargetRegister | None = None self.backend = backend
embed()
Section titled “
embed()
”Creates a layout of atoms as the register.
RETURNS | DESCRIPTION |
---|---|
Register
|
The register.
TYPE:
|
Source code in qubosolver/pipeline/embedder.py
@typing.no_type_checkdef embed(self) -> TargetRegister: """ Creates a layout of atoms as the register.
Returns: Register: The register. """ if self.config.embedding.traps < self.instance.size: raise ValueError( "Number of traps must be at least equal to the number of atoms on the register." )
# compute density (unchanged) self.config.embedding.density = calculate_density( self.instance.coefficients, self.instance.size )
# build params for the Greedy algorithm params = { "device": self.backend.device(), "layout": self.config.embedding.layout_greedy_embedder, "traps": int(self.config.embedding.traps), "spacing": float(self.config.embedding.spacing), # animation controls (all read by Greedy) "draw_steps": bool(self.config.embedding.draw_steps), # collect per-step data "animation": bool(self.config.embedding.draw_steps), # render animation after run "animation_save_path": self.config.embedding.animation_save_path, # optional export # "animation_top_k": 5, # (optional) uncomment if you add support for this in Greedy }
# --- DEBUG / INFO: show where Greedy comes from + the params we’ll pass dev = params["device"] dev_str = ( getattr(dev, "name", None) or getattr(dev, "device_name", None) or dev.__class__.__name__ ) printable = dict(params) printable["device"] = dev_str # avoid dumping the whole object # --- Call Greedy (unchanged public signature) best, _, coords, _, _ = Greedy().launch_greedy( Q=self.instance.coefficients, params=params, # no extra kwargs; Greedy reads animation/draw/save_path from params )
# build the register (unchanged) qubits = {f"q{i}": coord for i, coord in enumerate(coords)} register = PulserRegister(qubits) return TargetRegister(self.backend.device(), register)
get_embedder(instance, config, backend)
Section titled “
get_embedder(instance, config, backend)
”Method that returns the correct embedder based on configuration. The correct embedding method can be identified using the config, and an object of this embedding can be returned using this function.
PARAMETER | DESCRIPTION |
---|---|
instance
|
The QUBO problem to embed.
TYPE:
|
config
|
The quantum device to target.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
BaseEmbedder
|
The representative embedder object. |
Source code in qubosolver/pipeline/embedder.py
def get_embedder( instance: QUBOInstance, config: SolverConfig, backend: BaseBackend) -> BaseEmbedder: """ Method that returns the correct embedder based on configuration. The correct embedding method can be identified using the config, and an object of this embedding can be returned using this function.
Args: instance (QUBOInstance): The QUBO problem to embed. config (Device): The quantum device to target.
Returns: (BaseEmbedder): The representative embedder object. """
if config.embedding.embedding_method == EmbedderType.BLADE: return BLaDEmbedder(instance, config, backend) elif config.embedding.embedding_method == EmbedderType.GREEDY: return GreedyEmbedder(instance, config, backend) elif issubclass(config.embedding.embedding_method, BaseEmbedder): return typing.cast( BaseEmbedder, config.embedding.embedding_method(instance, config, backend) ) else: raise NotImplementedError