Skip to content

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
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

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)