LogiCorp operates a logistics network that ships two types of products: Basic and Advanced. The raw materials needed to produce these products can be purchased for $5 per unit. Processing 1 unit of raw material requires 2 hours of warehouse labor. Each unit of processed raw material yields 2 units of Basic Product and 3 units of Advanced Product. Basic Product can be sold for $10/unit, and Advanced Product can be sold for $12/unit. LogiCorp also has the option of further processing Basic and Advanced Products to produce Premium Basic and Premium Advanced, which sell for $20/unit and $25/unit, respectively. Each unit of Basic Product processed further requires an additional 3 hours of warehouse labor and $5 processing cost, yielding 1 unit of Premium Basic. Each unit of Advanced Product processed further requires an additional 4 hours of warehouse labor and $6 processing cost, yielding 1 unit of Premium Advanced. Each year, LogiCorp has 8,000 hours of warehouse labor available and can purchase up to 3,000 units of raw material. How can LogiCorp maximize its profits? Assume that the cost of warehouse labor is a fixed cost, raw materials can only be purchased in whole units.

Problem type: ILP (Toy: 5 variables, 4 constraints, 10 nonzeros)

Mathematical model:
Set:
1. Categories
The set of products categories, \( I = \{1, 2\} \), where 1 represents Basic Product and 2 represents Advanced Product. 

Parameter:
1. Product Production per Raw Material
\( p = [2, 3] \)
2. Labor Hours per Raw Material
\( h^r = 2 \)
3. Labor Hours per Premium Product
\( h^q = [3, 4] \)
4. Conversion Rate
# The number of products not further processed needed to produce one unit of further processed product.
\( p^q = [1, 1] \) 
5. Processing Cost per Premium Product ($/unit)
\( c^q = [5, 6] \)
6. Product Selling Price ($/unit)
\( s = [10, 12] \)
7. Premium Product Selling Price ($/unit)
\( s^q = [20, 25] \)
8. Raw Material Cost ($/unit)
\( c^r = 5 \)
9. Max Labor Hours
\( H_{\max} = 8000 \)
10. Max Raw Material
\( R_{\max} = 3000 \)

Decision variable:
1. RawMaterialPurchased
Integer variable, \( r \in \mathbb{Z}_{\geq 0} \), the amount of raw material purchased in units.
2. ProductProduced
Integer variable, \( x_{i} \in \mathbb{Z}_{\geq 0}, \forall i \in I \), the amount of product without further processing produced in units.
3. PremiumProductProduced
Integer variable, \( y_{i} \in \mathbb{Z}_{\geq 0}, \forall i \in I \), the amount of further processed product produced in units.

Objective:
1. Maximize the total profit.
max: \sum_{i \in I} s_{i} x_{i} + \sum_{i \in I} s^q_{i} y_{i} - c^r r - \sum_{i \in I} c^q_{i} y_{i}
Constraint:
1. Labor hours constraint.
h^r r + \sum_{i \in I} h^q_{i} y_{i} \leq H_{\max}
2. Raw material constraint.
r \leq R_{\max}
3. Material balance constraint.
x_{i} + p^q_{i} y_{i} = p_{i} r, \forall i \in I

Type:
Integer, Linear, Linear
ILP


Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB

def solve(
    I=[1, 2],
    p=[2, 3],
    h_r=2,
    h_q=[3, 4],
    p_q=[1, 1],
    c_q=[5, 6],
    s=[10, 12],
    s_q=[20, 25],
    c_r=5,
    H_max=8000,
    R_max=3000
):
    """
    Args:
        I: Category identifiers.
        p: Units of base product produced per unit of raw material for each category.
        h_r: Labor hours required per unit of raw material processed.
        h_q: Labor hours required per unit of premium product processed for each category.
        p_q: Base-to-premium conversion coefficient for each category (base units consumed per 1 premium).
        c_q: Additional processing cost per unit of premium product for each category.
        s: Selling price per unit of base product for each category.
        s_q: Selling price per unit of premium product for each category.
        c_r: Raw material cost per unit.
        H_max: Total available labor hours.
        R_max: Maximum purchasable raw material units.
    """
    
    # Create a new model
    model = gp.Model("Product Manufacturing Optimization")
    
    # Sets
    N = range(len(I))  # Categories
    
    # Decision Variables
    r = model.addVar(vtype=GRB.INTEGER, lb=0, name="raw_material_purchased")
    x = model.addVars(N, vtype=GRB.INTEGER, lb=0, name="product_produced_wo_further_processing")
    y = model.addVars(N, vtype=GRB.INTEGER, lb=0, name="premium_product_produced")
    
    # Objective
    revenue = gp.quicksum(s[i] * x[i] for i in N) + gp.quicksum(s_q[i] * y[i] for i in N)
    raw_material_cost = c_r * r
    processing_cost = gp.quicksum(c_q[i] * y[i] for i in N)
    model.setObjective(revenue - raw_material_cost - processing_cost, GRB.MAXIMIZE)
    
    # Constraints
    # Constraint 1: Labor hours constraint
    labor_hours = h_r * r + gp.quicksum(h_q[i] * y[i] for i in N)
    model.addConstr(labor_hours <= H_max, "LaborHoursLimit")
    
    # Constraint 2: Raw material constraint
    model.addConstr(r <= R_max, "RawMaterialLimit")
    
    # Constraint 3: Material balance constraint
    for i in N:
        model.addConstr(
            x[i] + p_q[i] * y[i] == p[i] * r,
            f"MaterialBalance_{i}",
        )
    
    # Optimize the model
    model.optimize()
    
    # Return Results
    if model.status == GRB.OPTIMAL:
        return {"status": "optimal", "obj": model.ObjVal}
    else:
        return {"status": f"{model.status}"}
    
if __name__ == "__main__":
    result = solve()
    print(result)


Solution status: OPTIMAL
Optimal objective value: 156500