diff --git a/src/charts/utilization_gap.py b/src/charts/utilization_gap.py new file mode 100644 index 0000000..f737549 --- /dev/null +++ b/src/charts/utilization_gap.py @@ -0,0 +1,119 @@ +"""GPU Utilization Paradox Chart""" +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from matplotlib.patches import Circle +import numpy as np +from src.utils.styling import ( + get_theme, EXPORT_DPI, BUBBLE_ZONE, NORMAL_ZONE, WARNING_ZONE, + GRAY_LIGHT, GRAY_DARK, GRAY_MEDIUM, BLACK, WHITE +) +from src.utils.export import save_chart_tight + + +def plot_gpu_utilization() -> str: + plt.rcParams.update(get_theme()) + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8)) + + # --------------------------------------------------------------------------- + # LEFT PANEL: Horizontal bar comparison + # --------------------------------------------------------------------------- + categories = [ + "Total AI Infrastructure Spend (2025)", + "Effective GPU Utilization (~5%)", + "Industry Target (~65%)", + "Human Workforce Utilization (~85%)", + ] + # Normalize to percentages for visual comparison + values = [100, 5, 65, 85] + colors_bar = [GRAY_DARK, BUBBLE_ZONE, NORMAL_ZONE, WARNING_ZONE] + + y_pos = np.arange(len(categories)) + bars = ax1.barh(y_pos, values, color=colors_bar, edgecolor="white", height=0.6) + + ax1.set_yticks(y_pos) + ax1.set_yticklabels(categories, fontsize=11) + ax1.set_xlabel("Relative Percentage (%)", fontsize=12) + ax1.set_title("GPU Utilization Paradox", fontsize=16, fontweight="bold") + ax1.set_xlim(0, 110) + ax1.grid(True, alpha=0.3, axis="x") + + # Value labels on bars + for bar, val in zip(bars, values): + ax1.text( + val + 1, bar.get_y() + bar.get_height() / 2, + f"{val}%", + va="center", fontsize=11, fontweight="bold", + ) + + # --------------------------------------------------------------------------- + # RIGHT PANEL: Donut chart + # --------------------------------------------------------------------------- + sizes = [5, 95] # Utilized vs Idle + colors_donut = [BUBBLE_ZONE, GRAY_LIGHT] + labels_donut = ["Utilized (5%)", "Idle (95%)"] + + wedges, texts, autotexts = ax2.pie( + sizes, colors=colors_donut, startangle=90, + textprops={"fontsize": 10}, + autopct="", counterclock=False, + ) + + # Inner circle for donut + centre_circle = Circle((0, 0), 0.65, fc=WHITE) + ax2.add_artist(centre_circle) + + # Center text + ax2.text( + 0, 0, "5%\nGPU\nUTIL", ha="center", va="center", + fontsize=20, fontweight="bold", color=BUBBLE_ZONE, + ) + + ax2.set_title("GPU Capacity Breakdown", fontsize=14, fontweight="bold") + ax2.legend(wedges, labels_donut, loc="lower right", fontsize=10) + + # --------------------------------------------------------------------------- + # FIGURE-LEVEL: Title, subtitle, callout, source + # --------------------------------------------------------------------------- + fig.suptitle( + "The GPU Utilization Paradox", + fontsize=22, fontweight="bold", y=0.98, + ) + fig.text( + 0.5, 0.93, + "\\$400B+ spent on AI infrastructure — ~5% average GPU utilization", + ha="center", fontsize=13, color=GRAY_MEDIUM, + ) + + # Bold callout + fig.text( + 0.5, 0.02, + "\\$295B+ spent | ~5% utilized | ~\\$280B wasted capacity", + ha="center", fontsize=14, fontweight="bold", + bbox=dict( + boxstyle="round,pad=0.5", + facecolor=GRAY_LIGHT, + edgecolor=BUBBLE_ZONE, + linewidth=2, + ), + ) + + # Source note + fig.text( + 0.5, 0.07, + "Enterprise GPU utilization estimates from industry surveys (2024-2025)", + ha="center", fontsize=9, color=GRAY_MEDIUM, style="italic", + ) + + path = save_chart_tight(fig, "08_gpu_utilization.png") + plt.close(fig) + return path + + +def main(): + path = plot_gpu_utilization() + print(f"Chart saved: {path}") + + +if __name__ == "__main__": + main()