Manufacturing — production quality & OEE
The problem. A factory floor is dozens of stations, each emitting a cycle time, a unit count, and the occasional scrap event — thousands of readings a minute. Plant managers don't want the raw firehose; they want Overall Equipment Effectiveness (availability × performance × quality), rolled up per station, line and shift, plus an alert when a station's scrap rate spikes.
This scenario centres on ingesting at volume and aggregating the result into the hourly and shift-level numbers a production dashboard actually shows.
Set up demo data
New workspace? Run this once (Python) to create the station's series with an 8-hour shift of readings, so the OEE roll-up has data. Safe to re-run.
import datahub_sdk, numpy as np, pandas as pd
client = datahub_sdk.DataHubClient.from_env()
for s, u in [("station_07_cycle_time_s", "s"), ("station_07_scrap", "count")]:
client.timeseries.create([datahub_sdk.TimeSeries(external_id=s, name=s, unit=u, value_type="float")])
idx = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=8 * 60, freq="1min")
client.timeseries.insert_from_lists(timestamps=idx, values=11 + np.random.normal(0, 0.6, len(idx)), ts="station_07_cycle_time_s")
client.timeseries.insert_from_lists(timestamps=idx, values=np.random.poisson(0.3, len(idx)).astype(float), ts="station_07_scrap")
1. Model the line and stream its output
A plant contains lines, a line contains stations. Each station's signals are
series: station_07_cycle_time_s, station_07_units, station_07_scrap.
- Java
- Python
- Rust
var cycleTime = Timeseries.of("station_07_cycle_time_s").name("Station 07 cycle time");
cycleTime.setUnit("s");
var scrap = Timeseries.of("station_07_scrap").name("Station 07 scrap count");
scrap.setUnit("count");
client.timeseries().create(cycleTime, scrap);
// readings arrive continuously across every station — hand them off in bulk
client.timeseries().ingest(Map.of(
"station_07_cycle_time_s", cycleTimes, // List<Datapoint>
"station_07_scrap", scrapCounts));
import datahub_sdk
client.timeseries.create([
datahub_sdk.TimeSeries(external_id="station_07_cycle_time_s",
name="Station 07 cycle time", unit="s"),
datahub_sdk.TimeSeries(external_id="station_07_scrap", name="Station 07 scrap count", unit="count")])
client.timeseries.insert_from_lists(
timestamps=timestamps, values=cycle_times, ts="station_07_cycle_time_s")
use dataplatform_rust_sdk::timeseries::TimeSeries;
let mut cycle = TimeSeries::new("station_07_cycle_time_s", "Station 07 cycle time");
cycle.unit = Some("s".into());
api.time_series.create_one(&cycle).await?;
let mut scrap = TimeSeries::new("station_07_scrap", "Station 07 scrap count");
scrap.unit = Some("count".into());
api.time_series.create_one(&scrap).await?;
api.time_series
.insert_datapoint(None, Some("station_07_cycle_time_s".into()), ts, "11.4".into())
.await?;
See high-throughput ingestion for sustaining floor-wide volume.
2. Roll up to hourly OEE inputs
The dashboard needs per-hour averages and totals, not raw cycles. Ask for the
aggregates and granularity and the platform returns one value per bucket — the
performance inputs to OEE. (There's no count aggregate; a per-hour count comes
from sum-ming a per-event count series.) Full mechanics in
Query & aggregate.
- Java
- Python
- Rust
var filter = new RetrieveFilter();
filter.setExternalId("station_07_cycle_time_s");
filter.setStart(ZonedDateTime.now().minusHours(8)); // one shift
filter.setEnd(ZonedDateTime.now());
filter.setAggregates(List.of("avg", "sum"));
filter.setGranularity("1h");
var request = new DataRetriever<RetrieveFilter>();
request.setItems(List.of(filter));
client.timeseries().retrieve(request).getItems().get(0).getDatapoints()
.forEach(p -> recordHourlyPerformance(p.getTimestamp(), p.getValue()));
import pandas as pd
rf = datahub_sdk.RetrieveFilter(
ts="station_07_cycle_time_s",
start=pd.Timestamp.now(tz="UTC") - pd.Timedelta(hours=8),
end=pd.Timestamp.now(tz="UTC"),
aggregates=["avg", "sum"], granularity="1h")
for dp in client.timeseries.retrieve_datapoints(rf)[0].get_datapoints():
record_hourly_performance(dp.timestamp, dp.average, dp.sum)
use dataplatform_rust_sdk::generic::{DataWrapper, RetrieveFilter};
use chrono::Utc;
let filter = RetrieveFilter {
external_id: Some("station_07_cycle_time_s".into()),
start: Some(Utc::now() - chrono::Duration::hours(8)),
end: Some(Utc::now()),
aggregates: Some(vec!["avg".into(), "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 { record_hourly_performance(&p.timestamp, p.average); }
3. Flag a quality excursion
When a station's hourly scrap rate jumps past its limit, record a scrap_excursion
event so the quality team sees a ranked list of stations needing attention.
- Java
- Python
- Rust
EventModel excursion = new EventModel();
excursion.setExternalId("scrap_excursion_st07_" + System.currentTimeMillis());
excursion.setType("scrap_excursion");
excursion.setStatus("open");
excursion.setMetadata(Map.of("station", "station_07", "scrap_rate_pct", "4.8", "limit_pct", "2.0"));
excursion.setEventTime(ZonedDateTime.now());
client.events().create(List.of(excursion));
client.events.create([datahub_sdk.Event(
external_id=f"scrap_excursion_st07_{int(pd.Timestamp.now().timestamp())}",
type="scrap_excursion", status="open",
event_time=pd.Timestamp.now(tz="UTC"),
metadata={"station": "station_07", "scrap_rate_pct": "4.8", "limit_pct": "2.0"})])
use dataplatform_rust_sdk::events::Event;
use chrono::Utc;
let mut excursion = Event::new(format!("scrap_excursion_st07_{}", Utc::now().timestamp()));
excursion.r#type = Some("scrap_excursion".into());
excursion.status = Some("open".into());
excursion.add_metadata("station".into(), "station_07".into());
excursion.add_metadata("scrap_rate_pct".into(), "4.8".into());
excursion.add_metadata("limit_pct".into(), "2.0".into());
excursion.set_event_time(Utc::now());
api.events.create(&vec![excursion]).await?;
See the result
Chart the hourly average cycle time — the line the production dashboard watches:
import matplotlib.pyplot as plt
rf = datahub_sdk.RetrieveFilter(ts="station_07_cycle_time_s",
start=pd.Timestamp.now(tz="UTC") - pd.Timedelta(hours=8), end=pd.Timestamp.now(tz="UTC"),
aggregates=["avg"], granularity="1h")
vals = [dp.average for dp in client.timeseries.retrieve_datapoints(rf)[0].get_datapoints()]
plt.plot(vals); plt.title("Station 07 hourly avg cycle time (s)"); plt.show()
See also
- Query & aggregate — the OEE roll-up mechanics.
- High-throughput ingestion — floor-wide write volume.
- Turn readings into events — the scrap-rate rule.