Skip to main content

Ports — container terminal throughput

The problem. A container terminal is judged on two numbers: how fast cranes work a ship, and how long boxes sit in the yard. A crane running below its move-rate keeps a vessel — and its berthing fees — alongside longer than planned; a container that dwells too long clogs stacks and blocks the next move. Terminal operations needs to see a crane falling behind and a yard filling up in time to rebalance, not at the end-of- shift report.

What we solve here is berth productivity and yard congestion — the throughput the whole terminal is measured on.

Set up demo data

New workspace? Run this once (Python) to create a crane's move series with six hours of readings. Safe to re-run.

import datahub_sdk, numpy as np, pandas as pd

client = datahub_sdk.DataHubClient.from_env()

client.timeseries.create([datahub_sdk.TimeSeries(external_id="crane_qc_07_moves", name="Crane QC-07 moves", unit="count", value_type="float")])
idx = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=6 * 60, freq="1min")
client.timeseries.insert_from_lists(timestamps=idx, values=np.random.poisson(0.45, len(idx)).astype(float), ts="crane_qc_07_moves")

1. Watch crane productivity by the hour

Crane moves are series per crane. Roll them up to moves-per-hour and compare against the target to find an under-performing crane mid-vessel. See Query & aggregate.

var filter = new RetrieveFilter();
filter.setExternalId("crane_qc_07_moves");
filter.setStart(ZonedDateTime.now().minusHours(6));
filter.setEnd(ZonedDateTime.now());
filter.setAggregates(List.of("sum"));
filter.setGranularity("1h");

var request = new DataRetriever<RetrieveFilter>();
request.setItems(List.of(filter));

client.timeseries().retrieve(request).getItems().get(0).getDatapoints()
.forEach(p -> compareToTarget(p.getTimestamp(), p.getValue())); // moves/hour

2. Flag yard congestion before it blocks moves

Stack occupancy is a series per yard block. When a block crosses its safe fill, raise a yard_congestion event so planners divert incoming boxes to another block. See Turn readings into events.

EventModel congestion = new EventModel();
congestion.setExternalId("yard_congestion_b07_" + System.currentTimeMillis());
congestion.setType("yard_congestion");
congestion.setStatus("open");
congestion.setMetadata(Map.of("block", "yard_block_b07", "occupancy_pct", "94"));
congestion.setEventTime(ZonedDateTime.now());
client.events().create(List.of(congestion));

See the result

Chart moves-per-hour against the target the terminal works to:

import matplotlib.pyplot as plt

rf = datahub_sdk.RetrieveFilter(ts="crane_qc_07_moves",
start=pd.Timestamp.now(tz="UTC") - pd.Timedelta(hours=6), end=pd.Timestamp.now(tz="UTC"),
aggregates=["sum"], granularity="1h")
moves = [dp.sum for dp in client.timeseries.retrieve_datapoints(rf)[0].get_datapoints()]
plt.bar(range(len(moves)), moves); plt.axhline(25, color="r", ls="--")
plt.title("Crane QC-07 moves per hour"); plt.show()

See also