0

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

2 Answers 2

1

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]
Sign up to request clarification or add additional context in comments.

3 Comments

I realize now that I flipped your ratio from 1:3 to 3:1. It is an easy fix, but I like that the current solution above shows a change in plans to minimize cost, whereas after fixing the typo off-line, the solution remains the same, so I'm not going to edit.
Thank you very much for your solution. It looks much more elegant than mine. But I see that the final result is not as expected i.e. you got 4 wagons of Type1 at station B, 5 wagons of Type2 at station C, 7 wagons of Type2 at station C. So there is actually no proportion of 1/3 or 3/1 (doesn’t matter actually it is an easy fix as you mentioned). I need a proportion of Type1 and Type2 on the same station e.g. at station B or at station C or at each of them. In your case it only works like globally not taking into account the station.
The proportion is enforced over "the sum of distributed wagons", which is what the text of your question said. It is 12:4, aka 3:1. If you want to enforce it "for each destination" or "for each source" that is a minor tweak to the ratio constraint where you will need to inspect the r[0] or r[1] item.
0

You didn't show any of your code (generally not a good idea if you want detailed help...) but, presumably you have some decision variable that is integer bound that represents the # of wagons from your single start to station other, right?

Then, in pseudocode:

send_wagons[type, destination] ∈ {integers}  # your decision variable

You have some ratios by destination:

ratios = {'B': (1, 3), 'C': ...}

and then you can make a constraint for each destination to enforce the ratio

for d in destinations:
  send_wagons[type_1, d] * ratios[d][0] == send_wagons[type_2, d] * ratios[d][1]

Do not use division because you will get numerical error that will cause problems.

If you have more than 1 start or more than 2 types of wagons, it gets more complicated, but that is the basic idea

3 Comments

Sorry for that, I edited my question with my code.
I think your solution won’t work for that case as it doesn’t account different cases of proportions.
See my other solution... it covers that issue and others that are now clear based on your code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.