In [1]:
!cat max_flow_interdict.py


import pandas
import pyomo
import pyomo.opt
import pyomo.environ as pe
import logging

class MaxFlowInterdiction:
    """A class to compute max-flow interdictions."""

    def __init__(self, nodefile, arcfile, attacks=0):
        """
        All the files are CSVs with columns described below.  Attacks is the number of attacks.

        - nodefile:
            Node

        Every node must appear as a line in the nodefile. There are two special nodes called 'Start' and 'End' that define the source and sink of the max-flow problem. 

        - arcfile:
            StartNode,EndNode,Capacity,Attackable

        Every arc must appear in the arcfile.  The data also describes the arc's capacity and whether we can attack this arc.
        """
        # Read in the node_data
        self.node_data = pandas.read_csv(nodefile)
        self.node_data.set_index(['Node'], inplace=True)
        self.node_data.sort_index(inplace=True)
        # Read in the arc_data
        self.arc_data = pandas.read_csv(arcfile)
        self.arc_data['xbar'] = 0
        self.arc_data.set_index(['StartNode','EndNode'], inplace=True)
        self.arc_data.sort_index(inplace=True)

        self.attacks = attacks
     
        self.node_set = self.node_data.index.unique()
        self.arc_set = self.arc_data.index.unique()
        
        self.createPrimal()
        self.createInterdictionDual()


    def createPrimal(self):  
        """Create the primal pyomo model.  
        
        This is used to compute flows after interdiction.  The interdiction is stored in arc_data.xbar."""

        model = pe.ConcreteModel()
        # Tell pyomo to read in dual-variable information from the solver
        model.dual = pe.Suffix(direction=pe.Suffix.IMPORT) 

        # Add the sets
        model.node_set = pe.Set( initialize=self.node_set )
        model.edge_set = pe.Set( initialize=self.arc_set, dimen=2)

        # Create the variables
        model.y = pe.Var(model.edge_set, domain=pe.NonNegativeReals) 
        model.v = pe.Var(domain=pe.NonNegativeReals)

        
        # Create the objective
        def obj_rule(model):
            return  model.v - 1.1*sum( data['xbar']*model.y[e] for e,data in self.arc_data.iterrows())
        model.OBJ = pe.Objective(rule=obj_rule, sense=pe.maximize)

        # Create the constraints, one for each node
        def flow_bal_rule(model, n):
            tmp = self.arc_data.reset_index()
            successors = tmp.ix[ tmp.StartNode == n, 'EndNode'].values
            predecessors = tmp.ix[ tmp.EndNode == n, 'StartNode'].values 
            lhs = sum(model.y[(i,n)] for i in predecessors) - sum(model.y[(n,i)] for i in successors) 
            start_node = int(n == 'Start')
            end_node = int(n == 'End')
            rhs = 0 - model.v*(start_node) + model.v*(end_node)
            constr = (lhs == rhs)
            if isinstance(constr, bool):
                return pe.Constraint.Skip
            return constr

        model.FlowBalance = pe.Constraint(model.node_set, rule=flow_bal_rule)
        
        # Capacity constraints, one for each edge
        def capacity_rule(model, i, j):
            capacity = self.arc_data['Capacity'].get((i,j),-1)
            if capacity < 0:
                return pe.Constraint.Skip
            return model.y[(i,j)] <= capacity 

        model.Capacity = pe.Constraint(model.edge_set, rule=capacity_rule)
 
        # Store the model
        self.primal = model

    def createInterdictionDual(self):
        # Create the model
        model = pe.ConcreteModel()
        
        # Add the sets
        model.node_set = pe.Set( initialize=self.node_set )
        model.edge_set = pe.Set( initialize=self.arc_set, dimen=2)

        # Create the variables
        model.rho = pe.Var(model.node_set, domain=pe.Reals)
        model.pi = pe.Var(model.edge_set, domain=pe.NonNegativeReals)
        
        model.x = pe.Var(model.edge_set, domain=pe.Binary)

        # Create the objective
        def obj_rule(model):
            return  sum(data['Capacity']*model.pi[e] for e,data in self.arc_data.iterrows() if data['Capacity']>=0)

        model.OBJ = pe.Objective(rule=obj_rule, sense=pe.minimize)

        # Create the constraints for y_ij
        def edge_constraint_rule(model, i, j):
            attackable = int(self.arc_data['Attackable'].get((i,j),0))
            hasCap = int(self.arc_data['Capacity'].get((i,j),-1)>=0)
            return model.rho[j] - model.rho[i] + model.pi[(i,j)]*hasCap >=  0 - 1.1*model.x[(i,j)]*attackable

        model.DualEdgeConstraint = pe.Constraint(model.edge_set, rule=edge_constraint_rule)

        # Set the x's for non-blockable arcs
        def v_constraint_rule(model):
            return model.rho['Start'] - model.rho['End'] == 1

        model.VConstraint = pe.Constraint(rule=v_constraint_rule)
     
        # Create the interdiction budget constraint 
        def block_limit_rule(model):
            model.attacks = self.attacks
            return pe.summation(model.x) <= model.attacks

        model.BlockLimit = pe.Constraint(rule=block_limit_rule)

        # Create, save the model
        self.Idual = model

    def solve(self, tee=False):
        solver = pyomo.opt.SolverFactory('gurobi')

        # Solve the dual first
        self.Idual.BlockLimit.construct()
        self.Idual.BlockLimit._constructed = False
        del self.Idual.BlockLimit._data[None] 
        self.Idual.BlockLimit.reconstruct()
        self.Idual.preprocess()
        results = solver.solve(self.Idual, tee=tee, keepfiles=False, options_string="mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0")

        # Check that we actually computed an optimal solution, load results
        if (results.solver.status != pyomo.opt.SolverStatus.ok):
            logging.warning('Check solver not ok?')
        if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal):  
            logging.warning('Check solver optimality?')

        self.Idual.solutions.load_from(results)
        # Now put interdictions into xbar and solve primal
       
        for e in self.arc_data.index:
            self.arc_data.ix[e,'xbar'] = self.Idual.x[e].value

        self.primal.OBJ.construct()
        self.primal.OBJ._constructed = False
        self.primal.OBJ._init_sense = pe.maximize
        del self.primal.OBJ._data[None] 
        self.primal.OBJ.reconstruct()
        self.primal.preprocess()
        results = solver.solve(self.primal, tee=tee, keepfiles=False, options_string="mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0")

        # Check that we actually computed an optimal solution, load results
        if (results.solver.status != pyomo.opt.SolverStatus.ok):
            logging.warning('Check solver not ok?')
        if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal):  
            logging.warning('Check solver optimality?')

        self.primal.solutions.load_from(results)

    def printSolution(self):
        print()
        print('Using %d attacks:'%self.attacks)
        print()
        edges = sorted(self.arc_set)
        for e in edges:
            if self.Idual.x[e].value > 0:
                print('Interdict arc %s -> %s'%(str(e[0]), str(e[1])))
        print()
         
        for e0,e1 in self.arc_set:
            flow = self.primal.y[(e0,e1)].value
            if flow > 0:
                print('Flow on arc %s -> %s: %.2f'%(str(e0), str(e1), flow))
        print()

        print('----------')
        print('Total flow = %.2f (primal) %.2f (dual)'%(self.primal.OBJ(), self.Idual.OBJ()))


########################
# Now lets do something
########################

if __name__ == '__main__':
    m = MaxFlowInterdiction('sample_nodes_data.csv', 'sample_arcs_data.csv')
    m.solve()
    m.printSolution()
    m.attacks = 1
    m.solve()
    m.printSolution()
    m.attacks = 2
    m.solve()
    m.printSolution()

In [2]:
!python max_flow_interdict.py


Using 0 attacks:


Flow on arc B -> End: 40.00
Flow on arc C -> B: 30.00
Flow on arc C -> D: 40.00
Flow on arc D -> End: 40.00
Flow on arc Start -> B: 10.00
Flow on arc Start -> C: 70.00

----------
Total flow = 80.00 (primal) 80.00 (dual)

Using 1 attacks:

Interdict arc Start -> C

Flow on arc B -> End: 10.00
Flow on arc Start -> B: 10.00

----------
Total flow = 10.00 (primal) 10.00 (dual)

Using 2 attacks:

Interdict arc B -> End
Interdict arc D -> End


----------
Total flow = 0.00 (primal) 0.00 (dual)