Skip to content

qubosolver

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 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:

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:

Note

This class acts as a factory and never instantiates itself. The __new__ method directly returns instances of the selected backend type.

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:

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

Candidate(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 \(\\{0, 1\\}^n\) (int8).

  • cost (float) –

    Objective value \(x^T Q x\). Defaults to \(+\\infty\) when constructed without one.

  • count (int) –

    Number of times this bitstring was sampled. Defaults to 0 when constructed without one.

  • probability (float) –

    Sampling probability of this bitstring. Defaults to 0.0 when constructed without one.

string property

string: str

The bitstring represented as a plain "0"/"1" character string.

Dataset

Dataset(matrices: torch.Tensor, solutions: Sequence[Solution] = (), *, copy: bool = True)

A dataset of QUBO instances.

Each instance is represented by a square matrix \(Q\) such that the optimization objective is \(x^T Q x\), where \(x\) is a binary vector.

Parameters:

  • matrices (torch.Tensor) –

    Matrices of shape (size, size, num_instances). matrices[:, :, i] is the i-th QUBO matrix.

  • solutions (Sequence[Solution], default: () ) –

    Ground-truth solutions, one per instance. Pass an empty list (default) when solutions are unknown.

  • copy (bool, default: True ) –

    Whether to deep-copy matrices and solutions on construction. Pass False to 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.

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__

__getitem__(idx: int) -> tuple[Instance, Solution]

Return the matrix and solution for instance idx.

Parameters:

  • idx (int) –

    Zero-based index of the instance.

Returns:

  • tuple[Instance, Solution] –

    A symmetric matrix and a solution (Q, solution). When no solutions were provided, solution is an empty Solution.

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__

__iter__() -> Iterator[tuple[Instance, Solution]]

Iterate over all (matrix, solution) pairs in order.

Yields:

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__

__len__() -> int

Return 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

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

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.

Parameters:

  • n_matrices (int) –

    Number of QUBO matrices to generate for each density.

  • matrix_dim (int) –

    The dimension of each QUBO matrix.

  • densities (Sequence[float], default: (0.5,) ) –

    List of densities (ratio of non-zero elements).

  • coefficient_bounds (tuple[float, float], default: (-10.0, 10.0) ) –

    Range (min, max) of random values for the coefficients.

  • dtype (torch.dtype | None, default: None ) –

    Data type for the coefficient matrices.

  • rng (torch.Generator | None, default: None ) –

    Random number generator controlling the sampling.

  • negative_offdiag_rate (float, 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
@classmethod
def 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

load(file_like: FileLike[bytes]) -> Dataset

Load a dataset previously saved with save.

Parameters:

  • file_like (FileLike[bytes]) –

    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 –

    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
@staticmethod
def 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

save(file_like: FileLike[bytes]) -> None

Persist this dataset to disk using torch.save.

Parameters:

  • file_like (FileLike[bytes]) –

    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

Instance(matrix: Matrix | None = None)

A single QUBO problem instance.

Wraps a symmetric square matrix \(Q\) and exposes helpers for evaluation, serialization, and introspection. The objective to minimize is:

\[\\text{cost}(x) = x^T Q x, \\quad x \\in \\{0, 1\\}^n\]

Parameters:

  • matrix (Matrix | None, default: None ) –

    Symmetric matrix \(Q\) of shape (n, n). Defaults to an empty (0, 0) zero matrix, which represents a trivial problem with no variables.

Methods:

  • __init_subclass__ –

    Register cls under its _tag so load can dispatch to it.

  • __len__ –

    Number of binary variables in the QUBO problem (same as size).

  • cost –

    Compute the QUBO objective \(x^T Q x\) for a candidate solution \(x\).

  • load –

    Deserialize an Instance previously saved with save.

  • save –

    Serialize this instance to file_like, tagged with its type.

Attributes:

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

matrix: Matrix

The QUBO symmetric matrix.

Returns:

  • Matrix –

    QUBO symmetric matrix of shape (size, size).

Raises:

  • AssertionError –

    If the internal tensor is not 2-D or not square.

negative_bitflip property

negative_bitflip: negative_bitflip.Instance

View 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 check and the TypeError below just mirror the guarantee that the assert would otherwise provide.

Returns:

  • negative_bitflip.Instance –

    This instance, narrowed to the negative-bitflip subclass.

Raises:

size property

size: int

Number of binary variables in the QUBO problem.

variable_fixing property

variable_fixing: variable_fixing.Instance

View 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 check and the TypeError below just mirror the guarantee that the assert would otherwise provide.

Returns:

  • variable_fixing.Instance –

    This instance, narrowed to the variable-fixing subclass.

Raises:

zeroing property

zeroing: zeroing.Instance

View 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 check and the TypeError below just mirror the guarantee that the assert would otherwise provide.

Returns:

  • zeroing.Instance –

    This instance, narrowed to the zeroing subclass.

Raises:

__init_subclass__

__init_subclass__(**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.

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__

__len__() -> int

Number 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.size

cost

cost(solution: Bitstring) -> float

Compute the QUBO objective \(x^T Q x\) for a candidate solution \(x\).

Parameters:

  • solution (Bitstring) –

    Binary vector \(x\) of shape (size,).

Returns:

  • float –

    Scalar cost value.

Source code in qubosolver/types/instance.py
def cost(self, solution: Bitstring) -> float:
    """Compute the QUBO objective $x^T Q x$ for a candidate solution $x$.

    Args:
        solution: Binary vector $x$ 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

load(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)), it additionally requires the loaded Instance to be an instance of that subclass (raising TypeError otherwise).

Parameters:

  • file_like (FileLike[bytes]) –

    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 –

    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
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)
Source code in qubosolver/types/instance.py
@classmethod
def 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 instance

save

save(file_like: FileLike[bytes]) -> None

Serialize this instance to file_like, tagged with its type.

Parameters:

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

LocalEmulator(backend_type: type[EmulatorBackend] = AutoLocalEmulatorBackend, **kwargs: Any)

Local quantum emulator with automatic backend selection.

This class wraps qoolqit.execution.LocalEmulator and automatically selects the optimal local backend based on the quantum register size. It provides the same interface as the base qoolqit.execution.LocalEmulator but with improved performance through intelligent backend selection.

The optimal backend selection follows these guidelines:

Parameters:

Example
from qubosolver import LocalEmulator
emulator = LocalEmulator(num_shots=1000)
# Backend will be automatically selected based on problem size

Methods:

  • 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

run(program: qoolqit.QuantumProgram, *args: Any, **kwargs: Any) -> Any

Run the quantum program on the selected backend.

Parameters:

Returns:

  • Any –

    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

RemoteEmulator(backend_type: type[RemoteEmulatorBackend] = RemoteEmuFreeBackend, **kwargs: Any)

Remote quantum emulator with automatic backend selection.

This class wraps qoolqit.execution.RemoteEmulator and provides backend selection recommendations based on quantum register size and tractability constraints.

Backend selection guidelines based on computational tractability:

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[RemoteEmulatorBackend], default: RemoteEmuFreeBackend ) –

    Backend type to use.

  • **kwargs (Any, default: {} ) –

    Additional keyword arguments passed to the base qoolqit.execution.RemoteEmulator.

Example
from qubosolver import RemoteEmulator
from pasqal_cloud import PasqalCloudConnection
connection = PasqalCloudConnection(username="user", password="pass", project_id="project")
emulator = RemoteEmulator(connection=connection, num_shots=1000)
# Uses RemoteEmuFreeBackend by default

Methods:

  • 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

run(program: qoolqit.QuantumProgram, *args: Any, **kwargs: Any) -> Any

Run the quantum program on the selected backend.

Parameters:

Returns:

  • Any –

    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

Solution(bitstrings: Bitstrings = _bitstrings.zeros_field(0, 0), costs: Vector = vector.zeros_field(0), counts: Vectori = vectori.zeros_field(0), probabilities: Vector = vector.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) –

    int8 tensor of shape (num_solutions, n) containing candidate binary vectors (values in \(\\{0, 1\\}^n\)).

  • costs (Vector) –

    Float tensor of shape (num_solutions,) with the QUBO objective \(x^T Q x\) for each bitstring.

  • counts (Vectori) –

    int64 tensor 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 idx as a Candidate.

  • __iter__ –

    Iterate over all candidates in index order, yielding Candidate objects.

  • __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 Solution from Pulser quantum-simulation results.

  • load –

    Deserialize a Solution previously saved with save.

  • save –

    Serialize this solution to file_like using torch.save.

  • truncate –

    Keep only the first k candidates in-place.

  • zeros –

    Build a single all-zero candidate solution of zero cost.

num_variables property

num_variables: int

Return the number of variables per bitstring.

__getitem__

__getitem__(idx: int) -> Candidate

Return the candidate at position idx as a Candidate.

Parameters:

  • idx (int) –

    Zero-based index into the num_solutions axis.

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__

__iter__() -> Iterator[Candidate]

Iterate over all candidates in index order, yielding Candidate objects.

Yields:

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__

__len__() -> int

Return 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

check_consistency(*, instance: Instance | None = None, throw: bool = False, full: bool = True, rtol: float = 1e-05, atol: float = 1e-08) -> 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 \(x^T Q x\) 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.

Parameters:

  • instance (Instance | None, default: None ) –

    The QUBO instance this solution is expected to solve.

  • throw (bool, default: False ) –

    When True, raise an AssertionError on the first failing check instead of returning False.

  • full (bool, default: True ) –

    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 (float, default: 1e-05 ) –

    Relative tolerance forwarded to torch.allclose when comparing costs against \(x^T Q x\) and probabilities against normalized counts.

  • atol (float, default: 1e-08 ) –

    Absolute tolerance forwarded to torch.allclose when comparing costs against \(x^T Q x\) and probabilities against normalized counts.

Returns:

  • bool –

    True if all checks pass, False otherwise (unless throw is True, 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 $x^T Q x$ 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 $x^T Q x$ and `probabilities`
            against normalized `counts`.
        atol: Absolute tolerance forwarded to `torch.allclose` when
            comparing `costs` against $x^T Q x$ 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

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.

Parameters:

  • solutions (Iterable[Solution]) –

    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), which raises AssertionError otherwise).

  • unit_counts (bool, default: False ) –

    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:

  • Solution –

    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
# 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()
Source code in qubosolver/types/solution.py
@staticmethod
def 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

deduplicate(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.

Parameters:

  • update (bool, default: True ) –

    When True (default), sort the result by cost and recompute probabilities from the new counts before returning. Pass False to skip both.

Returns:

  • Self –

    The same Solution instance, allowing method chaining.

Raises:

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

from_results(results: Results, instance: Instance) -> Solution

Build a Solution from Pulser quantum-simulation results.

Parameters:

  • results (Results) –

    Pulser results object whose final_bitstrings attribute is a dict[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
@staticmethod
def 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

load(file_like: FileLike[bytes]) -> Solution

Deserialize a Solution previously saved with save.

Parameters:

  • file_like (FileLike[bytes]) –

    Source — a file path (str or os.PathLike), or a binary-readable typing.IO stream. Must contain data written by save.

Returns:

  • Solution –

    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
from pathlib import Path

with Path("solution.bin").open("rb") as f:
    solution = Solution.load(f)
Source code in qubosolver/types/solution.py
@staticmethod
def 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

save(file_like: FileLike[bytes]) -> None

Serialize this solution to file_like using torch.save.

Parameters:

  • file_like (FileLike[bytes]) –

    Destination — a file path (str or os.PathLike), or a binary-writable typing.IO stream.

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

truncate(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
# Keep only the best candidate (lowest cost).
solution.truncate(1)

Parameters:

  • k (int) –

    Number of candidates to keep. When k >= len(self), this is a no-op.

Returns:

  • Self –

    The same Solution instance, allowing method chaining.

Raises:

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

zeros(length: int, *, count: int = 1) -> Solution

Build a single all-zero candidate solution of zero cost.

Parameters:

  • length (int) –

    Number of variables in the bitstring.

  • count (int, default: 1 ) –

    Number of samples to attribute to this candidate.

Returns:

  • Solution –

    A Solution holding one all-zero candidate.

Source code in qubosolver/types/solution.py
@staticmethod
def 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

extract_qubo(register: qoolqit.Register, drive: qoolqit.Drive) -> Instance

Reconstruct the QUBO encoded by a register's geometry and a drive's final detuning.

Parameters:

  • register (qoolqit.Register) –

    The physical register whose geometry encodes the QUBO's off-diagonal coefficients.

  • drive (qoolqit.Drive) –

    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

torch_rng(seed: int | None = None) -> torch.Generator

Creates a torch.Generator compatible with qubosolver's torch typing.

Parameters:

  • seed (int | None, default: None ) –

    Optional seed for reproducibility. If None, the generator is left with its default (non-deterministic) state.

Returns:

  • torch.Generator –

    A torch.Generator instance, 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)