Spaces:
Running
Running
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}") | |