DevNumb's picture
Update app.py
2732f53 verified
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel
from typing import List
import numpy as np
import os
import joblib
from datetime import datetime
app = FastAPI(title="York Chiller Energy Optimizer")
# ============================================
# LOAD YOUR TRAINED MODEL (if exists)
# ============================================
model = None
feature_cols = None
MODEL_READY = False
try:
if os.path.exists("retrained_best_model.pkl"):
model = joblib.load("retrained_best_model.pkl")
feature_cols = joblib.load("retrained_feature_cols.pkl")
MODEL_READY = True
print(f"✅ Model loaded: {type(model).__name__}")
print(f"✅ Features: {len(feature_cols)}")
else:
print("⚠️ Model files not found, using fallback mode")
except Exception as e:
print(f"⚠️ Could not load model: {e}")
# ============================================
# FEATURE ENGINEERING (if model exists)
# ============================================
def create_features(hour: int, minute: int, day_of_week: int, month: int, recent_power: List[float]) -> dict:
"""Create features for model prediction"""
features = {}
# Time features
features['hour'] = hour
features['minute'] = minute
features['dayofweek'] = day_of_week
features['day'] = 1
features['month'] = month
features['year'] = 2025
# Cyclical features
features['sin_hour'] = np.sin(2 * np.pi * hour / 24)
features['cos_hour'] = np.cos(2 * np.pi * hour / 24)
features['sin_month'] = np.sin(2 * np.pi * month / 12)
features['cos_month'] = np.cos(2 * np.pi * month / 12)
# Binary features
features['is_weekend'] = 1 if day_of_week >= 5 else 0
features['is_business_hours'] = 1 if 8 <= hour <= 18 else 0
# Pad power history if needed
if len(recent_power) < 150:
last_val = recent_power[-1] if recent_power else 50
recent_power = recent_power + [last_val] * (150 - len(recent_power))
# Lag features
lags = [1, 2, 3, 6, 12, 24, 48, 96, 144]
for lag in lags:
if len(recent_power) > lag:
features[f'power_lag_{lag}'] = float(recent_power[-lag])
else:
features[f'power_lag_{lag}'] = float(recent_power[-1]) if recent_power else 50.0
# Rolling statistics
windows = [6, 12, 24, 48, 96, 144]
for window in windows:
if len(recent_power) >= window:
window_data = recent_power[-window:]
features[f'power_rolling_mean_{window}'] = float(np.mean(window_data))
features[f'power_rolling_std_{window}'] = float(np.std(window_data))
features[f'power_rolling_min_{window}'] = float(np.min(window_data))
features[f'power_rolling_max_{window}'] = float(np.max(window_data))
else:
default = float(recent_power[-1]) if recent_power else 50.0
features[f'power_rolling_mean_{window}'] = default
features[f'power_rolling_std_{window}'] = 10.0
features[f'power_rolling_min_{window}'] = default * 0.8
features[f'power_rolling_max_{window}'] = default * 1.2
# Difference features
if len(recent_power) >= 2:
features['power_change_1min'] = float(recent_power[-1] - recent_power[-2])
features['power_change_5min'] = float(recent_power[-1] - recent_power[-6]) if len(recent_power) >= 6 else 0
features['power_change_15min'] = float(recent_power[-1] - recent_power[-16]) if len(recent_power) >= 16 else 0
else:
features['power_change_1min'] = 0.0
features['power_change_5min'] = 0.0
features['power_change_15min'] = 0.0
features['power_acceleration'] = 0.0
features['power_ratio_to_mean_24'] = 1.0
return features
# ============================================
# PREDICTION FUNCTION
# ============================================
def predict_power(hour: int, minute: int, day_of_week: int, month: int, recent_power: List[float]) -> float:
"""Predict power using model or fallback"""
if MODEL_READY and feature_cols:
try:
features = create_features(hour, minute, day_of_week, month, recent_power)
input_array = []
for feat in feature_cols:
input_array.append(features.get(feat, 0.0))
X_input = np.array(input_array).reshape(1, -1)
prediction = float(model.predict(X_input)[0])
return max(0, min(500, prediction))
except Exception as e:
print(f"Model prediction error: {e}")
# Fallback prediction
if 8 <= hour <= 18:
base = 120 + 30 * np.sin((hour - 12) * np.pi / 12)
elif hour < 6 or hour > 22:
base = 30
else:
base = 70
if len(recent_power) >= 6:
trend = np.mean(recent_power[-3:]) - np.mean(recent_power[-6:-3])
base += trend * 0.3
return max(10, min(400, base))
# ============================================
# OPTIMIZATION RECOMMENDATIONS
# ============================================
def get_recommendations(predicted_power: float, hour: int, day_of_week: int) -> List[dict]:
recommendations = []
is_weekend = day_of_week >= 5
# Chiller staging
if predicted_power < 45:
recommendations.append({
"variable": "Chiller Staging",
"current": "2+ chillers running",
"recommended": "🔴 TURN OFF all chillers",
"saving_kw": round(predicted_power * 0.3, 1),
"priority": "HIGH"
})
elif predicted_power < 120:
recommendations.append({
"variable": "Chiller Staging",
"current": "2+ chillers running",
"recommended": "🟢 RUN 1 chiller",
"saving_kw": round(predicted_power * 0.12, 1),
"priority": "HIGH"
})
elif predicted_power < 250:
recommendations.append({
"variable": "Chiller Staging",
"current": "1 chiller (overloaded)",
"recommended": "🟡 RUN 2 chillers",
"saving_kw": round(predicted_power * 0.08, 1),
"priority": "MEDIUM"
})
else:
recommendations.append({
"variable": "Chiller Staging",
"current": "2 chillers (overloaded)",
"recommended": "🟡 RUN 3+ chillers",
"saving_kw": round(predicted_power * 0.05, 1),
"priority": "MEDIUM"
})
# Pump speed (VFD)
optimal_speed = 35 + (predicted_power / 300) * 50
optimal_speed = min(90, max(30, optimal_speed))
if optimal_speed < 60:
action = f"🔵 REDUCE to {optimal_speed:.0f}% speed"
saving = 8
else:
action = f"🔵 INCREASE to {optimal_speed:.0f}% speed"
saving = 5
recommendations.append({
"variable": "Chilled Water Pump (VFD)",
"current": "85% speed",
"recommended": action,
"saving_kw": saving,
"priority": "HIGH" if abs(optimal_speed - 85) > 20 else "MEDIUM"
})
# Cooling tower fans
if predicted_power < 50:
recommendations.append({
"variable": "Cooling Tower Fans",
"current": "100% speed",
"recommended": "🔴 TURN OFF",
"saving_kw": 12,
"priority": "HIGH"
})
elif predicted_power < 120:
recommendations.append({
"variable": "Cooling Tower Fans",
"current": "100% speed",
"recommended": "🟡 RUN at 40% speed",
"saving_kw": 7,
"priority": "MEDIUM"
})
else:
recommendations.append({
"variable": "Cooling Tower Fans",
"current": "Fixed speed",
"recommended": "🟡 RUN at 65-85% speed",
"saving_kw": 4,
"priority": "MEDIUM"
})
# Schedule optimization
if (hour < 6 or hour > 22 or is_weekend) and predicted_power < 60:
recommendations.append({
"variable": "Equipment Schedule",
"current": "Running",
"recommended": "🔴 CONSIDER SHUTDOWN",
"saving_kw": round(predicted_power, 1),
"priority": "HIGH"
})
return recommendations
# ============================================
# API REQUEST MODEL
# ============================================
class PredictionRequest(BaseModel):
hour: int
minute: int
day_of_week: int
month: int
recent_power: List[float]
# ============================================
# EMBEDDED HTML DASHBOARD
# ============================================
HTML_DASHBOARD = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>York Chiller Energy Optimizer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
min-height: 100vh;
padding: 20px;
}
.container { max-width: 1400px; margin: 0 auto; }
/* Header */
.header { text-align: center; margin-bottom: 30px; }
.header h1 { color: white; font-size: 2.5rem; margin-bottom: 10px; }
.header h1 span { color: #00d4ff; }
.header p { color: rgba(255,255,255,0.7); font-size: 1.1rem; }
/* Status Bar */
.status-bar {
background: rgba(255,255,255,0.1);
border-radius: 12px;
padding: 12px 20px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.status-indicator { display: flex; align-items: center; gap: 10px; }
.status-dot {
width: 12px; height: 12px; border-radius: 50%;
background: #ff4757; display: inline-block;
}
.status-dot.online { background: #2ecc71; animation: pulse 2s infinite; }
@keyframes pulse {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
100% { opacity: 1; transform: scale(1); }
}
.model-badge {
background: rgba(0,212,255,0.2);
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
color: #00d4ff;
}
/* Cards */
.card {
background: white;
border-radius: 20px;
padding: 28px;
margin-bottom: 24px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.card h2 { color: #1a1a2e; margin-bottom: 20px; font-size: 1.5rem; }
/* Forms */
.form-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 20px;
}
.form-group { margin-bottom: 20px; }
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #4a5568;
}
.form-group input, .form-group select {
width: 100%;
padding: 12px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-size: 14px;
transition: all 0.2s;
}
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: #00d4ff;
box-shadow: 0 0 0 3px rgba(0,212,255,0.1);
}
textarea {
width: 100%;
padding: 12px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-family: 'Courier New', monospace;
resize: vertical;
}
textarea:focus { outline: none; border-color: #00d4ff; }
small { display: block; margin-top: 5px; color: #718096; font-size: 12px; }
/* Button */
.btn-predict {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, #00d4ff 0%, #0099cc 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-predict:hover { transform: translateY(-2px); box-shadow: 0 5px 20px rgba(0,212,255,0.4); }
.btn-predict:disabled { opacity: 0.6; cursor: not-allowed; }
/* Results */
.prediction-box {
text-align: center;
padding: 30px;
background: linear-gradient(135deg, #00d4ff 0%, #0099cc 100%);
border-radius: 16px;
color: white;
margin-bottom: 20px;
}
.prediction-box .value { font-size: 56px; font-weight: bold; }
.savings-box {
text-align: center;
padding: 20px;
background: #e8f5e9;
border-radius: 16px;
margin-bottom: 20px;
}
.savings-box .value { font-size: 36px; font-weight: bold; color: #2e7d32; }
/* Recommendations */
.recommendation {
padding: 16px;
margin-bottom: 12px;
border-radius: 12px;
border-left: 4px solid;
transition: all 0.2s;
}
.recommendation:hover { transform: translateX(5px); }
.recommendation.high { background: #ffebee; border-left-color: #f44336; }
.recommendation.medium { background: #fff3e0; border-left-color: #ff9800; }
.recommendation.low { background: #e8f5e9; border-left-color: #4caf50; }
.recommendation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
flex-wrap: wrap;
}
.recommendation-title { font-weight: bold; font-size: 16px; }
.recommendation-priority {
padding: 4px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.high .recommendation-priority { background: #f44336; color: white; }
.medium .recommendation-priority { background: #ff9800; color: white; }
.low .recommendation-priority { background: #4caf50; color: white; }
.recommendation-saving {
margin-top: 8px;
font-weight: bold;
color: #2e7d32;
}
/* Footer */
.footer { text-align: center; padding: 20px; color: rgba(255,255,255,0.5); font-size: 14px; }
/* Loading */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 768px) {
.form-grid { grid-template-columns: 1fr; gap: 15px; }
.header h1 { font-size: 1.8rem; }
.prediction-box .value { font-size: 40px; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>❄️ York Chiller <span>Energy Optimizer</span></h1>
<p>AI-powered prediction and optimization for York chiller systems</p>
</div>
<div class="status-bar">
<div class="status-indicator">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Checking API...</span>
</div>
<div class="model-badge" id="modelBadge">Loading...</div>
</div>
<div class="card">
<h2>Input Parameters</h2>
<div class="form-grid">
<div class="form-group">
<label> Hour (0-23)</label>
<input type="number" id="hour" min="0" max="23" value="14">
</div>
<div class="form-group">
<label> Day of Week</label>
<select id="dayofweek">
<option value="0">Monday</option><option value="1">Tuesday</option>
<option value="2" selected>Wednesday</option><option value="3">Thursday</option>
<option value="4">Friday</option><option value="5">Saturday</option>
<option value="6">Sunday</option>
</select>
</div>
<div class="form-group">
<label> Month</label>
<select id="month">
<option value="1">January</option><option value="2">February</option>
<option value="3">March</option><option value="4">April</option>
<option value="5">May</option><option value="6">June</option>
<option value="7" selected>July</option><option value="8">August</option>
<option value="9">September</option><option value="10">October</option>
<option value="11">November</option><option value="12">December</option>
</select>
</div>
</div>
<div class="form-group">
<label>⚡ Recent Power Readings (kW)</label>
<textarea id="powerHistory" rows="3" placeholder="Enter comma-separated values...">45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55</textarea>
<small>Last 10-30 readings at 1-minute intervals (comma-separated)</small>
</div>
<button class="btn-predict" onclick="makePrediction()">🔮 Predict & Optimize</button>
</div>
<div id="results" style="display: none;"></div>
<div class="footer">
<p>Powered by Random Forest AI </p>
</div>
</div>
<script>
async function checkHealth() {
try {
const response = await fetch('/health');
if (response.ok) {
const data = await response.json();
document.getElementById('statusDot').classList.add('online');
document.getElementById('statusText').textContent = 'API Online';
document.getElementById('modelBadge').innerHTML = ` ${data.model_type || 'AI Model'} | Active`;
} else {
throw new Error('API not responding');
}
} catch (error) {
document.getElementById('statusDot').classList.remove('online');
document.getElementById('statusText').textContent = 'API Offline';
document.getElementById('modelBadge').innerHTML = ' Fallback Mode';
}
}
async function makePrediction() {
const hour = parseInt(document.getElementById('hour').value);
const dayofweek = parseInt(document.getElementById('dayofweek').value);
const month = parseInt(document.getElementById('month').value);
const powerText = document.getElementById('powerHistory').value;
const recentPower = powerText.split(',').map(x => parseFloat(x.trim())).filter(x => !isNaN(x));
if (recentPower.length < 3) {
alert('Please enter at least 3 power readings');
return;
}
const btn = document.querySelector('.btn-predict');
const originalText = btn.innerHTML;
btn.innerHTML = '<div class="loading"></div> Predicting...';
btn.disabled = true;
try {
const response = await fetch('/api/predict', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
hour: hour,
minute: 0,
day_of_week: dayofweek,
month: month,
recent_power: recentPower
})
});
const data = await response.json();
if (data.success) {
displayResults(data);
} else {
alert('Error: ' + (data.error || 'Unknown error'));
}
} catch (error) {
alert('Error: ' + error.message);
} finally {
btn.innerHTML = originalText;
btn.disabled = false;
}
}
function displayResults(data) {
const resultsDiv = document.getElementById('results');
let recommendationsHtml = '';
for (const rec of data.recommendations) {
const priorityClass = rec.priority.toLowerCase();
recommendationsHtml += `
<div class="recommendation ${priorityClass}">
<div class="recommendation-header">
<span class="recommendation-title">${rec.variable}</span>
<span class="recommendation-priority">${rec.priority}</span>
</div>
<div><strong>Current:</strong> ${rec.current}</div>
<div><strong>Recommended:</strong> ${rec.recommended}</div>
<div class="recommendation-saving"> Saving: ${rec.saving_kw} kW</div>
</div>
`;
}
resultsDiv.innerHTML = `
<div class="card">
<div class="prediction-box">
<div> Predicted Power</div>
<div class="value">${data.predicted_power_kw.toFixed(1)} <span style="font-size: 20px;">kW</span></div>
</div>
<div class="savings-box">
<div> Total Potential Savings</div>
<div class="value">${data.total_saving_kw.toFixed(1)} <span style="font-size: 16px;">kW</span></div>
</div>
<h2 style="margin-top: 20px;"> Optimization Recommendations</h2>
${recommendationsHtml}
<div style="margin-top: 20px; padding: 12px; background: #f0f2f5; border-radius: 8px; font-size: 12px;">
Model: ${data.model_info.type} | ${data.model_info.loaded ? 'Active' : 'Fallback Mode'}
</div>
</div>
`;
resultsDiv.style.display = 'block';
resultsDiv.scrollIntoView({ behavior: 'smooth' });
}
// Check health on load and every 30 seconds
checkHealth();
setInterval(checkHealth, 30000);
</script>
</body>
</html>
"""
# ============================================
# FASTAPI ENDPOINTS
# ============================================
@app.get("/")
async def root():
"""Serve the dashboard"""
return HTMLResponse(content=HTML_DASHBOARD)
@app.post("/api/predict")
async def predict(request: PredictionRequest):
"""Make prediction and return JSON"""
try:
predicted_power = predict_power(
request.hour, request.minute,
request.day_of_week, request.month,
request.recent_power
)
recommendations = get_recommendations(predicted_power, request.hour, request.day_of_week)
total_saving = sum(r["saving_kw"] for r in recommendations)
return {
"success": True,
"predicted_power_kw": round(predicted_power, 1),
"total_saving_kw": round(total_saving, 1),
"recommendations": recommendations,
"model_info": {
"loaded": MODEL_READY,
"type": "Random Forest" if MODEL_READY else "Fallback Predictor",
"features": len(feature_cols) if feature_cols else 0
}
}
except Exception as e:
return {"success": False, "error": str(e)}
@app.get("/health")
async def health():
"""Health check"""
return {
"status": "healthy",
"model_type": "Random Forest" if MODEL_READY else "Fallback",
"timestamp": datetime.now().isoformat()
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860)