ChemCo produces two types of solvents: Standard and Premium. The solvents are produced by blending five raw materials: Alpha, Beta, Gamma, Delta, and Epsilon. Each raw material has four key properties:

1.  Property 1: Viscosity Index (VI)
2.  Property 2: Flash Point (°C)
3.  Property 3: Boiling Point (°C)
4.  Property 4: Density (g/cm³)

The properties, daily availability (in liters), and cost (in ¢/liter) of each raw material are given in Table 1.

Table 1: Raw Materials' Properties, Daily Availability, and Costs

| Raw Material | Availability (Liters) | Cost (¢/Liter) | VI  | Flash Point (°C) | Boiling Point (°C) | Density (g/cm³) |
|--------------|-----------------------|----------------|-----|------------------|--------------------|-----------------|
| Alpha        | 12,000                | 15.0           | 95  | 45               | 150                | 0.85            |
| Beta         | 10,500                | 12.0           | 88  | 55               | 180                | 0.92            |
| Gamma        | 8,000                 | 10.0           | 82  | 60               | 200                | 0.89            |
| Delta        | 5,000                 | 18.0           | 105 | 40               | 130                | 0.87            |
| Epsilon      | Unlimited             | 16.0           | 100 | 50               | 160                | 0.91            |

The final properties of the blended solvents are determined by the volume-weighted average of the properties of the raw materials used. The requirements for each solvent type are provided in Table 2. The Viscosity Index and Boiling Point requirements are minimums, while the Flash Point and Density requirements must be met exactly.

Table 2: Solvent Requirements and Demand

| Solvent  | Demand (Thousands of Liters) | VI (min) | Flash Point (°C) (exact) | Boiling Point (°C) (min) | Density (g/cm³) (exact) |
|----------|-------------------------------|----------|--------------------------|--------------------------|------------------------|
| Standard | 8.5                           | 85       | 50                       | 140                      | 0.88                   |
| Premium  | 25                            | 92       | 50                       | 140                      | 0.88                   |

The daily demand (in thousands of liters) for each solvent must be met, but additional production beyond the demand is allowed. Standard solvent sells for 25.50¢/liter, while Premium solvent sells for 28.75¢/liter. Additionally, no more than 35% of the volume of each solvent type can consist of raw material Beta. How can ChemCo formulate its solvents using the available raw materials to maximize its total daily profit? Total daily profit is defined as total revenue from sales minus total raw material costs.

Problem type: LP (Small: 10 variables, 17 constraints, 68 nonzeros)

Mathematical model:
Set:
1. Solvent
The set of solvents, \( S = \{1, 2\} \)  # 1: Standard, 2: Premium
2. RawMaterial
The set of raw materials, \( R = \{1, 2, 3, 4, 5\} \)  # 1: Alpha, 2: Beta, 3: Gamma, 4: Delta, 5: Epsilon
3. Property
The set of properties, \( P = \{1, 2, 3, 4\} \)  # 1: Viscosity Index, 2: Flash Point, 3: Boiling Point, 4: Density

Parameter:
1. Availability
[12000, 10500, 8000, 5000, 999999]  # Daily availability of each raw material in liters
2. Cost
[15, 12, 10, 18, 16]  # Cost of each raw material in cents per liter
3. PropertyValue
[[95, 45, 150, 0.85], 
 [88, 55, 180, 0.92], 
 [82, 60, 200, 0.89], 
 [105, 40, 130, 0.87], 
 [100, 50, 160, 0.91]]  # Properties of each raw material
4. SolventPropertyValue
[[85, 50, 140, 0.88], 
 [92, 50, 140, 0.88]]  # Properties of each solvent
4. Demand
[8500, 25000]  # Daily demand for each solvent in thousands of liters
5. SellingPrice
[25.50, 28.75]  # Selling price per liter for each solvent in cents
6. MaxBetaPercentage
0.35  # Maximum percentage of Beta in each solvent

Decision variable:
1. AmountProduced
Continuous variable, \( AmountProduced[s][r] \forall s \in S, r \in R \), where \( AmountProduced[s][r] \) is the amount of solvent \( s \) produced from raw material \( r \) in liters.

Objective:
1. Maximize the total daily profit.
max: \sum_{s \in S} SellingPrice[s] * Demand[s] - \sum_{r \in R} Cost[r] * \sum_{s \in S} AmountProduced[s][r]

Constraint:
1. Demand constraint.
\sum_{r \in R} AmountProduced[s][r] \geq Demand[s] \forall s \in S
2. Availability constraint.
\sum_{s \in S} AmountProduced[s][r] \leq Availability[r] \forall r \in R
3. Property constraints.
\sum_{r \in R} PropertyValue[r][p] * AmountProduced[s][r] \geq \sum_{r \in R} SolventPropertyValue[s][p] * AmountProduced[s][r] \forall s \in S, p \in {1, 3}  # Viscosity Index and Boiling Point
\sum_{r \in R} PropertyValue[r][p] * AmountProduced[s][r] = \sum_{r \in R} SolventPropertyValue[s][p] * AmountProduced[s][r] \forall s \in S, p \in {2, 4}  # Flash Point and Density
4. Beta constraint.
AmountProduced[s][2] \leq MaxBetaPercentage * \sum_{r \in R} AmountProduced[s][r] \forall s \in S # Beta is raw material 2
5. Non-negativity constraint.
AmountProduced[s][r] \geq 0 \forall s \in S, r \in R

Type:
Continuous, Linear, Linear
LP

Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB


def solve_solvent_blending_optimization(
    Availability=[12000, 10500, 8000, 5000, 999999],
    Cost=[15, 12, 10, 18, 16],
    PropertyValue=[
        [95, 45, 150, 0.85],
        [88, 55, 180, 0.92],
        [82, 60, 200, 0.89],
        [105, 40, 130, 0.87],
        [100, 50, 160, 0.91]
    ],
    SolventPropertyValue=[
        [85, 50, 140, 0.88],
        [92, 50, 140, 0.88]
    ],
    Demand=[8500, 25000],
    SellingPrice=[25.50, 28.75],
    MaxBetaPercentage=0.35
):
    """
    Models and solves the solvent blending optimization problem.
    """
    # Create a new model
    model = gp.Model("Solvent Blending Optimization")

    # Sets
    Solvents = range(len(Demand))
    RawMaterials = range(len(Cost))
    Properties = range(len(PropertyValue[0]))

    # Decision Variables
    AmountProduced = {}
    for s in Solvents:
        for r in RawMaterials:
            AmountProduced[s, r] = model.addVar(
                vtype=GRB.CONTINUOUS,
                name=f"AmountProduced_{s+1}_{r+1}"
            )

    # Objective: Maximize profit (revenue from demand only - total production cost)
    total_revenue = gp.quicksum(SellingPrice[s] * Demand[s] for s in Solvents)
    total_cost = gp.quicksum(
        Cost[r] * gp.quicksum(AmountProduced[s, r] for s in Solvents)
        for r in RawMaterials
    )
    model.setObjective(total_revenue - total_cost, GRB.MAXIMIZE)

    # Constraint 1: Demand constraint (production must meet exactly the demand)
    for s in Solvents:
        model.addConstr(
            gp.quicksum(AmountProduced[s, r] for r in RawMaterials) == Demand[s],
            f"Demand_{s+1}"
        )

    # Constraint 2: Availability constraint
    for r in RawMaterials:
        model.addConstr(
            gp.quicksum(AmountProduced[s, r] for s in Solvents) <= Availability[r],
            f"Availability_{r+1}"
        )

    # Constraint 3: Property constraints
    for s in Solvents:
        # For properties 0 (VI) and 2 (Boiling Point) - minimum requirements
        for p in [0, 2]:
            model.addConstr(
                gp.quicksum(PropertyValue[r][p] * AmountProduced[s, r] for r in RawMaterials) >=
                SolventPropertyValue[s][p] * gp.quicksum(AmountProduced[s, r] for r in RawMaterials),
                f"MinProperty_{s+1}_{p+1}"
            )

        # For properties 1 (Flash Point) and 3 (Density) - exact requirements
        for p in [1, 3]:
            model.addConstr(
                gp.quicksum(PropertyValue[r][p] * AmountProduced[s, r] for r in RawMaterials) ==
                SolventPropertyValue[s][p] * gp.quicksum(AmountProduced[s, r] for r in RawMaterials),
                f"ExactProperty_{s+1}_{p+1}"
            )

    # Constraint 4: Beta constraint (raw material 1 is Beta)
    for s in Solvents:
        model.addConstr(
            AmountProduced[s, 1] <= MaxBetaPercentage * gp.quicksum(AmountProduced[s, r] for r in RawMaterials),
            f"BetaLimit_{s+1}"
        )

    # 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_solvent_blending_optimization()
    print(result)


Solution status: OPTIMAL
Optimal objective value: 474250