Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,75 +1,233 @@
|
|
| 1 |
-
import matplotlib
|
| 2 |
-
matplotlib.use("Agg") # IMPORTANT: prevents runtime exit on HF
|
| 3 |
-
|
| 4 |
-
import streamlit as st
|
| 5 |
import numpy as np
|
| 6 |
-
import pandas as pd
|
| 7 |
import matplotlib.pyplot as plt
|
| 8 |
-
|
| 9 |
|
| 10 |
-
#
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
#
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
"Monazite": [0.70, 0.20, 0.10]
|
| 24 |
-
}
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
for m in minerals:
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
#
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
|
| 50 |
-
params = solution.x
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
)
|
|
|
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import numpy as np
|
|
|
|
| 2 |
import matplotlib.pyplot as plt
|
| 3 |
+
import gradio as gr
|
| 4 |
|
| 5 |
+
# =============================================
|
| 6 |
+
# Constants & Mineral Densities (for physical influence)
|
| 7 |
+
# =============================================
|
| 8 |
+
minerals = ['Gold', 'Ilmenite', 'Rutile', 'Monazite']
|
| 9 |
+
densities = {'Gold': 19.3, 'Ilmenite': 4.7, 'Rutile': 4.2, 'Monazite': 5.2}
|
| 10 |
+
max_density = 19.3 # Gold for normalization
|
| 11 |
|
| 12 |
+
# Fixed upstream feed to shaking table (as before)
|
| 13 |
+
feed_rate_default = 300.0 # tph - but we'll scale with user input
|
| 14 |
+
feed_to_shaking = {
|
| 15 |
+
'Gold': 3.744e-06, # enriched g/t fraction
|
| 16 |
+
'Ilmenite': 0.05625,
|
| 17 |
+
'Rutile': 0.0135,
|
| 18 |
+
'Monazite': 0.006,
|
| 19 |
+
'Gangue': 0.924 # approximate balance
|
| 20 |
+
}
|
| 21 |
|
| 22 |
+
# =============================================
|
| 23 |
+
# Physical-inspired Digital Twin Model
|
| 24 |
+
# =============================================
|
| 25 |
+
def shaking_table_model(params, feed_grades, feed_rate_tph):
|
| 26 |
+
stroke_len = params['stroke_len'] # mm, 8-25
|
| 27 |
+
freq = params['freq'] # strokes/min, 250-350
|
| 28 |
+
tilt = params['tilt'] # degrees, 2-8
|
| 29 |
+
wash_water = params['wash_water'] # arbitrary units 5-20
|
| 30 |
+
# Other params ignored for simplicity but can be extended
|
| 31 |
|
| 32 |
+
# Base mass yields influenced by parameters (empirical)
|
| 33 |
+
# Higher tilt & wash water → lower conc yield (sharper separation)
|
| 34 |
+
base_yield_conc = 50 - 3*(tilt - 5) - 0.5*(wash_water - 12)
|
| 35 |
+
yield_conc = np.clip(base_yield_conc + 0.05*(300 - freq), 10, 45)
|
|
|
|
|
|
|
| 36 |
|
| 37 |
+
# Middlings ~ proportional
|
| 38 |
+
yield_midd = yield_conc * 1.2
|
| 39 |
+
yield_tail = 100 - yield_conc - yield_midd
|
| 40 |
+
if yield_tail < 0:
|
| 41 |
+
yield_tail = 10
|
| 42 |
+
yield_midd = 100 - yield_conc - yield_tail
|
| 43 |
+
|
| 44 |
+
model_rec = {}
|
| 45 |
+
for minrl in minerals:
|
| 46 |
+
rho_norm = densities[minrl] / max_density
|
| 47 |
+
# Higher density → higher recovery to conc
|
| 48 |
+
# Optimal around stroke 18mm, freq 300, tilt 5°, wash 12
|
| 49 |
+
stroke_effect = 100 * (1 - 0.5 * ((stroke_len - 18)/10)**2)
|
| 50 |
+
freq_effect = 100 * (freq / 350)
|
| 51 |
+
tilt_effect = 100 * (1 - 0.8 * ((tilt - 5)/3)**2)
|
| 52 |
+
wash_effect = 100 * (1 - 0.6 * ((wash_water - 12)/8)**2)
|
| 53 |
+
|
| 54 |
+
rec_conc = 50 + rho_norm * (stroke_effect + freq_effect + tilt_effect + wash_effect - 200)
|
| 55 |
+
rec_conc = np.clip(rec_conc, 30, 95)
|
| 56 |
+
|
| 57 |
+
rec_tail = 100 - rec_conc - 20 # midd approx
|
| 58 |
+
rec_midd = 100 - rec_conc - rec_tail
|
| 59 |
+
if rec_midd < 0:
|
| 60 |
+
rec_midd = 0
|
| 61 |
+
rec_tail = 100 - rec_conc
|
| 62 |
+
|
| 63 |
+
model_rec[minrl] = {'Conc': rec_conc, 'Midd': rec_midd, 'Tail': rec_tail}
|
| 64 |
+
|
| 65 |
+
# Compute grades & rates
|
| 66 |
+
y_conc_frac = yield_conc / 100.0
|
| 67 |
+
mass_rate_conc = feed_rate_tph * y_conc_frac
|
| 68 |
+
|
| 69 |
+
grades_conc = {}
|
| 70 |
+
rates_conc = {}
|
| 71 |
for m in minerals:
|
| 72 |
+
rec = model_rec[m]['Conc'] / 100.0
|
| 73 |
+
mineral_feed = feed_rate_tph * feed_grades[m]
|
| 74 |
+
rate_conc = mineral_feed * rec
|
| 75 |
+
rates_conc[m] = rate_conc
|
| 76 |
+
if m == 'Gold':
|
| 77 |
+
grades_conc[m] = (rate_conc / mass_rate_conc) * 1e6 if mass_rate_conc > 0 else 0
|
| 78 |
+
else:
|
| 79 |
+
grades_conc[m] = (rate_conc / mass_rate_conc) * 100 if mass_rate_conc > 0 else 0
|
| 80 |
+
|
| 81 |
+
return model_rec, yield_conc, yield_midd, yield_tail, grades_conc, rates_conc
|
| 82 |
+
|
| 83 |
+
# =============================================
|
| 84 |
+
# Target Rates Mode (inverse: adjust params to hit user target rates)
|
| 85 |
+
# =============================================
|
| 86 |
+
def find_parameters_for_targets(target_rates_conc, feed_grades, feed_rate_tph):
|
| 87 |
+
# target_rates_conc dict mineral: tph to conc
|
| 88 |
+
|
| 89 |
+
def error(p):
|
| 90 |
+
params = {'stroke_len': p[0], 'freq': p[1], 'tilt': p[2], 'wash_water': p[3]}
|
| 91 |
+
_, _, _, _, _, rates = shaking_table_model(params, feed_grades, feed_rate_tph)
|
| 92 |
+
err = 0
|
| 93 |
+
for m in minerals:
|
| 94 |
+
err += (rates[m] - target_rates_conc[m]) ** 2
|
| 95 |
+
return err
|
| 96 |
+
|
| 97 |
+
from scipy.optimize import minimize
|
| 98 |
+
initial = [18, 300, 5, 12]
|
| 99 |
+
bounds = [(8,25), (250,350), (2,8), (5,20)]
|
| 100 |
+
res = minimize(error, initial, bounds=bounds, method='L-BFGS-B')
|
| 101 |
+
if res.success:
|
| 102 |
+
best_params = {'stroke_len': res.x[0], 'freq': res.x[1], 'tilt': res.x[2], 'wash_water': res.x[3]}
|
| 103 |
+
return best_params, res.fun
|
| 104 |
+
else:
|
| 105 |
+
return None, 1e6
|
| 106 |
+
|
| 107 |
+
# =============================================
|
| 108 |
+
# Main Simulation Function
|
| 109 |
+
# =============================================
|
| 110 |
+
def simulate(
|
| 111 |
+
feed_rate_tph,
|
| 112 |
+
gold_feed_gpt, ilmenite_feed_wt, rutile_feed_wt, monazite_feed_wt,
|
| 113 |
+
stroke_len, freq, tilt, wash_water,
|
| 114 |
+
mode, # "forward" or "target"
|
| 115 |
+
target_gold_tph, target_ilmenite_tph, target_rutile_tph, target_monazite_tph
|
| 116 |
+
):
|
| 117 |
+
# User-defined feed grades
|
| 118 |
+
feed_grades = {
|
| 119 |
+
'Gold': gold_feed_gpt * 1e-6,
|
| 120 |
+
'Ilmenite': ilmenite_feed_wt / 100,
|
| 121 |
+
'Rutile': rutile_feed_wt / 100,
|
| 122 |
+
'Monazite': monazite_feed_wt / 100,
|
| 123 |
+
'Gangue': 1 - (ilmenite_feed_wt + rutile_feed_wt + monazite_feed_wt)/100
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
if mode == "Forward Model (Adjust Parameters)":
|
| 127 |
+
params = {'stroke_len': stroke_len, 'freq': freq, 'tilt': tilt, 'wash_water': wash_water}
|
| 128 |
+
model_rec, yc, ym, yt, grades, rates = shaking_table_model(params, feed_grades, feed_rate_tph)
|
| 129 |
+
|
| 130 |
+
text = f"### Forward Model Results\n\n"
|
| 131 |
+
text += f"**Mass Yields:** Conc {yc:.1f}%, Midd {ym:.1f}%, Tail {yt:.1f}%\n\n"
|
| 132 |
+
text += "| Mineral | Rec Conc (%) | Grade Conc | Rate to Conc (tph) |\n"
|
| 133 |
+
text += "|-----------|--------------|----------------|--------------------|\n"
|
| 134 |
+
for m in minerals:
|
| 135 |
+
rec = model_rec[m]['Conc']
|
| 136 |
+
grade = grades[m]
|
| 137 |
+
unit = "g/t" if m=="Gold" else "wt%"
|
| 138 |
+
rate = rates[m]
|
| 139 |
+
text += f"| {m:<9} | {rec:11.1f} | {grade:13.2f} {unit} | {rate:17.3f} |\n"
|
| 140 |
+
|
| 141 |
+
else: # Target mode
|
| 142 |
+
target_rates = {
|
| 143 |
+
'Gold': target_gold_tph,
|
| 144 |
+
'Ilmenite': target_ilmenite_tph,
|
| 145 |
+
'Rutile': target_rutile_tph,
|
| 146 |
+
'Monazite': target_monazite_tph
|
| 147 |
+
}
|
| 148 |
+
best_params, err = find_parameters_for_targets(target_rates, feed_grades, feed_rate_tph)
|
| 149 |
+
if best_params:
|
| 150 |
+
model_rec, yc, ym, yt, grades, rates = shaking_table_model(best_params, feed_grades, feed_rate_tph)
|
| 151 |
+
text = f"### Suggested Parameters to Achieve Target Rates (Error: {err:.1f})\n\n"
|
| 152 |
+
text += f"**Suggested:** Stroke {best_params['stroke_len']:.1f} mm, Freq {best_params['freq']:.0f}/min, Tilt {best_params['tilt']:.1f}°, Wash {best_params['wash_water']:.1f}\n\n"
|
| 153 |
+
text += "| Mineral | Target Rate (tph) | Achieved Rate | Grade Conc |\n"
|
| 154 |
+
text += "|-----------|-------------------|---------------|----------------|\n"
|
| 155 |
+
for m in minerals:
|
| 156 |
+
grade = grades[m]
|
| 157 |
+
unit = "g/t" if m=="Gold" else "wt%"
|
| 158 |
+
text += f"| {m:<9} | {target_rates[m]:17.3f} | {rates[m]:12.3f} | {grade:13.2f} {unit} |\n"
|
| 159 |
+
else:
|
| 160 |
+
text = "Could not find parameters to match targets."
|
| 161 |
+
|
| 162 |
+
# Plots
|
| 163 |
+
fig1 = plt.figure(figsize=(10,6))
|
| 164 |
+
x = np.arange(len(minerals))
|
| 165 |
+
width = 0.25
|
| 166 |
+
plt.bar(x, [model_rec[m]['Conc'] for m in minerals], width, label='Conc')
|
| 167 |
+
plt.bar(x + width, [model_rec[m]['Midd'] for m in minerals], width, label='Midd')
|
| 168 |
+
plt.bar(x + 2*width, [model_rec[m]['Tail'] for m in minerals], width, label='Tail')
|
| 169 |
+
plt.xlabel('Mineral')
|
| 170 |
+
plt.ylabel('Distribution (%)')
|
| 171 |
+
plt.title('Mineral Distribution Across Products')
|
| 172 |
+
plt.xticks(x + width, minerals)
|
| 173 |
+
plt.legend()
|
| 174 |
+
plt.grid(True, axis='y')
|
| 175 |
+
|
| 176 |
+
fig2 = plt.figure(figsize=(8,8))
|
| 177 |
+
labels = ['Conc', 'Midd', 'Tail']
|
| 178 |
+
sizes = [yc, ym, yt]
|
| 179 |
+
plt.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90)
|
| 180 |
+
plt.title('Mass Yield Pie Chart')
|
| 181 |
+
|
| 182 |
+
# Simple estimated "cost" (arbitrary: lower yield higher cost due to more processing)
|
| 183 |
+
est_cost = 1000 + 500 * (yc / 30) # dummy
|
| 184 |
+
text += f"\n\n**Estimated Relative Operating Cost:** ${est_cost:.0f} (arbitrary units)"
|
| 185 |
+
|
| 186 |
+
return text, fig1, fig2
|
| 187 |
|
| 188 |
+
# =============================================
|
| 189 |
+
# Gradio Interface with Sidebar Sections
|
| 190 |
+
# =============================================
|
| 191 |
+
with gr.Blocks(title="Advanced Shaking Table Digital Twin") as demo:
|
| 192 |
+
gr.Markdown("# Advanced Shaking Table Digital Twin - Attock Placer Plant")
|
| 193 |
+
gr.Markdown("Physical-inspired model with manual inputs for feed, operating parameters, and target rates.")
|
| 194 |
|
| 195 |
+
mode = gr.Radio(["Forward Model (Adjust Parameters)", "Inverse: Find Parameters for Target Rates"], value="Forward Model (Adjust Parameters)", label="Mode")
|
|
|
|
| 196 |
|
| 197 |
+
with gr.Row():
|
| 198 |
+
with gr.Column(scale=1): # Sidebar-like
|
| 199 |
+
gr.Markdown("### 1. Feed Composition & Rate")
|
| 200 |
+
feed_rate_tph = gr.Slider(50, 500, value=300, step=10, label="Feed Rate (tph)")
|
| 201 |
+
gold_feed_gpt = gr.Number(value=3.74, label="Gold Feed Grade (g/t)")
|
| 202 |
+
ilmenite_feed_wt = gr.Number(value=5.625, label="Ilmenite Feed Grade (wt%)")
|
| 203 |
+
rutile_feed_wt = gr.Number(value=1.35, label="Rutile Feed Grade (wt%)")
|
| 204 |
+
monazite_feed_wt = gr.Number(value=0.6, label="Monazite Feed Grade (wt%)")
|
| 205 |
|
| 206 |
+
gr.Markdown("### 2. Operating Parameters")
|
| 207 |
+
stroke_len = gr.Slider(8, 25, value=18, step=0.5, label="Stroke Length (mm)")
|
| 208 |
+
freq = gr.Slider(250, 350, value=300, step=5, label="Stroke Frequency (/min)")
|
| 209 |
+
tilt = gr.Slider(2, 8, value=5, step=0.1, label="Deck Inclination (degrees)")
|
| 210 |
+
wash_water = gr.Slider(5, 20, value=12, step=0.5, label="Wash Water Flow (units)")
|
| 211 |
|
| 212 |
+
gr.Markdown("### 3. Target Rates to Concentrate (tph) - for Inverse Mode")
|
| 213 |
+
target_gold_tph = gr.Number(value=0.8, label="Target Gold Rate to Conc (tph)")
|
| 214 |
+
target_ilmenite_tph = gr.Number(value=15, label="Target Ilmenite Rate (tph)")
|
| 215 |
+
target_rutile_tph = gr.Number(value=3.5, label="Target Rutile Rate (tph)")
|
| 216 |
+
target_monazite_tph = gr.Number(value=1.5, label="Target Monazite Rate (tph)")
|
| 217 |
|
| 218 |
+
with gr.Column(scale=3):
|
| 219 |
+
btn = gr.Button("Run Simulation", variant="primary")
|
| 220 |
+
output_text = gr.Markdown()
|
| 221 |
+
with gr.Row():
|
| 222 |
+
plot1 = gr.Plot(label="Mineral Distribution Bar Chart")
|
| 223 |
+
plot2 = gr.Plot(label="Mass Yield Pie Chart")
|
| 224 |
|
| 225 |
+
btn.click(
|
| 226 |
+
fn=simulate,
|
| 227 |
+
inputs=[feed_rate_tph, gold_feed_gpt, ilmenite_feed_wt, rutile_feed_wt, monazite_feed_wt,
|
| 228 |
+
stroke_len, freq, tilt, wash_water, mode,
|
| 229 |
+
target_gold_tph, target_ilmenite_tph, target_rutile_tph, target_monazite_tph],
|
| 230 |
+
outputs=[output_text, plot1, plot2]
|
| 231 |
+
)
|
| 232 |
|
| 233 |
+
demo.launch()
|