deropxyz commited on
Commit
03bcd34
·
0 Parent(s):
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ models/*.joblib
2
+ **/__pycache__/
3
+
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Optional system packages for faster numpy/pandas builds
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ build-essential \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ WORKDIR /app
9
+
10
+ # Install Python dependencies first to leverage Docker layer caching
11
+ COPY api/requirements.txt ./requirements.txt
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy application code and model artifacts
15
+ COPY api ./api
16
+ COPY models ./models
17
+
18
+ # Spaces injects the listening port through the PORT env var
19
+ ENV PORT=7860
20
+
21
+ # Expose FastAPI via uvicorn
22
+ CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "7860"]
23
+
README.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Spaces Deployment
2
+
3
+ This folder contains everything required to deploy the Predictive Maintenance API to Hugging Face Spaces as a Docker Space.
4
+
5
+ ## Contents
6
+
7
+ - `api/`: FastAPI application (copied from `AC02-ML/api`)
8
+ - `requirements.txt`: Python dependencies (now includes `huggingface_hub`)
9
+ - `Dockerfile`: Container recipe used by Spaces
10
+
11
+ ## One-Time Setup
12
+
13
+ 1. Install the Hugging Face CLI and log in:
14
+ ```
15
+ pip install huggingface_hub
16
+ huggingface-cli login
17
+ ```
18
+ 2. (Optional) Enable Git LFS if the model artifacts exceed 10 MB:
19
+ ```
20
+ git lfs install
21
+ ```
22
+
23
+ ## Deploy Steps
24
+
25
+ 1. Initialize the Space repository (replace `<org>` and `<space>`):
26
+ ```
27
+ cd hf-space
28
+ git init
29
+ git remote add origin https://huggingface.co/spaces/<org>/<space>
30
+ ```
31
+ 2. Commit the deployment bundle:
32
+ ```
33
+ git add .
34
+ git commit -m "Initial Hugging Face Space"
35
+ git push origin main
36
+ ```
37
+ 3. In the Hugging Face UI:
38
+ - Set **Space SDK** to `Docker`.
39
+ - Pick hardware (`CPU Basic` is enough for CPU-only inference).
40
+ - Restart the Space if you tweak environment variables (e.g., secrets).
41
+
42
+ Once the build succeeds, the FastAPI docs are accessible at `https://<space>.hf.space/docs` and the REST endpoints follow the paths defined in `api/main.py`.
43
+
44
+ ### Hosting model artifacts
45
+
46
+ - Upload the serialized pipelines to the existing Hugging Face _model_ repo `deropxyz/AC02-ML`:
47
+ ```
48
+ hf upload --repo-id deropxyz/AC02-ML AC02-ML/model_pipeline_improved.joblib
49
+ hf upload --repo-id deropxyz/AC02-ML AC02-ML/model_pipeline.joblib
50
+ ```
51
+ - `api/main.py` downloads the artifacts at startup using `HF_MODEL_REPO`, `HF_PRIMARY_MODEL`, and `HF_FALLBACK_MODEL` env vars (defaults target `deropxyz/AC02-ML`). If the model repo is private, add `HF_TOKEN` in the Space settings (Variables & secrets).
52
+ - No large binaries are stored inside this repo; the runtime pulls them from the Hub cache.
53
+
54
+ ## Keeping the Space Updated
55
+
56
+ - Copy any new code from `AC02-ML` into this folder, commit, and push.
57
+ - When retraining, upload the new model artifact to the Hub model repo and update the env vars if filenames change.
api/README.md ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Predictive Maintenance FastAPI
2
+
3
+ ## 🚀 Quick Start
4
+
5
+ ### 1. Install Dependencies
6
+ ```bash
7
+ pip install -r requirements.txt
8
+ ```
9
+
10
+ ### 2. Run API Server
11
+ ```bash
12
+ # Development mode with auto-reload
13
+ python main.py
14
+
15
+ # Or using uvicorn directly
16
+ uvicorn main:app --reload --host 0.0.0.0 --port 8000
17
+ ```
18
+
19
+ ### 3. Access API
20
+ - **HTML Test Interface**: Open `index.html` in browser (recommended for testing)
21
+ - **Interactive Docs**: http://localhost:8000/docs
22
+ - **ReDoc**: http://localhost:8000/redoc
23
+ - **Health Check**: http://localhost:8000/health
24
+
25
+ ### 4. Quick Testing (Windows)
26
+ ```bash
27
+ # Install dependencies
28
+ install_requirements.bat
29
+
30
+ # Start server
31
+ start_server.bat
32
+
33
+ # Open test interface
34
+ open_test_interface.bat
35
+ ```
36
+
37
+ ---
38
+
39
+ ## 📡 API Endpoints
40
+
41
+ ### 1. Single Prediction: `POST /predict`
42
+
43
+ **Request Body:**
44
+ ```json
45
+ {
46
+ "machine_id": "M_L_01",
47
+ "air_temperature": 298.1,
48
+ "process_temperature": 308.6,
49
+ "rotational_speed": 1551,
50
+ "torque": 42.8,
51
+ "tool_wear": 0,
52
+ "type": "M"
53
+ }
54
+ ```
55
+
56
+ **Response (Enhanced with Features & Anomaly Detection):**
57
+ ```json
58
+ {
59
+ "machine_id": "M_L_01",
60
+ "timestamp": "2024-11-26T10:30:00.123456",
61
+ "prediction": "HEALTHY",
62
+ "confidence": 0.9234,
63
+ "diagnostics": {
64
+ "primary_cause": "Normal Operation",
65
+ "sensor_alert": "All parameters within optimal range. Health score: 100%.",
66
+ "recommended_action": "CONTINUE NORMAL OPERATION. Next scheduled maintenance as planned.",
67
+ "severity": "LOW"
68
+ },
69
+ "features": {
70
+ "Air_temp_K": 298.1,
71
+ "Process_temp_K": 308.6,
72
+ "Speed_rpm": 1551,
73
+ "Torque_Nm": 42.8,
74
+ "Tool_wear_min": 0,
75
+ "Tool_wear_hours": 0.0,
76
+ "Temperature_Diff": 10.5,
77
+ "Power_W": 6951.2,
78
+ "Type_Encoded": 1
79
+ },
80
+ "anomalies": [],
81
+ "overall_health": "🟢 EXCELLENT HEALTH - All systems operating optimally within normal parameters."
82
+ }
83
+ ```
84
+
85
+ ### 2. Batch Prediction (CSV Download): `POST /predict/batch`
86
+
87
+ **Request:**
88
+ - Upload CSV file (any size)
89
+ - **Flexible CSV Format** - accepts columns with:
90
+ - UPPERCASE/lowercase/MixedCase names
91
+ - Units in brackets: `air_temperature_[k]`, `Air Temperature [K]`, etc.
92
+ - Extra columns (will be ignored)
93
+ - Any column order
94
+
95
+ **Required Columns (flexible naming):**
96
+ - `air_temperature` or `air_temperature_[k]`
97
+ - `process_temperature` or `process_temperature_[k]`
98
+ - `rotational_speed` or `rotational_speed_[rpm]`
99
+ - `torque` or `torque_[nm]`
100
+ - `tool_wear` or `tool_wear_[min]`
101
+ - `type`
102
+ - Optional: `machine_id`
103
+
104
+ **Response:**
105
+ - CSV file download with added columns:
106
+ - `Prediction`: HEALTHY or FAILURE
107
+ - `Confidence`: 0.0 to 1.0
108
+ - `Primary_Cause`: Root cause analysis
109
+ - `Sensor_Alert`: Specific sensor readings
110
+ - `Recommended_Action`: Actionable maintenance advice
111
+ - `Severity`: LOW, MEDIUM, HIGH, CRITICAL
112
+ - `Processed_At`: Timestamp
113
+
114
+ ### 3. Batch Prediction (JSON for UI): `POST /predict/batch/json`
115
+
116
+ **Request:**
117
+ - Upload CSV file (max 500 rows)
118
+ - Same flexible format as `/predict/batch`
119
+
120
+ **Response:**
121
+ ```json
122
+ {
123
+ "summary": {
124
+ "total_records": 5,
125
+ "predictions": {
126
+ "failure": 2,
127
+ "healthy": 3,
128
+ "errors": 0
129
+ },
130
+ "failure_rate": 40.0
131
+ },
132
+ "results": [
133
+ {
134
+ "row_number": 1,
135
+ "machine_id": "M_001",
136
+ "prediction": "HEALTHY",
137
+ "confidence": 0.9234,
138
+ "diagnostics": {...},
139
+ "features": {...},
140
+ "anomalies": [...],
141
+ "overall_health": "🟢 EXCELLENT HEALTH",
142
+ "input_data": {...}
143
+ }
144
+ ],
145
+ "processed_at": "2024-11-26T10:30:00"
146
+ }
147
+ ```
148
+
149
+ ---
150
+
151
+ ## 🧠 Diagnostic Engine Rules
152
+
153
+ ### Priority 1: CRITICAL
154
+ - **Tool Wear > 200 min**: Immediate tool replacement required
155
+
156
+ ### Priority 2: HIGH
157
+ - **Power > 9000 W** or **Torque > 60 Nm**: Mechanical overload detected
158
+ - **ML Anomaly (Confidence > 80%)**: Unusual pattern detected
159
+
160
+ ### Priority 3: MEDIUM
161
+ - **Temperature Diff < 8.0 K at Speed > 1300 RPM**: Cooling system issue
162
+ - **Temperature Diff > 15.0 K**: Excessive thermal stress
163
+ - **Speed > 2500 RPM**: High-speed operation risk
164
+
165
+ ### Priority 4: LOW
166
+ - **All parameters normal**: Continue operation
167
+
168
+ ---
169
+
170
+ ## 🔬 Feature Engineering (Physics-Based)
171
+
172
+ The API automatically calculates:
173
+
174
+ 1. **Temperature_Diff** = `process_temperature` - `air_temperature`
175
+ - Indicates thermal stress
176
+ - Optimal range: 8-12 K
177
+
178
+ 2. **Power_W** = `torque` × `rotational_speed` × (2π / 60)
179
+ - Mechanical power output
180
+ - Safety limit: 9000 W
181
+
182
+ 3. **Type_Encoded** = {L: 0, M: 1, H: 2}
183
+ - Machine type categorization
184
+
185
+ ---
186
+
187
+ ## 📊 Example cURL Commands
188
+
189
+ ### Single Prediction
190
+ ```bash
191
+ curl -X POST "http://localhost:8000/predict" \
192
+ -H "Content-Type: application/json" \
193
+ -d '{
194
+ "machine_id": "M_L_01",
195
+ "air_temperature": 298.1,
196
+ "process_temperature": 308.6,
197
+ "rotational_speed": 1551,
198
+ "torque": 42.8,
199
+ "tool_wear": 0,
200
+ "type": "M"
201
+ }'
202
+ ```
203
+
204
+ ### Batch Prediction
205
+ ```bash
206
+ curl -X POST "http://localhost:8000/predict/batch" \
207
+ -F "file=@test_data.csv" \
208
+ -o predictions_output.csv
209
+ ```
210
+
211
+ ### Health Check
212
+ ```bash
213
+ curl http://localhost:8000/health
214
+ ```
215
+
216
+ ### Model Info
217
+ ```bash
218
+ curl http://localhost:8000/model/info
219
+ ```
220
+
221
+ ---
222
+
223
+ ## 🛠️ Python Client Example
224
+
225
+ ```python
226
+ import requests
227
+ import pandas as pd
228
+
229
+ # Single prediction
230
+ url = "http://localhost:8000/predict"
231
+ data = {
232
+ "machine_id": "M_L_01",
233
+ "air_temperature": 298.1,
234
+ "process_temperature": 308.6,
235
+ "rotational_speed": 1551,
236
+ "torque": 42.8,
237
+ "tool_wear": 0,
238
+ "type": "M"
239
+ }
240
+
241
+ response = requests.post(url, json=data)
242
+ result = response.json()
243
+ print(f"Prediction: {result['prediction']}")
244
+ print(f"Confidence: {result['confidence']}")
245
+ print(f"Action: {result['diagnostics']['recommended_action']}")
246
+
247
+ # Batch prediction
248
+ url_batch = "http://localhost:8000/predict/batch"
249
+ files = {'file': open('test_data.csv', 'rb')}
250
+ response = requests.post(url_batch, files=files)
251
+
252
+ with open('predictions.csv', 'wb') as f:
253
+ f.write(response.content)
254
+ ```
255
+
256
+ ---
257
+
258
+ ## 📂 CSV Format for Batch Processing (FLEXIBLE!)
259
+
260
+ ### ✅ Format 1: Simple (Recommended)
261
+ ```csv
262
+ machine_id,air_temperature,process_temperature,rotational_speed,torque,tool_wear,type
263
+ M_L_01,298.1,308.6,1551,42.8,0,M
264
+ M_L_02,299.2,310.1,1450,38.5,150,L
265
+ M_H_01,297.5,312.8,1800,65.2,220,H
266
+ ```
267
+
268
+ ### ✅ Format 2: With Units (Dataset Format)
269
+ ```csv
270
+ machine_id,air_temperature_[k],process_temperature_[k],rotational_speed_[rpm],torque_[nm],tool_wear_[min],type
271
+ M_L_01,298.1,308.6,1551,42.8,0,M
272
+ M_L_02,299.2,310.1,1450,38.5,150,L
273
+ ```
274
+
275
+ ### ✅ Format 3: Mixed Case with Spaces
276
+ ```csv
277
+ Machine ID,Air Temperature [K],Process Temperature [K],Rotational Speed [RPM],Torque [Nm],Tool Wear [min],TYPE
278
+ M_L_01,298.1,308.6,1551,42.8,0,M
279
+ ```
280
+
281
+ ### ✅ Format 4: Extra Columns (Will be Ignored)
282
+ ```csv
283
+ machine_id,timestamp,air_temperature,process_temperature,rotational_speed,torque,tool_wear,type,location,operator
284
+ M_L_01,2024-11-26,298.1,308.6,1551,42.8,0,M,Factory_A,John
285
+ ```
286
+
287
+ **All formats above are automatically handled by the API!**
288
+
289
+ **Output CSV** (additional columns):
290
+ ```csv
291
+ ...,Prediction,Confidence,Primary_Cause,Sensor_Alert,Recommended_Action,Severity,Processed_At
292
+ ...,HEALTHY,0.9234,Normal Operation,All parameters within optimal range...,CONTINUE NORMAL OPERATION...,LOW,2024-11-26T10:30:00
293
+ ...,HEALTHY,0.8876,Normal Operation,All parameters within optimal range...,CONTINUE NORMAL OPERATION...,LOW,2024-11-26T10:30:01
294
+ ...,FAILURE,0.9567,Tool End of Life,Tool wear (220 min) exceeds safety threshold...,IMMEDIATE ACTION: Replace cutting tool...,CRITICAL,2024-11-26T10:30:02
295
+ ```
296
+
297
+ ---
298
+
299
+ ## 🔐 Production Deployment
300
+
301
+ ### Environment Variables
302
+ ```bash
303
+ export MODEL_PATH="/path/to/model_pipeline.joblib"
304
+ export API_HOST="0.0.0.0"
305
+ export API_PORT="8000"
306
+ export LOG_LEVEL="info"
307
+ ```
308
+
309
+ ### Docker Deployment
310
+ ```dockerfile
311
+ FROM python:3.11-slim
312
+
313
+ WORKDIR /app
314
+ COPY requirements.txt .
315
+ RUN pip install --no-cache-dir -r requirements.txt
316
+
317
+ COPY . .
318
+ EXPOSE 8000
319
+
320
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
321
+ ```
322
+
323
+ ### Run with Gunicorn (Production)
324
+ ```bash
325
+ pip install gunicorn
326
+ gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
327
+ ```
328
+
329
+ ---
330
+
331
+ ## 📈 Performance Metrics
332
+
333
+ - **Model**: RandomForest Classifier (Optimized with Ensemble)
334
+ - **Features**: 9 engineered features (including Tool_wear_hours)
335
+ - **F1-Score**: ~75-85% (after optimization)
336
+ - **Inference Time**: ~10-20ms per prediction
337
+ - **Batch Processing**:
338
+ - CSV Download: Unlimited rows
339
+ - JSON Response: Max 500 rows (for UI display)
340
+ - Throughput: ~100-200 records/second
341
+ - **Anomaly Detection**: 7 parameter checks (Real-time)
342
+ - **Overall Health Assessment**: 4 levels (EXCELLENT/GOOD/FAIR/POOR)
343
+
344
+ ---
345
+
346
+ ## 🐛 Troubleshooting
347
+
348
+ ### Model Not Loading
349
+ ```bash
350
+ # Check if model file exists
351
+ ls ../models/model_pipeline_improved.joblib
352
+ ls ../models/model_pipeline.joblib
353
+
354
+ # Verify model structure
355
+ python -c "import joblib; m = joblib.load('../models/model_pipeline.joblib'); print(m.keys())"
356
+ ```
357
+
358
+ ### CORS Issues
359
+ - Already configured for `allow_origins=["*"]`
360
+ - Modify in `main.py` if needed for specific domains
361
+
362
+ ### Port Already in Use
363
+ ```bash
364
+ # Change port in main.py or use:
365
+ uvicorn main:app --port 8001
366
+ ```
367
+
368
+ ---
369
+
370
+ ## 📞 API Support
371
+
372
+ For issues or questions:
373
+ 1. Check `/health` endpoint for model status
374
+ 2. Review logs for detailed error messages
375
+ 3. Validate input data matches schema in `/docs`
376
+
377
+ ---
378
+
379
+ **Built with FastAPI 🚀 | Production-Ready | ML + Physics-Based Diagnostics**
api/index.html ADDED
@@ -0,0 +1,1155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Predictive Maintenance API - Test Interface</title>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
+ min-height: 100vh;
19
+ padding: 20px;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1200px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ .header {
28
+ text-align: center;
29
+ color: white;
30
+ margin-bottom: 30px;
31
+ }
32
+
33
+ .header h1 {
34
+ font-size: 2.5em;
35
+ margin-bottom: 10px;
36
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
37
+ }
38
+
39
+ .header p {
40
+ font-size: 1.1em;
41
+ opacity: 0.9;
42
+ }
43
+
44
+ .tabs {
45
+ display: flex;
46
+ gap: 10px;
47
+ margin-bottom: 20px;
48
+ }
49
+
50
+ .tab-button {
51
+ flex: 1;
52
+ padding: 15px;
53
+ background: white;
54
+ border: none;
55
+ border-radius: 10px 10px 0 0;
56
+ font-size: 1.1em;
57
+ font-weight: 600;
58
+ cursor: pointer;
59
+ transition: all 0.3s;
60
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
61
+ }
62
+
63
+ .tab-button:hover {
64
+ transform: translateY(-2px);
65
+ box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
66
+ }
67
+
68
+ .tab-button.active {
69
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
70
+ color: white;
71
+ }
72
+
73
+ .tab-content {
74
+ display: none;
75
+ background: white;
76
+ border-radius: 0 0 10px 10px;
77
+ padding: 30px;
78
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
79
+ }
80
+
81
+ .tab-content.active {
82
+ display: block;
83
+ animation: fadeIn 0.3s;
84
+ }
85
+
86
+ @keyframes fadeIn {
87
+ from {
88
+ opacity: 0;
89
+ transform: translateY(10px);
90
+ }
91
+
92
+ to {
93
+ opacity: 1;
94
+ transform: translateY(0);
95
+ }
96
+ }
97
+
98
+ .form-grid {
99
+ display: grid;
100
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
101
+ gap: 20px;
102
+ margin-bottom: 20px;
103
+ }
104
+
105
+ .form-group {
106
+ display: flex;
107
+ flex-direction: column;
108
+ }
109
+
110
+ label {
111
+ font-weight: 600;
112
+ margin-bottom: 8px;
113
+ color: #333;
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 5px;
117
+ }
118
+
119
+ .info-icon {
120
+ font-size: 0.9em;
121
+ color: #667eea;
122
+ cursor: help;
123
+ }
124
+
125
+ input,
126
+ select {
127
+ padding: 12px;
128
+ border: 2px solid #e0e0e0;
129
+ border-radius: 8px;
130
+ font-size: 1em;
131
+ transition: all 0.3s;
132
+ }
133
+
134
+ input:focus,
135
+ select:focus {
136
+ outline: none;
137
+ border-color: #667eea;
138
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
139
+ }
140
+
141
+ .button-group {
142
+ display: flex;
143
+ gap: 15px;
144
+ margin-top: 20px;
145
+ }
146
+
147
+ button {
148
+ flex: 1;
149
+ padding: 15px 30px;
150
+ font-size: 1.1em;
151
+ font-weight: 600;
152
+ border: none;
153
+ border-radius: 8px;
154
+ cursor: pointer;
155
+ transition: all 0.3s;
156
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
157
+ }
158
+
159
+ .btn-primary {
160
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
161
+ color: white;
162
+ }
163
+
164
+ .btn-primary:hover {
165
+ transform: translateY(-2px);
166
+ box-shadow: 0 6px 12px rgba(102, 126, 234, 0.4);
167
+ }
168
+
169
+ .btn-secondary {
170
+ background: #f0f0f0;
171
+ color: #333;
172
+ }
173
+
174
+ .btn-secondary:hover {
175
+ background: #e0e0e0;
176
+ }
177
+
178
+ .result-container {
179
+ margin-top: 30px;
180
+ padding: 25px;
181
+ border-radius: 10px;
182
+ display: none;
183
+ animation: slideIn 0.4s;
184
+ }
185
+
186
+ @keyframes slideIn {
187
+ from {
188
+ opacity: 0;
189
+ transform: translateX(-20px);
190
+ }
191
+
192
+ to {
193
+ opacity: 1;
194
+ transform: translateX(0);
195
+ }
196
+ }
197
+
198
+ .result-container.success {
199
+ background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
200
+ border: 2px solid #667eea;
201
+ display: block;
202
+ }
203
+
204
+ .result-container.error {
205
+ background: #fff0f0;
206
+ border: 2px solid #ff4444;
207
+ display: block;
208
+ }
209
+
210
+ .prediction-header {
211
+ display: flex;
212
+ align-items: center;
213
+ gap: 15px;
214
+ margin-bottom: 20px;
215
+ padding-bottom: 15px;
216
+ border-bottom: 2px solid #e0e0e0;
217
+ }
218
+
219
+ .prediction-badge {
220
+ padding: 10px 20px;
221
+ border-radius: 25px;
222
+ font-weight: 700;
223
+ font-size: 1.2em;
224
+ text-transform: uppercase;
225
+ }
226
+
227
+ .badge-healthy {
228
+ background: #4caf50;
229
+ color: white;
230
+ }
231
+
232
+ .badge-failure {
233
+ background: #ff4444;
234
+ color: white;
235
+ }
236
+
237
+ .confidence-bar {
238
+ flex: 1;
239
+ height: 30px;
240
+ background: #e0e0e0;
241
+ border-radius: 15px;
242
+ overflow: hidden;
243
+ position: relative;
244
+ }
245
+
246
+ .confidence-fill {
247
+ height: 100%;
248
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: center;
252
+ color: white;
253
+ font-weight: 600;
254
+ transition: width 0.5s ease;
255
+ }
256
+
257
+ .diagnostics-grid {
258
+ display: grid;
259
+ gap: 15px;
260
+ }
261
+
262
+ .diagnostic-item {
263
+ padding: 15px;
264
+ background: white;
265
+ border-radius: 8px;
266
+ border-left: 4px solid #667eea;
267
+ }
268
+
269
+ .diagnostic-label {
270
+ font-weight: 600;
271
+ color: #667eea;
272
+ margin-bottom: 5px;
273
+ text-transform: uppercase;
274
+ font-size: 0.9em;
275
+ }
276
+
277
+ .diagnostic-value {
278
+ color: #333;
279
+ line-height: 1.6;
280
+ }
281
+
282
+ .severity-badge {
283
+ display: inline-block;
284
+ padding: 5px 15px;
285
+ border-radius: 15px;
286
+ font-weight: 600;
287
+ font-size: 0.9em;
288
+ }
289
+
290
+ .severity-low {
291
+ background: #4caf50;
292
+ color: white;
293
+ }
294
+
295
+ .severity-medium {
296
+ background: #ff9800;
297
+ color: white;
298
+ }
299
+
300
+ .severity-high {
301
+ background: #ff5722;
302
+ color: white;
303
+ }
304
+
305
+ .severity-critical {
306
+ background: #f44336;
307
+ color: white;
308
+ animation: pulse 1.5s infinite;
309
+ }
310
+
311
+ @keyframes pulse {
312
+
313
+ 0%,
314
+ 100% {
315
+ opacity: 1;
316
+ }
317
+
318
+ 50% {
319
+ opacity: 0.7;
320
+ }
321
+ }
322
+
323
+ .upload-area {
324
+ border: 3px dashed #667eea;
325
+ border-radius: 10px;
326
+ padding: 40px;
327
+ text-align: center;
328
+ cursor: pointer;
329
+ transition: all 0.3s;
330
+ background: #f8f9ff;
331
+ }
332
+
333
+ .upload-area:hover {
334
+ background: #eef1ff;
335
+ border-color: #764ba2;
336
+ }
337
+
338
+ .upload-area.dragging {
339
+ background: #e0e7ff;
340
+ border-color: #4f46e5;
341
+ }
342
+
343
+ .upload-icon {
344
+ font-size: 3em;
345
+ color: #667eea;
346
+ margin-bottom: 15px;
347
+ }
348
+
349
+ .file-name {
350
+ margin-top: 15px;
351
+ padding: 10px;
352
+ background: white;
353
+ border-radius: 8px;
354
+ font-weight: 600;
355
+ color: #667eea;
356
+ }
357
+
358
+ .loading {
359
+ display: none;
360
+ text-align: center;
361
+ padding: 20px;
362
+ }
363
+
364
+ .loading.active {
365
+ display: block;
366
+ }
367
+
368
+ .spinner {
369
+ border: 4px solid #f3f3f3;
370
+ border-top: 4px solid #667eea;
371
+ border-radius: 50%;
372
+ width: 50px;
373
+ height: 50px;
374
+ animation: spin 1s linear infinite;
375
+ margin: 0 auto 15px;
376
+ }
377
+
378
+ @keyframes spin {
379
+ 0% {
380
+ transform: rotate(0deg);
381
+ }
382
+
383
+ 100% {
384
+ transform: rotate(360deg);
385
+ }
386
+ }
387
+
388
+ .preset-buttons {
389
+ display: flex;
390
+ gap: 10px;
391
+ margin-bottom: 20px;
392
+ flex-wrap: wrap;
393
+ }
394
+
395
+ .preset-btn {
396
+ padding: 8px 16px;
397
+ background: #f0f0f0;
398
+ border: 2px solid #667eea;
399
+ border-radius: 20px;
400
+ cursor: pointer;
401
+ transition: all 0.3s;
402
+ font-size: 0.9em;
403
+ font-weight: 600;
404
+ color: #667eea;
405
+ }
406
+
407
+ .preset-btn:hover {
408
+ background: #667eea;
409
+ color: white;
410
+ }
411
+
412
+ .api-status {
413
+ position: fixed;
414
+ top: 20px;
415
+ right: 20px;
416
+ padding: 10px 20px;
417
+ background: white;
418
+ border-radius: 25px;
419
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
420
+ display: flex;
421
+ align-items: center;
422
+ gap: 10px;
423
+ font-weight: 600;
424
+ }
425
+
426
+ .status-dot {
427
+ width: 12px;
428
+ height: 12px;
429
+ border-radius: 50%;
430
+ animation: blink 2s infinite;
431
+ }
432
+
433
+ .status-online {
434
+ background: #4caf50;
435
+ }
436
+
437
+ .status-offline {
438
+ background: #ff4444;
439
+ }
440
+
441
+ @keyframes blink {
442
+
443
+ 0%,
444
+ 100% {
445
+ opacity: 1;
446
+ }
447
+
448
+ 50% {
449
+ opacity: 0.5;
450
+ }
451
+ }
452
+
453
+ @media (max-width: 768px) {
454
+ .form-grid {
455
+ grid-template-columns: 1fr;
456
+ }
457
+
458
+ .button-group {
459
+ flex-direction: column;
460
+ }
461
+
462
+ .header h1 {
463
+ font-size: 1.8em;
464
+ }
465
+ }
466
+ </style>
467
+ </head>
468
+
469
+ <body>
470
+ <div class="api-status" id="apiStatus">
471
+ <div class="status-dot status-offline" id="statusDot"></div>
472
+ <span id="statusText">Checking...</span>
473
+ </div>
474
+
475
+ <div class="container">
476
+ <div class="header">
477
+ <h1>🔧 Predictive Maintenance</h1>
478
+ <p>ML-Powered Equipment Failure Prediction & Diagnostics</p>
479
+ </div>
480
+
481
+ <div class="tabs">
482
+ <button class="tab-button active" onclick="switchTab('single')">
483
+ 📊 Single Prediction
484
+ </button>
485
+ <button class="tab-button" onclick="switchTab('batch')">
486
+ 📁 Batch Processing
487
+ </button>
488
+ </div>
489
+
490
+ <!-- SINGLE PREDICTION TAB -->
491
+ <div id="singleTab" class="tab-content active">
492
+ <h2 style="margin-bottom: 20px; color: #333;">🎯 Single Machine Prediction</h2>
493
+
494
+ <div class="preset-buttons">
495
+ <button class="preset-btn" onclick="loadPreset('healthy')">✅ Healthy Machine</button>
496
+ <button class="preset-btn" onclick="loadPreset('toolwear')">🔨 Tool Wear Critical</button>
497
+ <button class="preset-btn" onclick="loadPreset('overstrain')">⚡ Power Overstrain</button>
498
+ <button class="preset-btn" onclick="loadPreset('cooling')">❄️ Cooling Issue</button>
499
+ </div>
500
+
501
+ <form id="predictionForm" onsubmit="handleSinglePredict(event)">
502
+ <div class="form-grid">
503
+ <div class="form-group">
504
+ <label for="machine_id">
505
+ Machine ID <span class="info-icon" title="Optional identifier">ℹ️</span>
506
+ </label>
507
+ <input type="text" id="machine_id" placeholder="e.g., M_L_01">
508
+ </div>
509
+
510
+ <div class="form-group">
511
+ <label for="type">
512
+ Machine Type <span class="info-icon" title="L=Low, M=Medium, H=High">ℹ️</span>
513
+ </label>
514
+ <select id="type" required>
515
+ <option value="">Select Type</option>
516
+ <option value="L">L - Low Quality</option>
517
+ <option value="M">M - Medium Quality</option>
518
+ <option value="H">H - High Quality</option>
519
+ </select>
520
+ </div>
521
+
522
+ <div class="form-group">
523
+ <label for="air_temperature">
524
+ Air Temperature (K) <span class="info-icon" title="Range: 250-350 K">ℹ️</span>
525
+ </label>
526
+ <input type="number" id="air_temperature" step="0.1" min="250" max="350" required
527
+ placeholder="e.g., 298.1">
528
+ </div>
529
+
530
+ <div class="form-group">
531
+ <label for="process_temperature">
532
+ Process Temperature (K) <span class="info-icon" title="Range: 250-400 K">ℹ️</span>
533
+ </label>
534
+ <input type="number" id="process_temperature" step="0.1" min="250" max="400" required
535
+ placeholder="e.g., 308.6">
536
+ </div>
537
+
538
+ <div class="form-group">
539
+ <label for="rotational_speed">
540
+ Rotational Speed (RPM) <span class="info-icon" title="Range: 0-3000 RPM">ℹ️</span>
541
+ </label>
542
+ <input type="number" id="rotational_speed" min="0" max="3000" required placeholder="e.g., 1551">
543
+ </div>
544
+
545
+ <div class="form-group">
546
+ <label for="torque">
547
+ Torque (Nm) <span class="info-icon" title="Range: 0-100 Nm">ℹ️</span>
548
+ </label>
549
+ <input type="number" id="torque" step="0.1" min="0" max="100" required placeholder="e.g., 42.8">
550
+ </div>
551
+
552
+ <div class="form-group">
553
+ <label for="tool_wear">
554
+ Tool Wear (min) <span class="info-icon" title="Range: 0-300 minutes">ℹ️</span>
555
+ </label>
556
+ <input type="number" id="tool_wear" min="0" max="300" required placeholder="e.g., 0">
557
+ </div>
558
+ </div>
559
+
560
+ <div class="button-group">
561
+ <button type="submit" class="btn-primary">🔍 Analyze & Predict</button>
562
+ <button type="button" class="btn-secondary" onclick="resetForm()">🔄 Reset Form</button>
563
+ </div>
564
+ </form>
565
+
566
+ <div class="loading" id="loading">
567
+ <div class="spinner"></div>
568
+ <p>Analyzing machine data...</p>
569
+ </div>
570
+
571
+ <div id="result" class="result-container"></div>
572
+ </div>
573
+
574
+ <!-- BATCH PREDICTION TAB -->
575
+ <div id="batchTab" class="tab-content">
576
+ <h2 style="margin-bottom: 20px; color: #333;">📁 Batch Processing</h2>
577
+
578
+ <div class="upload-area" id="uploadArea" onclick="document.getElementById('csvFile').click()">
579
+ <div class="upload-icon">📤</div>
580
+ <h3>Click or Drag & Drop CSV File</h3>
581
+ <p style="margin-top: 10px; color: #666;">Upload a CSV file with machine data for batch prediction</p>
582
+ <input type="file" id="csvFile" accept=".csv" style="display: none;" onchange="handleFileSelect(event)">
583
+ <div id="fileName" class="file-name" style="display: none;"></div>
584
+ </div>
585
+
586
+ <div style="margin-top: 20px; padding: 20px; background: #f8f9ff; border-radius: 10px;">
587
+ <h4 style="margin-bottom: 10px;">📋 Required CSV Columns:</h4>
588
+ <ul style="list-style: none; padding-left: 0;">
589
+ <li>✓ <code>air_temperature</code> - Air temperature in Kelvin</li>
590
+ <li>✓ <code>process_temperature</code> - Process temperature in Kelvin</li>
591
+ <li>✓ <code>rotational_speed</code> - Rotational speed in RPM</li>
592
+ <li>✓ <code>torque</code> - Torque in Nm</li>
593
+ <li>✓ <code>tool_wear</code> - Tool wear in minutes</li>
594
+ <li>✓ <code>type</code> - Machine type (L, M, or H)</li>
595
+ <li>⭕ <code>machine_id</code> - Optional machine identifier</li>
596
+ </ul>
597
+ </div>
598
+
599
+ <div class="button-group" style="margin-top: 20px;">
600
+ <button class="btn-primary" onclick="uploadBatch()" id="uploadBtn" disabled>📊 Process Batch</button>
601
+ <button class="btn-secondary" onclick="downloadTemplate()">⬇️ Download Template CSV</button>
602
+ </div>
603
+
604
+ <div class="loading" id="batchLoading">
605
+ <div class="spinner"></div>
606
+ <p>Processing batch predictions...</p>
607
+ </div>
608
+
609
+ <div id="batchResult" class="result-container"></div>
610
+ </div>
611
+ </div>
612
+
613
+ <script>
614
+ const API_URL = 'http://localhost:8000';
615
+ let selectedFile = null;
616
+
617
+ // Check API status on load
618
+ async function checkApiStatus() {
619
+ try {
620
+ const response = await fetch(`${API_URL}/health`);
621
+ const data = await response.json();
622
+ document.getElementById('statusDot').className = 'status-dot status-online';
623
+ document.getElementById('statusText').textContent = 'API Online';
624
+ } catch (error) {
625
+ document.getElementById('statusDot').className = 'status-dot status-offline';
626
+ document.getElementById('statusText').textContent = 'API Offline';
627
+ }
628
+ }
629
+
630
+ // Switch between tabs
631
+ function switchTab(tab) {
632
+ document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
633
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
634
+
635
+ if (tab === 'single') {
636
+ document.querySelector('.tab-button:nth-child(1)').classList.add('active');
637
+ document.getElementById('singleTab').classList.add('active');
638
+ } else {
639
+ document.querySelector('.tab-button:nth-child(2)').classList.add('active');
640
+ document.getElementById('batchTab').classList.add('active');
641
+ }
642
+ }
643
+
644
+ // Load preset values
645
+ function loadPreset(type) {
646
+ const presets = {
647
+ healthy: {
648
+ machine_id: 'M_L_01',
649
+ type: 'M',
650
+ air_temperature: 298.1,
651
+ process_temperature: 308.6,
652
+ rotational_speed: 1551,
653
+ torque: 42.8,
654
+ tool_wear: 0
655
+ },
656
+ toolwear: {
657
+ machine_id: 'M_H_03',
658
+ type: 'H',
659
+ air_temperature: 297.5,
660
+ process_temperature: 312.8,
661
+ rotational_speed: 1800,
662
+ torque: 52.0,
663
+ tool_wear: 220
664
+ },
665
+ overstrain: {
666
+ machine_id: 'M_H_05',
667
+ type: 'H',
668
+ air_temperature: 298.5,
669
+ process_temperature: 316.0,
670
+ rotational_speed: 1900,
671
+ torque: 72.5,
672
+ tool_wear: 150
673
+ },
674
+ cooling: {
675
+ machine_id: 'M_M_02',
676
+ type: 'M',
677
+ air_temperature: 299.8,
678
+ process_temperature: 305.2,
679
+ rotational_speed: 1650,
680
+ torque: 45.0,
681
+ tool_wear: 100
682
+ }
683
+ };
684
+
685
+ const preset = presets[type];
686
+ Object.keys(preset).forEach(key => {
687
+ const element = document.getElementById(key);
688
+ if (element) element.value = preset[key];
689
+ });
690
+ }
691
+
692
+ // Handle single prediction
693
+ async function handleSinglePredict(event) {
694
+ event.preventDefault();
695
+
696
+ const formData = {
697
+ machine_id: document.getElementById('machine_id').value || null,
698
+ type: document.getElementById('type').value,
699
+ air_temperature: parseFloat(document.getElementById('air_temperature').value),
700
+ process_temperature: parseFloat(document.getElementById('process_temperature').value),
701
+ rotational_speed: parseInt(document.getElementById('rotational_speed').value),
702
+ torque: parseFloat(document.getElementById('torque').value),
703
+ tool_wear: parseInt(document.getElementById('tool_wear').value)
704
+ };
705
+
706
+ document.getElementById('loading').classList.add('active');
707
+ document.getElementById('result').style.display = 'none';
708
+
709
+ try {
710
+ console.log('Sending request to API:', formData); // Debug log
711
+
712
+ const response = await fetch(`${API_URL}/predict`, {
713
+ method: 'POST',
714
+ headers: { 'Content-Type': 'application/json' },
715
+ body: JSON.stringify(formData)
716
+ });
717
+
718
+ console.log('Response status:', response.status); // Debug log
719
+
720
+ const data = await response.json();
721
+ console.log('Response data:', data); // Debug log
722
+
723
+ if (!response.ok) {
724
+ throw new Error(data.detail || 'Prediction failed');
725
+ }
726
+
727
+ displayResult(data);
728
+ } catch (error) {
729
+ console.error('Error during prediction:', error); // Debug log
730
+ displayError(error.message);
731
+ } finally {
732
+ document.getElementById('loading').classList.remove('active');
733
+ }
734
+ }
735
+
736
+ // Display prediction result
737
+ function displayResult(data) {
738
+ console.log('Displaying result:', data); // Debug log
739
+
740
+ const resultDiv = document.getElementById('result');
741
+ const isFailure = data.prediction === 'FAILURE';
742
+ const severityClass = `severity-${data.diagnostics.severity.toLowerCase()}`;
743
+
744
+ // Calculate probability/confidence percentage
745
+ const probability = (data.confidence * 100).toFixed(1);
746
+
747
+ resultDiv.className = 'result-container success';
748
+ resultDiv.style.display = 'block'; // FORCE DISPLAY
749
+ resultDiv.innerHTML = `
750
+ <h3 style="margin-bottom: 15px;">🔍 Prediction Result</h3>
751
+
752
+ <!-- Prediction Badge and Confidence Bar -->
753
+ <div class="prediction-header">
754
+ <span class="prediction-badge ${isFailure ? 'badge-failure' : 'badge-healthy'}">
755
+ ${data.prediction}
756
+ </span>
757
+ <div class="confidence-bar">
758
+ <div class="confidence-fill" style="width: ${probability}%">
759
+ ${probability}% Confidence
760
+ </div>
761
+ </div>
762
+ </div>
763
+
764
+ <!-- Severity and Risk Level -->
765
+ <div style="display: flex; gap: 10px; margin: 15px 0;">
766
+ <span class="severity-badge ${severityClass}" style="font-size: 14px; padding: 8px 16px;">
767
+ ${data.diagnostics.severity} SEVERITY
768
+ </span>
769
+ </div>
770
+
771
+ <!-- Main Diagnostics Grid -->
772
+ <div class="diagnostics-grid">
773
+ <div class="diagnostic-item">
774
+ <div class="diagnostic-label">🎯 Primary Cause</div>
775
+ <div class="diagnostic-value">${data.diagnostics.primary_cause}</div>
776
+ </div>
777
+
778
+ <div class="diagnostic-item">
779
+ <div class="diagnostic-label">📊 Sensor Alert</div>
780
+ <div class="diagnostic-value">${data.diagnostics.sensor_alert}</div>
781
+ </div>
782
+
783
+ <div class="diagnostic-item">
784
+ <div class="diagnostic-label">🔧 Recommended Action</div>
785
+ <div class="diagnostic-value">${data.diagnostics.recommended_action}</div>
786
+ </div>
787
+
788
+ ${data.machine_id ? `
789
+ <div class="diagnostic-item">
790
+ <div class="diagnostic-label">🏷️ Machine ID</div>
791
+ <div class="diagnostic-value">${data.machine_id}</div>
792
+ </div>
793
+ ` : ''}
794
+
795
+ <div class="diagnostic-item">
796
+ <div class="diagnostic-label">⏰ Timestamp</div>
797
+ <div class="diagnostic-value">${new Date(data.timestamp).toLocaleString()}</div>
798
+ </div>
799
+ </div>
800
+
801
+ <!-- Engineered Features Section -->
802
+ ${data.features && Object.keys(data.features).length > 0 ? `
803
+ <div style="margin-top: 25px;">
804
+ <h4 style="margin-bottom: 15px; color: #667eea;">📊 Engineered Features Analysis</h4>
805
+ <div class="feature-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
806
+ ${Object.entries(data.features).map(([key, value]) => `
807
+ <div class="feature-item" style="background: white; padding: 12px; border-radius: 8px; border-left: 3px solid #667eea;">
808
+ <div class="feature-label" style="font-size: 11px; color: #6c757d; margin-bottom: 4px; text-transform: uppercase;">
809
+ ${key.replace(/_/g, ' ')}
810
+ </div>
811
+ <div class="feature-value" style="font-size: 16px; font-weight: 600; color: #333;">
812
+ ${typeof value === 'number' ? value.toFixed(2) : value}
813
+ </div>
814
+ </div>
815
+ `).join('')}
816
+ </div>
817
+ </div>
818
+ ` : ''}
819
+
820
+ <!-- Anomaly Detection Section (if available) -->
821
+ ${data.anomalies && data.anomalies.length > 0 ? `
822
+ <div style="margin-top: 25px; padding: 20px; background: #f8f9fa; border-radius: 10px; border-left: 5px solid #667eea;">
823
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
824
+ <h4 style="margin: 0; color: #667eea;">🔧 Parameter Anomaly Analysis</h4>
825
+ <span style="color: #667eea; font-weight: 600; font-size: 14px;">
826
+ ${data.anomalies.length} anomalies detected
827
+ </span>
828
+ </div>
829
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px;">
830
+ ${data.anomalies.map(anomaly => `
831
+ <div style="background: white; padding: 15px; border-radius: 10px; border-left: 4px solid ${anomaly.status === 'CRITICAL' ? '#dc3545' : anomaly.status === 'WARNING' ? '#ffc107' : '#28a745'};">
832
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
833
+ <strong style="font-size: 16px;">${anomaly.parameter}</strong>
834
+ <span style="font-size: 12px; padding: 4px 8px; border-radius: 10px; font-weight: 600; background: ${anomaly.status === 'CRITICAL' ? '#f8d7da' : anomaly.status === 'WARNING' ? '#fff3cd' : '#d4edda'}; color: ${anomaly.status === 'CRITICAL' ? '#721c24' : anomaly.status === 'WARNING' ? '#856404' : '#155724'};">
835
+ ${anomaly.status}
836
+ </span>
837
+ </div>
838
+ <div style="font-size: 18px; font-weight: 600; color: #333; margin: 5px 0;">
839
+ Current: ${anomaly.value}
840
+ </div>
841
+ <div style="font-size: 12px; color: #6c757d; margin: 5px 0;">
842
+ Normal range: ${anomaly.normal_range}
843
+ </div>
844
+ <div style="font-size: 13px; color: #495057; margin-top: 10px; padding: 8px; background: #f1f3f4; border-radius: 6px;">
845
+ ${anomaly.explanation}
846
+ </div>
847
+ </div>
848
+ `).join('')}
849
+ </div>
850
+ </div>
851
+ ` : ''}
852
+
853
+ <!-- Overall Health Summary (if available) -->
854
+ ${data.overall_health ? `
855
+ <div style="margin-top: 20px; padding: 15px; border-radius: 10px; font-weight: 600; text-align: center; background: ${data.overall_health.includes('EXCELLENT') ? '#d4edda' : data.overall_health.includes('GOOD') ? '#d1ecf1' : data.overall_health.includes('FAIR') ? '#fff3cd' : '#f8d7da'}; color: ${data.overall_health.includes('EXCELLENT') ? '#155724' : data.overall_health.includes('GOOD') ? '#0c5460' : data.overall_health.includes('FAIR') ? '#856404' : '#721c24'}; border: 2px solid ${data.overall_health.includes('EXCELLENT') ? '#28a745' : data.overall_health.includes('GOOD') ? '#17a2b8' : data.overall_health.includes('FAIR') ? '#ffc107' : '#dc3545'};">
856
+ 🏥 ${data.overall_health}
857
+ </div>
858
+ ` : ''}
859
+ `;
860
+ }
861
+
862
+ // Display error
863
+ function displayError(message) {
864
+ console.error('Displaying error:', message); // Debug log
865
+
866
+ const resultDiv = document.getElementById('result');
867
+ resultDiv.className = 'result-container error';
868
+ resultDiv.style.display = 'block'; // FORCE DISPLAY
869
+ resultDiv.innerHTML = `
870
+ <h3 style="color: #ff4444; margin-bottom: 10px;">❌ Error</h3>
871
+ <p>${message}</p>
872
+ `;
873
+ }
874
+
875
+ // Reset form
876
+ function resetForm() {
877
+ document.getElementById('predictionForm').reset();
878
+ document.getElementById('result').style.display = 'none';
879
+ }
880
+
881
+ // File upload handlers
882
+ const uploadArea = document.getElementById('uploadArea');
883
+
884
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
885
+ uploadArea.addEventListener(eventName, preventDefaults, false);
886
+ });
887
+
888
+ function preventDefaults(e) {
889
+ e.preventDefault();
890
+ e.stopPropagation();
891
+ }
892
+
893
+ ['dragenter', 'dragover'].forEach(eventName => {
894
+ uploadArea.addEventListener(eventName, () => {
895
+ uploadArea.classList.add('dragging');
896
+ }, false);
897
+ });
898
+
899
+ ['dragleave', 'drop'].forEach(eventName => {
900
+ uploadArea.addEventListener(eventName, () => {
901
+ uploadArea.classList.remove('dragging');
902
+ }, false);
903
+ });
904
+
905
+ uploadArea.addEventListener('drop', (e) => {
906
+ const files = e.dataTransfer.files;
907
+ if (files.length > 0) {
908
+ handleFile(files[0]);
909
+ }
910
+ }, false);
911
+
912
+ function handleFileSelect(event) {
913
+ const file = event.target.files[0];
914
+ if (file) {
915
+ handleFile(file);
916
+ }
917
+ }
918
+
919
+ function handleFile(file) {
920
+ if (!file.name.endsWith('.csv')) {
921
+ alert('Please upload a CSV file');
922
+ return;
923
+ }
924
+
925
+ selectedFile = file;
926
+ document.getElementById('fileName').style.display = 'block';
927
+ document.getElementById('fileName').textContent = `📄 ${file.name}`;
928
+ document.getElementById('uploadBtn').disabled = false;
929
+ }
930
+
931
+ // Upload batch
932
+ async function uploadBatch() {
933
+ if (!selectedFile) return;
934
+
935
+ const formData = new FormData();
936
+ formData.append('file', selectedFile);
937
+
938
+ document.getElementById('batchLoading').classList.add('active');
939
+ document.getElementById('batchResult').style.display = 'none';
940
+
941
+ try {
942
+ console.log('Uploading batch file:', selectedFile.name);
943
+
944
+ const response = await fetch(`${API_URL}/predict/batch/json`, {
945
+ method: 'POST',
946
+ body: formData
947
+ });
948
+
949
+ if (!response.ok) {
950
+ const error = await response.json();
951
+ throw new Error(error.detail || 'Batch processing failed');
952
+ }
953
+
954
+ const data = await response.json();
955
+ console.log('Batch response:', data);
956
+
957
+ displayBatchResults(data);
958
+
959
+ } catch (error) {
960
+ console.error('Batch processing error:', error);
961
+ const resultDiv = document.getElementById('batchResult');
962
+ resultDiv.className = 'result-container error';
963
+ resultDiv.style.display = 'block';
964
+ resultDiv.innerHTML = `
965
+ <h3 style="color: #ff4444; margin-bottom: 10px;">❌ Batch Processing Error</h3>
966
+ <p>${error.message}</p>
967
+ <p style="margin-top: 10px; font-size: 0.9em; color: #666;">
968
+ Make sure your CSV has the required columns: air_temperature, process_temperature,
969
+ rotational_speed, torque, tool_wear, type
970
+ </p>
971
+ `;
972
+ } finally {
973
+ document.getElementById('batchLoading').classList.remove('active');
974
+ }
975
+ }
976
+
977
+ // Display batch results
978
+ function displayBatchResults(data) {
979
+ const resultDiv = document.getElementById('batchResult');
980
+ resultDiv.className = 'result-container success';
981
+ resultDiv.style.display = 'block';
982
+
983
+ const summary = data.summary;
984
+ const results = data.results;
985
+
986
+ let html = `
987
+ <h3 style="margin-bottom: 20px;">📊 Batch Processing Results</h3>
988
+
989
+ <!-- Summary Statistics -->
990
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 25px;">
991
+ <div style="background: white; padding: 20px; border-radius: 10px; text-align: center; border-left: 4px solid #667eea;">
992
+ <div style="font-size: 32px; font-weight: 700; color: #667eea;">${summary.total_records}</div>
993
+ <div style="font-size: 14px; color: #666; margin-top: 5px;">Total Records</div>
994
+ </div>
995
+ <div style="background: white; padding: 20px; border-radius: 10px; text-align: center; border-left: 4px solid #4caf50;">
996
+ <div style="font-size: 32px; font-weight: 700; color: #4caf50;">${summary.predictions.healthy}</div>
997
+ <div style="font-size: 14px; color: #666; margin-top: 5px;">Healthy</div>
998
+ </div>
999
+ <div style="background: white; padding: 20px; border-radius: 10px; text-align: center; border-left: 4px solid #ff4444;">
1000
+ <div style="font-size: 32px; font-weight: 700; color: #ff4444;">${summary.predictions.failure}</div>
1001
+ <div style="font-size: 14px; color: #666; margin-top: 5px;">Failures</div>
1002
+ </div>
1003
+ <div style="background: white; padding: 20px; border-radius: 10px; text-align: center; border-left: 4px solid #ff9800;">
1004
+ <div style="font-size: 32px; font-weight: 700; color: #ff9800;">${summary.failure_rate}%</div>
1005
+ <div style="font-size: 14px; color: #666; margin-top: 5px;">Failure Rate</div>
1006
+ </div>
1007
+ </div>
1008
+
1009
+ <!-- Individual Results -->
1010
+ <h4 style="margin: 25px 0 15px 0; color: #333;">🔍 Detailed Analysis</h4>
1011
+ <div style="display: flex; flex-direction: column; gap: 20px;">
1012
+ `;
1013
+
1014
+ // Display each result
1015
+ results.forEach((result, index) => {
1016
+ const isFailure = result.prediction === 'FAILURE';
1017
+ const severityClass = result.diagnostics ? `severity-${result.diagnostics.severity.toLowerCase()}` : '';
1018
+ const probability = (result.confidence * 100).toFixed(1);
1019
+
1020
+ html += `
1021
+ <div style="background: white; padding: 20px; border-radius: 10px; border-left: 5px solid ${isFailure ? '#ff4444' : '#4caf50'};">
1022
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 2px solid #e0e0e0;">
1023
+ <div style="display: flex; align-items: center; gap: 15px;">
1024
+ <span style="font-size: 14px; font-weight: 600; color: #666;">Row ${result.row_number}</span>
1025
+ <span style="font-size: 14px; font-weight: 600; color: #667eea;">${result.machine_id}</span>
1026
+ <span class="prediction-badge ${isFailure ? 'badge-failure' : 'badge-healthy'}" style="font-size: 14px; padding: 6px 14px;">
1027
+ ${result.prediction}
1028
+ </span>
1029
+ ${result.diagnostics ? `
1030
+ <span class="severity-badge ${severityClass}" style="font-size: 12px; padding: 4px 10px;">
1031
+ ${result.diagnostics.severity}
1032
+ </span>
1033
+ ` : ''}
1034
+ </div>
1035
+ <div style="background: #e0e0e0; height: 20px; width: 120px; border-radius: 10px; overflow: hidden;">
1036
+ <div style="height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); width: ${probability}%; display: flex; align-items: center; justify-content: center; font-size: 11px; color: white; font-weight: 600;">
1037
+ ${probability}%
1038
+ </div>
1039
+ </div>
1040
+ </div>
1041
+
1042
+ ${result.diagnostics ? `
1043
+ <div style="display: grid; grid-template-columns: 1fr 2fr; gap: 15px; margin-bottom: 15px;">
1044
+ <div style="padding: 12px; background: #f8f9fa; border-radius: 8px;">
1045
+ <div style="font-size: 11px; color: #667eea; font-weight: 600; margin-bottom: 5px; text-transform: uppercase;">🎯 Primary Cause</div>
1046
+ <div style="font-size: 13px; color: #333;">${result.diagnostics.primary_cause}</div>
1047
+ </div>
1048
+ <div style="padding: 12px; background: #f8f9fa; border-radius: 8px;">
1049
+ <div style="font-size: 11px; color: #667eea; font-weight: 600; margin-bottom: 5px; text-transform: uppercase;">🔧 Recommended Action</div>
1050
+ <div style="font-size: 13px; color: #333;">${result.diagnostics.recommended_action}</div>
1051
+ </div>
1052
+ </div>
1053
+ ` : ''}
1054
+
1055
+ ${result.anomalies && result.anomalies.length > 0 ? `
1056
+ <div style="margin-top: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
1057
+ <div style="font-size: 12px; color: #667eea; font-weight: 600; margin-bottom: 10px;">
1058
+ ⚠️ ${result.anomalies.length} Anomalies Detected
1059
+ </div>
1060
+ <div style="display: flex; flex-wrap: wrap; gap: 10px;">
1061
+ ${result.anomalies.map(anomaly => `
1062
+ <span style="font-size: 11px; padding: 4px 8px; border-radius: 12px; background: ${anomaly.status === 'CRITICAL' ? '#f8d7da' : anomaly.status === 'WARNING' ? '#fff3cd' : '#d4edda'}; color: ${anomaly.status === 'CRITICAL' ? '#721c24' : anomaly.status === 'WARNING' ? '#856404' : '#155724'};">
1063
+ ${anomaly.parameter}
1064
+ </span>
1065
+ `).join('')}
1066
+ </div>
1067
+ </div>
1068
+ ` : ''}
1069
+
1070
+ ${result.overall_health ? `
1071
+ <div style="margin-top: 15px; padding: 10px; border-radius: 8px; text-align: center; font-size: 13px; font-weight: 600; background: ${result.overall_health.includes('EXCELLENT') ? '#d4edda' : result.overall_health.includes('GOOD') ? '#d1ecf1' : result.overall_health.includes('FAIR') ? '#fff3cd' : '#f8d7da'}; color: ${result.overall_health.includes('EXCELLENT') ? '#155724' : result.overall_health.includes('GOOD') ? '#0c5460' : result.overall_health.includes('FAIR') ? '#856404' : '#721c24'};">
1072
+ ${result.overall_health}
1073
+ </div>
1074
+ ` : ''}
1075
+
1076
+ ${result.error ? `
1077
+ <div style="margin-top: 10px; padding: 10px; background: #fff0f0; border-radius: 8px; color: #ff4444; font-size: 13px;">
1078
+ <strong>Error:</strong> ${result.error}
1079
+ </div>
1080
+ ` : ''}
1081
+ </div>
1082
+ `;
1083
+ });
1084
+
1085
+ html += `
1086
+ </div>
1087
+
1088
+ <!-- Download CSV Button -->
1089
+ <div style="margin-top: 25px; text-align: center;">
1090
+ <button onclick="downloadBatchCSV()" class="btn-secondary" style="padding: 12px 24px;">
1091
+ ⬇️ Download Results as CSV
1092
+ </button>
1093
+ </div>
1094
+ `;
1095
+
1096
+ resultDiv.innerHTML = html;
1097
+ }
1098
+
1099
+ // Download batch results as CSV
1100
+ async function downloadBatchCSV() {
1101
+ if (!selectedFile) return;
1102
+
1103
+ try {
1104
+ const formData = new FormData();
1105
+ formData.append('file', selectedFile);
1106
+
1107
+ const response = await fetch(`${API_URL}/predict/batch`, {
1108
+ method: 'POST',
1109
+ body: formData
1110
+ });
1111
+
1112
+ if (!response.ok) {
1113
+ throw new Error('CSV download failed');
1114
+ }
1115
+
1116
+ // Download the result CSV
1117
+ const blob = await response.blob();
1118
+ const url = window.URL.createObjectURL(blob);
1119
+ const a = document.createElement('a');
1120
+ a.href = url;
1121
+ a.download = `predictions_${new Date().getTime()}.csv`;
1122
+ document.body.appendChild(a);
1123
+ a.click();
1124
+ window.URL.revokeObjectURL(url);
1125
+ document.body.removeChild(a);
1126
+ } catch (error) {
1127
+ alert('Failed to download CSV: ' + error.message);
1128
+ }
1129
+ }
1130
+
1131
+ // Download template CSV
1132
+ function downloadTemplate() {
1133
+ const template = `machine_id,air_temperature,process_temperature,rotational_speed,torque,tool_wear,type
1134
+ M_L_01,298.1,308.6,1551,42.8,0,M
1135
+ M_L_02,299.2,310.1,1450,38.5,150,L
1136
+ M_H_01,297.5,312.8,1800,65.2,220,H`;
1137
+
1138
+ const blob = new Blob([template], { type: 'text/csv' });
1139
+ const url = window.URL.createObjectURL(blob);
1140
+ const a = document.createElement('a');
1141
+ a.href = url;
1142
+ a.download = 'template.csv';
1143
+ document.body.appendChild(a);
1144
+ a.click();
1145
+ window.URL.revokeObjectURL(url);
1146
+ document.body.removeChild(a);
1147
+ }
1148
+
1149
+ // Initialize
1150
+ checkApiStatus();
1151
+ setInterval(checkApiStatus, 10000); // Check every 10 seconds
1152
+ </script>
1153
+ </body>
1154
+
1155
+ </html>
api/install_requirements.bat ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo ========================================
3
+ echo Installing Requirements
4
+ echo ========================================
5
+ echo.
6
+
7
+ REM Check if Python is installed
8
+ python --version >nul 2>&1
9
+ if errorlevel 1 (
10
+ echo ERROR: Python is not installed or not in PATH
11
+ echo Please install Python 3.11+ from https://www.python.org/
12
+ pause
13
+ exit /b 1
14
+ )
15
+
16
+ echo Python version:
17
+ python --version
18
+ echo.
19
+
20
+ REM Check if pip is available
21
+ python -m pip --version >nul 2>&1
22
+ if errorlevel 1 (
23
+ echo ERROR: pip is not available
24
+ echo Installing pip...
25
+ python -m ensurepip --default-pip
26
+ )
27
+
28
+ echo.
29
+ echo Installing packages from requirements.txt...
30
+ echo.
31
+
32
+ python -m pip install -r requirements.txt
33
+
34
+ if errorlevel 1 (
35
+ echo.
36
+ echo ERROR: Failed to install some packages
37
+ echo Please check the error messages above
38
+ pause
39
+ exit /b 1
40
+ )
41
+
42
+ echo.
43
+ echo ========================================
44
+ echo Installation Complete!
45
+ echo ========================================
46
+ echo.
47
+ echo All packages have been installed successfully.
48
+ echo You can now run: start_server.bat
49
+ echo.
50
+ pause
api/main.py ADDED
@@ -0,0 +1,886 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI Predictive Maintenance API with Rule-Based Diagnostics
3
+ Senior Backend Engineer & Reliability Engineer Implementation
4
+
5
+ Features:
6
+ - ML-based failure prediction using XGBoost
7
+ - Physics-based diagnostic engine
8
+ - Single prediction and batch processing endpoints
9
+ - Production-ready with proper error handling
10
+ """
11
+
12
+ from fastapi import FastAPI, HTTPException, UploadFile, File
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from fastapi.responses import StreamingResponse
15
+ from pydantic import BaseModel, Field, validator
16
+ from typing import Optional, Dict, Any, List
17
+ import joblib
18
+ import numpy as np
19
+ import pandas as pd
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+ import os
23
+ import io
24
+ import logging
25
+
26
+ # Configure logging
27
+ logging.basicConfig(level=logging.INFO)
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Initialize FastAPI
31
+ app = FastAPI(
32
+ title="Predictive Maintenance API",
33
+ description="ML + Rule-Based Diagnostics for Industrial Equipment",
34
+ version="1.0.0"
35
+ )
36
+
37
+ # CORS Configuration
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["*"],
41
+ allow_credentials=True,
42
+ allow_methods=["*"],
43
+ allow_headers=["*"],
44
+ )
45
+
46
+ from huggingface_hub import hf_hub_download
47
+
48
+ # Global model pipeline
49
+ MODEL_PIPELINE = None
50
+ HF_MODEL_REPO = os.getenv("HF_MODEL_REPO", "deropxyz/AC02-ML")
51
+ PRIMARY_MODEL_FILENAME = os.getenv("HF_PRIMARY_MODEL", "model_pipeline_improved.joblib")
52
+ FALLBACK_MODEL_FILENAME = os.getenv("HF_FALLBACK_MODEL", "model_pipeline.joblib")
53
+ HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN")
54
+
55
+
56
+ def download_model_artifact(filename: str) -> Path:
57
+ """
58
+ Download a model artifact from Hugging Face Hub and return the local cache path.
59
+ """
60
+ if not filename:
61
+ raise ValueError("Filename must be provided for model download")
62
+
63
+ logger.info("Downloading model '%s' from repo '%s'...", filename, HF_MODEL_REPO)
64
+ cache_path = hf_hub_download(
65
+ repo_id=HF_MODEL_REPO,
66
+ filename=filename,
67
+ repo_type="model",
68
+ token=HF_TOKEN,
69
+ )
70
+ return Path(cache_path)
71
+
72
+
73
+ # ============================================================================
74
+ # REQUEST/RESPONSE SCHEMAS
75
+ # ============================================================================
76
+
77
+ class PredictionRequest(BaseModel):
78
+ """Single prediction request schema matching dataset columns"""
79
+ machine_id: Optional[str] = Field(None, description="Machine identifier (e.g., M_L_01)")
80
+ air_temperature: float = Field(..., ge=250, le=350, description="Air temperature in Kelvin")
81
+ process_temperature: float = Field(..., ge=250, le=400, description="Process temperature in Kelvin")
82
+ rotational_speed: int = Field(..., ge=0, le=3000, description="Rotational speed in RPM")
83
+ torque: float = Field(..., ge=0, le=100, description="Torque in Nm")
84
+ tool_wear: int = Field(..., ge=0, le=300, description="Tool wear in minutes")
85
+ type: str = Field(..., description="Machine type: L (Low), M (Medium), H (High)")
86
+
87
+ @validator('type')
88
+ def validate_type(cls, v):
89
+ if v not in ['L', 'M', 'H']:
90
+ raise ValueError('Type must be L, M, or H')
91
+ return v
92
+
93
+ class Config:
94
+ schema_extra = {
95
+ "example": {
96
+ "machine_id": "M_L_01",
97
+ "air_temperature": 298.1,
98
+ "process_temperature": 308.6,
99
+ "rotational_speed": 1551,
100
+ "torque": 42.8,
101
+ "tool_wear": 0,
102
+ "type": "M"
103
+ }
104
+ }
105
+
106
+
107
+ class DiagnosticResult(BaseModel):
108
+ """Diagnostic analysis result"""
109
+ primary_cause: str
110
+ sensor_alert: str
111
+ recommended_action: str
112
+ severity: str # LOW, MEDIUM, HIGH, CRITICAL
113
+
114
+
115
+ class AnomalyDetail(BaseModel):
116
+ """Anomaly detection detail"""
117
+ parameter: str
118
+ value: str
119
+ normal_range: str
120
+ status: str # NORMAL, WARNING, CRITICAL
121
+ explanation: str
122
+
123
+
124
+ class PredictionResponse(BaseModel):
125
+ """Single prediction response"""
126
+ machine_id: Optional[str]
127
+ timestamp: str
128
+ prediction: str # HEALTHY or FAILURE
129
+ confidence: float
130
+ diagnostics: DiagnosticResult
131
+ features: Optional[Dict[str, Any]] = None # Engineered features
132
+ anomalies: Optional[List[AnomalyDetail]] = None # Parameter anomaly analysis
133
+ overall_health: Optional[str] = None # Overall health status
134
+
135
+
136
+ # ============================================================================
137
+ # STARTUP EVENT: LOAD MODEL
138
+ # ============================================================================
139
+
140
+ @app.on_event("startup")
141
+ async def load_model():
142
+ """Download and load model pipeline on startup"""
143
+ global MODEL_PIPELINE
144
+
145
+ last_error: Optional[Exception] = None
146
+ for candidate in [PRIMARY_MODEL_FILENAME, FALLBACK_MODEL_FILENAME]:
147
+ if not candidate:
148
+ continue
149
+ try:
150
+ model_path = download_model_artifact(candidate)
151
+ MODEL_PIPELINE = joblib.load(model_path)
152
+ logger.info("✅ Model loaded from %s", candidate)
153
+ break
154
+ except Exception as err:
155
+ last_error = err
156
+ logger.error("Failed to load model '%s': %s", candidate, err)
157
+
158
+ if MODEL_PIPELINE is None:
159
+ raise RuntimeError(f"Unable to load any model artifact: {last_error}") from last_error
160
+
161
+ # Validate model structure
162
+ required_keys = ['model', 'scaler', 'features']
163
+ for key in required_keys:
164
+ if key not in MODEL_PIPELINE:
165
+ raise ValueError(f"Model pipeline missing key: {key}")
166
+
167
+ logger.info("Model type: %s", type(MODEL_PIPELINE['model']).__name__)
168
+ logger.info("Features: %s", MODEL_PIPELINE['features'])
169
+
170
+
171
+ # ============================================================================
172
+ # FEATURE ENGINEERING (Must match training logic)
173
+ # ============================================================================
174
+
175
+ def engineer_features(data: Dict[str, Any]) -> pd.DataFrame:
176
+ """
177
+ Apply physics-based feature engineering matching training pipeline
178
+
179
+ Features:
180
+ - Temperature_Diff: Thermal stress indicator
181
+ - Power_W: Mechanical power (P = τω)
182
+ - Type_Encoded: Categorical encoding
183
+ - Tool_wear_hours: Tool wear converted to hours
184
+ """
185
+ # Create base dataframe
186
+ df = pd.DataFrame([{
187
+ 'Air_temp_K': data['air_temperature'],
188
+ 'Process_temp_K': data['process_temperature'],
189
+ 'Speed_rpm': data['rotational_speed'],
190
+ 'Torque_Nm': data['torque'],
191
+ 'Tool_wear_min': data['tool_wear']
192
+ }])
193
+
194
+ # Physics-based features
195
+ df['Tool_wear_hours'] = df['Tool_wear_min'] / 60.0 # Convert minutes to hours
196
+ df['Temperature_Diff'] = df['Process_temp_K'] - df['Air_temp_K']
197
+ df['Power_W'] = df['Torque_Nm'] * df['Speed_rpm'] * (2 * np.pi / 60)
198
+
199
+ # Type encoding
200
+ type_mapping = {'L': 0, 'M': 1, 'H': 2}
201
+ df['Type_Encoded'] = type_mapping[data['type']]
202
+
203
+ # Ensure correct feature order matching training
204
+ feature_cols = MODEL_PIPELINE['features']
205
+ return df[feature_cols]
206
+
207
+
208
+ # ============================================================================
209
+ # DIAGNOSTIC ENGINE (Rule-Based Intelligence)
210
+ # ============================================================================
211
+
212
+ def analyze_condition(
213
+ data: Dict[str, Any],
214
+ pred_class: int,
215
+ pred_proba: float,
216
+ engineered_features: pd.DataFrame
217
+ ) -> DiagnosticResult:
218
+ """
219
+ Advanced diagnostic engine combining ML predictions with physics-based rules
220
+
221
+ Priority Rules:
222
+ 1. Tool Wear Failure (CRITICAL)
223
+ 2. Power Overstrain (HIGH)
224
+ 3. Heat Dissipation Issue (MEDIUM)
225
+ 4. ML Anomaly Detection (VARIABLE)
226
+ 5. Healthy Operation (LOW)
227
+ """
228
+
229
+ # Extract values for rule evaluation
230
+ temp_diff = engineered_features['Temperature_Diff'].iloc[0]
231
+ power_w = engineered_features['Power_W'].iloc[0]
232
+ torque = data['torque']
233
+ tool_wear = data['tool_wear']
234
+ speed = data['rotational_speed']
235
+ air_temp = data['air_temperature']
236
+ process_temp = data['process_temperature']
237
+
238
+ # ========================================================================
239
+ # RULE 1: TOOL WEAR FAILURE (CRITICAL PRIORITY)
240
+ # ========================================================================
241
+ if tool_wear > 200:
242
+ return DiagnosticResult(
243
+ primary_cause="Tool End of Life",
244
+ sensor_alert=f"Tool wear ({tool_wear} min) exceeds safety threshold (200 min)",
245
+ recommended_action="IMMEDIATE ACTION: Replace cutting tool before catastrophic failure. "
246
+ "Schedule downtime within 4 hours.",
247
+ severity="CRITICAL"
248
+ )
249
+
250
+ # ========================================================================
251
+ # RULE 2: POWER OVERSTRAIN (HIGH PRIORITY)
252
+ # ========================================================================
253
+ if power_w > 9000 or torque > 60:
254
+ cause_details = []
255
+ if power_w > 9000:
256
+ cause_details.append(f"Power output ({power_w:.0f} W) exceeds design limit (9000 W)")
257
+ if torque > 60:
258
+ cause_details.append(f"Torque ({torque:.1f} Nm) exceeds safety limit (60 Nm)")
259
+
260
+ return DiagnosticResult(
261
+ primary_cause="Power Overstrain / Mechanical Overload",
262
+ sensor_alert=" | ".join(cause_details),
263
+ recommended_action="URGENT: Reduce operational load immediately. "
264
+ "Inspect shaft alignment, bearing condition, and drive belt tension. "
265
+ "Check for material jamming or obstruction.",
266
+ severity="HIGH"
267
+ )
268
+
269
+ # ========================================================================
270
+ # RULE 3: HEAT DISSIPATION ISSUE (MEDIUM PRIORITY)
271
+ # ========================================================================
272
+ if temp_diff < 8.0 and speed > 1300:
273
+ return DiagnosticResult(
274
+ primary_cause="Inefficient Cooling System",
275
+ sensor_alert=f"Temperature differential ({temp_diff:.1f} K) too low at high speed ({speed} RPM). "
276
+ f"Expected ≥8.0 K for proper heat dissipation.",
277
+ recommended_action="MAINTENANCE REQUIRED: Inspect cooling system within 24 hours. "
278
+ "Check heat exchanger efficiency, coolant flow rate, and radiator fins. "
279
+ "Verify coolant temperature and pressure.",
280
+ severity="MEDIUM"
281
+ )
282
+
283
+ # ========================================================================
284
+ # RULE 4: THERMAL STRESS (MEDIUM PRIORITY)
285
+ # ========================================================================
286
+ if temp_diff > 15.0:
287
+ return DiagnosticResult(
288
+ primary_cause="Excessive Thermal Stress",
289
+ sensor_alert=f"Temperature differential ({temp_diff:.1f} K) exceeds normal range (8-12 K). "
290
+ f"Process temp: {process_temp:.1f} K, Air temp: {air_temp:.1f} K",
291
+ recommended_action="MONITOR CLOSELY: High thermal gradient detected. "
292
+ "Check process parameters and reduce processing intensity if possible. "
293
+ "Verify insulation integrity and ambient conditions.",
294
+ severity="MEDIUM"
295
+ )
296
+
297
+ # ========================================================================
298
+ # RULE 5: HIGH-SPEED OPERATION RISK (MEDIUM PRIORITY)
299
+ # ========================================================================
300
+ if speed > 2500:
301
+ return DiagnosticResult(
302
+ primary_cause="High-Speed Operation Risk",
303
+ sensor_alert=f"Rotational speed ({speed} RPM) in critical range (>2500 RPM). "
304
+ f"Vibration and wear accelerate exponentially.",
305
+ recommended_action="PREVENTIVE ACTION: Monitor vibration levels closely. "
306
+ "Perform balance check and bearing inspection. "
307
+ "Consider reducing speed if not operationally critical.",
308
+ severity="MEDIUM"
309
+ )
310
+
311
+ # ========================================================================
312
+ # RULE 6: ML DETECTED ANOMALY (VARIABLE PRIORITY)
313
+ # ========================================================================
314
+ if pred_class == 1:
315
+ # ML model detected failure but no specific rule matched
316
+ confidence_level = "high" if pred_proba > 0.8 else "moderate"
317
+
318
+ return DiagnosticResult(
319
+ primary_cause="ML-Detected Anomaly Pattern",
320
+ sensor_alert=f"Machine learning model detected failure risk with {confidence_level} confidence ({pred_proba:.1%}). "
321
+ f"No specific rule violation identified.",
322
+ recommended_action="DIAGNOSTIC SCAN REQUIRED: Perform comprehensive machine diagnostic. "
323
+ "Check for: (1) Unusual vibration patterns, (2) Bearing wear, "
324
+ "(3) Lubrication quality, (4) Electrical anomalies, (5) Sensor calibration. "
325
+ "Review recent maintenance history.",
326
+ severity="HIGH" if pred_proba > 0.8 else "MEDIUM"
327
+ )
328
+
329
+ # ========================================================================
330
+ # RULE 7: HEALTHY OPERATION (LOW PRIORITY)
331
+ # ========================================================================
332
+ # Calculate health score
333
+ health_indicators = {
334
+ 'tool_wear_ok': tool_wear <= 150,
335
+ 'power_ok': power_w <= 8000,
336
+ 'temp_ok': 8.0 <= temp_diff <= 12.0,
337
+ 'speed_ok': speed <= 2000,
338
+ 'torque_ok': torque <= 50
339
+ }
340
+ health_score = sum(health_indicators.values()) / len(health_indicators) * 100
341
+
342
+ return DiagnosticResult(
343
+ primary_cause="Normal Operation",
344
+ sensor_alert=f"All parameters within optimal range. Health score: {health_score:.0f}%. "
345
+ f"Tool wear: {tool_wear}/200 min, Power: {power_w:.0f}/9000 W, "
346
+ f"Temp diff: {temp_diff:.1f} K, Speed: {speed}/2500 RPM",
347
+ recommended_action="CONTINUE NORMAL OPERATION. Next scheduled maintenance as planned. "
348
+ "Continue routine monitoring.",
349
+ severity="LOW"
350
+ )
351
+
352
+
353
+ # ============================================================================
354
+ # ANOMALY DETECTION & HEALTH ASSESSMENT
355
+ # ============================================================================
356
+
357
+ def detect_anomalies(data: Dict[str, Any], features_df: pd.DataFrame) -> List[AnomalyDetail]:
358
+ """
359
+ Detect anomalies in sensor readings and engineered features
360
+ Returns list of anomaly details for visualization
361
+ """
362
+ anomalies = []
363
+
364
+ # Extract values
365
+ air_temp = data['air_temperature']
366
+ process_temp = data['process_temperature']
367
+ speed = data['rotational_speed']
368
+ torque = data['torque']
369
+ tool_wear = data['tool_wear']
370
+ temp_diff = features_df['Temperature_Diff'].iloc[0]
371
+ power_w = features_df['Power_W'].iloc[0]
372
+
373
+ # Define normal ranges and check each parameter
374
+
375
+ # 1. Air Temperature
376
+ if air_temp < 290 or air_temp > 310:
377
+ status = "CRITICAL" if air_temp < 285 or air_temp > 315 else "WARNING"
378
+ anomalies.append(AnomalyDetail(
379
+ parameter="Air Temperature",
380
+ value=f"{air_temp:.1f} K",
381
+ normal_range="290-310 K",
382
+ status=status,
383
+ explanation=f"Air temperature is {'too low' if air_temp < 290 else 'too high'}. This can affect cooling efficiency and equipment performance."
384
+ ))
385
+
386
+ # 2. Process Temperature
387
+ if process_temp < 300 or process_temp > 315:
388
+ status = "CRITICAL" if process_temp < 295 or process_temp > 320 else "WARNING"
389
+ anomalies.append(AnomalyDetail(
390
+ parameter="Process Temperature",
391
+ value=f"{process_temp:.1f} K",
392
+ normal_range="300-315 K",
393
+ status=status,
394
+ explanation=f"Process temperature is {'below optimal' if process_temp < 300 else 'exceeding optimal'} range. May indicate cooling system issues or excessive load."
395
+ ))
396
+
397
+ # 3. Temperature Differential
398
+ if temp_diff < 8.0 or temp_diff > 12.0:
399
+ status = "CRITICAL" if temp_diff < 5.0 or temp_diff > 15.0 else "WARNING"
400
+ anomalies.append(AnomalyDetail(
401
+ parameter="Temperature Differential",
402
+ value=f"{temp_diff:.1f} K",
403
+ normal_range="8.0-12.0 K",
404
+ status=status,
405
+ explanation=f"Temperature gradient is {'insufficient' if temp_diff < 8.0 else 'excessive'}. This indicates {'poor heat dissipation' if temp_diff < 8.0 else 'thermal stress'}."
406
+ ))
407
+
408
+ # 4. Rotational Speed
409
+ if speed < 1200 or speed > 2000:
410
+ status = "CRITICAL" if speed < 1000 or speed > 2500 else "WARNING"
411
+ anomalies.append(AnomalyDetail(
412
+ parameter="Rotational Speed",
413
+ value=f"{speed} RPM",
414
+ normal_range="1200-2000 RPM",
415
+ status=status,
416
+ explanation=f"Speed is {'below normal' if speed < 1200 else 'above normal'} operating range. This affects wear rate and vibration levels."
417
+ ))
418
+
419
+ # 5. Torque
420
+ if torque > 50:
421
+ status = "CRITICAL" if torque > 60 else "WARNING"
422
+ anomalies.append(AnomalyDetail(
423
+ parameter="Torque",
424
+ value=f"{torque:.1f} Nm",
425
+ normal_range="20-50 Nm",
426
+ status=status,
427
+ explanation=f"Torque is {'significantly ' if torque > 60 else ''}exceeding normal range. This indicates mechanical overload or obstruction."
428
+ ))
429
+
430
+ # 6. Tool Wear
431
+ if tool_wear > 150:
432
+ status = "CRITICAL" if tool_wear > 200 else "WARNING"
433
+ anomalies.append(AnomalyDetail(
434
+ parameter="Tool Wear",
435
+ value=f"{tool_wear} min",
436
+ normal_range="0-150 min",
437
+ status=status,
438
+ explanation=f"Tool wear is {'critically high' if tool_wear > 200 else 'elevated'}. {'IMMEDIATE replacement required' if tool_wear > 200 else 'Schedule replacement soon'}."
439
+ ))
440
+
441
+ # 7. Power Output
442
+ if power_w > 8000:
443
+ status = "CRITICAL" if power_w > 9000 else "WARNING"
444
+ anomalies.append(AnomalyDetail(
445
+ parameter="Power Output",
446
+ value=f"{power_w:.0f} W",
447
+ normal_range="3000-8000 W",
448
+ status=status,
449
+ explanation=f"Power consumption is {'critically ' if power_w > 9000 else ''}high. This suggests mechanical resistance or overload conditions."
450
+ ))
451
+
452
+ return anomalies
453
+
454
+
455
+ def determine_overall_health(prediction: int, confidence: float, severity: str, anomaly_count: int) -> str:
456
+ """
457
+ Determine overall machine health status based on multiple factors
458
+ Returns: EXCELLENT, GOOD, FAIR, or POOR health status
459
+ """
460
+ if prediction == 1:
461
+ # Failure predicted
462
+ if severity == "CRITICAL" or confidence > 0.9:
463
+ return "🔴 POOR HEALTH - Critical failure risk detected. Immediate action required."
464
+ elif severity == "HIGH" or confidence > 0.75:
465
+ return "🟠 POOR HEALTH - High failure risk. Urgent maintenance needed."
466
+ else:
467
+ return "🟡 FAIR HEALTH - Moderate failure risk detected. Schedule maintenance soon."
468
+ else:
469
+ # No failure predicted
470
+ if anomaly_count == 0:
471
+ return "🟢 EXCELLENT HEALTH - All systems operating optimally within normal parameters."
472
+ elif anomaly_count <= 2:
473
+ return "🟢 GOOD HEALTH - Minor anomalies detected but within acceptable limits."
474
+ else:
475
+ return "🟡 FAIR HEALTH - Multiple anomalies detected. Monitor closely and address issues."
476
+
477
+
478
+ # ============================================================================
479
+ # ENDPOINT 1: SINGLE PREDICTION
480
+ # ============================================================================
481
+
482
+ @app.post("/predict", response_model=PredictionResponse)
483
+ async def predict_single(request: PredictionRequest):
484
+ """
485
+ Predict failure risk for a single machine reading
486
+
487
+ Returns:
488
+ - Prediction (HEALTHY/FAILURE)
489
+ - Confidence score
490
+ - Detailed diagnostics with actionable recommendations
491
+ """
492
+ try:
493
+ # Convert request to dict
494
+ data = request.dict()
495
+
496
+ # Feature engineering
497
+ features_df = engineer_features(data)
498
+
499
+ # Scale features
500
+ features_scaled = MODEL_PIPELINE['scaler'].transform(features_df)
501
+
502
+ # Predict
503
+ prediction = MODEL_PIPELINE['model'].predict(features_scaled)[0]
504
+ prediction_proba = MODEL_PIPELINE['model'].predict_proba(features_scaled)[0]
505
+
506
+ # Get confidence (probability of predicted class)
507
+ confidence = float(prediction_proba[prediction])
508
+
509
+ # Run diagnostic engine
510
+ diagnostics = analyze_condition(data, prediction, confidence, features_df)
511
+
512
+ # Prepare features dict for display
513
+ features_dict = features_df.iloc[0].to_dict()
514
+
515
+ # Detect anomalies
516
+ anomalies = detect_anomalies(data, features_df)
517
+
518
+ # Determine overall health
519
+ overall_health = determine_overall_health(prediction, confidence, diagnostics.severity, len(anomalies))
520
+
521
+ # Format response
522
+ return PredictionResponse(
523
+ machine_id=request.machine_id,
524
+ timestamp=datetime.now().isoformat(),
525
+ prediction="FAILURE" if prediction == 1 else "HEALTHY",
526
+ confidence=round(confidence, 4),
527
+ diagnostics=diagnostics,
528
+ features=features_dict,
529
+ anomalies=anomalies,
530
+ overall_health=overall_health
531
+ )
532
+
533
+ except Exception as e:
534
+ logger.error(f"Prediction error: {e}")
535
+ raise HTTPException(status_code=500, detail=f"Prediction failed: {str(e)}")
536
+
537
+
538
+ # ============================================================================
539
+ # ENDPOINT 2: BATCH PREDICTION (CSV UPLOAD)
540
+ # ============================================================================
541
+
542
+ @app.post("/predict/batch")
543
+ async def predict_batch(file: UploadFile = File(...)):
544
+ """
545
+ Batch prediction from CSV upload
546
+
547
+ Input: CSV file with columns matching PredictionRequest
548
+ Output: CSV with added columns: Prediction, Probability, Primary_Cause,
549
+ Sensor_Alert, Recommended_Action, Severity
550
+ """
551
+ try:
552
+ # Validate file type
553
+ if not file.filename.endswith('.csv'):
554
+ raise HTTPException(status_code=400, detail="File must be CSV format")
555
+
556
+ # Read CSV
557
+ contents = await file.read()
558
+ df = pd.read_csv(io.StringIO(contents.decode('utf-8')))
559
+
560
+ # Normalize column names (handle case-insensitive, spaces, brackets, etc)
561
+ df.columns = df.columns.str.strip().str.lower().str.replace(' ', '_').str.replace('[', '_').str.replace(']', '').str.replace('__', '_').str.rstrip('_')
562
+
563
+ # Create column mapping for different formats
564
+ column_mapping = {
565
+ 'air_temperature_k': 'air_temperature',
566
+ 'process_temperature_k': 'process_temperature',
567
+ 'rotational_speed_rpm': 'rotational_speed',
568
+ 'torque_nm': 'torque',
569
+ 'tool_wear_min': 'tool_wear'
570
+ }
571
+
572
+ # Apply mapping if columns have units
573
+ df.rename(columns=column_mapping, inplace=True)
574
+
575
+ # Validate required columns
576
+ required_columns = [
577
+ 'air_temperature', 'process_temperature', 'rotational_speed',
578
+ 'torque', 'tool_wear', 'type'
579
+ ]
580
+ missing_cols = [col for col in required_columns if col not in df.columns]
581
+ if missing_cols:
582
+ raise HTTPException(
583
+ status_code=400,
584
+ detail=f"Missing required columns: {missing_cols}. Found columns: {list(df.columns)}"
585
+ )
586
+
587
+ # Initialize result columns
588
+ predictions = []
589
+ probabilities = []
590
+ causes = []
591
+ alerts = []
592
+ actions = []
593
+ severities = []
594
+
595
+ # Process each row
596
+ for idx, row in df.iterrows():
597
+ try:
598
+ # Prepare data
599
+ data = {
600
+ 'machine_id': row.get('machine_id', f'MACHINE_{idx}'),
601
+ 'air_temperature': float(row['air_temperature']),
602
+ 'process_temperature': float(row['process_temperature']),
603
+ 'rotational_speed': int(row['rotational_speed']),
604
+ 'torque': float(row['torque']),
605
+ 'tool_wear': int(row['tool_wear']),
606
+ 'type': str(row['type'])
607
+ }
608
+
609
+ # Feature engineering
610
+ features_df = engineer_features(data)
611
+
612
+ # Scale and predict
613
+ features_scaled = MODEL_PIPELINE['scaler'].transform(features_df)
614
+ prediction = MODEL_PIPELINE['model'].predict(features_scaled)[0]
615
+ prediction_proba = MODEL_PIPELINE['model'].predict_proba(features_scaled)[0]
616
+ confidence = float(prediction_proba[prediction])
617
+
618
+ # Diagnostics
619
+ diagnostics = analyze_condition(data, prediction, confidence, features_df)
620
+
621
+ # Append results
622
+ predictions.append("FAILURE" if prediction == 1 else "HEALTHY")
623
+ probabilities.append(round(confidence, 4))
624
+ causes.append(diagnostics.primary_cause)
625
+ alerts.append(diagnostics.sensor_alert)
626
+ actions.append(diagnostics.recommended_action)
627
+ severities.append(diagnostics.severity)
628
+
629
+ except Exception as e:
630
+ logger.warning(f"Row {idx} processing error: {e}")
631
+ predictions.append("ERROR")
632
+ probabilities.append(0.0)
633
+ causes.append("Processing Error")
634
+ alerts.append(str(e))
635
+ actions.append("Review input data")
636
+ severities.append("UNKNOWN")
637
+
638
+ # Add results to dataframe
639
+ df['Prediction'] = predictions
640
+ df['Confidence'] = probabilities
641
+ df['Primary_Cause'] = causes
642
+ df['Sensor_Alert'] = alerts
643
+ df['Recommended_Action'] = actions
644
+ df['Severity'] = severities
645
+ df['Processed_At'] = datetime.now().isoformat()
646
+
647
+ # Convert to CSV
648
+ output = io.StringIO()
649
+ df.to_csv(output, index=False)
650
+ output.seek(0)
651
+
652
+ # Return as downloadable file
653
+ return StreamingResponse(
654
+ iter([output.getvalue()]),
655
+ media_type="text/csv",
656
+ headers={
657
+ "Content-Disposition": f"attachment; filename=predictions_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
658
+ }
659
+ )
660
+
661
+ except HTTPException:
662
+ raise
663
+ except Exception as e:
664
+ logger.error(f"Batch prediction error: {e}")
665
+ raise HTTPException(status_code=500, detail=f"Batch prediction failed: {str(e)}")
666
+
667
+
668
+ # ============================================================================
669
+ # ENDPOINT 3: BATCH PREDICTION JSON (For UI Display)
670
+ # ============================================================================
671
+
672
+ @app.post("/predict/batch/json")
673
+ async def predict_batch_json(file: UploadFile = File(...)):
674
+ """
675
+ Batch prediction from CSV upload - returns detailed JSON for UI display
676
+
677
+ Input: CSV file with required columns
678
+ Output: JSON with detailed predictions, features, and anomalies for each row
679
+ """
680
+ try:
681
+ # Validate file type
682
+ if not file.filename.endswith('.csv'):
683
+ raise HTTPException(status_code=400, detail="File must be CSV format")
684
+
685
+ # Read CSV
686
+ contents = await file.read()
687
+ df = pd.read_csv(io.StringIO(contents.decode('utf-8')))
688
+
689
+ # Normalize column names
690
+ df.columns = df.columns.str.strip().str.lower().str.replace(' ', '_')
691
+
692
+ # Validate required columns
693
+ required_columns = [
694
+ 'air_temperature', 'process_temperature', 'rotational_speed',
695
+ 'torque', 'tool_wear', 'type'
696
+ ]
697
+ missing_cols = [col for col in required_columns if col not in df.columns]
698
+ if missing_cols:
699
+ raise HTTPException(
700
+ status_code=400,
701
+ detail=f"Missing required columns: {missing_cols}. Found columns: {list(df.columns)}"
702
+ )
703
+
704
+ # Limit batch size for JSON response (increased for better UX)
705
+ if len(df) > 1000:
706
+ raise HTTPException(
707
+ status_code=400,
708
+ detail=f"Batch JSON endpoint limited to 500 rows. Your file has {len(df)} rows. Use /predict/batch for larger files or download CSV results."
709
+ )
710
+
711
+ results = []
712
+
713
+ # Process each row
714
+ for idx, row in df.iterrows():
715
+ try:
716
+ # Prepare data
717
+ data = {
718
+ 'machine_id': row.get('machine_id', f'MACHINE_{idx+1}'),
719
+ 'air_temperature': float(row['air_temperature']),
720
+ 'process_temperature': float(row['process_temperature']),
721
+ 'rotational_speed': int(row['rotational_speed']),
722
+ 'torque': float(row['torque']),
723
+ 'tool_wear': int(row['tool_wear']),
724
+ 'type': str(row['type']).strip().upper()
725
+ }
726
+
727
+ # Feature engineering
728
+ features_df = engineer_features(data)
729
+
730
+ # Scale and predict
731
+ features_scaled = MODEL_PIPELINE['scaler'].transform(features_df)
732
+ prediction = MODEL_PIPELINE['model'].predict(features_scaled)[0]
733
+ prediction_proba = MODEL_PIPELINE['model'].predict_proba(features_scaled)[0]
734
+ confidence = float(prediction_proba[prediction])
735
+
736
+ # Diagnostics
737
+ diagnostics = analyze_condition(data, prediction, confidence, features_df)
738
+
739
+ # Features dict
740
+ features_dict = features_df.iloc[0].to_dict()
741
+
742
+ # Detect anomalies
743
+ anomalies = detect_anomalies(data, features_df)
744
+
745
+ # Overall health
746
+ overall_health = determine_overall_health(prediction, confidence, diagnostics.severity, len(anomalies))
747
+
748
+ # Append result
749
+ results.append({
750
+ "row_number": idx + 1,
751
+ "machine_id": data['machine_id'],
752
+ "timestamp": datetime.now().isoformat(),
753
+ "prediction": "FAILURE" if prediction == 1 else "HEALTHY",
754
+ "confidence": round(confidence, 4),
755
+ "diagnostics": {
756
+ "primary_cause": diagnostics.primary_cause,
757
+ "sensor_alert": diagnostics.sensor_alert,
758
+ "recommended_action": diagnostics.recommended_action,
759
+ "severity": diagnostics.severity
760
+ },
761
+ "features": features_dict,
762
+ "anomalies": [
763
+ {
764
+ "parameter": a.parameter,
765
+ "value": a.value,
766
+ "normal_range": a.normal_range,
767
+ "status": a.status,
768
+ "explanation": a.explanation
769
+ } for a in anomalies
770
+ ],
771
+ "overall_health": overall_health,
772
+ "input_data": {
773
+ "air_temperature": data['air_temperature'],
774
+ "process_temperature": data['process_temperature'],
775
+ "rotational_speed": data['rotational_speed'],
776
+ "torque": data['torque'],
777
+ "tool_wear": data['tool_wear'],
778
+ "type": data['type']
779
+ }
780
+ })
781
+
782
+ except Exception as e:
783
+ logger.warning(f"Row {idx+1} processing error: {e}")
784
+ results.append({
785
+ "row_number": idx + 1,
786
+ "machine_id": row.get('machine_id', f'MACHINE_{idx+1}'),
787
+ "timestamp": datetime.now().isoformat(),
788
+ "prediction": "ERROR",
789
+ "confidence": 0.0,
790
+ "diagnostics": {
791
+ "primary_cause": "Processing Error",
792
+ "sensor_alert": str(e),
793
+ "recommended_action": "Review input data format and values",
794
+ "severity": "UNKNOWN"
795
+ },
796
+ "error": str(e)
797
+ })
798
+
799
+ # Summary statistics
800
+ total = len(results)
801
+ failures = sum(1 for r in results if r.get('prediction') == 'FAILURE')
802
+ healthy = sum(1 for r in results if r.get('prediction') == 'HEALTHY')
803
+ errors = sum(1 for r in results if r.get('prediction') == 'ERROR')
804
+
805
+ return {
806
+ "summary": {
807
+ "total_records": total,
808
+ "predictions": {
809
+ "failure": failures,
810
+ "healthy": healthy,
811
+ "errors": errors
812
+ },
813
+ "failure_rate": round(failures / total * 100, 2) if total > 0 else 0
814
+ },
815
+ "results": results,
816
+ "processed_at": datetime.now().isoformat()
817
+ }
818
+
819
+ except HTTPException:
820
+ raise
821
+ except Exception as e:
822
+ logger.error(f"Batch JSON prediction error: {e}")
823
+ raise HTTPException(status_code=500, detail=f"Batch processing failed: {str(e)}")
824
+
825
+
826
+ # ============================================================================
827
+ # HEALTH CHECK ENDPOINTS
828
+ # ============================================================================
829
+
830
+ @app.get("/")
831
+ async def root():
832
+ """API root endpoint"""
833
+ return {
834
+ "service": "Predictive Maintenance API",
835
+ "version": "1.0.0",
836
+ "status": "operational",
837
+ "model_loaded": MODEL_PIPELINE is not None,
838
+ "endpoints": {
839
+ "single_prediction": "/predict",
840
+ "batch_prediction": "/predict/batch",
841
+ "health": "/health",
842
+ "model_info": "/model/info"
843
+ }
844
+ }
845
+
846
+
847
+ @app.get("/health")
848
+ async def health_check():
849
+ """Health check endpoint"""
850
+ return {
851
+ "status": "healthy" if MODEL_PIPELINE is not None else "unhealthy",
852
+ "timestamp": datetime.now().isoformat(),
853
+ "model_loaded": MODEL_PIPELINE is not None,
854
+ "model_path": str(MODEL_PATH if MODEL_PATH.exists() else FALLBACK_MODEL_PATH)
855
+ }
856
+
857
+
858
+ @app.get("/model/info")
859
+ async def model_info():
860
+ """Get model information"""
861
+ if MODEL_PIPELINE is None:
862
+ raise HTTPException(status_code=503, detail="Model not loaded")
863
+
864
+ return {
865
+ "model_type": type(MODEL_PIPELINE['model']).__name__,
866
+ "features": MODEL_PIPELINE['features'],
867
+ "model_name": MODEL_PIPELINE.get('model_name', 'Unknown'),
868
+ "performance": MODEL_PIPELINE.get('performance', {}),
869
+ "training_config": MODEL_PIPELINE.get('training_config', {}),
870
+ "random_state": MODEL_PIPELINE.get('random_state', None)
871
+ }
872
+
873
+
874
+ # ============================================================================
875
+ # MAIN
876
+ # ============================================================================
877
+
878
+ if __name__ == "__main__":
879
+ import uvicorn
880
+ uvicorn.run(
881
+ "main:app",
882
+ host="0.0.0.0",
883
+ port=8000,
884
+ reload=True,
885
+ log_level="info"
886
+ )
api/open_test_interface.bat ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo ========================================
3
+ echo Opening Test Interface
4
+ echo ========================================
5
+ echo.
6
+
7
+ REM Check if index.html exists
8
+ if not exist "index.html" (
9
+ echo ERROR: index.html not found in current directory
10
+ echo Please ensure you are running this from the 'api' folder
11
+ pause
12
+ exit /b 1
13
+ )
14
+
15
+ echo Opening index.html in default browser...
16
+ echo.
17
+ echo Make sure the API server is running!
18
+ echo If not, run: start_server.bat
19
+ echo.
20
+ echo API should be available at: http://localhost:8000
21
+ echo.
22
+
23
+ REM Open in default browser
24
+ start "" "index.html"
25
+
26
+ echo.
27
+ echo Test interface opened successfully!
28
+ echo.
29
+ echo Tips:
30
+ echo 1. Check API status indicator (top-right corner)
31
+ echo 2. Try preset scenarios: Healthy, Tool Wear, Overstrain, Cooling
32
+ echo 3. For batch processing, upload CSV with required columns
33
+ echo.
34
+ pause
api/requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.5
2
+ uvicorn==0.32.1
3
+ pydantic==2.10.3
4
+ joblib==1.5.2
5
+ numpy==2.3.5
6
+ scikit-learn==1.7.2
7
+ xgboost==3.1.2
8
+ python-multipart==0.0.9
9
+ pandas==2.2.3
10
+ huggingface_hub==1.1.5
api/start_server.bat ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo ========================================
3
+ echo Starting FastAPI Server
4
+ echo ========================================
5
+ echo.
6
+
7
+ REM Check if Python is installed
8
+ python --version >nul 2>&1
9
+ if errorlevel 1 (
10
+ echo ERROR: Python is not installed or not in PATH
11
+ echo Please run: install_requirements.bat first
12
+ pause
13
+ exit /b 1
14
+ )
15
+
16
+ REM Check if model exists
17
+ if not exist "..\models\model_pipeline_improved.joblib" (
18
+ if not exist "..\models\model_pipeline.joblib" (
19
+ echo WARNING: Model file not found!
20
+ echo Please ensure model file exists in ../models/ directory
21
+ echo.
22
+ )
23
+ )
24
+
25
+ echo Starting server at http://localhost:8000
26
+ echo.
27
+ echo Available endpoints:
28
+ echo - HTML Test Interface: Open index.html in browser
29
+ echo - API Documentation: http://localhost:8000/docs
30
+ echo - Health Check: http://localhost:8000/health
31
+ echo.
32
+ echo Press Ctrl+C to stop the server
33
+ echo ========================================
34
+ echo.
35
+
36
+ REM Start the server
37
+ python main.py
38
+
39
+ if errorlevel 1 (
40
+ echo.
41
+ echo ERROR: Server failed to start
42
+ echo Please check the error messages above
43
+ echo.
44
+ echo Common issues:
45
+ echo 1. Port 8000 is already in use
46
+ echo 2. Missing dependencies (run: install_requirements.bat)
47
+ echo 3. Model file not found
48
+ pause
49
+ exit /b 1
50
+ )
api/test_batch.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ machine_id,air_temperature,process_temperature,rotational_speed,torque,tool_wear,type,extra_column
2
+ M_001,298.1,308.6,1551,42.8,0,M,some_data
3
+ M_002,297.5,312.8,1800,52.0,220,H,other_data
4
+ M_003,298.5,316.0,1900,72.5,150,H,more_data
5
+ M_004,299.8,305.2,1650,45.0,100,M,extra_info
6
+ M_005,300.2,310.5,1400,38.5,50,L,additional
api/test_data.csv ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ machine_id,air_temperature,process_temperature,rotational_speed,torque,tool_wear,type
2
+ M_L_01,298.1,308.6,1551,42.8,0,M
3
+ M_L_02,299.2,310.1,1450,38.5,150,L
4
+ M_H_01,297.5,312.8,1800,65.2,220,H
5
+ M_M_03,300.1,311.5,2600,55.0,180,M
6
+ M_L_04,296.8,305.2,1200,35.0,50,L
7
+ M_H_05,298.5,316.0,1900,72.5,210,H