Skip to content

QUBODataset(coefficients, solutions=None)

Bases: Dataset

Represents a dataset for Quadratic Unconstrained Binary Optimization (QUBO) problems.

ATTRIBUTE DESCRIPTION
coefficients

Tensor of shape (size, size, num_instances), containing the QUBO coefficient matrices.

TYPE: Tensor

solutions

Optional list of QUBOSolution objects corresponding to each instance in the dataset.

TYPE: list[QUBOSolution] | None

METHOD DESCRIPTION
__len__

Returns the number of instances in the dataset.

__getitem__

Retrieves the coefficient matrix and optionally the solution for the specified index.

from_random

Class method to generate a QUBODataset with random coefficient matrices.

Initializes a QUBODataset.

PARAMETER DESCRIPTION
coefficients

Tensor of shape (size, size, num_instances), containing the QUBO coefficient matrices.

TYPE: Tensor

solutions

Optional list of QUBOSolution objects corresponding to each instance in the dataset.

TYPE: list[QUBOSolution] | None DEFAULT: None

Source code in qubosolver/data.py
def __init__(self, coefficients: torch.Tensor, solutions: list[QUBOSolution] | None = None):
    """
    Initializes a QUBODataset.

    Args:
        coefficients (torch.Tensor):
            Tensor of shape (size, size, num_instances), containing the QUBO
            coefficient matrices.
        solutions (list[QUBOSolution] | None):
            Optional list of QUBOSolution objects corresponding to each instance
            in the dataset.
    """
    self.coefficients = coefficients
    self.solutions = solutions

__getitem__(idx)

Retrieves the coefficient matrix and optionally the solution for the specified index.

PARAMETER DESCRIPTION
idx

Index of the dataset instance to retrieve.

TYPE: int

RETURNS DESCRIPTION
tuple[Tensor, QUBOSolution | None]

tuple[torch.Tensor, QUBOSolution | None]: The coefficient matrix of shape (size, size) and optionally the corresponding QUBOSolution.

Source code in qubosolver/data.py
def __getitem__(self, idx: int) -> tuple[torch.Tensor, QUBOSolution | None]:
    """
    Retrieves the coefficient matrix and optionally the solution for the specified index.

    Args:
        idx (int):
            Index of the dataset instance to retrieve.

    Returns:
        tuple[torch.Tensor, QUBOSolution | None]:
            The coefficient matrix of shape (size, size) and optionally
            the corresponding QUBOSolution.
    """
    if self.solutions is not None:
        return self.coefficients[:, :, idx], self.solutions[idx]
    return self.coefficients[:, :, idx], None

__len__()

Returns the number of instances in the dataset.

RETURNS DESCRIPTION
int

The number of coefficient matrices (num_instances).

TYPE: int

Source code in qubosolver/data.py
def __len__(self) -> int:
    """
    Returns the number of instances in the dataset.

    Returns:
        int: The number of coefficient matrices (num_instances).
    """
    return int(self.coefficients.shape[2])

from_random(n_matrices, matrix_dim, densities=[0.5], coefficient_bounds=(-10.0, 10.0), device='cpu', dtype=torch.float32, seed=None) classmethod

Generates a QUBODataset with random QUBO coefficient matrices.

Generation Steps: 1. Initialize a reproducible random generator. 2. Create a storage tensor for coefficients. 3. For each density: a. Compute the exact target number of non-zero elements. b. For each instance: i. Generate a symmetric boolean mask with an exact number of True elements. ii. Generate random values within the coefficient_bounds. iii. Apply the mask to zero out unselected elements. iv. Symmetrize the matrix by mirroring the upper triangle onto the lower triangle. v. Force all off-diagonal coefficients to be positive. vi. Ensure that at least one diagonal element is negative. vii. Ensure at least one coefficient equals the upper bound, excluding any diagonal already at the lower bound. 4. Return a QUBODataset instance containing the generated matrices.

PARAMETER DESCRIPTION
n_matrices

Number of QUBO matrices to generate for each density.

TYPE: int

matrix_dim

The dimension of each QUBO matrix.

TYPE: int

densities

List of densities (ratio of non-zero elements). Defaults to [0.5].

TYPE: list[float] DEFAULT: [0.5]

coefficient_bounds

Range (min, max) of random values for the coefficients. Defaults to (-10.0, 10.0).

TYPE: tuple[float, float] DEFAULT: (-10.0, 10.0)

device

Device for the tensors. Defaults to "cpu".

TYPE: str DEFAULT: 'cpu'

dtype

Data type for the coefficient tensors. Defaults to torch.float32.

TYPE: dtype DEFAULT: float32

seed

Seed for reproducibility. Defaults to None.

TYPE: int | None DEFAULT: None

RETURNS DESCRIPTION
QUBODataset

A dataset containing the generated coefficient matrices.

TYPE: QUBODataset

Source code in qubosolver/data.py
@classmethod
def from_random(
    cls,
    n_matrices: int,
    matrix_dim: int,
    densities: list[float] = [0.5],
    coefficient_bounds: tuple[float, float] = (-10.0, 10.0),
    device: str = "cpu",
    dtype: torch.dtype = torch.float32,
    seed: int | None = None,
) -> QUBODataset:
    """
    Generates a QUBODataset with random QUBO coefficient matrices.

    Generation Steps:
    1. Initialize a reproducible random generator.
    2. Create a storage tensor for coefficients.
    3. For each density:
        a. Compute the exact target number of non-zero elements.
        b. For each instance:
            i.  Generate a symmetric boolean mask with an exact number of True elements.
            ii. Generate random values within the coefficient_bounds.
            iii. Apply the mask to zero out unselected elements.
            iv. Symmetrize the matrix by mirroring the upper triangle onto the lower triangle.
            v. Force all off-diagonal coefficients to be positive.
            vi. Ensure that at least one diagonal element is negative.
            vii. Ensure at least one coefficient equals the upper bound, excluding
            any diagonal already at the lower bound.
    4. Return a QUBODataset instance containing the generated matrices.

    Args:
        n_matrices (int): Number of QUBO matrices to generate for each density.
        matrix_dim (int): The dimension of each QUBO matrix.
        densities (list[float], optional): List of densities (ratio of non-zero elements).
            Defaults to [0.5].
        coefficient_bounds (tuple[float, float], optional): Range (min, max) of
            random values for the coefficients. Defaults to (-10.0, 10.0).
        device (str): Device for the tensors. Defaults to "cpu".
        dtype (torch.dtype, optional): Data type for the coefficient tensors.
            Defaults to torch.float32.
        seed (int | None, optional): Seed for reproducibility. Defaults to None.

    Returns:
        QUBODataset: A dataset containing the generated coefficient matrices.
    """
    # Step 1: Initialize a reproducible random generator.
    generator = torch.Generator(device=device)
    if seed is not None:
        generator.manual_seed(seed)

    # 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 _ in range(n_matrices):
            mask = generate_symmetric_mask(matrix_dim, target, device, generator)
            random_vals = torch.empty(
                matrix_dim, matrix_dim, device=device, dtype=dtype
            ).uniform_(*coefficient_bounds, generator=generator)
            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 not (coeff.diag() < 0).any():
                diag_vals = coeff.diag()
                non_neg = (diag_vals >= 0).nonzero(as_tuple=True)[0]
                diag_idx = (
                    non_neg[0].item()
                    if non_neg.numel() > 0
                    else torch.randint(
                        0, matrix_dim, (1,), device=device, generator=generator
                    ).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=generator)
                        .abs()
                        .item()
                    )
                coeff[diag_idx, diag_idx] = neg_val

            if not (coeff == coefficient_bounds[1]).any():
                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[
                        torch.randint(
                            0,
                            len(filtered),
                            (1,),
                            device=device,
                            generator=generator,
                        ).item()
                    ]
                else:
                    chosen = [
                        torch.randint(
                            0, matrix_dim, (1,), device=device, generator=generator
                        ).item()
                    ] * 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
            idx += 1

    # Step 4: Return the dataset.
    return cls(coefficients=coefficients)

QUBOSolution(bitstrings, costs, counts=None, probabilities=None, solution_status=SolutionStatusType.UNPROCESSED) dataclass

Represents a solution to a QUBO problem.

ATTRIBUTE DESCRIPTION
bitstrings

Tensor of shape (num_solutions, bitstring_length), containing the bitstring solutions. Each entry is an integer tensor with 0s and 1s.

TYPE: Tensor

counts

Tensor of shape (num_solutions,), containing the count of occurrences of each bitstring. Optional, can be None.

TYPE: Tensor | None

probabilities

Tensor of shape (num_solutions,), containing the probability of each bitstring solution. Optional, can be None.

TYPE: Tensor | None

costs

Tensor of shape (num_solutions,), containing the cost associated with each bitstring solution.

TYPE: Tensor

compute_costs(instance)

Computes the cost for each bitstring solution based on the provided QUBO instance.

PARAMETER DESCRIPTION
instance

The QUBO instance containing the QUBO matrix.

TYPE: QUBOInstance

RETURNS DESCRIPTION
Tensor

torch.Tensor: A tensor of costs for each bitstring.

Source code in qubosolver/data.py
def compute_costs(self, instance: Any) -> torch.Tensor:
    """
    Computes the cost for each bitstring solution based on the provided QUBO instance.

    Args:
        instance (QUBOInstance): The QUBO instance containing the QUBO matrix.

    Returns:
        torch.Tensor: A tensor of costs for each bitstring.
    """
    # Retrieve the QUBO matrix from the QUBOInstance
    QUBO = instance.coefficients  # Assuming `coefficients` holds the QUBO matrix

    costs = []
    for bitstring in self.bitstrings:
        if isinstance(bitstring, str):
            z = torch.tensor([int(b) for b in bitstring], dtype=torch.float32)
        elif isinstance(bitstring, torch.Tensor):
            z = bitstring.detach().clone()
        else:
            z = torch.tensor(bitstring, dtype=torch.float32)
        cost = torch.matmul(
            z.permute(*torch.arange(z.ndim - 1, -1, -1)), torch.matmul(QUBO, z)
        ).item()  # Use the QUBO matrix from the instance
        costs.append(cost)

    return torch.tensor(costs)

compute_probabilities()

Computes the probabilities of each bitstring solution based on their counts.

RETURNS DESCRIPTION
Tensor

torch.Tensor: A tensor of probabilities for each bitstring.

Source code in qubosolver/data.py
def compute_probabilities(self) -> torch.Tensor:
    """
    Computes the probabilities of each bitstring solution based on their counts.

    Returns:
        torch.Tensor: A tensor of probabilities for each bitstring.
    """
    if self.counts is None:
        raise ValueError("Counts are required to compute probabilities.")

    total_counts = self.counts.sum().item()
    probabilities = (
        self.counts / total_counts if total_counts > 0 else torch.zeros_like(self.counts)
    )
    return probabilities

sort_by_cost()

Sorts the QUBOSolution in-place by increasing cost.

Reorders bitstrings, costs, counts, and probabilities (if available) based on the ascending order of the costs.

Source code in qubosolver/data.py
def sort_by_cost(self) -> None:
    """
    Sorts the QUBOSolution in-place by increasing cost.

    Reorders bitstrings, costs, counts, and probabilities (if available)
    based on the ascending order of the costs.
    """

    sorted_indices = torch.argsort(self.costs)
    self.bitstrings = self.bitstrings[sorted_indices]
    self.costs = self.costs[sorted_indices]
    if self.counts is not None:
        self.counts = self.counts[sorted_indices]
    if self.probabilities is not None:
        self.probabilities = self.probabilities[sorted_indices]

Utility functions

generate_symmetric_mask(size, target, device, generator)

Generate a symmetric boolean mask with an exact number of True elements to match a certain density of QUBO. Used in the from_random method of QUBODataset.

PARAMETER DESCRIPTION
size

Size of problem.

TYPE: int

target

Target number of elements.

TYPE: int

device

Torch device.

TYPE: str

generator

generator for randomness.

TYPE: Generator

RETURNS DESCRIPTION
Tensor

torch.Tensor: Mask.

Source code in qubosolver/data_utils.py
def generate_symmetric_mask(
    size: int, target: int, device: str, generator: torch.Generator
) -> torch.Tensor:
    """Generate a symmetric boolean mask with an exact number of True elements
        to match a certain density of QUBO.
        Used in the `from_random` method of `QUBODataset`.

    Args:
        size (int): Size of problem.
        target (int): Target number of elements.
        device (str): Torch device.
        generator (torch.Generator): generator for randomness.

    Returns:
        torch.Tensor: Mask.
    """
    possible_x = []
    for x in range(1, min(size, target) + 1):
        if (target - x) % 2 == 0:
            y = (target - x) // 2
            if y <= (size * (size - 1)) // 2:
                possible_x.append(x)
    if not possible_x:
        x, y = 1, 0
    else:
        x = possible_x[
            torch.randint(0, len(possible_x), (1,), device=device, generator=generator).item()
        ]
        y = (target - x) // 2

    mask = torch.zeros((size, size), dtype=torch.bool, device=device)
    diag_indices = torch.randperm(size, device=device, generator=generator)[:x]
    for i in diag_indices.tolist():
        mask[i, i] = True

    upper_indices = torch.tensor(
        [(i, j) for i in range(size) for j in range(i + 1, size)],
        device=device,
    )
    if upper_indices.size(0) > 0 and y > 0:
        perm = torch.randperm(upper_indices.size(0), device=device, generator=generator)[:y]
        chosen_upper = upper_indices[perm]
        for i, j in chosen_upper.tolist():
            mask[i, j] = True
            mask[j, i] = True
    return mask

load_qubo_dataset(filepath)

Loads a QUBODataset from a file. Notes: The file should contain data saved in the format used by save_qubo_dataset.

PARAMETER DESCRIPTION
filepath

Path to the file from which the QUBODataset will be loaded.

TYPE: str

RETURNS DESCRIPTION
QUBODataset

The loaded QUBODataset object.

TYPE: QUBODataset

Source code in qubosolver/saveload.py
def load_qubo_dataset(filepath: str) -> QUBODataset:
    """
    Loads a QUBODataset from a file.
    Notes:
        The file should contain data saved in the format used by `save_qubo_dataset`.

    Args:
        filepath (str):
            Path to the file from which the QUBODataset will be loaded.

    Returns:
        QUBODataset:
            The loaded QUBODataset object.


    """
    data = torch.load(filepath)
    solutions = None
    if data["solutions"] is not None:
        solutions = [
            QUBOSolution(
                bitstrings=solution["bitstrings"],
                counts=solution["counts"],
                probabilities=solution["probabilities"],
                costs=solution["costs"],
            )
            for solution in data["solutions"]
        ]
    return QUBODataset(coefficients=data["coefficients"], solutions=solutions)

load_qubo_instance(filepath)

Loads a QUBOInstance from a file.

PARAMETER DESCRIPTION
filepath

Path to the file from which the QUBOInstance will be loaded.

TYPE: str

RETURNS DESCRIPTION
QUBOInstance

The loaded QUBOInstance object.

TYPE: QUBOInstance

Notes

The file should contain data saved in the format used by save_qubo_instance.

Source code in qubosolver/saveload.py
def load_qubo_instance(filepath: str) -> QUBOInstance:
    """
    Loads a QUBOInstance from a file.

    Args:
        filepath (str):
            Path to the file from which the QUBOInstance will be loaded.

    Returns:
        QUBOInstance:
            The loaded QUBOInstance object.

    Notes:
        The file should contain data saved in the format used by `save_qubo_instance`.
    """
    data = torch.load(filepath, weights_only=False)
    instance = QUBOInstance(
        coefficients=data["coefficients"],
        device=data["device"],
        dtype=data["dtype"],
    )
    instance.solution = data["solution"]
    return instance

save_qubo_dataset(dataset, filepath)

Saves a QUBODataset to a file.

PARAMETER DESCRIPTION
dataset

The QUBODataset object to save.

TYPE: QUBODataset

filepath

Path to the file where the QUBODataset will be saved.

TYPE: str

Notes

The saved data includes: - Coefficients (size x size x num_instances tensor) - Solutions (optional, includes bitstrings, counts, probabilities, and costs)

Source code in qubosolver/saveload.py
def save_qubo_dataset(dataset: QUBODataset, filepath: str) -> None:
    """
    Saves a QUBODataset to a file.

    Args:
        dataset (QUBODataset):
            The QUBODataset object to save.
        filepath (str):
            Path to the file where the QUBODataset will be saved.

    Notes:
        The saved data includes:
            - Coefficients (size x size x num_instances tensor)
            - Solutions (optional, includes bitstrings, counts, probabilities, and costs)
    """
    data = {"coefficients": dataset.coefficients, "solutions": None}
    if dataset.solutions is not None:
        data["solutions"] = [
            {
                "bitstrings": solution.bitstrings,
                "counts": solution.counts,
                "probabilities": solution.probabilities,
                "costs": solution.costs,
            }
            for solution in dataset.solutions
        ]
    torch.save(data, filepath)

save_qubo_instance(instance, filepath)

Saves a QUBOInstance to a file.

PARAMETER DESCRIPTION
instance

The QUBOInstance object to save.

TYPE: QUBOInstance

filepath

Path to the file where the QUBOInstance will be saved.

TYPE: str

Notes

The saved data includes: - Coefficients (N x N matrix) - Device (e.g., 'cpu' or 'cuda') - Data type (e.g., torch.float32) - Solution (optional)

Source code in qubosolver/saveload.py
def save_qubo_instance(instance: QUBOInstance, filepath: str) -> None:
    """
    Saves a QUBOInstance to a file.

    Args:
        instance (QUBOInstance):
            The QUBOInstance object to save.
        filepath (str):
            Path to the file where the QUBOInstance will be saved.

    Notes:
        The saved data includes:
            - Coefficients (N x N matrix)
            - Device (e.g., 'cpu' or 'cuda')
            - Data type (e.g., torch.float32)
            - Solution (optional)
    """
    data = {
        "coefficients": instance.coefficients,  # N x N
        "device": instance.device,
        "dtype": instance.dtype,
        "solution": instance.solution,
    }
    torch.save(data, filepath)