Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
# ============================================
|
| 2 |
# YORK CHILLER OPTIMIZER API
|
| 3 |
# Compatible with NumPy 2.0.2 and pandas 2.2.3
|
|
|
|
| 4 |
# ============================================
|
| 5 |
|
| 6 |
import os
|
|
@@ -23,8 +24,8 @@ import joblib
|
|
| 23 |
import pickle
|
| 24 |
|
| 25 |
app = FastAPI(
|
| 26 |
-
title="York Chiller Energy Optimizer",
|
| 27 |
-
description="Random Forest Model for Chiller Energy Efficiency - 12 Features",
|
| 28 |
version="2.0.0"
|
| 29 |
)
|
| 30 |
|
|
@@ -142,9 +143,9 @@ else:
|
|
| 142 |
# ============================================
|
| 143 |
|
| 144 |
class ChillerInput(BaseModel):
|
| 145 |
-
"""12 input features
|
| 146 |
-
total_building_load: float = Field(..., ge=
|
| 147 |
-
avg_chilled_water_rate: float = Field(..., ge=
|
| 148 |
avg_cooling_water_temp: float = Field(..., ge=15, le=35, description="Average cooling water temperature (°C)")
|
| 149 |
avg_outside_temp: float = Field(..., ge=32, le=120, description="Average outside air temperature (°F)")
|
| 150 |
avg_dew_point: float = Field(..., ge=20, le=80, description="Average dew point temperature (°F)")
|
|
@@ -156,13 +157,22 @@ class ChillerInput(BaseModel):
|
|
| 156 |
month: int = Field(..., ge=1, le=12, description="Month of the year (1-12)")
|
| 157 |
day_of_year: int = Field(..., ge=1, le=366, description="Day of the year (1-365)")
|
| 158 |
|
| 159 |
-
# Optional
|
| 160 |
current_chw_setpoint_c: Optional[float] = Field(8.0, ge=5, le=10, description="Current CHW setpoint (°C)")
|
|
|
|
|
|
|
| 161 |
|
| 162 |
class PredictionResponse(BaseModel):
|
| 163 |
status: str
|
| 164 |
kw_per_tr: float
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
efficiency_rating: str
|
|
|
|
|
|
|
| 166 |
features_used: int
|
| 167 |
model_type: str
|
| 168 |
timestamp: str
|
|
@@ -178,8 +188,14 @@ class OptimizationRecommendation(BaseModel):
|
|
| 178 |
class OptimizeResponse(BaseModel):
|
| 179 |
timestamp: str
|
| 180 |
current_kw_per_tr: float
|
|
|
|
| 181 |
optimal_kw_per_tr: float
|
|
|
|
| 182 |
efficiency_improvement_pct: float
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
recommendations: List[OptimizationRecommendation]
|
| 184 |
summary: Dict[str, str]
|
| 185 |
|
|
@@ -218,11 +234,49 @@ def predict_kw_per_tr(input_data: ChillerInput) -> float:
|
|
| 218 |
# Predict
|
| 219 |
prediction = model.predict(features)[0]
|
| 220 |
|
| 221 |
-
#
|
| 222 |
-
|
|
|
|
|
|
|
| 223 |
|
| 224 |
return float(prediction)
|
| 225 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
def get_efficiency_rating(kw_per_tr: float) -> str:
|
| 227 |
"""Get efficiency rating based on kW/TR value"""
|
| 228 |
if kw_per_tr < 0.55:
|
|
@@ -266,13 +320,14 @@ def optimize_chw_setpoint(input_data: ChillerInput) -> tuple:
|
|
| 266 |
|
| 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",
|
| 275 |
"version": "2.0.0",
|
|
|
|
| 276 |
"status": "online" if model is not None else "model_not_loaded",
|
| 277 |
"model_info": {
|
| 278 |
"loaded": model is not None,
|
|
@@ -284,15 +339,36 @@ async def root():
|
|
| 284 |
"endpoints": {
|
| 285 |
"/": "This information",
|
| 286 |
"/health": "Health check",
|
| 287 |
-
"/predict": "POST - Predict efficiency
|
| 288 |
-
"/optimize": "POST - Get optimization
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
},
|
| 290 |
"interpretation": {
|
| 291 |
-
"kw_per_tr": "
|
| 292 |
-
"
|
| 293 |
-
"
|
| 294 |
-
"
|
| 295 |
-
"poor": "> 0.75 kW/TR"
|
| 296 |
}
|
| 297 |
}
|
| 298 |
|
|
@@ -313,20 +389,61 @@ async def health():
|
|
| 313 |
|
| 314 |
@app.post("/predict", response_model=PredictionResponse)
|
| 315 |
async def predict_endpoint(input_data: ChillerInput):
|
| 316 |
-
"""Predict
|
| 317 |
try:
|
| 318 |
if model is None:
|
| 319 |
raise HTTPException(status_code=503, detail="Model not loaded - check logs")
|
| 320 |
|
|
|
|
| 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()
|
|
@@ -337,83 +454,130 @@ async def predict_endpoint(input_data: ChillerInput):
|
|
| 337 |
|
| 338 |
@app.post("/optimize", response_model=OptimizeResponse)
|
| 339 |
async def optimize_endpoint(input_data: ChillerInput):
|
| 340 |
-
"""Get optimization recommendations"""
|
| 341 |
try:
|
| 342 |
if model is None:
|
| 343 |
raise HTTPException(status_code=503, detail="Model not loaded - check logs")
|
| 344 |
|
| 345 |
-
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
|
| 348 |
# Find optimal setpoint
|
| 349 |
-
optimal_sp,
|
|
|
|
| 350 |
|
| 351 |
# Calculate savings
|
| 352 |
-
savings_pct = ((
|
| 353 |
savings_pct = max(0, savings_pct)
|
| 354 |
|
| 355 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
recommendations = []
|
| 357 |
|
|
|
|
| 358 |
current_sp = input_data.current_chw_setpoint_c or 8.0
|
| 359 |
if optimal_sp != current_sp and savings_pct > 1:
|
| 360 |
recommendations.append(OptimizationRecommendation(
|
| 361 |
action="CHW Setpoint Optimization",
|
| 362 |
current_value=f"{current_sp:.1f}°C",
|
| 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"
|
| 367 |
))
|
| 368 |
|
| 369 |
-
#
|
| 370 |
-
|
|
|
|
|
|
|
| 371 |
recommendations.append(OptimizationRecommendation(
|
| 372 |
action="Chiller Sequencing",
|
| 373 |
-
current_value=f"{
|
| 374 |
-
recommended_value="
|
| 375 |
-
expected_savings="
|
| 376 |
priority="HIGH",
|
| 377 |
-
operator_action="
|
| 378 |
))
|
| 379 |
elif input_data.total_building_load > 1800:
|
| 380 |
recommendations.append(OptimizationRecommendation(
|
| 381 |
-
action="
|
| 382 |
-
current_value=f"{
|
| 383 |
-
recommended_value="Verify all chillers",
|
| 384 |
-
expected_savings="
|
| 385 |
-
priority="
|
| 386 |
-
operator_action="Check
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
))
|
| 388 |
|
| 389 |
# Free cooling recommendation
|
| 390 |
if input_data.avg_outside_temp < 50 and input_data.avg_humidity < 60:
|
|
|
|
|
|
|
| 391 |
recommendations.append(OptimizationRecommendation(
|
| 392 |
-
action="Free Cooling",
|
| 393 |
-
current_value="
|
| 394 |
-
recommended_value="
|
| 395 |
-
expected_savings="
|
| 396 |
priority="HIGH",
|
| 397 |
-
operator_action="
|
| 398 |
))
|
| 399 |
|
| 400 |
# Efficiency rating
|
| 401 |
-
rating = get_efficiency_rating(
|
|
|
|
|
|
|
| 402 |
|
| 403 |
summary = {
|
| 404 |
-
"
|
| 405 |
-
"
|
| 406 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
"efficiency_rating": rating,
|
| 408 |
-
"
|
| 409 |
-
"recommended_setpoint": f"{optimal_sp:.1f}°C"
|
|
|
|
| 410 |
}
|
| 411 |
|
| 412 |
return OptimizeResponse(
|
| 413 |
timestamp=datetime.now().isoformat(),
|
| 414 |
-
current_kw_per_tr=round(
|
| 415 |
-
|
|
|
|
|
|
|
| 416 |
efficiency_improvement_pct=round(savings_pct, 2),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
recommendations=recommendations,
|
| 418 |
summary=summary
|
| 419 |
)
|
|
@@ -421,6 +585,67 @@ async def optimize_endpoint(input_data: ChillerInput):
|
|
| 421 |
except Exception as e:
|
| 422 |
raise HTTPException(status_code=500, detail=str(e))
|
| 423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
if __name__ == "__main__":
|
| 425 |
import uvicorn
|
| 426 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 1 |
# ============================================
|
| 2 |
# YORK CHILLER OPTIMIZER API
|
| 3 |
# Compatible with NumPy 2.0.2 and pandas 2.2.3
|
| 4 |
+
# For 4-Chiller Plants (1000+ TR)
|
| 5 |
# ============================================
|
| 6 |
|
| 7 |
import os
|
|
|
|
| 24 |
import pickle
|
| 25 |
|
| 26 |
app = FastAPI(
|
| 27 |
+
title="York Chiller Energy Optimizer - 4 Chiller Plant Edition",
|
| 28 |
+
description="Random Forest Model for Chiller Energy Efficiency - 12 Features (1000+ TR Plants)",
|
| 29 |
version="2.0.0"
|
| 30 |
)
|
| 31 |
|
|
|
|
| 143 |
# ============================================
|
| 144 |
|
| 145 |
class ChillerInput(BaseModel):
|
| 146 |
+
"""12 input features - optimized for 1000+ TR plants"""
|
| 147 |
+
total_building_load: float = Field(..., ge=400, le=2500, description="Total building cooling load (RT) - For 4 chiller plant: 400-2500 TR")
|
| 148 |
+
avg_chilled_water_rate: float = Field(..., ge=200, le=2000, description="Average chilled water flow rate (L/sec)")
|
| 149 |
avg_cooling_water_temp: float = Field(..., ge=15, le=35, description="Average cooling water temperature (°C)")
|
| 150 |
avg_outside_temp: float = Field(..., ge=32, le=120, description="Average outside air temperature (°F)")
|
| 151 |
avg_dew_point: float = Field(..., ge=20, le=80, description="Average dew point temperature (°F)")
|
|
|
|
| 157 |
month: int = Field(..., ge=1, le=12, description="Month of the year (1-12)")
|
| 158 |
day_of_year: int = Field(..., ge=1, le=366, description="Day of the year (1-365)")
|
| 159 |
|
| 160 |
+
# Optional parameters for 4-chiller plant
|
| 161 |
current_chw_setpoint_c: Optional[float] = Field(8.0, ge=5, le=10, description="Current CHW setpoint (°C)")
|
| 162 |
+
num_chillers_operating: Optional[int] = Field(4, ge=1, le=4, description="Number of chillers currently operating")
|
| 163 |
+
electricity_rate_usd_per_kwh: Optional[float] = Field(0.12, ge=0.05, le=0.50, description="Electricity rate ($/kWh)")
|
| 164 |
|
| 165 |
class PredictionResponse(BaseModel):
|
| 166 |
status: str
|
| 167 |
kw_per_tr: float
|
| 168 |
+
total_power_kw: float
|
| 169 |
+
power_per_chiller_kw: float
|
| 170 |
+
cooling_load_tr: float
|
| 171 |
+
load_per_chiller_tr: float
|
| 172 |
+
cop: float
|
| 173 |
efficiency_rating: str
|
| 174 |
+
plant_summary: Dict[str, Any]
|
| 175 |
+
annual_cost_estimate: Dict[str, Any]
|
| 176 |
features_used: int
|
| 177 |
model_type: str
|
| 178 |
timestamp: str
|
|
|
|
| 188 |
class OptimizeResponse(BaseModel):
|
| 189 |
timestamp: str
|
| 190 |
current_kw_per_tr: float
|
| 191 |
+
current_total_power_kw: float
|
| 192 |
optimal_kw_per_tr: float
|
| 193 |
+
optimal_total_power_kw: float
|
| 194 |
efficiency_improvement_pct: float
|
| 195 |
+
power_savings_kw: float
|
| 196 |
+
power_savings_pct: float
|
| 197 |
+
annual_savings_usd: float
|
| 198 |
+
co2_reduction_kg_per_year: float
|
| 199 |
recommendations: List[OptimizationRecommendation]
|
| 200 |
summary: Dict[str, str]
|
| 201 |
|
|
|
|
| 234 |
# Predict
|
| 235 |
prediction = model.predict(features)[0]
|
| 236 |
|
| 237 |
+
# Typical range for large centrifugal chillers (1000+ TR)
|
| 238 |
+
# Modern efficient: 0.45-0.55 kW/TR
|
| 239 |
+
# Older units: 0.60-0.80 kW/TR
|
| 240 |
+
prediction = np.clip(prediction, 0.40, 0.90)
|
| 241 |
|
| 242 |
return float(prediction)
|
| 243 |
|
| 244 |
+
def calculate_total_power(kw_per_tr: float, cooling_load_tr: float) -> float:
|
| 245 |
+
"""
|
| 246 |
+
Calculate total chiller plant power consumption for all chillers
|
| 247 |
+
|
| 248 |
+
Formula: Total Power (kW) = kW/TR × Cooling Load (TR)
|
| 249 |
+
"""
|
| 250 |
+
return kw_per_tr * cooling_load_tr
|
| 251 |
+
|
| 252 |
+
def calculate_annual_energy_cost(kw_per_tr: float, avg_load_tr: float,
|
| 253 |
+
operating_hours: int = 3000,
|
| 254 |
+
electricity_rate: float = 0.12) -> dict:
|
| 255 |
+
"""
|
| 256 |
+
Calculate annual energy cost and consumption
|
| 257 |
+
"""
|
| 258 |
+
avg_power_kw = kw_per_tr * avg_load_tr
|
| 259 |
+
annual_kwh = avg_power_kw * operating_hours
|
| 260 |
+
annual_cost_usd = annual_kwh * electricity_rate
|
| 261 |
+
|
| 262 |
+
return {
|
| 263 |
+
"avg_power_kw": round(avg_power_kw, 1),
|
| 264 |
+
"annual_kwh": round(annual_kwh / 1000, 1),
|
| 265 |
+
"annual_cost_usd": round(annual_cost_usd, 0),
|
| 266 |
+
"operating_hours": operating_hours,
|
| 267 |
+
"electricity_rate": electricity_rate
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
def calculate_cop_from_kw_per_tr(kw_per_tr: float) -> float:
|
| 271 |
+
"""
|
| 272 |
+
Convert kW/TR to COP (Coefficient of Performance)
|
| 273 |
+
|
| 274 |
+
Formula: COP = 3.516 / kW/TR
|
| 275 |
+
"""
|
| 276 |
+
if kw_per_tr <= 0:
|
| 277 |
+
return 0
|
| 278 |
+
return 3.516 / kw_per_tr
|
| 279 |
+
|
| 280 |
def get_efficiency_rating(kw_per_tr: float) -> str:
|
| 281 |
"""Get efficiency rating based on kW/TR value"""
|
| 282 |
if kw_per_tr < 0.55:
|
|
|
|
| 320 |
|
| 321 |
@app.get("/")
|
| 322 |
async def root():
|
| 323 |
+
"""Root endpoint with API information for large chiller plants"""
|
| 324 |
feature_count = len(feature_names) if feature_names and isinstance(feature_names, list) else len(EXPECTED_FEATURES)
|
| 325 |
|
| 326 |
return {
|
| 327 |
+
"service": "York Chiller Energy Optimizer - 4 Chiller Plant Edition",
|
| 328 |
"model_type": "Random Forest Regressor",
|
| 329 |
"version": "2.0.0",
|
| 330 |
+
"plant_size": "1000+ TR (4 centrifugal chillers)",
|
| 331 |
"status": "online" if model is not None else "model_not_loaded",
|
| 332 |
"model_info": {
|
| 333 |
"loaded": model is not None,
|
|
|
|
| 339 |
"endpoints": {
|
| 340 |
"/": "This information",
|
| 341 |
"/health": "Health check",
|
| 342 |
+
"/predict": "POST - Predict efficiency and power for 4-chiller plant",
|
| 343 |
+
"/optimize": "POST - Get optimization with annual savings ($ and CO2)",
|
| 344 |
+
"/plant-analysis": "POST - Detailed 4-chiller plant analysis"
|
| 345 |
+
},
|
| 346 |
+
"sample_4_chiller_input": {
|
| 347 |
+
"total_building_load": 1200,
|
| 348 |
+
"avg_chilled_water_rate": 800,
|
| 349 |
+
"avg_cooling_water_temp": 28,
|
| 350 |
+
"avg_outside_temp": 95,
|
| 351 |
+
"avg_dew_point": 65,
|
| 352 |
+
"avg_humidity": 60,
|
| 353 |
+
"avg_wind_speed": 8,
|
| 354 |
+
"avg_pressure": 29.9,
|
| 355 |
+
"hour": 14,
|
| 356 |
+
"day_of_week": 2,
|
| 357 |
+
"month": 7,
|
| 358 |
+
"day_of_year": 200,
|
| 359 |
+
"num_chillers_operating": 4,
|
| 360 |
+
"electricity_rate_usd_per_kwh": 0.12
|
| 361 |
+
},
|
| 362 |
+
"expected_output_sample": {
|
| 363 |
+
"total_power_kw": "~700-800 kW total",
|
| 364 |
+
"power_per_chiller_kw": "~175-200 kW each",
|
| 365 |
+
"annual_energy_cost": "$250,000-$350,000 at $0.12/kWh"
|
| 366 |
},
|
| 367 |
"interpretation": {
|
| 368 |
+
"kw_per_tr": "Lower is better for 1000+ TR plants: <0.55 = excellent",
|
| 369 |
+
"total_power_kw": "Actual plant power for all chillers combined",
|
| 370 |
+
"typical_power_for_1000tr": "500-700 kW at full load",
|
| 371 |
+
"potential_savings": "50-150 kW possible through optimization = $15k-45k/year"
|
|
|
|
| 372 |
}
|
| 373 |
}
|
| 374 |
|
|
|
|
| 389 |
|
| 390 |
@app.post("/predict", response_model=PredictionResponse)
|
| 391 |
async def predict_endpoint(input_data: ChillerInput):
|
| 392 |
+
"""Predict efficiency and power for 4-chiller plant (1000+ TR)"""
|
| 393 |
try:
|
| 394 |
if model is None:
|
| 395 |
raise HTTPException(status_code=503, detail="Model not loaded - check logs")
|
| 396 |
|
| 397 |
+
# Get predicted kW/TR
|
| 398 |
kw_per_tr = predict_kw_per_tr(input_data)
|
| 399 |
+
|
| 400 |
+
# Calculate total plant power
|
| 401 |
+
total_power_kw = calculate_total_power(kw_per_tr, input_data.total_building_load)
|
| 402 |
+
|
| 403 |
+
# Get number of chillers (default 4)
|
| 404 |
+
num_chillers = input_data.num_chillers_operating or 4
|
| 405 |
+
|
| 406 |
+
# Per-chiller metrics
|
| 407 |
+
power_per_chiller = total_power_kw / num_chillers
|
| 408 |
+
load_per_chiller = input_data.total_building_load / num_chillers
|
| 409 |
+
|
| 410 |
+
# Calculate COP
|
| 411 |
+
cop = calculate_cop_from_kw_per_tr(kw_per_tr)
|
| 412 |
+
|
| 413 |
+
# Get efficiency rating
|
| 414 |
rating = get_efficiency_rating(kw_per_tr)
|
| 415 |
|
| 416 |
+
# Calculate annual cost estimate
|
| 417 |
+
electricity_rate = input_data.electricity_rate_usd_per_kwh or 0.12
|
| 418 |
+
annual_cost = calculate_annual_energy_cost(
|
| 419 |
+
kw_per_tr,
|
| 420 |
+
input_data.total_building_load,
|
| 421 |
+
operating_hours=3000,
|
| 422 |
+
electricity_rate=electricity_rate
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
# Plant summary
|
| 426 |
+
plant_summary = {
|
| 427 |
+
"num_chillers": num_chillers,
|
| 428 |
+
"total_capacity_tr": round(input_data.total_building_load, 0),
|
| 429 |
+
"load_per_chiller_tr": round(load_per_chiller, 1),
|
| 430 |
+
"power_density_kw_per_100tr": round(kw_per_tr * 100, 2),
|
| 431 |
+
"chiller_type": "Centrifugal (1000+ TR scale)"
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
feature_count = len(feature_names) if feature_names and isinstance(feature_names, list) else len(EXPECTED_FEATURES)
|
| 435 |
|
| 436 |
return PredictionResponse(
|
| 437 |
status="success",
|
| 438 |
kw_per_tr=round(kw_per_tr, 4),
|
| 439 |
+
total_power_kw=round(total_power_kw, 1),
|
| 440 |
+
power_per_chiller_kw=round(power_per_chiller, 1),
|
| 441 |
+
cooling_load_tr=round(input_data.total_building_load, 1),
|
| 442 |
+
load_per_chiller_tr=round(load_per_chiller, 1),
|
| 443 |
+
cop=round(cop, 2),
|
| 444 |
efficiency_rating=rating,
|
| 445 |
+
plant_summary=plant_summary,
|
| 446 |
+
annual_cost_estimate=annual_cost,
|
| 447 |
features_used=feature_count,
|
| 448 |
model_type="RandomForestRegressor",
|
| 449 |
timestamp=datetime.now().isoformat()
|
|
|
|
| 454 |
|
| 455 |
@app.post("/optimize", response_model=OptimizeResponse)
|
| 456 |
async def optimize_endpoint(input_data: ChillerInput):
|
| 457 |
+
"""Get optimization recommendations for large chiller plant"""
|
| 458 |
try:
|
| 459 |
if model is None:
|
| 460 |
raise HTTPException(status_code=503, detail="Model not loaded - check logs")
|
| 461 |
|
| 462 |
+
num_chillers = input_data.num_chillers_operating or 4
|
| 463 |
+
electricity_rate = input_data.electricity_rate_usd_per_kwh or 0.12
|
| 464 |
+
|
| 465 |
+
# Current efficiency and power
|
| 466 |
+
current_kw_per_tr = predict_kw_per_tr(input_data)
|
| 467 |
+
current_power_kw = calculate_total_power(current_kw_per_tr, input_data.total_building_load)
|
| 468 |
|
| 469 |
# Find optimal setpoint
|
| 470 |
+
optimal_sp, optimal_kw_per_tr = optimize_chw_setpoint(input_data)
|
| 471 |
+
optimal_power_kw = calculate_total_power(optimal_kw_per_tr, input_data.total_building_load)
|
| 472 |
|
| 473 |
# Calculate savings
|
| 474 |
+
savings_pct = ((current_kw_per_tr - optimal_kw_per_tr) / current_kw_per_tr) * 100 if current_kw_per_tr > 0 else 0
|
| 475 |
savings_pct = max(0, savings_pct)
|
| 476 |
|
| 477 |
+
# Power savings
|
| 478 |
+
power_savings_kw = current_power_kw - optimal_power_kw
|
| 479 |
+
power_savings_pct = (power_savings_kw / current_power_kw) * 100 if current_power_kw > 0 else 0
|
| 480 |
+
|
| 481 |
+
# Annual savings
|
| 482 |
+
annual_operating_hours = 3000
|
| 483 |
+
annual_savings_kwh = power_savings_kw * annual_operating_hours
|
| 484 |
+
annual_savings_usd = annual_savings_kwh * electricity_rate
|
| 485 |
+
|
| 486 |
+
# CO2 reduction (assuming 0.4 kg CO2 per kWh - US average)
|
| 487 |
+
co2_reduction_kg = annual_savings_kwh * 0.4
|
| 488 |
+
|
| 489 |
+
# Build recommendations for 4-chiller plant
|
| 490 |
recommendations = []
|
| 491 |
|
| 492 |
+
# Setpoint optimization
|
| 493 |
current_sp = input_data.current_chw_setpoint_c or 8.0
|
| 494 |
if optimal_sp != current_sp and savings_pct > 1:
|
| 495 |
recommendations.append(OptimizationRecommendation(
|
| 496 |
action="CHW Setpoint Optimization",
|
| 497 |
current_value=f"{current_sp:.1f}°C",
|
| 498 |
recommended_value=f"{optimal_sp:.1f}°C",
|
| 499 |
+
expected_savings=f"{savings_pct:.1f}% ({power_savings_kw:.0f} kW, ${annual_savings_usd:,.0f}/year)",
|
| 500 |
priority="HIGH" if savings_pct > 5 else "MEDIUM",
|
| 501 |
+
operator_action=f"Raise CHW setpoint to {optimal_sp:.1f}°C across all {num_chillers} chillers"
|
| 502 |
))
|
| 503 |
|
| 504 |
+
# Chiller sequencing for large plants
|
| 505 |
+
load_per_chiller = input_data.total_building_load / num_chillers
|
| 506 |
+
|
| 507 |
+
if input_data.total_building_load < 800:
|
| 508 |
recommendations.append(OptimizationRecommendation(
|
| 509 |
action="Chiller Sequencing",
|
| 510 |
+
current_value=f"{num_chillers} chillers operating",
|
| 511 |
+
recommended_value="3 chillers (or less)",
|
| 512 |
+
expected_savings="20-30% power reduction at low load",
|
| 513 |
priority="HIGH",
|
| 514 |
+
operator_action=f"Sequentially shut down one chiller, adjust load on remaining {num_chillers-1} chillers"
|
| 515 |
))
|
| 516 |
elif input_data.total_building_load > 1800:
|
| 517 |
recommendations.append(OptimizationRecommendation(
|
| 518 |
+
action="Load Balancing",
|
| 519 |
+
current_value=f"{load_per_chiller:.0f} TR per chiller",
|
| 520 |
+
recommended_value="Verify all chillers are contributing equally",
|
| 521 |
+
expected_savings="5-10% efficiency improvement",
|
| 522 |
+
priority="MEDIUM",
|
| 523 |
+
operator_action="Check operating logs for load sharing between chillers"
|
| 524 |
+
))
|
| 525 |
+
|
| 526 |
+
# Condenser water temperature optimization
|
| 527 |
+
if input_data.avg_cooling_water_temp < 24:
|
| 528 |
+
recommendations.append(OptimizationRecommendation(
|
| 529 |
+
action="Condenser Water Reset",
|
| 530 |
+
current_value=f"{input_data.avg_cooling_water_temp:.1f}°C",
|
| 531 |
+
recommended_value="Allow cooling tower to float higher",
|
| 532 |
+
expected_savings="3-8% pump energy reduction",
|
| 533 |
+
priority="MEDIUM",
|
| 534 |
+
operator_action="Reduce cooling tower fan speed or disable some cells"
|
| 535 |
))
|
| 536 |
|
| 537 |
# Free cooling recommendation
|
| 538 |
if input_data.avg_outside_temp < 50 and input_data.avg_humidity < 60:
|
| 539 |
+
estimated_savings_kw = current_power_kw * 0.35
|
| 540 |
+
estimated_savings_usd = estimated_savings_kw * annual_operating_hours * electricity_rate
|
| 541 |
recommendations.append(OptimizationRecommendation(
|
| 542 |
+
action="Free Cooling Mode",
|
| 543 |
+
current_value="Mechanical cooling only",
|
| 544 |
+
recommended_value="Enable waterside economizer",
|
| 545 |
+
expected_savings=f"35% (approx {estimated_savings_kw:.0f} kW, ${estimated_savings_usd:,.0f}/year)",
|
| 546 |
priority="HIGH",
|
| 547 |
+
operator_action="Open bypass valve for cooling tower to provide directly chilled water"
|
| 548 |
))
|
| 549 |
|
| 550 |
# Efficiency rating
|
| 551 |
+
rating = get_efficiency_rating(current_kw_per_tr)
|
| 552 |
+
current_cop = calculate_cop_from_kw_per_tr(current_kw_per_tr)
|
| 553 |
+
optimal_cop = calculate_cop_from_kw_per_tr(optimal_kw_per_tr)
|
| 554 |
|
| 555 |
summary = {
|
| 556 |
+
"plant_summary": f"{num_chillers} chillers, {input_data.total_building_load:.0f} TR total",
|
| 557 |
+
"current_efficiency": f"{current_kw_per_tr:.3f} kW/TR (COP: {current_cop:.2f})",
|
| 558 |
+
"current_power": f"{current_power_kw:.0f} kW ({current_power_kw/num_chillers:.0f} kW per chiller)",
|
| 559 |
+
"optimal_efficiency": f"{optimal_kw_per_tr:.3f} kW/TR (COP: {optimal_cop:.2f})",
|
| 560 |
+
"optimal_power": f"{optimal_power_kw:.0f} kW",
|
| 561 |
+
"power_savings": f"{power_savings_kw:.0f} kW ({savings_pct:.1f}%)",
|
| 562 |
+
"annual_savings_usd": f"${annual_savings_usd:,.0f}",
|
| 563 |
+
"co2_reduction": f"{co2_reduction_kg/1000:.1f} metric tons CO2/year",
|
| 564 |
"efficiency_rating": rating,
|
| 565 |
+
"load_per_chiller": f"{load_per_chiller:.0f} TR",
|
| 566 |
+
"recommended_setpoint": f"{optimal_sp:.1f}°C",
|
| 567 |
+
"electricity_rate": f"${electricity_rate:.2f}/kWh"
|
| 568 |
}
|
| 569 |
|
| 570 |
return OptimizeResponse(
|
| 571 |
timestamp=datetime.now().isoformat(),
|
| 572 |
+
current_kw_per_tr=round(current_kw_per_tr, 4),
|
| 573 |
+
current_total_power_kw=round(current_power_kw, 1),
|
| 574 |
+
optimal_kw_per_tr=round(optimal_kw_per_tr, 4),
|
| 575 |
+
optimal_total_power_kw=round(optimal_power_kw, 1),
|
| 576 |
efficiency_improvement_pct=round(savings_pct, 2),
|
| 577 |
+
power_savings_kw=round(power_savings_kw, 1),
|
| 578 |
+
power_savings_pct=round(power_savings_pct, 2),
|
| 579 |
+
annual_savings_usd=round(annual_savings_usd, 0),
|
| 580 |
+
co2_reduction_kg_per_year=round(co2_reduction_kg, 0),
|
| 581 |
recommendations=recommendations,
|
| 582 |
summary=summary
|
| 583 |
)
|
|
|
|
| 585 |
except Exception as e:
|
| 586 |
raise HTTPException(status_code=500, detail=str(e))
|
| 587 |
|
| 588 |
+
@app.post("/plant-analysis")
|
| 589 |
+
async def plant_analysis(input_data: ChillerInput):
|
| 590 |
+
"""Detailed analysis for 4-chiller plant"""
|
| 591 |
+
try:
|
| 592 |
+
if model is None:
|
| 593 |
+
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 594 |
+
|
| 595 |
+
kw_per_tr = predict_kw_per_tr(input_data)
|
| 596 |
+
num_chillers = input_data.num_chillers_operating or 4
|
| 597 |
+
|
| 598 |
+
# Calculate various metrics
|
| 599 |
+
total_power = kw_per_tr * input_data.total_building_load
|
| 600 |
+
power_per_chiller = total_power / num_chillers
|
| 601 |
+
|
| 602 |
+
# Compare to industry benchmarks
|
| 603 |
+
benchmarks = {
|
| 604 |
+
"excellent": 0.50,
|
| 605 |
+
"good": 0.60,
|
| 606 |
+
"average": 0.65,
|
| 607 |
+
"poor": 0.75
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
# Energy cost at different loads
|
| 611 |
+
load_levels = [50, 75, 100]
|
| 612 |
+
cost_analysis = []
|
| 613 |
+
for load_pct in load_levels:
|
| 614 |
+
load_tr = input_data.total_building_load * load_pct / 100
|
| 615 |
+
power_kw = kw_per_tr * load_tr
|
| 616 |
+
cost_analysis.append({
|
| 617 |
+
"load_pct": load_pct,
|
| 618 |
+
"load_tr": round(load_tr, 0),
|
| 619 |
+
"power_kw": round(power_kw, 0),
|
| 620 |
+
"power_per_chiller_kw": round(power_kw / num_chillers, 0)
|
| 621 |
+
})
|
| 622 |
+
|
| 623 |
+
return {
|
| 624 |
+
"timestamp": datetime.now().isoformat(),
|
| 625 |
+
"plant_configuration": {
|
| 626 |
+
"num_chillers": num_chillers,
|
| 627 |
+
"total_capacity_tr": input_data.total_building_load,
|
| 628 |
+
"current_load_tr": input_data.total_building_load,
|
| 629 |
+
"load_factor": "100%"
|
| 630 |
+
},
|
| 631 |
+
"performance_metrics": {
|
| 632 |
+
"kw_per_tr": round(kw_per_tr, 3),
|
| 633 |
+
"total_power_kw": round(total_power, 0),
|
| 634 |
+
"power_per_chiller_kw": round(power_per_chiller, 0),
|
| 635 |
+
"cop": round(calculate_cop_from_kw_per_tr(kw_per_tr), 2),
|
| 636 |
+
"vs_benchmark_excellent": f"{((kw_per_tr - benchmarks['excellent'])/benchmarks['excellent']*100):+.1f}%"
|
| 637 |
+
},
|
| 638 |
+
"cost_analysis": cost_analysis,
|
| 639 |
+
"recommendations": [
|
| 640 |
+
f"Target kW/TR < 0.60 for this plant size (currently {kw_per_tr:.3f})",
|
| 641 |
+
f"At full load, each chiller draws ~{power_per_chiller:.0f} kW",
|
| 642 |
+
"Review if all 4 chillers are needed at current load"
|
| 643 |
+
]
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
except Exception as e:
|
| 647 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 648 |
+
|
| 649 |
if __name__ == "__main__":
|
| 650 |
import uvicorn
|
| 651 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|