Example #1
0
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
Example #4
0
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()
Example #5
0
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
Example #6
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
Example #8
0
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