gausubash's picture
Deploy mineral processing forecast dashboard with ZeroGPU
65f8f70 verified
Raw
History Blame Contribute Delete
5.08 kB
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
import numpy as np
import pandas as pd
TARGETS = ["mill_power_mw", "feed_rate_tph", "target_impurity_pct"]
TARGET_LABELS = {
"mill_power_mw": "Mill Power (MW)",
"feed_rate_tph": "Feed Rate (tph)",
"target_impurity_pct": "Impurity (%)",
}
CONTEXT_WINDOWS = {
"48 hours": 48,
"96 hours (4 days)": 96,
"168 hours (1 week)": 168,
}
HORIZONS = {
"12 hours": 12,
"24 hours": 24,
"48 hours": 48,
}
RANDOM_SEED = 42
ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets"
TELEMETRY_FILE = "telemetry_synthetic.parquet"
def generate_mineral_processing_telemetry(n_timestamps: int = 1000) -> pd.DataFrame:
"""Synthetic comminution/flotation plant telemetry."""
rng = np.random.default_rng(RANDOM_SEED)
timestamps = pd.date_range("2024-01-01", periods=n_timestamps, freq="h", tz="UTC")
hour = np.arange(n_timestamps) % 24
day = np.arange(n_timestamps) // 24
shift_cycle = 0.6 * np.sin(2 * np.pi * hour / 24) + 0.25 * np.sin(2 * np.pi * hour / 12)
hardness_drift = 0.15 * np.sin(2 * np.pi * day / 14)
feed_rate_tph = 285 + 55 * shift_cycle + 20 * hardness_drift + rng.normal(0, 8, n_timestamps)
feed_rate_tph = np.clip(feed_rate_tph, 180, 420)
mill_power_mw = (
7.5
+ 0.018 * feed_rate_tph
+ 2.2 * hardness_drift
+ 0.35 * shift_cycle
+ rng.normal(0, 0.25, n_timestamps)
)
mill_power_mw = np.clip(mill_power_mw, 5.0, 16.0)
impurity_driver = (
2.4
- 0.004 * feed_rate_tph
+ 0.35 * hardness_drift
+ rng.normal(0, 0.08, n_timestamps)
)
target_impurity_pct = np.zeros(n_timestamps)
target_impurity_pct[0] = impurity_driver[0]
for t in range(1, n_timestamps):
target_impurity_pct[t] = 0.82 * target_impurity_pct[t - 1] + 0.18 * impurity_driver[t]
target_impurity_pct = np.clip(target_impurity_pct, 0.4, 4.5)
return pd.DataFrame(
{
"timestamp": timestamps,
"mill_power_mw": mill_power_mw.round(3),
"feed_rate_tph": feed_rate_tph.round(2),
"target_impurity_pct": target_impurity_pct.round(4),
}
)
def ensure_telemetry_asset() -> Path:
"""Persist synthetic telemetry to assets/ for fast Space startup."""
ASSETS_DIR.mkdir(parents=True, exist_ok=True)
path = ASSETS_DIR / TELEMETRY_FILE
if not path.exists():
generate_mineral_processing_telemetry().to_parquet(path, index=False)
return path
@lru_cache(maxsize=1)
def load_telemetry() -> pd.DataFrame:
path = ensure_telemetry_asset()
frame = pd.read_parquet(path)
frame["timestamp"] = pd.to_datetime(frame["timestamp"], utc=True)
return frame.sort_values("timestamp").reset_index(drop=True)
def telemetry_summary(telemetry: pd.DataFrame) -> pd.DataFrame:
rows = []
for col in TARGETS:
rows.append(
{
"Signal": TARGET_LABELS[col],
"Rows": len(telemetry),
"Start (UTC)": telemetry["timestamp"].min(),
"End (UTC)": telemetry["timestamp"].max(),
"Min": round(telemetry[col].min(), 4),
"Max": round(telemetry[col].max(), 4),
"Mean": round(telemetry[col].mean(), 4),
"Std": round(telemetry[col].std(), 4),
}
)
return pd.DataFrame(rows)
def correlation_matrix(telemetry: pd.DataFrame) -> pd.DataFrame:
return telemetry[TARGETS].corr().round(3)
def build_context_frame(
telemetry: pd.DataFrame,
target: str,
anchor_time: pd.Timestamp,
context_steps: int,
) -> pd.DataFrame:
history = telemetry.loc[telemetry["timestamp"] <= anchor_time].tail(context_steps)
if len(history) < max(24, context_steps // 4):
raise ValueError("Not enough history before the selected anchor time.")
return pd.DataFrame(
{
"item_id": target,
"timestamp": history["timestamp"],
"target": history[target].astype(float),
}
)
def holdout_actuals(
telemetry: pd.DataFrame,
target: str,
anchor_time: pd.Timestamp,
horizon: int,
) -> pd.DataFrame:
future = telemetry.loc[
(telemetry["timestamp"] > anchor_time)
& (telemetry["timestamp"] <= anchor_time + pd.Timedelta(hours=horizon))
]
if future.empty:
return pd.DataFrame(columns=["timestamp", "actual"])
return future[["timestamp", target]].rename(columns={target: "actual"})
def compute_metrics(actual: pd.Series, predicted: pd.Series) -> dict[str, float]:
diff = actual.values - predicted.values
mae = float(np.mean(np.abs(diff)))
rmse = float(np.sqrt(np.mean(diff**2)))
mape = float(np.mean(np.abs(diff / np.clip(actual.values, 1e-6, None))) * 100)
return {"mae": mae, "rmse": rmse, "mape_pct": mape}