def min_unproportionality_allocation(utilities: Dict[int, List[float]], m: Model) -> None: """ Computes (one of) the item allocation(s) which minimizes global unproportionality (observe we only sum unproportionality when it is larger than 0). :param utilities: the dictionary representing the utility profile, where each key is an agent and its value an array of floats such that the i-th float is the utility of the i-th item for the key-agent. :param m: the MIP model to optimize. :return: a dictionary mapping to each agent the bundle which has been assigned to her so that unproportionality is minimized. """ agents, items = len(utilities), len(list(utilities.values())[0]) dummies = [ m.add_var(name='dummy_{}'.format(agent), var_type=CONTINUOUS) for agent in range(agents) ] m.objective = minimize( xsum( m.var_by_name('dummy_{}'.format(agent)) for agent in range(agents))) for agent in range(agents): m += m.var_by_name('dummy_{}'.format(agent)) >= 0 m += m.var_by_name('dummy_{}'.format(agent)) >= (sum(utilities[agent][item] for item in range(items)) / agents)\ - (sum(utilities[agent][item] * m.var_by_name('assign_{}_{}'.format(item ,agent)) for item in range(items))) m.optimize()
def get_var(self, solver: Model) -> Var: if self.__cache: var = self.__cache else: var = solver.var_by_name(self.name) self.__cache = var if self.multiplier: return self.multiplier * var return var
def exists_envy_free_assignment_mip(agents: List[int], utils: Dict[int, List[float]], graph: nx.Graph) -> bool: """ Checks whether there is an envy-free position assignment given the set of agents, the utilities of each agent for each assigned bundle and the social graph. Instead of iterating over all possible position assignments, this function uses an ILP. :param agents: the list of agents. :param utils: the dictionary which represents the utilities of each agent for each assigned bundle (recall that the item allocation is fixed in this case), a key is an agent and a value a list of floats where the float in position i is the utility of the bundle assigned to agent i for the key-agent. :param graph: the social graph. :return: True if there is a local envy-free assignment and False otherwise. """ m, nodes = Model(), list(graph) for (agent, node) in product(agents, nodes): positions = [ m.add_var(name='position_{}_{}'.format(agent, node), var_type=BINARY) ] for agent in agents: m += xsum( m.var_by_name('position_{}_{}'.format(agent, node)) for node in nodes) == 1 for node in nodes: m += xsum( m.var_by_name('position_{}_{}'.format(agent, node)) for agent in agents) == 1 edges = [e for e in graph.edges] for (edge, agent1, agent2) in product(edges, agents, agents): node1, node2 = edge[0], edge[1] if utils[agent1][agent1] < utils[agent1][agent2]: m += m.var_by_name('position_' + str(agent1) + '_' + str(node1)) + \ m.var_by_name('position_' + str(agent2) + '_' + str(node2)) <= 1 m += m.var_by_name('position_' + str(agent2) + '_' + str(node1)) + \ m.var_by_name('position_' + str(agent1) + '_' + str(node2)) <= 1 return m.optimize() == OptimizationStatus.OPTIMAL
def max_utilitarian_welfare_allocation(utilities: Dict[int, List[float]], m: Model) -> None: """ Computes (one of) the item allocation(s) which maximizes utilitarian welfare, returning the optimized model. :param utilities: the dictionary representing the utility profile, where each key is an agent and its value an array of floats such that the i-th float is the utility of the i-th item for the key-agent. :param m: the MIP model which represents the integer linear program. """ agents, items = len(utilities), len(list(utilities.values())[0]) m.objective = maximize( xsum(utilities[agent][item] * m.var_by_name('assign_{}_{}'.format(item, agent)) for item in range(items) for agent in range(agents))) m.optimize()
class InterfaceToSolver: """A wrapper for the mip model class, allows interaction with mip using pd.DataFrames.""" def __init__(self, solver_name='CBC'): self.variables = {} self.linear_mip_variables = {} self.solver_name = solver_name if solver_name == 'CBC': self.mip_model = Model("market", solver_name=CBC) self.linear_mip_model = Model("market", solver_name=CBC) elif solver_name == 'GUROBI': self.mip_model = Model("market", solver_name=GUROBI) self.linear_mip_model = Model("market", solver_name=GUROBI) else: raise ValueError("Solver '{}' not recognised.") self.mip_model.verbose = 0 self.mip_model.solver.set_mip_gap_abs(1e-10) self.mip_model.solver.set_mip_gap(1e-20) self.mip_model.lp_method = LP_Method.DUAL self.linear_mip_model.verbose = 0 self.linear_mip_model.solver.set_mip_gap_abs(1e-10) self.linear_mip_model.solver.set_mip_gap(1e-20) self.linear_mip_model.lp_method = LP_Method.DUAL def add_variables(self, decision_variables): """Add decision variables to the model. Examples -------- >>> decision_variables = pd.DataFrame({ ... 'variable_id': [0, 1], ... 'lower_bound': [0.0, 0.0], ... 'upper_bound': [6.0, 1.0], ... 'type': ['continuous', 'binary']}) >>> si = InterfaceToSolver() >>> si.add_variables(decision_variables) The underlying mip_model should now have 2 variables. >>> print(si.mip_model.num_cols) 2 The first one should have the following properties. >>> print(si.mip_model.var_by_name('0').var_type) C >>> print(si.mip_model.var_by_name('0').lb) 0.0 >>> print(si.mip_model.var_by_name('0').ub) 6.0 The second one should have the following properties. >>> print(si.mip_model.var_by_name('1').var_type) B >>> print(si.mip_model.var_by_name('1').lb) 0.0 >>> print(si.mip_model.var_by_name('1').ub) 1.0 """ # Create a mapping between the nempy level names for variable types and the mip representation. variable_types = {'continuous': CONTINUOUS, 'binary': BINARY} # Add each variable to the mip model. for variable_id, lower_bound, upper_bound, variable_type in zip( list(decision_variables['variable_id']), list(decision_variables['lower_bound']), list(decision_variables['upper_bound']), list(decision_variables['type'])): self.variables[variable_id] = self.mip_model.add_var( lb=lower_bound, ub=upper_bound, var_type=variable_types[variable_type], name=str(variable_id)) self.linear_mip_variables[ variable_id] = self.linear_mip_model.add_var( lb=lower_bound, ub=upper_bound, var_type=variable_types[variable_type], name=str(variable_id)) def add_sos_type_2(self, sos_variables, sos_id_columns, position_column): """Add groups of special ordered sets of type 2 two the mip model. Examples -------- >>> decision_variables = pd.DataFrame({ ... 'variable_id': [0, 1, 2, 3, 4, 5], ... 'lower_bound': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], ... 'upper_bound': [5.0, 5.0, 5.0, 5.0, 5.0, 5.0], ... 'type': ['continuous', 'continuous', 'continuous', ... 'continuous', 'continuous', 'continuous']}) >>> sos_variables = pd.DataFrame({ ... 'variable_id': [0, 1, 2, 3, 4, 5], ... 'sos_id': ['A', 'A', 'A', 'B', 'B', 'B'], ... 'position': [0, 1, 2, 0, 1, 2]}) >>> si = InterfaceToSolver() >>> si.add_variables(decision_variables) >>> si.add_sos_type_2(sos_variables, 'sos_id', 'position') """ # Function that adds sets to mip model. def add_sos_vars(sos_group): self.mip_model.add_sos( list(zip(sos_group['vars'], sos_group[position_column])), 2) # For each variable_id get the variable object from the mip model sos_variables['vars'] = sos_variables['variable_id'].apply( lambda x: self.variables[x]) # Break up the sets based on their id and add them to the model separately. sos_variables.groupby(sos_id_columns).apply(add_sos_vars) # This is a hack to make sure mip knows there are binary constraints. self.mip_model.add_var(var_type=BINARY, obj=0.0) def add_sos_type_1(self, sos_variables): # Function that adds sets to mip model. def add_sos_vars(sos_group): self.mip_model.add_sos( list( zip(sos_group['vars'], [1.0 for i in range(len(sos_variables['vars']))])), 1) # For each variable_id get the variable object from the mip model sos_variables['vars'] = sos_variables['variable_id'].apply( lambda x: self.variables[x]) # Break up the sets based on their id and add them to the model separately. sos_variables.groupby('sos_id').apply(add_sos_vars) # This is a hack to make mip knows there are binary constraints. self.mip_model.add_var(var_type=BINARY, obj=0.0) def add_objective_function(self, objective_function): """Add the objective function to the mip model. Examples -------- >>> decision_variables = pd.DataFrame({ ... 'variable_id': [0, 1, 2, 3, 4, 5], ... 'lower_bound': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], ... 'upper_bound': [5.0, 5.0, 5.0, 5.0, 5.0, 5.0], ... 'type': ['continuous', 'continuous', 'continuous', ... 'continuous', 'continuous', 'continuous']}) >>> objective_function = pd.DataFrame({ ... 'variable_id': [0, 1, 3, 4, 5], ... 'cost': [1.0, 2.0, -1.0, 5.0, 0.0]}) >>> si = InterfaceToSolver() >>> si.add_variables(decision_variables) >>> si.add_objective_function(objective_function) >>> print(si.mip_model.var_by_name('0').obj) 1.0 >>> print(si.mip_model.var_by_name('5').obj) 0.0 """ objective_function = objective_function.sort_values('variable_id') objective_function = objective_function.set_index('variable_id') obj = minimize( xsum(objective_function['cost'][i] * self.variables[i] for i in list(objective_function.index))) self.mip_model.objective = obj self.linear_mip_model.objective = obj def add_constraints(self, constraints_lhs, constraints_type_and_rhs): """Add constraints to the mip model. Examples -------- >>> decision_variables = pd.DataFrame({ ... 'variable_id': [0, 1, 2, 3, 4, 5], ... 'lower_bound': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], ... 'upper_bound': [5.0, 5.0, 10.0, 10.0, 5.0, 5.0], ... 'type': ['continuous', 'continuous', 'continuous', ... 'continuous', 'continuous', 'continuous']}) >>> constraints_lhs = pd.DataFrame({ ... 'constraint_id': [1, 1, 2, 2], ... 'variable_id': [0, 1, 3, 4], ... 'coefficient': [1.0, 0.5, 1.0, 2.0]}) >>> constraints_type_and_rhs = pd.DataFrame({ ... 'constraint_id': [1, 2], ... 'type': ['<=', '='], ... 'rhs': [10.0, 20.0]}) >>> si = InterfaceToSolver() >>> si.add_variables(decision_variables) >>> si.add_constraints(constraints_lhs, constraints_type_and_rhs) >>> print(si.mip_model.constr_by_name('1')) 1: +1.0 0 +0.5 1 <= 10.0 >>> print(si.mip_model.constr_by_name('2')) 2: +1.0 3 +2.0 4 = 20.0 """ constraints_lhs = constraints_lhs.groupby( ['constraint_id', 'variable_id'], as_index=False).agg({'coefficient': 'sum'}) rows = constraints_lhs.groupby(['constraint_id'], as_index=False) # Make a dictionary so constraint rhs values can be accessed using the constraint id. rhs = dict( zip(constraints_type_and_rhs['constraint_id'], constraints_type_and_rhs['rhs'])) # Make a dictionary so constraint type can be accessed using the constraint id. enq_type = dict( zip(constraints_type_and_rhs['constraint_id'], constraints_type_and_rhs['type'])) var_ids = constraints_lhs['variable_id'].to_numpy() vars = np.asarray([ self.variables[k] if k in self.variables.keys() else None for k in range(0, max(var_ids) + 1) ]) coefficients = constraints_lhs['coefficient'].to_numpy() for row_id, row in rows.indices.items(): # Use the variable_ids to get mip variable objects present in the constraints lhs_variables = vars[var_ids[row]] # Use the positions of the non nan values to the lhs coefficients. lhs = coefficients[row] # Multiply and the variables by their coefficients and sum to create the lhs of the constraint. exp = lhs_variables * lhs exp = exp.tolist() exp = xsum(exp) # Add based on inequality type. if enq_type[row_id] == '<=': new_constraint = exp <= rhs[row_id] elif enq_type[row_id] == '>=': new_constraint = exp >= rhs[row_id] elif enq_type[row_id] == '=': new_constraint = exp == rhs[row_id] else: raise ValueError( "Constraint type not recognised should be one of '<=', '>=' or '='." ) self.mip_model.add_constr(new_constraint, name=str(row_id)) self.linear_mip_model.add_constr(new_constraint, name=str(row_id)) def optimize(self): """Optimize the mip model. If an optimal solution cannot be found and the investigate_infeasibility flag is set to True then remove constraints until a feasible solution is found. Examples -------- >>> decision_variables = pd.DataFrame({ ... 'variable_id': [0, 1, 2, 3, 4, 5], ... 'lower_bound': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], ... 'upper_bound': [5.0, 5.0, 10.0, 10.0, 5.0, 5.0], ... 'type': ['continuous', 'continuous', 'continuous', ... 'continuous', 'continuous', 'continuous']}) >>> constraints_lhs = pd.DataFrame({ ... 'constraint_id': [1, 1, 2, 2], ... 'variable_id': [0, 1, 3, 4], ... 'coefficient': [1.0, 0.5, 1.0, 2.0]}) >>> constraints_type_and_rhs = pd.DataFrame({ ... 'constraint_id': [1, 2], ... 'type': ['<=', '='], ... 'rhs': [10.0, 20.0]}) >>> si = InterfaceToSolver() >>> si.add_variables(decision_variables) >>> si.add_constraints(constraints_lhs, constraints_type_and_rhs) >>> si.optimize() >>> decision_variables['value'] = si.get_optimal_values_of_decision_variables(decision_variables) >>> print(decision_variables) variable_id lower_bound upper_bound type value 0 0 0.0 5.0 continuous 0.0 1 1 0.0 5.0 continuous 0.0 2 2 0.0 10.0 continuous 0.0 3 3 0.0 10.0 continuous 10.0 4 4 0.0 5.0 continuous 5.0 5 5 0.0 5.0 continuous 0.0 """ status = self.mip_model.optimize() if status != OptimizationStatus.OPTIMAL: # Attempt find constraint causing infeasibility. print('Model infeasible attempting to find problem constraint.') con_index = find_problem_constraint(self.mip_model) print( 'Couldn\'t find an optimal solution, but removing con {} fixed INFEASIBLITY' .format(con_index)) raise ValueError('Linear program infeasible') def get_optimal_values_of_decision_variables(self, variable_definitions): """Get the optimal values for each decision variable. Examples -------- >>> decision_variables = pd.DataFrame({ ... 'variable_id': [0, 1, 2, 3, 4, 5], ... 'lower_bound': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], ... 'upper_bound': [5.0, 5.0, 10.0, 10.0, 5.0, 5.0], ... 'type': ['continuous', 'continuous', 'continuous', ... 'continuous', 'continuous', 'continuous']}) >>> constraints_lhs = pd.DataFrame({ ... 'constraint_id': [1, 1, 2, 2], ... 'variable_id': [0, 1, 3, 4], ... 'coefficient': [1.0, 0.5, 1.0, 2.0]}) >>> constraints_type_and_rhs = pd.DataFrame({ ... 'constraint_id': [1, 2], ... 'type': ['<=', '='], ... 'rhs': [10.0, 20.0]}) >>> si = InterfaceToSolver() >>> si.add_variables(decision_variables) >>> si.add_constraints(constraints_lhs, constraints_type_and_rhs) >>> si.optimize() >>> decision_variables['value'] = si.get_optimal_values_of_decision_variables(decision_variables) >>> print(decision_variables) variable_id lower_bound upper_bound type value 0 0 0.0 5.0 continuous 0.0 1 1 0.0 5.0 continuous 0.0 2 2 0.0 10.0 continuous 0.0 3 3 0.0 10.0 continuous 10.0 4 4 0.0 5.0 continuous 5.0 5 5 0.0 5.0 continuous 0.0 """ values = variable_definitions['variable_id'].apply( lambda x: self.mip_model.var_by_name(str(x)).x, self.mip_model) return values def get_optimal_values_of_decision_variables_lin(self, variable_definitions): values = variable_definitions['variable_id'].apply( lambda x: self.linear_mip_model.var_by_name(str(x)).x, self.mip_model) return values def get_slack_in_constraints(self, constraints_type_and_rhs): """Get the slack values in each constraint. Examples -------- >>> decision_variables = pd.DataFrame({ ... 'variable_id': [0, 1, 2, 3, 4, 5], ... 'lower_bound': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], ... 'upper_bound': [5.0, 5.0, 10.0, 10.0, 5.0, 5.0], ... 'type': ['continuous', 'continuous', 'continuous', ... 'continuous', 'continuous', 'continuous']}) >>> constraints_lhs = pd.DataFrame({ ... 'constraint_id': [1, 1, 2, 2], ... 'variable_id': [0, 1, 3, 4], ... 'coefficient': [1.0, 0.5, 1.0, 2.0]}) >>> constraints_type_and_rhs = pd.DataFrame({ ... 'constraint_id': [1, 2], ... 'type': ['<=', '='], ... 'rhs': [10.0, 20.0]}) >>> si = InterfaceToSolver() >>> si.add_variables(decision_variables) >>> si.add_constraints(constraints_lhs, constraints_type_and_rhs) >>> si.optimize() >>> constraints_type_and_rhs['slack'] = si.get_slack_in_constraints(constraints_type_and_rhs) >>> print(constraints_type_and_rhs) constraint_id type rhs slack 0 1 <= 10.0 10.0 1 2 = 20.0 0.0 """ slack = constraints_type_and_rhs['constraint_id'].apply( lambda x: self.mip_model.constr_by_name(str(x)).slack, self.mip_model) return slack def price_constraints(self, constraint_ids_to_price): """For each constraint_id find the marginal value of the constraint. This is done by incrementing the constraint by a value of 1.0 and re-optimizing the model, the marginal cost of the constraint is increase in the objective function value between model runs. Examples -------- >>> decision_variables = pd.DataFrame({ ... 'variable_id': [0, 1, 2, 3, 4, 5], ... 'lower_bound': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], ... 'upper_bound': [5.0, 5.0, 10.0, 10.0, 5.0, 5.0], ... 'type': ['continuous', 'continuous', 'continuous', ... 'continuous', 'continuous', 'continuous']}) >>> objective_function = pd.DataFrame({ ... 'variable_id': [0, 1, 2, 3, 4, 5], ... 'cost': [1.0, 3.0, 10.0, 8.0, 9.0, 7.0]}) >>> constraints_lhs = pd.DataFrame({ ... 'constraint_id': [1, 1, 1, 1], ... 'variable_id': [0, 1, 3, 4], ... 'coefficient': [1.0, 1.0, 1.0, 1.0]}) >>> constraints_type_and_rhs = pd.DataFrame({ ... 'constraint_id': [1], ... 'type': ['='], ... 'rhs': [20.0]}) >>> si = InterfaceToSolver() >>> si.add_variables(decision_variables) >>> si.add_constraints(constraints_lhs, constraints_type_and_rhs) >>> si.add_objective_function(objective_function) >>> si.optimize() >>> si.linear_mip_model.optimize() <OptimizationStatus.OPTIMAL: 0> >>> prices = si.price_constraints([1]) >>> print(prices) {1: 8.0} >>> decision_variables['value'] = si.get_optimal_values_of_decision_variables(decision_variables) >>> print(decision_variables) variable_id lower_bound upper_bound type value 0 0 0.0 5.0 continuous 5.0 1 1 0.0 5.0 continuous 5.0 2 2 0.0 10.0 continuous 0.0 3 3 0.0 10.0 continuous 10.0 4 4 0.0 5.0 continuous 0.0 5 5 0.0 5.0 continuous 0.0 """ costs = {} for id in constraint_ids_to_price: costs[id] = self.linear_mip_model.constr_by_name(str(id)).pi return costs def update_rhs(self, constraint_id, violation_degree): constraint = self.linear_mip_model.constr_by_name(str(constraint_id)) constraint.rhs += violation_degree def update_variable_bounds(self, new_bounds): for variable_id, lb, ub in zip(new_bounds['variable_id'], new_bounds['lower_bound'], new_bounds['upper_bound']): self.mip_model.var_by_name(str(variable_id)).lb = lb self.mip_model.var_by_name(str(variable_id)).ub = ub def disable_variables(self, variables): for var_id in variables['variable_id']: var = self.linear_mip_model.var_by_name(str(var_id)) var.lb = 0.0 var.ub = 0.0
def generate_allocation(utilities: Dict[int, List[float]], criterion: str) -> Dict[int, List[int]]: """ :param utilities: the dictionary representing the utility profile, where each key is an agent and its value an array of floats such that the i-th float is the utility of the i-th item for the key-agent. :param criterion: the string which represents the criteria by which the item allocation is performed. This can be either 'max_utilitarian', 'min_enviness', 'min_unproportional' and 'random'. :return: a dictionary where each key is an agent and its corresponding value is the list of item which were assigned to her. """ assert len(set(len(utilities[a]) for a in utilities.keys())) == 1, 'All agents should have a utility for the same '\ 'number of items.' assert len(utilities) <= len(list(utilities.values())[0]), 'The number of agents should be smaller than or equal ' \ 'to the number of items.' criteria = { 'max_utilitarian': max_utilitarian_welfare_allocation, 'min_enviness': min_enviness_allocation, 'min_unproportionality': min_unproportionality_allocation, 'random': random_allocation } if criterion == 'random': allocation = criteria[criterion](list( utilities.keys()), list(range(len(list(utilities.values())[0])))) elif criterion in criteria: m = Model(name=criterion) agents, items = len(utilities), len(list(utilities.values())[0]) allocations = [ m.add_var(name='assign_{}_{}'.format(item, agent), var_type=BINARY) for item in range(items) for agent in range(agents) ] for item in range(items): m += xsum( m.var_by_name('assign_{}_{}'.format(item, agent)) for agent in range(agents)) == 1 for agent in range(agents): m += xsum( m.var_by_name('assign_{}_{}'.format(item, agent)) for item in range(items)) >= 1 criteria[criterion](utilities, m) allocation = {} for agent in range(agents): items_assigned = [] for item in range(items): if m.var_by_name('assign_{}_{}'.format(item, agent)).x == 1: items_assigned += [item] allocation.update({agent: items_assigned}) else: raise Exception( "Criterion {} has not been implemented yet.".format(criterion)) return allocation
def parameterized_tree_lef1_position_assignment( T: nx.DiGraph, root: int, agents: List[int], utils: Dict[int, List[float]], item_utils: Dict[int, List[float]], item_allocation: Dict[int, List[int]]) -> Model: """ Solves an integer linear program (ILP) to determine whether there is an LEF1 position assignment in a tree. :param T: the input graph, which must be a tree. :param root: the integer which corresponds to the tree's root. :param agents: the list of agents. :param utils: the dictionary of utilities given a fixed item allocation, where each agent is a key and its value is a list of floats such that the i-th float is the utility of the bundle assigned to agent i for the key-agent. :param item_utils: the dictionary which represents the utilities of each item for each agent, where each key is an agent and its value a list of floats where the float in position i is the utility of item i for the key-agent. :param item_allocation: the fixed item allocation, represented as a dictionary where each key is an agent and the value is the agent's allocated bundle represented as a list. :return: an optimized mip.Model which constraints are the same as in the proof of Theorem 8. """ assert type( T ) == nx.classes.digraph.DiGraph, 'Input graph should be a directed graph.' assert nx.algorithms.tree.is_tree(T), 'Input graph should be a tree.' assert len([n for n in nx.nodes(T)]) == len(agents), \ 'Input tree should have a number of nodes equal to the number of input agents.' m = Model("Tree_ILP_Position_Assignment") up_one_output = find_envy_up_one_agent_types(agents, utils, item_utils, item_allocation) up_one_bools, agent_types = up_one_output[0], up_one_output[1] vertex_types = find_vertex_types(T) vertex_types_edges = find_vertex_types_edges(T, root, vertex_types) edges = [ m.add_var(name='edge_{}_{}_{}_{}'.format(a1, a2, v1, v2), var_type=INTEGER) for a1 in agent_types.keys() for a2 in agent_types.keys() for v1 in vertex_types.keys() for v2 in vertex_types.keys() ] roots = [ m.add_var(name='root_{}_{}'.format(a, v), var_type=BINARY) for a in agent_types.keys() for v in vertex_types.keys() ] for v in vertex_types.keys(): is_root_type = 1 if root in vertex_types[v] else 0 m += xsum(m.var_by_name('root_' + str(a) + '_' + str(v)) for a in agent_types.keys()) == \ is_root_type for a1 in agent_types.keys(): m += \ xsum(m.var_by_name('edge_' + str(a2) + '_' + str(a1) + '_' + str(v1) + '_' + str(v2)) for a2 in agent_types.keys() for v1 in vertex_types.keys() for v2 in vertex_types.keys()) + \ xsum(m.var_by_name('root_' + str(a1) + '_' + str(v)) for v in vertex_types.keys()) == len(agent_types[a1]) for v1 in vertex_types.keys(): for v2 in vertex_types.keys(): if root in vertex_types[v1]: m += \ xsum(m.var_by_name('edge_' + str(a1) + '_' + str(a2) + '_' + str(v1) + '_' + str(v2)) for a2 in agent_types.keys()) - \ m.var_by_name('root_' + str(a1) + '_' + str(v1)) * vertex_types_edges[v1][v2] == 0 else: m += \ xsum(m.var_by_name('edge_' + str(a1) + '_' + str(a2) + '_' + str(v1) + '_' + str(v2)) for a2 in agent_types.keys()) - \ vertex_types_edges[v1][v2] * \ xsum(m.var_by_name('edge_' + str(a2) + '_' + str(a1) + '_' + str(v3) + '_' + str(v1)) for a2 in agent_types.keys() for v3 in vertex_types.keys()) == 0 for a2 in agent_types.keys(): agent_a1, agent_a2 = agent_types[a1][0], agent_types[a2][0] if up_one_bools[a1][a2] == -1 or up_one_bools[a2][a1] < 0: m += m.var_by_name('edge_' + str(a1) + '_' + str(a2) + '_' + str(v1) + '_' + str(v2)) == 0 return m
class DispatchPlanner: def __init__(self, dispatch_interval, planning_horizon): self.dispatch_interval = dispatch_interval self.planning_horizon = planning_horizon self.regional_markets = [] self.price_traces_by_market = {} self.units = [] self.unit_energy_market_mapping = {} self.model = Model(solver_name='CBC', sense='MAX') self.unit_in_flow_variables = {} self.unit_out_flow_variables = {} self.units_with_storage = [] self.unit_storage_input_capacity = {} self.unit_storage_output_capacity = {} self.unit_storage_input_efficiency = {} self.unit_storage_output_efficiency = {} self.unit_storage_mwh = {} self.unit_storage_initial_mwh = {} self.unit_storage_level_variables = {} self.unit_initial_mw = {} self.market_dispatch_variables = {} self.market_net_dispatch_variables = {} self.nominal_price_forecast = {} self.expected_regions = ['qld', 'nsw', 'vic', 'sa', 'tas', 'mainland'] self.expected_service = ['energy'] self.unit_commitment_vars = {} self.unit_capacity = {} self.unit_min_loading = {} self.unit_min_down_time = {} self.unit_initial_state = {} self.unit_initial_down_time = {} self.forecast = None def get_model(self): return self.model def get_planning_horizon(self): return int(self.planning_horizon * self.dispatch_interval) def get_horizon_in_intervals(self): return self.planning_horizon def get_time_step(self): return self.dispatch_interval def add_regional_market(self, region, service, forecast): self.forecast = forecast if len(forecast.columns) > 2: self._add_elastic_price_regional_market(region, service, forecast) else: self._add_fixed_price_regional_market(region, service, forecast) def _add_fixed_price_regional_market(self, region, service, forecast): market_name = region + '-' + service self.regional_markets.append(market_name) if region not in self.market_net_dispatch_variables: self.market_net_dispatch_variables[region] = {} self.market_net_dispatch_variables[region][service] = {} forward_prices = forecast.set_index('interval') forward_prices = forward_prices[market_name] for i in range(0, self.planning_horizon): dispatch_var_name = "net_dispatch_{}_{}".format(market_name, i) self.market_net_dispatch_variables[region][service][i] = \ self.model.add_var(name=dispatch_var_name, lb=-1.0 * INF, ub=INF, obj=forward_prices[i]) def _add_elastic_price_regional_market(self, region, service, forward_price_traces): forward_price_traces = forward_price_traces.sort_values('interval') market_name = region + '-' + service self.regional_markets.append(market_name) positive_rate, positive_dispatch, negative_rate, negative_dispatch = \ self._marginal_market_trade(forward_price_traces) if region not in self.market_dispatch_variables: self.market_dispatch_variables[region] = {} self.market_dispatch_variables[region][service] = {} if region not in self.market_net_dispatch_variables: self.market_net_dispatch_variables[region] = {} self.market_net_dispatch_variables[region][service] = {} for i in range(0, self.planning_horizon): self.market_dispatch_variables[region][service][i] = {} self.market_dispatch_variables[region][service][i]['positive'] = {} self.market_dispatch_variables[region][service][i]['negative'] = {} self.market_net_dispatch_variables[region][service][i] = {} if len(positive_rate) > 0: for dispatch, rate in positive_rate[i].items(): dispatch_var_name = "dispatch_{}_{}_positive_{}".format(market_name, i, dispatch) self.market_dispatch_variables[region][service][i]['positive'][dispatch] = \ self.model.add_var(name=dispatch_var_name, lb=0.0, ub=positive_dispatch[i][dispatch], obj=rate) if len(negative_rate) > 0: for dispatch, rate in negative_rate[i].items(): dispatch_var_name = "dispatch_{}_{}_negative_{}".format(market_name, i, dispatch) self.market_dispatch_variables[region][service][i]['negative'][dispatch] = \ self.model.add_var(name=dispatch_var_name, lb=0.0, ub=negative_dispatch[i][dispatch], obj=rate) dispatch_var_name = "net_dispatch_{}_{}".format(market_name, i) self.market_net_dispatch_variables[region][service][i] = \ self.model.add_var(name=dispatch_var_name, lb=-1.0 * INF, ub=INF) positive_vars = list(self.market_dispatch_variables[region][service][i]['positive'].values()) negative_vars = list(self.market_dispatch_variables[region][service][i]['negative'].values()) self.model += xsum([-1 * self.market_net_dispatch_variables[region][service][i]] + positive_vars + [-1 * var for var in negative_vars]) == 0.0 @staticmethod def _get_revenue_traces(price_traces): for col in price_traces.columns: if col != 'interval': price_traces[col] = price_traces[col] * (col + 0.00001) return price_traces def _marginal_market_trade(self, price_traces): revenue_trace = self._get_revenue_traces(price_traces) value_columns = [col for col in revenue_trace.columns if col != 'interval'] stacked = pd.melt(revenue_trace, id_vars=['interval'], value_vars=value_columns, var_name='dispatch', value_name='revenue') positive = stacked[stacked['dispatch'] >= 0.0] negative = stacked[stacked['dispatch'] <= 0.0].copy() negative['dispatch'] = negative['dispatch'] * -1.0 positive = positive.sort_values('dispatch') negative = negative.sort_values('dispatch') positive['marginal_revenue'] = positive.groupby('interval', as_index=False)['revenue'].diff() negative['marginal_revenue'] = negative.groupby('interval', as_index=False)['revenue'].diff() positive['marginal_dispatch'] = positive.groupby('interval', as_index=False)['dispatch'].diff() negative['marginal_dispatch'] = negative.groupby('interval', as_index=False)['dispatch'].diff() positive = positive[positive['dispatch'] != 0.0] negative = negative[negative['dispatch'] != 0.0] positive['marginal_rate'] = positive['marginal_revenue'] / positive['marginal_dispatch'] negative['marginal_rate'] = negative['marginal_revenue'] / negative['marginal_dispatch'] positive = positive.set_index(['interval', 'dispatch']).loc[:, ['marginal_rate', 'marginal_dispatch']] negative = negative.set_index(['interval', 'dispatch']).loc[:, ['marginal_rate', 'marginal_dispatch']] positive_rate = positive.groupby(level=0).apply(lambda df: df.xs(df.name).marginal_rate.to_dict()).to_dict() positive_dispatch = positive.groupby(level=0).apply( lambda df: df.xs(df.name).marginal_dispatch.to_dict()).to_dict() negative_rate = negative.groupby(level=0).apply(lambda df: df.xs(df.name).marginal_rate.to_dict()).to_dict() negative_dispatch = negative.groupby(level=0).apply( lambda df: df.xs(df.name).marginal_dispatch.to_dict()).to_dict() return positive_rate, positive_dispatch, negative_rate, negative_dispatch def add_unit(self, unit): self.units.append(unit) def optimise(self): for unit in self.units: unit.create_constraints_to_balance_unit_energy_flows() unit.create_net_output_vars() self._create_constraints_to_balance_grid_nodes() self.model.optimize() def _create_constraints_to_balance_grid_nodes(self): for market in self.regional_markets: region, service = market.split('-') for i in range(0, self.planning_horizon): net_vars = [] for unit in self.units: if service in unit.service_region_mapping and unit.service_region_mapping[service] == region: net_vars.append(unit.net_dispatch_vars[service][i]) self.model += xsum([self.market_net_dispatch_variables[region][service][i]] + [-1 * var for var in net_vars]) == 0.0 def _create_constraints_to_balance_unit_nodes(self): for unit in self.units: for i in range(0, self.planning_horizon): in_flow_vars = [var for var_name, var in self.unit_in_flow_variables[unit][i].items()] out_flow_vars = [var for var_name, var in self.unit_out_flow_variables[unit][i].items()] if unit in self.unit_commitment_vars: min_loading_var = [self.unit_commitment_vars[unit]['state'][i] * self.unit_min_loading[unit] * -1] self.model += xsum(in_flow_vars + [-1 * var for var in out_flow_vars] + min_loading_var) == 0.0 else: self.model += xsum(in_flow_vars + [-1 * var for var in out_flow_vars]) == 0.0 def get_storage_energy_flows_and_state_of_charge(self, unit_name): if unit_name not in self.units_with_storage: raise ValueError('The unit specified does not have a storage component.') energy_flows = self.price_traces_by_market[self.regional_markets[0]].loc[:, ['interval']] def get_dispatch(self): trace = self.forward_data.loc[:, ['interval']] for market in self.regional_markets: trace[market + '-dispatch'] = \ trace['interval'].apply(lambda x: self.model.var_by_name(str("net_dispatch_{}_{}".format(market, x))).x, self.model) return trace def get_market_dispatch(self, market): trace = self.forecast.loc[:, ['interval']] trace['dispatch'] = \ trace['interval'].apply(lambda x: self.model.var_by_name(str("net_dispatch_{}_{}".format(market, x))).x, self.model) return trace def get_fcas_dispatch(self, unit_name): trace = self.forecast.loc[:, ['interval']] for service in self.expected_service: if service in self.unit_output_fcas_variables[unit_name] and service != 'energy': trace[service] = \ trace['interval'].apply( lambda x: self.unit_output_fcas_variables[unit_name][service][x].x,) return trace def get_template_trace(self): return self.forecast.loc[:, ['interval']]
def tsp_mip_solver(input_data): # parse the input lines = input_data.split('\n') nodeCount = int(lines[0]) points = [] for i in range(1, nodeCount + 1): line = lines[i] parts = line.split() points.append(Point(float(parts[0]), float(parts[1]))) print('Points parsed!') # calculate distance matrix d_m = [[length(q, p) for q in points] for p in points] print('Distance matrix ready!') # declare MIP model m = Model(solver_name='GRB') print('-Model instatiated!', datetime.datetime.now()) # states search emphasis # - '0' (default) balanced approach # - '1' (feasibility) aggressively searches for feasible solutions # - '2' (optimality) explores search space to tighten dual gap m.emphasis = 0 # whenever the distance of the lower and upper bounds is less or # equal max_gap*100%, the search can be finished m.max_gap = 0.05 # specifies number of used threads # 0 uses solver default configuration, # -1 uses the number of available processing cores # ≥1 uses the specified number of threads. # An increased number of threads may improve the solution time but also increases # the memory consumption. Each thread needs to store a different model instance! m.threads = 0 # controls the generation of cutting planes # cutting planes usually improve the LP relaxation bound but also make the solution time of the LP relaxation larger # -1 means automatic # 0 disables completely # 1 (default) generates cutting planes in a moderate way # 2 generates cutting planes aggressively # 3 generates even more cutting planes m.cuts = -1 m.preprocess = 1 m.pump_passes = 20 m.sol_pool_size = 1 nodes = set(range(nodeCount)) # instantiate "entering and leaving" variables x = [[m.add_var(name="x{}_{}".format(p, q), var_type='B') for q in nodes] for p in nodes] # instantiate subtour elimination variables y = [m.add_var(name="y{}".format(i)) for i in nodes] print('-Variables instantiated', datetime.datetime.now()) # declare objective function m.objective = minimize( xsum(d_m[i][j] * x[i][j] for i in nodes for j in nodes)) print('-Objective declared!', datetime.datetime.now()) # declare constraints # leave each city only once for i in tqdm(nodes): m.add_constr(xsum(x[i][j] for j in nodes - {i}) == 1) # enter each city only once for i in tqdm(nodes): m.add_constr(xsum(x[j][i] for j in nodes - {i}) == 1) # subtour elimination constraints for (i, j) in tqdm(product(nodes - {0}, nodes - {0})): if i != j: m.add_constr(y[i] - (nodeCount + 1) * x[i][j] >= y[j] - nodeCount) print('-Constraints declared!', datetime.datetime.now()) #Maximum time in seconds that the search can go on if a feasible solution #is available and it is not being improved mssi = 1000 #default = inf # specifies maximum number of nodes to be explored in the search tree (default = inf) mn = 1000000 #default = 1073741824 # optimize model m within a processing time limit of 'ms' seconds ms = 3000 #default = inf # executes the optimization print('-Optimizer start.', datetime.datetime.now()) #status = m.optimize(max_seconds = ms,max_seconds_same_incumbent = mssi,max_nodes = mn) status = m.optimize(max_seconds=ms, max_seconds_same_incumbent=mssi) print('Opt. Status:', status) print('MIP Sol. Obj.:', m.objective_value) print('Dual Bound:', m.objective_bound) print('Dual gap:', m.gap) sol = [0] c_node = 0 for j in range(nodeCount - 1): for i in range(nodeCount): if round(m.var_by_name("x{}_{}".format(c_node, i)).x) != 0: sol.append(i) c_node = i break obj = m.objective_value # prepare the solution in the specified output format if status == OptimizationStatus.OPTIMAL: output_data = '%.2f' % obj + ' ' + str(1) + '\n' output_data += ' '.join(map(str, sol)) elif status == OptimizationStatus.FEASIBLE: output_data = '%.2f' % obj + ' ' + str(0) + '\n' output_data += ' '.join(map(str, sol)) return output_data