import streamlit as st import numpy as np import pandas as pd import plotly.graph_objects as go from plotly.subplots import make_subplots import time # ===================================================== # PAGE CONFIGURATION # ===================================================== # ===================================================== # FIX FOR STREAMLIT CLOUD / HUGGING FACE # ===================================================== st.set_page_config( page_title="Industrial PLC-DCS-SCADA Simulator", layout="wide" ) st.title("🏭 Industrial Chemical Plant Simulator") st.markdown("## PLC + DCS + SCADA Integrated Control System") # ===================================================== # SIDEBAR # ===================================================== st.sidebar.header("Plant Parameters") setpoint = st.sidebar.slider( "Reactor Temperature Setpoint", 40, 120, 80 ) simulation_time = st.sidebar.slider( "Simulation Time", 100, 500, 250 ) noise = st.sidebar.slider( "Sensor Noise", 0.0, 2.0, 0.3 ) # ===================================================== # PROCESS MODEL # ===================================================== ambient_temp = 25 process_gain = 0.12 cooling_gain = 0.20 thermal_mass = 18 # ===================================================== # PLC TAGS # ===================================================== class PLC: def __init__(self): self.tags = { "TT101": 0, "CV101": 0, "SP101": setpoint, "ALARM_HIGH": False, "PUMP_RUN": True } def update(self, temperature, valve): self.tags["TT101"] = temperature self.tags["CV101"] = valve if temperature > setpoint + 10: self.tags["ALARM_HIGH"] = True else: self.tags["ALARM_HIGH"] = False # ===================================================== # CONTROL SIMULATION # ===================================================== def simulate(controller): plc = PLC() temp = 30 integral = 0 previous_error = 0 temperatures = [] valves = [] alarms = [] times = [] for t in range(simulation_time): error = setpoint - temp # ============================================= # PLC + ON/OFF CONTROL # ============================================= if controller == "ONOFF": if temp < setpoint: valve = 0 else: valve = 100 # ============================================= # PLC + PID CONTROL # ============================================= elif controller == "PID": kp = 2.5 ki = 0.04 kd = 0.7 integral += error derivative = error - previous_error valve = kp * error + ki * integral + kd * derivative valve = np.clip(valve, 0, 100) previous_error = error # ============================================= # PLC + MPC CONTROL # ============================================= elif controller == "MPC": future_error = error * 0.85 valve = 1.8 * future_error + 25 valve = np.clip(valve, 0, 100) # ============================================= # PROCESS DYNAMICS # ============================================= heating = process_gain * (100 - temp) cooling = cooling_gain * (valve / 100) * (temp - ambient_temp) dT = (heating - cooling) / thermal_mass temp += dT + np.random.normal(0, noise) # PLC UPDATE plc.update(temp, valve) temperatures.append(temp) valves.append(valve) alarms.append(plc.tags["ALARM_HIGH"]) times.append(t) return pd.DataFrame({ "Time": times, "Temperature": temperatures, "Valve": valves, "Alarm": alarms }) # ===================================================== # RUN ALL CONTROLLERS # ===================================================== onoff_df = simulate("ONOFF") pid_df = simulate("PID") mpc_df = simulate("MPC") # ===================================================== # SCADA HEADER # ===================================================== st.markdown("---") sc1, sc2, sc3, sc4 = st.columns(4) with sc1: st.metric("Plant Status", "RUNNING") with sc2: st.metric("PLC Status", "ONLINE") with sc3: st.metric("DCS Network", "CONNECTED") with sc4: st.metric("SCADA", "ACTIVE") # ===================================================== # SECTION 1 : PLC + ON-OFF CONTROL # ===================================================== st.markdown("---") st.header("🟢 SECTION 1 : PLC + ON-OFF CONTROL") import streamlit as st import numpy as np import pandas as pd import plotly.graph_objects as go from plotly.subplots import make_subplots import time # ------------------------------------------------- # PAGE CONFIG # ------------------------------------------------- st.set_page_config( page_title="Chemical Plant Control Simulator", layout="wide" ) st.title("🏭 Chemical Plant DCS / SCADA Simulator") st.markdown("### Temperature Control of a Chemical Reactor") # ------------------------------------------------- # SIDEBAR # ------------------------------------------------- st.sidebar.header("Simulation Settings") setpoint = st.sidebar.slider( "Temperature Setpoint (°C)", 40, 120, 80 ) simulation_time = st.sidebar.slider( "Simulation Time", 50, 500, 200 ) noise_level = st.sidebar.slider( "Process Noise", 0.0, 2.0, 0.3 ) # ------------------------------------------------- # PROCESS MODEL # ------------------------------------------------- ambient_temp = 25 process_gain = 0.12 cooling_gain = 0.18 thermal_mass = 18 # ------------------------------------------------- # SIMULATION FUNCTION # ------------------------------------------------- def simulate(controller_type): temp = 30 integral = 0 previous_error = 0 temperatures = [] valve_positions = [] times = [] for t in range(simulation_time): error = setpoint - temp # ----------------------------------------- # ON-OFF CONTROL # ----------------------------------------- if controller_type == "ON-OFF": if temp < setpoint: valve = 0 else: valve = 100 # ----------------------------------------- # PID CONTROL # ----------------------------------------- elif controller_type == "PID": kp = 2.5 ki = 0.05 kd = 0.8 integral += error derivative = error - previous_error valve = ( kp * error + ki * integral + kd * derivative ) valve = np.clip(valve, 0, 100) previous_error = error # ----------------------------------------- # MPC-LIKE CONTROL # ----------------------------------------- elif controller_type == "MPC": future_error = error * 0.85 valve = 1.8 * future_error + 25 valve = np.clip(valve, 0, 100) # ----------------------------------------- # PROCESS DYNAMICS # ----------------------------------------- heating = process_gain * (100 - temp) cooling = cooling_gain * (valve / 100) * (temp - ambient_temp) dT = (heating - cooling) / thermal_mass temp += dT + np.random.normal(0, noise_level) temperatures.append(temp) valve_positions.append(valve) times.append(t) return pd.DataFrame({ "Time": times, "Temperature": temperatures, "Valve": valve_positions }) # ------------------------------------------------- # RUN SIMULATIONS # ------------------------------------------------- onoff_df = simulate("ON-OFF") pid_df = simulate("PID") mpc_df = simulate("MPC") # ------------------------------------------------- # LIVE SCADA CARDS # ------------------------------------------------- col1, col2, col3 = st.columns(3) latest_onoff = round(onoff_df["Temperature"].iloc[-1], 2) latest_pid = round(pid_df["Temperature"].iloc[-1], 2) latest_mpc = round(mpc_df["Temperature"].iloc[-1], 2) with col1: st.metric("ON-OFF Temp", f"{latest_onoff} °C") with col2: st.metric("PID Temp", f"{latest_pid} °C") with col3: st.metric("MPC Temp", f"{latest_mpc} °C") # ------------------------------------------------- # DCS STYLE REACTOR DIAGRAM # ------------------------------------------------- st.markdown("---") st.subheader("⚙️ Reactor DCS Overview") reactor_col1, reactor_col2 = st.columns([2, 1]) with reactor_col1: fig_reactor = go.Figure() fig_reactor.add_shape( type="rect", x0=1, y0=1, x1=4, y1=8, line=dict(color="cyan", width=4), fillcolor="rgba(0,255,255,0.1)" ) fig_reactor.add_annotation( x=2.5, y=4.5, text=f"Reactor\nSP = {setpoint}°C", showarrow=False, font=dict(size=18) ) fig_reactor.add_shape( type="line", x0=4, y0=4, x1=7, y1=4, line=dict(color="orange", width=8) ) fig_reactor.add_annotation( x=6, y=4.5, text="Cooling Water", showarrow=False ) fig_reactor.update_layout( height=400, paper_bgcolor="black", plot_bgcolor="black", font_color="white", xaxis=dict(visible=False), yaxis=dict(visible=False) ) st.plotly_chart(fig_reactor, use_container_width=True) with reactor_col2: gauge = go.Figure(go.Indicator( mode="gauge+number", value=latest_pid, title={'text': "PID Reactor Temp"}, gauge={ 'axis': {'range': [None, 120]}, 'bar': {'color': "red"}, 'steps': [ {'range': [0, 60], 'color': "lightgreen"}, {'range': [60, 90], 'color': "yellow"}, {'range': [90, 120], 'color': "orange"} ] } )) gauge.update_layout(height=400) st.plotly_chart(gauge, use_container_width=True) # ------------------------------------------------- # TEMPERATURE COMPARISON # ------------------------------------------------- st.markdown("---") st.subheader("📈 Temperature Response Comparison") fig = go.Figure() fig.add_trace(go.Scatter( x=onoff_df["Time"], y=onoff_df["Temperature"], mode='lines', name='ON-OFF' )) fig.add_trace(go.Scatter( x=pid_df["Time"], y=pid_df["Temperature"], mode='lines', name='PID' )) fig.add_trace(go.Scatter( x=mpc_df["Time"], y=mpc_df["Temperature"], mode='lines', name='MPC' )) fig.add_hline( y=setpoint, line_dash="dash", annotation_text="Setpoint" ) fig.update_layout( template="plotly_dark", height=500, xaxis_title="Time", yaxis_title="Temperature (°C)" ) st.plotly_chart(fig, use_container_width=True) # ------------------------------------------------- # VALVE POSITION COMPARISON # ------------------------------------------------- st.markdown("---") st.subheader("🌀 Cooling Valve Position") fig2 = make_subplots(rows=1, cols=3, subplot_titles=("ON-OFF", "PID", "MPC")) fig2.add_trace( go.Scatter( x=onoff_df["Time"], y=onoff_df["Valve"], mode='lines' ), row=1, col=1 ) fig2.add_trace( go.Scatter( x=pid_df["Time"], y=pid_df["Valve"], mode='lines' ), row=1, col=2 ) fig2.add_trace( go.Scatter( x=mpc_df["Time"], y=mpc_df["Valve"], mode='lines' ), row=1, col=3 ) fig2.update_layout( template="plotly_dark", height=450, showlegend=False ) st.plotly_chart(fig2, use_container_width=True) # ------------------------------------------------- # CONTROL SYSTEM EXPLANATION # ------------------------------------------------- st.markdown("---") st.subheader("📚 Control System Explanation") st.markdown(""" ### 1. ON-OFF Control - Simplest controller - Valve fully opens or fully closes - Causes oscillation - Used in household thermostats ### 2. PID Control - Most common industrial controller - Smooth response - Reduces overshoot - Widely used in DCS systems ### 3. MPC Control - Predictive strategy - Estimates future behavior - Used in advanced chemical plants - Common in refineries and petrochemical plants """) # ------------------------------------------------- # PERFORMANCE TABLE # ------------------------------------------------- st.markdown("---") st.subheader("📊 Performance Comparison") performance = pd.DataFrame({ "Controller": ["ON-OFF", "PID", "MPC"], "Final Temp": [ round(onoff_df["Temperature"].iloc[-1], 2), round(pid_df["Temperature"].iloc[-1], 2), round(mpc_df["Temperature"].iloc[-1], 2) ], "Valve Stability": [ "Low", "High", "Very High" ], "Industrial Use": [ "Basic", "Very Common", "Advanced" ] }) st.dataframe(performance, use_container_width=True) # ------------------------------------------------- # FOOTER # ------------------------------------------------- st.markdown("---") st.markdown("## ✅ Simulation Complete") st.markdown("Designed for Chemical Engineering DCS / SCADA Learning")