Mining — processing plant recovery
The problem. After the rock is hauled, the money is made (or lost) in the concentrator: how much of the valuable metal the mill actually recovers from the ore. A recovery that quietly slips a couple of percent — from a grind that's too coarse, a reagent dose that's off, a feed grade that changed — is metal going out with the tailings, worth a fortune over a shift. Metallurgists need recovery and throughput as live, rolled-up numbers so they can act on a dip while the ore is still in the circuit.
What we solve here is protecting metal recovery by turning mill telemetry into the KPIs the control room steers on.
Set up demo data
New workspace? Run this once (Python) to create the mill's recovery series with a dip below target. 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="mill_1_recovery_pct", name="Mill 1 recovery %", unit="pct", value_type="float")])
idx = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=12, freq="1h")
rec = np.full(12, 91.0); rec[-4:] -= np.linspace(0, 3, 4) # dips below the 91% target
client.timeseries.insert_from_lists(timestamps=idx, values=rec, ts="mill_1_recovery_pct")
1. Track recovery and throughput
Feed grade, concentrate grade, tailings grade and tonnage are series; recovery is computed from them. Roll them up hourly to see recovery against target. See Query & aggregate.
- Java
- Python
- Rust
var filter = new RetrieveFilter();
filter.setExternalId("mill_1_recovery_pct");
filter.setStart(ZonedDateTime.now().minusHours(12));
filter.setEnd(ZonedDateTime.now());
filter.setAggregates(List.of("avg"));
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())); // % recovery
import pandas as pd
rf = datahub_sdk.RetrieveFilter(
ts="mill_1_recovery_pct",
start=pd.Timestamp.now(tz="UTC") - pd.Timedelta(hours=12),
end=pd.Timestamp.now(tz="UTC"),
aggregates=["avg"], granularity="1h")
for dp in client.timeseries.retrieve_datapoints(rf)[0].get_datapoints():
compare_to_target(dp.timestamp, dp.average)
use dataplatform_rust_sdk::generic::{DataWrapper, RetrieveFilter};
use chrono::Utc;
let filter = RetrieveFilter {
external_id: Some("mill_1_recovery_pct".into()),
aggregates: Some(vec!["avg".into()]),
granularity: Some("1h".into()),
start: Some(Utc::now() - chrono::Duration::hours(12)),
end: Some(Utc::now()),
..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.average); }
2. Flag a recovery dip
When recovery drops below target for a sustained period, raise a recovery_loss event so
the metallurgist investigates grind, reagents or feed before more metal is lost. See
Turn readings into events.
- Java
- Python
- Rust
EventModel loss = new EventModel();
loss.setExternalId("recovery_loss_mill1_" + System.currentTimeMillis());
loss.setType("recovery_loss");
loss.setStatus("open");
loss.setMetadata(Map.of("mill", "mill_1", "recovery_pct", "88.4", "target_pct", "91.0"));
loss.setEventTime(ZonedDateTime.now());
client.events().create(List.of(loss));
client.events.create([datahub_sdk.Event(
external_id=f"recovery_loss_mill1_{int(pd.Timestamp.now().timestamp())}",
type="recovery_loss", status="open",
event_time=pd.Timestamp.now(tz="UTC"),
metadata={"mill": "mill_1", "recovery_pct": "88.4", "target_pct": "91.0"})])
use dataplatform_rust_sdk::events::Event;
use chrono::Utc;
let mut loss = Event::new(format!("recovery_loss_mill1_{}", Utc::now().timestamp()));
loss.r#type = Some("recovery_loss".into());
loss.status = Some("open".into());
loss.add_metadata("mill".into(), "mill_1".into());
loss.add_metadata("recovery_pct".into(), "88.4".into());
loss.add_metadata("target_pct".into(), "91.0".into());
loss.set_event_time(Utc::now());
api.events.create(&vec![loss]).await?;
3. Find the best operating point
Recovery, grind and reagents interact in complex ways — cluster the operating states to discover which regimes recover best, or build a soft sensor to estimate recovery between assays.
See the result
Chart recovery against target — the dip is metal walking out with the tailings:
import matplotlib.pyplot as plt
rf = datahub_sdk.RetrieveFilter(ts="mill_1_recovery_pct",
start=pd.Timestamp.now(tz="UTC") - pd.Timedelta(hours=12), end=pd.Timestamp.now(tz="UTC"),
aggregates=["avg"], granularity="1h")
v = [dp.average for dp in client.timeseries.retrieve_datapoints(rf)[0].get_datapoints()]
plt.plot(v); plt.axhline(91, color="r", ls="--"); plt.title("Mill 1 recovery (%)"); plt.show()
See also
- Query & aggregate — recovery and throughput KPIs.
- Turn readings into events — the recovery-dip rule.
- Mining operations — the haul fleet that feeds the mill.