Younasmomand's picture
Update app.py
63cee15 verified
import streamlit as st
import pandas as pd
import numpy as np
from scipy.optimize import minimize
# --- Page Setup ---
st.set_page_config(page_title="Cement Rawmix Optimizer", layout="wide")
st.title("Cement Rawmix Optimizer")
st.markdown("""
This app optimizes raw material fractions, computes clinker oxides & Bogue phases,
Liquid Phase (%), Coating Index, and provides a quality & operational summary.
""")
# --- Constants ---
OXIDES = ["SiO2","Al2O3","Fe2O3","CaO","MgO","SO3","K2O","Na2O"]
DEFAULT_MATERIALS = ["Limestone","Clay","Shale","Marl","Laterite","Bauxite","Other"]
# --- Example Oxide Table ---
default_data = {
"Limestone":[0.5,0.01,0.005,50.0,0.1,0.01,0.01,0.05],
"Clay":[60.0,30.0,5.0,2.5,0.5,0.2,0.1,0.2],
"Shale":[60.0,20.0,6.0,4.0,1.0,0.2,0.1,0.3],
"Marl":[30.0,10.0,3.0,25.0,1.0,0.1,0.05,0.2],
"Laterite":[25.0,35.0,10.0,2.0,5.0,0.1,0.05,0.1],
"Bauxite":[10.0,50.0,5.0,1.0,0.2,0.01,0.01,0.01],
"Other":[40.0,20.0,5.0,10.0,1.0,0.1,0.05,0.1]
}
# --- Session state for persistence ---
if "oxide_df" not in st.session_state:
st.session_state.oxide_df = pd.DataFrame(default_data, index=OXIDES)
# --- Sidebar: Inputs ---
st.sidebar.header("Input Options")
uploaded_file = st.sidebar.file_uploader("Upload CSV (oxides x materials)", type=["csv"])
use_example = st.sidebar.checkbox("Use Example Data", value=True)
if uploaded_file:
try:
df = pd.read_csv(uploaded_file, index_col=0)
if set(OXIDES).issubset(df.index):
st.session_state.oxide_df = df.loc[OXIDES]
elif set(OXIDES).issubset(df.columns):
st.session_state.oxide_df = df[OXIDES].T
else:
st.sidebar.error("CSV must have oxide names as rows or columns: "+", ".join(OXIDES))
except Exception as e:
st.sidebar.error(f"Error reading CSV: {e}")
elif use_example:
st.session_state.oxide_df = pd.DataFrame(default_data,index=OXIDES)
oxide_df = st.session_state.oxide_df
st.subheader("Oxide Table")
oxide_df = st.data_editor(oxide_df, num_rows="fixed", use_container_width=True)
# --- Helper Functions ---
def bulk_from_frac(frac, oxide_matrix):
frac = np.array(frac)
frac = frac/frac.sum() if frac.sum()>0 else np.ones_like(frac)/len(frac)
bulk = np.dot(frac, oxide_matrix)
return dict(zip(OXIDES, bulk))
def compute_moduli(bulk):
Si = bulk['SiO2']; Al = bulk['Al2O3']; Fe = bulk['Fe2O3']; Ca = bulk['CaO']
denom = 2.8*Si + 1.2*Al + 0.65*Fe
LSF = (Ca/denom)*100 if denom>0 else 0
SM = Si/(Al+Fe) if (Al+Fe)>0 else 0
AM = Al/Fe if Fe>0 else 0
return LSF, SM, AM
def liquid_phase(Ca, Mg, Al, Fe, Si):
# Corrected: output as percentage
LP = (2.8*Ca + 1.18*Mg + 1.2*Al + 0.65*Fe) / (Ca + Mg + Si + Al + Fe) * 100
return LP
def coating_index(Al, Fe, Si):
CI = (Al + Fe) / Si if Si>0 else 0
return CI
def quality_review(LP, CI, C3S, C2S, C3A, C4AF):
review = []
if LP<25:
review.append("Low Liquid Phase: risk of coating & nodulization issues")
elif LP>35:
review.append("High Liquid Phase: may reduce strength and increase kiln wear")
else:
review.append("Liquid Phase in optimal range")
if CI>1.0:
review.append("Coating Index high: kiln coating tendency")
else:
review.append("Coating Index acceptable")
if C3S<45:
review.append("C3S low: may reduce early strength")
if C3A>10:
review.append("C3A high: may increase sulfate attack risk")
return review
# --- Automatic Targets ---
oxide_matrix = oxide_df.values.T
n_materials = oxide_matrix.shape[0]
equal_frac = np.ones(n_materials)/n_materials
bulk_auto = bulk_from_frac(equal_frac, oxide_matrix)
LSF_auto, SM_auto, AM_auto = compute_moduli(bulk_auto)
st.subheader("Automatic LSF/SM/AM Targets")
st.table(pd.DataFrame({"Target":["LSF","SM","AM"],
"Value":[round(LSF_auto,2),round(SM_auto,3),round(AM_auto,3)]}))
# --- Target Overrides ---
st.sidebar.header("Target Overrides")
LSF_target = st.sidebar.number_input("LSF target", value=float(round(LSF_auto,2)))
SM_target = st.sidebar.number_input("SM target", value=float(round(SM_auto,3)))
AM_target = st.sidebar.number_input("AM target", value=float(round(AM_auto,3)))
# --- Conversion Factors ---
st.sidebar.header("Clinker Conversion Factors")
conv_factors = {ox: st.sidebar.number_input(f"{ox} factor", value=1.0, format="%.4f") for ox in OXIDES}
# --- Material Fraction Bounds ---
st.sidebar.header("Material Bounds")
bounds = [(st.sidebar.number_input(f"Lower {m}",0.0,1.0,0.0),
st.sidebar.number_input(f"Upper {m}",0.0,1.0,1.0)) for m in oxide_df.columns]
# --- Optional Material Costs ---
st.sidebar.header("Optional Material Costs")
costs = [st.sidebar.number_input(f"Cost {m}", min_value=0.0, value=1.0) for m in oxide_df.columns]
# --- Optimization ---
st.subheader("Run Optimization")
penalty = st.number_input("Penalty factor", value=1.0)
max_iter = st.number_input("Max Iterations", value=500)
def objective(x):
x = np.clip(x,0,1)
x = x/x.sum() if x.sum()>0 else np.ones_like(x)/len(x)
bulk = bulk_from_frac(x, oxide_matrix)
LSF_c, SM_c, AM_c = compute_moduli(bulk)
err = ((LSF_c-LSF_target)/LSF_target)**2 + ((SM_c-SM_target)/SM_target)**2 + ((AM_c-AM_target)/AM_target)**2
return penalty*err + 0.001*np.dot(x,costs)/(np.mean(costs)+1e-9)
if st.button("Run Optimization"):
x0 = np.ones(n_materials)/n_materials
res = minimize(objective, x0, method='SLSQP', bounds=bounds,
constraints=({'type':'eq','fun':lambda x: np.sum(x)-1},),
options={'maxiter':int(max_iter)})
frac_opt = np.clip(res.x,0,1)
frac_opt /= frac_opt.sum()
st.subheader("Optimized Material Fractions")
st.table(pd.DataFrame({"Material":oxide_df.columns,"Fraction":frac_opt,"Percent":frac_opt*100}).set_index("Material"))
# Achieved bulk oxides
bulk_ach = bulk_from_frac(frac_opt, oxide_matrix)
st.subheader("Achieved Bulk Oxides (wt%)")
st.table(pd.DataFrame.from_dict(bulk_ach, orient='index', columns=['Wt%']))
# Achieved moduli
LSF_ach, SM_ach, AM_ach = compute_moduli(bulk_ach)
st.markdown(f"**LSF:** {LSF_ach:.2f} | **SM:** {SM_ach:.3f} | **AM:** {AM_ach:.3f}")
# Clinker oxides
clinker_ox = {ox:bulk_ach[ox]*conv_factors[ox] for ox in OXIDES}
st.subheader("Clinker Oxides (after conversion factors)")
st.table(pd.DataFrame.from_dict(clinker_ox, orient='index', columns=['Wt%']))
# Clinker moduli
LSF_cl, SM_cl, AM_cl = compute_moduli(clinker_ox)
st.markdown(f"**Clinker LSF:** {LSF_cl:.2f} | **SM:** {SM_cl:.3f} | **AM:** {AM_cl:.3f}")
# Bogue phases
Ca = clinker_ox['CaO']; Si = clinker_ox['SiO2']; Al = clinker_ox['Al2O3']
Fe = clinker_ox['Fe2O3']; Mg = clinker_ox['MgO']
C3S = max(4.071*Ca - 7.600*Si - 6.718*Al - 1.430*Fe, 0)
C2S = max(2.867*Si - 0.7544*C3S, 0)
C3A = max(2.650*Al - 1.692*Fe, 0)
C4AF = max(3.043*Fe, 0)
st.subheader("Bogue Phase Estimates (wt%)")
st.table(pd.Series({'C3S':C3S,'C2S':C2S,'C3A':C3A,'C4AF':C4AF}).to_frame('Wt%'))
# Liquid Phase & Coating Index
LP = liquid_phase(Ca, Mg, Al, Fe, Si)
CI = coating_index(Al, Fe, Si)
st.subheader("Liquid Phase and Coating Index")
st.markdown(f"**Liquid Phase Fraction (%):** {LP:.2f} | **Coating Index:** {CI:.3f}")
# Quality & Operational Summary
review = quality_review(LP, CI, C3S, C2S, C3A, C4AF)
st.subheader("Quality & Operational Summary")
for line in review:
st.markdown(f"- {line}")