Emanrashid7's picture
Update app.py
463a5ad verified
import numpy as np
import matplotlib.pyplot as plt
import gradio as gr
# =============================================
# Constants & Mineral Densities (for physical influence)
# =============================================
minerals = ['Gold', 'Ilmenite', 'Rutile', 'Monazite']
densities = {'Gold': 19.3, 'Ilmenite': 4.7, 'Rutile': 4.2, 'Monazite': 5.2}
max_density = 19.3 # Gold for normalization
# Fixed upstream feed to shaking table (as before)
feed_rate_default = 300.0 # tph - but we'll scale with user input
feed_to_shaking = {
'Gold': 3.744e-06, # enriched g/t fraction
'Ilmenite': 0.05625,
'Rutile': 0.0135,
'Monazite': 0.006,
'Gangue': 0.924 # approximate balance
}
# =============================================
# Physical-inspired Digital Twin Model
# =============================================
def shaking_table_model(params, feed_grades, feed_rate_tph):
stroke_len = params['stroke_len'] # mm, 8-25
freq = params['freq'] # strokes/min, 250-350
tilt = params['tilt'] # degrees, 2-8
wash_water = params['wash_water'] # arbitrary units 5-20
# Other params ignored for simplicity but can be extended
# Base mass yields influenced by parameters (empirical)
# Higher tilt & wash water → lower conc yield (sharper separation)
base_yield_conc = 50 - 3*(tilt - 5) - 0.5*(wash_water - 12)
yield_conc = np.clip(base_yield_conc + 0.05*(300 - freq), 10, 45)
# Middlings ~ proportional
yield_midd = yield_conc * 1.2
yield_tail = 100 - yield_conc - yield_midd
if yield_tail < 0:
yield_tail = 10
yield_midd = 100 - yield_conc - yield_tail
model_rec = {}
for minrl in minerals:
rho_norm = densities[minrl] / max_density
# Higher density → higher recovery to conc
# Optimal around stroke 18mm, freq 300, tilt 5°, wash 12
stroke_effect = 100 * (1 - 0.5 * ((stroke_len - 18)/10)**2)
freq_effect = 100 * (freq / 350)
tilt_effect = 100 * (1 - 0.8 * ((tilt - 5)/3)**2)
wash_effect = 100 * (1 - 0.6 * ((wash_water - 12)/8)**2)
rec_conc = 50 + rho_norm * (stroke_effect + freq_effect + tilt_effect + wash_effect - 200)
rec_conc = np.clip(rec_conc, 30, 95)
rec_tail = 100 - rec_conc - 20 # midd approx
rec_midd = 100 - rec_conc - rec_tail
if rec_midd < 0:
rec_midd = 0
rec_tail = 100 - rec_conc
model_rec[minrl] = {'Conc': rec_conc, 'Midd': rec_midd, 'Tail': rec_tail}
# Compute grades & rates
y_conc_frac = yield_conc / 100.0
mass_rate_conc = feed_rate_tph * y_conc_frac
grades_conc = {}
rates_conc = {}
for m in minerals:
rec = model_rec[m]['Conc'] / 100.0
mineral_feed = feed_rate_tph * feed_grades[m]
rate_conc = mineral_feed * rec
rates_conc[m] = rate_conc
if m == 'Gold':
grades_conc[m] = (rate_conc / mass_rate_conc) * 1e6 if mass_rate_conc > 0 else 0
else:
grades_conc[m] = (rate_conc / mass_rate_conc) * 100 if mass_rate_conc > 0 else 0
return model_rec, yield_conc, yield_midd, yield_tail, grades_conc, rates_conc
# =============================================
# Target Rates Mode (inverse: adjust params to hit user target rates)
# =============================================
def find_parameters_for_targets(target_rates_conc, feed_grades, feed_rate_tph):
# target_rates_conc dict mineral: tph to conc
def error(p):
params = {'stroke_len': p[0], 'freq': p[1], 'tilt': p[2], 'wash_water': p[3]}
_, _, _, _, _, rates = shaking_table_model(params, feed_grades, feed_rate_tph)
err = 0
for m in minerals:
err += (rates[m] - target_rates_conc[m]) ** 2
return err
from scipy.optimize import minimize
initial = [18, 300, 5, 12]
bounds = [(8,25), (250,350), (2,8), (5,20)]
res = minimize(error, initial, bounds=bounds, method='L-BFGS-B')
if res.success:
best_params = {'stroke_len': res.x[0], 'freq': res.x[1], 'tilt': res.x[2], 'wash_water': res.x[3]}
return best_params, res.fun
else:
return None, 1e6
# =============================================
# Main Simulation Function
# =============================================
def simulate(
feed_rate_tph,
gold_feed_gpt, ilmenite_feed_wt, rutile_feed_wt, monazite_feed_wt,
stroke_len, freq, tilt, wash_water,
mode, # "forward" or "target"
target_gold_tph, target_ilmenite_tph, target_rutile_tph, target_monazite_tph
):
# User-defined feed grades
feed_grades = {
'Gold': gold_feed_gpt * 1e-6,
'Ilmenite': ilmenite_feed_wt / 100,
'Rutile': rutile_feed_wt / 100,
'Monazite': monazite_feed_wt / 100,
'Gangue': 1 - (ilmenite_feed_wt + rutile_feed_wt + monazite_feed_wt)/100
}
if mode == "Forward Model (Adjust Parameters)":
params = {'stroke_len': stroke_len, 'freq': freq, 'tilt': tilt, 'wash_water': wash_water}
model_rec, yc, ym, yt, grades, rates = shaking_table_model(params, feed_grades, feed_rate_tph)
text = f"### Forward Model Results\n\n"
text += f"**Mass Yields:** Conc {yc:.1f}%, Midd {ym:.1f}%, Tail {yt:.1f}%\n\n"
text += "| Mineral | Rec Conc (%) | Grade Conc | Rate to Conc (tph) |\n"
text += "|-----------|--------------|----------------|--------------------|\n"
for m in minerals:
rec = model_rec[m]['Conc']
grade = grades[m]
unit = "g/t" if m=="Gold" else "wt%"
rate = rates[m]
text += f"| {m:<9} | {rec:11.1f} | {grade:13.2f} {unit} | {rate:17.3f} |\n"
else: # Target mode
target_rates = {
'Gold': target_gold_tph,
'Ilmenite': target_ilmenite_tph,
'Rutile': target_rutile_tph,
'Monazite': target_monazite_tph
}
best_params, err = find_parameters_for_targets(target_rates, feed_grades, feed_rate_tph)
if best_params:
model_rec, yc, ym, yt, grades, rates = shaking_table_model(best_params, feed_grades, feed_rate_tph)
text = f"### Suggested Parameters to Achieve Target Rates (Error: {err:.1f})\n\n"
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"
text += "| Mineral | Target Rate (tph) | Achieved Rate | Grade Conc |\n"
text += "|-----------|-------------------|---------------|----------------|\n"
for m in minerals:
grade = grades[m]
unit = "g/t" if m=="Gold" else "wt%"
text += f"| {m:<9} | {target_rates[m]:17.3f} | {rates[m]:12.3f} | {grade:13.2f} {unit} |\n"
else:
text = "Could not find parameters to match targets."
# Plots
fig1 = plt.figure(figsize=(10,6))
x = np.arange(len(minerals))
width = 0.25
plt.bar(x, [model_rec[m]['Conc'] for m in minerals], width, label='Conc')
plt.bar(x + width, [model_rec[m]['Midd'] for m in minerals], width, label='Midd')
plt.bar(x + 2*width, [model_rec[m]['Tail'] for m in minerals], width, label='Tail')
plt.xlabel('Mineral')
plt.ylabel('Distribution (%)')
plt.title('Mineral Distribution Across Products')
plt.xticks(x + width, minerals)
plt.legend()
plt.grid(True, axis='y')
fig2 = plt.figure(figsize=(8,8))
labels = ['Conc', 'Midd', 'Tail']
sizes = [yc, ym, yt]
plt.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90)
plt.title('Mass Yield Pie Chart')
# Simple estimated "cost" (arbitrary: lower yield higher cost due to more processing)
est_cost = 1000 + 500 * (yc / 30) # dummy
text += f"\n\n**Estimated Relative Operating Cost:** ${est_cost:.0f} (arbitrary units)"
return text, fig1, fig2
# =============================================
# Gradio Interface with Sidebar Sections
# =============================================
with gr.Blocks(title="Advanced Shaking Table Digital Twin") as demo:
gr.Markdown("# Advanced Shaking Table Digital Twin - Attock Placer Plant")
gr.Markdown("Physical-inspired model with manual inputs for feed, operating parameters, and target rates.")
mode = gr.Radio(["Forward Model (Adjust Parameters)", "Inverse: Find Parameters for Target Rates"], value="Forward Model (Adjust Parameters)", label="Mode")
with gr.Row():
with gr.Column(scale=1): # Sidebar-like
gr.Markdown("### 1. Feed Composition & Rate")
feed_rate_tph = gr.Slider(50, 500, value=300, step=10, label="Feed Rate (tph)")
gold_feed_gpt = gr.Number(value=3.74, label="Gold Feed Grade (g/t)")
ilmenite_feed_wt = gr.Number(value=5.625, label="Ilmenite Feed Grade (wt%)")
rutile_feed_wt = gr.Number(value=1.35, label="Rutile Feed Grade (wt%)")
monazite_feed_wt = gr.Number(value=0.6, label="Monazite Feed Grade (wt%)")
gr.Markdown("### 2. Operating Parameters")
stroke_len = gr.Slider(8, 25, value=18, step=0.5, label="Stroke Length (mm)")
freq = gr.Slider(250, 350, value=300, step=5, label="Stroke Frequency (/min)")
tilt = gr.Slider(2, 8, value=5, step=0.1, label="Deck Inclination (degrees)")
wash_water = gr.Slider(5, 20, value=12, step=0.5, label="Wash Water Flow (units)")
gr.Markdown("### 3. Target Rates to Concentrate (tph) - for Inverse Mode")
target_gold_tph = gr.Number(value=0.8, label="Target Gold Rate to Conc (tph)")
target_ilmenite_tph = gr.Number(value=15, label="Target Ilmenite Rate (tph)")
target_rutile_tph = gr.Number(value=3.5, label="Target Rutile Rate (tph)")
target_monazite_tph = gr.Number(value=1.5, label="Target Monazite Rate (tph)")
with gr.Column(scale=3):
btn = gr.Button("Run Simulation", variant="primary")
output_text = gr.Markdown()
with gr.Row():
plot1 = gr.Plot(label="Mineral Distribution Bar Chart")
plot2 = gr.Plot(label="Mass Yield Pie Chart")
btn.click(
fn=simulate,
inputs=[feed_rate_tph, gold_feed_gpt, ilmenite_feed_wt, rutile_feed_wt, monazite_feed_wt,
stroke_len, freq, tilt, wash_water, mode,
target_gold_tph, target_ilmenite_tph, target_rutile_tph, target_monazite_tph],
outputs=[output_text, plot1, plot2]
)
demo.launch()