File size: 15,741 Bytes
1aa7fae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52992f2
 
8468016
 
52992f2
 
1aa7fae
 
52992f2
 
 
1aa7fae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52992f2
 
1aa7fae
52992f2
 
aeb81be
52992f2
aeb81be
52992f2
aeb81be
 
52992f2
 
 
 
 
 
 
1aa7fae
 
 
 
52992f2
704fdd8
52992f2
704fdd8
 
52992f2
704fdd8
 
 
 
 
 
 
 
1aa7fae
704fdd8
52992f2
 
 
 
 
 
1aa7fae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52992f2
 
 
 
 
 
 
 
 
 
 
 
 
1aa7fae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
"""
Enhanced Streamlit application for engine predictive maintenance.

Intended to be run locally or deployed as a Hugging Face Space.
Features an interactive, modern UI with real-time predictions,
visualizations, and detailed insights.
"""

from __future__ import annotations

import os
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
import streamlit as st

import config
from inference import predict_engine_condition


def _is_running_in_hf_space() -> bool:
    """Check if app is running in Hugging Face Space."""
    # HF Spaces set SPACE_ID or SYSTEM environment variable
    return os.getenv("SPACE_ID") is not None or os.getenv("SYSTEM") == "spaces"


def _get_default_source() -> str:
    """Decide whether to load the model from HF or local based on env vars."""
    # In HF Space, always use HF model
    if _is_running_in_hf_space():
        return "hf"
    if config.HF_TOKEN and config.HF_MODEL_REPO:
        return "hf"
    return "local"


def create_gauge_chart(value: float, title: str, color: str) -> go.Figure:
    """Create a gauge chart for sensor readings."""
    fig = go.Figure(go.Indicator(
        mode="gauge+number",
        value=value,
        domain={'x': [0, 1], 'y': [0, 1]},
        title={'text': title, 'font': {'size': 16}},
        gauge={
            'axis': {'range': [None, 100]},
            'bar': {'color': color},
            'steps': [
                {'range': [0, 50], 'color': "lightgray"},
                {'range': [50, 80], 'color': "gray"}
            ],
            'threshold': {
                'line': {'color': "red", 'width': 4},
                'thickness': 0.75,
                'value': 90
            }
        }
    ))
    fig.update_layout(height=200, margin=dict(l=20, r=20, t=40, b=20))
    return fig


def create_sensor_comparison_chart(sensor_data: dict) -> go.Figure:
    """Create a radar chart comparing sensor values."""
    categories = list(sensor_data.keys())
    values = list(sensor_data.values())
    
    # Normalize values for better visualization (0-100 scale)
    max_values = {
        "Engine_RPM": 4000,
        "Lub_Oil_Pressure": 10,
        "Fuel_Pressure": 30,
        "Coolant_Pressure": 10,
        "Lub_Oil_Temperature": 150,
        "Coolant_Temperature": 150,
    }
    
    normalized_values = [
        (v / max_values.get(k, 100)) * 100 for k, v in zip(categories, values)
    ]
    
    fig = go.Figure()
    
    fig.add_trace(go.Scatterpolar(
        r=normalized_values + [normalized_values[0]],  # Close the loop
        theta=[k.replace("_", " ") for k in categories] + [categories[0].replace("_", " ")],
        fill='toself',
        name='Current Readings',
        line_color='#1f77b4'
    ))
    
    fig.update_layout(
        polar=dict(
            radialaxis=dict(
                visible=True,
                range=[0, 100]
            )),
        showlegend=True,
        height=400,
        title="Sensor Readings Overview"
    )
    
    return fig


def main() -> None:
    # MUST be first Streamlit command
    st.set_page_config(
        page_title="Engine Predictive Maintenance",
        page_icon="πŸ› οΈ",
        layout="wide",
        initial_sidebar_state="expanded"
    )
    
    # Custom CSS for better styling
    st.markdown("""
    <style>
        .main-header {
            font-size: 2.5rem;
            font-weight: bold;
            color: #1f77b4;
            text-align: center;
            margin-bottom: 1rem;
        }
        .metric-card {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            padding: 1.5rem;
            border-radius: 10px;
            color: white;
            text-align: center;
            margin: 0.5rem 0;
        }
        .prediction-box {
            padding: 2rem;
            border-radius: 15px;
            margin: 1.5rem 0;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }
        .success-box {
            background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
            color: white;
        }
        .warning-box {
            background: linear-gradient(135deg, #ee0979 0%, #ff6a00 100%);
            color: white;
        }
        .stSlider > div > div > div {
            background-color: #1f77b4;
        }
        .stButton > button {
            width: 100%;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            font-weight: bold;
            font-size: 1.2rem;
            padding: 0.75rem;
            border-radius: 10px;
            border: none;
            transition: all 0.3s;
        }
        .stButton > button:hover {
            transform: scale(1.05);
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
        }
    </style>
    """, unsafe_allow_html=True)

    # Compact Header
    st.markdown('<h1 style="font-size: 2rem; text-align: center; color: #1f77b4; margin-bottom: 0.5rem;">πŸ› οΈ Engine Predictive Maintenance</h1>', unsafe_allow_html=True)
    st.markdown('<p style="text-align: center; color: #666; margin-bottom: 1rem; font-size: 0.9rem;">AI-Powered Engine Health Monitoring & Failure Prediction</p>', unsafe_allow_html=True)

    # Sidebar
    with st.sidebar:
        # In HF Space, always use HF model and hide selection
        is_in_space = _is_running_in_hf_space()
        default_source = _get_default_source()
        
        if is_in_space:
            # In Space: completely hide model source selection, always use HF
            source = "hf"
            # Don't show any configuration UI in Space
        else:
            # Local development: show configuration section
            st.header("βš™οΈ Configuration")
            source = st.radio(
                "πŸ“¦ Model Source:",
                options=["local", "hf"],
                index=0 if default_source == "hf" else 1,
                format_func=lambda x: "πŸ€– Hugging Face Hub" if x == "hf" else "πŸ’Ύ Local File",
                help="Select where to load the trained model from"
            )
        
        st.markdown("---")
        
        st.header("πŸ“Š Quick Stats")
        if is_in_space:
            # In Space: always show HF model status
            if config.HF_TOKEN and config.HF_MODEL_REPO:
                st.success("βœ… Model Ready")
                st.caption(f"Loading from: {config.HF_MODEL_REPO}")
            else:
                st.error("❌ Configuration Missing")
                st.caption("Set HF_TOKEN as Space secret in Settings")
                st.markdown("""
                **To fix:**
                1. Go to Space Settings
                2. Add secret: `HF_TOKEN`
                3. Restart Space
                """)
        else:
            # Local development: check local model
            if os.path.exists(config.BEST_MODEL_LOCAL_PATH):
                st.success("βœ… Model Available")
                st.caption("Trained model found locally")
            else:
                st.warning("⚠️ Model Not Found")
                st.caption("Run training script first")
        
        st.markdown("---")
        
        st.header("ℹ️ About")
        st.markdown("""
        This application uses machine learning to predict engine failures based on:
        - Engine RPM
        - Oil & Fuel Pressures
        - Coolant Pressure
        - Temperature Readings
        
        **Status**: 0 = Normal | 1 = Requires Maintenance
        """)
        
        st.markdown("---")
        st.caption("Built with ❀️ using Streamlit & Scikit-learn")

    # Balanced layout - inputs on left, larger visualization on right
    col_input, col_viz = st.columns([1, 1.2])
    
    with col_input:
        # Input form
        with st.form(key="engine_form", clear_on_submit=False):
            st.markdown("### πŸ”§ Sensor Inputs")
            
            # 2 columns for inputs
            col_a, col_b = st.columns(2)
            
            with col_a:
                engine_rpm = st.number_input(
                    "βš™οΈ Engine RPM",
                    min_value=0.0,
                    max_value=4000.0,
                    value=800.0,
                    step=10.0,
                    help="Revolutions per minute"
                )
                lub_oil_pressure = st.number_input(
                    "πŸ›’οΈ Lub Oil Pressure",
                    min_value=0.0,
                    max_value=10.0,
                    value=3.0,
                    step=0.1,
                    help="bar/kPa"
                )
                fuel_pressure = st.number_input(
                    "β›½ Fuel Pressure",
                    min_value=0.0,
                    max_value=30.0,
                    value=10.0,
                    step=0.1,
                    help="bar/kPa"
                )
            
            with col_b:
                coolant_pressure = st.number_input(
                    "πŸ’§ Coolant Pressure",
                    min_value=0.0,
                    max_value=10.0,
                    value=2.0,
                    step=0.1,
                    help="bar/kPa"
                )
                lub_oil_temp = st.number_input(
                    "🌑️ Lub Oil Temp",
                    min_value=0.0,
                    max_value=150.0,
                    value=80.0,
                    step=0.5,
                    help="Β°C"
                )
                coolant_temp = st.number_input(
                    "🌑️ Coolant Temp",
                    min_value=0.0,
                    max_value=150.0,
                    value=80.0,
                    step=0.5,
                    help="Β°C"
                )
            
            submitted = st.form_submit_button("πŸš€ Predict Engine Condition", use_container_width=True)
    
    with col_viz:
        st.markdown("### πŸ“Š Sensor Visualization")
        
        # Real-time sensor visualization
        sensor_data = {
            "Engine_RPM": engine_rpm,
            "Lub_Oil_Pressure": lub_oil_pressure,
            "Fuel_Pressure": fuel_pressure,
            "Coolant_Pressure": coolant_pressure,
            "Lub_Oil_Temperature": lub_oil_temp,
            "Coolant_Temperature": coolant_temp,
        }
        
        # Larger, more readable radar chart
        radar_fig = create_sensor_comparison_chart(sensor_data)
        radar_fig.update_layout(height=450, margin=dict(l=40, r=40, t=50, b=40))  # Larger and more readable
        st.plotly_chart(radar_fig, use_container_width=True, config={'displayModeBar': False})

    # Prediction results
    if submitted:
        inputs = {
            "Engine_RPM": engine_rpm,
            "Lub_Oil_Pressure": lub_oil_pressure,
            "Fuel_Pressure": fuel_pressure,
            "Coolant_Pressure": coolant_pressure,
            "Lub_Oil_Temperature": lub_oil_temp,
            "Coolant_Temperature": coolant_temp,
        }

        # Check if HF_TOKEN is set when using HF model
        if source == "hf" and not config.HF_TOKEN:
            st.error("❌ **HF_TOKEN not configured**")
            st.markdown("""
            **To fix this:**
            1. Go to Space Settings β†’ Repository secrets
            2. Add secret: `HF_TOKEN` with your Hugging Face token
            3. Restart the Space
            
            Get token from: https://huggingface.co/settings/tokens
            """)
            st.stop()
        
        with st.spinner("πŸ€– Loading model and analyzing sensor data..."):
            try:
                result = predict_engine_condition(inputs=inputs, source=source)
            except Exception as e:
                st.error(
                    f"❌ **Prediction Failed**\n\n"
                    f"Error: {str(e)}\n\n"
                    f"**Troubleshooting:**\n"
                    f"- Ensure the model is trained: `python src/train.py`\n"
                    f"- Check model file exists: `models/best_model.joblib`\n"
                    f"- Verify HF credentials if using Hugging Face Hub"
                )
                return

        pred_label = result["prediction"]
        prob_faulty = result["probability_faulty"]
        prob_normal = 1 - prob_faulty

        # Compact results section
        st.markdown("---")
        
        result_col1, result_col2 = st.columns([1.5, 1])
        
        with result_col1:
            if pred_label == 1:
                st.markdown(
                    f'<div class="prediction-box warning-box" style="padding: 1rem;">'
                    f'<h2 style="color: white; margin: 0; font-size: 1.5rem;">🚨 MAINTENANCE REQUIRED</h2>'
                    f'<p style="font-size: 1.1rem; margin: 0.5rem 0;">Engine is <b>LIKELY FAULTY</b> - Fault Probability: <b>{prob_faulty:.1%}</b></p>'
                    f'</div>',
                    unsafe_allow_html=True
                )
                with st.expander("πŸ”§ Recommended Actions", expanded=False):
                    st.markdown("""
                    - Schedule immediate engine inspection
                    - Verify sensor readings are accurate
                    - Review maintenance history
                    - Consult maintenance specialist
                    """)
            else:
                st.markdown(
                    f'<div class="prediction-box success-box" style="padding: 1rem;">'
                    f'<h2 style="color: white; margin: 0; font-size: 1.5rem;">βœ… ENGINE HEALTHY</h2>'
                    f'<p style="font-size: 1.1rem; margin: 0.5rem 0;">Engine is <b>OPERATING NORMALLY</b> - Fault Probability: <b>{prob_faulty:.1%}</b></p>'
                    f'</div>',
                    unsafe_allow_html=True
                )
                st.success("βœ… All sensors within normal ranges. Continue regular monitoring.")
        
        with result_col2:
            # Compact probability gauge
            fig = go.Figure(go.Indicator(
                mode="gauge+number",
                value=prob_faulty * 100,
                domain={'x': [0, 1], 'y': [0, 1]},
                title={'text': "Fault Risk %", 'font': {'size': 16}},
                gauge={
                    'axis': {'range': [None, 100]},
                    'bar': {'color': "darkred" if pred_label == 1 else "darkgreen"},
                    'steps': [
                        {'range': [0, 30], 'color': "lightgreen"},
                        {'range': [30, 70], 'color': "yellow"},
                        {'range': [70, 100], 'color': "lightcoral"}
                    ],
                    'threshold': {
                        'line': {'color': "red", 'width': 4},
                        'thickness': 0.75,
                        'value': 70
                    }
                }
            ))
            fig.update_layout(height=200, margin=dict(l=10, r=10, t=30, b=10))
            st.plotly_chart(fig, use_container_width=True, config={'displayModeBar': False})
            
            # Compact metrics
            col_m1, col_m2 = st.columns(2)
            with col_m1:
                st.metric("Normal", f"{prob_normal:.0%}")
            with col_m2:
                st.metric("Fault", f"{prob_faulty:.0%}")

    # Compact Footer
    st.markdown("---")
    st.markdown("""
    <div style='text-align: center; color: #666; padding: 0.5rem; font-size: 0.85rem;'>
        <p>πŸ› οΈ <b>Predictive Maintenance System</b> | Built with Streamlit, Scikit-learn & Plotly | Developed by <b>Anant Tripathi</b></p>
        <p style='font-size: 0.75rem; color: #888; margin-top: 0.25rem;'>⚠️ Use as decision-support tool, not replacement for expert diagnostics</p>
    </div>
    """, unsafe_allow_html=True)


if __name__ == "__main__":
    main()