Emanrashid7 commited on
Commit
463a5ad
·
verified ·
1 Parent(s): b5ef06a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +217 -59
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
- from scipy.optimize import minimize
9
 
10
- # ---------------- PAGE CONFIG ----------------
11
- st.set_page_config(page_title="Shaking Table Digital Twin", layout="centered")
 
 
 
 
12
 
13
- st.title("Digital Twin Shaking Table")
14
- st.write("Attock Placer Plant | CEP Project")
 
 
 
 
 
 
 
15
 
16
- # ---------------- DATA ----------------
17
- minerals = ["Gold", "Ilmenite", "Rutile", "Monazite"]
 
 
 
 
 
 
 
18
 
19
- plant_data = {
20
- "Gold": [0.75, 0.15, 0.10],
21
- "Ilmenite": [0.60, 0.25, 0.15],
22
- "Rutile": [0.65, 0.20, 0.15],
23
- "Monazite": [0.70, 0.20, 0.10]
24
- }
25
 
26
- # ---------------- MODEL ----------------
27
- def shaking_table(params):
28
- results = {}
29
- i = 0
30
- for m in minerals:
31
- Rc = params[i]
32
- Rm = params[i + 1]
33
- Rt = max(0.0, 1.0 - Rc - Rm)
34
- results[m] = [Rc, Rm, Rt]
35
- i += 2
36
- return results
37
-
38
- def error_fn(params):
39
- res = shaking_table(params)
40
- err = 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  for m in minerals:
42
- err += np.sum((np.array(res[m]) - np.array(plant_data[m])) ** 2)
43
- return err
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
- # ---------------- CALIBRATION ----------------
46
- initial = [0.7, 0.2, 0.55, 0.3, 0.6, 0.25, 0.65, 0.25]
47
- bounds = [(0, 1)] * len(initial)
 
 
 
48
 
49
- solution = minimize(error_fn, initial, bounds=bounds, method="SLSQP")
50
- params = solution.x
51
 
52
- # ---------------- RESULTS ----------------
53
- rows = []
54
- res = shaking_table(params)
55
- for m in minerals:
56
- rows.append([m] + res[m])
 
 
 
57
 
58
- df = pd.DataFrame(
59
- rows,
60
- columns=["Mineral", "Concentrate Recovery", "Middlings Recovery", "Tailings Recovery"]
61
- )
 
62
 
63
- st.subheader("Calibrated Recovery Results")
64
- st.dataframe(df)
 
 
 
65
 
66
- # ---------------- PLOT ----------------
67
- fig, ax = plt.subplots()
68
- df.set_index("Mineral").plot(kind="bar", ax=ax)
69
- ax.set_ylabel("Recovery Fraction")
70
- ax.set_title("Shaking Table Recovery Distribution")
71
- ax.grid(True)
72
 
73
- st.pyplot(fig)
 
 
 
 
 
 
74
 
75
- st.caption("Python Digital Twin | Attock Placer Plant | CEP")
 
 
 
 
 
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()