A container terminal needs to allocate berths for a fleet of 10 cargo ships within the next 72 hours. The terminal has 3 available berths (B1, B2, B3). Due to interdependencies where some ships carry import/export goods required by other ships, there are collaborative operation requirements between them. Specifically, ship C can only start loading after ship A completes unloading; ship E can only start loading after ship D completes unloading. To incentivize efficient collaboration, a **¥50,000 "rapid transshipment" bonus** can be earned if the start time of a dependent task (e.g., C starting to load) immediately follows the completion time of its predecessor task (e.g., A completing unloading) with a time gap of 2 hours or less. Ship information and operational requirements are shown in Table 1. The objective is to assign each ship a unique berth and a conflict-free berthing time window to maximize the total rapid transshipment bonus while minimizing the ships' waiting time costs in the port** (¥2,000 per hour).

Table 1: Ship Information and Operational Requirements

| Ship ID | Operation Type | Dependencies | Estimated Arrival Time (hours) | Required Berth Operation Time (hours) |
|:---|:---|:---|:---:|:---:|
| A | Unloading | - | 2 | 8 |
| B | Loading/Unloading | - | 5 | 10 |
| C | Loading | A | 10 | 6 |
| D | Unloading | - | 12 | 9 |
| E | Loading | D | 18 | 7 |
| F | Loading | - | 20 | 11 |
| G | Loading | B | 25 | 8 |
| H | Unloading | - | 30 | 12 |
| I | Loading | H | 35 | 5 |
| J | Unloading | G | 40 | 9 |

Problem type: MILP (Medium: 155 variables, 590 constraints, 2525 nonzeros)

Mathematical model:
Set:
1. Ships
The set of ships, \( S = \{A, B, C, D, E, F, G, H, I, J\} \)
2. Berths
The set of berths, \( B = \{B1, B2, B3\} \)
3. Dependencies
The set of dependencies, \( D = \{(A, C), (D, E), (B, G), (H, I), (G, J)\} \)  # where each tuple represents a dependency where the first ship must complete its operation before the second ship can start its operation.

Parameter:
1. ArrivalTime
# The estimated arrival time of each ship
[2, 5, 10, 12, 18, 20, 25, 30, 35, 40] # in hours
2. OperationTime
# The required operation time for each ship at the berth
[8, 10, 6, 9, 7, 11, 8, 12, 5, 9] # in hours
3. RapidTransshipmentBonus
# The bonus for rapid transshipment
50000 # in currency units
4. MaxTransshipmentGap
# The maximum time gap for rapid transshipment to qualify for the bonus
2 # in hours
5. WaitingCost
# The cost of waiting per hour
2000 # in currency units
6. MaxTime
# The maximum time for the scheduling horizon
72 # in hours

Decision variable:
1. StartTime
Continuous variable, \(StartTime_i \forall i \in Ships\), where \( StartTime_i \) is the start time of ship \( i \) at its assigned berth.
2. Assignment
Binary variable, \(Assignment_{ib} \forall i \in Ships, b \in Berths\), where \(Assignment_{ib} = 1\) if vessel \(i\) is assigned to berth \(b\), and 0 otherwise.
3. Precedence
Binary variable, \(Precedence_{ij} \forall i, j \in Ships, i \neq j\), where \(Precedence_{ij} = 1\) if vessels \(i\) and \(j\) are assigned to the same berth and \(i\) is serviced before \(j\), and 0 otherwise.
4. DependencySatisfaction
Binary variable, \(DependencySatisfaction_{p} \forall p \in Dependencies\), where \(DependencySatisfaction_p = 1\) if the dependency \(p\) is satisfied (i.e., the dependent ship starts its operation within the allowed time gap after the predecessor ship completes its operation), and 0 otherwise.

Objective:
1. Maximize the total rapid transshipment bonus and minimize the total waiting cost.
max: \sum_{p \in Dependencies} RapidTransshipmentBonus * DependencySatisfaction_p - \sum_{i \in Ships} WaitingCost * (StartTime_i - ArrivalTime_i)

Constraint:
1. Each ship must be assigned to exactly one berth.
\sum_{b \in Berths} Assignment_{ib} = 1 \forall i \in Ships
2. The start time of each ship must be after its arrival time.
StartTime_i \geq ArrivalTime_i \forall i \in Ships
3. The start time of each ship plus its operation time must not exceed the maximum time.
StartTime_i + OperationTime_i \leq MaxTime \forall i \in Ships
4. The precedence relation must be respected.
StartTime_j \geq StartTime_i + OperationTime_i \forall (i, j) \in Dependencies
5. The berth assignment must be non-overlapping for ships assigned to the same berth.
StartTime_i + OperationTime_i \leq StartTime_j + M * (1 - Precedence_{ij}) + M * (2 - Assignment_{ib} - Assignment_{jb}) \forall i, j \in Ships, i \neq j, b \in Berths
StartTime_j + OperationTime_j \leq StartTime_i + M * Precedence_{ij} + M * (2 - Assignment_{ib} - Assignment_{jb}) \forall i, j \in Ships, i \neq j, b \in Berths
6. The bonus logic constraint.
StartTime_j - (StartTime_i + OperationTime_i) \leq MaxTransshipmentGap + M * (1 - DependencySatisfaction_p) \forall p=\(i, j\) \in Dependencies

Type:
Mixed, Linear, Linear
MILP

Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB
from typing import List, Dict, Tuple

def solve_berth_allocation(
    Ships: List[str] = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
    Berths: List[str] = ['B1', 'B2', 'B3'],
    ArrivalTime: Dict[str, int] = {'A': 2, 'B': 5, 'C': 10, 'D': 12, 'E': 18, 'F': 20, 'G': 25, 'H': 30, 'I': 35, 'J': 40},
    OperationTime: Dict[str, int] = {'A': 8, 'B': 10, 'C': 6, 'D': 9, 'E': 7, 'F': 11, 'G': 8, 'H': 12, 'I': 5, 'J': 9},
    Dependencies: List[Tuple[str, str]] = [('A', 'C'), ('D', 'E'), ('B', 'G'), ('H', 'I'), ('G', 'J')],
    MaxTime: int = 72,
    RapidTransshipmentBonus: int = 50000,
    WaitingCost: int = 2000,
    MaxTransshipmentGap: int = 2,
    M: int = 1000
):
    """
    Solves the berth allocation problem using Gurobi.

    Args:
        Ships (List[str]): A list of ship identifiers.
        Berths (List[str]): A list of available berth identifiers.
        ArrivalTime (Dict[str, int]): A dictionary mapping each ship to its Estimated Time of Arrival (ETA) in hours.
        OperationTime (Dict[str, int]): A dictionary mapping each ship to its required service time at the berth in hours.
        Dependencies (List[Tuple[str, str]]): A list of tuples, where each tuple (i, j) means ship i must finish before ship j can start.
        MaxTime (int): The total time window for scheduling, in hours.
        RapidTransshipmentBonus (int): The monetary bonus for achieving a rapid transshipment.
        WaitingCost (int): The cost incurred for each hour a ship waits before service.
        MaxTransshipmentGap (int): The maximum time gap (in hours) between dependent tasks to qualify for the bonus.
        M (int): A sufficiently large number used for "big-M" method in linear programming.
    """
    
    # --- Model Initialization ---
    model = gp.Model("BerthAllocation")

    # --- Decision Variables ---
    # StartTime_i: Start time of service for ship i
    StartTime = model.addVars(Ships, name="StartTime", vtype=GRB.CONTINUOUS, lb=0)
    
    # Assignment_ib: 1 if ship i is assigned to berth b, 0 otherwise
    Assignment = model.addVars(Ships, Berths, name="Assignment", vtype=GRB.BINARY)
    
    # Precedence_ij: 1 if ship i and j are on the same berth and i is first, 0 otherwise
    Precedence = model.addVars(Ships, Ships, name="Precedence", vtype=GRB.BINARY)

    # DependencySatisfaction_p: 1 if precedence pair p=(i,j) gets the bonus, 0 otherwise
    DependencySatisfaction = model.addVars(Dependencies, name="DependencySatisfaction", vtype=GRB.BINARY)

    # W_i: Waiting time for ship i (StartTime_i - ArrivalTime_i)
    w = model.addVars(Ships, name="WaitTime", vtype=GRB.CONTINUOUS, lb=0)


    # --- Objective Function ---
    total_bonus = gp.quicksum(RapidTransshipmentBonus * DependencySatisfaction[p] for p in Dependencies)
    total_waiting_cost = gp.quicksum(WaitingCost * w[i] for i in Ships)

    model.setObjective(total_bonus - total_waiting_cost, GRB.MAXIMIZE)

    # --- Constraints ---
    # Waiting time definition
    model.addConstrs((w[i] == StartTime[i] - ArrivalTime[i] for i in Ships), "DefWaitTime")

    # Constraint 1: Unique berth assignment for each ship
    model.addConstrs((gp.quicksum(Assignment[i, b] for b in Berths) == 1 for i in Ships), "UniqueBerth")

    # Constraint 2: Service must start after ETA
    model.addConstrs((StartTime[i] >= ArrivalTime[i] for i in Ships), "RespectETA")

    # Constraint 3: Service must finish within the planning horizon
    model.addConstrs((StartTime[i] + OperationTime[i] <= MaxTime for i in Ships), "Horizon")

    # Constraint 4: Precedence constraints for dependent ships
    for i, j in Dependencies:
        model.addConstr(StartTime[j] >= StartTime[i] + OperationTime[i], f"Precedence_{i}_{j}")

    # Constraint 5: Berth non-overlapping constraints
    for i in Ships:
        for j in Ships:
            if i == j:
                continue
            for b in Berths:
                # If i and j are on the same berth b, one must precede the other.
                # Precedence_ij + Precedence_ji = 1 if Assignment_ib=1 and Assignment_jb=1
                model.addConstr(Precedence[i, j] + Precedence[j, i] >= Assignment[i, b] + Assignment[j, b] - 1, f"RelativeOrder_{i}_{j}_{b}")

                # If Precedence_ij = 1 (i before j), enforce time separation
                model.addConstr(StartTime[i] + OperationTime[i] <= StartTime[j] + M * (1 - Precedence[i, j]) + M * (2 - Assignment[i, b] - Assignment[j, b]), f"NoOverlap_{i}_before_{j}_on_{b}")

    # Constraint 6: Bonus logic constraints
    for p in Dependencies:
        i, j = p
        # If DependencySatisfaction_p is 1, then the gap must be <= MaxTransshipmentGap.
        # StartTime_j - (StartTime_i + OperationTime_i) <= MaxTransshipmentGap + M * (1 - DependencySatisfaction_p)
        model.addConstr(StartTime[j] - (StartTime[i] + OperationTime[i]) <= MaxTransshipmentGap + M * (1 - DependencySatisfaction[p]), f"BonusLogic_{i}_{j}")


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


Solution status: OPTIMAL
Optimal objective value: 194000