Optimizing Port Rotation using Python and Searoute

Jordan Taylor
Shipping Intel
Published in
7 min readAug 24, 2023

Vessel owners are often tasked with finding optimal port rotations for oceangoing vessels. Obtaining the least distance throughout a rotation may lower the cost of operating a vessel though savings in fuel and time. Use of Python’s Searoute (Halili, 2022) used in conjunction with the United States National Geospatial Agency’s World Port Index is offered for consideration.

Introduction

The push is on to use information technology to streamline the supply chain in an effort to reduce consumption of fossil fuels (International Maritime Organization, 2023). This effort aligns with the goals of vessel owners who often search for ways to reduce cost.

Searoute is a Python package based on Gaffuri’s Searoute, which is deployed by the statistical office of the European Union. A gridded network is combined with Dijkstra’s algorithm to determine economical routes. By obtaining all possible combinations of port rotations and the resultant combinations applied to Searoute, a resultant path of least distance can be calculated. The outcome may serve to assist a vessel owner on settling on an optimal rotation vis-à-vis other factors such as controlling draft, weather, or changes in cargo intake.

Method

A 20,000 deadweight chemical tanker departs New Orleans to load edible oils in:

  1. Maracaibo
  2. Buenaventura
  3. Puerto Limon

The vessel will return to Houston to discharge. What is the most economical route?

Step 1: Declaration of Dependencies

Import Python’s fuzzywuzzy, searoute, csv, and intertools.

import csv, itertools
import searoute as sr
from fuzzywuzzy import process

Step 2: Create a Function to Access the World Port Database

A world port database file (“UpdatedPub150.csv”) must be uploaded and a path established to the file. The database, which contains port coordinate data, is updated monthly by the United States National Geospatial Agency.

Step two is detailed in a separate article.

def world_port_index(port_to_match):
# Establish a file path.
# Note that the file in this example must be in the local directory.
# Location of original WPI CSV file: https://msi.nga.mil/Publications/WPI
csv_file = "UpdatedPub150.csv"

# Obtain Query Values as a List
with open(csv_file, "r") as file:
reader = csv.reader(file)
column_values = [row[2] for row in reader]

# Natural Language Processing
best_match = process.extractOne(port_to_match, column_values)

# Match the Port with the Row Data
with open(csv_file, "r") as file:
reader = csv.DictReader(file)
for row in reader:
if row['Main Port Name'] == best_match[0]:
port_information = dict(row)

return port_information

Step 3: Create a Function to Obtain Distance

For the purposes of this effort a route is a path between two ports within the voyage. A rotation is the sequence of ports that the vessel will visit. Obtaining the distance between each port is necessary to find the total distance throughout the rotation. Therefore, the function route_distance() is defined. The return value is the route distance in nautical miles.

def route_distance(origin, destination):
# Find origin and destination port in the World Port Information (WPI) database
wpi_origin_port = world_port_index(origin)
wpi_destination_port = world_port_index(destination)

# Extract WPI-sourced latitude and longitude coordinates from origin and destination ports
wpi_origin_coordinates = [float(wpi_origin_port['Latitude']), float(wpi_origin_port['Longitude'])]
wpi_destination_coordinates = [float(wpi_destination_port['Latitude']), float(wpi_destination_port['Longitude'])]

# Swap latitude and longitude values to conform with Searoute's coordinate index
wpi_origin_coordinates[0], wpi_origin_coordinates[1] = wpi_origin_coordinates[1], wpi_origin_coordinates[0]
wpi_destination_coordinates[0], wpi_destination_coordinates[1] = wpi_destination_coordinates[1], \
wpi_destination_coordinates[0]

# Create Searoute object
sea_route = sr.searoute(wpi_origin_coordinates, wpi_destination_coordinates, units="naut")

# Extract distance and duration from the route properties
distance = int(sea_route['properties']['length'])

# Return distance in nautical miles
return distance

Step 4: Obtain all Rotations for Intermediary Ports

In this example we will find all permutations of intermediate ports between New Orleans and Houston. The vessel must depart from New Orleans and return to Houston. The port rotation in between New Orleans is changeable. Itertools is employed to find all permutations of intermediary ports.

all_intermediate_rotations = itertools.permutations(proposed_rotation[1:-1])

Step 5: Create a Container for all Possible Rotations

rotations = []

Step 6: Iterate through all Rotations and Find the Total Distance

All possible rotations are evaluated based on distance.

# Apply all rotations to Searoute
for single_intermediate_rotation in all_intermediate_rotations:
# Reassemble full rotation from intermediary rotations
proposed_rotation = [proposed_rotation[0], *single_intermediate_rotation, proposed_rotation[-1]]

# Declare a new total distance and total time object for the rotation
total_distance = 0

# Get route distance between each port
for i in range(0, len(proposed_rotation) - 1):
leg_distance = route_distance(proposed_rotation[i], proposed_rotation[i + 1])
total_distance += leg_distance

# Return the WPI port name for comparison to entering arguments.
wpi_port_names = [world_port_index(port)['Main Port Name'] for port in proposed_rotation]
rotations.append([total_distance, wpi_port_names])

Step 7: Obtain Best Possible Rotation

# Obtain the best rotation among a list of rotations based on distance
rotations = sorted(rotations, key=lambda x: x[0])

Outcome

From the effort above we find the best rotation is New Orleans / Maracaibo / Buenaventura / Puerto Limon / Houston. The total distance is 5,140 nautical miles. At 12.5 knots, the total time enroute excluding port time is 17.1 days.

Credit: Folium

For comparison, the worst route is New Orleans / Puerto Limon / Maracaibo / Buenaventura / Houston at a total distance of 5,407 nautical miles. At 12.5 knots, the total time enroute excluding port time is 18 days.

Bunker fuel is approximately $600 USD per metric ton. If the vessel burns 35 metric tons a day, the difference in fuel cost between the best and worst rotation is $18,900 USD.

The solution also presents opportunities to overlay data on top of the routes as well. For example, within the WPI port database channel depth can be obtained. When combined with vessel draft, the concept of a rotation based on distance and maximum allowable draft is possible.

Final Code Example

Note that steps four through seven have been packaged as a function.

import csv, itertools
import searoute as sr
from fuzzywuzzy import process


def world_port_index(port_to_match):
# Establish a file path.
# Note that the file in this example must be in the local directory.
# Location of original WPI CSV file: https://msi.nga.mil/Publications/WPI
csv_file = "UpdatedPub150.csv"

# Obtain Query Values as a List
with open(csv_file, "r") as file:
reader = csv.reader(file)
column_values = [row[2] for row in reader]

# Natural Language Processing
best_match = process.extractOne(port_to_match, column_values)

# Match the Port with the Row Data
with open(csv_file, "r") as file:
reader = csv.DictReader(file)
for row in reader:
if row['Main Port Name'] == best_match[0]:
port_information = dict(row)

return port_information


def route_distance(origin, destination):
# Find origin and destination port in the World Port Information (WPI) database
wpi_origin_port = world_port_index(origin)
wpi_destination_port = world_port_index(destination)

# Extract WPI-sourced latitude and longitude coordinates from origin and destination ports
wpi_origin_coordinates = [float(wpi_origin_port['Latitude']), float(wpi_origin_port['Longitude'])]
wpi_destination_coordinates = [float(wpi_destination_port['Latitude']), float(wpi_destination_port['Longitude'])]

# Swap latitude and longitude values to conform with Searoute's coordinate index
wpi_origin_coordinates[0], wpi_origin_coordinates[1] = wpi_origin_coordinates[1], wpi_origin_coordinates[0]
wpi_destination_coordinates[0], wpi_destination_coordinates[1] = wpi_destination_coordinates[1], \
wpi_destination_coordinates[0]

# Create Searoute object
sea_route = sr.searoute(wpi_origin_coordinates, wpi_destination_coordinates, units="naut")

# Extract distance and duration from the route properties
distance = int(sea_route['properties']['length'])

# Return distance in nautical miles
return distance


def optimize_rotation(proposed_rotation):
# Obtain all rotations for intermediary ports
all_intermediate_rotations = itertools.permutations(proposed_rotation[1:-1])

rotations = []

# Apply all rotations to Searoute
for single_intermediate_rotation in all_intermediate_rotations:
# Reassemble full rotation from intermediary rotations
proposed_rotation = [proposed_rotation[0], *single_intermediate_rotation, proposed_rotation[-1]]

# Declare a new total distance and total time object for the rotation
total_distance = 0

# Get route distance between each port
for i in range(0, len(proposed_rotation) - 1):
leg_distance = route_distance(proposed_rotation[i], proposed_rotation[i + 1])
total_distance += leg_distance

# Return the WPI port name for comparison to entering arguments.
wpi_port_names = [world_port_index(port)['Main Port Name'] for port in proposed_rotation]
rotations.append([total_distance, wpi_port_names])

# Obtain the best rotation among a list of rotations based on distance
rotations = sorted(rotations, key=lambda x: x[0])

# Return all rotations with distances as a list. The first rotation in the index is the optimized rotation.
return rotations

rotation = ['New Orleans', 'Maracaibo', 'Buenaventura', 'Puerto Limon', 'Houston']

rotation = optimize_rotation(rotation)

Conclusion

There are limitations. The first is the employment of a natural language processor within the function world_port_index() to look up port names rather than a port locator code. This may cause an unwanted port to appear in the place of the wanted port within the rotation. Step 6 includes the WPI-sourced port rotation names (line 15) to check against entering arguments.

With respect to the incredible work that Eurostat, Julien Gaffuri, and Gent Halili have done with Searoute, the maritime network grid fidelity is poor in the Western Hemisphere. Therefore, tracks along the route may not reflect actual vessel routes. Python Searoute’s author Gent Halili underlines this point by indicating that the package should not be used for routing and used for visualization only.

An improvement in fidelity of the gridded network will be necessary to achieve a more accurate representation of route distances and coordinates. A solution to this issue is producing a representation of the proposed route using Folium, and also checking against another source such as the vessel’s actual route plan. Distance tables may be employed as well.

Over time, with an improvement in fidelity, the gridded network solution will present a powerful tool and will be of great benefit to promoting efficiency within the supply chain.

References

Gaffuri, J., & Eurostat. (n.d.). Searoute. Retrieved August 20, 2023, from https://github.com/eurostat/searoute

Halili, G. (n.d.). Searoute. Searoute. Retrieved August 15, 2023, from https://pypi.org/project/searoute/

IMO’s work to cut GHG emissions from ships. (n.d.). International Maritime Organization. Retrieved August 20, 2023, from https://www.imo.org/en/MediaCentre/HotTopics/Pages/Cutting-GHG-emissions.aspx

Maritime Safety Information. (n.d.). Maritime Safety Information. Retrieved August 19, 2023, from https://msi.nga.mil/Publications/WPI

seatgeek/fuzzywuzzy: Fuzzy String Matching in Python. (n.d.). GitHub. Retrieved August 20, 2023, from https://github.com/seatgeek/fuzzywuzzy

Stopford, M. (2009). Maritime economics. Routledge.

--

--

Jordan Taylor
Shipping Intel

Merchant marine officer with a B.S. in Marine Transportation and a M.S. in Transportation Management.