Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# ============================================
|
| 2 |
# YORK CHILLER OPTIMIZER API
|
| 3 |
-
# Using existing model files with NumPy
|
| 4 |
# ============================================
|
| 5 |
|
| 6 |
import os
|
|
@@ -8,31 +8,22 @@ import sys
|
|
| 8 |
import warnings
|
| 9 |
warnings.filterwarnings('ignore')
|
| 10 |
|
| 11 |
-
#
|
| 12 |
-
# This is needed for models saved with NumPy 2.0
|
| 13 |
-
import importlib
|
| 14 |
-
import types
|
| 15 |
-
|
| 16 |
-
# Create the missing _core module
|
| 17 |
-
if 'numpy._core' not in sys.modules:
|
| 18 |
-
mock_core = types.ModuleType('numpy._core')
|
| 19 |
-
sys.modules['numpy._core'] = mock_core
|
| 20 |
-
print("✅ Created numpy._core module for compatibility")
|
| 21 |
-
|
| 22 |
-
# Now import numpy (this will use our mock)
|
| 23 |
import numpy as np
|
| 24 |
print(f"NumPy version: {np.__version__}")
|
| 25 |
|
| 26 |
-
#
|
| 27 |
import joblib
|
| 28 |
from datetime import datetime
|
| 29 |
from typing import List, Optional, Dict, Any
|
| 30 |
from fastapi import FastAPI, HTTPException
|
| 31 |
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
| 32 |
|
| 33 |
app = FastAPI(
|
| 34 |
title="York Chiller Energy Optimizer",
|
| 35 |
-
description="Random Forest Model for Chiller Energy Efficiency",
|
| 36 |
version="2.0.0"
|
| 37 |
)
|
| 38 |
|
|
@@ -52,26 +43,22 @@ def load_existing_model():
|
|
| 52 |
print("\n📂 Checking for model files...")
|
| 53 |
|
| 54 |
# Check what files exist
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
else:
|
| 64 |
-
print(f" ⚠️ Missing: scaler.pkl")
|
| 65 |
|
| 66 |
-
if os.path.exists("
|
| 67 |
-
print(
|
| 68 |
-
|
| 69 |
-
print(f" ⚠️ Missing: features.pkl")
|
| 70 |
|
| 71 |
-
# Load the model
|
| 72 |
try:
|
| 73 |
-
# Try
|
| 74 |
-
import joblib
|
| 75 |
model = joblib.load("production_model.pkl")
|
| 76 |
print(f"\n✅ Model loaded successfully")
|
| 77 |
print(f" Type: {type(model).__name__}")
|
|
@@ -92,18 +79,9 @@ def load_existing_model():
|
|
| 92 |
|
| 93 |
except Exception as e:
|
| 94 |
print(f"❌ Error loading model: {e}")
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
print(" Trying alternative loading method...")
|
| 99 |
-
import pickle
|
| 100 |
-
with open("production_model.pkl", 'rb') as f:
|
| 101 |
-
model = pickle.load(f)
|
| 102 |
-
print(f"✅ Model loaded with pickle")
|
| 103 |
-
model_feature_count = 12
|
| 104 |
-
except Exception as e2:
|
| 105 |
-
print(f"❌ Alternative loading failed: {e2}")
|
| 106 |
-
return False
|
| 107 |
|
| 108 |
# Load scaler
|
| 109 |
try:
|
|
@@ -119,10 +97,12 @@ def load_existing_model():
|
|
| 119 |
if os.path.exists("features.pkl"):
|
| 120 |
feature_names = joblib.load("features.pkl")
|
| 121 |
print(f"✅ Features loaded from features.pkl")
|
| 122 |
-
print(f" Features: {feature_names}")
|
| 123 |
if isinstance(feature_names, list):
|
| 124 |
-
|
| 125 |
-
print(f"
|
|
|
|
|
|
|
|
|
|
| 126 |
except Exception as e:
|
| 127 |
print(f"⚠️ Could not load feature names: {e}")
|
| 128 |
# Create default 12 feature names
|
|
@@ -149,13 +129,14 @@ load_success = load_existing_model()
|
|
| 149 |
|
| 150 |
if model:
|
| 151 |
print(f"\n📊 Model Configuration:")
|
| 152 |
-
print(f" Status: ONLINE")
|
| 153 |
print(f" Features expected: {model_feature_count}")
|
| 154 |
-
print(f" Scaler: {'Loaded' if scaler else 'Not loaded'}")
|
|
|
|
| 155 |
else:
|
| 156 |
print(f"\n📊 Model Configuration:")
|
| 157 |
-
print(f" Status:
|
| 158 |
-
print(f"
|
| 159 |
|
| 160 |
# ============================================
|
| 161 |
# REQUEST MODELS
|
|
@@ -177,8 +158,8 @@ class ChillerInput(BaseModel):
|
|
| 177 |
day_of_year: int = Field(..., ge=1, le=366, description="Day of the year (1-365)")
|
| 178 |
|
| 179 |
# Optional optimization parameters
|
| 180 |
-
current_chw_setpoint_c: Optional[float] = Field(8.0, ge=5, le=10)
|
| 181 |
-
current_limit_pct: Optional[float] = Field(100, ge=50, le=100)
|
| 182 |
|
| 183 |
class PredictionResponse(BaseModel):
|
| 184 |
status: str
|
|
@@ -204,42 +185,58 @@ class OptimizeResponse(BaseModel):
|
|
| 204 |
summary: Dict[str, str]
|
| 205 |
|
| 206 |
# ============================================
|
| 207 |
-
#
|
| 208 |
# ============================================
|
| 209 |
|
| 210 |
-
def
|
| 211 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
-
#
|
| 214 |
if feature_names and isinstance(feature_names, list):
|
| 215 |
-
# Build features in the order specified by features.pkl
|
| 216 |
-
feature_dict = {
|
| 217 |
-
'total_building_load_rt': input_data.total_building_load_rt,
|
| 218 |
-
'avg_chilled_water_rate_lps': input_data.avg_chilled_water_rate_lps,
|
| 219 |
-
'avg_cooling_water_temp_c': input_data.avg_cooling_water_temp_c,
|
| 220 |
-
'avg_outside_temp_f': input_data.avg_outside_temp_f,
|
| 221 |
-
'avg_dew_point_f': input_data.avg_dew_point_f,
|
| 222 |
-
'avg_humidity_pct': input_data.avg_humidity_pct,
|
| 223 |
-
'avg_wind_speed_mph': input_data.avg_wind_speed_mph,
|
| 224 |
-
'avg_pressure_in': input_data.avg_pressure_in,
|
| 225 |
-
'hour': input_data.hour,
|
| 226 |
-
'day_of_week': input_data.day_of_week,
|
| 227 |
-
'month': input_data.month,
|
| 228 |
-
'day_of_year': input_data.day_of_year
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
# Build array in the order of feature_names
|
| 232 |
features_list = []
|
| 233 |
for f_name in feature_names:
|
| 234 |
if f_name in feature_dict:
|
| 235 |
features_list.append(feature_dict[f_name])
|
| 236 |
else:
|
| 237 |
-
#
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
features = np.array([features_list])
|
| 242 |
-
print(f"
|
| 243 |
|
| 244 |
else:
|
| 245 |
# Default: use all 12 features in standard order
|
|
@@ -257,7 +254,7 @@ def prepare_features(input_data: ChillerInput) -> np.ndarray:
|
|
| 257 |
input_data.month,
|
| 258 |
input_data.day_of_year
|
| 259 |
]])
|
| 260 |
-
print(f"
|
| 261 |
|
| 262 |
# Apply scaler if available
|
| 263 |
if scaler is not None:
|
|
@@ -270,12 +267,14 @@ def prepare_features(input_data: ChillerInput) -> np.ndarray:
|
|
| 270 |
return features
|
| 271 |
|
| 272 |
def predict_kw_per_tr(input_data: ChillerInput) -> float:
|
| 273 |
-
"""Predict Combined_Kw_per_TR"""
|
| 274 |
if model is None:
|
| 275 |
raise ValueError("Model not loaded")
|
| 276 |
|
| 277 |
-
features =
|
| 278 |
prediction = model.predict(features)[0]
|
|
|
|
|
|
|
| 279 |
prediction = np.clip(prediction, 0.4, 1.2)
|
| 280 |
|
| 281 |
return float(prediction)
|
|
@@ -296,7 +295,8 @@ async def root():
|
|
| 296 |
"loaded": model is not None,
|
| 297 |
"features_expected": model_feature_count,
|
| 298 |
"features_from_pkl": len(feature_names) if feature_names else 0,
|
| 299 |
-
"scaler_loaded": scaler is not None
|
|
|
|
| 300 |
},
|
| 301 |
"endpoints": {
|
| 302 |
"/": "This information",
|
|
@@ -305,7 +305,8 @@ async def root():
|
|
| 305 |
"/optimize": "POST - Get optimization recommendations"
|
| 306 |
},
|
| 307 |
"interpretation": {
|
| 308 |
-
"kw_per_tr": "Combined energy efficiency - LOWER is better",
|
|
|
|
| 309 |
"excellent": "< 0.55 kW/TR",
|
| 310 |
"good": "0.55-0.65 kW/TR",
|
| 311 |
"fair": "0.65-0.75 kW/TR",
|
|
@@ -327,10 +328,10 @@ async def health():
|
|
| 327 |
|
| 328 |
@app.post("/predict", response_model=PredictionResponse)
|
| 329 |
async def predict_endpoint(input_data: ChillerInput):
|
| 330 |
-
"""Predict Combined_Kw_per_TR"""
|
| 331 |
try:
|
| 332 |
if model is None:
|
| 333 |
-
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 334 |
|
| 335 |
kw_per_tr = predict_kw_per_tr(input_data)
|
| 336 |
|
|
@@ -350,12 +351,12 @@ async def optimize_endpoint(input_data: ChillerInput):
|
|
| 350 |
"""Get optimization recommendations"""
|
| 351 |
try:
|
| 352 |
if model is None:
|
| 353 |
-
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 354 |
|
| 355 |
# Current efficiency
|
| 356 |
current_kw = predict_kw_per_tr(input_data)
|
| 357 |
|
| 358 |
-
# Test different CHW setpoints
|
| 359 |
current_sp = input_data.current_chw_setpoint_c or 8.0
|
| 360 |
test_setpoints = [6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0]
|
| 361 |
|
|
@@ -387,7 +388,27 @@ async def optimize_endpoint(input_data: ChillerInput):
|
|
| 387 |
recommended_value=f"{best_sp:.1f}°C",
|
| 388 |
expected_savings=f"{savings_pct:.1f}%",
|
| 389 |
priority="HIGH" if savings_pct > 5 else "MEDIUM",
|
| 390 |
-
operator_action=f"Adjust CHW setpoint to {best_sp:.1f}°C"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
))
|
| 392 |
|
| 393 |
# Efficiency rating
|
|
@@ -409,7 +430,8 @@ async def optimize_endpoint(input_data: ChillerInput):
|
|
| 409 |
"optimal_efficiency": f"{best_kw:.3f} kW/TR",
|
| 410 |
"potential_savings": f"{savings_pct:.1f}%",
|
| 411 |
"efficiency_rating": rating,
|
| 412 |
-
"recommendation": message
|
|
|
|
| 413 |
}
|
| 414 |
|
| 415 |
return OptimizeResponse(
|
|
|
|
| 1 |
# ============================================
|
| 2 |
# YORK CHILLER OPTIMIZER API
|
| 3 |
+
# Using existing model files with NumPy 1.23.5
|
| 4 |
# ============================================
|
| 5 |
|
| 6 |
import os
|
|
|
|
| 8 |
import warnings
|
| 9 |
warnings.filterwarnings('ignore')
|
| 10 |
|
| 11 |
+
# Import normally with NumPy 1.23.5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
import numpy as np
|
| 13 |
print(f"NumPy version: {np.__version__}")
|
| 14 |
|
| 15 |
+
# Import rest of the libraries
|
| 16 |
import joblib
|
| 17 |
from datetime import datetime
|
| 18 |
from typing import List, Optional, Dict, Any
|
| 19 |
from fastapi import FastAPI, HTTPException
|
| 20 |
from pydantic import BaseModel, Field
|
| 21 |
+
from sklearn.ensemble import RandomForestRegressor
|
| 22 |
+
from sklearn.preprocessing import StandardScaler
|
| 23 |
|
| 24 |
app = FastAPI(
|
| 25 |
title="York Chiller Energy Optimizer",
|
| 26 |
+
description="Random Forest Model for Chiller Energy Efficiency - Based on 12 Operational Features",
|
| 27 |
version="2.0.0"
|
| 28 |
)
|
| 29 |
|
|
|
|
| 43 |
print("\n📂 Checking for model files...")
|
| 44 |
|
| 45 |
# Check what files exist
|
| 46 |
+
files_found = []
|
| 47 |
+
for file in ['production_model.pkl', 'scaler.pkl', 'features.pkl']:
|
| 48 |
+
if os.path.exists(file):
|
| 49 |
+
size = os.path.getsize(file) / 1024
|
| 50 |
+
files_found.append(f"{file} ({size:.1f} KB)")
|
| 51 |
+
print(f" ✅ Found: {file}")
|
| 52 |
+
else:
|
| 53 |
+
print(f" ❌ Missing: {file}")
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
if not os.path.exists("production_model.pkl"):
|
| 56 |
+
print("❌ production_model.pkl not found! Cannot continue.")
|
| 57 |
+
return False
|
|
|
|
| 58 |
|
| 59 |
+
# Load the model
|
| 60 |
try:
|
| 61 |
+
# Try loading with joblib
|
|
|
|
| 62 |
model = joblib.load("production_model.pkl")
|
| 63 |
print(f"\n✅ Model loaded successfully")
|
| 64 |
print(f" Type: {type(model).__name__}")
|
|
|
|
| 79 |
|
| 80 |
except Exception as e:
|
| 81 |
print(f"❌ Error loading model: {e}")
|
| 82 |
+
print(f" This might be a NumPy version mismatch.")
|
| 83 |
+
print(f" Current NumPy: {np.__version__}")
|
| 84 |
+
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
# Load scaler
|
| 87 |
try:
|
|
|
|
| 97 |
if os.path.exists("features.pkl"):
|
| 98 |
feature_names = joblib.load("features.pkl")
|
| 99 |
print(f"✅ Features loaded from features.pkl")
|
|
|
|
| 100 |
if isinstance(feature_names, list):
|
| 101 |
+
print(f" Feature count: {len(feature_names)}")
|
| 102 |
+
print(f" First 5 features: {feature_names[:5]}")
|
| 103 |
+
# Update feature count if needed
|
| 104 |
+
if model_feature_count == 0:
|
| 105 |
+
model_feature_count = len(feature_names)
|
| 106 |
except Exception as e:
|
| 107 |
print(f"⚠️ Could not load feature names: {e}")
|
| 108 |
# Create default 12 feature names
|
|
|
|
| 129 |
|
| 130 |
if model:
|
| 131 |
print(f"\n📊 Model Configuration:")
|
| 132 |
+
print(f" Status: ✅ ONLINE")
|
| 133 |
print(f" Features expected: {model_feature_count}")
|
| 134 |
+
print(f" Scaler: {'✅ Loaded' if scaler else '❌ Not loaded'}")
|
| 135 |
+
print(f" NumPy version: {np.__version__}")
|
| 136 |
else:
|
| 137 |
print(f"\n📊 Model Configuration:")
|
| 138 |
+
print(f" Status: ❌ OFFLINE")
|
| 139 |
+
print(f" NumPy version: {np.__version__}")
|
| 140 |
|
| 141 |
# ============================================
|
| 142 |
# REQUEST MODELS
|
|
|
|
| 158 |
day_of_year: int = Field(..., ge=1, le=366, description="Day of the year (1-365)")
|
| 159 |
|
| 160 |
# Optional optimization parameters
|
| 161 |
+
current_chw_setpoint_c: Optional[float] = Field(8.0, ge=5, le=10, description="Current CHW setpoint (°C)")
|
| 162 |
+
current_limit_pct: Optional[float] = Field(100, ge=50, le=100, description="Current limit percentage")
|
| 163 |
|
| 164 |
class PredictionResponse(BaseModel):
|
| 165 |
status: str
|
|
|
|
| 185 |
summary: Dict[str, str]
|
| 186 |
|
| 187 |
# ============================================
|
| 188 |
+
# FEATURE PREPARATION FUNCTION
|
| 189 |
# ============================================
|
| 190 |
|
| 191 |
+
def prepare_features_for_model(input_data: ChillerInput) -> np.ndarray:
|
| 192 |
+
"""
|
| 193 |
+
Prepare features in the order expected by the model
|
| 194 |
+
Uses feature_names from features.pkl if available
|
| 195 |
+
"""
|
| 196 |
+
|
| 197 |
+
# Create dictionary of all possible features
|
| 198 |
+
feature_dict = {
|
| 199 |
+
'total_building_load_rt': input_data.total_building_load_rt,
|
| 200 |
+
'avg_chilled_water_rate_lps': input_data.avg_chilled_water_rate_lps,
|
| 201 |
+
'avg_cooling_water_temp_c': input_data.avg_cooling_water_temp_c,
|
| 202 |
+
'avg_outside_temp_f': input_data.avg_outside_temp_f,
|
| 203 |
+
'avg_dew_point_f': input_data.avg_dew_point_f,
|
| 204 |
+
'avg_humidity_pct': input_data.avg_humidity_pct,
|
| 205 |
+
'avg_wind_speed_mph': input_data.avg_wind_speed_mph,
|
| 206 |
+
'avg_pressure_in': input_data.avg_pressure_in,
|
| 207 |
+
'hour': input_data.hour,
|
| 208 |
+
'day_of_week': input_data.day_of_week,
|
| 209 |
+
'month': input_data.month,
|
| 210 |
+
'day_of_year': input_data.day_of_year,
|
| 211 |
+
# Alternative names that might be in the model
|
| 212 |
+
'load': input_data.total_building_load_rt,
|
| 213 |
+
'chilled_water_flow': input_data.avg_chilled_water_rate_lps,
|
| 214 |
+
'outside_temp': input_data.avg_outside_temp_f,
|
| 215 |
+
'humidity': input_data.avg_humidity_pct,
|
| 216 |
+
}
|
| 217 |
|
| 218 |
+
# If we have feature names from features.pkl, use them
|
| 219 |
if feature_names and isinstance(feature_names, list):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
features_list = []
|
| 221 |
for f_name in feature_names:
|
| 222 |
if f_name in feature_dict:
|
| 223 |
features_list.append(feature_dict[f_name])
|
| 224 |
else:
|
| 225 |
+
# Try to find alternative
|
| 226 |
+
found = False
|
| 227 |
+
for key in feature_dict:
|
| 228 |
+
if f_name.lower() in key.lower() or key.lower() in f_name.lower():
|
| 229 |
+
features_list.append(feature_dict[key])
|
| 230 |
+
print(f" Mapped '{f_name}' → '{key}'")
|
| 231 |
+
found = True
|
| 232 |
+
break
|
| 233 |
+
if not found:
|
| 234 |
+
# Default value if feature not found
|
| 235 |
+
print(f" ⚠️ Feature '{f_name}' not found, using 0")
|
| 236 |
+
features_list.append(0.0)
|
| 237 |
|
| 238 |
features = np.array([features_list])
|
| 239 |
+
print(f" Prepared {len(features_list)} features from features.pkl")
|
| 240 |
|
| 241 |
else:
|
| 242 |
# Default: use all 12 features in standard order
|
|
|
|
| 254 |
input_data.month,
|
| 255 |
input_data.day_of_year
|
| 256 |
]])
|
| 257 |
+
print(f" Prepared default 12 features")
|
| 258 |
|
| 259 |
# Apply scaler if available
|
| 260 |
if scaler is not None:
|
|
|
|
| 267 |
return features
|
| 268 |
|
| 269 |
def predict_kw_per_tr(input_data: ChillerInput) -> float:
|
| 270 |
+
"""Predict Combined_Kw_per_TR using the loaded model"""
|
| 271 |
if model is None:
|
| 272 |
raise ValueError("Model not loaded")
|
| 273 |
|
| 274 |
+
features = prepare_features_for_model(input_data)
|
| 275 |
prediction = model.predict(features)[0]
|
| 276 |
+
|
| 277 |
+
# Clip to realistic range
|
| 278 |
prediction = np.clip(prediction, 0.4, 1.2)
|
| 279 |
|
| 280 |
return float(prediction)
|
|
|
|
| 295 |
"loaded": model is not None,
|
| 296 |
"features_expected": model_feature_count,
|
| 297 |
"features_from_pkl": len(feature_names) if feature_names else 0,
|
| 298 |
+
"scaler_loaded": scaler is not None,
|
| 299 |
+
"numpy_version": np.__version__
|
| 300 |
},
|
| 301 |
"endpoints": {
|
| 302 |
"/": "This information",
|
|
|
|
| 305 |
"/optimize": "POST - Get optimization recommendations"
|
| 306 |
},
|
| 307 |
"interpretation": {
|
| 308 |
+
"kw_per_tr": "Combined energy efficiency indicator - LOWER is better",
|
| 309 |
+
"typical_range": "0.45 - 1.0 kW/TR",
|
| 310 |
"excellent": "< 0.55 kW/TR",
|
| 311 |
"good": "0.55-0.65 kW/TR",
|
| 312 |
"fair": "0.65-0.75 kW/TR",
|
|
|
|
| 328 |
|
| 329 |
@app.post("/predict", response_model=PredictionResponse)
|
| 330 |
async def predict_endpoint(input_data: ChillerInput):
|
| 331 |
+
"""Predict Combined_Kw_per_TR for given conditions"""
|
| 332 |
try:
|
| 333 |
if model is None:
|
| 334 |
+
raise HTTPException(status_code=503, detail="Model not loaded - check logs")
|
| 335 |
|
| 336 |
kw_per_tr = predict_kw_per_tr(input_data)
|
| 337 |
|
|
|
|
| 351 |
"""Get optimization recommendations"""
|
| 352 |
try:
|
| 353 |
if model is None:
|
| 354 |
+
raise HTTPException(status_code=503, detail="Model not loaded - check logs")
|
| 355 |
|
| 356 |
# Current efficiency
|
| 357 |
current_kw = predict_kw_per_tr(input_data)
|
| 358 |
|
| 359 |
+
# Test different CHW setpoints to find optimum
|
| 360 |
current_sp = input_data.current_chw_setpoint_c or 8.0
|
| 361 |
test_setpoints = [6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0]
|
| 362 |
|
|
|
|
| 388 |
recommended_value=f"{best_sp:.1f}°C",
|
| 389 |
expected_savings=f"{savings_pct:.1f}%",
|
| 390 |
priority="HIGH" if savings_pct > 5 else "MEDIUM",
|
| 391 |
+
operator_action=f"Adjust CHW setpoint from {current_sp:.1f}°C to {best_sp:.1f}°C on chiller controller"
|
| 392 |
+
))
|
| 393 |
+
|
| 394 |
+
# Load-based recommendations
|
| 395 |
+
if input_data.total_building_load_rt < 600:
|
| 396 |
+
recommendations.append(OptimizationRecommendation(
|
| 397 |
+
action="Chiller Sequencing",
|
| 398 |
+
current_value=f"{input_data.total_building_load_rt:.0f} RT",
|
| 399 |
+
recommended_value="Single chiller operation",
|
| 400 |
+
expected_savings="15-25% reduction in parasitic losses",
|
| 401 |
+
priority="HIGH",
|
| 402 |
+
operator_action="Consider operating only one chiller"
|
| 403 |
+
))
|
| 404 |
+
elif input_data.total_building_load_rt > 1800:
|
| 405 |
+
recommendations.append(OptimizationRecommendation(
|
| 406 |
+
action="Chiller Staging",
|
| 407 |
+
current_value=f"{input_data.total_building_load_rt:.0f} RT",
|
| 408 |
+
recommended_value="Verify all chillers online",
|
| 409 |
+
expected_savings="Prevents overload conditions",
|
| 410 |
+
priority="HIGH",
|
| 411 |
+
operator_action="Check if all chillers are operating properly"
|
| 412 |
))
|
| 413 |
|
| 414 |
# Efficiency rating
|
|
|
|
| 430 |
"optimal_efficiency": f"{best_kw:.3f} kW/TR",
|
| 431 |
"potential_savings": f"{savings_pct:.1f}%",
|
| 432 |
"efficiency_rating": rating,
|
| 433 |
+
"recommendation": message,
|
| 434 |
+
"load": f"{input_data.total_building_load_rt:.0f} RT"
|
| 435 |
}
|
| 436 |
|
| 437 |
return OptimizeResponse(
|