Commit
·
03bcd34
0
Parent(s):
init
Browse files- .gitignore +3 -0
- Dockerfile +23 -0
- README.md +57 -0
- api/README.md +379 -0
- api/index.html +1155 -0
- api/install_requirements.bat +50 -0
- api/main.py +886 -0
- api/open_test_interface.bat +34 -0
- api/requirements.txt +10 -0
- api/start_server.bat +50 -0
- api/test_batch.csv +6 -0
- api/test_data.csv +7 -0
.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
|