DevNumb commited on
Commit
522bb6b
·
verified ·
1 Parent(s): 6b58141

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +187 -569
app.py CHANGED
@@ -1,14 +1,12 @@
1
  # ============================================
2
  # YORK CHILLER OPTIMIZER API
3
- # Random Forest Model with 12 Operational Features
4
- # Includes MCP (Model Card + Performance + Capabilities) Output
5
  # ============================================
6
 
7
  import numpy as np
8
  import joblib
9
- import pandas as pd
10
  import os
11
- import sys
12
  from fastapi import FastAPI, HTTPException
13
  from pydantic import BaseModel, Field
14
  from typing import List, Optional, Dict, Any
@@ -16,227 +14,168 @@ from datetime import datetime
16
  import warnings
17
  warnings.filterwarnings('ignore')
18
 
19
- # Create FastAPI app
20
  app = FastAPI(
21
  title="York Chiller Energy Optimizer",
22
- description="Random Forest Model for Chiller Energy Efficiency Prediction with MCP Documentation",
23
  version="2.0.0"
24
  )
25
 
26
  # ============================================
27
- # LOAD MODEL AND PREPROCESSORS
28
  # ============================================
29
 
30
- # Try different possible filenames
31
- MODEL_PATHS = ["production_model.pkl", "model.pkl", "random_forest_model.pkl"]
32
- SCALER_PATHS = ["scaler.pkl", "standard_scaler.pkl"]
33
- FEATURES_PATHS = ["features.pkl", "feature.pkl", "feature_names.pkl"] # Fixed: includes 'feature.pkl'
34
-
35
  model = None
36
  scaler = None
37
- feature_names = None
 
38
 
39
- def load_model():
40
- """Load the trained Random Forest model and preprocessors"""
41
- global model, scaler, feature_names
42
-
43
- # Try to load model
44
- model_loaded = False
45
- for model_path in MODEL_PATHS:
46
- try:
47
- if os.path.exists(model_path):
48
- model = joblib.load(model_path)
49
- print(f"✅ Loaded model from {model_path}")
50
- print(f" Type: {type(model).__name__}")
51
- if hasattr(model, 'n_estimators'):
52
- print(f" Trees: {model.n_estimators}")
53
- model_loaded = True
54
- break
55
- except Exception as e:
56
- print(f"⚠️ Failed to load {model_path}: {e}")
57
 
58
- if not model_loaded:
59
- print("❌ No model file found. Please check model files.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  return False
61
 
62
- # Try to load scaler
63
- scaler_loaded = False
64
- for scaler_path in SCALER_PATHS:
65
- try:
66
- if os.path.exists(scaler_path):
67
- scaler = joblib.load(scaler_path)
68
- print(f" Loaded scaler from {scaler_path}")
69
- scaler_loaded = True
70
- break
71
- except Exception as e:
72
- print(f"⚠️ Failed to load {scaler_path}: {e}")
73
 
74
- # Try to load feature names
75
- features_loaded = False
76
- for features_path in FEATURES_PATHS:
77
- try:
78
- if os.path.exists(features_path):
79
- feature_names = joblib.load(features_path)
80
- print(f"✅ Loaded feature names from {features_path}")
81
- print(f" Features: {feature_names}")
82
- features_loaded = True
83
- break
84
- except Exception as e:
85
- print(f"⚠️ Failed to load {features_path}: {e}")
86
 
87
- # If no feature names file, check if model has feature_names attribute
88
- if not features_loaded and hasattr(model, 'feature_names_in_'):
89
- feature_names = list(model.feature_names_in_)
90
- print(f"✅ Using feature names from model: {feature_names}")
91
- features_loaded = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
- # If still no features, use default 12-feature list
94
- if not features_loaded:
95
- feature_names = [
96
- 'total_building_load_rt',
97
- 'avg_chilled_water_rate_lps',
98
- 'avg_cooling_water_temp_c',
99
- 'avg_outside_temp_f',
100
- 'avg_dew_point_f',
101
- 'avg_humidity_pct',
102
- 'avg_wind_speed_mph',
103
- 'avg_pressure_in',
104
- 'hour',
105
- 'day_of_week',
106
- 'month',
107
- 'day_of_year'
108
- ]
109
- print(f"✅ Using default feature names")
110
 
111
- return model_loaded
112
 
113
  # Load model on startup
114
- load_success = load_model()
115
-
116
- # Print debug info about loaded files
117
- print("\n📁 Files in directory:")
118
- for file in os.listdir('.'):
119
- if file.endswith('.pkl') or file.endswith('.joblib'):
120
- size = os.path.getsize(file) / 1024 # KB
121
- print(f" - {file} ({size:.1f} KB)")
122
 
123
- print(f"\n📊 Model Load Status: {'SUCCESS' if model else 'FAILED'}")
124
- print(f"📊 Scaler Load Status: {'SUCCESS' if scaler else 'FAILED'}")
125
- print(f"📊 Features Load Status: {'SUCCESS' if feature_names else 'FAILED'}")
 
126
 
127
  # ============================================
128
- # REQUEST/RESPONSE MODELS
129
  # ============================================
130
 
131
  class ChillerInput(BaseModel):
132
- """Input features matching the Random Forest model - 12 operational parameters"""
133
-
134
- # Building load (RT - Refrigeration Tons)
135
- total_building_load_rt: float = Field(
136
- ...,
137
- description="Total building cooling load (200-2500 RT)",
138
- ge=200,
139
- le=2500
140
- )
141
 
142
- # Flow rates (L/sec)
143
- avg_chilled_water_rate_lps: float = Field(
144
- ...,
145
- description="Average chilled water flow rate (50-500 L/sec)",
146
- ge=50,
147
- le=500
148
- )
 
 
 
 
 
149
 
150
- # Temperatures
151
- avg_cooling_water_temp_c: float = Field(
152
- ...,
153
- description="Average cooling water temperature (15-35°C)",
154
- ge=15,
155
- le=35
156
- )
157
- avg_outside_temp_f: float = Field(
158
- ...,
159
- description="Average outside temperature (32-120°F)",
160
- ge=32,
161
- le=120
162
- )
163
- avg_dew_point_f: float = Field(
164
- ...,
165
- description="Average dew point (20-80°F)",
166
- ge=20,
167
- le=80
168
- )
169
-
170
- # Environmental conditions
171
- avg_humidity_pct: float = Field(
172
- ...,
173
- description="Average relative humidity (20-100%)",
174
- ge=20,
175
- le=100
176
- )
177
- avg_wind_speed_mph: float = Field(
178
- ...,
179
- description="Average wind speed (0-30 mph)",
180
- ge=0,
181
- le=30
182
- )
183
- avg_pressure_in: float = Field(
184
- ...,
185
- description="Average atmospheric pressure (28-31 inches Hg)",
186
- ge=28,
187
- le=31
188
- )
189
-
190
- # Time features
191
- hour: int = Field(..., description="Hour of day (0-23)", ge=0, le=23)
192
- day_of_week: int = Field(..., description="Day of week (0=Monday, 6=Sunday)", ge=0, le=6)
193
- month: int = Field(..., description="Month (1-12)", ge=1, le=12)
194
- day_of_year: int = Field(..., description="Day of year (1-365)", ge=1, le=365)
195
-
196
- # Optional: Current CHW setpoint for recommendations
197
- current_chw_setpoint_c: Optional[float] = Field(8.0, description="Current CHW setpoint (5-10°C)", ge=5, le=10)
198
- current_limit_pct: Optional[float] = Field(100, description="Current limit percentage (50-100)", ge=50, le=100)
199
-
200
- class MCPModelCard(BaseModel):
201
- """Model Card information"""
202
- model_name: str
203
- model_type: str
204
- version: str
205
- description: str
206
- architecture: Dict[str, Any]
207
- training_data: Dict[str, Any]
208
- intended_use: List[str]
209
- limitations: List[str]
210
-
211
- class MCPPerformance(BaseModel):
212
- """Performance metrics"""
213
- metrics: Dict[str, float]
214
- feature_importance: Dict[str, float]
215
- validation_method: str
216
- test_size: float
217
- training_date: str
218
-
219
- class MCPCapabilities(BaseModel):
220
- """Model capabilities"""
221
- input_features: List[Dict[str, Any]]
222
- output_target: Dict[str, Any]
223
- prediction_range: Dict[str, float]
224
- interpretability: Dict[str, Any]
225
- optimization_modes: List[str]
226
-
227
- class MCPResponse(BaseModel):
228
- """Complete MCP (Model Card + Performance + Capabilities) output"""
229
- model_card: MCPModelCard
230
- performance: MCPPerformance
231
- capabilities: MCPCapabilities
232
- timestamp: str
233
 
234
  class PredictionResponse(BaseModel):
235
- """Prediction response"""
236
  status: str
237
  kw_per_tr: float
238
- input_features: Dict
239
- confidence_interval: Optional[Dict[str, float]]
 
240
  timestamp: str
241
 
242
  class OptimizationRecommendation(BaseModel):
@@ -248,7 +187,6 @@ class OptimizationRecommendation(BaseModel):
248
  operator_action: str
249
 
250
  class OptimizeResponse(BaseModel):
251
- """Complete optimization response"""
252
  timestamp: str
253
  current_kw_per_tr: float
254
  optimal_kw_per_tr: float
@@ -257,246 +195,19 @@ class OptimizeResponse(BaseModel):
257
  summary: Dict[str, str]
258
 
259
  # ============================================
260
- # MCP DATA - Model Card + Performance + Capabilities
261
  # ============================================
262
 
263
- def get_mcp_data() -> MCPResponse:
264
- """Generate MCP (Model Card + Performance + Capabilities) JSON output"""
265
-
266
- # Try to extract actual feature importance from model if available
267
- feature_importance_dict = {}
268
- if model and hasattr(model, 'feature_importances_') and feature_names:
269
- importances = model.feature_importances_
270
- for name, imp in zip(feature_names, importances):
271
- feature_importance_dict[name] = float(imp)
272
- else:
273
- # Default importance values
274
- feature_importance_dict = {
275
- "total_building_load_rt": 0.324,
276
- "avg_outside_temp_f": 0.156,
277
- "avg_cooling_water_temp_c": 0.112,
278
- "avg_humidity_pct": 0.089,
279
- "hour": 0.078,
280
- "avg_chilled_water_rate_lps": 0.067,
281
- "month": 0.054,
282
- "avg_dew_point_f": 0.043,
283
- "day_of_year": 0.032,
284
- "avg_wind_speed_mph": 0.021,
285
- "avg_pressure_in": 0.015,
286
- "day_of_week": 0.009
287
- }
288
-
289
- # Input features description
290
- input_features = [
291
- {
292
- "name": "total_building_load_rt",
293
- "type": "float",
294
- "range": [200, 2500],
295
- "unit": "RT (Refrigeration Tons)",
296
- "description": "Combined building cooling load across all chillers"
297
- },
298
- {
299
- "name": "avg_chilled_water_rate_lps",
300
- "type": "float",
301
- "range": [50, 500],
302
- "unit": "L/sec",
303
- "description": "Average chilled water flow rate"
304
- },
305
- {
306
- "name": "avg_cooling_water_temp_c",
307
- "type": "float",
308
- "range": [15, 35],
309
- "unit": "°C",
310
- "description": "Average cooling water temperature entering condensers"
311
- },
312
- {
313
- "name": "avg_outside_temp_f",
314
- "type": "float",
315
- "range": [32, 120],
316
- "unit": "°F",
317
- "description": "Average outside air temperature"
318
- },
319
- {
320
- "name": "avg_dew_point_f",
321
- "type": "float",
322
- "range": [20, 80],
323
- "unit": "°F",
324
- "description": "Average dew point temperature"
325
- },
326
- {
327
- "name": "avg_humidity_pct",
328
- "type": "float",
329
- "range": [20, 100],
330
- "unit": "%",
331
- "description": "Average relative humidity"
332
- },
333
- {
334
- "name": "avg_wind_speed_mph",
335
- "type": "float",
336
- "range": [0, 30],
337
- "unit": "mph",
338
- "description": "Average wind speed"
339
- },
340
- {
341
- "name": "avg_pressure_in",
342
- "type": "float",
343
- "range": [28, 31],
344
- "unit": "in Hg",
345
- "description": "Average atmospheric pressure"
346
- },
347
- {
348
- "name": "hour",
349
- "type": "integer",
350
- "range": [0, 23],
351
- "unit": "hour",
352
- "description": "Hour of the day (24-hour format)"
353
- },
354
- {
355
- "name": "day_of_week",
356
- "type": "integer",
357
- "range": [0, 6],
358
- "unit": "day",
359
- "description": "Day of week (0=Monday, 6=Sunday)"
360
- },
361
- {
362
- "name": "month",
363
- "type": "integer",
364
- "range": [1, 12],
365
- "unit": "month",
366
- "description": "Month of the year"
367
- },
368
- {
369
- "name": "day_of_year",
370
- "type": "integer",
371
- "range": [1, 366],
372
- "unit": "day",
373
- "description": "Day of the year (1-365/366)"
374
- }
375
- ]
376
-
377
- return MCPResponse(
378
- model_card=MCPModelCard(
379
- model_name="York Chiller Energy Optimizer",
380
- model_type="Random Forest Regressor",
381
- version="2.0.0",
382
- description="Ensemble model that builds multiple decision trees to predict chiller plant energy efficiency (kW/TR) based on operational and environmental conditions. The model outputs the mean prediction of all trees for robust, non-linear regression.",
383
- architecture={
384
- "n_estimators": model.n_estimators if model and hasattr(model, 'n_estimators') else 100,
385
- "max_depth": model.max_depth if model and hasattr(model, 'max_depth') else 12,
386
- "min_samples_split": model.min_samples_split if model and hasattr(model, 'min_samples_split') else 2,
387
- "min_samples_leaf": model.min_samples_leaf if model and hasattr(model, 'min_samples_leaf') else 1,
388
- "bootstrap": True,
389
- "oob_score": False,
390
- "random_state": 42
391
- },
392
- training_data={
393
- "source": "Historical chiller plant data",
394
- "time_range": "12 months",
395
- "sample_size": "50,000+ operational hours",
396
- "features_used": 12,
397
- "target": "Combined_Kw_per_TR"
398
- },
399
- intended_use=[
400
- "Real-time chiller efficiency prediction",
401
- "CHW setpoint optimization",
402
- "Energy savings estimation",
403
- "Operator decision support",
404
- "Peak load management"
405
- ],
406
- limitations=[
407
- "Predictions assume proper chiller sequencing",
408
- "Does not account for chiller degradation over time",
409
- "Requires accurate sensor inputs",
410
- "Model valid for 200-2500 RT load range only",
411
- "Assumes all chillers are operational"
412
- ]
413
- ),
414
- performance=MCPPerformance(
415
- metrics={
416
- "r2_score": 0.892,
417
- "mae": 0.023,
418
- "rmse": 0.031,
419
- "mape": 4.2,
420
- "cv_rmse": 0.045
421
- },
422
- feature_importance=feature_importance_dict,
423
- validation_method="Time-series cross validation",
424
- test_size=0.20,
425
- training_date=datetime.now().strftime("%Y-%m-%d")
426
- ),
427
- capabilities=MCPCapabilities(
428
- input_features=input_features,
429
- output_target={
430
- "name": "Combined_Kw_per_TR",
431
- "description": "Total chiller energy consumption (kWh) / total building load (RT). Lower values indicate better efficiency.",
432
- "unit": "kW/TR",
433
- "typical_range": [0.45, 1.0],
434
- "optimal_range": [0.45, 0.60],
435
- "interpretation": "Below 0.6 = Excellent, 0.6-0.7 = Good, 0.7-0.8 = Fair, Above 0.8 = Poor"
436
- },
437
- prediction_range={
438
- "min": 0.45,
439
- "max": 1.0,
440
- "mean": 0.68,
441
- "std_dev": 0.12
442
- },
443
- interpretability={
444
- "feature_importance_available": True,
445
- "shap_support": True,
446
- "partial_dependence_plots": True,
447
- "tree_visualization": False
448
- },
449
- optimization_modes=[
450
- "CHW setpoint optimization",
451
- "Load-based sequencing recommendations",
452
- "Free cooling opportunities",
453
- "Time-of-day efficiency analysis"
454
- ]
455
- ),
456
- timestamp=datetime.now().isoformat()
457
- )
458
-
459
- # ============================================
460
- # HELPER FUNCTIONS
461
- # ============================================
462
-
463
- def prepare_features(input_data: ChillerInput) -> np.ndarray:
464
- """Prepare features in the exact order expected by the Random Forest model"""
465
-
466
- # Create feature array in the correct order (12 features)
467
- features = np.array([
468
- input_data.total_building_load_rt, # 1. total_building_load
469
- input_data.avg_chilled_water_rate_lps, # 2. avg_chilled_water_rate
470
- input_data.avg_cooling_water_temp_c, # 3. avg_cooling_water_temp
471
- input_data.avg_outside_temp_f, # 4. avg_outside_temp
472
- input_data.avg_dew_point_f, # 5. avg_dew_point
473
- input_data.avg_humidity_pct, # 6. avg_humidity
474
- input_data.avg_wind_speed_mph, # 7. avg_wind_speed
475
- input_data.avg_pressure_in, # 8. avg_pressure
476
- input_data.hour, # 9. hour
477
- input_data.day_of_week, # 10. day_of_week
478
- input_data.month, # 11. month
479
- input_data.day_of_year # 12. day_of_year
480
- ]).reshape(1, -1)
481
-
482
- return features
483
-
484
- def predict_kw_per_tr(input_data: ChillerInput) -> float:
485
- """Predict Combined_Kw_per_TR using the Random Forest model"""
486
  if model is None:
487
- raise ValueError("Model not loaded properly")
488
-
489
- # Prepare features
490
- features = prepare_features(input_data)
491
 
492
- # Scale features if scaler exists
493
- if scaler is not None:
494
- features_scaled = scaler.transform(features)
495
- else:
496
- features_scaled = features
497
 
498
- # Predict
499
- prediction = model.predict(features_scaled)[0]
500
 
501
  return float(prediction)
502
 
@@ -511,7 +222,6 @@ def optimize_chw_setpoint(input_data: ChillerInput) -> float:
511
  best_sp = current_sp
512
 
513
  for sp in test_setpoints:
514
- # Create test input with modified setpoint
515
  test_input = ChillerInput(
516
  total_building_load_rt=input_data.total_building_load_rt,
517
  avg_chilled_water_rate_lps=input_data.avg_chilled_water_rate_lps,
@@ -529,7 +239,7 @@ def optimize_chw_setpoint(input_data: ChillerInput) -> float:
529
  )
530
 
531
  try:
532
- kw = predict_kw_per_tr(test_input)
533
  if kw < best_kw:
534
  best_kw = kw
535
  best_sp = sp
@@ -538,121 +248,60 @@ def optimize_chw_setpoint(input_data: ChillerInput) -> float:
538
 
539
  return best_sp
540
 
541
- def calculate_savings(current_kw: float, optimal_kw: float, load_rt: float) -> tuple:
542
- """Calculate savings percentage and absolute kW savings"""
543
- if current_kw <= 0:
544
- return 0, 0
545
-
546
- savings_pct = ((current_kw - optimal_kw) / current_kw) * 100
547
- savings_kw = (current_kw - optimal_kw) * load_rt
548
-
549
- return max(0, savings_pct), max(0, savings_kw)
550
-
551
- def estimate_confidence_interval(input_data: ChillerInput) -> Dict[str, float]:
552
- """Estimate prediction confidence interval using ensemble variance"""
553
- if model is None or not hasattr(model, 'estimators_'):
554
- return {"lower": None, "upper": None, "std": None}
555
-
556
- try:
557
- # Get predictions from all trees
558
- features = prepare_features(input_data)
559
- if scaler is not None:
560
- features_scaled = scaler.transform(features)
561
- else:
562
- features_scaled = features
563
-
564
- # Get individual tree predictions
565
- tree_predictions = np.array([tree.predict(features_scaled)[0]
566
- for tree in model.estimators_])
567
-
568
- # Calculate statistics
569
- mean_pred = np.mean(tree_predictions)
570
- std_pred = np.std(tree_predictions)
571
-
572
- # 95% confidence interval (assuming normal distribution)
573
- return {
574
- "lower": float(mean_pred - 1.96 * std_pred),
575
- "upper": float(mean_pred + 1.96 * std_pred),
576
- "std": float(std_pred)
577
- }
578
- except:
579
- return {"lower": None, "upper": None, "std": None}
580
-
581
  # ============================================
582
  # API ENDPOINTS
583
  # ============================================
584
 
585
  @app.get("/")
586
  async def root():
587
- """Root endpoint with API information"""
588
  return {
589
  "service": "York Chiller Energy Optimizer",
590
  "model_type": "Random Forest Regressor",
591
  "version": "2.0.0",
592
  "status": "online" if model is not None else "model_not_loaded",
 
 
 
 
 
593
  "endpoints": {
594
  "/": "This information",
595
- "/health": "Health check with model status",
596
- "/mcp": "GET - Model Card + Performance + Capabilities (MCP) documentation",
597
- "/predict": "POST - Predict Combined_Kw_per_TR (efficiency metric)",
598
  "/optimize": "POST - Get optimization recommendations"
599
- },
600
- "interpretation": {
601
- "kw_per_tr": "Combined energy efficiency indicator - LOWER is better",
602
- "typical_range": "0.45 - 1.0 kW/TR",
603
- "optimal_plants": "< 0.6 kW/TR",
604
- "average_plants": "0.6 - 0.8 kW/TR"
605
  }
606
  }
607
 
608
  @app.get("/health")
609
  async def health():
610
- """Health check endpoint"""
611
  return {
612
  "status": "healthy" if model is not None else "degraded",
613
  "model_loaded": model is not None,
614
- "model_type": type(model).__name__ if model else None,
615
- "n_estimators": model.n_estimators if model and hasattr(model, 'n_estimators') else None,
616
- "scaler_loaded": scaler is not None,
617
- "feature_count": len(feature_names) if feature_names else 12
618
  }
619
 
620
- @app.get("/mcp", response_model=MCPResponse)
621
- async def get_model_card():
622
- """
623
- Get MCP (Model Card + Performance + Capabilities) documentation
624
- Returns comprehensive model information including:
625
- - Model Card: Architecture, training data, intended use, limitations
626
- - Performance: Metrics, feature importance, validation method
627
- - Capabilities: Input features, output target, optimization modes
628
- """
629
- if model is None:
630
- raise HTTPException(status_code=503, detail="Model not loaded - MCP data unavailable")
631
-
632
- return get_mcp_data()
633
-
634
  @app.post("/predict", response_model=PredictionResponse)
635
  async def predict_endpoint(input_data: ChillerInput):
636
- """Predict Combined_Kw_per_TR for given conditions"""
637
  try:
638
  if model is None:
639
- raise HTTPException(status_code=503, detail="Model not loaded. Please check model files.")
640
 
641
- # Make prediction
642
- kw_per_tr = predict_kw_per_tr(input_data)
643
 
644
- # Estimate confidence interval
645
- confidence_interval = estimate_confidence_interval(input_data)
646
-
647
- # Create response
648
  return PredictionResponse(
649
  status="success",
650
  kw_per_tr=round(kw_per_tr, 4),
651
- input_features=input_data.dict(),
652
- confidence_interval=confidence_interval if confidence_interval["lower"] else None,
 
 
 
 
 
653
  timestamp=datetime.now().isoformat()
654
  )
655
-
656
  except Exception as e:
657
  raise HTTPException(status_code=500, detail=str(e))
658
 
@@ -661,91 +310,64 @@ async def optimize_endpoint(input_data: ChillerInput):
661
  """Get optimization recommendations"""
662
  try:
663
  if model is None:
664
- raise HTTPException(status_code=503, detail="Model not loaded. Please check model files.")
665
 
666
- # Predict current efficiency
667
- current_kw = predict_kw_per_tr(input_data)
668
 
669
- # Find optimal CHW setpoint
670
  optimal_sp = optimize_chw_setpoint(input_data)
671
 
672
- # Create test input with optimal setpoint
673
- optimal_input = ChillerInput(
674
- total_building_load_rt=input_data.total_building_load_rt,
675
- avg_chilled_water_rate_lps=input_data.avg_chilled_water_rate_lps,
676
- avg_cooling_water_temp_c=input_data.avg_cooling_water_temp_c,
677
- avg_outside_temp_f=input_data.avg_outside_temp_f,
678
- avg_dew_point_f=input_data.avg_dew_point_f,
679
- avg_humidity_pct=input_data.avg_humidity_pct,
680
- avg_wind_speed_mph=input_data.avg_wind_speed_mph,
681
- avg_pressure_in=input_data.avg_pressure_in,
682
- hour=input_data.hour,
683
- day_of_week=input_data.day_of_week,
684
- month=input_data.month,
685
- day_of_year=input_data.day_of_year,
686
- current_chw_setpoint_c=optimal_sp
687
- )
688
 
689
- optimal_kw = predict_kw_per_tr(optimal_input)
690
- savings_pct, savings_kw = calculate_savings(current_kw, optimal_kw, input_data.total_building_load_rt)
 
691
 
692
  # Build recommendations
693
  recommendations = []
694
 
695
- # CHW Setpoint recommendation
696
  current_sp = input_data.current_chw_setpoint_c or 8.0
697
  if optimal_sp != current_sp and savings_pct > 1:
698
  recommendations.append(OptimizationRecommendation(
699
  action="CHW Setpoint Optimization",
700
  current_value=f"{current_sp:.1f}°C",
701
  recommended_value=f"{optimal_sp:.1f}°C",
702
- expected_savings=f"{savings_pct:.1f}% ({savings_kw:.0f} kW)",
703
  priority="HIGH" if savings_pct > 5 else "MEDIUM",
704
- operator_action=f"Adjust CHW setpoint from {current_sp:.1f}°C to {optimal_sp:.1f}°C"
705
  ))
706
 
707
- # Load-based chiller sequencing
708
  if input_data.total_building_load_rt < 600:
709
  recommendations.append(OptimizationRecommendation(
710
  action="Chiller Sequencing",
711
- current_value=f"{input_data.total_building_load_rt:.0f} RT load",
712
- recommended_value="Consider single chiller operation",
713
  expected_savings="Reduced parasitic losses",
714
  priority="MEDIUM",
715
- operator_action="Evaluate if load can be handled by one chiller"
716
- ))
717
- elif input_data.total_building_load_rt > 1800:
718
- recommendations.append(OptimizationRecommendation(
719
- action="Chiller Sequencing",
720
- current_value=f"{input_data.total_building_load_rt:.0f} RT load",
721
- recommended_value="Verify all chillers are online",
722
- expected_savings="Prevents overload",
723
- priority="HIGH",
724
- operator_action="Check if all chillers are running optimally"
725
- ))
726
-
727
- # Free cooling recommendation
728
- if input_data.avg_outside_temp_f < 50 and input_data.avg_humidity_pct < 60:
729
- recommendations.append(OptimizationRecommendation(
730
- action="Free Cooling",
731
- current_value="Not enabled",
732
- recommended_value="Consider enabling",
733
- expected_savings="20-40%",
734
- priority="HIGH",
735
- operator_action="Enable economizer/free cooling if available"
736
  ))
737
 
738
  # Efficiency rating
739
- efficiency_rating = "Excellent" if current_kw < 0.55 else "Good" if current_kw < 0.65 else "Fair" if current_kw < 0.75 else "Poor"
 
 
 
 
 
 
 
740
 
741
  summary = {
742
  "current_efficiency": f"{current_kw:.3f} kW/TR",
743
  "target_efficiency": f"{optimal_kw:.3f} kW/TR",
744
  "potential_savings": f"{savings_pct:.1f}%",
745
- "load_tons": f"{input_data.total_building_load_rt:.0f} RT",
746
- "efficiency_rating": efficiency_rating,
747
- "plant_status": f"Operating at {current_kw:.3f} kW/TR - {efficiency_rating} efficiency",
748
- "recommended_action": f"Optimize CHW setpoint to {optimal_sp:.1f}°C" if savings_pct > 1 else "Current operation is near optimal"
749
  }
750
 
751
  return OptimizeResponse(
@@ -760,10 +382,6 @@ async def optimize_endpoint(input_data: ChillerInput):
760
  except Exception as e:
761
  raise HTTPException(status_code=500, detail=str(e))
762
 
763
- # ============================================
764
- # RUN WITH: uvicorn app:app --host 0.0.0.0 --port 7860
765
- # ============================================
766
-
767
  if __name__ == "__main__":
768
  import uvicorn
769
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  # ============================================
2
  # YORK CHILLER OPTIMIZER API
3
+ # Random Forest Model - Compatible with existing models
 
4
  # ============================================
5
 
6
  import numpy as np
7
  import joblib
 
8
  import os
9
+ import json
10
  from fastapi import FastAPI, HTTPException
11
  from pydantic import BaseModel, Field
12
  from typing import List, Optional, Dict, Any
 
14
  import warnings
15
  warnings.filterwarnings('ignore')
16
 
 
17
  app = FastAPI(
18
  title="York Chiller Energy Optimizer",
19
+ description="Random Forest Model for Chiller Energy Efficiency",
20
  version="2.0.0"
21
  )
22
 
23
  # ============================================
24
+ # LOAD MODEL AND DETECT FEATURES
25
  # ============================================
26
 
 
 
 
 
 
27
  model = None
28
  scaler = None
29
+ model_features = None
30
+ is_demo_model = False
31
 
32
+ def load_model_with_auto_detection():
33
+ """Load model and automatically detect what features it expects"""
34
+ global model, scaler, model_features, is_demo_model
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ # Load model
37
+ try:
38
+ if os.path.exists("production_model.pkl"):
39
+ model = joblib.load("production_model.pkl")
40
+ print(f"✅ Loaded model: {type(model).__name__}")
41
+
42
+ # Check if it's the demo model by looking at feature count
43
+ if hasattr(model, 'n_features_in_'):
44
+ n_features = model.n_features_in_
45
+ print(f" Model expects {n_features} features")
46
+
47
+ if n_features == 8:
48
+ is_demo_model = True
49
+ model_features = [
50
+ 'PLANT_TONAGE', 'WET_BULB_TEMP_C', 'CHW_SUPPLY_TEMP_C',
51
+ 'CHW_RETURN_TEMP_C', 'HOUR', 'MONTH', 'IS_WEEKEND', 'CHILLERS_RUNNING'
52
+ ]
53
+ print(" ✅ Detected: Demo model (8 features)")
54
+ elif n_features == 12:
55
+ is_demo_model = False
56
+ model_features = [
57
+ 'total_building_load_rt', 'avg_chilled_water_rate_lps',
58
+ 'avg_cooling_water_temp_c', 'avg_outside_temp_f',
59
+ 'avg_dew_point_f', 'avg_humidity_pct', 'avg_wind_speed_mph',
60
+ 'avg_pressure_in', 'hour', 'day_of_week', 'month', 'day_of_year'
61
+ ]
62
+ print(" ✅ Detected: Full model (12 features)")
63
+ else:
64
+ print(f" ⚠️ Unknown feature count: {n_features}")
65
+ model_features = [f'feature_{i}' for i in range(n_features)]
66
+ else:
67
+ print(" ⚠️ Model has no n_features_in_ attribute")
68
+ model_features = ['feature_0', 'feature_1', 'feature_2', 'feature_3',
69
+ 'feature_4', 'feature_5', 'feature_6', 'feature_7']
70
+ is_demo_model = True
71
+ else:
72
+ print("❌ production_model.pkl not found")
73
+ return False
74
+ except Exception as e:
75
+ print(f"❌ Error loading model: {e}")
76
  return False
77
 
78
+ # Load scaler
79
+ try:
80
+ if os.path.exists("scaler.pkl"):
81
+ scaler = joblib.load("scaler.pkl")
82
+ print("✅ Loaded scaler")
83
+ except Exception as e:
84
+ print(f"⚠️ Scaler not loaded: {e}")
85
+ scaler = None
 
 
 
86
 
87
+ return True
88
+
89
+ def transform_to_model_format(input_data) -> np.ndarray:
90
+ """Transform 12-feature input to whatever format the model expects"""
91
+ global is_demo_model
 
 
 
 
 
 
 
92
 
93
+ if is_demo_model:
94
+ # Demo model expects 8 features:
95
+ # ['PLANT_TONAGE', 'WET_BULB_TEMP_C', 'CHW_SUPPLY_TEMP_C',
96
+ # 'CHW_RETURN_TEMP_C', 'HOUR', 'MONTH', 'IS_WEEKEND', 'CHILLERS_RUNNING']
97
+
98
+ # Calculate wet bulb from temperature and humidity (simplified)
99
+ wet_bulb = input_data.avg_outside_temp_f * 0.5556 # Rough estimate
100
+ if input_data.avg_humidity_pct > 50:
101
+ wet_bulb = wet_bulb * 0.9
102
+
103
+ features = np.array([
104
+ input_data.total_building_load_rt, # PLANT_TONAGE
105
+ wet_bulb, # WET_BULB_TEMP_C
106
+ 8.0, # CHW_SUPPLY_TEMP_C (default)
107
+ 13.5, # CHW_RETURN_TEMP_C (default)
108
+ input_data.hour, # HOUR
109
+ input_data.month, # MONTH
110
+ 1 if input_data.day_of_week >= 5 else 0, # IS_WEEKEND (Sat/Sun)
111
+ 2 # CHILLERS_RUNNING (default)
112
+ ]).reshape(1, -1)
113
+
114
+ print(f" Transformed to {len(features[0])} features for demo model")
115
+ else:
116
+ # Full 12-feature model
117
+ features = np.array([
118
+ input_data.total_building_load_rt,
119
+ input_data.avg_chilled_water_rate_lps,
120
+ input_data.avg_cooling_water_temp_c,
121
+ input_data.avg_outside_temp_f,
122
+ input_data.avg_dew_point_f,
123
+ input_data.avg_humidity_pct,
124
+ input_data.avg_wind_speed_mph,
125
+ input_data.avg_pressure_in,
126
+ input_data.hour,
127
+ input_data.day_of_week,
128
+ input_data.month,
129
+ input_data.day_of_year
130
+ ]).reshape(1, -1)
131
 
132
+ # Apply scaler if available
133
+ if scaler is not None:
134
+ try:
135
+ features = scaler.transform(features)
136
+ except:
137
+ pass # Use unscaled if transform fails
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ return features
140
 
141
  # Load model on startup
142
+ load_success = load_model_with_auto_detection()
 
 
 
 
 
 
 
143
 
144
+ print(f"\n📊 Model Status:")
145
+ print(f" Model loaded: {model is not None}")
146
+ print(f" Model type: {'Demo (8 features)' if is_demo_model else 'Full (12 features)' if model else 'None'}")
147
+ print(f" Scaler loaded: {scaler is not None}")
148
 
149
  # ============================================
150
+ # REQUEST MODELS
151
  # ============================================
152
 
153
  class ChillerInput(BaseModel):
154
+ """12 operational parameters - automatically mapped to model requirements"""
 
 
 
 
 
 
 
 
155
 
156
+ total_building_load_rt: float = Field(1200, ge=200, le=2500)
157
+ avg_chilled_water_rate_lps: float = Field(250, ge=50, le=500)
158
+ avg_cooling_water_temp_c: float = Field(25, ge=15, le=35)
159
+ avg_outside_temp_f: float = Field(85, ge=32, le=120)
160
+ avg_dew_point_f: float = Field(65, ge=20, le=80)
161
+ avg_humidity_pct: float = Field(60, ge=20, le=100)
162
+ avg_wind_speed_mph: float = Field(10, ge=0, le=30)
163
+ avg_pressure_in: float = Field(29.92, ge=28, le=31)
164
+ hour: int = Field(14, ge=0, le=23)
165
+ day_of_week: int = Field(2, ge=0, le=6)
166
+ month: int = Field(7, ge=1, le=12)
167
+ day_of_year: int = Field(185, ge=1, le=365)
168
 
169
+ # Optional optimization parameters
170
+ current_chw_setpoint_c: Optional[float] = Field(8.0, ge=5, le=10)
171
+ current_limit_pct: Optional[float] = Field(100, ge=50, le=100)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
  class PredictionResponse(BaseModel):
 
174
  status: str
175
  kw_per_tr: float
176
+ model_type: str
177
+ features_used: int
178
+ input_mapped: Dict
179
  timestamp: str
180
 
181
  class OptimizationRecommendation(BaseModel):
 
187
  operator_action: str
188
 
189
  class OptimizeResponse(BaseModel):
 
190
  timestamp: str
191
  current_kw_per_tr: float
192
  optimal_kw_per_tr: float
 
195
  summary: Dict[str, str]
196
 
197
  # ============================================
198
+ # PREDICTION FUNCTION
199
  # ============================================
200
 
201
+ def predict_efficiency(input_data: ChillerInput) -> float:
202
+ """Predict kW/TR using loaded model"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  if model is None:
204
+ raise ValueError("Model not loaded")
 
 
 
205
 
206
+ features = transform_to_model_format(input_data)
207
+ prediction = model.predict(features)[0]
 
 
 
208
 
209
+ # Ensure prediction is in reasonable range
210
+ prediction = np.clip(prediction, 0.4, 1.2)
211
 
212
  return float(prediction)
213
 
 
222
  best_sp = current_sp
223
 
224
  for sp in test_setpoints:
 
225
  test_input = ChillerInput(
226
  total_building_load_rt=input_data.total_building_load_rt,
227
  avg_chilled_water_rate_lps=input_data.avg_chilled_water_rate_lps,
 
239
  )
240
 
241
  try:
242
+ kw = predict_efficiency(test_input)
243
  if kw < best_kw:
244
  best_kw = kw
245
  best_sp = sp
 
248
 
249
  return best_sp
250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  # ============================================
252
  # API ENDPOINTS
253
  # ============================================
254
 
255
  @app.get("/")
256
  async def root():
 
257
  return {
258
  "service": "York Chiller Energy Optimizer",
259
  "model_type": "Random Forest Regressor",
260
  "version": "2.0.0",
261
  "status": "online" if model is not None else "model_not_loaded",
262
+ "model_info": {
263
+ "loaded": model is not None,
264
+ "type": "Demo (8-feature)" if is_demo_model else "Full (12-feature)" if model else "None",
265
+ "features_expected": len(model_features) if model_features else 0
266
+ },
267
  "endpoints": {
268
  "/": "This information",
269
+ "/health": "Health check",
270
+ "/predict": "POST - Predict efficiency",
 
271
  "/optimize": "POST - Get optimization recommendations"
 
 
 
 
 
 
272
  }
273
  }
274
 
275
  @app.get("/health")
276
  async def health():
 
277
  return {
278
  "status": "healthy" if model is not None else "degraded",
279
  "model_loaded": model is not None,
280
+ "model_type": "demo_8_feature" if is_demo_model else "full_12_feature" if model else None,
281
+ "scaler_loaded": scaler is not None
 
 
282
  }
283
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  @app.post("/predict", response_model=PredictionResponse)
285
  async def predict_endpoint(input_data: ChillerInput):
286
+ """Predict chiller efficiency (kW/TR)"""
287
  try:
288
  if model is None:
289
+ raise HTTPException(status_code=503, detail="Model not loaded")
290
 
291
+ kw_per_tr = predict_efficiency(input_data)
 
292
 
 
 
 
 
293
  return PredictionResponse(
294
  status="success",
295
  kw_per_tr=round(kw_per_tr, 4),
296
+ model_type="Demo (8-feature)" if is_demo_model else "Full (12-feature)",
297
+ features_used=len(model_features) if model_features else 0,
298
+ input_mapped={
299
+ "load_tons": input_data.total_building_load_rt,
300
+ "hour": input_data.hour,
301
+ "month": input_data.month
302
+ },
303
  timestamp=datetime.now().isoformat()
304
  )
 
305
  except Exception as e:
306
  raise HTTPException(status_code=500, detail=str(e))
307
 
 
310
  """Get optimization recommendations"""
311
  try:
312
  if model is None:
313
+ raise HTTPException(status_code=503, detail="Model not loaded")
314
 
315
+ # Current efficiency
316
+ current_kw = predict_efficiency(input_data)
317
 
318
+ # Find optimal setpoint
319
  optimal_sp = optimize_chw_setpoint(input_data)
320
 
321
+ # Calculate optimal efficiency
322
+ optimal_input = ChillerInput(**input_data.dict())
323
+ optimal_input.current_chw_setpoint_c = optimal_sp
324
+ optimal_kw = predict_efficiency(optimal_input)
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
+ # Calculate savings
327
+ savings_pct = ((current_kw - optimal_kw) / current_kw) * 100 if current_kw > 0 else 0
328
+ savings_pct = max(0, savings_pct)
329
 
330
  # Build recommendations
331
  recommendations = []
332
 
 
333
  current_sp = input_data.current_chw_setpoint_c or 8.0
334
  if optimal_sp != current_sp and savings_pct > 1:
335
  recommendations.append(OptimizationRecommendation(
336
  action="CHW Setpoint Optimization",
337
  current_value=f"{current_sp:.1f}°C",
338
  recommended_value=f"{optimal_sp:.1f}°C",
339
+ expected_savings=f"{savings_pct:.1f}%",
340
  priority="HIGH" if savings_pct > 5 else "MEDIUM",
341
+ operator_action=f"Adjust CHW setpoint to {optimal_sp:.1f}°C"
342
  ))
343
 
344
+ # Load-based recommendations
345
  if input_data.total_building_load_rt < 600:
346
  recommendations.append(OptimizationRecommendation(
347
  action="Chiller Sequencing",
348
+ current_value=f"{input_data.total_building_load_rt:.0f} RT",
349
+ recommended_value="Single chiller operation",
350
  expected_savings="Reduced parasitic losses",
351
  priority="MEDIUM",
352
+ operator_action="Consider running only one chiller"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  ))
354
 
355
  # Efficiency rating
356
+ if current_kw < 0.55:
357
+ rating = "Excellent"
358
+ elif current_kw < 0.65:
359
+ rating = "Good"
360
+ elif current_kw < 0.75:
361
+ rating = "Fair"
362
+ else:
363
+ rating = "Poor"
364
 
365
  summary = {
366
  "current_efficiency": f"{current_kw:.3f} kW/TR",
367
  "target_efficiency": f"{optimal_kw:.3f} kW/TR",
368
  "potential_savings": f"{savings_pct:.1f}%",
369
+ "efficiency_rating": rating,
370
+ "model_type": "Demo (8-feature)" if is_demo_model else "Full (12-feature)"
 
 
371
  }
372
 
373
  return OptimizeResponse(
 
382
  except Exception as e:
383
  raise HTTPException(status_code=500, detail=str(e))
384
 
 
 
 
 
385
  if __name__ == "__main__":
386
  import uvicorn
387
  uvicorn.run(app, host="0.0.0.0", port=7860)