Source code for robustx.lib.OptSolver

import pandas as pd

from gurobipy import Model, GRB
from gurobipy.gurobipy import quicksum

from robustx.lib.intabs.IntervalAbstractionPyTorch import IntervalAbstractionPytorch
from robustx.lib.tasks.Task import Task


[docs] class OptSolver: """ A solver class that uses Gurobi to optimize a model based on a given task and instance. Attributes / Properties ------- task: Task The task to be optimized. gurobiModel: Model The Gurobi optimization model. inputNodes: dict Dictionary to store Gurobi variables for input nodes. outputNode: Gurobi variable The Gurobi variable representing the output node. Methods ------- setup(instance, desired_output=1, delta=0.5, bias_delta=0, M=1000000000, epsilon=0.0001, fix_inputs=True): Sets up the Gurobi model with constraints based on the provided instance and parameters. ------- """ def __init__(self, ct: Task): """ Initializes the OptSolver with a given Task. @param ct: Task, The task to be optimized. """ self.task = ct self.gurobiModel = Model() self.inputNodes = None self.outputNode = None
[docs] def setup(self, instance, delta=0, bias_delta=0, M=1000, fix_inputs=True): """ Sets up the Gurobi model with constraints based on the provided instance and parameters. @param instance: pd.DataFrame or list, The input data instance for which to set up the model. @param desired_output: int, Optional, The desired output value (default is 1). @param delta: float, Optional, The delta value used in constraints (default is 0.5). @param bias_delta: float, Optional, The bias delta value used in constraints (default is 0). @param M: float, Optional, A large constant used in constraints (default is 1000000000). @param epsilon: float, Optional, The epsilon value used in constraints (default is 0.0001). @param fix_inputs: bool, Optional, Whether to fix input values or use variable bounds (default is True). @return: None """ # Turn off the Gurobi output self.gurobiModel.setParam('OutputFlag', 0) # Convert instance to a list if isinstance(instance, pd.DataFrame): try: ilist = instance.iloc[0].tolist() except Exception as e: raise Exception("Empty instance provided") else: ilist = instance.tolist() intabs = IntervalAbstractionPytorch(self.task.model, delta, bias_delta=bias_delta) self.inputNodes = {} all_nodes = {} activation_states = {} if fix_inputs: # Create the Gurobi variables for the inputs for i in range(len(self.task.training_data.X.columns)): key = f"v_0_{i}" self.inputNodes[key] = self.gurobiModel.addVar(lb=-float('inf'), name=key) all_nodes[key] = self.inputNodes[key] self.gurobiModel.addConstr(self.inputNodes[key] == ilist[i], name=f"constr_input_{i}") else: # Create the Gurobi variables for the inputs for i, col in enumerate(self.task.training_data.X.columns): key = f"v_0_{i}" # Calculate the minimum and maximum values for the current column col_min = self.task.training_data.X[col].min() col_max = self.task.training_data.X[col].max() # Use the calculated min and max for the bounds of the variable self.inputNodes[key] = self.gurobiModel.addVar(lb=col_min, ub=col_max, name=key) all_nodes[key] = self.inputNodes[key] self.gurobiModel.update() num_layers = len(intabs.layers) # Iterate through all "hidden" layers for layer in range(num_layers - 2): # Go through each node in the current layer for node in range(intabs.layers[layer + 1]): var_name = f"v_{layer + 1}_{node}" activation_name = f"xi_{layer + 1}_{node}" all_nodes[var_name] = self.gurobiModel.addVar(lb=-float('inf'), name=var_name) activation_states[activation_name] = self.gurobiModel.addVar(vtype=GRB.BINARY, name=activation_name) self.gurobiModel.update() # 1) Add v_i_j >= 0 constraint self.gurobiModel.addConstr(all_nodes[var_name] >= 0, name="constr1_" + var_name) # 2) Add v_i_j <= M ( 1 - xi_i_j ) self.gurobiModel.addConstr(M * (1 - activation_states[activation_name]) >= all_nodes[var_name], name="constr2_" + var_name) # 3) Add v_i_j <= sum((W_i_j + delta)v_i-1_j + ... + M xi_i_j) self.gurobiModel.addConstr(quicksum(( intabs.weight_intervals[f'weight_l{layer}_n{prev_node_index}_to_l{layer + 1}_n{node}'][1] * all_nodes[f"v_{layer}_{prev_node_index}"] for prev_node_index in range(intabs.layers[layer]) )) + intabs.bias_intervals[f'bias_into_l{layer + 1}_n{node}'][1] + M * activation_states[ activation_name] >= all_nodes[var_name], name="constr3_" + var_name) # 4) Add v_i_j => sum((W_i_j - delta)v_i-1_j + ...) self.gurobiModel.addConstr(quicksum(( intabs.weight_intervals[f'weight_l{layer}_n{prev_node_index}_to_l{layer + 1}_n{node}'][0] * all_nodes[f"v_{layer}_{prev_node_index}"] for prev_node_index in range(intabs.layers[layer]) )) + intabs.bias_intervals[f'bias_into_l{layer + 1}_n{node}'][0] <= all_nodes[var_name], name="constr4_" + var_name) self.gurobiModel.update() # Create a singular output node self.outputNode = self.gurobiModel.addVar(lb=-float('inf'), vtype=GRB.CONTINUOUS, name='output_node') # Constraint 1: node <= ub(W)x + ub(B) self.gurobiModel.addConstr(quicksum(( intabs.weight_intervals[f'weight_l{num_layers - 2}_n{prev_node_index}_to_l{num_layers - 1}_n{0}'][1] * all_nodes[f"v_{num_layers - 2}_{prev_node_index}"] for prev_node_index in range(intabs.layers[num_layers - 2]) )) + intabs.bias_intervals[f'bias_into_l{num_layers - 1}_n{0}'][1] >= self.outputNode, name="output_node_C1") # Constraint 2: node => lb(W)x + lb(B) self.gurobiModel.addConstr(quicksum(( intabs.weight_intervals[f'weight_l{num_layers - 2}_n{prev_node_index}_to_l{num_layers - 1}_n{0}'][0] * all_nodes[f"v_{num_layers - 2}_{prev_node_index}"] for prev_node_index in range(intabs.layers[num_layers - 2]) )) + intabs.bias_intervals[f'bias_into_l{num_layers - 1}_n{0}'][0] <= self.outputNode, name="output_node_C2")