| import numpy as np |
| import matplotlib.pyplot as plt |
| import gradio as gr |
| import pandas as pd |
| from datetime import datetime |
| from fpdf import FPDF |
| import tempfile |
| import os |
| from scipy.optimize import minimize |
|
|
| |
| |
| |
| minerals = ['Gold', 'Ilmenite', 'Rutile', 'Monazite'] |
| densities = {'Gold': 19.3, 'Ilmenite': 4.7, 'Rutile': 4.2, 'Monazite': 5.2} |
| max_density = 19.3 |
|
|
| default_feed = { |
| 'Gold': 3.74, 'Ilmenite': 5.625, 'Rutile': 1.35, 'Monazite': 0.6 |
| } |
|
|
| |
| |
| |
| def shaking_table_model(params, feed_grades, feed_rate_tph): |
| stroke_len = params['stroke_len'] |
| freq = params['freq'] |
| tilt = params['tilt'] |
| wash_water = params['wash_water'] |
|
|
| 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) |
| yield_midd = yield_conc * 1.2 |
| yield_tail = 100 - yield_conc - yield_midd |
| if yield_tail < 0: |
| yield_tail = 10 |
| yield_midd = 90 - yield_conc |
|
|
| model_rec = {} |
| for minrl in minerals: |
| rho_norm = densities[minrl] / max_density |
| 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_midd = 100 - rec_conc - (100 - rec_conc) * 0.4 |
| rec_tail = 100 - rec_conc - rec_midd |
| if rec_midd < 0: |
| rec_midd = 0 |
| rec_tail = 100 - rec_conc |
|
|
| model_rec[minrl] = {'Conc': rec_conc, 'Midd': rec_midd, 'Tail': rec_tail} |
|
|
| 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_rate = feed_rate_tph * feed_grades[m] |
| rate_conc = mineral_feed_rate * 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 |
|
|
| |
| |
| |
| def find_parameters_for_targets(target_rates_conc, feed_grades, feed_rate_tph): |
| 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 = sum((rates[m] - target_rates_conc[m]) ** 2 for m in minerals) |
| return err |
|
|
| 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 and res.fun < 1e-2: |
| return {'stroke_len': res.x[0], 'freq': res.x[1], 'tilt': res.x[2], 'wash_water': res.x[3]}, res.fun |
| return None, res.fun |
|
|
| |
| |
| |
| def import_csv(file): |
| if file is None: return [gr.update()]*9 |
| try: |
| df = pd.read_csv(file.name) |
| d = dict(zip(df.iloc[:,0], df.iloc[:,1])) |
| return ( |
| d.get('feed_rate', 300), d.get('gold_feed', 3.74), d.get('ilmenite_feed', 5.625), |
| d.get('rutile_feed', 1.35), d.get('monazite_feed', 0.6), |
| d.get('target_gold', 0.001), d.get('target_ilmenite', 15.0), |
| d.get('target_rutile', 3.5), d.get('target_monazite', 1.5) |
| ) |
| except: return [gr.update()]*9 |
|
|
| |
| |
| |
| def export_pdf_report(report_md, fig1, fig2): |
| pdf = FPDF() |
| pdf.add_page() |
| pdf.set_font("Arial", 'B', 16) |
| pdf.cell(200, 10, "Attock Placer Plant - Digital Twin Report", ln=True, align='C') |
| pdf.set_font("Arial", size=10) |
| pdf.ln(5) |
| |
| clean_text = report_md.replace("#", "").replace("**", "").replace("|", " ") |
| for line in clean_text.split('\n'): |
| pdf.multi_cell(0, 8, line) |
|
|
| with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as t1, \ |
| tempfile.NamedTemporaryFile(delete=False, suffix=".png") as t2: |
| fig1.savefig(t1.name, bbox_inches='tight') |
| fig2.savefig(t2.name, bbox_inches='tight') |
| pdf.add_page() |
| pdf.image(t1.name, x=10, y=20, w=180) |
| pdf.image(t2.name, x=50, y=130, w=110) |
| |
| path = "Shaking_Table_Report.pdf" |
| pdf.output(path) |
| return path |
|
|
| |
| |
| |
| def simulate_wrapper(*args): |
| |
| from datetime import datetime |
| |
| |
| (feed_rate, g_f, i_f, r_f, m_f, s_l, fr, tl, ww, mode, tg, ti, tr, tm) = args |
| |
| |
| feed_grades = {'Gold': g_f*1e-6, 'Ilmenite': i_f/100, 'Rutile': r_f/100, 'Monazite': m_f/100} |
| |
| suggested_params = None |
| if mode == "Forward Model (Adjust Parameters)": |
| params = {'stroke_len': s_l, 'freq': fr, 'tilt': tl, 'wash_water': ww} |
| model_rec, yc, ym, yt, grades_conc, rates_conc = shaking_table_model(params, feed_grades, feed_rate) |
| text_mode = "Forward Mode" |
| else: |
| targets = {'Gold': tg, 'Ilmenite': ti, 'Rutile': tr, 'Monazite': tm} |
| suggested_params, err = find_parameters_for_targets(targets, feed_grades, feed_rate) |
| if suggested_params: |
| model_rec, yc, ym, yt, grades_conc, rates_conc = shaking_table_model(suggested_params, feed_grades, feed_rate) |
| text_mode = f"Inverse Mode (Error: {err:.4f})" |
| else: return "Optimization failed.", None, None, None |
|
|
| |
| text = f"### {text_mode} Results\nMass Yields: Conc {yc:.1f}%, Midd {ym:.1f}%, Tail {yt:.1f}%\n" |
| |
| |
| fig1, ax1 = plt.subplots(figsize=(10,5)) |
| x = np.arange(len(minerals)) |
| ax1.bar(x - 0.2, [model_rec[m]['Conc'] for m in minerals], 0.2, label='Conc') |
| ax1.bar(x, [model_rec[m]['Midd'] for m in minerals], 0.2, label='Midd') |
| ax1.bar(x + 0.2, [model_rec[m]['Tail'] for m in minerals], 0.2, label='Tail') |
| ax1.set_xticks(x), ax1.set_xticklabels(minerals), ax1.legend(), ax1.set_title("Distribution") |
|
|
| fig2, ax2 = plt.subplots() |
| ax2.pie([yc, ym, yt], labels=['Conc', 'Midd', 'Tail'], autopct='%1.1f%%') |
| |
| pdf_path = export_pdf_report(text, fig1, fig2) |
| return text, fig1, fig2, pdf_path |
|
|
| |
| |
| |
| with gr.Blocks(title="Shaking Table Digital Twin") as demo: |
| gr.Markdown("# Shaking Table Digital Twin & Optimizer") |
| |
| with gr.Row(): |
| with gr.Column(scale=1): |
| csv_in = gr.File(label="Import Feed/Target CSV", file_types=[".csv"]) |
| mode = gr.Radio(["Forward Model (Adjust Parameters)", "Inverse: Find Parameters for Target Rates"], value="Forward Model (Adjust Parameters)", label="Mode") |
| |
| with gr.Group(): |
| gr.Markdown("### Feed Composition") |
| f_rate = gr.Slider(50, 500, 300, label="Feed Rate (tph)") |
| g_f = gr.Number(value=3.74, label="Gold (g/t)") |
| i_f = gr.Number(value=5.625, label="Ilmenite (%)") |
| r_f = gr.Number(value=1.35, label="Rutile (%)") |
| m_f = gr.Number(value=0.6, label="Monazite (%)") |
|
|
| with gr.Group(): |
| gr.Markdown("### Mechanical Parameters") |
| s_l = gr.Slider(8, 25, 18, label="Stroke Length") |
| freq = gr.Slider(250, 350, 300, label="Frequency") |
| tilt = gr.Slider(2, 8, 5, label="Tilt") |
| wash = gr.Slider(5, 20, 12, label="Wash Water") |
|
|
| with gr.Group(): |
| gr.Markdown("### Inverse Targets") |
| tg = gr.Number(0.001, label="Target Gold (tph)") |
| ti = gr.Number(15.0, label="Target Ilmenite (tph)") |
| tr = gr.Number(3.5, label="Target Rutile (tph)") |
| tm = gr.Number(1.5, label="Target Monazite (tph)") |
|
|
| with gr.Column(scale=2): |
| run_btn = gr.Button("Run Simulation", variant="primary") |
| out_txt = gr.Markdown() |
| plot1 = gr.Plot() |
| plot2 = gr.Plot() |
| pdf_file = gr.File(label="Download PDF Report") |
|
|
| csv_in.change(import_csv, csv_in, [f_rate, g_f, i_f, r_f, m_f, tg, ti, tr, tm]) |
| run_btn.click(simulate_wrapper, [f_rate, g_f, i_f, r_f, m_f, s_l, freq, tilt, wash, mode, tg, ti, tr, tm], [out_txt, plot1, plot2, pdf_file]) |
|
|
| demo.launch() |