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}")