qubosolver
qubosolver
Section titled “
qubosolver
”QUBO Solver: a library for solving QUBO problems with classical, quantum, and hybrid algorithms.
Solves Quadratic Unconstrained Binary Optimization (QUBO) problems using classical, quantum, and hybrid algorithms, including on Pasqal neutral-atom QPUs.
Exposes the core data types (Instance, Solution, Dataset, ...), the
Solver entry point, and the transforms, embedding,
drive_shaping, and solving submodules used to build and run quantum,
hybrid, and classical QUBO solvers.
Modules:
-
analysis–Free functions for analysing QUBO solutions.
-
bitstring–Bitstring utilities for QUBO solvers.
-
bitstrings–Batch bitstring utilities for QUBO solvers.
-
drive_shaping–Drive shaping algorithms for generating quantum drive schedules.
-
embedding–Embedding algorithms for mapping QUBO variables onto quantum hardware registers.
-
matrix–Square matrix utilities for QUBO solvers.
-
protocols–Structural protocols for the QUBO solver, using PEP 544
Protocol(external) typing. -
solver–Config-based entry point for building and running QUBO solvers.
-
solving–Solving algorithms for QUBO problems.
-
tensor–Arbitrary-rank tensor utilities for QUBO solvers.
-
transforms–Transforms for QUBO instances.
-
vector–1-D vector utilities for QUBO solvers.
-
vectori–1-D integer vector utilities for QUBO solvers.
Classes:
-
AutoLocalEmulatorBackend–Factory class that automatically selects optimal emulator backends.
-
AutoRemoteEmulatorBackend–Factory class that automatically selects optimal remote emulator backends.
-
Candidate–A single candidate solution extracted from a
Solution. -
Dataset–A dataset of QUBO instances.
-
Instance–A single QUBO problem instance.
-
LocalEmulator–Local quantum emulator with automatic backend selection.
-
RemoteEmulator–Remote quantum emulator with automatic backend selection.
-
Solution–A collection of candidate solutions for a QUBO problem.
Functions:
-
extract_qubo–Reconstruct the QUBO encoded by a register's geometry and a drive's final detuning.
-
torch_rng–Creates a
torch.Generator(external) compatible withqubosolver's torch typing.
AutoLocalEmulatorBackend
Section titled “
AutoLocalEmulatorBackend
”Factory class that automatically selects optimal emulator backends.
This factory uses __new__ to return instances of different backend types
based on quantum register size for optimal performance:
MPSBackend(external) for large problems (≥26 qubits)SVBackend(external) for medium problems (15-25 qubits)QutipBackendV2(external) for small problems (<15 qubits)
Note
This class acts as a factory and never instantiates itself.
The __new__ method directly returns instances of the selected backend type.
AutoRemoteEmulatorBackend
Section titled “
AutoRemoteEmulatorBackend
”Factory class that automatically selects optimal remote emulator backends.
This factory uses __new__ to return instances of different remote backend types
based on quantum register size for optimal performance:
RemoteMPSBackend(external) for large problems (≥26 qubits)RemoteSVBackend(external) for medium problems (15-25 qubits)RemoteEmuFreeBackend(external) for small problems (<15 qubits)
Note
This class acts as a factory and never instantiates itself.
The __new__ method directly returns instances of the selected remote backend type.
Candidate
dataclass
Section titled “
Candidate
dataclass
”Candidate(bitstring: qubosolver.Bitstring
module-attribute (qubosolver.types.linalg.Bitstring)" href="../bitstring/#qubosolver.Bitstring">Bitstring, cost: float = float('inf'), count: int = 0, probability: float = 0.0)A single candidate solution extracted from a Solution.
Instances are normally obtained via Solution.__getitem__ rather
than constructed directly.
Attributes:
-
bitstring(Bitstring) –Binary vector of shape
(n,)with values in (int8). -
cost(float (external)) –Objective value . Defaults to when constructed without one.
-
count(int (external)) –Number of times this bitstring was sampled. Defaults to
0when constructed without one. -
probability(float (external)) –Sampling probability of this bitstring. Defaults to
0.0when constructed without one.
string
property
Section titled “
string
property
”string: strThe bitstring represented as a plain "0"/"1" character string.
Dataset
Section titled “
Dataset
”Dataset(matrices: torch.Tensor, solutions: Sequence[ Solution
dataclass (qubosolver.types.solution.Solution)" href="#qubosolver.Solution">Solution] = (), *, copy: bool = True)A dataset of QUBO instances.
Each instance is represented by a square matrix such that the optimization objective is , where is a binary vector.
Parameters:
-
matrices(torch (external).Tensor (external)) –Matrices of shape
(size, size, num_instances).matrices[:, :, i]is thei-th QUBO matrix. -
solutions(Sequence (external)[Solution], default:()) –Ground-truth solutions, one per instance. Pass an empty list (default) when solutions are unknown.
-
copy(bool (external), default:True) –Whether to deep-copy
matricesandsolutionson construction. PassFalseto store the given values directly (no copy), e.g. when the caller already owns them exclusively.
Attributes:
-
matrices–Matrices stored as a 3-D tensor of shape
(size, size, num_instances). The third axis indexes individual problem instances. -
solutions–Known solutions for each instance. Empty when the dataset was created without ground-truth solutions (e.g. via
from_random).
Note
matrices and solutions are deep-copied on construction by
default. Mutating the values passed in afterwards will not affect
the dataset, unless copy=False was given.
Methods:
-
__getitem__–Return the matrix and solution for instance idx.
-
__iter__–Iterate over all
(matrix, solution)pairs in order. -
__len__–Return the number of QUBO instances in the dataset.
-
from_random–Generates a Dataset of random, symmetric QUBO coefficient matrices.
-
load–Load a dataset previously saved with
save. -
save–Persist this dataset to disk using
torch.save(external).
Source code in qubosolver/types/dataset.py
def __init__( self, matrices: torch.Tensor, solutions: Sequence[Solution] = (), *, copy: bool = True) -> None: """Build a dataset from `matrices` and optional `solutions`, deep-copying by default.""" if copy: matrices = matrices.detach().clone() solutions = deepcopy(solutions) self.matrices = matrices self.solutions = solutions
__getitem__
Section titled “
__getitem__
”__getitem__(idx: int) -> tuple[ Instance (qubosolver.types.instance.Instance)" href="#qubosolver.Instance">Instance, Solution
dataclass (qubosolver.types.solution.Solution)" href="#qubosolver.Solution">Solution]Return the matrix and solution for instance idx.
Parameters:
-
idx(int (external)) –Zero-based index of the instance.
Returns:
-
tuple (external)[Instance, Solution]–A symmetric matrix and a solution
(Q, solution). When no solutions were provided,solutionis an emptySolution.
Source code in qubosolver/types/dataset.py
def __getitem__(self, idx: int) -> tuple[Instance, Solution]: """Return the matrix and solution for instance *idx*.
Args: idx: Zero-based index of the instance.
Returns: A symmetric matrix and a solution ``(Q, solution)``. When no solutions were provided, ``solution`` is an empty [`Solution`][]. """ instance = Instance(self.matrices[:, :, idx]) if self.solutions: return instance, self.solutions[idx] return instance, Solution()
__iter__
Section titled “
__iter__
”__iter__() -> Iterator[tuple[ Instance (qubosolver.types.instance.Instance)" href="#qubosolver.Instance">Instance, Solution
dataclass (qubosolver.types.solution.Solution)" href="#qubosolver.Solution">Solution]]Iterate over all (matrix, solution) pairs in order.
Yields:
-
tuple (external)[Instance, Solution]–A matrix and a solution. Same as
__getitem__for each index0 … len(self)-1.
Source code in qubosolver/types/dataset.py
def __iter__(self) -> Iterator[tuple[Instance, Solution]]: """Iterate over all ``(matrix, solution)`` pairs in order.
Yields: A matrix and a solution. Same as [`__getitem__`][] for each index ``0 … len(self)-1``. """ return map(self.__getitem__, range(len(self)))
__len__
Section titled “
__len__
”__len__() -> intReturn the number of QUBO instances in the dataset.
Source code in qubosolver/types/dataset.py
def __len__(self) -> int: """Return the number of QUBO instances in the dataset.""" return int(self.matrices.shape[2])
from_random
classmethod
Section titled “
from_random
classmethod
”from_random(n_matrices: int, matrix_dim: int, *, densities: Sequence[float] = (0.5,), coefficient_bounds: tuple[float, float] = (-10.0, 10.0), dtype: torch.dtype | None = None, rng: torch.Generator | None = None, negative_offdiag_rate: float = 0.0) -> Dataset (qubosolver.types.dataset.Dataset)" href="#qubosolver.Dataset">DatasetGenerates a Dataset of random, symmetric QUBO coefficient matrices.
For each requested density, n_matrices symmetric matrices of shape
(matrix_dim, matrix_dim) are generated with (approximately) that
fraction of non-zero entries. Off-diagonal coefficients are positive
(unless flipped negative by negative_offdiag_rate), each matrix is
guaranteed at least one negative diagonal element and at least one
coefficient equal to coefficient_bounds[1], so that the resulting
instances are non-trivial to solve.
Parameters:
-
n_matrices(int (external)) –Number of QUBO matrices to generate for each density.
-
matrix_dim(int (external)) –The dimension of each QUBO matrix.
-
densities(Sequence (external)[float (external)], default:(0.5,)) –List of densities (ratio of non-zero elements).
-
coefficient_bounds(tuple (external)[float (external), float (external)], default:(-10.0, 10.0)) –Range (min, max) of random values for the coefficients.
-
dtype(torch (external).dtype (external) | None, default:None) –Data type for the coefficient matrices.
-
rng(torch (external).Generator (external) | None, default:None) –Random number generator controlling the sampling.
-
negative_offdiag_rate(float (external), default:0.0) –Fraction of the non-zero off-diagonal coefficients to flip negative. A value of 0 means that no off-diagonal coefficient is negative.
Returns:
-
Dataset–A dataset containing
n_matrices * len(densities)generated coefficient matrices, with no associated solutions.
Source code in qubosolver/types/dataset.py
@classmethoddef from_random( cls, n_matrices: int, matrix_dim: int, *, densities: Sequence[float] = (0.5,), coefficient_bounds: tuple[float, float] = (-10.0, 10.0), dtype: torch.dtype | None = None, rng: torch.Generator | None = None, negative_offdiag_rate: float = 0.0,) -> Dataset: """Generates a Dataset of random, symmetric QUBO coefficient matrices.
For each requested density, `n_matrices` symmetric matrices of shape ``(matrix_dim, matrix_dim)`` are generated with (approximately) that fraction of non-zero entries. Off-diagonal coefficients are positive (unless flipped negative by `negative_offdiag_rate`), each matrix is guaranteed at least one negative diagonal element and at least one coefficient equal to `coefficient_bounds[1]`, so that the resulting instances are non-trivial to solve.
Args: n_matrices: Number of QUBO matrices to generate for each density. matrix_dim: The dimension of each QUBO matrix. densities: List of densities (ratio of non-zero elements). coefficient_bounds: Range (min, max) of random values for the coefficients. dtype: Data type for the coefficient matrices. rng: Random number generator controlling the sampling. negative_offdiag_rate: Fraction of the non-zero off-diagonal coefficients to flip negative. A value of 0 means that no off-diagonal coefficient is negative.
Returns: A dataset containing ``n_matrices * len(densities)`` generated coefficient matrices, with no associated solutions. """ # Step 1: Initialize a reproducible random generator. dtype = dtype or matrix.dtype() rng = rng or torch_rng() device = rng.device.type
# Step 2: Create a tensor for the coefficients. total_instances = n_matrices * len(densities) coefficients = torch.zeros( matrix_dim, matrix_dim, total_instances, device=device, dtype=dtype )
# Step 3: Generate matrices for each density. idx = 0 for d in densities: target = int(d * matrix_dim * matrix_dim) for idx in range(n_matrices): # generate mask mask = _generate_symmetric_mask(matrix_dim, target, device, rng)
# random sampling and apply mask random_vals = torch.empty( matrix_dim, matrix_dim, device=device, dtype=dtype ).uniform_(*coefficient_bounds, generator=rng) random_vals = random_vals * mask.to(dtype)
original_diag = random_vals.diag().clone() coeff = torch.triu(random_vals, diagonal=1) coeff = coeff + coeff.T coeff.diagonal().copy_(original_diag)
off_diag = ~torch.eye(matrix_dim, dtype=torch.bool, device=device) coeff[off_diag] = coeff[off_diag].abs() if negative_offdiag_rate > 0.0: # make non-diagonal negative elements rate = float(max(0.0, min(1.0, negative_offdiag_rate))) upper_mask = torch.triu(mask, diagonal=1) nz_pairs = upper_mask.nonzero(as_tuple=False) M = nz_pairs.size(0) # Return K negative elements if M > 0: K = max(1, round(rate * M)) perm = torch.randperm(M, generator=rng, device=device)[:K] chosen = nz_pairs[perm] i_idx, j_idx = chosen[:, 0], chosen[:, 1] vals = coeff[i_idx, j_idx] neg_vals = -vals coeff[i_idx, j_idx] = neg_vals coeff[j_idx, i_idx] = neg_vals else: # Edge case to force creating one a negative element i, j = 0, 1 if matrix_dim > 1 else (0, 0) coeff[i, j] = -torch.rand(1, device=device, generator=rng) * abs( coefficient_bounds[0] ) coeff[j, i] = coeff[i, j] if not (coeff.diag() < 0).any(): diag_vals = coeff.diag() non_neg = (diag_vals >= 0).nonzero(as_tuple=True)[0] diag_idx = ( int(non_neg[0].item()) if non_neg.numel() > 0 else int( torch.randint(0, matrix_dim, (1,), device=device, generator=rng).item() ) ) if coefficient_bounds[0] < 0: neg_val = coefficient_bounds[0] else: neg_val = ( -torch.empty(1, device=device, dtype=dtype) .uniform_(*coefficient_bounds, generator=rng) .abs() .item() ) coeff[diag_idx, diag_idx] = neg_val if not (coeff == coefficient_bounds[1]).any(): # do not select negative coefficients nz = (coeff > 0).nonzero(as_tuple=False) filtered = [ idx_pair for idx_pair in nz.tolist() if not ( idx_pair[0] == idx_pair[1] and coeff[idx_pair[0], idx_pair[1]].item() == coefficient_bounds[0] ) ] if filtered: chosen = filtered[ int( torch.randint( 0, len(filtered), (1,), device=device, generator=rng, dtype=torch.int64, ).item() ) ] else: chosen = torch.randint( 0, matrix_dim, (1,), device=device, generator=rng ).repeat(2) i_ch, j_ch = chosen coeff[i_ch, j_ch] = coefficient_bounds[1] if i_ch != j_ch: coeff[j_ch, i_ch] = coefficient_bounds[1]
coefficients[:, :, idx] = coeff
# Step 4: Return the dataset. return cls(matrices=coefficients, copy=False)
load
staticmethod
Section titled “
load
staticmethod
”load(file_like: FileLike[bytes]) -> Dataset (qubosolver.types.dataset.Dataset)" href="#qubosolver.Dataset">DatasetLoad a dataset previously saved with save.
Parameters:
-
file_like(FileLike[bytes (external)]) –Source file path or readable binary file object, as produced by
save.
Returns:
-
Dataset–The deserialized dataset, including solutions if they were present when the file was saved.
Raises:
-
ValueError (external)–If the stream is not a qubosolver file.
Example
from pathlib import Path
with Path("dataset.bin").open("rb") as f: dataset = Dataset.load(f)Source code in qubosolver/types/dataset.py
@staticmethoddef load(file_like: FileLike[bytes]) -> Dataset: """Load a dataset previously saved with [`save`][].
Args: file_like: Source file path or readable binary file object, as produced by [`save`][].
Returns: The deserialized dataset, including solutions if they were present when the file was saved.
Raises: ValueError: If the stream is not a qubosolver file.
Example: ```python from pathlib import Path
with Path("dataset.bin").open("rb") as f: dataset = Dataset.load(f) ``` """ with io_utils.open(file_like, "rb") as f: io_utils.load_header(f) # torch.load might consume too much of the src buffer. # Use a dedicated limited buffer buffer = io.BytesIO(io_utils.load_sized_buffer(f)) matrices = torch.load(buffer, weights_only=True) n = io_utils.load(f, ">I") solutions = [Solution.load(f) for _ in range(n)]
return Dataset(matrices, solutions, copy=False)save(file_like: FileLike[bytes]) -> NonePersist this dataset to disk using torch.save (external).
Parameters:
-
file_like(FileLike[bytes (external)]) –Destination file path or writable binary file object.
Example
from pathlib import Path
with Path("dataset.bin").open("wb") as f: dataset.save(f)Source code in qubosolver/types/dataset.py
def save(self, file_like: FileLike[bytes]) -> None: """Persist this dataset to disk using [`torch.save`][].
Args: file_like: Destination file path or writable binary file object.
Example: ```python from pathlib import Path
with Path("dataset.bin").open("wb") as f: dataset.save(f) ``` """ with io_utils.open(file_like, "wb") as f: io_utils.save_header(f) buffer = io.BytesIO() torch.save(self.matrices, buffer) io_utils.save_sized_buffer(f, buffer.getbuffer()) io_utils.save(f, ">I", len(self.solutions)) # Written into the already-open stream *f*, not into `file_like`: # re-opening a path here would truncate everything written above. for s in self.solutions: s.save(f)
Instance
Section titled “
Instance
”Instance(matrix: qubosolver.Matrix
module-attribute (qubosolver.types.linalg.Matrix)" href="../matrix/#qubosolver.Matrix">Matrix | None = None)A single QUBO problem instance.
Wraps a symmetric square matrix and exposes helpers for evaluation, serialization, and introspection. The objective to minimize is:
Parameters:
-
matrix(Matrix | None, default:None) –Symmetric matrix of shape
(n, n). Defaults to an empty(0, 0)zero matrix, which represents a trivial problem with no variables.
Methods:
-
__init_subclass__–Register
clsunder its_tagsoloadcan dispatch to it. -
__len__–Number of binary variables in the QUBO problem (same as
size). -
cost–Compute the QUBO objective for a candidate solution .
-
load– -
save–Serialize this instance to
file_like, tagged with its type.
Attributes:
-
matrix(Matrix) –The QUBO symmetric matrix.
-
negative_bitflip(negative_bitflip.Instance) –View of this instance as a negative-bitflip instance.
-
size(int (external)) –Number of binary variables in the QUBO problem.
-
variable_fixing(variable_fixing.Instance) –View of this instance as a variable-fixing instance.
-
zeroing(zeroing.Instance) –View of this instance as a zeroing instance.
Source code in qubosolver/types/instance.py
def __init__( self, matrix: Matrix | None = None,) -> None: """Wrap `matrix` as a QUBO instance.""" self._matrix: Matrix = matrix if matrix is not None else _matrix_module.zeros(0)
matrix
property
Section titled “
matrix
property
”matrix: qubosolver.Matrix
module-attribute (qubosolver.types.linalg.Matrix)" href="../matrix/#qubosolver.Matrix">MatrixThe QUBO symmetric matrix.
Returns:
-
Matrix–QUBO symmetric matrix of shape
(size, size).
Raises:
-
AssertionError (external)–If the internal tensor is not 2-D or not square.
negative_bitflip
property
Section titled “
negative_bitflip
property
”negative_bitflip: negative_bitflip
property (qubosolver.types.instance.Instance.negative_bitflip)" href="#qubosolver.Instance.negative_bitflip">negative_bitflip.InstanceView of this instance as a negative-bitflip instance.
Convenience property to avoid the boilerplate of
assert isinstance(instance, negative_bitflip.Instance) before calling
a method specific to that subclass. It exists purely to satisfy static
type checkers (mypy) and enable IDE code completion — the runtime
isinstance (external) check and the TypeError (external) below just mirror the guarantee that
the assert (external)
would otherwise provide.
Returns:
-
negative_bitflip.Instance–This instance, narrowed to the negative-bitflip subclass.
Raises:
-
TypeError (external)–If this instance is not a
negative_bitflip.Instance.
size
property
Section titled “
size
property
”size: intNumber of binary variables in the QUBO problem.
variable_fixing
property
Section titled “
variable_fixing
property
”variable_fixing: variable_fixing
property (qubosolver.types.instance.Instance.variable_fixing)" href="#qubosolver.Instance.variable_fixing">variable_fixing.InstanceView of this instance as a variable-fixing instance.
Convenience property to avoid the boilerplate of
assert isinstance(instance, variable_fixing.Instance) before calling
a method specific to that subclass. It exists purely to satisfy static
type checkers (mypy) and enable IDE code completion — the runtime
isinstance (external) check and the TypeError (external) below just mirror the guarantee that
the assert (external)
would otherwise provide.
Returns:
-
variable_fixing.Instance–This instance, narrowed to the variable-fixing subclass.
Raises:
-
TypeError (external)–If this instance is not a
variable_fixing.Instance.
zeroing
property
Section titled “
zeroing
property
”zeroing: zeroing
property (qubosolver.types.instance.Instance.zeroing)" href="#qubosolver.Instance.zeroing">zeroing.InstanceView of this instance as a zeroing instance.
Convenience property to avoid the boilerplate of
assert isinstance(instance, zeroing.Instance) before calling
a method specific to that subclass. It exists purely to satisfy static
type checkers (mypy) and enable IDE code completion — the runtime
isinstance (external) check and the TypeError (external) below just mirror the guarantee that
the assert (external)
would otherwise provide.
Returns:
-
zeroing.Instance–This instance, narrowed to the zeroing subclass.
Raises:
-
TypeError (external)–If this instance is not a
zeroing.Instance.
__init_subclass__
Section titled “
__init_subclass__
”__init_subclass__(**kwargs: object) -> NoneRegister cls under its _tag so load can dispatch to it.
Automatic, so a new Instance subclass never needs to be added to a
separate registry by hand.
Source code in qubosolver/types/instance.py
def __init_subclass__(cls, **kwargs: object) -> None: """Register `cls` under its `_tag` so [`load`][] can dispatch to it.
Automatic, so a new `Instance` subclass never needs to be added to a separate registry by hand. """ super().__init_subclass__(**kwargs) Instance._registry[cls._tag()] = cls
__len__
Section titled “
__len__
”__len__() -> intNumber of binary variables in the QUBO problem (same as size).
Source code in qubosolver/types/instance.py
def __len__(self) -> int: """Number of binary variables in the QUBO problem (same as [`size`][]).""" return self.sizecost(solution: qubosolver.Bitstring
module-attribute (qubosolver.types.linalg.Bitstring)" href="../bitstring/#qubosolver.Bitstring">Bitstring) -> floatCompute the QUBO objective for a candidate solution .
Parameters:
-
solution(Bitstring) –Binary vector of shape
(size,).
Returns:
-
float (external)–Scalar cost value.
Source code in qubosolver/types/instance.py
def cost(self, solution: Bitstring) -> float: """Compute the QUBO objective xTQxx^T Q xxTQx for a candidate solution xxx.
Args: solution: Binary vector xxx of shape ``(size,)``.
Returns: Scalar cost value. """ # Import here to avoid circular imports from qubosolver.utils import _costs
cost = _costs.quadratic_cost(solution, self.matrix) assert type(cost) is float # nosec B101 return cost
load
classmethod
Section titled “
load
classmethod
”load(file_like: FileLike[bytes]) -> SelfDeserialize an Instance previously saved with save.
Called on the base class qubosolver.Instance, it loads any instance with
automatic dispatch.
Called on a subclass (e.g.
variable_fixing.Instance.load(f)),
it additionally requires the loaded Instance to be an instance of that subclass
(raising TypeError (external) otherwise).
Parameters:
-
file_like(FileLike[bytes (external)]) –Source file path or readable binary file object, as produced by
save.
Returns:
-
Self–A new instance of whichever concrete type wrote the tag.
Raises:
-
ValueError (external)–If the stream is not a qubosolver file, or if its type tag is missing or unrecognized.
-
TypeError (external)–If the loaded instance's type is not
clsor a subclass thereof.
Example
from pathlib import Pathfrom qubosolver import Instancefrom qubosolver.transforms import variable_fixing
file = Path("instance.bin")instance = variable_fixing.Instance(Instance())
with file.open("wb") as f: instance.save(f)
# Three ways to load it back, from least to most strict:with file.open("rb") as f: loaded = Instance.load(f) # accepts any Instance subtype # loads, then narrows (fails after loading) loaded = Instance.load(f).variable_fixing loaded = variable_fixing.Instance.load(f) # narrows first (fails before loading)Source code in qubosolver/types/instance.py
@classmethoddef load(cls, file_like: FileLike[bytes]) -> Self: """Deserialize an [`Instance`][] previously saved with [`save`][].
Called on the base class [`qubosolver.Instance`][], it loads any instance with automatic dispatch.
Called on a subclass (e.g. [`variable_fixing.Instance.load(f)`][qubosolver.transforms.variable_fixing.Instance.load]), it additionally requires the loaded `Instance` to be an instance of that subclass (raising [`TypeError`][] otherwise).
Args: file_like: Source file path or readable binary file object, as produced by [`save`][].
Returns: A new instance of whichever concrete type wrote the tag.
Raises: ValueError: If the stream is not a qubosolver file, or if its type tag is missing or unrecognized. TypeError: If the loaded instance's type is not `cls` or a subclass thereof.
Example: ```python from pathlib import Path from qubosolver import Instance from qubosolver.transforms import variable_fixing
file = Path("instance.bin") instance = variable_fixing.Instance(Instance())
with file.open("wb") as f: instance.save(f)
# Three ways to load it back, from least to most strict: with file.open("rb") as f: loaded = Instance.load(f) # accepts any Instance subtype # loads, then narrows (fails after loading) loaded = Instance.load(f).variable_fixing loaded = variable_fixing.Instance.load(f) # narrows first (fails before loading) ``` """ with io_utils.open(file_like, "rb") as f: io_utils.load_header(f) tag = io_utils.load_string(f) target_cls = Instance._registry.get(tag) if target_cls is None: raise ValueError(f"Cannot load Instance: unrecognized type tag {tag!r}.") instance = target_cls._read_body(f) if not isinstance(instance, cls): raise TypeError( f"Cannot load {cls.__module__}.{cls.__qualname__}: " f"stream contains a {type(instance).__module__}.{type(instance).__qualname__}." ) return instancesave(file_like: FileLike[bytes]) -> NoneSerialize this instance to file_like, tagged with its type.
Parameters:
-
file_like(FileLike[bytes (external)]) –Destination — a file path (
str(external) oros.PathLike(external)), or a binary-writabletyping.IO(external) stream.
Example
from pathlib import Path
with Path("instance.bin").open("wb") as f: instance.save(f)Source code in qubosolver/types/instance.py
def save(self, file_like: FileLike[bytes]) -> None: """Serialize this instance to ``file_like``, tagged with its type.
Args: file_like: Destination — a file path ([`str`][] or [`os.PathLike`][]), or a binary-writable [`typing.IO`][] stream.
Example: ```python from pathlib import Path
with Path("instance.bin").open("wb") as f: instance.save(f) ``` """ with io_utils.open(file_like, "wb") as f: io_utils.save_header(f) io_utils.save_string(f, self._tag()) self._write_body(f)
LocalEmulator
Section titled “
LocalEmulator
”LocalEmulator(backend_type: type[EmulatorBackend] = AutoLocalEmulatorBackend (qubosolver.types.backends.AutoLocalEmulatorBackend)" href="#qubosolver.AutoLocalEmulatorBackend">AutoLocalEmulatorBackend, **kwargs: Any)Local quantum emulator with automatic backend selection.
This class wraps qoolqit.execution.LocalEmulator (external) and automatically selects
the optimal local backend based on the quantum register size.
It provides the same interface as the base qoolqit.execution.LocalEmulator (external) but with
improved performance through intelligent backend selection.
The optimal backend selection follows these guidelines:
- Small problems (< 15 qubits):
QutipBackendV2(external) - Medium problems (15-25 qubits):
SVBackend(external) - Large problems (≥ 26 qubits):
MPSBackend(external)
Parameters:
-
backend_type(type (external)[EmulatorBackend], default:AutoLocalEmulatorBackend) –Backend type to use.
-
**kwargs(Any (external), default:{}) –Additional keyword arguments passed to the base
qoolqit.execution.LocalEmulator(external).
Example
from qubosolver import LocalEmulatoremulator = LocalEmulator(num_shots=1000)# Backend will be automatically selected based on problem sizeMethods:
-
run–Run the quantum program on the selected backend.
Source code in qubosolver/types/backends.py
def __init__( self, backend_type: type[EmulatorBackend] = AutoLocalEmulatorBackend, **kwargs: Any, # noqa: ANN401 (forwarded to qoolqit.execution.LocalEmulator)) -> None: """Create a local emulator backend of the given `backend_type`.""" super().__init__(backend_type=backend_type, **kwargs)run(program: qoolqit.QuantumProgram, *args: Any, **kwargs: Any) -> AnyRun the quantum program on the selected backend.
Parameters:
-
program(qoolqit (external).QuantumProgram (external)) –The quantum program to execute.
-
*args(Any (external), default:()) –Additional positional arguments from
qoolqit.execution.LocalEmulator(external). -
**kwargs(Any (external), default:{}) –Additional keyword arguments from
qoolqit.execution.LocalEmulator(external).
Returns:
-
Any (external)–The execution results from the local backend.
Source code in qubosolver/types/backends.py
def run( self, program: qoolqit.QuantumProgram, *args: Any, # noqa: ANN401 (forwarded to qoolqit.execution.LocalEmulator.run) **kwargs: Any, # noqa: ANN401 (forwarded to qoolqit.execution.LocalEmulator.run)) -> Any: # noqa: ANN401 (return type mirrors the selected backend's run() result) """Run the quantum program on the selected backend.
Args: program: The quantum program to execute. *args: Additional positional arguments from [`qoolqit.execution.LocalEmulator`][]. **kwargs: Additional keyword arguments from [`qoolqit.execution.LocalEmulator`][].
Returns: The execution results from the local backend. """ _warn_suboptimal_backend(self._backend_type, program.register.n_qubits) return super().run(program, *args, **kwargs)
RemoteEmulator
Section titled “
RemoteEmulator
”RemoteEmulator(backend_type: type[RemoteEmulatorBackend] = RemoteEmuFreeBackend, **kwargs: Any)Remote quantum emulator with automatic backend selection.
This class wraps qoolqit.execution.RemoteEmulator (external) and provides backend selection
recommendations based on quantum register size and tractability constraints.
Backend selection guidelines based on computational tractability:
- Small problems (< 15 qubits):
RemoteEmuFreeBackend(external) (default) - Medium problems (15-25 qubits):
RemoteSVBackend(external) - Large problems (≥ 26 qubits):
RemoteMPSBackend(external)
Note
RemoteEmuFreeBackend becomes intractable beyond ~15 qubits, similar to its
local counterpart QutipBackendV2. For larger problems, RemoteSVBackend and
RemoteMPSBackend are necessary. Fees may apply for remote execution.
Parameters:
-
backend_type(type (external)[RemoteEmulatorBackend], default:RemoteEmuFreeBackend) –Backend type to use.
-
**kwargs(Any (external), default:{}) –Additional keyword arguments passed to the base
qoolqit.execution.RemoteEmulator(external).
Example
from qubosolver import RemoteEmulatorfrom pasqal_cloud import PasqalCloudConnectionconnection = PasqalCloudConnection(username="user", password="pass", project_id="project")emulator = RemoteEmulator(connection=connection, num_shots=1000)# Uses RemoteEmuFreeBackend by defaultMethods:
-
run–Run the quantum program on the selected backend.
Source code in qubosolver/types/backends.py
def __init__( self, backend_type: type[RemoteEmulatorBackend] = RemoteEmuFreeBackend, **kwargs: Any, # noqa: ANN401 (forwarded to qoolqit.execution.RemoteEmulator)) -> None: """Create a remote emulator backend of the given `backend_type`.""" super().__init__(backend_type=backend_type, **kwargs)run(program: qoolqit.QuantumProgram, *args: Any, **kwargs: Any) -> AnyRun the quantum program on the selected backend.
Parameters:
-
program(qoolqit (external).QuantumProgram (external)) –The quantum program to execute.
-
*args(Any (external), default:()) –Additional positional arguments for
qoolqit.execution.RemoteEmulator(external). -
**kwargs(Any (external), default:{}) –Additional keyword arguments for
qoolqit.execution.RemoteEmulator(external).
Returns:
-
Any (external)–The execution results from the remote backend.
Source code in qubosolver/types/backends.py
def run( self, program: qoolqit.QuantumProgram, *args: Any, # noqa: ANN401 (forwarded to qoolqit.execution.RemoteEmulator.run) **kwargs: Any, # noqa: ANN401 (forwarded to qoolqit.execution.RemoteEmulator.run)) -> Any: # noqa: ANN401 (return type mirrors the selected backend's run() result) """Run the quantum program on the selected backend.
Args: program: The quantum program to execute. *args: Additional positional arguments for [`qoolqit.execution.RemoteEmulator`][]. **kwargs: Additional keyword arguments for [`qoolqit.execution.RemoteEmulator`][].
Returns: The execution results from the remote backend. """ _warn_suboptimal_backend(self._backend_type, program.register.n_qubits) return super().run(program, *args, **kwargs)
Solution
dataclass
Section titled “
Solution
dataclass
”Solution(bitstrings: qubosolver.Bitstrings
module-attribute (qubosolver.types.linalg.Bitstrings)" href="../bitstrings/#qubosolver.Bitstrings">Bitstrings = qubosolver.bitstrings (qubosolver.types.bitstrings)" href="../bitstrings/#qubosolver.bitstrings">_bitstrings. zeros_field (qubosolver.types.bitstrings.zeros_field)" href="../bitstrings/#qubosolver.bitstrings.zeros_field">zeros_field(0, 0), costs: qubosolver.Vector
module-attribute (qubosolver.types.linalg.Vector)" href="../vector/#qubosolver.Vector">Vector = qubosolver.vector (qubosolver.types.vector)" href="../vector/#qubosolver.vector">vector. zeros_field (qubosolver.types.vector.zeros_field)" href="../vector/#qubosolver.vector.zeros_field">zeros_field(0), counts: qubosolver.Vectori
module-attribute (qubosolver.types.linalg.Vectori)" href="../vectori/#qubosolver.Vectori">Vectori = qubosolver.vectori (qubosolver.types.vectori)" href="../vectori/#qubosolver.vectori">vectori. zeros_field (qubosolver.types.vectori.zeros_field)" href="../vectori/#qubosolver.vectori.zeros_field">zeros_field(0), probabilities: qubosolver.Vector
module-attribute (qubosolver.types.linalg.Vector)" href="../vector/#qubosolver.Vector">Vector = qubosolver.vector (qubosolver.types.vector)" href="../vector/#qubosolver.vector">vector. zeros_field (qubosolver.types.vector.zeros_field)" href="../vector/#qubosolver.vector.zeros_field">zeros_field(0))A collection of candidate solutions for a QUBO problem.
Stores all bitstrings returned by a solver together with their associated metadata (costs, sample counts, probabilities).
Attributes:
-
bitstrings(Bitstrings) –int8tensor of shape(num_solutions, n)containing candidate binary vectors (values in ). -
costs(Vector) –Float tensor of shape
(num_solutions,)with the QUBO objective for each bitstring. -
counts(Vectori) –int64tensor of shape(num_solutions,)with the number of times each bitstring was sampled. -
probabilities(Vector) –Float tensor of shape
(num_solutions,)with the empirical sampling probability of each bitstring.
Methods:
-
__getitem__–Return the candidate at position
idxas aCandidate. -
__iter__–Iterate over all candidates in index order, yielding
Candidateobjects. -
__len__–Return the number of candidate solutions (
num_solutions). -
check_consistency–Check internal consistency of this solution against a QUBO instance.
-
concat–Concatenate several solutions into a new one, without deduplication.
-
deduplicate–Collapse duplicate bitstrings in-place, summing their counts.
-
from_results–Build a
Solutionfrom Pulser quantum-simulation results. -
load– -
save–Serialize this solution to
file_likeusingtorch.save. -
truncate–Keep only the first
kcandidates in-place. -
zeros–Build a single all-zero candidate solution of zero cost.
num_variables
property
Section titled “
num_variables
property
”num_variables: intReturn the number of variables per bitstring.
__getitem__
Section titled “
__getitem__
”__getitem__(idx: int) -> Candidate
dataclass (qubosolver.types.solution.Candidate)" href="#qubosolver.Candidate">CandidateReturn the candidate at position idx as a Candidate.
Parameters:
-
idx(int (external)) –Zero-based index into the
num_solutionsaxis.
Returns:
-
Candidate–Snapshot of the candidate at
idx.
Source code in qubosolver/types/solution.py
def __getitem__(self, idx: int) -> Candidate: """Return the candidate at position `idx` as a [`Candidate`][].
Args: idx: Zero-based index into the ``num_solutions`` axis.
Returns: Snapshot of the candidate at `idx`. """ candidate = Candidate(self.bitstrings[idx]) candidate.count = int(self.counts[idx].item()) if self.costs.numel() > 0: candidate.cost = self.costs[idx].item() if self.probabilities.numel() > 0: candidate.probability = self.probabilities[idx].item()
return candidate
__iter__
Section titled “
__iter__
”__iter__() -> Iterator[ Candidate
dataclass (qubosolver.types.solution.Candidate)" href="#qubosolver.Candidate">Candidate]Iterate over all candidates in index order, yielding Candidate objects.
Yields:
-
Candidate–Same as
__getitem__for each index0 … len(self)-1.
Source code in qubosolver/types/solution.py
def __iter__(self) -> Iterator[Candidate]: """Iterate over all candidates in index order, yielding [`Candidate`][] objects.
Yields: Same as [`__getitem__`][] for each index ``0 … len(self)-1``. """ for i in range(len(self)): yield self[i]
__len__
Section titled “
__len__
”__len__() -> intReturn the number of candidate solutions (num_solutions).
Source code in qubosolver/types/solution.py
def __len__(self) -> int: """Return the number of candidate solutions (``num_solutions``).""" return self.bitstrings.shape[0]
check_consistency
Section titled “
check_consistency
”check_consistency(*, instance: Instance (qubosolver.types.instance.Instance)" href="#qubosolver.Instance">Instance | None = None, throw: bool = False, full: bool = True, rtol: float = 1e-05, atol: float = 1e-08) -> boolCheck internal consistency of this solution against a QUBO instance.
Recomputes costs from bitstrings and instance.matrix and checks
for duplicate rows, so this can be slow on large solutions — prefer
calling it in tests / debugging rather than on every solver result.
Pass full=False to restrict this to the O(1) shape checks when
calling on a hot path.
Verifies that:
bitstringshasinstance.sizecolumns (wheninstanceis given; otherwise this check is skipped).costs,counts, andprobabilitieseach have exactlylen(self)elements (i.e. none of them is empty).costsmatches for every bitstring, computed frominstance.matrix(wheninstanceis given; otherwise this check is skipped).costsis sorted in non-decreasing order.probabilitiesmatchescountsnormalized by their sum.countsare strictly positive integers.bitstringscontains no duplicate rows.bitstringsentries are all0or1.
Parameters:
-
instance(Instance | None, default:None) –The QUBO instance this solution is expected to solve.
-
throw(bool (external), default:False) –When
True, raise anAssertionErroron the first failing check instead of returningFalse. -
full(bool (external), default:True) –When
True(default), run every check listed above. WhenFalse, only check tensor shapes (constant-time) and skip the rest (cost recomputation, sortedness, duplicate/binary/count/probability checks), which scale with the number of solutions. -
rtol(float (external), default:1e-05) –Relative tolerance forwarded to
torch.allclosewhen comparingcostsagainst andprobabilitiesagainst normalizedcounts. -
atol(float (external), default:1e-08) –Absolute tolerance forwarded to
torch.allclosewhen comparingcostsagainst andprobabilitiesagainst normalizedcounts.
Returns:
-
bool (external)–Trueif all checks pass,Falseotherwise (unlessthrowisTrue, in which case an exception is raised).
Source code in qubosolver/types/solution.py
def check_consistency( self, *, instance: Instance | None = None, throw: bool = False, full: bool = True, rtol: float = 1e-5, atol: float = 1e-8,) -> bool: """Check internal consistency of this solution against a QUBO instance.
Recomputes costs from `bitstrings` and `instance.matrix` and checks for duplicate rows, so this can be slow on large solutions — prefer calling it in tests / debugging rather than on every solver result. Pass ``full=False`` to restrict this to the O(1) shape checks when calling on a hot path.
Verifies that:
* `bitstrings` has ``instance.size`` columns (when `instance` is given; otherwise this check is skipped). * `costs`, `counts`, and `probabilities` each have exactly `len(self)` elements (i.e. none of them is empty). * `costs` matches xTQxx^T Q xxTQx for every bitstring, computed from ``instance.matrix`` (when `instance` is given; otherwise this check is skipped). * `costs` is sorted in non-decreasing order. * `probabilities` matches `counts` normalized by their sum. * `counts` are strictly positive integers. * `bitstrings` contains no duplicate rows. * `bitstrings` entries are all ``0`` or ``1``.
Args: instance: The QUBO instance this solution is expected to solve. throw: When ``True``, raise an `AssertionError` on the first failing check instead of returning ``False``. full: When ``True`` (default), run every check listed above. When ``False``, only check tensor shapes (constant-time) and skip the rest (cost recomputation, sortedness, duplicate/binary/count/probability checks), which scale with the number of solutions. rtol: Relative tolerance forwarded to `torch.allclose` when comparing `costs` against xTQxx^T Q xxTQx and `probabilities` against normalized `counts`. atol: Absolute tolerance forwarded to `torch.allclose` when comparing `costs` against xTQxx^T Q xxTQx and `probabilities` against normalized `counts`.
Returns: ``True`` if all checks pass, ``False`` otherwise (unless ``throw`` is ``True``, in which case an exception is raised). """ num_solutions = len(self) bitstring_size = instance.size if instance is not None else self.bitstrings.shape[1]
def check(condition: bool, message: str) -> bool: if condition: return True logger.warning(message) if throw: raise AssertionError(message) return False
expected_shapes = ( ("bitstrings", self.bitstrings, (num_solutions, bitstring_size)), ("costs", self.costs, (num_solutions,)), ("counts", self.counts, (num_solutions,)), ("probabilities", self.probabilities, (num_solutions,)), )
valid = True for name, tensor, expected_shape in expected_shapes: valid &= check( tuple(tensor.shape) == expected_shape, f"{name} has shape {tuple(tensor.shape)}, expected {expected_shape}", )
if not valid: return False
if not full: return valid
from qubosolver.utils import _costs
if instance is not None: expected_costs = _costs.batched_quadratic_cost( self.bitstrings.to(instance.matrix.dtype), instance.matrix )
valid &= check( torch.allclose( self.costs, expected_costs.to(self.costs.dtype), rtol=rtol, atol=atol ), f"costs {self.costs.tolist()} does not match x^T Q x " f"{expected_costs.tolist()} for the corresponding bitstrings", )
valid &= check( bool(torch.all(self.costs[:-1] <= self.costs[1:])), f"costs {self.costs.tolist()} is not sorted in non-decreasing order", )
if num_solutions == 0: return valid
# torch.unique(dim=0) rejects zero-width tensors outright ("0 sized # dimensions... aren't selected"), but a width-0 bitstring is just the # single empty tuple repeated: there is exactly one distinct row. num_unique_bitstrings = ( 1 if self.bitstrings.shape[1] == 0 else self.bitstrings.unique(dim=0).shape[0] ) valid &= check( num_unique_bitstrings == num_solutions, f"bitstrings contains {num_solutions - num_unique_bitstrings} duplicate row(s)", )
valid &= check( bool(torch.all((self.bitstrings == 0) | (self.bitstrings == 1))), f"bitstrings {self.bitstrings.tolist()} contains entries other than 0 or 1", )
valid &= check( bool(torch.all(self.counts == self.counts.round())), f"counts {self.counts.tolist()} contains non-integer values", ) valid &= check( bool(torch.all(self.counts > 0)), f"counts {self.counts.tolist()} contains non-positive entries", )
expected_probabilities = self.counts / self.counts.sum() valid &= check( torch.allclose( self.probabilities, expected_probabilities.to(self.probabilities.dtype), rtol=rtol, atol=atol, ), f"probabilities {self.probabilities.tolist()} does not match counts " f"{self.counts.tolist()} normalized by their sum", )
return valid
concat
staticmethod
Section titled “
concat
staticmethod
”concat(solutions: Iterable[ Solution
dataclass (qubosolver.types.solution.Solution)" href="#qubosolver.Solution">Solution], *, unit_counts: bool = False) -> Solution
dataclass (qubosolver.types.solution.Solution)" href="#qubosolver.Solution">SolutionConcatenate several solutions into a new one, without deduplication.
Concatenates bitstrings, costs, counts, and
probabilities from every solution in solutions. Duplicate
bitstrings, if any, are kept as separate rows — call
deduplicate on the result to collapse them.
Parameters:
-
solutions(Iterable (external)[Solution]) –Solutions to concatenate. Empty solutions (no bitstrings) are skipped. Each remaining solution must have
costs,counts, andprobabilitiespopulated (checked viacheck_consistency(full=False), which raisesAssertionErrorotherwise). -
unit_counts(bool (external), default:False) –When
True, setcountsto1for every concatenated candidate instead of concatenating their original counts — useful when each candidate should count as a single vote once merged.probabilitiesare always recomputed from the resultingcountsrather than concatenated, so they still sum to 1.
Returns:
-
Solution–
Example
# Chain with deduplicate() to mergemerged = Solution.concat([a, b]).deduplicate()
# Alternative merge with unit_countsmerged = Solution.concat([a, b], unit_counts=True).deduplicate()Source code in qubosolver/types/solution.py
@staticmethoddef concat(solutions: Iterable[Solution], *, unit_counts: bool = False) -> Solution: """Concatenate several solutions into a new one, without deduplication.
Concatenates `bitstrings`, `costs`, `counts`, and `probabilities` from every solution in `solutions`. Duplicate bitstrings, if any, are kept as separate rows — call `deduplicate` on the result to collapse them.
Args: solutions: Solutions to concatenate. Empty solutions (no bitstrings) are skipped. Each remaining solution must have `costs`, `counts`, and `probabilities` populated (checked via [`check_consistency(full=False)`][check_consistency], which raises `AssertionError` otherwise). unit_counts: When ``True``, set `counts` to ``1`` for every concatenated candidate instead of concatenating their original counts — useful when each candidate should count as a single vote once merged. `probabilities` are always recomputed from the resulting `counts` rather than concatenated, so they still sum to 1.
Returns: A new [`Solution`][] containing every candidate from every solution, sorted by ascending cost with `probabilities` recomputed from `counts`, or an empty [`Solution`][] if `solutions` is empty or contains only empty solutions.
Example: ```python # Chain with deduplicate() to merge merged = Solution.concat([a, b]).deduplicate()
# Alternative merge with unit_counts merged = Solution.concat([a, b], unit_counts=True).deduplicate() ``` """ non_empty = [solution for solution in solutions if solution] if not non_empty: return Solution()
for s in non_empty: s.check_consistency(instance=None, throw=True, full=False)
bitstrings = torch.cat([s.bitstrings for s in non_empty], dim=0) if unit_counts: counts = vectori.zeros(bitstrings.shape[0]).fill_(1) else: counts = torch.cat([s.counts for s in non_empty], dim=0)
return ( Solution( bitstrings=bitstrings, costs=torch.cat([s.costs for s in non_empty], dim=0), counts=counts, ) ._sort_by_cost() ._compute_probabilities() )
deduplicate
Section titled “
deduplicate
”deduplicate(update: bool = True) -> SelfCollapse duplicate bitstrings in-place, summing their counts.
Rows sharing the same bitstring are merged into a single row:
counts are summed and the minimum cost is kept.
Parameters:
-
update(bool (external), default:True) –When
True(default), sort the result by cost and recomputeprobabilitiesfrom the new counts before returning. PassFalseto skip both.
Returns:
-
Self–The same
Solutioninstance, allowing method chaining.
Raises:
-
AssertionError (external)–If this solution is non-empty and
costs,counts, orprobabilitiesis not populated (checked viacheck_consistency(full=False)).
Warning
With update=False, probabilities is left stale and
bitstrings unsorted by cost. Most (if not all) algorithms
expect a consistent solution (see
check_consistency),
so only pass update=False if you will restore consistency
yourself before the solution is used further.
Note
Rows sharing a bitstring are expected to also share the same
cost, since they represent the same candidate — but this
is not checked. The minimum of their costs is kept as a
conservative choice in case that expectation doesn't hold.
Use solution.check_consistency(instance=instance, full=True)
(see check_consistency)
to check the result against that instance — this check is
expensive, so prefer it in tests / debugging rather than
on every call.
Source code in qubosolver/types/solution.py
def deduplicate(self, update: bool = True) -> Self: """Collapse duplicate bitstrings in-place, summing their counts.
Rows sharing the same bitstring are merged into a single row: `counts` are summed and the minimum `cost` is kept.
Args: update: When ``True`` (default), sort the result by cost and recompute `probabilities` from the new counts before returning. Pass ``False`` to skip both.
Returns: The same [`Solution`][] instance, allowing method chaining.
Raises: AssertionError: If this solution is non-empty and `costs`, `counts`, or `probabilities` is not populated (checked via [`check_consistency(full=False)`][check_consistency]).
Warning: With ``update=False``, `probabilities` is left stale and `bitstrings` unsorted by cost. Most (if not all) algorithms expect a consistent solution (see [`check_consistency`][qubosolver.types.solution.Solution.check_consistency]), so only pass ``update=False`` if you will restore consistency yourself before the solution is used further.
Note: Rows sharing a bitstring are expected to also share the same `cost`, since they represent the same candidate — but this is not checked. The minimum of their `costs` is kept as a conservative choice in case that expectation doesn't hold. Use ``solution.check_consistency(instance=instance, full=True)`` (see [`check_consistency`][qubosolver.types.solution.Solution.check_consistency]) to check the result against that instance — this check is expensive, so prefer it in tests / debugging rather than on every call. """ if not self: return self
self.check_consistency(throw=True, full=False)
# torch.unique(dim=0) rejects zero-width tensors outright ("0 sized # dimensions... aren't selected"), but a width-0 bitstring is just the # single empty tuple repeated: every row collapses to that one row. if self.bitstrings.shape[1] == 0: unique_bitstrings = self.bitstrings[:1] inverse = vectori.zeros(len(self)) else: unique_bitstrings, inverse = self.bitstrings.unique(dim=0, return_inverse=True) n = unique_bitstrings.shape[0] self.bitstrings = unique_bitstrings
self.counts = vectori.zeros(n).scatter_reduce( dim=0, index=inverse, src=self.counts, reduce="sum", include_self=False )
self.costs = vector.zeros(n).scatter_reduce( dim=0, index=inverse, src=self.costs, reduce="amin", include_self=False )
if update: self._sort_by_cost()._compute_probabilities()
return self
from_results
staticmethod
Section titled “
from_results
staticmethod
”from_results(results: Results, instance: Instance (qubosolver.types.instance.Instance)" href="#qubosolver.Instance">Instance) -> Solution
dataclass (qubosolver.types.solution.Solution)" href="#qubosolver.Solution">SolutionBuild a Solution from Pulser quantum-simulation results.
Parameters:
-
results(Results (external)) –Pulser results object whose
final_bitstringsattribute is adict[str, int]. -
instance(Instance) –The QUBO instance whose matrix is used to compute
costs.
Returns:
-
Solution–A new solution with all four fields populated, sorted by ascending cost.
Note
When final_bitstrings is empty (no samples recorded),
bitstrings is set to a (0, 0) tensor rather than the
default shape inferred from an empty list, avoiding shape ambiguity.
Source code in qubosolver/types/solution.py
@staticmethoddef from_results(results: Results, instance: Instance) -> Solution: """Build a [`Solution`][] from Pulser quantum-simulation results.
Args: results: Pulser results object whose ``final_bitstrings`` attribute is a ``dict[str, int]``. instance: The QUBO instance whose matrix is used to compute `costs`.
Returns: A new solution with all four fields populated, sorted by ascending cost.
Note: When ``final_bitstrings`` is empty (no samples recorded), ``bitstrings`` is set to a ``(0, 0)`` tensor rather than the default shape inferred from an empty list, avoiding shape ambiguity. """ counter = results.final_bitstrings bitstrings = _bitstrings.tensor([list(map(int, list(b))) for b in list(counter.keys())]) if bitstrings.numel() == 0: bitstrings = _bitstrings.zeros(0, 0) counts = vectori.tensor(list(map(int, list(counter.values()))))
solution = Solution(bitstrings=bitstrings, counts=counts)._update(instance)
return solution
load
staticmethod
Section titled “
load
staticmethod
”load(file_like: FileLike[bytes]) -> Solution
dataclass (qubosolver.types.solution.Solution)" href="#qubosolver.Solution">SolutionDeserialize a Solution previously saved with save.
Parameters:
-
file_like(FileLike[bytes (external)]) –Source — a file path (
stroros.PathLike), or a binary-readabletyping.IOstream. Must contain data written bysave.
Returns:
-
Solution–A new solution with the tensor fields deserialized from
file_like.
Raises:
-
ValueError (external)–If the stream is not a qubosolver file.
Note
torch.load (external) is called with weights_only=True to prevent
arbitrary code execution from untrusted checkpoint files.
Example
from pathlib import Path
with Path("solution.bin").open("rb") as f: solution = Solution.load(f)Source code in qubosolver/types/solution.py
@staticmethoddef load(file_like: FileLike[bytes]) -> Solution: """Deserialize a [`Solution`][] previously saved with [`save`][].
Args: file_like: Source — a file path (`str` or `os.PathLike`), or a binary-readable `typing.IO` stream. Must contain data written by [`save`][].
Returns: A new solution with the tensor fields deserialized from `file_like`.
Raises: ValueError: If the stream is not a qubosolver file.
Note: [`torch.load`][] is called with `weights_only=True` to prevent arbitrary code execution from untrusted checkpoint files.
Example: ```python from pathlib import Path
with Path("solution.bin").open("rb") as f: solution = Solution.load(f) ``` """ with io_utils.open(file_like, "rb") as f: io_utils.load_header(f) # torch.load might consume too much of the src buffer. # Use a dedicated limited buffer buffer = io.BytesIO(io_utils.load_sized_buffer(f)) data = torch.load(buffer, weights_only=True)
return Solution( bitstrings=data["bitstrings"], costs=data["costs"], counts=data["counts"], probabilities=data["probabilities"], )save(file_like: FileLike[bytes]) -> NoneSerialize this solution to file_like using torch.save.
Parameters:
-
file_like(FileLike[bytes (external)]) –Destination — a file path (
stroros.PathLike), or a binary-writabletyping.IOstream.
Example
from pathlib import Path
with Path("solution.bin").open("wb") as f: solution.save(f)Source code in qubosolver/types/solution.py
def save(self, file_like: FileLike[bytes]) -> None: """Serialize this solution to `file_like` using `torch.save`.
Args: file_like: Destination — a file path (`str` or `os.PathLike`), or a binary-writable `typing.IO` stream.
Example: ```python from pathlib import Path
with Path("solution.bin").open("wb") as f: solution.save(f) ``` """ with io_utils.open(file_like, "wb") as f: io_utils.save_header(f) buffer = io.BytesIO() torch.save( { "bitstrings": self.bitstrings, "costs": self.costs, "counts": self.counts, "probabilities": self.probabilities, }, buffer, ) io_utils.save_sized_buffer(f, buffer.getbuffer())
truncate
Section titled “
truncate
”truncate(k: int) -> SelfKeep only the first k candidates in-place.
Recomputes probabilities so they still sum to 1. Does not sort
first; assumes solution is already sorted by ascending cost.
Example
# Keep only the best candidate (lowest cost).solution.truncate(1)Parameters:
-
k(int (external)) –Number of candidates to keep. When
k >= len(self), this is a no-op.
Returns:
-
Self–The same
Solutioninstance, allowing method chaining.
Raises:
-
AssertionError (external)–If this solution is non-empty and
costs,counts, orprobabilitiesis not populated (checked viacheck_consistency(full=False)).
Source code in qubosolver/types/solution.py
def truncate(self, k: int) -> Self: """Keep only the first `k` candidates in-place.
Recomputes `probabilities` so they still sum to 1. Does not sort first; assumes `solution` is already sorted by ascending cost.
Example: ```python # Keep only the best candidate (lowest cost). solution.truncate(1) ```
Args: k: Number of candidates to keep. When ``k >= len(self)``, this is a no-op.
Returns: The same [`Solution`][] instance, allowing method chaining.
Raises: AssertionError: If this solution is non-empty and `costs`, `counts`, or `probabilities` is not populated (checked via [`check_consistency(full=False)`][check_consistency]). """ self.check_consistency(throw=True, full=False)
self.bitstrings = self.bitstrings[:k] self.costs = self.costs[:k] self.counts = self.counts[:k] self._compute_probabilities()
return self
zeros
staticmethod
Section titled “
zeros
staticmethod
”zeros(length: int, *, count: int = 1) -> Solution
dataclass (qubosolver.types.solution.Solution)" href="#qubosolver.Solution">SolutionBuild a single all-zero candidate solution of zero cost.
Parameters:
-
length(int (external)) –Number of variables in the bitstring.
-
count(int (external), default:1) –Number of samples to attribute to this candidate.
Returns:
-
Solution–A
Solutionholding one all-zero candidate.
Source code in qubosolver/types/solution.py
@staticmethoddef zeros(length: int, *, count: int = 1) -> Solution: """Build a single all-zero candidate solution of zero cost.
Args: length: Number of variables in the bitstring. count: Number of samples to attribute to this candidate.
Returns: A `Solution` holding one all-zero candidate. """ return Solution( bitstrings=_bitstrings.zeros(1, length), counts=vectori.tensor([count]), probabilities=vector.tensor([1.0]), costs=vector.tensor([0.0]), )
extract_qubo
Section titled “
extract_qubo
”extract_qubo(register: qoolqit.Register, drive: qoolqit.Drive) -> Instance (qubosolver.types.instance.Instance)" href="#qubosolver.Instance">InstanceReconstruct the QUBO encoded by a register's geometry and a drive's final detuning.
Parameters:
-
register(qoolqit (external).Register (external)) –The physical register whose geometry encodes the QUBO's off-diagonal coefficients.
-
drive(qoolqit (external).Drive (external)) –The drive whose final detuning (and, if present, DMM) encodes the QUBO's diagonal coefficients.
Returns:
-
Instance–The reconstructed QUBO instance.
Source code in qubosolver/utils/quantum.py
def extract_qubo(register: qoolqit.Register, drive: qoolqit.Drive) -> Instance: """Reconstruct the QUBO encoded by a register's geometry and a drive's final detuning.
Args: register: The physical register whose geometry encodes the QUBO's off-diagonal coefficients. drive: The drive whose final detuning (and, if present, DMM) encodes the QUBO's diagonal coefficients.
Returns: The reconstructed QUBO instance. """ Q = matrix.as_tensor(register.interaction_matrix())
delta = _detuning(drive, drive.duration, n=len(register), qubit_ids=register.qubits_ids) Q += torch.diag(-2 * delta)
return Instance(Q)
torch_rng
Section titled “
torch_rng
”torch_rng(seed: int | None = None) -> torch.GeneratorCreates a torch.Generator (external) compatible with qubosolver's torch typing.
Parameters:
-
seed(int (external) | None, default:None) –Optional seed for reproducibility. If
None, the generator is left with its default (non-deterministic) state.
Returns:
-
torch (external).Generator (external)–A
torch.Generatorinstance, optionally seeded.
Source code in qubosolver/types/random.py
def torch_rng(seed: int | None = None) -> torch.Generator: """Creates a [`torch.Generator`][] compatible with [`qubosolver`][]'s torch typing.
Args: seed: Optional seed for reproducibility. If ``None``, the generator is left with its default (non-deterministic) state.
Returns: A `torch.Generator` instance, optionally seeded. """ generator = torch.Generator(linalg.device()) if seed is None: return generator return generator.manual_seed(seed)