Update app.py
Browse files
app.py
CHANGED
|
@@ -3,7 +3,10 @@ import pandas as pd
|
|
| 3 |
import joblib
|
| 4 |
import os
|
| 5 |
import plotly.graph_objects as go
|
|
|
|
| 6 |
from huggingface_hub import hf_hub_download
|
|
|
|
|
|
|
| 7 |
|
| 8 |
# --- CONFIGURATION ---
|
| 9 |
HF_USERNAME = os.getenv("HF_USERNAME", "iStillWaters")
|
|
@@ -13,26 +16,56 @@ MODEL_REPO_ID = f"{HF_USERNAME}/{MODEL_REPO_NAME}"
|
|
| 13 |
MODEL_FILENAME = "best_engine_model.pkl"
|
| 14 |
SCALER_FILENAME = "scaler.joblib"
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
# --- LOAD ARTIFACTS ---
|
| 17 |
@st.cache_resource
|
| 18 |
def load_artifacts():
|
|
|
|
| 19 |
try:
|
| 20 |
model_path = hf_hub_download(repo_id=MODEL_REPO_ID, filename=MODEL_FILENAME)
|
| 21 |
scaler_path = hf_hub_download(repo_id=MODEL_REPO_ID, filename=SCALER_FILENAME)
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
except Exception as e:
|
| 24 |
-
|
| 25 |
-
return None, None
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
# --- HELPER
|
| 30 |
-
def create_gauge(value, title, min_val, max_val, color="blue"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
fig = go.Figure(go.Indicator(
|
| 32 |
-
mode = "gauge+number",
|
| 33 |
value = value,
|
| 34 |
-
domain = {'x': [0, 1], 'y': [0, 1]},
|
| 35 |
title = {'text': title, 'font': {'size': 18}},
|
|
|
|
|
|
|
| 36 |
gauge = {
|
| 37 |
'axis': {'range': [min_val, max_val]},
|
| 38 |
'bar': {'color': color},
|
|
@@ -40,133 +73,531 @@ def create_gauge(value, title, min_val, max_val, color="blue"):
|
|
| 40 |
'borderwidth': 2,
|
| 41 |
'bordercolor': "gray",
|
| 42 |
'steps': [
|
| 43 |
-
{'range': [
|
| 44 |
-
{'range': [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
))
|
| 47 |
-
# Fixed height ensures alignment with images
|
| 48 |
fig.update_layout(height=250, margin=dict(l=20, r=20, t=50, b=20))
|
| 49 |
return fig
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
.
|
| 58 |
-
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
}
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
with
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
-
#
|
| 122 |
-
|
| 123 |
-
pred = model.predict(scaled)[0]
|
| 124 |
-
try:
|
| 125 |
-
prob = model.predict_proba(scaled)[0][1]
|
| 126 |
-
except:
|
| 127 |
-
prob = 0.0
|
| 128 |
-
|
| 129 |
-
# --- DYNAMIC DASHBOARD DISPLAY ---
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
fig_prob = go.Figure(go.Indicator(
|
| 134 |
-
mode = "gauge+number",
|
| 135 |
-
value = prob * 100,
|
| 136 |
-
title = {'text': "Failure Probability (%)", 'font': {'size': 20}},
|
| 137 |
-
gauge = {
|
| 138 |
-
'axis': {'range': [0, 100]},
|
| 139 |
-
'bar': {'color': gauge_color},
|
| 140 |
-
'threshold': {
|
| 141 |
-
'line': {'color': "red", 'width': 4},
|
| 142 |
-
'thickness': 0.75,
|
| 143 |
-
'value': 90
|
| 144 |
-
}
|
| 145 |
-
}
|
| 146 |
-
))
|
| 147 |
-
fig_prob.update_layout(height=300, margin=dict(l=20, r=20, t=50, b=20))
|
| 148 |
-
|
| 149 |
-
# --- SIDE-BY-SIDE LAYOUT ---
|
| 150 |
-
# Create two columns within the center column
|
| 151 |
-
res_col1, res_col2 = st.columns([1, 1], gap="medium")
|
| 152 |
-
|
| 153 |
-
if pred == 1:
|
| 154 |
-
st.error("🚨 CRITICAL FAILURE DETECTED")
|
| 155 |
-
img_url = "https://freesvg.org/img/check-engine.png"
|
| 156 |
else:
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
st.markdown(
|
| 162 |
-
f
|
| 163 |
-
f
|
| 164 |
-
f
|
| 165 |
unsafe_allow_html=True
|
| 166 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
-
|
| 172 |
-
|
|
|
|
| 3 |
import joblib
|
| 4 |
import os
|
| 5 |
import plotly.graph_objects as go
|
| 6 |
+
from plotly.subplots import make_subplots
|
| 7 |
from huggingface_hub import hf_hub_download
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
import json
|
| 10 |
|
| 11 |
# --- CONFIGURATION ---
|
| 12 |
HF_USERNAME = os.getenv("HF_USERNAME", "iStillWaters")
|
|
|
|
| 16 |
MODEL_FILENAME = "best_engine_model.pkl"
|
| 17 |
SCALER_FILENAME = "scaler.joblib"
|
| 18 |
|
| 19 |
+
# Sensor thresholds for warnings
|
| 20 |
+
SENSOR_THRESHOLDS = {
|
| 21 |
+
'rpm_warning': 2000,
|
| 22 |
+
'rpm_critical': 2300,
|
| 23 |
+
'coolant_temp_warning': 100,
|
| 24 |
+
'coolant_temp_critical': 120,
|
| 25 |
+
'oil_pressure_low': 2.0,
|
| 26 |
+
'oil_pressure_critical': 1.5,
|
| 27 |
+
'fuel_pressure_low': 5.0,
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
# --- LOAD ARTIFACTS ---
|
| 31 |
@st.cache_resource
|
| 32 |
def load_artifacts():
|
| 33 |
+
"""Load model and scaler from HuggingFace Hub with error handling"""
|
| 34 |
try:
|
| 35 |
model_path = hf_hub_download(repo_id=MODEL_REPO_ID, filename=MODEL_FILENAME)
|
| 36 |
scaler_path = hf_hub_download(repo_id=MODEL_REPO_ID, filename=SCALER_FILENAME)
|
| 37 |
+
model = joblib.load(model_path)
|
| 38 |
+
scaler = joblib.load(scaler_path)
|
| 39 |
+
return model, scaler, None
|
| 40 |
except Exception as e:
|
| 41 |
+
return None, None, str(e)
|
|
|
|
| 42 |
|
| 43 |
+
# --- SESSION STATE INITIALIZATION ---
|
| 44 |
+
def init_session_state():
|
| 45 |
+
"""Initialize session state variables"""
|
| 46 |
+
if 'history' not in st.session_state:
|
| 47 |
+
st.session_state.history = []
|
| 48 |
+
if 'alert_threshold' not in st.session_state:
|
| 49 |
+
st.session_state.alert_threshold = 0.7
|
| 50 |
+
if 'dark_mode' not in st.session_state:
|
| 51 |
+
st.session_state.dark_mode = True
|
| 52 |
+
if 'show_recommendations' not in st.session_state:
|
| 53 |
+
st.session_state.show_recommendations = True
|
| 54 |
|
| 55 |
+
# --- HELPER FUNCTIONS ---
|
| 56 |
+
def create_gauge(value, title, min_val, max_val, thresholds=None, color="blue"):
|
| 57 |
+
"""Create an enhanced gauge chart with color-coded zones"""
|
| 58 |
+
if thresholds is None:
|
| 59 |
+
# Default thresholds: green (0-60%), yellow (60-80%), red (80-100%)
|
| 60 |
+
thresholds = [min_val, min_val + (max_val-min_val)*0.6,
|
| 61 |
+
min_val + (max_val-min_val)*0.8, max_val]
|
| 62 |
+
|
| 63 |
fig = go.Figure(go.Indicator(
|
| 64 |
+
mode = "gauge+number+delta",
|
| 65 |
value = value,
|
|
|
|
| 66 |
title = {'text': title, 'font': {'size': 18}},
|
| 67 |
+
delta = {'reference': (max_val + min_val) / 2, 'relative': False},
|
| 68 |
+
domain = {'x': [0, 1], 'y': [0, 1]},
|
| 69 |
gauge = {
|
| 70 |
'axis': {'range': [min_val, max_val]},
|
| 71 |
'bar': {'color': color},
|
|
|
|
| 73 |
'borderwidth': 2,
|
| 74 |
'bordercolor': "gray",
|
| 75 |
'steps': [
|
| 76 |
+
{'range': [thresholds[0], thresholds[1]], 'color': "#90EE90"},
|
| 77 |
+
{'range': [thresholds[1], thresholds[2]], 'color': "#FFD700"},
|
| 78 |
+
{'range': [thresholds[2], thresholds[3]], 'color': "#FFB6C1"}
|
| 79 |
+
],
|
| 80 |
+
'threshold': {
|
| 81 |
+
'line': {'color': "red", 'width': 4},
|
| 82 |
+
'thickness': 0.75,
|
| 83 |
+
'value': thresholds[2]
|
| 84 |
+
}
|
| 85 |
}
|
| 86 |
))
|
|
|
|
| 87 |
fig.update_layout(height=250, margin=dict(l=20, r=20, t=50, b=20))
|
| 88 |
return fig
|
| 89 |
|
| 90 |
+
def validate_inputs(rpm, fuel_p, lub_oil_p, coolant_temp, coolant_p, lub_oil_t):
|
| 91 |
+
"""Validate sensor readings and return warnings"""
|
| 92 |
+
warnings = []
|
| 93 |
+
critical = []
|
| 94 |
+
|
| 95 |
+
# RPM checks
|
| 96 |
+
if rpm > SENSOR_THRESHOLDS['rpm_critical']:
|
| 97 |
+
critical.append("🔴 CRITICAL: RPM exceeding safe limits!")
|
| 98 |
+
elif rpm > SENSOR_THRESHOLDS['rpm_warning']:
|
| 99 |
+
warnings.append("⚠️ RPM approaching redline")
|
| 100 |
+
|
| 101 |
+
# Coolant temperature checks
|
| 102 |
+
if coolant_temp > SENSOR_THRESHOLDS['coolant_temp_critical']:
|
| 103 |
+
critical.append("🔴 CRITICAL: Engine overheating!")
|
| 104 |
+
elif coolant_temp > SENSOR_THRESHOLDS['coolant_temp_warning']:
|
| 105 |
+
warnings.append("⚠️ Coolant temperature elevated")
|
| 106 |
+
|
| 107 |
+
# Oil pressure checks
|
| 108 |
+
if lub_oil_p < SENSOR_THRESHOLDS['oil_pressure_critical']:
|
| 109 |
+
critical.append("🔴 CRITICAL: Oil pressure dangerously low!")
|
| 110 |
+
elif lub_oil_p < SENSOR_THRESHOLDS['oil_pressure_low']:
|
| 111 |
+
warnings.append("⚠️ Oil pressure below normal")
|
| 112 |
+
|
| 113 |
+
# Fuel pressure checks
|
| 114 |
+
if fuel_p < SENSOR_THRESHOLDS['fuel_pressure_low']:
|
| 115 |
+
warnings.append("⚠️ Fuel pressure low")
|
| 116 |
+
|
| 117 |
+
return warnings, critical
|
| 118 |
+
|
| 119 |
+
def prepare_input_data(rpm, lub_oil_p, fuel_p, coolant_p, lub_oil_t, coolant_temp):
|
| 120 |
+
"""Prepare input data for model prediction"""
|
| 121 |
+
return pd.DataFrame({
|
| 122 |
+
'Engine rpm': [rpm],
|
| 123 |
+
'Lub oil pressure': [lub_oil_p],
|
| 124 |
+
'Fuel pressure': [fuel_p],
|
| 125 |
+
'Coolant pressure': [coolant_p],
|
| 126 |
+
'lub oil temp': [lub_oil_t],
|
| 127 |
+
'Coolant temp': [coolant_temp]
|
| 128 |
+
})
|
| 129 |
+
|
| 130 |
+
def run_prediction(model, scaler, input_df):
|
| 131 |
+
"""Run model prediction with error handling"""
|
| 132 |
+
try:
|
| 133 |
+
scaled = scaler.transform(input_df)
|
| 134 |
+
pred = model.predict(scaled)[0]
|
| 135 |
+
prob = model.predict_proba(scaled)[0][1] if hasattr(model, 'predict_proba') else 0.0
|
| 136 |
+
return pred, prob, None
|
| 137 |
+
except Exception as e:
|
| 138 |
+
return None, None, str(e)
|
| 139 |
+
|
| 140 |
+
def get_status_info(prob):
|
| 141 |
+
"""Get status badge and color based on probability"""
|
| 142 |
+
if prob > 0.75:
|
| 143 |
+
return "🔴 CRITICAL", "#ff4b4b", "CRITICAL FAILURE RISK"
|
| 144 |
+
elif prob > 0.5:
|
| 145 |
+
return "🟡 WARNING", "#ffa500", "ELEVATED FAILURE RISK"
|
| 146 |
+
elif prob > 0.25:
|
| 147 |
+
return "🟠 CAUTION", "#ff8c00", "MODERATE RISK"
|
| 148 |
+
else:
|
| 149 |
+
return "🟢 HEALTHY", "#00cc00", "SYSTEMS NOMINAL"
|
| 150 |
+
|
| 151 |
+
def get_recommendations(input_df, prob, warnings, critical):
|
| 152 |
+
"""Generate actionable recommendations based on sensor readings and prediction"""
|
| 153 |
+
recommendations = []
|
| 154 |
+
|
| 155 |
+
# Critical alerts first
|
| 156 |
+
if critical:
|
| 157 |
+
recommendations.extend(critical)
|
| 158 |
+
recommendations.append("🚨 IMMEDIATE ACTION REQUIRED - SHUT DOWN ENGINE")
|
| 159 |
+
|
| 160 |
+
# Sensor-specific recommendations
|
| 161 |
+
rpm = input_df['Engine rpm'].values[0]
|
| 162 |
+
coolant_temp = input_df['Coolant temp'].values[0]
|
| 163 |
+
lub_oil_p = input_df['Lub oil pressure'].values[0]
|
| 164 |
+
fuel_p = input_df['Fuel pressure'].values[0]
|
| 165 |
+
|
| 166 |
+
if coolant_temp > SENSOR_THRESHOLDS['coolant_temp_warning']:
|
| 167 |
+
recommendations.append("🔧 Check coolant level and radiator for blockages")
|
| 168 |
+
recommendations.append("🔧 Inspect water pump operation")
|
| 169 |
+
|
| 170 |
+
if lub_oil_p < SENSOR_THRESHOLDS['oil_pressure_low']:
|
| 171 |
+
recommendations.append("🔧 Check engine oil level immediately")
|
| 172 |
+
recommendations.append("🔧 Inspect oil pump and filter for blockages")
|
| 173 |
+
|
| 174 |
+
if fuel_p < SENSOR_THRESHOLDS['fuel_pressure_low']:
|
| 175 |
+
recommendations.append("🔧 Check fuel filter for clogs")
|
| 176 |
+
recommendations.append("🔧 Inspect fuel pump performance")
|
| 177 |
+
|
| 178 |
+
if rpm > SENSOR_THRESHOLDS['rpm_warning'] and prob > 0.5:
|
| 179 |
+
recommendations.append("🔧 Reduce engine load immediately")
|
| 180 |
+
recommendations.append("🔧 Schedule comprehensive engine inspection")
|
| 181 |
+
|
| 182 |
+
# Model-based recommendations
|
| 183 |
+
if prob > 0.7:
|
| 184 |
+
recommendations.append("📅 Schedule immediate maintenance inspection")
|
| 185 |
+
recommendations.append("📊 Consider engine diagnostics scan")
|
| 186 |
+
elif prob > 0.4:
|
| 187 |
+
recommendations.append("📅 Schedule preventive maintenance within 48 hours")
|
| 188 |
+
|
| 189 |
+
if not recommendations and not warnings:
|
| 190 |
+
recommendations.append("✅ All systems operating within normal parameters")
|
| 191 |
+
recommendations.append("📅 Continue regular maintenance schedule")
|
| 192 |
+
|
| 193 |
+
return recommendations
|
| 194 |
|
| 195 |
+
def calculate_feature_importance(input_df):
|
| 196 |
+
"""Calculate normalized feature importance scores"""
|
| 197 |
+
rpm_score = input_df['Engine rpm'].values[0] / 2500
|
| 198 |
+
coolant_score = input_df['Coolant temp'].values[0] / 200
|
| 199 |
+
oil_score = max(0, 1 - (input_df['Lub oil pressure'].values[0] / 10))
|
| 200 |
+
fuel_score = input_df['Fuel pressure'].values[0] / 25
|
| 201 |
|
| 202 |
+
return {
|
| 203 |
+
'Engine RPM': rpm_score,
|
| 204 |
+
'Coolant Temperature': coolant_score,
|
| 205 |
+
'Oil Pressure (Risk)': oil_score,
|
| 206 |
+
'Fuel Pressure': fuel_score
|
| 207 |
}
|
| 208 |
|
| 209 |
+
def create_history_chart():
|
| 210 |
+
"""Create historical trend chart"""
|
| 211 |
+
if len(st.session_state.history) < 2:
|
| 212 |
+
return None
|
| 213 |
+
|
| 214 |
+
hist_df = pd.DataFrame(st.session_state.history)
|
| 215 |
+
|
| 216 |
+
fig = make_subplots(
|
| 217 |
+
rows=2, cols=1,
|
| 218 |
+
subplot_titles=('Failure Probability Over Time', 'Engine RPM Over Time'),
|
| 219 |
+
vertical_spacing=0.15
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
# Probability trend
|
| 223 |
+
fig.add_trace(
|
| 224 |
+
go.Scatter(
|
| 225 |
+
x=hist_df['timestamp'],
|
| 226 |
+
y=hist_df['probability']*100,
|
| 227 |
+
mode='lines+markers',
|
| 228 |
+
name='Failure Risk (%)',
|
| 229 |
+
line=dict(color='red', width=2),
|
| 230 |
+
marker=dict(size=8)
|
| 231 |
+
),
|
| 232 |
+
row=1, col=1
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
# Add threshold line
|
| 236 |
+
fig.add_hline(
|
| 237 |
+
y=st.session_state.alert_threshold*100,
|
| 238 |
+
line_dash="dash",
|
| 239 |
+
line_color="orange",
|
| 240 |
+
annotation_text="Alert Threshold",
|
| 241 |
+
row=1, col=1
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
# RPM trend
|
| 245 |
+
fig.add_trace(
|
| 246 |
+
go.Scatter(
|
| 247 |
+
x=hist_df['timestamp'],
|
| 248 |
+
y=hist_df['rpm'],
|
| 249 |
+
mode='lines+markers',
|
| 250 |
+
name='RPM',
|
| 251 |
+
line=dict(color='cyan', width=2),
|
| 252 |
+
marker=dict(size=8)
|
| 253 |
+
),
|
| 254 |
+
row=2, col=1
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
fig.update_xaxes(title_text="Time", row=2, col=1)
|
| 258 |
+
fig.update_yaxes(title_text="Probability (%)", row=1, col=1)
|
| 259 |
+
fig.update_yaxes(title_text="RPM", row=2, col=1)
|
| 260 |
+
|
| 261 |
+
fig.update_layout(height=500, showlegend=True)
|
| 262 |
+
return fig
|
| 263 |
+
|
| 264 |
+
def export_report(input_df, prob, pred, warnings, critical, recommendations):
|
| 265 |
+
"""Generate downloadable report"""
|
| 266 |
+
report = {
|
| 267 |
+
'Timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 268 |
+
'Status': 'FAILURE RISK' if pred == 1 else 'OPERATIONAL',
|
| 269 |
+
'Failure Probability': f"{prob*100:.2f}%",
|
| 270 |
+
'Engine RPM': input_df['Engine rpm'].values[0],
|
| 271 |
+
'Fuel Pressure (Bar)': input_df['Fuel pressure'].values[0],
|
| 272 |
+
'Oil Pressure': input_df['Lub oil pressure'].values[0],
|
| 273 |
+
'Oil Temperature': input_df['lub oil temp'].values[0],
|
| 274 |
+
'Coolant Pressure': input_df['Coolant pressure'].values[0],
|
| 275 |
+
'Coolant Temperature': input_df['Coolant temp'].values[0],
|
| 276 |
+
'Warnings': '; '.join(warnings) if warnings else 'None',
|
| 277 |
+
'Critical Alerts': '; '.join(critical) if critical else 'None',
|
| 278 |
+
'Recommendations': '; '.join(recommendations[:3]) if recommendations else 'None'
|
| 279 |
}
|
| 280 |
+
return pd.DataFrame([report])
|
| 281 |
+
|
| 282 |
+
# --- MAIN APP ---
|
| 283 |
+
def main():
|
| 284 |
+
# Page config
|
| 285 |
+
st.set_page_config(
|
| 286 |
+
layout="wide",
|
| 287 |
+
page_title="Engine Health Monitor",
|
| 288 |
+
page_icon="🏎️",
|
| 289 |
+
initial_sidebar_state="expanded"
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
# Initialize session state
|
| 293 |
+
init_session_state()
|
| 294 |
+
|
| 295 |
+
# Load model
|
| 296 |
+
model, scaler, error = load_artifacts()
|
| 297 |
+
|
| 298 |
+
# Check if model loaded successfully
|
| 299 |
+
if model is None or scaler is None:
|
| 300 |
+
st.error(f"⚠️ Failed to load model: {error}")
|
| 301 |
+
st.info("Please check your HuggingFace configuration and ensure the model repository is accessible.")
|
| 302 |
+
st.stop()
|
| 303 |
+
|
| 304 |
+
# --- SIDEBAR ---
|
| 305 |
+
with st.sidebar:
|
| 306 |
+
st.image("https://img.icons8.com/color/96/000000/engine.png", width=80)
|
| 307 |
+
st.title("⚙️ Settings")
|
| 308 |
+
|
| 309 |
+
# Model info
|
| 310 |
+
st.subheader("Model Information")
|
| 311 |
+
st.info(f"**Repository:** {MODEL_REPO_ID}")
|
| 312 |
+
st.info(f"**Model File:** {MODEL_FILENAME}")
|
| 313 |
+
|
| 314 |
+
# Settings
|
| 315 |
+
st.subheader("Configuration")
|
| 316 |
+
st.session_state.alert_threshold = st.slider(
|
| 317 |
+
"Alert Threshold",
|
| 318 |
+
0.0, 1.0,
|
| 319 |
+
st.session_state.alert_threshold,
|
| 320 |
+
help="Probability threshold for failure alerts"
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
st.session_state.show_recommendations = st.checkbox(
|
| 324 |
+
"Show Recommendations",
|
| 325 |
+
st.session_state.show_recommendations
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
auto_refresh = st.checkbox("🔄 Auto-refresh (10s)", value=False)
|
| 329 |
+
|
| 330 |
+
# History management
|
| 331 |
+
st.subheader("History")
|
| 332 |
+
if st.session_state.history:
|
| 333 |
+
st.metric("Total Analyses", len(st.session_state.history))
|
| 334 |
+
if st.button("Clear History"):
|
| 335 |
+
st.session_state.history = []
|
| 336 |
+
st.rerun()
|
| 337 |
+
else:
|
| 338 |
+
st.info("No analysis history yet")
|
| 339 |
+
|
| 340 |
+
# Info
|
| 341 |
+
st.divider()
|
| 342 |
+
st.caption("🏎️ Digital Twin Engine Monitor v2.0")
|
| 343 |
+
st.caption("Powered by Streamlit & ML")
|
| 344 |
+
|
| 345 |
+
# --- CUSTOM CSS ---
|
| 346 |
+
st.markdown("""
|
| 347 |
+
<style>
|
| 348 |
+
.main {background-color: #0e1117;}
|
| 349 |
+
h1 {text-align: center; color: white;}
|
| 350 |
+
|
| 351 |
+
.diag-header {
|
| 352 |
+
text-align: center;
|
| 353 |
+
font-weight: bold;
|
| 354 |
+
margin-bottom: 20px;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
div.stButton > button {
|
| 358 |
+
font-size: 20px !important;
|
| 359 |
+
font-weight: bold !important;
|
| 360 |
+
padding: 10px 20px !important;
|
| 361 |
+
width: 100%;
|
| 362 |
+
background-color: #ff4b4b;
|
| 363 |
+
color: white;
|
| 364 |
+
border-radius: 10px;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.metric-card {
|
| 368 |
+
background-color: #1e1e1e;
|
| 369 |
+
padding: 15px;
|
| 370 |
+
border-radius: 10px;
|
| 371 |
+
border-left: 4px solid #ff4b4b;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.status-badge {
|
| 375 |
+
padding: 10px 20px;
|
| 376 |
+
border-radius: 20px;
|
| 377 |
+
font-weight: bold;
|
| 378 |
+
text-align: center;
|
| 379 |
+
font-size: 24px;
|
| 380 |
+
}
|
| 381 |
+
</style>
|
| 382 |
+
""", unsafe_allow_html=True)
|
| 383 |
+
|
| 384 |
+
# --- HEADER ---
|
| 385 |
+
st.title("🏎️ Digital Twin: Engine Health Monitor")
|
| 386 |
+
st.markdown("### Real-time Predictive Maintenance System")
|
| 387 |
+
|
| 388 |
+
# --- DASHBOARD INPUTS ---
|
| 389 |
+
col_left, col_center, col_right = st.columns([1.2, 2, 1.2])
|
| 390 |
+
|
| 391 |
+
with col_left:
|
| 392 |
+
st.subheader("⛽ Fuel & Air Systems")
|
| 393 |
+
rpm = st.slider("Engine RPM", 0, 2500, 750, step=50)
|
| 394 |
+
fuel_p = st.slider("Fuel Pressure (Bar)", 0.0, 25.0, 6.2, step=0.1)
|
| 395 |
+
st.plotly_chart(
|
| 396 |
+
create_gauge(rpm, "RPM", 0, 2500, color="cyan"),
|
| 397 |
+
use_container_width=True
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
with col_right:
|
| 401 |
+
st.subheader("🛢️ Cooling & Lubrication")
|
| 402 |
+
lub_oil_p = st.slider("Oil Pressure (Bar)", 0.0, 10.0, 3.16, step=0.1)
|
| 403 |
+
coolant_temp = st.slider("Coolant Temp (°C)", 0.0, 200.0, 80.0, step=1.0)
|
| 404 |
+
st.plotly_chart(
|
| 405 |
+
create_gauge(coolant_temp, "Coolant Temp (°C)", 0, 200, color="orange"),
|
| 406 |
+
use_container_width=True
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
# Additional sensors
|
| 410 |
+
with st.expander("🔧 Advanced Sensor Configuration"):
|
| 411 |
+
col_ex1, col_ex2 = st.columns(2)
|
| 412 |
+
with col_ex1:
|
| 413 |
+
coolant_p = st.number_input(
|
| 414 |
+
"Coolant Pressure (Bar)",
|
| 415 |
+
0.0, 10.0, 2.16, step=0.1,
|
| 416 |
+
help="Cooling system pressure"
|
| 417 |
+
)
|
| 418 |
+
with col_ex2:
|
| 419 |
+
lub_oil_t = st.number_input(
|
| 420 |
+
"Oil Temperature (°C)",
|
| 421 |
+
0.0, 150.0, 80.0, step=1.0,
|
| 422 |
+
help="Lubrication oil temperature"
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
# --- PREDICTION CENTER ---
|
| 426 |
+
with col_center:
|
| 427 |
+
st.markdown("<h2 class='diag-header'>🩺 Real-Time Diagnostics</h2>", unsafe_allow_html=True)
|
| 428 |
+
|
| 429 |
+
# Validation warnings (before prediction)
|
| 430 |
+
warnings, critical = validate_inputs(rpm, fuel_p, lub_oil_p, coolant_temp, coolant_p, lub_oil_t)
|
| 431 |
+
|
| 432 |
+
if critical:
|
| 433 |
+
for alert in critical:
|
| 434 |
+
st.error(alert)
|
| 435 |
+
|
| 436 |
+
if warnings:
|
| 437 |
+
with st.expander("⚠️ Sensor Warnings", expanded=True):
|
| 438 |
+
for warning in warnings:
|
| 439 |
+
st.warning(warning)
|
| 440 |
+
|
| 441 |
+
# Analysis button
|
| 442 |
+
if st.button("🔍 Analyze Engine Status"):
|
| 443 |
+
# Prepare input
|
| 444 |
+
input_df = prepare_input_data(rpm, lub_oil_p, fuel_p, coolant_p, lub_oil_t, coolant_temp)
|
| 445 |
|
| 446 |
+
# Run prediction
|
| 447 |
+
pred, prob, pred_error = run_prediction(model, scaler, input_df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
|
| 449 |
+
if pred_error:
|
| 450 |
+
st.error(f"Prediction error: {pred_error}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
else:
|
| 452 |
+
# Store in history
|
| 453 |
+
st.session_state.history.append({
|
| 454 |
+
'timestamp': datetime.now(),
|
| 455 |
+
'rpm': rpm,
|
| 456 |
+
'probability': prob,
|
| 457 |
+
'status': 'FAIL' if pred == 1 else 'OK',
|
| 458 |
+
'coolant_temp': coolant_temp,
|
| 459 |
+
'oil_pressure': lub_oil_p
|
| 460 |
+
})
|
| 461 |
+
|
| 462 |
+
# Get status
|
| 463 |
+
status_badge, status_color, status_text = get_status_info(prob)
|
| 464 |
+
|
| 465 |
+
# Create probability gauge
|
| 466 |
+
gauge_color = "red" if prob > 0.5 else "green"
|
| 467 |
+
fig_prob = go.Figure(go.Indicator(
|
| 468 |
+
mode = "gauge+number",
|
| 469 |
+
value = prob * 100,
|
| 470 |
+
title = {'text': "Failure Probability (%)", 'font': {'size': 20}},
|
| 471 |
+
number = {'suffix': "%", 'font': {'size': 40}},
|
| 472 |
+
gauge = {
|
| 473 |
+
'axis': {'range': [0, 100]},
|
| 474 |
+
'bar': {'color': gauge_color},
|
| 475 |
+
'steps': [
|
| 476 |
+
{'range': [0, 25], 'color': "#90EE90"},
|
| 477 |
+
{'range': [25, 50], 'color': "#FFD700"},
|
| 478 |
+
{'range': [50, 75], 'color': "#FFA500"},
|
| 479 |
+
{'range': [75, 100], 'color': "#FF6B6B"}
|
| 480 |
+
],
|
| 481 |
+
'threshold': {
|
| 482 |
+
'line': {'color': "red", 'width': 4},
|
| 483 |
+
'thickness': 0.75,
|
| 484 |
+
'value': st.session_state.alert_threshold * 100
|
| 485 |
+
}
|
| 486 |
+
}
|
| 487 |
+
))
|
| 488 |
+
fig_prob.update_layout(height=300, margin=dict(l=20, r=20, t=50, b=20))
|
| 489 |
+
|
| 490 |
+
# Display results
|
| 491 |
st.markdown(
|
| 492 |
+
f"<div class='status-badge' style='background-color: {status_color}20; "
|
| 493 |
+
f"color: {status_color}; border: 2px solid {status_color};'>"
|
| 494 |
+
f"{status_badge}</div>",
|
| 495 |
unsafe_allow_html=True
|
| 496 |
)
|
| 497 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 498 |
+
|
| 499 |
+
# Side-by-side layout
|
| 500 |
+
res_col1, res_col2 = st.columns([1, 1], gap="medium")
|
| 501 |
+
|
| 502 |
+
# Image based on status
|
| 503 |
+
if pred == 1 or prob > st.session_state.alert_threshold:
|
| 504 |
+
img_url = "https://freesvg.org/img/check-engine.png"
|
| 505 |
+
else:
|
| 506 |
+
img_url = "https://img.freepik.com/premium-vector/check-engine-light-icon-vector-illustration_529846-559.jpg"
|
| 507 |
+
|
| 508 |
+
with res_col1:
|
| 509 |
+
st.markdown(
|
| 510 |
+
f'<div style="display: flex; justify-content: center; align-items: center; height: 300px;">'
|
| 511 |
+
f'<img src="{img_url}" style="max-height: 250px; max-width: 100%; border-radius: 10px;">'
|
| 512 |
+
f'</div>',
|
| 513 |
+
unsafe_allow_html=True
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
with res_col2:
|
| 517 |
+
st.plotly_chart(fig_prob, use_container_width=True)
|
| 518 |
+
|
| 519 |
+
# Feature importance
|
| 520 |
+
if prob > 0.3:
|
| 521 |
+
st.markdown("---")
|
| 522 |
+
st.subheader("⚡ Key Risk Factors")
|
| 523 |
+
feature_scores = calculate_feature_importance(input_df)
|
| 524 |
+
sorted_factors = sorted(feature_scores.items(), key=lambda x: x[1], reverse=True)
|
| 525 |
+
|
| 526 |
+
cols = st.columns(4)
|
| 527 |
+
for idx, (factor, severity) in enumerate(sorted_factors):
|
| 528 |
+
with cols[idx]:
|
| 529 |
+
st.metric(
|
| 530 |
+
factor,
|
| 531 |
+
f"{severity*100:.1f}%",
|
| 532 |
+
delta=None,
|
| 533 |
+
help=f"Normalized risk score for {factor}"
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
# Recommendations
|
| 537 |
+
if st.session_state.show_recommendations:
|
| 538 |
+
st.markdown("---")
|
| 539 |
+
recommendations = get_recommendations(input_df, prob, warnings, critical)
|
| 540 |
+
|
| 541 |
+
if recommendations:
|
| 542 |
+
st.subheader("🛠️ Recommended Actions")
|
| 543 |
+
for i, rec in enumerate(recommendations, 1):
|
| 544 |
+
st.write(f"{i}. {rec}")
|
| 545 |
+
|
| 546 |
+
# Export report
|
| 547 |
+
st.markdown("---")
|
| 548 |
+
report_df = export_report(input_df, prob, pred, warnings, critical, recommendations)
|
| 549 |
+
csv = report_df.to_csv(index=False)
|
| 550 |
+
|
| 551 |
+
col_export1, col_export2 = st.columns(2)
|
| 552 |
+
with col_export1:
|
| 553 |
+
st.download_button(
|
| 554 |
+
"📥 Download Report (CSV)",
|
| 555 |
+
csv,
|
| 556 |
+
f"engine_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
|
| 557 |
+
"text/csv",
|
| 558 |
+
use_container_width=True
|
| 559 |
+
)
|
| 560 |
+
|
| 561 |
+
with col_export2:
|
| 562 |
+
json_report = report_df.to_json(orient='records', indent=2)
|
| 563 |
+
st.download_button(
|
| 564 |
+
"📥 Download Report (JSON)",
|
| 565 |
+
json_report,
|
| 566 |
+
f"engine_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
|
| 567 |
+
"application/json",
|
| 568 |
+
use_container_width=True
|
| 569 |
+
)
|
| 570 |
+
else:
|
| 571 |
+
st.info("👆 Adjust the sensor sliders above and click **Analyze Engine Status** to run diagnostics")
|
| 572 |
+
|
| 573 |
+
# --- HISTORICAL TRENDS ---
|
| 574 |
+
if st.session_state.history:
|
| 575 |
+
st.markdown("---")
|
| 576 |
+
st.subheader("📊 Historical Analysis")
|
| 577 |
+
|
| 578 |
+
trend_chart = create_history_chart()
|
| 579 |
+
if trend_chart:
|
| 580 |
+
st.plotly_chart(trend_chart, use_container_width=True)
|
| 581 |
|
| 582 |
+
# Summary statistics
|
| 583 |
+
hist_df = pd.DataFrame(st.session_state.history)
|
| 584 |
+
col_stat1, col_stat2, col_stat3, col_stat4 = st.columns(4)
|
| 585 |
+
|
| 586 |
+
with col_stat1:
|
| 587 |
+
st.metric("Average Risk", f"{hist_df['probability'].mean()*100:.1f}%")
|
| 588 |
+
with col_stat2:
|
| 589 |
+
st.metric("Max Risk", f"{hist_df['probability'].max()*100:.1f}%")
|
| 590 |
+
with col_stat3:
|
| 591 |
+
failures = (hist_df['status'] == 'FAIL').sum()
|
| 592 |
+
st.metric("Failure Alerts", failures)
|
| 593 |
+
with col_stat4:
|
| 594 |
+
st.metric("Total Analyses", len(hist_df))
|
| 595 |
+
|
| 596 |
+
# Auto-refresh logic
|
| 597 |
+
if auto_refresh:
|
| 598 |
+
import time
|
| 599 |
+
time.sleep(10)
|
| 600 |
+
st.rerun()
|
| 601 |
|
| 602 |
+
if __name__ == "__main__":
|
| 603 |
+
main()
|