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-05-06 16:54 CEST 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", 400, "SOYBEAN OIL"),
        ("PORT-2", 1600, "SOYBEAN CAKE"),
        ("PORT-3", 1000, "SOYBEANS"),
    ],
    columns=["port_trase_id", "export_vol", "product"],
)

df_demand
port_trase_id export_vol product
0 PORT-1 400 SOYBEAN OIL
1 PORT-2 1600 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", "port_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 (port_trase_id, export_vol), variables in df.groupby(
    ["port_trase_id", "export_vol"]
)["variable"]:
    problem += (lpSum(variables) == export_vol, f"demand at {port_trase_id}")

problem += (lpSum(df["variable"] * df["cost"]), "total cost")

df
production_trase_id production_vol port_trase_id export_vol product cost variable_name variable
0 FARM-1 1000 PORT-1 400 SOYBEAN OIL 10 v0 v0
1 FARM-1 1000 PORT-2 1600 SOYBEAN CAKE 100 v1 v1
2 FARM-1 1000 PORT-3 1000 SOYBEANS 100 v2 v2
3 FARM-2 1000 PORT-1 400 SOYBEAN OIL 100 v3 v3
4 FARM-2 1000 PORT-2 1600 SOYBEAN CAKE 10 v4 v4
5 FARM-2 1000 PORT-3 1000 SOYBEANS 100 v5 v5
6 FARM-3 1000 PORT-1 400 SOYBEAN OIL 100 v6 v6
7 FARM-3 1000 PORT-2 1600 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 = 400

demand_at_PORT_2: v1 + v4 + v7 = 1600

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/139e225748f44d1ea905b6ae8f5570e3-pulp.lp -o /tmp/139e225748f44d1ea905b6ae8f5570e3-pulp.sol
Reading problem data from '/tmp/139e225748f44d1ea905b6ae8f5570e3-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 =   8.400000000e+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/139e225748f44d1ea905b6ae8f5570e3-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", "port_trase_id", "vol", "product"]]
df_solution
production_trase_id port_trase_id vol product
0 FARM-1 PORT-1 400.0 SOYBEAN OIL
1 FARM-1 PORT-2 600.0 SOYBEAN CAKE
2 FARM-2 PORT-2 1000.0 SOYBEAN CAKE
3 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", 400, "SOYBEAN OIL"),
        ("PORT-2", 1600, "SOYBEAN CAKE"),
        ("PORT-3", 1000, "SOYBEANS"),
    ],
    columns=["port_trase_id", "export_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 unprocessed 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", "port_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 (port_trase_id, export_vol), variables in df.groupby(
    ["port_trase_id", "export_vol"]
)["variable"]:
    problem += (lpSum(variables) == export_vol, f"demand at {port_trase_id}")

df
production_trase_id production_vol unprocessed_variable_name unprocessed_variable port_trase_id export_vol product cost variable_name variable
0 FARM-1 1000 u0 u0 PORT-1 400 SOYBEAN OIL 10 v0 v0
1 FARM-1 1000 u0 u0 PORT-2 1600 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 400 SOYBEAN OIL 100 v3 v3
4 FARM-2 1000 u1 u1 PORT-2 1600 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 400 SOYBEAN OIL 100 v6 v6
7 FARM-3 1000 u2 u2 PORT-2 1600 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, group in df[df["product"] == "SOYBEANS"].groupby(
    "production_trase_id"
):
    unprocessed_variable = group["unprocessed_variable"].iloc[0]
    problem += (
        lpSum(group["variable"]) <= unprocessed_variable,
        f"unprocessed soybeans at {production_trase_id}",
    )

Add constraints on processing products: exports of cake and oil must respect ratio at each farm

for production_trase_id, group in df[df["product"] == "SOYBEAN CAKE"].groupby(
    "production_trase_id"
):
    unprocessed_variable = group["unprocessed_variable"].iloc[0]
    supply = group["production_vol"].iloc[0]
    cake_vol = (supply - unprocessed_variable) * (4 / 5)
    problem += (
        lpSum(group["variable"]) <= cake_vol,
        f"soybean cake at {production_trase_id}",
    )

for production_trase_id, group in df[df["product"] == "SOYBEAN OIL"].groupby(
    "production_trase_id"
):
    unprocessed_variable = group["unprocessed_variable"].iloc[0]
    supply = group["production_vol"].iloc[0]
    oil_vol = (supply - unprocessed_variable) * (1 / 5)
    problem += (
        lpSum(group["variable"]) <= oil_vol,
        f"soybean oil 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 = 400

demand_at_PORT_2: v1 + v4 + v7 = 1600

demand_at_PORT_3: v2 + v5 + v8 = 1000

unprocessed_soybeans_at_FARM_1: - u0 + v2 <= 0

unprocessed_soybeans_at_FARM_2: - u1 + v5 <= 0

unprocessed_soybeans_at_FARM_3: - u2 + v8 <= 0

soybean_cake_at_FARM_1: 0.8 u0 + v1 <= 800

soybean_cake_at_FARM_2: 0.8 u1 + v4 <= 800

soybean_cake_at_FARM_3: 0.8 u2 + v7 <= 800

soybean_oil_at_FARM_1: 0.2 u0 + v0 <= 200

soybean_oil_at_FARM_2: 0.2 u1 + v3 <= 200

soybean_oil_at_FARM_3: 0.2 u2 + v6 <= 200

VARIABLES
u0 Continuous
u1 Continuous
u2 Continuous
v0 Continuous
v1 Continuous
v2 Continuous
v3 Continuous
v4 Continuous
v5 Continuous
v6 Continuous
v7 Continuous
v8 Continuous
problem.solve(getSolver("GLPK_CMD"))

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", "port_trase_id", "vol", "product"]]
df_solution
GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
 --cpxlp /tmp/1e0a395bdbc14f3c8923d0f804281ee2-pulp.lp -o /tmp/1e0a395bdbc14f3c8923d0f804281ee2-pulp.sol
Reading problem data from '/tmp/1e0a395bdbc14f3c8923d0f804281ee2-pulp.lp'...
15 rows, 12 columns, 36 non-zeros
21 lines were read
GLPK Simplex Optimizer 5.0
15 rows, 12 columns, 36 non-zeros
Preprocessing...
15 rows, 12 columns, 36 non-zeros
Scaling...
 A: min|aij| =  2.000e-01  max|aij| =  1.000e+00  ratio =  5.000e+00
Problem data seem to be well scaled
Constructing initial basis...
Size of triangular part is 15
      0: obj =   2.100000000e+05 inf =   4.000e+03 (4)
      5: obj =   2.820000000e+05 inf =   0.000e+00 (0)
*    10: obj =   1.200000000e+05 inf =   8.882e-15 (0)
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.0 Mb (39693 bytes)
Writing basic solution to '/tmp/1e0a395bdbc14f3c8923d0f804281ee2-pulp.sol'...
production_trase_id port_trase_id vol product
0 FARM-1 PORT-1 200.0 SOYBEAN OIL
1 FARM-1 PORT-2 800.0 SOYBEAN CAKE
2 FARM-2 PORT-1 200.0 SOYBEAN OIL
3 FARM-2 PORT-2 800.0 SOYBEAN CAKE
4 FARM-3 PORT-3 1000.0 SOYBEANS