Spaces:
Running
Running
File size: 8,603 Bytes
aedaf74 | 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 | # Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
"""
Renders simulation state into a text-based monitoring dashboard.
The dashboard mimics what a real datacenter operator would see on their
NOC (Network Operations Center) screens. It is the primary observation
for the LLM agent.
"""
from __future__ import annotations
from ..config import ASHRAE_CLASSES, m3s_to_cfm
from ..simulation.types import (
CRACFaultType,
CRACState,
CRACStatus,
DatacenterState,
PowerState,
ZoneState,
)
def render_dashboard(
state: DatacenterState,
*,
alert: str = "",
step: int = 0,
max_steps: int = 15,
scenario_type: str = "",
) -> str:
"""Render the full monitoring dashboard as a text string.
Args:
state: Current datacenter simulation state.
alert: Active alert message to display prominently.
step: Current step number in the episode.
max_steps: Maximum steps in the episode.
scenario_type: Type of scenario being run.
Returns:
Multi-line string formatted as a monitoring dashboard.
"""
w = 68 # Inner width of the dashboard frame
lines: list[str] = []
def hline(char: str = "═") -> str:
return f"╠{char * w}╣"
def row(text: str) -> str:
return f"║ {text:<{w - 2}} ║"
# Header
lines.append(f"╔{'═' * w}╗")
title = "DC-OPS MONITORING DASHBOARD"
lines.append(f"║{title:^{w}}║")
sim_min = state.sim_time_s / 60.0
status_line = f"Sim Time: {sim_min:.1f} min Step: {step}/{max_steps}"
if scenario_type:
status_line += f" [{scenario_type}]"
lines.append(row(status_line))
# Alert section
if alert:
lines.append(hline())
# Split long alerts across lines
alert_prefix = "!! ALERT: "
remaining = w - 2 - len(alert_prefix)
if len(alert) <= remaining:
lines.append(row(f"{alert_prefix}{alert}"))
else:
lines.append(row(f"{alert_prefix}{alert[:remaining]}"))
# Continuation lines
for i in range(remaining, len(alert), w - 4):
lines.append(row(f" {alert[i:i + w - 4]}"))
# Cooling Units
lines.append(hline())
lines.append(row("COOLING UNITS"))
lines.append(row(f"{'Unit':<10} {'Status':<12} {'Setpoint':>8} {'Supply':>8} {'Fan%':>5} {'CFM':>7} {'kW':>6}"))
lines.append(row("-" * (w - 2)))
for zone in state.zones:
for crac in zone.crac_units:
lines.append(row(_format_crac_row(crac, state.outside_temp_c, zone.hot_aisle_temp_c)))
# Zone Temperatures
lines.append(hline())
lines.append(row("ZONE TEMPERATURES"))
lines.append(row(f"{'Zone':<8} {'Cold Aisle':>10} {'Hot Aisle':>10} {'Max Inlet':>10} {'IT Load':>8} {'Class':>6}"))
lines.append(row("-" * (w - 2)))
for zone in state.zones:
lines.append(row(_format_zone_row(zone)))
# Rack Detail (per zone, show max-temp racks)
lines.append(hline())
lines.append(row("RACK TEMPERATURES (top 5 hottest)"))
lines.append(row(f"{'Rack':<8} {'Inlet':>8} {'Outlet':>8} {'Load kW':>8} {'CFM':>7}"))
lines.append(row("-" * (w - 2)))
# Collect all racks, sort by inlet temp descending
all_racks = []
for zone in state.zones:
all_racks.extend(zone.racks)
all_racks.sort(key=lambda r: r.inlet_temp_c, reverse=True)
for rack in all_racks[:5]:
cfm = m3s_to_cfm(rack.airflow_m3s)
lines.append(row(
f"{rack.rack_id:<8} {rack.inlet_temp_c:>7.1f}°C {rack.outlet_temp_c:>7.1f}°C "
f"{rack.it_load_kw:>7.1f} {cfm:>7.0f}"
))
# Power Section
lines.append(hline())
lines.append(row("POWER"))
p_it = state.total_it_load_kw
p_cooling = state.total_cooling_power_kw
pue = state.pue
lines.append(row(
f"IT Load: {p_it:.1f} kW | Cooling: {p_cooling:.1f} kW | PUE: {pue:.2f}"
))
if state.power is not None:
lines.append(row(_format_power_section(state.power)))
lines.append(row(_format_ups_summary(state.power)))
else:
lines.append(row("UPS: N/A | Generator: N/A"))
# Environment
lines.append(hline())
lines.append(row("ENVIRONMENT"))
lines.append(row(
f"Outside: {state.outside_temp_c:.1f}°C | "
f"Humidity: {state.outside_humidity_rh * 100:.0f}% RH"
))
# Footer
lines.append(f"╚{'═' * w}╝")
return "\n".join(lines)
def _format_crac_row(crac: CRACState, outside_temp_c: float, hot_aisle_temp_c: float) -> str:
"""Format a single CRAC row for the dashboard."""
# Status display
if crac.status == CRACStatus.FAULT:
fault_label = crac.fault_type.value.upper() if crac.fault_type != CRACFaultType.NONE else "FAULT"
status_str = f"!! {fault_label}"
elif crac.status == CRACStatus.MAINTENANCE:
status_str = "MAINT"
elif crac.status == CRACStatus.STANDBY:
status_str = "STANDBY"
else:
status_str = "RUNNING"
# Supply temp display
if crac.status != CRACStatus.RUNNING:
supply_str = "---"
else:
supply_str = f"{crac.supply_temp_c:.1f}°C"
# CFM
cfm = m3s_to_cfm(crac.current_airflow_m3s)
# Power consumption
q_cool = crac.compute_cooling_output_kw(hot_aisle_temp_c)
p_kw = crac.compute_power_consumption_kw(q_cool, outside_temp_c)
return (
f"{crac.unit_id:<10} {status_str:<12} {crac.setpoint_c:>7.1f}°C "
f"{supply_str:>8} {crac.fan_speed_pct:>5.0f} {cfm:>7.0f} {p_kw:>6.1f}"
)
def _format_zone_row(zone: ZoneState) -> str:
"""Format a single zone row for the dashboard."""
ashrae = ASHRAE_CLASSES.get(zone.ashrae_class)
max_inlet = zone.max_inlet_temp_c
# Mark if exceeding ASHRAE recommended
inlet_marker = ""
if ashrae and max_inlet > ashrae.recommended_max_c:
inlet_marker = "*"
if ashrae and max_inlet > ashrae.allowable_max_c:
inlet_marker = "!!"
return (
f"{zone.zone_id:<8} {zone.cold_aisle_temp_c:>9.1f}°C "
f"{zone.hot_aisle_temp_c:>9.1f}°C {max_inlet:>8.1f}°C{inlet_marker:<2}"
f"{zone.total_it_load_kw:>7.1f} {zone.ashrae_class:>6}"
)
def _format_power_section(power: PowerState) -> str:
"""Format power source status line."""
parts: list[str] = []
# Utility / generator status
if power.utility_available:
parts.append("Utility: NORMAL")
else:
parts.append("Utility: DOWN")
from ..simulation.types import GeneratorState as GS
gen = power.generator
if gen.state == GS.OFF:
parts.append("Gen: OFF")
elif gen.state == GS.LOADED:
fuel_hrs = gen.fuel_remaining_hours
fuel_str = f"{fuel_hrs:.1f}h" if fuel_hrs < 100 else ">100h"
parts.append(f"Gen: LOADED {gen.load_fraction * 100:.0f}% (fuel: {fuel_str})")
elif gen.state in (GS.START_DELAY, GS.CRANKING, GS.WARMING):
parts.append(f"Gen: STARTING ({gen.state.value})")
elif gen.state == GS.READY:
parts.append("Gen: READY")
elif gen.state == GS.COOLDOWN:
parts.append("Gen: COOLDOWN")
# ATS position
from ..simulation.types import ATSPosition
ats = power.ats
if ats.position == ATSPosition.UTILITY:
parts.append("ATS: UTILITY")
elif ats.position == ATSPosition.GENERATOR:
parts.append("ATS: GENERATOR")
elif ats.position == ATSPosition.TRANSFERRING:
parts.append("ATS: TRANSFERRING")
return " | ".join(parts)
def _format_ups_summary(power: PowerState) -> str:
"""Format UPS status summary line."""
if not power.ups_units:
return "UPS: N/A"
parts: list[str] = []
for ups in power.ups_units:
soc_pct = ups.battery_soc * 100
mode_str = ups.mode.value.upper().replace("_", " ")
load_pct = ups.load_fraction * 100
eta_pct = ups.efficiency * 100
if ups.mode.value == "on_battery":
time_str = ""
if ups.battery_time_remaining_s < float("inf"):
mins = ups.battery_time_remaining_s / 60.0
time_str = f" {mins:.0f}min"
parts.append(f"{ups.unit_id}: BATTERY {soc_pct:.0f}%{time_str}")
elif ups.mode.value == "fault":
parts.append(f"{ups.unit_id}: FAULT")
else:
parts.append(f"{ups.unit_id}: {mode_str} {load_pct:.0f}% η{eta_pct:.0f}%")
return "UPS: " + " | ".join(parts)
|