Mathematical Optimization of PSV Fleet Compartment Allocation in FPSO Operations (MILP)
laobear/FleetCompartmentOptimization
Mathematical Optimization of PSV Fleet Compartment Allocation in FPSO Operations (MILP)
laobear/FleetCompartmentOptimization
In the fast-paced world of logistics and supply chain management, every cubic meter counts. Whether you’re shipping packages across the country or ferrying passengers across the sky, efficient compartment allocation — deciding how to best utilize the limited space in a vehicle, ship or aircraft or determine the lowest cost fleet size and compartment to be able to fulfill customer demands. Which can translate directly into cost savings and improved performance.
Fleet Compartment Optimization is the process of assigning compartments on a vessel to specific compartments — such as tanks, holds, or deck areas — across a fleet of vessels in the most efficient and operationally sound way. In offshore logistics, especially for Platform Supply Vessels (PSVs) serving FPSOs, this involves carefully planning how different cargo types are distributed across multiple vessels and their compartments.
The objectives of fleet compartment optimization often include:
- Minimizing total transportation and operational costs
- Maximizing utilization of tank or deck space across the fleet
- Balancing weight and volume to ensure vessel stability and safety
- Reducing the number of trips and fuel consumption
- Ensuring timely, compliant deliveries to offshore facilities
This problem belongs to the realm of operations research and combinatorial optimization, and is typically solved using mathematical modeling, such as mixed integer linear programming (MILP), heuristics. It must account for real-world constraints like chemical compatibility, tank coating requirements, loading sequences, and vessel-specific limits.
FPSO operations

https://www.modec.com/business/service/floater/fpso/
Floating Production Storage and Offloading units (FPSOs) represent one of the most versatile solutions in modern offshore oil and gas operations. These massive floating vessels serve as complete production facilities on the ocean’s surface, bridging the gap between subsea wells and onshore refineries.
What Makes FPSOs Essential
FPSOs are engineered to handle the entire production chain at sea. They receive crude oil and gas from underwater wells through subsea pipelines, then process these fluids by separating oil, gas, and water using sophisticated onboard equipment. The processed oil is stored in large tanks within the vessel’s hull until it can be transferred to shuttle tankers or transported via pipeline to shore.
Design and Operation
Built on ship-shaped hulls, FPSOs combine maritime engineering with industrial processing capabilities. Their topsides house complex separation and treatment facilities, while the hull provides massive storage capacity. This design allows them to remain stationed over oil fields for years, continuously processing and storing production.
Strategic Advantages
The flexibility of FPSOs makes them particularly valuable for deepwater and remote offshore fields where traditional pipeline infrastructure would be prohibitively expensive. Unlike fixed platforms, FPSOs can be relocated to new fields once production declines, maximizing their economic lifespan and return on investment.
Global Impact
FPSOs have become indispensable in offshore oil production worldwide, enabling the development of previously inaccessible reserves and supporting energy security in regions with significant offshore resources. Their ability to operate in harsh marine environments has opened new frontiers in oil and gas exploration.
🛳️ PSV — Platform Supply Vessel

85m PSV DP-2 — Van Loon Maritime Services B.V.
A Platform Supply Vessel (PSV) is a specialized ship that supports offshore oil and gas platforms by transporting supplies, equipment, and personnel between shore bases and offshore installations.
Key Functions:
- Transport drilling materials: barite, bentonite, cement
- Carry liquid cargo: fuel, fresh water, drilling mud, brine
- Deliver general supplies: tools, spare parts, containers
- Act as an emergency response or standby vessel in some cases
Features:
- Large open deck for cargo containers or pipes
- Below-deck tanks for liquids (fuel, fresh water, chemicals)
- Dynamic positioning (DP) systems for precise maneuvering near platforms
Example:
A PSV might carry drilling mud and equipment to a rig located 150 km offshore, then return with waste or used equipment for maintenance onshore.
Model





🎯Objective Function
The model minimizes the total cost across three key components:
- Vessel Usage Cost
Annual cost depends on vessel class.
∑ vessel_class_choice[v,C] ⋅ annual_cost[C] - Variable Compartment Cost
Depends on size of compartment and type of product.
∑ V_p[v,p] ⋅ unit_cost[p] - Fixed Charge per Compartment Used
Captures one-time build cost.
∑ y_p[v,p] ⋅ fixed_charge
🔒 Constraints Explanation
1. Demand Fulfillment
∑_{v ∈ V} x_{vlpt} = D_{lpt} ∀ l ∈ L, p ∈ P, t ∈ T
This constraint ensures that the total amount of product p delivered by all vessels v to location l at time t exactly meets the known demand D_{lpt}. It guarantees 100% service level—no over- or under-supply.
2. Vessel Capacity Constraint
∑_{l ∈ L} x_{vlpt} ≤ V_{vp} ∀ v ∈ V, p ∈ P, t ∈ T
Each vessel has a limited capacity per product type. This constraint ensures that the total amount of product p transported by vessel v across all locations l at time t does not exceed its assigned capacity V_{vp}.
2b. Capacity Activation (Big-M Linearization)
V_{vp} ≤ M ⋅ y_{vp} ∀ v ∈ V, p ∈ P
This is a Big-M constraint used in linear programming to enforce logic: if compartment p is not used on vessel v (y_{vp} = 0), then its capacity V_{vp} must be 0. If it’s used, the model can assign up to M units. This linearizes a binary decision about whether to activate a compartment.
3. Minimum Compartment Size if Used
V_{vp} ≥ S_min ⋅ y_{vp} ∀ v ∈ V, p ∈ P
If a compartment is used (y_{vp} = 1), then it must carry at least a minimum volume S_min. This avoids allocating tiny, inefficient volumes that aren’t practical to load or clean—especially relevant for hazardous chemicals or viscous fuels.
4. Total Product Volume ≤ Vessel Class Capacity
∑_{p ∈ P} V_{vp} ≤ ∑_{C ∈ C} δ_{v,C} ⋅ Cap_C ∀ v ∈ V
This constraint ensures that the total assigned volume across all products p does not exceed the maximum capacity defined by the vessel’s class C. Each vessel can only belong to one class (δ_{v,C} = 1), and each class has a defined total tank capacity.
5. Product Compartment Usage Only If Vessel Is Used
y_{vp} ≤ z_v ∀ v ∈ V, p ∈ P
A vessel can’t use any compartments unless it’s activated for the operation. This constraint links binary vessel activation (z_v) to the product-level usage decisions (y_{vp}).
6. Max Compartments per Vessel
∑_{p ∈ P} y_{vp} ≤ ∑_{C ∈ C} δ_{v,C} ⋅ MaxComp_C ∀ v ∈ V
Each vessel class supports a maximum number of usable compartments. This constraint ensures we don’t assign more products than physically possible, respecting the compartmentalization structure of different vessel types.
7. Product Compatibility (Incompatible Pairs)
y_{v p₁} + y_{v p₂} ≤ 1 + (1 − δ_{v,C}) ∀ v ∈ V, C ∈ C, (p₁, p₂) ∈ VCPIC_C
Some products are chemically incompatible and cannot be carried on the same vessel due to contamination or safety risks (e.g., oxidizers vs flammables). This constraint ensures incompatible product pairs p₁ and p₂ aren’t assigned to the same vessel v, unless the vessel’s class C allows special containment or separation.
8. One Vessel Class Per Vessel
∑_{C ∈ C} δ_{v,C} = z_v ∀ v ∈ V
Each vessel must be assigned at most one vessel class if it is used. This ensures mutual exclusivity in classification — for example, a vessel cannot simultaneously act as a “small chemical tanker” and a “multi-product fuel vessel”.
Code
These are the necessary modules to build and run the model. The model will be built using gurobipy.
import gurobipy as gp
from gurobipy import GRB
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# For results reproduceability
np.random.seed(42)
The indices, sets, parameters are defined as follows:
# Define sets
vessels = list(range(4))
Vessel_class = ["LR", "MR", "SR"]
product_types = 5
num_demand_locations = 4
demand_locations = range(num_demand_locations)
periods = 14
# Random Demand
demand =
np.random.uniform(
low=50, high=100,
size=(num_demand_locations, product_types, periods)
)
*
np.random.uniform(
low=0.2, high=0.8,
size=(num_demand_locations, product_types, periods)
)

Random generated demand for product 0–4 for 14 time periods
# Vessel characteristics
vessel_annual_cost = {"LR": 150000, "MR": 100000, "SR": 40000}
vessel_compartment_capacity = {"LR": 1000, "MR": 800, "SR": 500}
vessel_max_compartments = {"LR": 8, "MR": 6, "SR": 4}
compartment_costs_per_unit_size_by_product = { 0: 5, 1: 10, 2: 15, 3: 20, 4: 25}
compartment_minimum_size_per_product = 100
compartment_fixed_cost_per_product = 1000
# Define product incompatibility with other products
Vessel_product_compatibility = {
"LR": [(3,4)],
"MR": [(3,4)],
"SR": [(0,3),(0,4),
(1,2),(1,3),(1,4),
(2,3),(2,4),
(3,4)]
}
Model
# === Setup ===
model = gp.Model("FleetCompartmentOptimization")
# === Variables ===
x = model.addVars(vessels, demand_locations, range(product_types), range(periods),
vtype=GRB.CONTINUOUS, name="x")
y_p = model.addVars(vessels, range(product_types), vtype=GRB.BINARY, name="vessel_product_compartment")
V_p = model.addVars(vessels, range(product_types), vtype=GRB.CONTINUOUS, name="vessel_capacity")
z = model.addVars(vessels, vtype=GRB.BINARY, name="vessel_used")
vessel_class_choice = model.addVars(vessels, Vessel_class, vtype=GRB.BINARY, name="vessel_class_choice")
# === Constraints ===
# 1. Fulfill demand
for l in demand_locations:
for p in range(product_types):
for t in range(periods):
model.addConstr(
gp.quicksum(x[v,l,p,t] for v in vessels) == demand[l,p,t],
name=f"DemandFulfillment_{l}_{p}_{t}"
)
# 2. Transported amount ≤ vessel capacity
for v in vessels:
for p in range(product_types):
for t in range(periods):
model.addConstr(
gp.quicksum(x[v, l, p, t] for l in demand_locations) <= V_p[v, p],
name=f"TransportedAmount_{v}_{p}_{t}"
)
# 2b. Activate capacity only if compartment is used
for v in vessels:
for p in range(product_types):
model.addConstr(
V_p[v, p] <= big_M * y_p[v, p],
name=f"CapacityActivation_{v}_{p}"
)
# 3. Minimum compartment size if y_p[v,p] > 0
for v in vessels:
for p in range(product_types):
model.addConstr(
V_p[v,p] >= compartment_minimum_size_per_product * y_p[v,p],
name=f"MinCompartmentSize_{v}_{p}"
)
# 4. Ensure vessel capacity is not exceeded
for v in vessels:
model.addConstr(
gp.quicksum(
V_p[v,p] for p in range(product_types)
) <= gp.quicksum(
vessel_class_choice[v,C] * vessel_compartment_capacity[C]
for C in Vessel_class
),
name=f"VesselTotalCapacity_{v}"
)
# 5. Vessel Product Compartment can only be used if the vessel is used
for v in vessels:
for p in range(product_types):
model.addConstr(
y_p[v,p] <= z[v],
name=f"CompartmentUsage_{v}_{p}"
)
# 6. Each vessel can have a maximum number of compartments
for v in vessels:
model.addConstr(
gp.quicksum(y_p[v,p] for p in range(product_types)) <= gp.quicksum(vessel_max_compartments[C] * vessel_class_choice[v, C] for C in Vessel_class),
name=f"MaxCompartments_{v}"
)
# 7. vessel compartment product incompatibility
for v in vessels:
for C in Vessel_class:
for p1, p2 in Vessel_product_compatibility.get(C, []):
model.addConstr(
y_p[v, p1] + y_p[v, p2] <= 1 + (1 - vessel_class_choice[v, C]),
name=f"Incompatibility_{v}_{C}_{p1}_{p2}"
)
# 8. Each vessel must choose exactly one type if used
for v in vessels:
model.addConstr(
gp.quicksum(vessel_class_choice[v,C] for C in Vessel_class) == z[v],
name=f"VesselTypeChoice_{v}"
)
# === Objective Function ===
# 1. Vessel usage cost based on selected type
vessel_cost_expr = gp.quicksum(
vessel_class_choice[v,C] * vessel_annual_cost[C]
for v in vessels for C in Vessel_class
)
# Fixed charge per compartment
# 2. Compartment size cost
compartment_cost_expr = gp.quicksum(
compartment_costs_per_unit_size_by_product[p] * V_p[v,p]
for v in vessels for p in range(product_types)
)
# 3. Fixed charge for each compartment used
fixed_charge_expr = gp.quicksum(
compartment_fixed_cost_per_product * y_p[v,p]
for v in vessels for p in range(product_types)
)
# Total cost = vessel cost + compartment variable cost + fixed charge per compartment
model.setObjective(
vessel_cost_expr + compartment_cost_expr + fixed_charge_expr,
GRB.MINIMIZE
)
# === Solve ===
# model.Params.MIPFocus = 1 # Focus on finding feasible solutions quickly
# model.Params.MIPGap = 0.01 # Set a gap tolerance of 1
# model.Params.TimeLimit = 600 # Set a time limit of 10 minutes
# model.Params.Presolve = 2 # Aggressive presolve to reduce problem size
# model.Params.NodeMethod = 1 # Use the best bound method for integer variables
# model.Params.Heuristics = 0 # Allow heuristics to find feasible solutions
# log to console
model.setParam('LogFile', 'gurobi_log.txt')
model.optimize()
Results
The optimized results are as follows:
Total load by product on each vessels vs time:

Total products carried by each vessel by each time period
As for the Vessel Fleet configuration, the most optimal configuration would be:

Vessel fleet compartment configuration, vessel class, capacity & compartment utilization
🙌 Thanks for reading! If you enjoyed this, feel free to clap 👏 and hit “Follow” to stay updated with my upcoming articles
메타데이터
- post_id
- 2dca8112f049
- slug
- mathematical-optimization-of-psv-fleet-compartment-allocation-in-fpso-operations-milp-2dca8112f049
- url
- https://medium.com/@williamyeoh/mathematical-optimization-of-psv-fleet-compartment-allocation-in-fpso-operations-milp-2dca8112f049
- canonical_url
- https://medium.com/@williamyeoh/mathematical-optimization-of-psv-fleet-compartment-allocation-in-fpso-operations-milp-2dca8112f049
- author_url
- https://medium.com/@williamyeoh
- status
- ok
- fetched_at
- 2026-07-18 21:04:36