File size: 25,919 Bytes
eac7bdc
 
ea59c0a
8a4b04d
eac7bdc
 
8b21b2a
 
 
 
 
ea59c0a
eac7bdc
84bb389
 
324a022
ea59c0a
 
8b21b2a
 
eac7bdc
 
324a022
84bb389
eac7bdc
 
8a4b04d
 
be6de94
eac7bdc
 
 
324a022
eac7bdc
 
 
 
8b21b2a
eac7bdc
324a022
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84bb389
324a022
8b21b2a
 
5b33abd
324a022
 
84bb389
 
324a022
 
14513fd
74de7b4
ea59c0a
522bb6b
74de7b4
84bb389
74de7b4
 
 
324a022
74de7b4
 
84bb389
 
 
522bb6b
ea59c0a
 
5b33abd
324a022
 
 
522bb6b
8b21b2a
324a022
 
5b33abd
84bb389
324a022
 
ea59c0a
 
 
 
 
 
 
 
 
 
 
 
84bb389
324a022
 
ea59c0a
324a022
 
 
 
 
5b33abd
ea59c0a
 
 
 
 
 
 
8b21b2a
5b33abd
324a022
 
5b33abd
74de7b4
324a022
84bb389
ea59c0a
 
324a022
74de7b4
324a022
eac7bdc
be6de94
84bb389
be6de94
 
 
8a4b04d
 
 
324a022
 
 
 
 
 
8b21b2a
 
 
 
eac7bdc
8a4b04d
14513fd
8a4b04d
 
be6de94
 
 
 
8a4b04d
 
 
 
 
324a022
8a4b04d
 
522bb6b
84bb389
be6de94
 
 
eac7bdc
 
 
 
 
 
 
be6de94
eac7bdc
be6de94
8a4b04d
be6de94
8a4b04d
eac7bdc
8a4b04d
 
 
 
be6de94
eac7bdc
 
be6de94
324a022
be6de94
 
ea59c0a
 
 
 
14513fd
84bb389
 
 
 
 
 
 
 
 
 
 
 
 
 
ea59c0a
8b21b2a
84bb389
 
 
 
 
 
8b21b2a
ea59c0a
84bb389
14513fd
8a4b04d
 
 
 
be6de94
 
 
8a4b04d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324a022
84bb389
324a022
 
 
 
 
 
 
 
 
 
84bb389
324a022
 
 
 
 
 
 
 
 
ea59c0a
 
 
 
84bb389
324a022
 
 
 
 
 
 
 
 
 
eac7bdc
 
 
 
 
 
8a4b04d
ea59c0a
 
eac7bdc
8a4b04d
be6de94
 
8a4b04d
be6de94
522bb6b
 
ea59c0a
84bb389
ea59c0a
 
522bb6b
eac7bdc
be6de94
522bb6b
8a4b04d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8b21b2a
 
8a4b04d
 
 
 
eac7bdc
 
 
 
 
8b21b2a
ea59c0a
 
eac7bdc
be6de94
eac7bdc
324a022
ea59c0a
8b21b2a
ea59c0a
 
eac7bdc
 
 
be6de94
8a4b04d
eac7bdc
be6de94
84bb389
eac7bdc
8a4b04d
8b21b2a
8a4b04d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324a022
eac7bdc
8a4b04d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea59c0a
 
eac7bdc
 
be6de94
8a4b04d
 
 
 
 
324a022
8a4b04d
 
ea59c0a
84bb389
be6de94
eac7bdc
8b21b2a
eac7bdc
 
 
be6de94
 
8a4b04d
eac7bdc
be6de94
84bb389
eac7bdc
8a4b04d
 
 
 
 
 
eac7bdc
324a022
8a4b04d
 
eac7bdc
522bb6b
8a4b04d
522bb6b
eac7bdc
8a4b04d
 
 
 
 
 
 
 
 
 
 
 
 
eac7bdc
 
8a4b04d
324a022
 
be6de94
 
 
324a022
8a4b04d
eac7bdc
8a4b04d
14513fd
 
8a4b04d
 
 
 
14513fd
 
8a4b04d
 
 
14513fd
8a4b04d
14513fd
324a022
14513fd
8a4b04d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eac7bdc
 
84bb389
 
8a4b04d
 
84bb389
8a4b04d
 
 
 
84bb389
8a4b04d
84bb389
 
be6de94
8a4b04d
 
 
be6de94
eac7bdc
8a4b04d
 
 
 
 
 
 
 
522bb6b
8a4b04d
 
 
eac7bdc
 
be6de94
eac7bdc
8a4b04d
 
 
 
eac7bdc
8a4b04d
 
 
 
eac7bdc
 
 
 
 
 
 
8a4b04d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eac7bdc
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
# ============================================
# YORK CHILLER OPTIMIZER API
# Compatible with NumPy 2.0.2 and pandas 2.2.3
# For 4-Chiller Plants (1000+ TR)
# ============================================

import os
import sys
import warnings
warnings.filterwarnings('ignore')

# Import libraries
import numpy as np
print(f"NumPy version: {np.__version__}")

import pandas as pd
print(f"Pandas version: {pd.__version__}")

from datetime import datetime
from typing import List, Optional, Dict, Any
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import joblib
import pickle

app = FastAPI(
    title="York Chiller Energy Optimizer - 4 Chiller Plant Edition",
    description="Random Forest Model for Chiller Energy Efficiency - 12 Features (1000+ TR Plants)",
    version="2.0.0"
)

# ============================================
# LOAD MODEL FILES
# ============================================

model = None
scaler = None
feature_names = None

# Exact feature names from your model
EXPECTED_FEATURES = [
    'total_building_load',
    'avg_chilled_water_rate',
    'avg_cooling_water_temp',
    'avg_outside_temp',
    'avg_dew_point',
    'avg_humidity',
    'avg_wind_speed',
    'avg_pressure',
    'hour',
    'day_of_week',
    'month',
    'day_of_year'
]

def load_model_files():
    """Load the existing model files with NumPy 2.0.2"""
    global model, scaler, feature_names
    
    print("\n📂 Checking for model files...")
    
    # Check for production_model.pkl
    if os.path.exists("production_model.pkl"):
        file_size = os.path.getsize("production_model.pkl") / 1024
        print(f"   ✅ Found: production_model.pkl ({file_size:.1f} KB)")
    else:
        print(f"   ❌ Missing: production_model.pkl")
        return False
    
    # Load model with joblib
    try:
        model = joblib.load("production_model.pkl")
        print(f"\n✅ Model loaded successfully with joblib")
        print(f"   Type: {type(model).__name__}")
        
        if hasattr(model, 'n_features_in_'):
            print(f"   Features expected: {model.n_features_in_}")
        if hasattr(model, 'n_estimators'):
            print(f"   Number of trees: {model.n_estimators}")
        if hasattr(model, 'max_depth'):
            print(f"   Max depth: {model.max_depth}")
            
    except Exception as e:
        print(f"⚠️ Error loading model with joblib: {e}")
        return False
    
    # Load scaler if exists
    if os.path.exists("scaler.pkl"):
        try:
            scaler = joblib.load("scaler.pkl")
            print(f"✅ Scaler loaded")
        except Exception as e:
            print(f"⚠️ Could not load scaler: {e}")
    
    # Load feature names from features.pkl
    if os.path.exists("features.pkl"):
        try:
            feature_data = joblib.load("features.pkl")
            # Convert to list if it's a pandas Series or other type
            if hasattr(feature_data, 'tolist'):
                feature_names = feature_data.tolist()
            elif isinstance(feature_data, (list, tuple)):
                feature_names = list(feature_data)
            elif hasattr(feature_data, 'values'):
                feature_names = list(feature_data.values)
            else:
                feature_names = list(feature_data)
            print(f"✅ Features loaded from features.pkl: {len(feature_names)} features")
            if len(feature_names) > 0:
                print(f"   First 3 features: {feature_names[:3]}")
        except Exception as e:
            print(f"⚠️ Could not load features.pkl: {e}")
            feature_names = None
    
    # Use default feature names if none loaded
    if feature_names is None:
        feature_names = EXPECTED_FEATURES
        print(f"✅ Using default feature names: {len(feature_names)} features")
    
    # Ensure feature_names is a list
    if not isinstance(feature_names, list):
        if hasattr(feature_names, 'tolist'):
            feature_names = feature_names.tolist()
        else:
            feature_names = list(feature_names)
    
    return True

# Load model on startup
load_success = load_model_files()

if model:
    print(f"\n📊 Model Status: ✅ ONLINE")
    print(f"   NumPy version: {np.__version__}")
    print(f"   Pandas version: {pd.__version__}")
    print(f"   Features: {len(feature_names) if feature_names else 0}")
    print(f"   Scaler: {'✅' if scaler else '❌'}")
else:
    print(f"\n📊 Model Status: ❌ OFFLINE")

# ============================================
# REQUEST MODELS
# ============================================

class ChillerInput(BaseModel):
    """12 input features - optimized for 1000+ TR plants"""
    total_building_load: float = Field(..., ge=400, le=2500, description="Total building cooling load (RT) - For 4 chiller plant: 400-2500 TR")
    avg_chilled_water_rate: float = Field(..., ge=200, le=2000, description="Average chilled water flow rate (L/sec)")
    avg_cooling_water_temp: float = Field(..., ge=15, le=35, description="Average cooling water temperature (°C)")
    avg_outside_temp: float = Field(..., ge=32, le=120, description="Average outside air temperature (°F)")
    avg_dew_point: float = Field(..., ge=20, le=80, description="Average dew point temperature (°F)")
    avg_humidity: float = Field(..., ge=20, le=100, description="Average relative humidity (%)")
    avg_wind_speed: float = Field(..., ge=0, le=30, description="Average wind speed (mph)")
    avg_pressure: float = Field(..., ge=28, le=31, description="Average atmospheric pressure (in Hg)")
    hour: int = Field(..., ge=0, le=23, description="Hour of the day (0-23)")
    day_of_week: int = Field(..., ge=0, le=6, description="Day of week (0=Monday, 6=Sunday)")
    month: int = Field(..., ge=1, le=12, description="Month of the year (1-12)")
    day_of_year: int = Field(..., ge=1, le=366, description="Day of the year (1-365)")
    
    # Optional parameters for 4-chiller plant
    current_chw_setpoint_c: Optional[float] = Field(8.0, ge=5, le=10, description="Current CHW setpoint (°C)")
    num_chillers_operating: Optional[int] = Field(4, ge=1, le=4, description="Number of chillers currently operating")
    electricity_rate_usd_per_kwh: Optional[float] = Field(0.12, ge=0.05, le=0.50, description="Electricity rate ($/kWh)")

class PredictionResponse(BaseModel):
    status: str
    kw_per_tr: float
    total_power_kw: float
    power_per_chiller_kw: float
    cooling_load_tr: float
    load_per_chiller_tr: float
    cop: float
    efficiency_rating: str
    plant_summary: Dict[str, Any]
    annual_cost_estimate: Dict[str, Any]
    features_used: int
    model_type: str
    timestamp: str

class OptimizationRecommendation(BaseModel):
    action: str
    current_value: str
    recommended_value: str
    expected_savings: str
    priority: str
    operator_action: str

class OptimizeResponse(BaseModel):
    timestamp: str
    current_kw_per_tr: float
    current_total_power_kw: float
    optimal_kw_per_tr: float
    optimal_total_power_kw: float
    efficiency_improvement_pct: float
    power_savings_kw: float
    power_savings_pct: float
    annual_savings_usd: float
    co2_reduction_kg_per_year: float
    recommendations: List[OptimizationRecommendation]
    summary: Dict[str, str]

# ============================================
# PREDICTION FUNCTIONS
# ============================================

def predict_kw_per_tr(input_data: ChillerInput) -> float:
    """Predict Combined_Kw_per_TR using the loaded model"""
    if model is None:
        raise ValueError("Model not loaded")
    
    # Create feature array with exact order
    features = np.array([[
        input_data.total_building_load,
        input_data.avg_chilled_water_rate,
        input_data.avg_cooling_water_temp,
        input_data.avg_outside_temp,
        input_data.avg_dew_point,
        input_data.avg_humidity,
        input_data.avg_wind_speed,
        input_data.avg_pressure,
        input_data.hour,
        input_data.day_of_week,
        input_data.month,
        input_data.day_of_year
    ]], dtype=np.float64)
    
    # Apply scaler if available
    if scaler is not None:
        try:
            features = scaler.transform(features)
        except Exception as e:
            print(f"   ⚠️ Scaler transform failed: {e}")
    
    # Predict
    prediction = model.predict(features)[0]
    
    # Typical range for large centrifugal chillers (1000+ TR)
    # Modern efficient: 0.45-0.55 kW/TR
    # Older units: 0.60-0.80 kW/TR
    prediction = np.clip(prediction, 0.40, 0.90)
    
    return float(prediction)

def calculate_total_power(kw_per_tr: float, cooling_load_tr: float) -> float:
    """
    Calculate total chiller plant power consumption for all chillers
    
    Formula: Total Power (kW) = kW/TR × Cooling Load (TR)
    """
    return kw_per_tr * cooling_load_tr

def calculate_annual_energy_cost(kw_per_tr: float, avg_load_tr: float, 
                                  operating_hours: int = 3000, 
                                  electricity_rate: float = 0.12) -> dict:
    """
    Calculate annual energy cost and consumption
    """
    avg_power_kw = kw_per_tr * avg_load_tr
    annual_kwh = avg_power_kw * operating_hours
    annual_cost_usd = annual_kwh * electricity_rate
    
    return {
        "avg_power_kw": round(avg_power_kw, 1),
        "annual_kwh": round(annual_kwh / 1000, 1),
        "annual_cost_usd": round(annual_cost_usd, 0),
        "operating_hours": operating_hours,
        "electricity_rate": electricity_rate
    }

def calculate_cop_from_kw_per_tr(kw_per_tr: float) -> float:
    """
    Convert kW/TR to COP (Coefficient of Performance)
    
    Formula: COP = 3.516 / kW/TR
    """
    if kw_per_tr <= 0:
        return 0
    return 3.516 / kw_per_tr

def get_efficiency_rating(kw_per_tr: float) -> str:
    """Get efficiency rating based on kW/TR value"""
    if kw_per_tr < 0.55:
        return "Excellent"
    elif kw_per_tr < 0.65:
        return "Good"
    elif kw_per_tr < 0.75:
        return "Fair"
    else:
        return "Poor"

def optimize_chw_setpoint(input_data: ChillerInput) -> tuple:
    """Find optimal CHW setpoint by testing different values"""
    current_sp = input_data.current_chw_setpoint_c or 8.0
    
    # Test different setpoints
    test_setpoints = [6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0]
    
    best_kw = float('inf')
    best_sp = current_sp
    
    for sp in test_setpoints:
        # Create copy with new setpoint using dict
        input_dict = input_data.dict()
        input_dict['current_chw_setpoint_c'] = sp
        test_input = ChillerInput(**input_dict)
        
        try:
            kw = predict_kw_per_tr(test_input)
            if kw < best_kw:
                best_kw = kw
                best_sp = sp
        except Exception as e:
            continue
    
    return best_sp, best_kw

# ============================================
# API ENDPOINTS
# ============================================

@app.get("/")
async def root():
    """Root endpoint with API information for large chiller plants"""
    feature_count = len(feature_names) if feature_names and isinstance(feature_names, list) else len(EXPECTED_FEATURES)
    
    return {
        "service": "York Chiller Energy Optimizer - 4 Chiller Plant Edition",
        "model_type": "Random Forest Regressor",
        "version": "2.0.0",
        "plant_size": "1000+ TR (4 centrifugal chillers)",
        "status": "online" if model is not None else "model_not_loaded",
        "model_info": {
            "loaded": model is not None,
            "features": feature_count,
            "scaler_loaded": scaler is not None,
            "numpy_version": np.__version__,
            "pandas_version": pd.__version__
        },
        "endpoints": {
            "/": "This information",
            "/health": "Health check",
            "/predict": "POST - Predict efficiency and power for 4-chiller plant",
            "/optimize": "POST - Get optimization with annual savings ($ and CO2)",
            "/plant-analysis": "POST - Detailed 4-chiller plant analysis"
        },
        "sample_4_chiller_input": {
            "total_building_load": 1200,
            "avg_chilled_water_rate": 800,
            "avg_cooling_water_temp": 28,
            "avg_outside_temp": 95,
            "avg_dew_point": 65,
            "avg_humidity": 60,
            "avg_wind_speed": 8,
            "avg_pressure": 29.9,
            "hour": 14,
            "day_of_week": 2,
            "month": 7,
            "day_of_year": 200,
            "num_chillers_operating": 4,
            "electricity_rate_usd_per_kwh": 0.12
        },
        "expected_output_sample": {
            "total_power_kw": "~700-800 kW total",
            "power_per_chiller_kw": "~175-200 kW each",
            "annual_energy_cost": "$250,000-$350,000 at $0.12/kWh"
        },
        "interpretation": {
            "kw_per_tr": "Lower is better for 1000+ TR plants: <0.55 = excellent",
            "total_power_kw": "Actual plant power for all chillers combined",
            "typical_power_for_1000tr": "500-700 kW at full load",
            "potential_savings": "50-150 kW possible through optimization = $15k-45k/year"
        }
    }

@app.get("/health")
async def health():
    """Health check endpoint"""
    feature_count = len(feature_names) if feature_names and isinstance(feature_names, list) else len(EXPECTED_FEATURES)
    
    return {
        "status": "healthy" if model is not None else "degraded",
        "model_loaded": model is not None,
        "model_type": type(model).__name__ if model else None,
        "feature_count": feature_count,
        "scaler_loaded": scaler is not None,
        "numpy_version": np.__version__,
        "pandas_version": pd.__version__
    }

@app.post("/predict", response_model=PredictionResponse)
async def predict_endpoint(input_data: ChillerInput):
    """Predict efficiency and power for 4-chiller plant (1000+ TR)"""
    try:
        if model is None:
            raise HTTPException(status_code=503, detail="Model not loaded - check logs")
        
        # Get predicted kW/TR
        kw_per_tr = predict_kw_per_tr(input_data)
        
        # Calculate total plant power
        total_power_kw = calculate_total_power(kw_per_tr, input_data.total_building_load)
        
        # Get number of chillers (default 4)
        num_chillers = input_data.num_chillers_operating or 4
        
        # Per-chiller metrics
        power_per_chiller = total_power_kw / num_chillers
        load_per_chiller = input_data.total_building_load / num_chillers
        
        # Calculate COP
        cop = calculate_cop_from_kw_per_tr(kw_per_tr)
        
        # Get efficiency rating
        rating = get_efficiency_rating(kw_per_tr)
        
        # Calculate annual cost estimate
        electricity_rate = input_data.electricity_rate_usd_per_kwh or 0.12
        annual_cost = calculate_annual_energy_cost(
            kw_per_tr, 
            input_data.total_building_load,
            operating_hours=3000,
            electricity_rate=electricity_rate
        )
        
        # Plant summary
        plant_summary = {
            "num_chillers": num_chillers,
            "total_capacity_tr": round(input_data.total_building_load, 0),
            "load_per_chiller_tr": round(load_per_chiller, 1),
            "power_density_kw_per_100tr": round(kw_per_tr * 100, 2),
            "chiller_type": "Centrifugal (1000+ TR scale)"
        }
        
        feature_count = len(feature_names) if feature_names and isinstance(feature_names, list) else len(EXPECTED_FEATURES)
        
        return PredictionResponse(
            status="success",
            kw_per_tr=round(kw_per_tr, 4),
            total_power_kw=round(total_power_kw, 1),
            power_per_chiller_kw=round(power_per_chiller, 1),
            cooling_load_tr=round(input_data.total_building_load, 1),
            load_per_chiller_tr=round(load_per_chiller, 1),
            cop=round(cop, 2),
            efficiency_rating=rating,
            plant_summary=plant_summary,
            annual_cost_estimate=annual_cost,
            features_used=feature_count,
            model_type="RandomForestRegressor",
            timestamp=datetime.now().isoformat()
        )
        
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/optimize", response_model=OptimizeResponse)
async def optimize_endpoint(input_data: ChillerInput):
    """Get optimization recommendations for large chiller plant"""
    try:
        if model is None:
            raise HTTPException(status_code=503, detail="Model not loaded - check logs")
        
        num_chillers = input_data.num_chillers_operating or 4
        electricity_rate = input_data.electricity_rate_usd_per_kwh or 0.12
        
        # Current efficiency and power
        current_kw_per_tr = predict_kw_per_tr(input_data)
        current_power_kw = calculate_total_power(current_kw_per_tr, input_data.total_building_load)
        
        # Find optimal setpoint
        optimal_sp, optimal_kw_per_tr = optimize_chw_setpoint(input_data)
        optimal_power_kw = calculate_total_power(optimal_kw_per_tr, input_data.total_building_load)
        
        # Calculate savings
        savings_pct = ((current_kw_per_tr - optimal_kw_per_tr) / current_kw_per_tr) * 100 if current_kw_per_tr > 0 else 0
        savings_pct = max(0, savings_pct)
        
        # Power savings
        power_savings_kw = current_power_kw - optimal_power_kw
        power_savings_pct = (power_savings_kw / current_power_kw) * 100 if current_power_kw > 0 else 0
        
        # Annual savings
        annual_operating_hours = 3000
        annual_savings_kwh = power_savings_kw * annual_operating_hours
        annual_savings_usd = annual_savings_kwh * electricity_rate
        
        # CO2 reduction (assuming 0.4 kg CO2 per kWh - US average)
        co2_reduction_kg = annual_savings_kwh * 0.4
        
        # Build recommendations for 4-chiller plant
        recommendations = []
        
        # Setpoint optimization
        current_sp = input_data.current_chw_setpoint_c or 8.0
        if optimal_sp != current_sp and savings_pct > 1:
            recommendations.append(OptimizationRecommendation(
                action="CHW Setpoint Optimization",
                current_value=f"{current_sp:.1f}°C",
                recommended_value=f"{optimal_sp:.1f}°C",
                expected_savings=f"{savings_pct:.1f}% ({power_savings_kw:.0f} kW, ${annual_savings_usd:,.0f}/year)",
                priority="HIGH" if savings_pct > 5 else "MEDIUM",
                operator_action=f"Raise CHW setpoint to {optimal_sp:.1f}°C across all {num_chillers} chillers"
            ))
        
        # Chiller sequencing for large plants
        load_per_chiller = input_data.total_building_load / num_chillers
        
        if input_data.total_building_load < 800:
            recommendations.append(OptimizationRecommendation(
                action="Chiller Sequencing",
                current_value=f"{num_chillers} chillers operating",
                recommended_value="3 chillers (or less)",
                expected_savings="20-30% power reduction at low load",
                priority="HIGH",
                operator_action=f"Sequentially shut down one chiller, adjust load on remaining {num_chillers-1} chillers"
            ))
        elif input_data.total_building_load > 1800:
            recommendations.append(OptimizationRecommendation(
                action="Load Balancing",
                current_value=f"{load_per_chiller:.0f} TR per chiller",
                recommended_value="Verify all chillers are contributing equally",
                expected_savings="5-10% efficiency improvement",
                priority="MEDIUM",
                operator_action="Check operating logs for load sharing between chillers"
            ))
        
        # Condenser water temperature optimization
        if input_data.avg_cooling_water_temp < 24:
            recommendations.append(OptimizationRecommendation(
                action="Condenser Water Reset",
                current_value=f"{input_data.avg_cooling_water_temp:.1f}°C",
                recommended_value="Allow cooling tower to float higher",
                expected_savings="3-8% pump energy reduction",
                priority="MEDIUM",
                operator_action="Reduce cooling tower fan speed or disable some cells"
            ))
        
        # Free cooling recommendation
        if input_data.avg_outside_temp < 50 and input_data.avg_humidity < 60:
            estimated_savings_kw = current_power_kw * 0.35
            estimated_savings_usd = estimated_savings_kw * annual_operating_hours * electricity_rate
            recommendations.append(OptimizationRecommendation(
                action="Free Cooling Mode",
                current_value="Mechanical cooling only",
                recommended_value="Enable waterside economizer",
                expected_savings=f"35% (approx {estimated_savings_kw:.0f} kW, ${estimated_savings_usd:,.0f}/year)",
                priority="HIGH",
                operator_action="Open bypass valve for cooling tower to provide directly chilled water"
            ))
        
        # Efficiency rating
        rating = get_efficiency_rating(current_kw_per_tr)
        current_cop = calculate_cop_from_kw_per_tr(current_kw_per_tr)
        optimal_cop = calculate_cop_from_kw_per_tr(optimal_kw_per_tr)
        
        summary = {
            "plant_summary": f"{num_chillers} chillers, {input_data.total_building_load:.0f} TR total",
            "current_efficiency": f"{current_kw_per_tr:.3f} kW/TR (COP: {current_cop:.2f})",
            "current_power": f"{current_power_kw:.0f} kW ({current_power_kw/num_chillers:.0f} kW per chiller)",
            "optimal_efficiency": f"{optimal_kw_per_tr:.3f} kW/TR (COP: {optimal_cop:.2f})",
            "optimal_power": f"{optimal_power_kw:.0f} kW",
            "power_savings": f"{power_savings_kw:.0f} kW ({savings_pct:.1f}%)",
            "annual_savings_usd": f"${annual_savings_usd:,.0f}",
            "co2_reduction": f"{co2_reduction_kg/1000:.1f} metric tons CO2/year",
            "efficiency_rating": rating,
            "load_per_chiller": f"{load_per_chiller:.0f} TR",
            "recommended_setpoint": f"{optimal_sp:.1f}°C",
            "electricity_rate": f"${electricity_rate:.2f}/kWh"
        }
        
        return OptimizeResponse(
            timestamp=datetime.now().isoformat(),
            current_kw_per_tr=round(current_kw_per_tr, 4),
            current_total_power_kw=round(current_power_kw, 1),
            optimal_kw_per_tr=round(optimal_kw_per_tr, 4),
            optimal_total_power_kw=round(optimal_power_kw, 1),
            efficiency_improvement_pct=round(savings_pct, 2),
            power_savings_kw=round(power_savings_kw, 1),
            power_savings_pct=round(power_savings_pct, 2),
            annual_savings_usd=round(annual_savings_usd, 0),
            co2_reduction_kg_per_year=round(co2_reduction_kg, 0),
            recommendations=recommendations,
            summary=summary
        )
        
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/plant-analysis")
async def plant_analysis(input_data: ChillerInput):
    """Detailed analysis for 4-chiller plant"""
    try:
        if model is None:
            raise HTTPException(status_code=503, detail="Model not loaded")
        
        kw_per_tr = predict_kw_per_tr(input_data)
        num_chillers = input_data.num_chillers_operating or 4
        
        # Calculate various metrics
        total_power = kw_per_tr * input_data.total_building_load
        power_per_chiller = total_power / num_chillers
        
        # Compare to industry benchmarks
        benchmarks = {
            "excellent": 0.50,
            "good": 0.60,
            "average": 0.65,
            "poor": 0.75
        }
        
        # Energy cost at different loads
        load_levels = [50, 75, 100]
        cost_analysis = []
        for load_pct in load_levels:
            load_tr = input_data.total_building_load * load_pct / 100
            power_kw = kw_per_tr * load_tr
            cost_analysis.append({
                "load_pct": load_pct,
                "load_tr": round(load_tr, 0),
                "power_kw": round(power_kw, 0),
                "power_per_chiller_kw": round(power_kw / num_chillers, 0)
            })
        
        return {
            "timestamp": datetime.now().isoformat(),
            "plant_configuration": {
                "num_chillers": num_chillers,
                "total_capacity_tr": input_data.total_building_load,
                "current_load_tr": input_data.total_building_load,
                "load_factor": "100%"
            },
            "performance_metrics": {
                "kw_per_tr": round(kw_per_tr, 3),
                "total_power_kw": round(total_power, 0),
                "power_per_chiller_kw": round(power_per_chiller, 0),
                "cop": round(calculate_cop_from_kw_per_tr(kw_per_tr), 2),
                "vs_benchmark_excellent": f"{((kw_per_tr - benchmarks['excellent'])/benchmarks['excellent']*100):+.1f}%"
            },
            "cost_analysis": cost_analysis,
            "recommendations": [
                f"Target kW/TR < 0.60 for this plant size (currently {kw_per_tr:.3f})",
                f"At full load, each chiller draws ~{power_per_chiller:.0f} kW",
                "Review if all 4 chillers are needed at current load"
            ]
        }
        
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=7860)