Quantum-related components of a QUBO solver
Drive Shapping
Section titled “Drive Shapping”
BaseDriveShaper(instance, config, backend)
Section titled “
BaseDriveShaper(instance, config, backend)
”
Bases: ABC
Abstract base class for generating Qoolqit drives based on a QUBO problem.
This class transforms the structure of a QUBOInstance into a quantum waveform sequence or drive that can be applied to a physical register. The register is passed at the time of drive generation, not during initialization.
| ATTRIBUTE | DESCRIPTION |
|---|---|
instance |
The QUBO problem instance.
TYPE:
|
config |
The solver configuration.
TYPE:
|
drive |
A saved current drive obtained by
TYPE:
|
backend |
Backend to use.
TYPE:
|
device |
Device from backend.
TYPE:
|
Initialize the drive 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/drive.py
def __init__(self, instance: QUBOInstance, config: SolverConfig, backend: concepts.Backend): """ Initialize the drive shaping module with a QUBO instance.
Args: instance (QUBOInstance): The QUBO problem instance. config (SolverConfig): The solver configuration. backend (Backend): Backend to use. """ self.instance: QUBOInstance = instance self.config: SolverConfig = config self.drive: Drive | None = None self.backend = backend self.device = self.config.device
# check if device allow DMM self.dmm = self.config.drive_shaping.dmm and ( len(list(self.config.device._device.dmm_channels.keys())) > 0 )
generate(register)
abstractmethod
Section titled “
generate(register)
abstractmethod
”Generate a drive based on the problem and the provided register.
| PARAMETER | DESCRIPTION |
|---|---|
register
|
The physical register layout.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Drive
|
A generated Drive.
TYPE:
|
QUBOSolution
|
An instance of the qubo solution
TYPE:
|
Source code in qubosolver/pipeline/drive.py
@abstractmethoddef generate( self, register: Register,) -> tuple[Drive, QUBOSolution]: """ Generate a drive based on the problem and the provided register.
Args: register (Register): The physical register layout.
Returns: Drive: A generated Drive. QUBOSolution: An instance of the qubo solution """ pass
HeuristicDriveShaper(instance, config, backend)
Section titled “
HeuristicDriveShaper(instance, config, backend)
”
Bases: BaseDriveShaper
Heuristic schedule drive shaper.
With DMM
Without DMM
Source code in qubosolver/pipeline/drive.py
def __init__(self, instance: QUBOInstance, config: SolverConfig, backend: concepts.Backend): """ Initialize the drive shaping module with a QUBO instance.
Args: instance (QUBOInstance): The QUBO problem instance. config (SolverConfig): The solver configuration. backend (Backend): Backend to use. """ self.instance: QUBOInstance = instance self.config: SolverConfig = config self.drive: Drive | None = None self.backend = backend self.device = self.config.device
# check if device allow DMM self.dmm = self.config.drive_shaping.dmm and ( len(list(self.config.device._device.dmm_channels.keys())) > 0 )
OptimizedDriveShaper(instance, config, backend)
Section titled “
OptimizedDriveShaper(instance, config, backend)
”
Bases: BaseDriveShaper
Drive shaper that uses optimization to find the best drive parameters for solving QUBOs. Returns an optimized drive, the bitstrings, their counts, probabilities, and costs.
| ATTRIBUTE | DESCRIPTION |
|---|---|
drive |
current drive.
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:
|
optimized_custom_qubo_cost |
Apply a different qubo cost evaluation during optimization.
Must be defined as:
TYPE:
|
optimized_custom_objective_fn |
For bayesian optimization, one can change the output of
TYPE:
|
optimized_callback_objective |
Apply a callback
during bayesian optimization. Only accepts one input dictionary
created during optimization
TYPE:
|
Instantiate an OptimizedDriveShaper.
| PARAMETER | DESCRIPTION |
|---|---|
instance
|
Qubo instance.
TYPE:
|
config
|
Configuration for solving.
TYPE:
|
backend
|
Backend to use during optimization.
TYPE:
|
Source code in qubosolver/pipeline/drive.py
def __init__( self, instance: QUBOInstance, config: SolverConfig, backend: concepts.Backend,): """Instantiate an `OptimizedDriveShaper`.
Args: instance (QUBOInstance): Qubo instance. config (SolverConfig): Configuration for solving. backend (Backend): Backend to use during optimization.
""" super().__init__(instance, config, backend)
self.drive = 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.optimized_custom_qubo_cost = self.config.drive_shaping.optimized_custom_qubo_cost self.optimized_custom_objective_fn = self.config.drive_shaping.optimized_custom_objective self.optimized_callback_objective = self.config.drive_shaping.optimized_callback_objective
build_drive(params)
Section titled “
build_drive(params)
”Build the drive waveform from a normalised parameter vector.
| PARAMETER | DESCRIPTION |
|---|---|
params
|
6 values — 3 amplitude breakpoints then 3 detuning breakpoints, all normalised to [0, 1] (or [-1, 1] for detuning).
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Drive
|
Drive sequence.
TYPE:
|
Source code in qubosolver/pipeline/drive.py
def build_drive(self, params: list) -> Drive: """Build the drive waveform from a normalised parameter vector.
Args: params (list): 6 values — 3 amplitude breakpoints then 3 detuning breakpoints, all normalised to [0, 1] (or [-1, 1] for detuning).
Returns: Drive: Drive sequence. """ specs = self.device.specs max_seq_duration: float = specs["max_duration"] or 1e3 max_amplitude: float = specs["max_amplitude"] or 1e4 max_detuning: float = specs["max_abs_detuning"] or 1e4
amp_params = [1e-9] + list(params[:3]) + [1e-9] det_params = list(params[3:]) amp_params = [p * max_amplitude for p in amp_params] det_params = [p * max_detuning for p in det_params]
amp_wave = InterpolatedWaveform(max_seq_duration, amp_params) det_wave = InterpolatedWaveform(max_seq_duration, det_params)
dmm = None final_detuning = det_params[-1] if self.dmm and final_detuning > 0: dmm = constant_weighted_dmm( self.register, max_seq_duration, self.norm_weights_list, final_detuning=-final_detuning, )
shaped_drive = Drive(amplitude=amp_wave, detuning=det_wave, dmm=dmm)
return shaped_drive
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/drive.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.optimized_custom_qubo_cost is None: return calculate_qubo_cost(bitstring, QUBO)
return cast(float, self.optimized_custom_qubo_cost(bitstring, QUBO))
generate(register)
Section titled “
generate(register)
”Generate a drive via Bayesian optimisation.
| PARAMETER | DESCRIPTION |
|---|---|
register
|
The physical register layout.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Drive
|
A generated Drive.
TYPE:
|
QUBOSolution
|
An instance of the qubo solution
TYPE:
|
Source code in qubosolver/pipeline/drive.py
def generate( self, register: Register,) -> tuple[Drive, QUBOSolution]: """ Generate a drive via Bayesian optimisation.
Args: register (Register): The physical register layout.
Returns: Drive: A generated Drive. QUBOSolution: An instance of the qubo solution """ # TODO: Harmonize the output of the pulse_shaper generate QUBO = self.qubo_coefficients self.register = register
self.norm_weights_list = self._compute_norm_weights()
n_amp = 3 n_det = 3
eps = 0.0001 zero = eps one = 1.0 - eps
bounds = ( [(zero, one)] * n_amp + [(-one, -zero)] + [(-one, one)] * (n_det - 2) + [(zero, one)] ) x0 = ( self.config.drive_shaping.optimized_initial_omega_parameters + self.config.drive_shaping.optimized_initial_detuning_parameters )
def objective(x: list[float]) -> float: drive = self.build_drive(x)
try: bitstrings, counts, probabilities, costs, cost_eval, best_bitstring = ( self.run_simulation( self.register, drive, QUBO, convert_to_tensor=False, ) ) if self.optimized_custom_objective_fn is not None: cost_eval = self.optimized_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.optimized_callback_objective is not None: self.optimized_callback_objective({"x": x, "cost_eval": cost_eval}) return float(cost_eval)
opt_result = gp_minimize( objective, bounds, x0=x0, n_calls=self.config.drive_shaping.optimized_n_calls, random_state=self.config.drive_shaping.optimized_seed, )
if opt_result and opt_result.x: self.best_params = opt_result.x self.drive = self.build_drive(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.drive, 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 drive - even if it is not good. # we need to return a drive (self.drive) - which is none here. # return self.drive, QUBOSolution(None, None) raise RuntimeError("No solution found")
assert self.costs is not None solution = QUBOSolution( bitstrings=self.bitstrings, counts=self.counts, probabilities=self.probabilities, costs=self.costs, ) assert self.drive is not None return self.drive, solution
run_simulation(register, drive, QUBO, convert_to_tensor=True)
Section titled “
run_simulation(register, drive, 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:
|
drive
|
drive 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/drive.py
def run_simulation( self, register: Register, drive: Drive, 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. drive (Drive): drive 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, drive=drive) program.compile_to( self.device, profile=compiler_profile(self.config), device_max_duration_ratio=max_duration_ratio(self.config), ) job = self.backend.run(program) bitstring_counts = job.results().final_bitstrings
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_drive_shaper(instance, config, backend)
Section titled “
get_drive_shaper(instance, config, backend)
”Method that returns the correct DriveShaper based on configuration. The correct drive shaping method can be identified using the config, and an object of this driveshaper 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 drive shaping.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
BaseDriveShaper
|
The representative Drive Shaper object. |
Source code in qubosolver/pipeline/drive.py
def get_drive_shaper( instance: QUBOInstance, config: SolverConfig, backend: concepts.Backend,) -> BaseDriveShaper: """ Method that returns the correct DriveShaper based on configuration. The correct drive shaping method can be identified using the config, and an object of this driveshaper can be returned using this function.
Args: instance (QUBOInstance): The QUBO problem to embed. config (SolverConfig): The solver configuration used. backend (Backend): Backend to extract device from or to use during drive shaping.
Returns: (BaseDriveShaper): The representative Drive Shaper object. """ if config.drive_shaping.drive_shaping_method == DriveType.HEURISTIC: return HeuristicDriveShaper(instance, config, backend) elif config.drive_shaping.drive_shaping_method == DriveType.OPTIMIZED: return OptimizedDriveShaper(instance, config, backend) elif issubclass(config.drive_shaping.drive_shaping_method, BaseDriveShaper): return cast( BaseDriveShaper, config.drive_shaping.drive_shaping_method(instance, config, backend), ) else: raise NotImplementedErrorEmbedding
Section titled “Embedding”
BLaDEmbedder(instance, config, backend)
Section titled “
BLaDEmbedder(instance, config, backend)
”
Bases: BaseEmbedder
Atom-register embedder using the qoolqit BLaDe (Block-Layout and Degree-based) matrix-embedding algorithm.
BLaDe jointly optimises atom positions to match the logical adjacency
structure of the QUBO graph with the physical Rydberg interaction matrix.
Configuration is taken from config.embedding (BLaDe-specific fields:
blade_steps_per_round, blade_starting_positions,
blade_dimensions, min_distance).
Source code in qubosolver/pipeline/embedder.py
def __init__(self, instance: QUBOInstance, config: SolverConfig, backend: concepts.Backend): """ Args: instance (QUBOInstance): The QUBO problem to embed. config (SolverConfig): Solver configuration. backend (Backend): Execution backend providing device information. """ self.instance: QUBOInstance = instance self.config: SolverConfig = config self.register: Register | None = None self.backend = backend
# TODO: remove when bumping to qoolqit v1 # for converting to qoolqit self._distance_conversion = self.config.device.converter.factors[2]
embed()
Section titled “
embed()
”Run the BLaDe embedding algorithm and return the resulting register.
Reads embedding hyper-parameters from self.config.embedding,
constructs a BladeConfig, runs Blade.embed on the QUBO
coefficient matrix, optionally rescales coordinates to satisfy the
min_distance constraint, and wraps the result as a Register.
See Qoolqit's documentation (external) for details.
| RETURNS | DESCRIPTION |
|---|---|
Register
|
Atom register with positions optimised by BLaDe.
TYPE:
|
Source code in qubosolver/pipeline/embedder.py
def embed(self) -> Register: """Run the BLaDe embedding algorithm and return the resulting register.
Reads embedding hyper-parameters from ``self.config.embedding``, constructs a ``BladeConfig``, runs ``Blade.embed`` on the QUBO coefficient matrix, optionally rescales coordinates to satisfy the ``min_distance`` constraint, and wraps the result as a ``Register``. See [Qoolqit's documentation](https://pasqal-io.github.io/qoolqit/latest/reference/embedding/#qoolqit.embedding.BladeConfig) for details.
Returns: Register: Atom register with positions optimised by BLaDe. """ embed_config = self.config.embedding default = BladeConfig() step_per_round = embed_config.blade_steps_per_round if step_per_round is None: step_per_round = default.steps_per_round if embed_config.blade_starting_positions is not None: starting_positions = embed_config.blade_starting_positions.numpy() else: starting_positions = None
min_distance = self.config.embedding.min_distance max_radial_distance = self.config.device.specs["max_radial_distance"] if min_distance is None or max_radial_distance is None: device = self.config.device max_min_dist_ratio = None else: device = None max_min_dist_ratio = max_radial_distance / min_distance
config = BladeConfig( steps_per_round=step_per_round, starting_positions=starting_positions, dimensions=tuple(embed_config.blade_dimensions), max_min_dist_ratio=max_min_dist_ratio, device=device, )
_blade = Blade(config) graph = _blade.embed(self.instance.coefficients.numpy()) if min_distance is not None: graph.rescale_coords(spacing=min_distance) register = Register.from_graph(graph)
return register
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
and returns a Register compatible with Pasqal/Pulser devices.
| ATTRIBUTE | DESCRIPTION |
|---|---|
instance |
The QUBO problem to embed.
TYPE:
|
config |
Solver configuration including embedding settings.
TYPE:
|
register |
The generated register (set after
TYPE:
|
backend |
The execution backend (used to access device specs).
TYPE:
|
Note
| PARAMETER | DESCRIPTION |
|---|---|
instance
|
The QUBO problem to embed.
TYPE:
|
config
|
Solver configuration.
TYPE:
|
backend
|
Execution backend providing device information.
TYPE:
|
Source code in qubosolver/pipeline/embedder.py
def __init__(self, instance: QUBOInstance, config: SolverConfig, backend: concepts.Backend): """ Args: instance (QUBOInstance): The QUBO problem to embed. config (SolverConfig): Solver configuration. backend (Backend): Execution backend providing device information. """ self.instance: QUBOInstance = instance self.config: SolverConfig = config self.register: Register | None = None self.backend = backend
# TODO: remove when bumping to qoolqit v1 # for converting to qoolqit self._distance_conversion = self.config.device.converter.factors[2]
embed()
abstractmethod
Section titled “
embed()
abstractmethod
”Create a register (atom layout) for the QUBO instance.
| RETURNS | DESCRIPTION |
|---|---|
Register
|
The register.
TYPE:
|
Source code in qubosolver/pipeline/embedder.py
@abstractmethoddef embed(self) -> Register: """Create a register (atom layout) for the QUBO instance.
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: concepts.Backend): """ Args: instance (QUBOInstance): The QUBO problem to embed. config (SolverConfig): Solver configuration. backend (Backend): Execution backend providing device information. """ self.instance: QUBOInstance = instance self.config: SolverConfig = config self.register: Register | None = None self.backend = backend
# TODO: remove when bumping to qoolqit v1 # for converting to qoolqit self._distance_conversion = self.config.device.converter.factors[2]
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) -> Register: """ Creates a layout of atoms as the register.
Returns: Register: The register. """ if self.config.embedding.greedy_traps == -1: self.config.embedding.greedy_traps = self._number_of_traps_from_device( self.config.device )
if self.config.embedding.greedy_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.greedy_density = calculate_density( self.instance.coefficients, self.instance.size )
# build params for the Greedy algorithm params = { "device": self.config.device._device, "layout": self.config.embedding.greedy_layout, "traps": int(self.config.embedding.greedy_traps), "spacing": float(self.config.embedding.greedy_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 }
# --- 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 ) min_distance = self.config.embedding.min_distance if min_distance is not None: min_reg_distance = torch.cdist(coords, coords).fill_diagonal_(float("inf")).min() coords *= min_distance / min_reg_distance else: coords /= self._distance_conversion
# build the register (unchanged) qubits = {f"q{i}": coord for i, coord in enumerate(coords)} register = Register(qubits) return 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: concepts.Backend) -> 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