Search code examples
pythonoptimizationconstraintspyomo

Set complicated constraints in mixed integer programming problem in pyomo


In my optimization problem I have to allocate as much wagons as possible to its optimal routes for minimum cost with defined constraints (multi objective problem). At first step I have to distribute as much wagons as possible (maximum amount) and the second step of optimization is to allocate its number to the cheapest routes. I have the number of wagons from each station to distribute and I have the capacity at each station of arrival. For simplicity, I have only two stations of departure where rem_type column means the type of wagon at this station. Table df_vrp contains information about the capacity and table df_unload contains information about the number of wagons at each departure station which we have to allocate to routes.It is clear for me how to the maximum possible number of wagons from station to station in regards with constraints and then find the cheapest routes. But, in my case, I have to take in account the proportion of distributes wagons such that the sum of distriubuted wagons of Type 2 must be equal to 1/3 of the sum of distributed wagons of Type 1. So I cannot understand how to create such constraint because 1/3 can be done in many ways (1 wagon of Type 2 and 3 wagons of Type 1, 3 wagons of Type 2 and 9 wagons of Type 1 and so on).

import pandas as pd
import numpy as np
from pyomo.environ import *

data = {'rem_type' : ['Type1', 'Type1', 'Type2', 'Type2', 'Type1', 'Type1', 'Type2', 'Type2'],
        'station_depart': ['A', 'A', 'A', 'A', 'F', 'F', 'F', 'F'],
        'station_arrive': ['B', 'C', 'B', 'C', 'B', 'C', 'B', 'C'],
        'cost': [100, 103, 111, 101, 105, 114, 95, 99]}

  
df_route = pd.DataFrame(data)

# unload
df_unload = pd.DataFrame({'rem_type' : ['Type1', 'Type2', 'Type1', 'Type2'], 
                          'station_depart' : ['A', 'A', 'F', 'F'],
                          'volume' : [5, 6, 4, 7]})

# repair
df_vrp = pd.DataFrame({'rem_type' : ['Type1', 'Type2'], 
                       'station_arrive' : ['B', 'C'],
                       'capacity' : [15, 30]})

# create routes
routes_unload = dict()   
routes_vrp = dict()    

# create model
model = ConcreteModel("OP")

# create multi objective function
model.Models = ObjectiveList()

# decision vars
model.x = Var([idx for idx in df_route.index], domain=NonNegativeIntegers)

# obj function to maximize the number of allocated wags to routes
model.Size = Objective(expr=sum([model.x[i] for i in model.x]), sense=maximize)

# obj function to minimize cost of those wags from the first solution
model.Cost = Objective(
    expr=sum([df_route.loc[idx, "cost"] * model.x[idx] for idx in df_route.index]),
    sense=minimize,
)

model.Size.activate()
model.Cost.deactivate()

# routes with indexes for creating constraints
for idx in tqdm(df_route.index):
    vrp = (df_route.loc[idx, "rem_type"], df_route.loc[idx, "station_arrive"])
    if vrp not in routes_vrp:
        routes_vrp[vrp] = list()
    routes_vrp[vrp].append(idx)

    t_unload = (df_route.loc[idx, "rem_type"], df_route.loc[idx, "station_depart"])
    if t_unload not in routes_unload:
        routes_unload[t_unload] = list()
    routes_unload[t_unload].append(idx)

# constraints on the arrive/repair station
model.vrp = ConstraintList()
for v in df_vrp.index:
    t = (df_vrp.loc[v, "rem_type"], df_vrp.loc[v, "station_arrive"])
    if t in routes_vrp:
        model.vrp.add(
            sum([model.x[idx] for idx in routes_vrp[t]]) <= df_vrp.loc[v, "capacity"]
        )

# constraints on the depart station (how many wagons we have for allocation)
model.unload = ConstraintList()
for u in df_unload.index:
    t = (df_unload.loc[u, "rem_type"], df_unload.loc[u, "station_depart"])
    if t in routes_unload:
        model.unload.add(
            sum([model.x[idx] for idx in routes_unload[t]]) <= df_unload.loc[u, "volume"]
        )

results = SolverFactory("glpk").solve(model)

results.write()

# the result from the first objective function as the constraint to the second one
model.fix_count = Constraint(expr=sum([model.x[i] for i in model.x]) == model.Size())

model.Size.deactivate()
model.Cost.activate()

results = SolverFactory("glpk").solve(model)

results.write()

# create df with results of allocation to the routes
vals = []
for i in tqdm(model.x):
    vals.append(model.x[i].value)
df_results = df_route.dropna(
    subset=["rem_type", "station_depart", "station_arrive", "cost"],
    how="all",
)

df_results["total"] = vals

Solution

  • Here is a complete answer, based on your updated code. I went a little "geek" on this, but in thinking it over, you helped me with a different problem I've been working on, so we're GTG.

    I think one of the main issues with your base code is that you are trying to force pandas into a pyomo box and it gets way difficult very quick and the code becomes pretty confusing. I'd recommend ditching pandas as quickly as possible for clarity and your ability to work with sets more cleanly because as you were doing it before, you have were indexing by the enumeration of all the routes, and you'd really like to be able to make constraints based on things like destinations, types of vehicles, etc. which is wayyy hard to do if you don't set it up that way.

    I also brought all of the parameters into the model. This is optional, but I think a good practice to be able to pprint() the whole model or parts of it more cleanly.

    If you have a non-complete set of pairs of routes and types, there is a bit more work to do. Meaning, if there are routes that only host 1 type of vehicle, you'd have to do a little filtering or use indexed sets or such, but right now all types are compatible on all routes, so this works.

    Note the result of the 2nd pass flips some of the results to minimize cost, which is your target.

    CODE:

    import pandas as pd  # <--- try to avoid this, unless needed to curate the data
    # import numpy as np
    from pyomo.environ import *
    
    data = {'rem_type' : ['Type1', 'Type1', 'Type2', 'Type2', 'Type1', 'Type1', 'Type2', 'Type2'],
            'station_depart': ['A', 'A', 'A', 'A', 'F', 'F', 'F', 'F'],
            'station_arrive': ['B', 'C', 'B', 'C', 'B', 'C', 'B', 'C'],
            'cost': [100, 103, 111, 101, 105, 114, 95, 99]}
    df_route = pd.DataFrame(data)
    
    costs = df_route.set_index(['station_depart', 'station_arrive', 'rem_type']).to_dict()['cost']
    
    # unload
    df_unload = pd.DataFrame({'rem_type' : ['Type1', 'Type2', 'Type1', 'Type2'], 
                              'station_depart' : ['A', 'A', 'F', 'F'],
                              'volume' : [5, 6, 4, 7]})
    demands = df_unload.set_index(['station_depart', 'rem_type']).to_dict()['volume']
    
    # repair
    df_vrp = pd.DataFrame({'rem_type' : ['Type1', 'Type2'], 
                           'station_arrive' : ['B', 'C'],
                           'capacity' : [15, 30]})
    capacity = df_vrp.set_index(['station_arrive', 'rem_type']).to_dict()['capacity']
    
    # find the set of available routes and others conveniences
    routes = {(s, t) for (s, t, cost) in costs.keys()}
    start_nodes = {k[0] for k in costs.keys()}
    end_nodes = {k[1] for k in costs.keys()}
    all_nodes = start_nodes | end_nodes
    
    # Target Ratios   
    ratios =    {('Type1', 'Type2') : (3, 1)
                  # others
                }
    
    # MODEL
    
    # create model
    model = ConcreteModel("OP")
    
    # SETS
    model.N = Set(initialize=sorted(all_nodes))
    model.S = Set(within=model.N, initialize=sorted(start_nodes))       # the set of start nodes
    model.T = Set(within=model.N, initialize=sorted(end_nodes))         # the set of terminations
    model.R = Set(within=model.N * model.N, initialize=sorted(routes))  # the set of connections
    model.V = Set(initialize=['Type1', 'Type2'])                        # vehicle types
    model.ratio_pairs = Set(within=model.V * model.V, initialize=ratios.keys())
    
    # PARAMS
    model.cost         = Param(model.R, model.V, initialize=costs)
    model.demand       = Param(model.S, model.V, initialize=demands, default=0)  # the demand to send from s of type v
    model.capacity     = Param(model.T, model.V, initialize=capacity, default=0) # the capacity to accept at t type v
    model.ratio_limits = Param(model.ratio_pairs, initialize=ratios, domain=Any)
    
    # VARS
    model.send = Var(model.R, model.V, domain=NonNegativeIntegers)  # send this number of vehicles of type v on route r
    
    # OBJ1: Route as much as possible
    model.obj1 = Objective(expr=sum_product(model.send), sense=maximize)
    
    # CONSTRAINTS
    
    @model.Constraint(model.S, model.V)
    def demand_limit(model, s, v):
        return sum(model.send[r, v] for r in model.R if r[0] == s) <= model.demand[s, v]
    
    @model.Constraint(model.T, model.V)
    def capacity_limit(model, t, v):
        return sum(model.send[r, v] for r in model.R if r[1] == t) <= model.capacity[t, v]
    
    # the ratio constraint...
    @model.Constraint(model.ratio_pairs)
    def ratio_limit(model, v1, v2):
        return sum(model.send[r, v1] for r in model.R) * model.ratio_limits[v1, v2][0] == \
               sum(model.send[r, v2] for r in model.R) * model.ratio_limits[v1, v2][1]
    
    
    # solve first pass...
    results = SolverFactory("glpk").solve(model)
    results.write()
    model.send.display()
    model.ratio_limit.pprint()
    
    # now, add a constraint to make sure we deliver at least as much as before and introduce a
    # new cost objective
    
    model.min_send = Constraint(expr=sum_product(model.send) >= value(model.obj1))
    
    model.obj2 = Objective(expr=sum_product(model.cost, model.send), sense=minimize)
    
    model.obj1.deactivate()
    
    results = SolverFactory("glpk").solve(model)
    
    results.write()
    model.send.display()
    

    OUTPUT:

    # ==========================================================
    # = Solver Results                                         =
    # ==========================================================
    # ----------------------------------------------------------
    #   Problem Information
    # ----------------------------------------------------------
    Problem: 
    - Name: unknown
      Lower bound: 16.0
      Upper bound: 16.0
      Number of objectives: 1
      Number of constraints: 10
      Number of variables: 9
      Number of nonzeros: 25
      Sense: maximize
    # ----------------------------------------------------------
    #   Solver Information
    # ----------------------------------------------------------
    Solver: 
    - Status: ok
      Termination condition: optimal
      Statistics: 
        Branch and bound: 
          Number of bounded subproblems: 9
          Number of created subproblems: 9
      Error rc: 0
      Time: 0.006043910980224609
    # ----------------------------------------------------------
    #   Solution Information
    # ----------------------------------------------------------
    Solution: 
    - number of solutions: 0
      number of solutions displayed: 0
    send : Size=8, Index=send_index
        Key                 : Lower : Value : Upper : Fixed : Stale : Domain
        ('A', 'B', 'Type1') :     0 :   0.0 :  None : False : False : NonNegativeIntegers
        ('A', 'B', 'Type2') :     0 :   0.0 :  None : False : False : NonNegativeIntegers
        ('A', 'C', 'Type1') :     0 :   0.0 :  None : False : False : NonNegativeIntegers
        ('A', 'C', 'Type2') :     0 :   5.0 :  None : False : False : NonNegativeIntegers
        ('F', 'B', 'Type1') :     0 :   4.0 :  None : False : False : NonNegativeIntegers
        ('F', 'B', 'Type2') :     0 :   0.0 :  None : False : False : NonNegativeIntegers
        ('F', 'C', 'Type1') :     0 :   0.0 :  None : False : False : NonNegativeIntegers
        ('F', 'C', 'Type2') :     0 :   7.0 :  None : False : False : NonNegativeIntegers
    ratio_limit : Size=1, Index=ratio_pairs, Active=True
        Key                : Lower : Body                                                                                                                                                : Upper : Active
        ('Type1', 'Type2') :   0.0 : (send[A,B,Type1] + send[A,C,Type1] + send[F,B,Type1] + send[F,C,Type1])*3 - (send[A,B,Type2] + send[A,C,Type2] + send[F,B,Type2] + send[F,C,Type2]) :   0.0 :   True
    # ==========================================================
    # = Solver Results                                         =
    # ==========================================================
    # ----------------------------------------------------------
    #   Problem Information
    # ----------------------------------------------------------
    Problem: 
    - Name: unknown
      Lower bound: 1598.0
      Upper bound: 1598.0
      Number of objectives: 1
      Number of constraints: 11
      Number of variables: 9
      Number of nonzeros: 33
      Sense: minimize
    # ----------------------------------------------------------
    #   Solver Information
    # ----------------------------------------------------------
    Solver: 
    - Status: ok
      Termination condition: optimal
      Statistics: 
        Branch and bound: 
          Number of bounded subproblems: 1
          Number of created subproblems: 1
      Error rc: 0
      Time: 0.00722813606262207
    # ----------------------------------------------------------
    #   Solution Information
    # ----------------------------------------------------------
    Solution: 
    - number of solutions: 0
      number of solutions displayed: 0
    send : Size=8, Index=send_index
        Key                 : Lower : Value : Upper : Fixed : Stale : Domain
        ('A', 'B', 'Type1') :     0 :   4.0 :  None : False : False : NonNegativeIntegers
        ('A', 'B', 'Type2') :     0 :   0.0 :  None : False : False : NonNegativeIntegers
        ('A', 'C', 'Type1') :     0 :   0.0 :  None : False : False : NonNegativeIntegers
        ('A', 'C', 'Type2') :     0 :   5.0 :  None : False : False : NonNegativeIntegers
        ('F', 'B', 'Type1') :     0 :   0.0 :  None : False : False : NonNegativeIntegers
        ('F', 'B', 'Type2') :     0 :   0.0 :  None : False : False : NonNegativeIntegers
        ('F', 'C', 'Type1') :     0 :   0.0 :  None : False : False : NonNegativeIntegers
        ('F', 'C', 'Type2') :     0 :   7.0 :  None : False : False : NonNegativeIntegers
    [Finished in 631ms]