Petrodactyl is the DS oil company. Their supply chain is hitting a bottleneck when they have 450,000 kL of (200,000 kL) Petrol, (150,000 kL) Diesel, and (100,000 kL) Kerosene that need to reach the depot within the next 168 hours. Usually, they’d group the products into long runs to save on waste, but the local depot just called: their tanks are low across the board.
To keep their trucks moving, we are forced to interleave. We have to push 9 batches (4 Petrol, 3 Diesel, 2 Kerosene) through the single pipeline with area 0.5 m^2, but with a strict inventory constraint: no two consecutive batches can be of the same type.
This constant switching is expensive. Every time we change products, the interface creates transmix waste costs that scale with the velocity of the leading batch to the power of 1.5 (c_ij v^1.5). Some transitions are pricier than others. Below are the (symmetrical) constants c_ij for each transition:
- Petrol-Diesel: $2,000
- Diesel-Kerosene: $3,500
- Petrol-Kerosene: $8,000
On top of the waste, we’re fighting physics. Energy costs scale with the square of the velocity times the volume of the batch (0.5 Volume v^2).
With batch sizes limited between 10k and 100k kL and velocities capped between 1.0 and 3.0 m/s, the clock is our enemy. Every second we save by speeding up increases the energy bill and the waste penalty. The time needed per batch is Volume / (1,800 Velocity) [in hours].
Your task: Sequence these 9 batches and choose volumes and velocities for each batch so that we move all product within the 168-hour deadline for the lowest total cost.

Start by sequencing the batches.

Seeker (full model below) suggests this solution:
Sequence K - P - D - P - D - P - D - P - K
Volumes 10000.23 (K) - 99750.44 (P) - 40087.36 (D) - 10001.90 (P) - 10000.01 (D) - 80247.57 (P) - 99912.63 (D) - 10000.08 (P) - 89999.77 (K)
Velocities 1.1910 (K) - 1.5100 (P) - 1.4928 (D) - 1.4120 (P) - 1.4134 (D) - 1.5074 (P) - 1.5105 (D) - 1.1889 (P) - 1.5219 (K)
Time: 168 hours
Waste Costs: $42,258.11
Energy Costs: $501,878.21
Total Costs: $544,136.32
Problems like this one are all around us. More often than not they are solved poorly by hand because no solver supports modeling them with ease. We created InsideOpt Seeker to change that. Modeling such a problem is as easy as 1, 2, 3: Define the decisions to be made (sequence, velocities, and volumes). Derive the terms for the constraints and the objective. Then add the constraints and optimize the objective. No need for a degree in operations research. Every developer can do this. When will you start modeling in Seeker?
products = ["petrol", "diesel", "kerosene"]
waste_costs_dir = {"petrol": {"diesel": 2000, "kerosene": 8000},
"diesel": {"petrol": 2000, "kerosene": 3500},
"kerosene": {"petrol": 8000, "diesel": 3500}
}
waste_costs = [[0, 2000, 8000],
[2000, 0, 3500],
[8000, 3500, 0]]
product_volumes_dir = {"petrol": 200000, "diesel": 150000, "kerosene": 100000}
product_volumes = [200000, 150000, 100000]
low_volume = 10000
high_volume = 100000
low_velocity = 1
high_velocity = 3
area_time = 168 * 0.5 * 3600
batch_number = [4, 3, 2]
batch_type = [0, 0, 0, 0, 1, 1, 1, 2, 2]
batch_index = [0, 1, 2, 3, 0, 1, 2, 0, 1]
# ----------------------- SEEKER MODEL ------------------
import seeker as skr
env = skr.Env("license.sio")
# make data indexable
waste_ten = env.tensor(waste_costs)
volume_ten = env.tensor(product_volumes)
types_ten = env.tensor(batch_type)
# create decisions
sequence = env.permutation(len(batch_type), 0).get_permutation()
velocity = [env.continuous(low_velocity, high_velocity)
for _ in batch_type]
velocities_ten = env.tensor(velocity)
volume = [[a * pv for a in env.convex_combination(batch_number[i]).get_combination()]
for i, pv in enumerate(product_volumes)]
volumes = [a for v in volume for a in v]
volumes_ten = env.tensor(volumes)
# constrain the volumes
for i, v in enumerate(volumes):
env.enforce_leq(v, high_volume, "high_volume" + str(i))
env.enforce_geq(v, low_volume, "low_volume" + str(i))
# constrain total time
area_times = [volumes[i] / velocity[i]
for i in range(len(batch_type))]
total_time = env.sum(area_times)
env.enforce_leq(total_time, area_time)
# compute waste costs and enforce product change between
# consecutive batches
energy_costs = []
waste_costs = []
for i in range(len(batch_type) - 1):
current_batch = sequence[i]
next_batch = sequence[i + 1]
current_type = types_ten[current_batch]
next_type = types_ten[next_batch]
env.enforce_neq(current_type, next_type, "neq" + str(i))
current_velo = velocities_ten[current_batch]
transition_cost = (waste_ten[current_type, next_type] *
current_velo * env.sqrt(current_velo))
waste_costs.append(transition_cost)
for i in range(len(batch_type)):
current_batch = sequence[i]
vol = volumes_ten[current_batch]
velo2 = env.sqr(velocities_ten[current_batch])
energy_costs.append(vol * velo2)
# derive total costs
total_waste = env.sum(waste_costs)
total_energy = env.sum(energy_costs) * 0.5
total_costs = total_energy + total_waste
# minimize costs
env.minimize(total_costs, 600)
print("sequence", [a.get_value() for a in sequence])
print("volumes", [v.get_value() for v in volumes])
print("velocities", [v.get_value() for v in velocity])
print("time", total_time.get_value() / (3600 * 0.5), "hours")
print("waste_costs", total_waste.get_value())
print("energy_costs", total_energy.get_value())
print("total", total_costs.get_value())
env.end()
