6 LP with commodity ratios
View or edit on GitHub
This page is synchronized from trase/models/lp/6_LP_with_commodity_ratios.ipynb. Last modified on 2026-03-20 23:00 CET by Trase Admin.
Please view or edit the original file there; changes should be reflected here after a midnight build (CET time),
or manually triggering it with a GitHub action (link).
LP with different products
import pandas as pd
from functools import partial
from pulp import (
getSolver,
LpProblem,
LpMinimize,
LpVariable,
lpSum,
)
df_supply = pd.DataFrame(
data=[
("FARM-1", 1000),
("FARM-2", 1000),
("FARM-3", 1000)
],
columns=["production_trase_id", "production_vol"],
)
df_supply
|
production_trase_id |
production_vol |
| 0 |
FARM-1 |
1000 |
| 1 |
FARM-2 |
1000 |
| 2 |
FARM-3 |
1000 |
df_demand = pd.DataFrame(
data=[
("PORT-1", 1000, "SOYBEAN OIL"),
("PORT-2", 1000, "SOYBEAN CAKE"),
("PORT-3", 1000, "SOYBEANS"),
],
columns=["export_trase_id", "export_demand_vol", "product"],
)
df_demand
|
export_trase_id |
export_demand_vol |
product |
| 0 |
PORT-1 |
1000 |
SOYBEAN OIL |
| 1 |
PORT-2 |
1000 |
SOYBEAN CAKE |
| 2 |
PORT-3 |
1000 |
SOYBEANS |
df_costs = pd.DataFrame(
data=[
# Farm 1 is closest to port 1
("FARM-1", "PORT-1", 10),
("FARM-1", "PORT-2", 100),
("FARM-1", "PORT-3", 100),
# Farm 2 is closest to port 2
("FARM-2", "PORT-1", 100),
("FARM-2", "PORT-2", 10),
("FARM-2", "PORT-3", 100),
# Farm 3 is closest to port 3
("FARM-3", "PORT-1", 100),
("FARM-3", "PORT-2", 100),
("FARM-3", "PORT-3", 10),
],
columns=["origin_trase_id", "destination_trase_id", "cost"],
)
df_costs
|
origin_trase_id |
destination_trase_id |
cost |
| 0 |
FARM-1 |
PORT-1 |
10 |
| 1 |
FARM-1 |
PORT-2 |
100 |
| 2 |
FARM-1 |
PORT-3 |
100 |
| 3 |
FARM-2 |
PORT-1 |
100 |
| 4 |
FARM-2 |
PORT-2 |
10 |
| 5 |
FARM-2 |
PORT-3 |
100 |
| 6 |
FARM-3 |
PORT-1 |
100 |
| 7 |
FARM-3 |
PORT-2 |
100 |
| 8 |
FARM-3 |
PORT-3 |
10 |
problem = LpProblem("MunicipalityToExport", LpMinimize)
df = pd.merge(df_supply, df_demand, how="cross")
df = pd.merge(
df,
df_costs,
left_on=["production_trase_id", "export_trase_id"],
right_on=["origin_trase_id", "destination_trase_id"],
).drop(columns=["origin_trase_id", "destination_trase_id"])
df["variable_name"] = "v" + df.index.to_series().astype(str)
df["variable"] = df["variable_name"].apply(partial(LpVariable, lowBound=0))
for (production_trase_id, production_vol), variables in df.groupby(
["production_trase_id", "production_vol"]
)["variable"]:
problem += (lpSum(variables) <= production_vol, f"supply at {production_trase_id}")
for (export_trase_id, export_demand_vol), variables in df.groupby(
["export_trase_id", "export_demand_vol"]
)["variable"]:
problem += (lpSum(variables) == export_demand_vol, f"demand at {export_trase_id}")
problem += (lpSum(df["variable"] * df["cost"]), "total cost")
df
|
production_trase_id |
production_vol |
export_trase_id |
export_demand_vol |
product |
cost |
variable_name |
variable |
| 0 |
FARM-1 |
1000 |
PORT-1 |
1000 |
SOYBEAN OIL |
10 |
v0 |
v0 |
| 1 |
FARM-1 |
1000 |
PORT-2 |
1000 |
SOYBEAN CAKE |
100 |
v1 |
v1 |
| 2 |
FARM-1 |
1000 |
PORT-3 |
1000 |
SOYBEANS |
100 |
v2 |
v2 |
| 3 |
FARM-2 |
1000 |
PORT-1 |
1000 |
SOYBEAN OIL |
100 |
v3 |
v3 |
| 4 |
FARM-2 |
1000 |
PORT-2 |
1000 |
SOYBEAN CAKE |
10 |
v4 |
v4 |
| 5 |
FARM-2 |
1000 |
PORT-3 |
1000 |
SOYBEANS |
100 |
v5 |
v5 |
| 6 |
FARM-3 |
1000 |
PORT-1 |
1000 |
SOYBEAN OIL |
100 |
v6 |
v6 |
| 7 |
FARM-3 |
1000 |
PORT-2 |
1000 |
SOYBEAN CAKE |
100 |
v7 |
v7 |
| 8 |
FARM-3 |
1000 |
PORT-3 |
1000 |
SOYBEANS |
10 |
v8 |
v8 |
MunicipalityToExport:
MINIMIZE
10*v0 + 100*v1 + 100*v2 + 100*v3 + 10*v4 + 100*v5 + 100*v6 + 100*v7 + 10*v8 + 0
SUBJECT TO
supply_at_FARM_1: v0 + v1 + v2 <= 1000
supply_at_FARM_2: v3 + v4 + v5 <= 1000
supply_at_FARM_3: v6 + v7 + v8 <= 1000
demand_at_PORT_1: v0 + v3 + v6 = 1000
demand_at_PORT_2: v1 + v4 + v7 = 1000
demand_at_PORT_3: v2 + v5 + v8 = 1000
VARIABLES
v0 Continuous
v1 Continuous
v2 Continuous
v3 Continuous
v4 Continuous
v5 Continuous
v6 Continuous
v7 Continuous
v8 Continuous
problem.solve(getSolver("GLPK_CMD"))
GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--cpxlp /tmp/aa4a4b5ebf6c45c5a51214033cbb3e71-pulp.lp -o /tmp/aa4a4b5ebf6c45c5a51214033cbb3e71-pulp.sol
Reading problem data from '/tmp/aa4a4b5ebf6c45c5a51214033cbb3e71-pulp.lp'...
6 rows, 9 columns, 18 non-zeros
12 lines were read
GLPK Simplex Optimizer 5.0
6 rows, 9 columns, 18 non-zeros
Preprocessing...
6 rows, 9 columns, 18 non-zeros
Scaling...
A: min|aij| = 1.000e+00 max|aij| = 1.000e+00 ratio = 1.000e+00
Problem data seem to be well scaled
Constructing initial basis...
Size of triangular part is 6
0: obj = 2.100000000e+05 inf = 2.000e+03 (1)
3: obj = 3.000000000e+04 inf = 0.000e+00 (0)
* 4: obj = 3.000000000e+04 inf = 0.000e+00 (0)
OPTIMAL LP SOLUTION FOUND
Time used: 0.0 secs
Memory used: 0.0 Mb (39693 bytes)
Writing basic solution to '/tmp/aa4a4b5ebf6c45c5a51214033cbb3e71-pulp.sol'...
1
df_solution = pd.DataFrame(
columns=["variable_name", "vol"],
data=[(v.name, v.varValue) for v in problem.variables()],
)
df_solution = df_solution[df_solution["vol"] > 0]
df_solution = pd.merge(df, df_solution, on="variable_name")
df_solution = df_solution[["production_trase_id", "export_trase_id", "vol", "product"]]
df_solution
|
production_trase_id |
export_trase_id |
vol |
product |
| 0 |
FARM-1 |
PORT-1 |
1000.0 |
SOYBEAN OIL |
| 1 |
FARM-2 |
PORT-2 |
1000.0 |
SOYBEAN CAKE |
| 2 |
FARM-3 |
PORT-3 |
1000.0 |
SOYBEANS |
This solution is not realistic: the soybeans get crushed into cake and oil with a fixed ratio of 4-to-1, and therefore a farm cannot supply only cake or only oil
Adding commodity ratios
import pandas as pd
from functools import partial
from pulp import (
getSolver,
LpProblem,
LpMinimize,
LpVariable,
lpSum,
)
df_supply = pd.DataFrame(
data=[
("FARM-1", 1000),
("FARM-2", 1000),
("FARM-3", 1000)
],
columns=["production_trase_id", "production_vol"],
)
df_demand = pd.DataFrame(
data=[
("PORT-1", 1000, "SOYBEAN OIL"),
("PORT-2", 1000, "SOYBEAN CAKE"),
("PORT-3", 1000, "SOYBEANS"),
],
columns=["export_trase_id", "export_demand_vol", "product"],
)
df_costs = pd.DataFrame(
data=[
# Farm 1 is closest to port 1
("FARM-1", "PORT-1", 10),
("FARM-1", "PORT-2", 100),
("FARM-1", "PORT-3", 100),
# Farm 2 is closest to port 2
("FARM-2", "PORT-1", 100),
("FARM-2", "PORT-2", 10),
("FARM-2", "PORT-3", 100),
# Farm 3 is closest to port 3
("FARM-3", "PORT-1", 100),
("FARM-3", "PORT-2", 100),
("FARM-3", "PORT-3", 10),
],
columns=["origin_trase_id", "destination_trase_id", "cost"],
)
problem = LpProblem("MunicipalityToExport", LpMinimize)
We create new variables defining how much of the soybeans are crushed per farm
df_supply["unprocessed_variable_name"] = "u" + df_supply.index.to_series().astype(str)
df_supply["unprocessed_variable"] = df_supply["unprocessed_variable_name"].apply(partial(LpVariable, lowBound=0))
df_supply
|
production_trase_id |
production_vol |
unprocessed_variable_name |
unprocessed_variable |
| 0 |
FARM-1 |
1000 |
u0 |
u0 |
| 1 |
FARM-2 |
1000 |
u1 |
u1 |
| 2 |
FARM-3 |
1000 |
u2 |
u2 |
df = pd.merge(df_supply, df_demand, how="cross")
df = pd.merge(
df,
df_costs,
left_on=["production_trase_id", "export_trase_id"],
right_on=["origin_trase_id", "destination_trase_id"],
).drop(columns=["origin_trase_id", "destination_trase_id"])
df["variable_name"] = "v" + df.index.to_series().astype(str)
df["variable"] = df["variable_name"].apply(partial(LpVariable, lowBound=0))
for (production_trase_id, production_vol), variables in df.groupby(
["production_trase_id", "production_vol"]
)["variable"]:
problem += (lpSum(variables) <= production_vol, f"supply at {production_trase_id}")
for (export_trase_id, export_demand_vol), variables in df.groupby(
["export_trase_id", "export_demand_vol"]
)["variable"]:
problem += (lpSum(variables) == export_demand_vol, f"demand at {export_trase_id}")
df
|
production_trase_id |
production_vol |
unprocessed_variable_name |
unprocessed_variable |
export_trase_id |
export_demand_vol |
product |
cost |
variable_name |
variable |
| 0 |
FARM-1 |
1000 |
u0 |
u0 |
PORT-1 |
1000 |
SOYBEAN OIL |
10 |
v0 |
v0 |
| 1 |
FARM-1 |
1000 |
u0 |
u0 |
PORT-2 |
1000 |
SOYBEAN CAKE |
100 |
v1 |
v1 |
| 2 |
FARM-1 |
1000 |
u0 |
u0 |
PORT-3 |
1000 |
SOYBEANS |
100 |
v2 |
v2 |
| 3 |
FARM-2 |
1000 |
u1 |
u1 |
PORT-1 |
1000 |
SOYBEAN OIL |
100 |
v3 |
v3 |
| 4 |
FARM-2 |
1000 |
u1 |
u1 |
PORT-2 |
1000 |
SOYBEAN CAKE |
10 |
v4 |
v4 |
| 5 |
FARM-2 |
1000 |
u1 |
u1 |
PORT-3 |
1000 |
SOYBEANS |
100 |
v5 |
v5 |
| 6 |
FARM-3 |
1000 |
u2 |
u2 |
PORT-1 |
1000 |
SOYBEAN OIL |
100 |
v6 |
v6 |
| 7 |
FARM-3 |
1000 |
u2 |
u2 |
PORT-2 |
1000 |
SOYBEAN CAKE |
100 |
v7 |
v7 |
| 8 |
FARM-3 |
1000 |
u2 |
u2 |
PORT-3 |
1000 |
SOYBEANS |
10 |
v8 |
v8 |
Add constraints on unprocessed soybeans: exports of soybeans must be less or equal to unprocessed soybeans at each farm
for (production_trase_id, unprocessed_variable), variables in df[df["product"] == "SOYBEANS"].groupby(
["production_trase_id", "unprocessed_variable"]
)["variable"]:
problem += (lpSum(variables) <= unprocessed_variable, f"unprocessed soybeans at {production_trase_id}")
problem += (lpSum(df["variable"] * df["cost"]), "total cost")
problem
MunicipalityToExport:
MINIMIZE
10*v0 + 100*v1 + 100*v2 + 100*v3 + 10*v4 + 100*v5 + 100*v6 + 100*v7 + 10*v8 + 0
SUBJECT TO
supply_at_FARM_1: v0 + v1 + v2 <= 1000
supply_at_FARM_2: v3 + v4 + v5 <= 1000
supply_at_FARM_3: v6 + v7 + v8 <= 1000
demand_at_PORT_1: v0 + v3 + v6 = 1000
demand_at_PORT_2: v1 + v4 + v7 = 1000
demand_at_PORT_3: v2 + v5 + v8 = 1000
unprocessed_soybeans_at_FARM_1: - u0 + v2 <= 0
unprocessed_soybeans_at_FARM_2: - u0 + v5 <= 0
unprocessed_soybeans_at_FARM_3: - u0 + v8 <= 0
VARIABLES
u0 Continuous
v0 Continuous
v1 Continuous
v2 Continuous
v3 Continuous
v4 Continuous
v5 Continuous
v6 Continuous
v7 Continuous
v8 Continuous
filtered = df[df["product"] == "SOYBEANS"]
for key, group in filtered.groupby(["production_trase_id", "unprocessed_variable"]):
print(key)
('FARM-1', u0)
('FARM-2', u0)
('FARM-3', u0)