Utility functions
QUBODataset(coefficients, solutions=None)
Section titled “
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:
|
solutions |
Optional list of QUBOSolution objects corresponding to each instance in the dataset.
TYPE:
|
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:
|
solutions
|
Optional list of QUBOSolution objects corresponding to each instance in the dataset.
TYPE:
|
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)
Section titled “
__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:
|
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__()
Section titled “
__len__()
”Returns the number of instances in the dataset.
RETURNS | DESCRIPTION |
---|---|
int
|
The number of coefficient matrices (num_instances).
TYPE:
|
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
Section titled “
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:
|
matrix_dim
|
The dimension of each QUBO matrix.
TYPE:
|
densities
|
List of densities (ratio of non-zero elements). Defaults to [0.5].
TYPE:
|
coefficient_bounds
|
Range (min, max) of random values for the coefficients. Defaults to (-10.0, 10.0).
TYPE:
|
device
|
Device for the tensors. Defaults to "cpu".
TYPE:
|
dtype
|
Data type for the coefficient tensors. Defaults to torch.float32.
TYPE:
|
seed
|
Seed for reproducibility. Defaults to None.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
QUBODataset
|
A dataset containing the generated coefficient matrices.
TYPE:
|
Source code in qubosolver/data.py
@classmethoddef 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
Section titled “
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:
|
counts |
Tensor of shape (num_solutions,), containing the count of occurrences of each bitstring. Optional, can be None.
TYPE:
|
probabilities |
Tensor of shape (num_solutions,), containing the probability of each bitstring solution. Optional, can be None.
TYPE:
|
costs |
Tensor of shape (num_solutions,), containing the cost associated with each bitstring solution.
TYPE:
|
compute_costs(instance)
Section titled “
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:
|
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()
Section titled “
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()
Section titled “
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]
generate_symmetric_mask(size, target, device, generator)
Section titled “
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:
|
target
|
Target number of elements.
TYPE:
|
device
|
Torch device.
TYPE:
|
generator
|
generator for randomness.
TYPE:
|
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)
Section titled “
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:
|
RETURNS | DESCRIPTION |
---|---|
QUBODataset
|
The loaded QUBODataset object.
TYPE:
|
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)
Section titled “
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:
|
RETURNS | DESCRIPTION |
---|---|
QUBOInstance
|
The loaded QUBOInstance object.
TYPE:
|
Notes
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)
Section titled “
save_qubo_dataset(dataset, filepath)
”Saves a QUBODataset to a file.
PARAMETER | DESCRIPTION |
---|---|
dataset
|
The QUBODataset object to save.
TYPE:
|
filepath
|
Path to the file where the QUBODataset will be saved.
TYPE:
|
Notes
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)
Section titled “
save_qubo_instance(instance, filepath)
”Saves a QUBOInstance to a file.
PARAMETER | DESCRIPTION |
---|---|
instance
|
The QUBOInstance object to save.
TYPE:
|
filepath
|
Path to the file where the QUBOInstance will be saved.
TYPE:
|
Notes
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)