Skip to content

AHU Economizer Analysis

This example demonstrates a detailed review of AHU economizing conditions

Output

Show Code
import pandas as pd
import plotly.graph_objects as go
import numpy as np

# # Load CSV data
# df = pd


# Dynamically identify the relevant column names
occupancy_col = [col for col in df.columns if 'occupancystatus' in col.lower()][0]
oat_col = [col for col in df.columns if 'outdoorairtemp' in col.lower()][0]
mat_col = [col for col in df.columns if 'mixedairtemp' in col.lower()][0]
rat_col = [col for col in df.columns if 'returnairtemp' in col.lower()][0]

# Filter data for rows where occupancyStatus is 'occupied' and drop rows with NaN in key columns
occupied_data = df[df[occupancy_col].str.lower() == 'occupied']
occupied_data = occupied_data.dropna(subset=[oat_col, mat_col, rat_col])

# Convert columns to numeric
occupied_data[oat_col] = pd.to_numeric(occupied_data[oat_col], errors='coerce')
occupied_data[mat_col] = pd.to_numeric(occupied_data[mat_col], errors='coerce')
occupied_data[rat_col] = pd.to_numeric(occupied_data[rat_col], errors='coerce')

# Drop rows with any remaining NaN after conversion
occupied_data = occupied_data.dropna(subset=[oat_col, mat_col, rat_col])

# Extract min and max values for axes
oat_min = occupied_data[oat_col].min()
oat_max = occupied_data[oat_col].max()

# Calculate thresholds
supply_air_setpoint = 55
red_threshold = supply_air_setpoint - 1  # 1 degree below supply air setpoint
average_rat = occupied_data[rat_col].mean()  # Average return air temperature
green_threshold = average_rat + 3  # 3 degrees higher than average RAT

# Define the piecewise function for the ideal line
def ideal_line(oat):
    if oat <= 53:
        return 53
    elif oat <= 70:
        return 53 + (oat - 53) * 1
    else:
        return 70

# Generate data points for the ideal line
oat_range = np.linspace(oat_min, green_threshold, 500)  # Extend range to green threshold
mat_ideal = [ideal_line(oat) for oat in oat_range]

# Control bands around the ideal line
upper_band = [mat + 2 for mat in mat_ideal]
lower_band = [mat - 2 for mat in mat_ideal]

# Define the percentage of outside air for minimum operation
percent_outside_air = 0.2  # 20% outside air

# Calculate the dynamic minimum outside air line
min_outside_air = oat_range * percent_outside_air + average_rat * (1 - percent_outside_air)

# Control bands around the dynamic minimum outside air line
min_outside_air_upper_band = min_outside_air + 2
min_outside_air_lower_band = min_outside_air - 2

# Create the plot using Plotly
fig = go.Figure()

# Scatter plot of observed data
fig.add_trace(go.Scatter(
    x=occupied_data[oat_col],
    y=occupied_data[mat_col],
    mode='markers',
    marker=dict(color='black', size=6, opacity=0.6),
    name='Observed Data'
))

# Ideal line and control bands
fig.add_trace(go.Scatter(
    x=oat_range,
    y=mat_ideal,
    mode='lines',
    line=dict(color='darkblue', width=3),
    name='Ideal Economizing Line'
))
fig.add_trace(go.Scatter(
    x=oat_range,
    y=upper_band,
    mode='lines',
    line=dict(color='darkblue', dash='dot', width=2),
    name='Upper Control Band'
))
fig.add_trace(go.Scatter(
    x=oat_range,
    y=lower_band,
    mode='lines',
    line=dict(color='darkblue', dash='dot', width=2),
    name='Lower Control Band'
))

# Vertical lines with dynamic thresholds
fig.add_vline(
    x=red_threshold,
    line_color='red',
    line_dash='dash',
    line_width=2,
    annotation_text=f'Red Threshold ({red_threshold}°F)',
    annotation_position='top left'
)
fig.add_vline(
    x=green_threshold,
    line_color='green',
    line_dash='dash',
    line_width=2,
    annotation_text=f'Green Threshold ({green_threshold:.2f}°F)',
    annotation_position='top left'
)

# Diagonal 100% outside air line (MAT = OAT)
fig.add_trace(go.Scatter(
    x=[oat_min, green_threshold],
    y=[oat_min, green_threshold],
    mode='lines',
    line=dict(color='green', width=2),
    name='100% Outside Air Line (MAT = OAT)'
))

# Dynamic minimum outside air line and its control bands
fig.add_trace(go.Scatter(
    x=oat_range,
    y=min_outside_air,
    mode='lines',
    line=dict(color='red', width=2),
    name='Dynamic Minimum Outside Air Line'
))
fig.add_trace(go.Scatter(
    x=oat_range,
    y=min_outside_air_upper_band,
    mode='lines',
    line=dict(color='red', dash='dot', width=2),
    name='Upper Control Band (+2°F)'
))
fig.add_trace(go.Scatter(
    x=oat_range,
    y=min_outside_air_lower_band,
    mode='lines',
    line=dict(color='red', dash='dot', width=2),
    name='Lower Control Band (-2°F)'
))

# Final plot adjustments
fig.update_layout(
    title='AHU Economizer Analysis - Operation Chart',
    xaxis_title='Outside Air Temperature (°F)',
    yaxis_title='Mixed Air Temperature (°F)',
    legend=dict(x=0.01, y=0.99, bgcolor='rgba(255,255,255,0.7)'),
    template='plotly_white'
)

# Show the plot
fig.show()