Source code for ccvm_simulators.problem_classes.boxqp.problem_instance

import torch
import enum


[docs] class DeviceType(enum.Enum): """The devices that can be used by pytorch""" CPU_DEVICE = "cpu" CUDA_DEVICE = "cuda"
[docs] class InstanceType(enum.Enum): """Enumerate instance types.""" TUNING = "tuning" TEST = "test"
# TODO: Revisit for a potential factory pattern
[docs] class ProblemInstance: """Defines a BoxQP problem instance.""" def __init__( self, device="cpu", instance_type="tuning", file_path=None, file_delimiter="\t", name=None, solution_bounds=(0.0, 1.0), ): """Problem instance constructor. Args: device (str, optional): Defines which GPU (or the CPU) to use. Defaults to "cpu". instance_type (str, optional): The type of the instance. Defaults to "tuning". file_path (str, optional): Path to file of problem instance. Defaults to None. file_delimiter (str, optional): The type of delimiter used in the file. Defaults to "\t". name (str, optional): The name of the problem instance. If not given, defaults to the file name when an instance is loaded. solution_bounds (tuple(float), optional): The minimum and maximum value allowed (inclusively) in the solution vector. The first value is the minimum, the second is the maximum. Defaults to (0.0,1.0). Attributes: problem_size (int): instance size. Defaults to None. optimal_sol (float): the optimal solution via Gurobi to the problem. Defaults to None. best_sol (float): the best solution via BFGS. Defaults to None optimality (bool): indicates whether the solution is optimal (True or False). Defaults to None. sol_time_gb (float): the time for Gurobi to solve it sol_time_bfgs (float): the time for BFGS to solve it num_frac_values (int): number of fractional values in the solution q_matrix (torch.tensor): Q matrix of the QP problem. Defaults to None. v_vector (torch.tensor): V vector of the QP problem. Defaults to None. solution_vector (list): the vector of solution to the problem instance found using Gurobi scaled_by (float): scaling value of the coefficient. Defaults to 1. """ self.problem_size = None self.optimal_sol = None self.best_sol = None self.optimality = None self.sol_time_gb = None self.sol_time_bfgs = None self.num_frac_values = None self.q_matrix = None self.v_vector = None self.solution_vector = None self.scaled_by = 1 self.device = device self._custom_name = False self.file_delimiter = file_delimiter instance_values = set(item.value for item in InstanceType) if instance_type in instance_values: self.instance_type = instance_type else: raise ValueError("instance_type must be tuning or test") if name: self.name = name self._custom_name = True if file_path: self.file_path = file_path self.load_instance( device=device, instance_type=instance_type, file_path=file_path, file_delimiter=file_delimiter, ) self.problem_category = "boxqp" self.solution_bounds = solution_bounds @property def solution_bounds(self): """ The minimum and maximum value allowed (inclusively) in the solution vector. The first value is the minimum, the second is the maximum. Returns: tuple(float): The minimum and maximum solution bounds. """ return self._solution_bounds @solution_bounds.setter def solution_bounds(self, bounds): if len(bounds) != 2: raise ValueError("solution_bounds must be a tuple of size 2, containing the minimum and maximum bounds (inclusive)") elif bounds[0] >= bounds[1]: raise ValueError("Minimum solution bound must be less than maximum solution bound") else: self._solution_bounds = bounds
[docs] def load_instance( self, device="cpu", instance_type="tuning", file_path=None, file_delimiter=None ): """Loads in a box constraint problem from a file. Args: device (str, optional): Device to use. Defaults to "cpu". instance_type (str, optional): The type of the instance. Defaults to "tuning". file_path (str, optional): Path to instance file. Defaults to None. file_delimiter (str, optional): Delimiter used in the instance file. If not specified, the file_delimiter value assigned at instance initialization will be used. Raises: Exception: File path is not specified. Exception: Error reading the instance file. """ rval_q = None rval_v = None problem_size = None # Raise an exception if the file path was neither given as a load_instance # parameter nor upon initialization if not file_path and not self.file_path: raise Exception("No file path specified, cannot load instance.") # Update the file path if it was given as a load_instance parameter if file_path: self.file_path = file_path file_path = self.file_path # Update the file delimiter if it was given as a load_instance parameter if file_delimiter: self.file_delimiter = file_delimiter file_delimiter = self.file_delimiter # Read in data from the instance file with open(file_path, "r") as stream: try: # Read metadata from the first line lines = stream.readlines() instance_info = lines[0].split("\n")[0].split(file_delimiter) # Save all metadata from the file problem_size = int(instance_info[0]) optimal_sol = float(instance_info[1]) best_sol = float(instance_info[2]) if instance_info[3].lower() == "true": optimality = True else: optimality = False sol_time_gb = float(instance_info[4]) sol_time_bfgs = float(instance_info[5]) num_frac_values = int( instance_info[7] ) # seed=int(instance_info[6]) # discarded # Initialize the q_matrix and v_vector matrices rval_q = torch.zeros( (problem_size, problem_size), dtype=torch.float ).to(device) rval_v = torch.zeros((problem_size,), dtype=torch.float).to(device) # Read in the v_vector matrix line_data_v = lines[1].split("\n")[0].split(file_delimiter) for idx in range(0, problem_size): rval_v[idx] = -torch.Tensor([float(line_data_v[idx])]) # Read in the q_matrix matrix for idx, line in enumerate(lines[2 : problem_size + 2]): line_data = line.split("\n")[0].split(file_delimiter) for j, value in enumerate(line_data[:problem_size]): rval_q[idx, j] = -torch.Tensor([float(value)]) # Read the last line as an additional information solution_vector = [] try: last_raw_data_line = ( lines[problem_size + 2].split("\n")[0].split(file_delimiter) ) for v in last_raw_data_line: if not v == "": solution_vector.append(float(v)) except IndexError: # solution_vector was not supplied pass except Exception as e: raise Exception("Error reading instance file: " + str(e)) # set class variables self.device = device self.instance_type = instance_type self.problem_size = problem_size self.optimal_sol = optimal_sol self.best_sol = best_sol self.optimality = optimality self.sol_time_gb = sol_time_gb self.sol_time_bfgs = sol_time_bfgs self.num_frac_values = num_frac_values self.q_matrix = rval_q self.v_vector = rval_v self.solution_vector = solution_vector self.scaled_by = 1 # Set the name of the instance if the user has not set it if not self._custom_name: # Remove the file extension and path, then name the instance after the file self.name = file_path.split("/")[-1].split(".")[0]
[docs] def compute_energy(self, confs): """Compute the objective value for the given BoxQP instance using the formula '0.5 xQx + Vx', where 'x' is the vector of variables. Args: confs (torch.Tensor): Configurations for which to compute energy Returns: torch.Tensor: Energy of configurations. """ energy1 = ( torch.einsum("bi, ij, bj -> b", confs, self.q_matrix, confs) * self.scaled_by ) energy2 = torch.einsum("bi, i -> b", confs, self.v_vector) * self.scaled_by return 0.5 * energy1 + energy2
[docs] def scale_coefs(self, scaling_factor): """Divides the coefficients of the problem stored in this instance by the given factor. Note that consecutive calls to this function will stack, e.g. scaling the problem by 4 twice would have the same result as scaling the original problem by 16. Args: scaling_factor (torch.Tensor): The amount by which the coefficients should be scaled. """ self.q_matrix = self.q_matrix / scaling_factor self.v_vector = self.v_vector / scaling_factor self.scaled_by *= scaling_factor