import os
import numpy as np
import math
[docs]
class Server():
def __init__(self, full_load_pwr=None, idle_pwr=None, gpu_full_load_pwr=None, gpu_idle_pwr=None, server_config=None):
"""Server class in charge of the energy consumption and thermal calculations of the individual servers
in a Rack.
Args:
full_load_pwr (float, optional): Power at full capacity for CPU.
idle_pwr (float, optional): Power while idle for CPU.
gpu_full_load_pwr (float, optional): Power at full capacity for GPU.
gpu_idle_pwr (float, optional): Power while idle for GPU.
server_config (config): Configuration for the DC.
"""
self.server_config = server_config
# CPU power parameters
self.full_load_pwr = full_load_pwr if full_load_pwr is not None else self.server_config.HP_PROLIANT[0]
self.idle_pwr = idle_pwr if idle_pwr is not None else self.server_config.HP_PROLIANT[1]
# GPU power parameters
self.gpu_full_load_pwr = gpu_full_load_pwr if gpu_full_load_pwr is not None else self.server_config.NVIDIA_V100[1]
self.gpu_idle_pwr = gpu_idle_pwr if gpu_idle_pwr is not None else self.server_config.NVIDIA_V100[0]
self.m_cpu = None
self.c_cpu = None
self.m_itfan = None
self.c_itfan = None
self.cpu_curve1()
self.itfan_curve2()
self.v_fan = None # needed later for calculating outlet temperature
self.itfan_v_ratio_at_inlet_temp = None
self.total_DC_full_load = None
[docs]
def cpu_curve1(self,):
"""
initialize the cpu power ratio curve at different IT workload ratios as a function of inlet temperatures [3]
"""
# curve parameters at lowest ITE utilization 0%
self.m_cpu = (self.server_config.CPU_POWER_RATIO_UB[0]-self.server_config.CPU_POWER_RATIO_LB[0])/(self.server_config.INLET_TEMP_RANGE[1]-self.server_config.INLET_TEMP_RANGE[0])
self.c_cpu = self.server_config.CPU_POWER_RATIO_UB[0] - self.m_cpu*self.server_config.INLET_TEMP_RANGE[1]
# max vertical shift in power ratio curve at a given point in inlet temperature for 100% change in ITE input load pct
self.ratio_shift_max_cpu = self.server_config.CPU_POWER_RATIO_LB[1] - self.server_config.CPU_POWER_RATIO_LB[0]
[docs]
def itfan_curve2(self,):
"""
initialize the itfan velocity ratio curve at different IT workload ratios as a function of inlet temperatures [3]
"""
# curve parameters at ITE utilization 25%
self.m_itfan = (self.server_config.IT_FAN_AIRFLOW_RATIO_UB[0]-self.server_config.IT_FAN_AIRFLOW_RATIO_LB[0])/(self.server_config.INLET_TEMP_RANGE[1]-self.server_config.INLET_TEMP_RANGE[0])
self.c_itfan = self.server_config.IT_FAN_AIRFLOW_RATIO_UB[0] - self.m_itfan*self.server_config.INLET_TEMP_RANGE[1]
# max vertical shift in fan flow ratio curve at a given point for 75% change in ITE input load pct
self.ratio_shift_max_itfan = self.server_config.IT_FAN_AIRFLOW_RATIO_LB[1] - self.server_config.IT_FAN_AIRFLOW_RATIO_LB[0]
[docs]
def compute_instantaneous_cpu_pwr(self, inlet_temp, ITE_load_pct):
"""Calculate the CPU power consumption
Args:
inlet_temp (float): Inlet temperature
ITE_load_pct (float): CPU utilization
Returns:
float: CPU power consumption in Watts
"""
# Existing CPU power calculation method (assuming this was in the original code)
base_cpu_power_ratio = self.m_cpu * inlet_temp + self.c_cpu
cpu_power_ratio_at_inlet_temp = base_cpu_power_ratio + self.ratio_shift_max_cpu * (ITE_load_pct/100)
cpu_power = max(self.idle_pwr, self.full_load_pwr * cpu_power_ratio_at_inlet_temp)
return cpu_power
[docs]
def compute_instantaneous_fan_pwr(self, inlet_temp, ITE_load_pct):
"""Calculate the IT fan power consumption
Args:
inlet_temp (float): Inlet temperature
ITE_load_pct (float): IT workload percentage
Returns:
float: Fan power consumption in Watts
"""
# Existing fan power calculation method (assuming this was in the original code)
base_itfan_v_ratio = self.m_itfan * inlet_temp + self.c_itfan
itfan_v_ratio_at_inlet_temp = base_itfan_v_ratio + self.ratio_shift_max_itfan * (ITE_load_pct/100)
self.itfan_v_ratio_at_inlet_temp = itfan_v_ratio_at_inlet_temp
self.v_fan = self.server_config.IT_FAN_FULL_LOAD_V * itfan_v_ratio_at_inlet_temp
itfan_pwr = self.server_config.ITFAN_REF_P * (itfan_v_ratio_at_inlet_temp/self.server_config.ITFAN_REF_V_RATIO)
return itfan_pwr
[docs]
def compute_instantaneous_gpu_pwr(self, gpu_utilization):
"""Calculate GPU power based on utilization using the logarithmic model
Args:
gpu_utilization (float): GPU utilization percentage (0-100)
Returns:
float: GPU power consumption in Watts
"""
# Apply the formula: y = α + β × log₂(1 + x)
# where α is idle_power and β is (max_power - idle_power)
# as referenced from from [6]
alpha = self.gpu_idle_pwr
beta = self.gpu_full_load_pwr - self.gpu_idle_pwr
gpu_power = alpha + beta * math.log2(1 + gpu_utilization)
return gpu_power
[docs]
class Rack():
def __init__(self, server_config_list, gpu_config_list=None, max_W_per_rack=10000, rack_config=None):
"""Defines the rack as a collection of servers
Args:
server_config_list (list): Server configuration
gpu_config_list (list, optional): GPU configuration
max_W_per_rack (int): Maximum power allowed for a whole rack. Defaults to 10000.
rack_config (config): Rack configuration. Defaults to None.
"""
self.rack_config = rack_config
self.server_list = []
self.has_gpus = gpu_config_list is not None
self.current_rack_load = 0
for i, server_config in enumerate(server_config_list):
# Get GPU info if available
gpu_full_load_pwr = None
gpu_idle_pwr = None
if self.has_gpus and i < len(gpu_config_list):
gpu_full_load_pwr = gpu_config_list[i]['full_load_pwr']
gpu_idle_pwr = gpu_config_list[i]['idle_pwr']
# Create server object with GPU info
self.server_list.append(Server(
full_load_pwr=server_config['full_load_pwr'],
idle_pwr=server_config['idle_pwr'],
gpu_full_load_pwr=gpu_full_load_pwr,
gpu_idle_pwr=gpu_idle_pwr,
server_config=self.rack_config
))
# Track power for capacity planning
self.current_rack_load += self.server_list[-1].full_load_pwr
if self.has_gpus and i < len(gpu_config_list):
self.current_rack_load += self.server_list[-1].gpu_full_load_pwr
if self.current_rack_load >= max_W_per_rack:
self.server_list.pop()
break
self.num_servers = len(self.server_list)
self.num_gpus = self.num_servers if self.has_gpus else 0
self.server_and_fan_init()
self.v_fan_rack = None
# class Rack():
# def __init__(self, server_config_list, gpu_config_list=None, max_W_per_rack=10000, rack_config=None):
# """Defines the rack as a collection of servers with improved fan control"""
# self.rack_config = rack_config
# self.server_list = []
# self.has_gpus = gpu_config_list is not None
# self.current_rack_load = 0
# # Fan control parameters
# # start at moderate fan speed to prevent thermal runaway
# self.prev_fan_ratio_rack = 0.5
# self.smoothing_alpha = 0.1 # very slow smoothing: 10% new, 90% old # slower response: 30% new, 70% old
# self.max_delta_ratio = 0.02 # tight rate limit: 2% change per step # limit change per step
# # moderate sensitivity
# self.m_coefficient = 1 # baseline sensitivity
# self.c_coefficient = 0 # baseline offset
# self.it_slope = 20
# for i, server_config in enumerate(server_config_list):
# gpu_full_load_pwr = None
# gpu_idle_pwr = None
# if self.has_gpus and i < len(gpu_config_list):
# gpu_full_load_pwr = gpu_config_list[i]['full_load_pwr']
# gpu_idle_pwr = gpu_config_list[i]['idle_pwr']
# self.server_list.append(Server(
# full_load_pwr=server_config['full_load_pwr'],
# idle_pwr=server_config['idle_pwr'],
# gpu_full_load_pwr=gpu_full_load_pwr,
# gpu_idle_pwr=gpu_idle_pwr,
# server_config=self.rack_config
# ))
# self.current_rack_load += self.server_list[-1].full_load_pwr
# if self.has_gpus and i < len(gpu_config_list):
# self.current_rack_load += self.server_list[-1].gpu_full_load_pwr
# if self.current_rack_load >= max_W_per_rack:
# self.server_list.pop()
# break
# self.num_servers = len(self.server_list)
# self.num_gpus = self.num_servers if self.has_gpus else 0
# self.server_and_fan_init()
# self.v_fan_rack = None
[docs]
def server_and_fan_init(self,):
"""
Initialize the Server and Fan parameters for the servers in each rack with the specified data center configurations
"""
#common to both cpu and fan
inlet_temp_lb, inlet_temp_ub = [], []
# only for cpu
m_cpu, c_cpu, ratio_shift_max_cpu, idle_pwr, full_load_pwr = [], [], [], [], []
# only for it fan
m_itfan, c_itfan, ratio_shift_max_itfan, ITFAN_REF_P, ITFAN_REF_V_RATIO, IT_FAN_FULL_LOAD_V = [], [], [], [], [], []
# GPU parameters
gpu_idle_pwr, gpu_full_load_pwr = [], []
self.m_coefficient = 10 #1 -> 10
self.c_coefficient = 5 #1 -> 5
self.it_slope = 20 #100 -> 20
for server_item in self.server_list:
#common to both cpu and fan
inlet_temp_lb.append(server_item.server_config.INLET_TEMP_RANGE[0])
inlet_temp_ub.append(server_item.server_config.INLET_TEMP_RANGE[1])
# only for cpu
m_cpu.append(server_item.m_cpu)
c_cpu.append(server_item.c_cpu)
ratio_shift_max_cpu.append(server_item.ratio_shift_max_cpu)
idle_pwr.append(server_item.idle_pwr)
full_load_pwr.append(server_item.full_load_pwr)
# for GPU
if self.has_gpus:
gpu_idle_pwr.append(server_item.gpu_idle_pwr)
gpu_full_load_pwr.append(server_item.gpu_full_load_pwr)
# only for itfan
m_itfan.append(server_item.m_itfan)
c_itfan.append(server_item.c_itfan)
ratio_shift_max_itfan.append(server_item.ratio_shift_max_itfan)
ITFAN_REF_P.append(server_item.server_config.ITFAN_REF_P)
ITFAN_REF_V_RATIO.append(server_item.server_config.ITFAN_REF_V_RATIO)
IT_FAN_FULL_LOAD_V.append(server_item.server_config.IT_FAN_FULL_LOAD_V)
# common to both cpu and itfan
self.inlet_temp_lb, self.inlet_temp_ub = np.array(inlet_temp_lb), np.array(inlet_temp_ub)
# only for cpu
self.m_cpu, self.c_cpu, self.ratio_shift_max_cpu, self.idle_pwr, self.full_load_pwr = \
np.array(m_cpu), np.array(c_cpu), np.array(ratio_shift_max_cpu), np.array(idle_pwr), np.array(full_load_pwr)
# for GPU
if self.has_gpus:
self.gpu_idle_pwr, self.gpu_full_load_pwr = np.array(gpu_idle_pwr), np.array(gpu_full_load_pwr)
# only for itfan
self.m_itfan, self.c_itfan, self.ratio_shift_max_itfan, self.ITFAN_REF_P, self.ITFAN_REF_V_RATIO, self.IT_FAN_FULL_LOAD_V = \
np.array(m_itfan), np.array(c_itfan), np.array(ratio_shift_max_itfan), \
np.array(ITFAN_REF_P), np.array(ITFAN_REF_V_RATIO), np.array(IT_FAN_FULL_LOAD_V)
[docs]
def compute_instantaneous_pwr(self, inlet_temp, ITE_load_pct, GPU_load_pct=0):
"""Calculate the power consumption of the whole rack at the current step
Args:
inlet_temp (float): Room temperature
ITE_load_pct (float): Current CPU usage
GPU_load_pct (float): Current GPU usage (optional, defaults to 0)
Returns:
tuple: (cpu_power, itfan_power, gpu_power)
"""
# Server CPU power
server = self.server_list[0]
tot_cpu_pwr = server.compute_instantaneous_cpu_pwr(inlet_temp, ITE_load_pct) * self.num_servers
# GPU power calculation
tot_gpu_pwr = 0
if self.has_gpus and GPU_load_pct > 0:
for server_item in self.server_list:
tot_gpu_pwr += server_item.compute_instantaneous_gpu_pwr(GPU_load_pct)
# IT fan power calculation (consider both CPU and GPU heat)
tot_itfan_pwr = []
for server_item in self.server_list:
# Fan responds to highest thermal load between CPU and GPU
effective_load = max(ITE_load_pct, GPU_load_pct) if self.has_gpus else ITE_load_pct
tot_itfan_pwr.append(server_item.compute_instantaneous_fan_pwr(inlet_temp, effective_load))
return tot_cpu_pwr, np.array(tot_itfan_pwr).sum(), tot_gpu_pwr
[docs]
def log_system_values(self, cpu_load, mem_load, gpu_load):
"""
Simple function to append CPU, memory, and GPU load values to a CSV file.
Args:
cpu_load (float): CPU load percentage (0-100)
mem_load (float): Memory load percentage (0-100)
gpu_load (float): GPU load percentage (0-100)
csv_path (str): Path to the CSV file
"""
# Check if file exists to determine if we need to write
import datetime
import csv
file_exists = os.path.isfile("system_resources.csv")
# Open file in append mode
with open("system_resources.csv", 'a', newline='') as csvfile:
fieldnames = ['cpu_load', 'mem_load', 'gpu_load']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
# Write header if file is new
if not file_exists:
writer.writeheader()
# Write the data row
writer.writerow({
'cpu_load': cpu_load,
'mem_load': mem_load,
'gpu_load': gpu_load
})
[docs]
def log_thermal_control_data(self, inlet_temp, fan_ratio, cpu_load, gpu_load, mem_load, cpu_power, gpu_power, fan_power):
"""
Logs thermal control data to CSV for analysis and visualization.
"""
import os
import csv
import datetime
import numpy as np
# Get current timestamp
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Create file if it doesn't exist
filename = "thermal_control_data.csv"
file_exists = os.path.isfile(filename)
# If fan_ratio is an array, use the average value
if hasattr(fan_ratio, '__iter__') and not isinstance(fan_ratio, str):
fan_ratio = float(np.mean(fan_ratio))
with open(filename, 'a', newline='') as csvfile:
fieldnames = [
'timestamp', 'inlet_temp', 'fan_ratio', 'cpu_load', 'gpu_load', 'mem_load',
'cpu_power', 'gpu_power', 'fan_power', 'has_gpus'
]
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
# Write headers if file is new
if not file_exists:
writer.writeheader()
# Write data row
writer.writerow({
'timestamp': timestamp,
'inlet_temp': inlet_temp,
'fan_ratio': fan_ratio,
'cpu_load': cpu_load,
'gpu_load': gpu_load,
'mem_load': mem_load,
'cpu_power': cpu_power,
'gpu_power': gpu_power,
'fan_power': fan_power,
'has_gpus': 1 if self.has_gpus else 0
})
[docs]
def compute_instantaneous_pwr_vecd(self, inlet_temp, ITE_load_pct, GPU_load_pct=0, MEMORY_load_pct=0):
"""Calculate the power consumption of the whole rack at the current step in a vectorized manner
Args:
inlet_temp (float): Room temperature
ITE_load_pct (float): Current CPU usage
GPU_load_pct (float): Current GPU usage (optional, defaults to 0)
mem_load_pct (float): Current memory usage (optional, defaults to 0)
Returns:
tuple: (cpu_power, itfan_power, gpu_power)
"""
# CPU power calculation
base_cpu_power_ratio = (self.m_cpu+0.05)*inlet_temp + self.c_cpu
cpu_power_ratio_at_inlet_temp = base_cpu_power_ratio + self.ratio_shift_max_cpu*(ITE_load_pct/100)
temp_arr = np.concatenate((self.idle_pwr.reshape(1,-1),
(self.full_load_pwr*cpu_power_ratio_at_inlet_temp).reshape(1,-1)),
axis=0)
cpu_power = np.max(temp_arr, axis=0)
# GPU power calculation
gpu_power = np.zeros_like(cpu_power) if self.has_gpus else np.zeros(1)
if self.has_gpus and GPU_load_pct >= 0:
# Normalize utilization for log formula
x = GPU_load_pct / 100.0
# Vectorized calculation of the logarithmic formula
alpha = self.gpu_idle_pwr
beta = self.gpu_full_load_pwr - self.gpu_idle_pwr
gpu_power = alpha + beta * np.log2(1 + x)
# IT fan power calculation - respond to the highest heat load
# Add heats generated
effective_load = (ITE_load_pct + GPU_load_pct + MEMORY_load_pct)/3 if self.has_gpus else (ITE_load_pct + MEMORY_load_pct)/2
base_itfan_v_ratio = self.m_itfan*self.m_coefficient*inlet_temp + self.c_itfan*self.c_coefficient
# Calculate fan ratio at current inlet temperature
itfan_v_ratio_at_inlet_temp = base_itfan_v_ratio + self.ratio_shift_max_itfan*(effective_load/self.it_slope)
# Calculate fan power based on ratio
itfan_pwr = self.ITFAN_REF_P * (itfan_v_ratio_at_inlet_temp/self.ITFAN_REF_V_RATIO)
# Set fan rack velocity for later use
self.v_fan_rack = self.IT_FAN_FULL_LOAD_V*itfan_v_ratio_at_inlet_temp
# Log fan speed ratio and other relevant values
# self.log_thermal_control_data(inlet_temp, itfan_v_ratio_at_inlet_temp, ITE_load_pct, GPU_load_pct, MEMORY_load_pct,
# np.sum(cpu_power), np.sum(gpu_power), np.sum(itfan_pwr))
return np.sum(cpu_power), np.sum(itfan_pwr), np.sum(gpu_power)
[docs]
def get_average_rack_fan_v(self,):
"""Calculate the average fan velocity for each rack
Returns:
(float): Average fan flow rate for the rack
"""
return self.v_fan_rack[0]
[docs]
def get_total_rack_fan_v(self,):
"""Calculate the total fan velocity for each rack
Returns:
(float): Total fan flow rate for the rack
"""
return np.sum(self.v_fan_rack)
[docs]
def get_current_rack_load(self,):
"""Returns the total power consumption of the rack
Returns:
float: Total power consumption of the rack
"""
return self.current_rack_load
[docs]
def clamp_supply_approach_temp(self,supply_approach_temperature):
"""Returns the clamped delta/ supply approach temperature between the range [3.8, 5.3]
Returns:
float: Supply approach temperature
"""
return max(3.8, min(supply_approach_temperature, 5.3))
[docs]
class DataCenter_ITModel():
def __init__(self, num_racks, dc_memory_GB, rack_supply_approach_temp_list, rack_CPU_config, rack_GPU_config=None, max_W_per_rack=10000, DC_ITModel_config=None, chiller_sizing=False):
"""Creates the DC from a giving DC configuration
Args:
num_racks (int): Number of racks in the DC
rack_supply_approach_temp_list (list[float]): models the supply approach temperature for each rack based on geometry and estimated from CFD
rack_CPU_config (list[list[dict]]): A list of lists where each list is associated with a rack.
It is a list of dictionaries with their full load and idle load values in W
rack_GPU_config (list[list[dict]], optional): Similar structure as rack_CPU_config but for GPUs
max_W_per_rack (int): Maximum power allowed for a whole rack. Defaults to 10000.
DC_ITModel_config (config): Data center configuration. Defaults to None.
chiller_sizing (bool): Whether to perform Chiller Power Sizing. Defaults to False.
"""
self.DC_ITModel_config = DC_ITModel_config
self.racks_list = []
self.rack_supply_approach_temp_list = rack_supply_approach_temp_list
self.rack_CPU_config = rack_CPU_config
self.rack_GPU_config = rack_GPU_config
self.dc_memory_GB = dc_memory_GB
self.has_gpus = rack_GPU_config is not None
self.rackwise_inlet_temp = []
for i in range(num_racks):
if self.has_gpus and i < len(self.rack_GPU_config):
self.racks_list.append(Rack(
self.rack_CPU_config[i],
self.rack_GPU_config[i],
max_W_per_rack=max_W_per_rack,
rack_config=self.DC_ITModel_config
))
else:
self.racks_list.append(Rack(
self.rack_CPU_config[i],
max_W_per_rack=max_W_per_rack,
rack_config=self.DC_ITModel_config
))
self.total_datacenter_full_load()
self.tower_flow_rate = np.round(DC_ITModel_config.CT_WATER_FLOW_RATE * 3600, 4) # m³/hr
self.hot_water_temp = None # °C
self.cold_water_temp = None # °C
self.wet_bulb_temp = None # °C
self.cycles_of_concentration = 5
self.drift_rate = 0.01
[docs]
def compute_datacenter_IT_load_outlet_temp(self, ITE_load_pct_list, CRAC_setpoint, GPU_load_pct_list=None, MEMORY_load_pct_list=None):
"""Optimized power and thermal model assuming all racks are identical and utilization is uniform."""
num_racks = len(self.racks_list)
# === Validate that all utilization values are equal ===
assert all(x == ITE_load_pct_list[0] for x in ITE_load_pct_list), "All CPU utilizations must be identical"
assert all(x == MEMORY_load_pct_list[0] for x in MEMORY_load_pct_list), "All MEMORY utilizations must be identical"
if self.has_gpus:
assert GPU_load_pct_list is not None, "GPU load list must be provided when GPUs are present"
assert all(x == GPU_load_pct_list[0] for x in GPU_load_pct_list), "All GPU utilizations must be identical"
# === Use the first rack as reference ===
rack = self.racks_list[0]
rack_supply_approach_temp = rack.clamp_supply_approach_temp(self.rack_supply_approach_temp_list[0])
rack_inlet_temp = rack_supply_approach_temp + CRAC_setpoint
cpu_util = ITE_load_pct_list[0]
mem_util = MEMORY_load_pct_list[0]
gpu_util = GPU_load_pct_list[0] if self.has_gpus else 0
rack_cpu_power, rack_itfan_power, rack_gpu_power = rack.compute_instantaneous_pwr_vecd(
inlet_temp=rack_inlet_temp,
ITE_load_pct=cpu_util,
GPU_load_pct=gpu_util,
MEMORY_load_pct=mem_util
)
background_memory_power = 0.07 * self.dc_memory_GB / num_racks # assume uniform per-rack memory power
total_power_per_rack = rack_cpu_power + rack_itfan_power + rack_gpu_power + background_memory_power
# === Thermal model constants ===
c = 1.918
d = 1.096
e = 0.824
f = 0.526
g = -14.01
fan_v = rack.get_total_rack_fan_v()
power_term = total_power_per_rack ** d
airflow_term = self.DC_ITModel_config.C_AIR * self.DC_ITModel_config.RHO_AIR * fan_v ** e * f
outlet_temp = rack_inlet_temp + c * power_term / airflow_term + g
if outlet_temp > 80:
print(f'[WARNING] High outlet temp: {outlet_temp:.2f}C')
if outlet_temp - rack_inlet_temp < 2:
print(f'[ERROR] Delta-T too small: {outlet_temp - rack_inlet_temp:.2f}')
raise Exception("Temperature rise is unreasonably low.")
# === Scale values across all racks ===
rackwise_cpu_pwr = [rack_cpu_power] * num_racks
rackwise_itfan_pwr = [rack_itfan_power] * num_racks
rackwise_gpu_pwr = [rack_gpu_power] * num_racks
rackwise_memory_pwr = [background_memory_power] * num_racks
rackwise_outlet_temp = [outlet_temp] * num_racks
self.rackwise_inlet_temp = [rack_inlet_temp] * num_racks
return rackwise_cpu_pwr, rackwise_itfan_pwr, rackwise_memory_pwr, rackwise_gpu_pwr, rackwise_outlet_temp
[docs]
def total_datacenter_full_load(self,):
"""Calculate the total DC IT power consumption (CPU, GPU, and fan)
"""
total_power = sum(rack.get_current_rack_load() for rack in self.racks_list)
self.total_DC_full_load = total_power
[docs]
def calculate_cooling_tower_water_usage(self):
"""
Calculate the estimated water usage of the cooling tower.
This function uses the attributes set in the class to estimate the water usage based
[Sharma, R.K., Shah, A., Bash, C.E., Christian, T., & Patel, C.D. (2009). Water efficiency management in datacenters: Metrics and methodology. 2009 IEEE International Symposium on Sustainable Systems and Technology, 1-6.]
[Mohammed Shublaq, Ahmad K. Sleiti., (2020). Experimental analysis of water evaporation losses in cooling towers using filters]
https://spxcooling.com/water-calculator/
"""
# We're assuming m³/hr, which is standard
# Calculate the range (difference between hot and cold water temperature)
range_temp = self.hot_water_temp - self.cold_water_temp
y_intercept = 0.3528 * range_temp + 0.101
# The water usage estimation formula would need to be derived from the graph you provided.
norm_water_usage = 0.044 * self.wet_bulb_temp + y_intercept
water_usage = np.clip(norm_water_usage, 0, None)
water_usage += water_usage * self.drift_rate # adjust for drift
# Convert m³/hr to the desired unit (e.g., liters per 15 minutes) if necessary
# There are 1000 liters in a cubic meter. There are 4 15-minute intervals in an hour.
water_usage_liters_per_15min = np.round((water_usage * 1000) / 4, 4)
return water_usage_liters_per_15min
[docs]
def calculate_chiller_power(max_cooling_cap, load, ambient_temp):
"""
Calculate the chiller power consumption based on load and operating conditions.
Obtained from:
1) https://github.com/NREL/EnergyPlus/blob/9bb39b77a871dee7543c892ae53b0812c4c17b0d/testfiles/AirCooledElectricChiller.idf
2) https://github.com/NREL/EnergyPlus/issues/763
3) https://dmey.github.io/EnergyPlusFortran-Reference/proc/calcelectricchillermodel.html
4) https://github.com/NREL/EnergyPlus/blob/9bb39b77a871dee7543c892ae53b0812c4c17b0d/tst/EnergyPlus/unit/ChillerElectric.unit.cc#L95
Args:
max_cooling_cap (float): Maximum cooling capacity of the chiller (Watts).
load (float): The heat load to be removed by the chiller (Watts).
ambient_temp (float): Current ambient temperature (Celsius).
Returns:
float: Estimated power consumption of the chiller (Watts).
"""
# Coefficients from https://github.com/NREL/EnergyPlus/blob/9bb39b77a871dee7543c892ae53b0812c4c17b0d/tst/EnergyPlus/unit/ChillerElectric.unit.cc#L95
capacity_coefficients = [0.94483600, -0.05700880, 0.00185486]
power_coefficients = [2.333, -1.975, 0.6121]
full_load_factor = [0.03303, 0.6852, 0.2818]
# Chiller design specifications
min_plr = 0.05
max_plr = 1.0
design_cond_temp = 35.0
design_evp_out_temp = 6.67
chiller_nominal_cap = max_cooling_cap
temp_rise_coef = 2.778
rated_cop = 3.0
# Calculate the delta temperature for capacity adjustment
delta_temp = (ambient_temp - design_cond_temp)/temp_rise_coef - (design_evp_out_temp - design_cond_temp)
# Calculate available nominal capacity ratio
avail_nom_cap_rat = capacity_coefficients[0] + capacity_coefficients[1] * delta_temp + capacity_coefficients[2] * delta_temp**2
# Calculate available chiller capacity
available_capacity = chiller_nominal_cap * avail_nom_cap_rat if avail_nom_cap_rat != 0 else 0
# Calculate power ratio
full_power_ratio = power_coefficients[0] + power_coefficients[1] * avail_nom_cap_rat + power_coefficients[2] * avail_nom_cap_rat**2
# Determine part load ratio (PLR)
part_load_ratio = max(min_plr, min(load / available_capacity, max_plr)) if available_capacity > 0 else 0
# Calculate fractional full load power
frac_full_load_power = full_load_factor[0] + full_load_factor[1] * part_load_ratio + full_load_factor[2] * part_load_ratio**2
# Determine operational part load ratio (OperPartLoadRat)
# If the PLR is less than Min PLR calculate the actual PLR for calculations. The power will then adjust for the cycling.
if available_capacity > 0:
if load / available_capacity < min_plr:
oper_part_load_rat = load / available_capacity
else:
oper_part_load_rat = part_load_ratio
else:
oper_part_load_rat = 0.0
# Operational PLR for actual conditions
if oper_part_load_rat < min_plr:
frac = min(1.0, oper_part_load_rat / min_plr)
else:
frac = 1.0
# Calculate the chiller compressor power
power = frac_full_load_power * full_power_ratio * available_capacity / rated_cop * frac
# Total heat rejection is the sum of the cooling capacity and the power input
total_heat_rejection = load + power
return power if oper_part_load_rat > 0 else 0
[docs]
def calculate_HVAC_power(CRAC_setpoint, avg_CRAC_return_temp, ambient_temp, data_center_full_load, DC_Config, ctafr=None):
"""Calculate the HVAC power attributes
Args:
CRAC_Setpoint (float): The control action
avg_CRAC_return_temp (float): The average of the temperatures from all the Racks + their corresponding return approach temperature (Delta)
ambient_temp (float): outside air temperature
data_center_full_load (float): total data center capacity
Returns:
CRAC_Fan_load (float): CRAC fan power
CT_Fan_pwr (float): Cooling tower fan power
CRAC_cooling_load (float): CRAC cooling load
Compressor_load (float): Chiller compressor load
"""
# Air system calculations
m_sys = DC_Config.RHO_AIR * DC_Config.CRAC_SUPPLY_AIR_FLOW_RATE_pu * data_center_full_load
CRAC_cooling_load = m_sys * DC_Config.C_AIR * max(0.0, avg_CRAC_return_temp - CRAC_setpoint) # coo.Q_thistime
CRAC_Fan_load = DC_Config.CRAC_FAN_REF_P * (DC_Config.CRAC_SUPPLY_AIR_FLOW_RATE_pu / DC_Config.CRAC_REFRENCE_AIR_FLOW_RATE_pu)**3
# CRAC_cooling_load_reduction = min(heat_recovery(data_center_full_load, ambient_temp, DC_Config), 0.25* data_center_full_load)
# CRAC_cooling_load = CRAC_cooling_load - CRAC_cooling_load_reduction
chiller_power = calculate_chiller_power(DC_Config.CT_FAN_REF_P, CRAC_cooling_load, ambient_temp)
# Chiller power calculation
power_consumed_CW = (DC_Config.CW_PRESSURE_DROP * DC_Config.CW_WATER_FLOW_RATE) / DC_Config.CW_PUMP_EFFICIENCY
# Chilled water pump power calculation
power_consumed_CT = (DC_Config.CT_PRESSURE_DROP*DC_Config.CT_WATER_FLOW_RATE)/DC_Config.CT_PUMP_EFFICIENCY
if ambient_temp < 5:
return CRAC_Fan_load, 0.0, CRAC_cooling_load, chiller_power, power_consumed_CW, power_consumed_CT
# Cooling tower fan power calculations
Cooling_tower_air_delta = max(50 - (ambient_temp - CRAC_setpoint), 1)
m_air = (CRAC_cooling_load + chiller_power) / (DC_Config.C_AIR * Cooling_tower_air_delta)
v_air = m_air / DC_Config.RHO_AIR
# Reference cooling tower air flow rate
if ctafr is None:
ctafr = DC_Config.CT_REFRENCE_AIR_FLOW_RATE
CT_Fan_pwr = DC_Config.CT_FAN_REF_P * (min(v_air / ctafr, 1))**3
# ToDo: exploring the new chiller_power method
return CRAC_Fan_load, CT_Fan_pwr, CRAC_cooling_load, chiller_power, power_consumed_CW, power_consumed_CT
[docs]
def chiller_sizing(DC_Config, total_mem_GB, min_CRAC_setpoint=16, max_CRAC_setpoint=22, max_ambient_temp=40.0):
'''
Calculates the chiller sizing for a data center based on the given configuration and parameters.
Parameters:
DC_Config (object): The data center configuration object.
dc_memory_GB (float): the total available memory in the datacenter.
min_CRAC_setpoint (float): The minimum CRAC setpoint temperature in degrees Celsius. Default is 16.
max_CRAC_setpoint (float): The maximum CRAC setpoint temperature in degrees Celsius. Default is 22.
max_ambient_temp (float): The maximum ambient temperature in degrees Celsius. Default is 40.0.
Returns:
tuple: A tuple containing the cooling tower reference air flow rate (ctafr) and the rated load of the cooling tower (CT_rated_load).
'''
# Create GPU configs if available in the DC_Config
gpu_config = None
if hasattr(DC_Config, 'RACK_GPU_CONFIG'):
gpu_config = DC_Config.RACK_GPU_CONFIG
dc = DataCenter_ITModel(num_racks=DC_Config.NUM_RACKS,
dc_memory_GB=total_mem_GB,
rack_supply_approach_temp_list=DC_Config.RACK_SUPPLY_APPROACH_TEMP_LIST,
rack_CPU_config=DC_Config.RACK_CPU_CONFIG,
rack_GPU_config=gpu_config,
max_W_per_rack=DC_Config.MAX_W_PER_RACK,
DC_ITModel_config=DC_Config)
# Set maximum load for both CPU and GPU if present
cpu_load = 100.0
mem_load = 100.0
ITE_load_pct_list = [cpu_load for i in range(DC_Config.NUM_RACKS)]
# Set GPU load if GPU config is available
GPU_load_pct_list = None
if gpu_config:
gpu_load = 100.0
GPU_load_pct_list = [gpu_load for i in range(DC_Config.NUM_RACKS)]
mem_load_pct_list = [mem_load for i in range(DC_Config.NUM_RACKS)]
# Calculate with both CPU and GPU loads if GPU is present
result = dc.compute_datacenter_IT_load_outlet_temp(
ITE_load_pct_list=ITE_load_pct_list,
CRAC_setpoint=max_CRAC_setpoint,
MEMORY_load_pct_list=mem_load_pct_list,
GPU_load_pct_list=GPU_load_pct_list
)
# Unpack result
if len(result) == 5: # Includes GPU power
rackwise_cpu_pwr, rackwise_itfan_pwr, rackwise_memory_power, rackwise_gpu_pwr, rackwise_outlet_temp = result
else: # No GPU
rackwise_cpu_pwr, rackwise_itfan_pwr, rackwise_memory_power, rackwise_outlet_temp = result
rackwise_gpu_pwr = [0] * len(rackwise_cpu_pwr)
avg_CRAC_return_temp = calculate_avg_CRAC_return_temp(
rack_return_approach_temp_list=DC_Config.RACK_RETURN_APPROACH_TEMP_LIST,
rackwise_outlet_temp=rackwise_outlet_temp
)
# Calculate total power including GPU if present
# 0.07 is the scaling factor for background power consumption in DRAM memory as discussed in [7]
data_center_total_ITE_Load = sum(rackwise_cpu_pwr) + sum(rackwise_itfan_pwr) + sum(rackwise_gpu_pwr) + sum(rackwise_memory_power)
m_sys = DC_Config.RHO_AIR * DC_Config.CRAC_SUPPLY_AIR_FLOW_RATE_pu * data_center_total_ITE_Load
CRAC_cooling_load = m_sys*DC_Config.C_AIR*max(0.0, avg_CRAC_return_temp-min_CRAC_setpoint)
Cooling_tower_air_delta = max(50 - (max_ambient_temp-min_CRAC_setpoint), 1)
m_air = CRAC_cooling_load/(DC_Config.C_AIR*Cooling_tower_air_delta)
# Cooling Tower Reference Air FlowRate
ctafr = m_air/DC_Config.RHO_AIR
CT_rated_load = CRAC_cooling_load
return ctafr, CT_rated_load
[docs]
def calculate_avg_CRAC_return_temp(rack_return_approach_temp_list, rackwise_outlet_temp):
"""Calculate the CRAC return air temperature
Args:
rack_return_approach_temp_list (List[float]): The delta change in temperature from each rack to the CRAC unit
rackwise_outlet_temp (float): The outlet temperature of each rack
Returns:
(float): CRAC return air temperature
"""
n = len(rack_return_approach_temp_list)
return sum([i + j for i,j in zip(rack_return_approach_temp_list, rackwise_outlet_temp)])/n # CRAC return is averaged across racks
[docs]
def heat_recovery(max_IT_load, ambient_temp, DC_Config):
temperature_delta = max((DC_Config.OFFICE_GUIDE_TEMP - ambient_temp), 0) #don't allow negative numbers, heat can't be lost to outside if it is warmer than inside
DC_office_heat = DC_Config.AVE_HLP * DC_Config.DC_AREA_PU * max_IT_load * temperature_delta
#Some heat can be sent to an office building immediatly next to the DC, this is not a District heating network model
office_heat = DC_Config.AVE_HLP *DC_Config.OFFICE_BUILDING_AREA * temperature_delta
cooling_load_reduction = DC_office_heat + office_heat
return cooling_load_reduction
"""
References:
[1]: Postema, Björn Frits. "Energy-efficient data centres: model-based analysis of power-performance trade-offs." (2018).
[2]: Raghunathan, S., & Vk, M. (2014). Power management using dynamic power state transitions and dynamic voltage frequency
scaling controls in virtualized server clusters. Turkish Journal of Electrical Engineering and Computer Sciences, 24(4). doi: 10.3906/elk-1403-264
[3]: Sun, Kaiyu, et al. "Prototype energy models for data centers." Energy and Buildings 231 (2021): 110603.
[4]: Breen, Thomas J., et al. "From chip to cooling tower data center modeling: Part I influence of server inlet temperature and temperature
rise across cabinet." 2010 12th IEEE Intersociety Conference on Thermal and Thermomechanical Phenomena in Electronic Systems. IEEE, 2010.
[5]: https://h2ocooling.com/blog/look-cooling-tower-fan-efficiences/#:~:text=The%20tower%20has%20been%20designed,of%200.42%20inches%20of%20water.
[6]: X. Tang and Z. Fu, "CPU–GPU Utilization Aware Energy-Efficient Scheduling Algorithm on Heterogeneous Computing Systems," in IEEE Access, vol. 8, pp. 58948-58958, 2020, doi: 10.1109/ACCESS.2020.2982956.
[7]: Seunghak Lee, Ki-Dong Kang, Hwanjun Lee, Hyungwon Park, Younghoon Son, Nam Sung Kim, and Daehoon Kim. 2021. GreenDIMM: OS-assisted DRAM Power Management for DRAM with a Sub-array Granularity Power-Down State. In MICRO-54: 54th Annual IEEE/ACM International Symposium on Microarchitecture (MICRO '21). Association for Computing Machinery, New York, NY, USA, 131–142. https://doi.org/10.1145/3466752.3480089.
"""