Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# ============================================
|
| 2 |
# YORK CHILLER OPTIMIZER API
|
| 3 |
-
#
|
| 4 |
# ============================================
|
| 5 |
|
| 6 |
import os
|
|
@@ -8,11 +8,13 @@ import sys
|
|
| 8 |
import warnings
|
| 9 |
warnings.filterwarnings('ignore')
|
| 10 |
|
| 11 |
-
# Import
|
| 12 |
import numpy as np
|
| 13 |
print(f"NumPy version: {np.__version__}")
|
| 14 |
|
| 15 |
import pandas as pd
|
|
|
|
|
|
|
| 16 |
from datetime import datetime
|
| 17 |
from typing import List, Optional, Dict, Any
|
| 18 |
from fastapi import FastAPI, HTTPException
|
|
@@ -64,7 +66,7 @@ def load_model_files():
|
|
| 64 |
print(f" ❌ Missing: production_model.pkl")
|
| 65 |
return False
|
| 66 |
|
| 67 |
-
# Load model with joblib
|
| 68 |
try:
|
| 69 |
model = joblib.load("production_model.pkl")
|
| 70 |
print(f"\n✅ Model loaded successfully with joblib")
|
|
@@ -78,17 +80,8 @@ def load_model_files():
|
|
| 78 |
print(f" Max depth: {model.max_depth}")
|
| 79 |
|
| 80 |
except Exception as e:
|
| 81 |
-
print(f"
|
| 82 |
-
|
| 83 |
-
# Try with pickle as fallback
|
| 84 |
-
try:
|
| 85 |
-
print(" Trying with pickle...")
|
| 86 |
-
with open("production_model.pkl", 'rb') as f:
|
| 87 |
-
model = pickle.load(f)
|
| 88 |
-
print(f"✅ Model loaded successfully with pickle")
|
| 89 |
-
except Exception as e2:
|
| 90 |
-
print(f"❌ Pickle also failed: {e2}")
|
| 91 |
-
return False
|
| 92 |
|
| 93 |
# Load scaler if exists
|
| 94 |
if os.path.exists("scaler.pkl"):
|
|
@@ -97,34 +90,39 @@ def load_model_files():
|
|
| 97 |
print(f"✅ Scaler loaded")
|
| 98 |
except Exception as e:
|
| 99 |
print(f"⚠️ Could not load scaler: {e}")
|
| 100 |
-
try:
|
| 101 |
-
with open("scaler.pkl", 'rb') as f:
|
| 102 |
-
scaler = pickle.load(f)
|
| 103 |
-
print(f"✅ Scaler loaded with pickle")
|
| 104 |
-
except:
|
| 105 |
-
pass
|
| 106 |
|
| 107 |
# Load feature names from features.pkl
|
| 108 |
if os.path.exists("features.pkl"):
|
| 109 |
try:
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
print(f" First 3 features: {feature_names[:3]}")
|
| 114 |
except Exception as e:
|
| 115 |
print(f"⚠️ Could not load features.pkl: {e}")
|
| 116 |
-
|
| 117 |
-
with open("features.pkl", 'rb') as f:
|
| 118 |
-
feature_names = pickle.load(f)
|
| 119 |
-
print(f"✅ Features loaded with pickle: {len(feature_names)} features")
|
| 120 |
-
except:
|
| 121 |
-
pass
|
| 122 |
|
| 123 |
# Use default feature names if none loaded
|
| 124 |
if feature_names is None:
|
| 125 |
feature_names = EXPECTED_FEATURES
|
| 126 |
print(f"✅ Using default feature names: {len(feature_names)} features")
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
return True
|
| 129 |
|
| 130 |
# Load model on startup
|
|
@@ -133,11 +131,11 @@ load_success = load_model_files()
|
|
| 133 |
if model:
|
| 134 |
print(f"\n📊 Model Status: ✅ ONLINE")
|
| 135 |
print(f" NumPy version: {np.__version__}")
|
| 136 |
-
print(f"
|
|
|
|
| 137 |
print(f" Scaler: {'✅' if scaler else '❌'}")
|
| 138 |
else:
|
| 139 |
print(f"\n📊 Model Status: ❌ OFFLINE")
|
| 140 |
-
print(f" NumPy version: {np.__version__}")
|
| 141 |
|
| 142 |
# ============================================
|
| 143 |
# REQUEST MODELS
|
|
@@ -189,8 +187,10 @@ class OptimizeResponse(BaseModel):
|
|
| 189 |
# PREDICTION FUNCTIONS
|
| 190 |
# ============================================
|
| 191 |
|
| 192 |
-
def
|
| 193 |
-
"""
|
|
|
|
|
|
|
| 194 |
|
| 195 |
# Create feature array with exact order
|
| 196 |
features = np.array([[
|
|
@@ -206,24 +206,16 @@ def prepare_features(input_data: ChillerInput) -> np.ndarray:
|
|
| 206 |
input_data.day_of_week,
|
| 207 |
input_data.month,
|
| 208 |
input_data.day_of_year
|
| 209 |
-
]])
|
| 210 |
|
| 211 |
# Apply scaler if available
|
| 212 |
if scaler is not None:
|
| 213 |
try:
|
| 214 |
features = scaler.transform(features)
|
| 215 |
-
print(f" Applied scaler transformation")
|
| 216 |
except Exception as e:
|
| 217 |
print(f" ⚠️ Scaler transform failed: {e}")
|
| 218 |
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
def predict_kw_per_tr(input_data: ChillerInput) -> float:
|
| 222 |
-
"""Predict Combined_Kw_per_TR using the loaded model"""
|
| 223 |
-
if model is None:
|
| 224 |
-
raise ValueError("Model not loaded")
|
| 225 |
-
|
| 226 |
-
features = prepare_features(input_data)
|
| 227 |
prediction = model.predict(features)[0]
|
| 228 |
|
| 229 |
# Clip to realistic range
|
|
@@ -253,9 +245,10 @@ def optimize_chw_setpoint(input_data: ChillerInput) -> tuple:
|
|
| 253 |
best_sp = current_sp
|
| 254 |
|
| 255 |
for sp in test_setpoints:
|
| 256 |
-
# Create copy with new setpoint
|
| 257 |
-
|
| 258 |
-
|
|
|
|
| 259 |
|
| 260 |
try:
|
| 261 |
kw = predict_kw_per_tr(test_input)
|
|
@@ -263,7 +256,6 @@ def optimize_chw_setpoint(input_data: ChillerInput) -> tuple:
|
|
| 263 |
best_kw = kw
|
| 264 |
best_sp = sp
|
| 265 |
except Exception as e:
|
| 266 |
-
print(f" Error testing setpoint {sp}: {e}")
|
| 267 |
continue
|
| 268 |
|
| 269 |
return best_sp, best_kw
|
|
@@ -275,6 +267,8 @@ def optimize_chw_setpoint(input_data: ChillerInput) -> tuple:
|
|
| 275 |
@app.get("/")
|
| 276 |
async def root():
|
| 277 |
"""Root endpoint with API information"""
|
|
|
|
|
|
|
| 278 |
return {
|
| 279 |
"service": "York Chiller Energy Optimizer",
|
| 280 |
"model_type": "Random Forest Regressor",
|
|
@@ -282,10 +276,10 @@ async def root():
|
|
| 282 |
"status": "online" if model is not None else "model_not_loaded",
|
| 283 |
"model_info": {
|
| 284 |
"loaded": model is not None,
|
| 285 |
-
"features":
|
| 286 |
-
"feature_count": len(feature_names) if feature_names else len(EXPECTED_FEATURES),
|
| 287 |
"scaler_loaded": scaler is not None,
|
| 288 |
-
"numpy_version": np.__version__
|
|
|
|
| 289 |
},
|
| 290 |
"endpoints": {
|
| 291 |
"/": "This information",
|
|
@@ -305,13 +299,16 @@ async def root():
|
|
| 305 |
@app.get("/health")
|
| 306 |
async def health():
|
| 307 |
"""Health check endpoint"""
|
|
|
|
|
|
|
| 308 |
return {
|
| 309 |
"status": "healthy" if model is not None else "degraded",
|
| 310 |
"model_loaded": model is not None,
|
| 311 |
"model_type": type(model).__name__ if model else None,
|
| 312 |
-
"feature_count":
|
| 313 |
"scaler_loaded": scaler is not None,
|
| 314 |
-
"numpy_version": np.__version__
|
|
|
|
| 315 |
}
|
| 316 |
|
| 317 |
@app.post("/predict", response_model=PredictionResponse)
|
|
@@ -324,11 +321,13 @@ async def predict_endpoint(input_data: ChillerInput):
|
|
| 324 |
kw_per_tr = predict_kw_per_tr(input_data)
|
| 325 |
rating = get_efficiency_rating(kw_per_tr)
|
| 326 |
|
|
|
|
|
|
|
| 327 |
return PredictionResponse(
|
| 328 |
status="success",
|
| 329 |
kw_per_tr=round(kw_per_tr, 4),
|
| 330 |
efficiency_rating=rating,
|
| 331 |
-
features_used=
|
| 332 |
model_type="RandomForestRegressor",
|
| 333 |
timestamp=datetime.now().isoformat()
|
| 334 |
)
|
|
@@ -364,7 +363,7 @@ async def optimize_endpoint(input_data: ChillerInput):
|
|
| 364 |
recommended_value=f"{optimal_sp:.1f}°C",
|
| 365 |
expected_savings=f"{savings_pct:.1f}%",
|
| 366 |
priority="HIGH" if savings_pct > 5 else "MEDIUM",
|
| 367 |
-
operator_action=f"Adjust CHW setpoint
|
| 368 |
))
|
| 369 |
|
| 370 |
# Load-based recommendations
|
|
|
|
| 1 |
# ============================================
|
| 2 |
# YORK CHILLER OPTIMIZER API
|
| 3 |
+
# Compatible with NumPy 2.0.2 and pandas 2.2.3
|
| 4 |
# ============================================
|
| 5 |
|
| 6 |
import os
|
|
|
|
| 8 |
import warnings
|
| 9 |
warnings.filterwarnings('ignore')
|
| 10 |
|
| 11 |
+
# Import libraries
|
| 12 |
import numpy as np
|
| 13 |
print(f"NumPy version: {np.__version__}")
|
| 14 |
|
| 15 |
import pandas as pd
|
| 16 |
+
print(f"Pandas version: {pd.__version__}")
|
| 17 |
+
|
| 18 |
from datetime import datetime
|
| 19 |
from typing import List, Optional, Dict, Any
|
| 20 |
from fastapi import FastAPI, HTTPException
|
|
|
|
| 66 |
print(f" ❌ Missing: production_model.pkl")
|
| 67 |
return False
|
| 68 |
|
| 69 |
+
# Load model with joblib
|
| 70 |
try:
|
| 71 |
model = joblib.load("production_model.pkl")
|
| 72 |
print(f"\n✅ Model loaded successfully with joblib")
|
|
|
|
| 80 |
print(f" Max depth: {model.max_depth}")
|
| 81 |
|
| 82 |
except Exception as e:
|
| 83 |
+
print(f"⚠️ Error loading model with joblib: {e}")
|
| 84 |
+
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
# Load scaler if exists
|
| 87 |
if os.path.exists("scaler.pkl"):
|
|
|
|
| 90 |
print(f"✅ Scaler loaded")
|
| 91 |
except Exception as e:
|
| 92 |
print(f"⚠️ Could not load scaler: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
# Load feature names from features.pkl
|
| 95 |
if os.path.exists("features.pkl"):
|
| 96 |
try:
|
| 97 |
+
feature_data = joblib.load("features.pkl")
|
| 98 |
+
# Convert to list if it's a pandas Series or other type
|
| 99 |
+
if hasattr(feature_data, 'tolist'):
|
| 100 |
+
feature_names = feature_data.tolist()
|
| 101 |
+
elif isinstance(feature_data, (list, tuple)):
|
| 102 |
+
feature_names = list(feature_data)
|
| 103 |
+
elif hasattr(feature_data, 'values'):
|
| 104 |
+
feature_names = list(feature_data.values)
|
| 105 |
+
else:
|
| 106 |
+
feature_names = list(feature_data)
|
| 107 |
+
print(f"✅ Features loaded from features.pkl: {len(feature_names)} features")
|
| 108 |
+
if len(feature_names) > 0:
|
| 109 |
print(f" First 3 features: {feature_names[:3]}")
|
| 110 |
except Exception as e:
|
| 111 |
print(f"⚠️ Could not load features.pkl: {e}")
|
| 112 |
+
feature_names = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
# Use default feature names if none loaded
|
| 115 |
if feature_names is None:
|
| 116 |
feature_names = EXPECTED_FEATURES
|
| 117 |
print(f"✅ Using default feature names: {len(feature_names)} features")
|
| 118 |
|
| 119 |
+
# Ensure feature_names is a list
|
| 120 |
+
if not isinstance(feature_names, list):
|
| 121 |
+
if hasattr(feature_names, 'tolist'):
|
| 122 |
+
feature_names = feature_names.tolist()
|
| 123 |
+
else:
|
| 124 |
+
feature_names = list(feature_names)
|
| 125 |
+
|
| 126 |
return True
|
| 127 |
|
| 128 |
# Load model on startup
|
|
|
|
| 131 |
if model:
|
| 132 |
print(f"\n📊 Model Status: ✅ ONLINE")
|
| 133 |
print(f" NumPy version: {np.__version__}")
|
| 134 |
+
print(f" Pandas version: {pd.__version__}")
|
| 135 |
+
print(f" Features: {len(feature_names) if feature_names else 0}")
|
| 136 |
print(f" Scaler: {'✅' if scaler else '❌'}")
|
| 137 |
else:
|
| 138 |
print(f"\n📊 Model Status: ❌ OFFLINE")
|
|
|
|
| 139 |
|
| 140 |
# ============================================
|
| 141 |
# REQUEST MODELS
|
|
|
|
| 187 |
# PREDICTION FUNCTIONS
|
| 188 |
# ============================================
|
| 189 |
|
| 190 |
+
def predict_kw_per_tr(input_data: ChillerInput) -> float:
|
| 191 |
+
"""Predict Combined_Kw_per_TR using the loaded model"""
|
| 192 |
+
if model is None:
|
| 193 |
+
raise ValueError("Model not loaded")
|
| 194 |
|
| 195 |
# Create feature array with exact order
|
| 196 |
features = np.array([[
|
|
|
|
| 206 |
input_data.day_of_week,
|
| 207 |
input_data.month,
|
| 208 |
input_data.day_of_year
|
| 209 |
+
]], dtype=np.float64)
|
| 210 |
|
| 211 |
# Apply scaler if available
|
| 212 |
if scaler is not None:
|
| 213 |
try:
|
| 214 |
features = scaler.transform(features)
|
|
|
|
| 215 |
except Exception as e:
|
| 216 |
print(f" ⚠️ Scaler transform failed: {e}")
|
| 217 |
|
| 218 |
+
# Predict
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
prediction = model.predict(features)[0]
|
| 220 |
|
| 221 |
# Clip to realistic range
|
|
|
|
| 245 |
best_sp = current_sp
|
| 246 |
|
| 247 |
for sp in test_setpoints:
|
| 248 |
+
# Create copy with new setpoint using dict
|
| 249 |
+
input_dict = input_data.dict()
|
| 250 |
+
input_dict['current_chw_setpoint_c'] = sp
|
| 251 |
+
test_input = ChillerInput(**input_dict)
|
| 252 |
|
| 253 |
try:
|
| 254 |
kw = predict_kw_per_tr(test_input)
|
|
|
|
| 256 |
best_kw = kw
|
| 257 |
best_sp = sp
|
| 258 |
except Exception as e:
|
|
|
|
| 259 |
continue
|
| 260 |
|
| 261 |
return best_sp, best_kw
|
|
|
|
| 267 |
@app.get("/")
|
| 268 |
async def root():
|
| 269 |
"""Root endpoint with API information"""
|
| 270 |
+
feature_count = len(feature_names) if feature_names and isinstance(feature_names, list) else len(EXPECTED_FEATURES)
|
| 271 |
+
|
| 272 |
return {
|
| 273 |
"service": "York Chiller Energy Optimizer",
|
| 274 |
"model_type": "Random Forest Regressor",
|
|
|
|
| 276 |
"status": "online" if model is not None else "model_not_loaded",
|
| 277 |
"model_info": {
|
| 278 |
"loaded": model is not None,
|
| 279 |
+
"features": feature_count,
|
|
|
|
| 280 |
"scaler_loaded": scaler is not None,
|
| 281 |
+
"numpy_version": np.__version__,
|
| 282 |
+
"pandas_version": pd.__version__
|
| 283 |
},
|
| 284 |
"endpoints": {
|
| 285 |
"/": "This information",
|
|
|
|
| 299 |
@app.get("/health")
|
| 300 |
async def health():
|
| 301 |
"""Health check endpoint"""
|
| 302 |
+
feature_count = len(feature_names) if feature_names and isinstance(feature_names, list) else len(EXPECTED_FEATURES)
|
| 303 |
+
|
| 304 |
return {
|
| 305 |
"status": "healthy" if model is not None else "degraded",
|
| 306 |
"model_loaded": model is not None,
|
| 307 |
"model_type": type(model).__name__ if model else None,
|
| 308 |
+
"feature_count": feature_count,
|
| 309 |
"scaler_loaded": scaler is not None,
|
| 310 |
+
"numpy_version": np.__version__,
|
| 311 |
+
"pandas_version": pd.__version__
|
| 312 |
}
|
| 313 |
|
| 314 |
@app.post("/predict", response_model=PredictionResponse)
|
|
|
|
| 321 |
kw_per_tr = predict_kw_per_tr(input_data)
|
| 322 |
rating = get_efficiency_rating(kw_per_tr)
|
| 323 |
|
| 324 |
+
feature_count = len(feature_names) if feature_names and isinstance(feature_names, list) else len(EXPECTED_FEATURES)
|
| 325 |
+
|
| 326 |
return PredictionResponse(
|
| 327 |
status="success",
|
| 328 |
kw_per_tr=round(kw_per_tr, 4),
|
| 329 |
efficiency_rating=rating,
|
| 330 |
+
features_used=feature_count,
|
| 331 |
model_type="RandomForestRegressor",
|
| 332 |
timestamp=datetime.now().isoformat()
|
| 333 |
)
|
|
|
|
| 363 |
recommended_value=f"{optimal_sp:.1f}°C",
|
| 364 |
expected_savings=f"{savings_pct:.1f}%",
|
| 365 |
priority="HIGH" if savings_pct > 5 else "MEDIUM",
|
| 366 |
+
operator_action=f"Adjust CHW setpoint to {optimal_sp:.1f}°C"
|
| 367 |
))
|
| 368 |
|
| 369 |
# Load-based recommendations
|