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.
- Java
- Python
- Rust
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
import pandas as pd
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")
for dp in client.timeseries.retrieve_datapoints(rf)[0].get_datapoints():
compare_to_target(dp.timestamp, dp.sum)
use dataplatform_rust_sdk::generic::{DataWrapper, RetrieveFilter};
use chrono::Utc;
let filter = RetrieveFilter {
external_id: Some("crane_qc_07_moves".into()),
start: Some(Utc::now() - chrono::Duration::hours(6)),
end: Some(Utc::now()),
aggregates: Some(vec!["sum".into()]),
granularity: Some("1h".into()),
..Default::default()
};
let series = api.time_series
.retrieve_datapoints(&DataWrapper::from(vec![filter])).await?
.get_items().remove(0);
for p in &series.datapoints { compare_to_target(&p.timestamp, p.sum); }
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.
- Java
- Python
- Rust
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));
client.events.create([datahub_sdk.Event(
external_id=f"yard_congestion_b07_{int(pd.Timestamp.now().timestamp())}",
type="yard_congestion", status="open",
event_time=pd.Timestamp.now(tz="UTC"),
metadata={"block": "yard_block_b07", "occupancy_pct": "94"})])
use dataplatform_rust_sdk::events::Event;
use chrono::Utc;
let mut congestion = Event::new(format!("yard_congestion_b07_{}", Utc::now().timestamp()));
congestion.r#type = Some("yard_congestion".into());
congestion.status = Some("open".into());
congestion.add_metadata("block".into(), "yard_block_b07".into());
congestion.add_metadata("occupancy_pct".into(), "94".into());
congestion.set_event_time(Utc::now());
api.events.create(&vec![congestion]).await?;
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
- Query & aggregate — crane moves-per-hour.
- Turn readings into events — the congestion rule.
- High-throughput ingestion — equipment telemetry volume.