Skip to main content

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.

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

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.

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));

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