File size: 8,788 Bytes
7690851
 
 
 
 
 
de597ec
7690851
 
 
 
 
 
de597ec
 
 
7690851
 
 
 
de597ec
7690851
 
de597ec
 
aeaf551
 
 
 
de597ec
aeaf551
de597ec
 
 
 
 
 
 
d8c5e83
 
7690851
aeaf551
 
7690851
 
 
 
 
 
 
 
aeaf551
 
7690851
 
 
 
 
 
 
 
aeaf551
 
7690851
 
 
 
 
caa411d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7690851
aeaf551
 
7690851
d8c5e83
 
7690851
 
 
 
 
aeaf551
 
de597ec
aeaf551
7690851
 
 
 
 
 
 
 
aeaf551
 
 
 
caa411d
aeaf551
7690851
 
 
caa411d
 
7690851
 
 
caa411d
 
7690851
 
 
caa411d
 
7690851
caa411d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7690851
aeaf551
caa411d
aeaf551
caa411d
 
 
 
 
7690851
 
aeaf551
1457ba8
de597ec
aeaf551
 
 
 
 
de597ec
aeaf551
 
 
1457ba8
 
 
 
 
 
de597ec
aeaf551
1457ba8
caa411d
 
 
aeaf551
 
 
caa411d
 
aeaf551
de597ec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import json
import os
import joblib
import torch
import numpy as np
import pandas as pd
import logging
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import IsolationForest
from safetensors.torch import load_file

from ml.features import build_features
from ml.lstm_model import LSTMAutoencoder
from src.config import MLConfig

logger = logging.getLogger(__name__)

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
ARTIFACTS_DIR = os.path.join(BASE_DIR, "artifacts")


class MLEngine:
    def __init__(self):
        logger.info("Initializing ML Engine...")
        self._load_ml_config()
        self._load_scaler()
        self._load_isolation_forest()
        self._load_xgboost_models()
        self._load_lstm_model()
        logger.info("ML Engine initialized successfully")

    def _load_ml_config(self):
        """Load ML configuration from config."""
        config = MLConfig.load()
        self.feature_cols = config["feature_cols"]
        self.window = config["window"]
        self.seq_len = config["seq_len"]
        self.design_life_days = config["design_life_days"]
        self.lstm_input_dim = config.get("lstm_input_dim", len(self.feature_cols))
        self.lstm_hidden_dim = config.get("lstm_hidden_dim", 32)

    def _load_scaler(self):
        """Load and reconstruct StandardScaler from JSON."""
        with open(os.path.join(ARTIFACTS_DIR, "scaler.json"), "r") as f:
            params = json.load(f)
        self.scaler = StandardScaler()
        self.scaler.mean_ = np.array(params["mean"])
        self.scaler.scale_ = np.array(params["scale"])
        self.scaler.var_ = self.scaler.scale_ ** 2
        self.scaler.n_features_in_ = len(self.scaler.mean_)

    def _load_isolation_forest(self):
        """Load and retrain IsolationForest using saved training data."""
        self.iso = IsolationForest(
            n_estimators=200,
            contamination=0.05,
            random_state=42
        )
        train_data = pd.read_json(os.path.join(ARTIFACTS_DIR, "training_data.json"))
        self.iso.fit(train_data[self.feature_cols])

    def _load_xgboost_models(self):
        """Load XGBoost models from JSON artifacts."""
        import xgboost as xgb
        self.ttf_model = xgb.XGBRegressor()
        self.ttf_model.load_model(os.path.join(ARTIFACTS_DIR, "xgb_ttf.json"))
        self.fail_model = xgb.XGBClassifier()
        self.fail_model.load_model(os.path.join(ARTIFACTS_DIR, "xgb_fail.json"))
        
        # Load fault type classifier if available
        fault_model_path = os.path.join(ARTIFACTS_DIR, "xgb_fault.json")
        if os.path.exists(fault_model_path):
            self.fault_model = xgb.XGBClassifier()
            self.fault_model.load_model(fault_model_path)
            self.fault_map = {
                0: "Normal",
                1: "Short Circuit", 
                2: "Degradation",
                3: "Open Circuit",
                4: "Shadowing"
            }
        else:
            self.fault_model = None

    def _load_lstm_model(self):
        """Load LSTM autoencoder from safetensors."""
        self.lstm = LSTMAutoencoder(
            input_dim=self.lstm_input_dim,
            hidden_dim=self.lstm_hidden_dim
        )
        state_dict = load_file(os.path.join(ARTIFACTS_DIR, "lstm_autoencoder.safetensors"))
        self.lstm.load_state_dict(state_dict)
        self.lstm.eval()

    def _compute_anomalies(self, df_scaled: pd.DataFrame) -> tuple:
        """Compute anomaly scores from LSTM and IsolationForest.
        Returns: (anomaly_lstm, health) tuple
        """
        df_scaled["anomaly_iforest"] = -self.iso.decision_function(df_scaled)
        X = df_scaled[self.feature_cols].values
        X_seq = np.array([X[-self.seq_len:]])
        with torch.no_grad():
            recon = self.lstm(torch.tensor(X_seq, dtype=torch.float32))
        anomaly_lstm = float(((recon - torch.tensor(X_seq)) ** 2).mean())
        anomaly_norm = min(anomaly_lstm / 1e6, 1.0)
        health = max(0.0, 1.0 - anomaly_norm)
        return anomaly_lstm, health

    def _make_predictions(self, df_scaled: pd.DataFrame, anomaly_lstm: float, health: float) -> dict:
        """Make TTF and failure probability predictions.
        Returns: Dictionary with all predictions including new metrics
        """
        latest_features = df_scaled[self.feature_cols].iloc[[-1]].copy()
        latest_features["anomaly_lstm"] = anomaly_lstm
        latest_features["health_index"] = health
        
        # TTF prediction
        expected_ttf_days = float(
            self.ttf_model.predict(latest_features, validate_features=False)[0]
        )
        
        # Failure probability (improved calculation from Colab)
        failure_probability = float(
            self.fail_model.predict_proba(latest_features, validate_features=False)[0][1]
        )
        
        # RUL calculation
        expected_rul_days = float(health * self.design_life_days)
        
        # Health trend (over last 200 points if available)
        if len(df_scaled) >= 200:
            # Calculate health trend by computing health over the window
            recent_health_values = []
            for i in range(max(0, len(df_scaled) - 200), len(df_scaled)):
                temp_df = df_scaled.iloc[:i+1]
                if len(temp_df) >= self.seq_len:
                    temp_anomaly, temp_health = self._compute_anomalies(temp_df.iloc[-self.seq_len:])
                    recent_health_values.append(temp_health)
            health_trend = recent_health_values[-1] - recent_health_values[0] if recent_health_values else 0.0
        else:
            health_trend = 0.0
            
        # Fault type prediction
        predicted_fault_type = "Unknown"
        fault_confidence = 0.0
        if self.fault_model is not None:
            fault_pred = int(self.fault_model.predict(latest_features, validate_features=False)[0])
            predicted_fault_type = self.fault_map.get(fault_pred, "Unknown")
            fault_proba = self.fault_model.predict_proba(latest_features, validate_features=False)[0]
            fault_confidence = float(np.max(fault_proba))
        
        # Improved failure probability calculation (from Colab)
        ttf_norm = 1 - min(expected_ttf_days / self.design_life_days, 1.0)
        health_risk = 1 - health
        trend_risk = max(-health_trend, 0) * 50
        anomaly_risk = min(anomaly_lstm / 1e6, 1.0)  # Normalize anomaly
        
        improved_failure_prob = (
            0.35 * anomaly_risk +
            0.30 * health_risk + 
            0.20 * ttf_norm +
            0.15 * trend_risk
        )
        improved_failure_prob = min(max(improved_failure_prob, 0), 1)
        
        # Overall confidence
        confidence = round(0.5 * abs(improved_failure_prob - 0.5) * 2 + 0.5 * health, 2)
        
        return {
            "ttf_days": expected_ttf_days,
            "failure_prob": improved_failure_prob,
            "rul_days": expected_rul_days,
            "health_score": round(health, 3),
            "anomaly_score": round(anomaly_lstm, 4),
            "health_trend_200step": round(health_trend, 4),
            "predicted_fault_type": predicted_fault_type,
            "fault_confidence": round(fault_confidence, 2),
            "confidence": confidence
        }

    def predict_from_raw(self, raw_df: pd.DataFrame, asset_id: str = None):
        logger.info("ML analysis start")
        df = build_features(raw_df, self.window)
        df = df[self.feature_cols].dropna()
        if len(df) < self.seq_len:
            raise ValueError("Not enough data for LSTM sequence")
        df_scaled = pd.DataFrame(
            self.scaler.transform(df), columns=self.feature_cols, index=df.index
        )
        anomaly_lstm, health = self._compute_anomalies(df_scaled)
        predictions = self._make_predictions(df_scaled, anomaly_lstm, health)
        
        # Use provided asset_id or generate default
        if asset_id is None:
            import uuid
            asset_id = f"Solar_Panel_{str(uuid.uuid4())[:8]}"
        
        logger.info("ML analysis end")
        return {
            "asset_id": asset_id,
            "health_score": predictions["health_score"],
            "anomaly_score": predictions["anomaly_score"],
            "health_trend_200step": predictions["health_trend_200step"],
            "failure_probability": round(predictions["failure_prob"], 2),
            "expected_ttf_days": round(predictions["ttf_days"], 1),
            "expected_rul_days": round(predictions["rul_days"], 1),
            "predicted_fault_type": predictions["predicted_fault_type"],
            "fault_confidence": predictions["fault_confidence"],
            "confidence": predictions["confidence"]
        }