A shipping company operates a fleet of cargo ships to transport containers between three ports: PortA, PortB, and PortC. The company aims to allocate its ships across six possible shipping routes in a way that maximizes overall profit while satisfying all container demand and adhering to operational constraints.

Each ship has a capacity of 8000 TEUs (Twenty-foot Equivalent Units) and consumes 200 tons of fuel per 1000 nautical miles traveled. Fuel costs 600 USD per ton. There are 12 ships available in total, but at least 2 must remain in reserve and cannot be assigned to any route. Each ship can only be assigned to one route per day.

The six possible shipping routes, along with their respective distances, are as follows:
- Route1: PortA → PortB (1200 nautical miles)
- Route2: PortA → PortC (1800 nautical miles)
- Route3: PortB → PortC (900 nautical miles)
- Route4: PortB → PortA (1200 nautical miles)
- Route5: PortC → PortA (1800 nautical miles)
- Route6: PortC → PortB (900 nautical miles)

For each route, the container demand in TEUs is:
- Route1: 6500 TEUs
- Route2: 7200 TEUs
- Route3: 4800 TEUs
- Route4: 5100 TEUs
- Route5: 6200 TEUs
- Route6: 3900 TEUs

Revenue generated per TEU varies by route:
- Route1: $120 per TEU
- Route2: $150 per TEU
- Route3: $90 per TEU
- Route4: $110 per TEU
- Route5: $140 per TEU
- Route6: $80 per TEU

Operational constraints include:
- A ship assigned to a route must carry at least 70% of its capacity, i.e., at least 5600 TEUs.
- The number of ships assigned to a route cannot exceed the ceiling of the ratio of demand to ship capacity (i.e., ceiling(demand / 8000)).
- All container demand for each route must be fully satisfied.

The objective is to determine the optimal allocation of ships to routes that maximizes net profit (revenue from transported containers minus fuel costs), while respecting all constraints.

Problem type: LP (Medium: 66 variables, 35 constraints, 318 nonzeros)

Mathematical model:
Set:
1. Grades
The set of orange grades, \( G = \{9, 6\} \)

Parameter:
1. Inventory (kg)
\( Q_9 = 90,000 \), \( Q_6 = 110,000 \)
2. Quality Scores
\( s_9 = 9 \), \( s_6 = 6 \)
3. FP Economic Data
Revenue \( R_{FP} = 2.80 \), Cost \( C_{FP} = 1.20 \)
4. JE Economic Data
Juice Price \( P_{JE} = 3.00 \), Yield \( Y_{JE} = 0.5 \), Cost \( C_{JE} = 0.90 \)
5. Quality Thresholds
\( T_{FP} = 7.5 \), \( T_{JE} = 8.25 \)
6. Operational Limits
\( JE_{max} = 70,000 \), \( FP_{min} = 20,000 \)

Decision variable:
1. Allocation
Continuous variable \( x_{g,p} \geq 0 \) for all \( g \in G, p \in \{FP, JE\} \), representing kg of grade \( g \) oranges allocated to process \( p \)

Objective:
Maximize daily profit
max: \( \sum_{g\in G} [(R_{FP} - C_{FP})x_{g,FP} + (P_{JE} \cdot Y_{JE} - C_{JE})x_{g,JE}] \)

Constraint:
1. Inventory limits
\( x_{9,FP} + x_{9,JE} \leq Q_9 \)
\( x_{6,FP} + x_{6,JE} \leq Q_6 \)
2. Quality requirements
\( \frac{9x_{9,FP} + 6x_{6,FP}}{x_{9,FP} + x_{6,FP}} \geq T_{FP} \)
\( \frac{9x_{9,JE} + 6x_{6,JE}}{x_{9,JE} + x_{6,JE}} \geq T_{JE} \)
3. Operational constraints
\( x_{9,JE} + x_{6,JE} \leq JE_{max} \)
\( x_{9,FP} + x_{6,FP} \geq FP_{min} \)
4. Non-negativity
\( x_{g,p} \geq 0 \)

Type:
Continuous, Linear, Linear
LP

Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB
import math


def solve_shipping_problem(
    routes=[1, 2, 3, 4, 5, 6],
    ships=list(range(1, 11)),
    RouteDemand={
        1: 6500,
        2: 7200,
        3: 4800,
        4: 5100,
        5: 6200,
        6: 3900
    },
    Revenue={
        1: 120,
        2: 150,
        3: 90,
        4: 110,
        5: 140,
        6: 80
    },
    Distance={
        1: 1200,
        2: 1800,
        3: 900,
        4: 1200,
        5: 1800,
        6: 900
    },
    ShipCapacity=8000,
    FuelConsumptionRate=0.2,
    FuelCost=600,
    MinLoadPercentage=0.7
):
    # Create model
    model = gp.Model("Shipping_Optimization")

    # Decision variables
    # Binary variable: 1 if ship s is assigned to route r
    x = model.addVars(ships, routes, vtype=GRB.BINARY, name="assign")

    # Continuous variable: total containers shipped on route r
    y = model.addVars(routes, lb=0, vtype=GRB.CONTINUOUS, name="containers")

    # Objective function: maximize profit (revenue - fuel costs)
    revenue = gp.quicksum(Revenue[r] * y[r] for r in routes)
    fuel_cost = gp.quicksum(
        x[s, r] * Distance[r] * FuelConsumptionRate * FuelCost
        for s in ships for r in routes
    )
    model.setObjective(revenue - fuel_cost, GRB.MAXIMIZE)

    # Constraints
    # 1. Demand satisfaction
    model.addConstrs(
        (y[r] >= RouteDemand[r] for r in routes),
        name="demand_satisfaction"
    )

    # 2. Ship capacity constraints
    model.addConstrs(
        (y[r] <= gp.quicksum(x[s, r] for s in ships) * ShipCapacity for r in routes),
        name="capacity_limit"
    )

    # 3. Minimum loading requirement (70% of capacity)
    model.addConstrs(
        (y[r] >= gp.quicksum(x[s, r] for s in ships) * ShipCapacity * MinLoadPercentage for r in routes),
        name="min_loading"
    )

    # 4. Each ship can be assigned to at most 1 route
    model.addConstrs(
        (gp.quicksum(x[s, r] for r in routes) <= 1 for s in ships),
        name="ship_assignment_limit"
    )

    # 5. Maximum ships per route (ceil(demand/capacity))
    max_ships_per_route = {
        r: math.ceil(RouteDemand[r] / ShipCapacity) for r in routes
    }
    model.addConstrs(
        (gp.quicksum(x[s, r] for s in ships) <= max_ships_per_route[r] for r in routes),
        name="max_ships_per_route"
    )

    # 6. Total ship usage cannot exceed available ships (10)
    model.addConstr(
        gp.quicksum(x[s, r] for s in ships for r in routes) <= 10,
        name="total_ship_usage"
    )

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

Solution status: OPTIMAL
Optimal objective value: 4584000