power-load-forecasting / code /visualize_3d_architecture.py
neitpm's picture
Release: reproducible power load forecasting KAN artifacts and benchmark manifests
04cc18f verified
Raw History Blame Contribute Delete
20.2 kB
# -*- coding: utf-8 -*-
"""
=============================================================================
VISUALIZE 3D-STYLE ISOMETRIC ARCHITECTURE FIGURES (300 DPI)
For Scientific ML & Industrial Time-Series Forecasting Portfolio
=============================================================================
Layout Principles:
1. Clear Canvas: 16:9 widescreen (figsize=(14, 8)), generous vertical & horizontal spacing.
2. 2D Isometric projection with explicit depth vector (dx=0.5, dy=0.3) for clean rendering.
3. Strict separation of hierarchy:
- Header: Category tag, Title, Subtitle at top.
- Central: Isometric 3D tensor blocks with ample horizontal clearance.
- Inside/Above Block: Stage / Module Title (Bold, concise).
- Below Block: Separate dedicated Annotation Box for specifications, shapes, metrics.
4. Zero Overlap: Arrows flow cleanly between blocks without crossing text or edges.
=============================================================================
"""
import os
import sys
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.patches import Polygon, FancyBboxPatch
plt.rcParams['font.sans-serif'] = ['DejaVu Sans', 'Arial', 'Helvetica']
plt.rcParams['axes.unicode_minus'] = False
OUTPUT_DIR = Path("docs/figures")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
BG_COLOR = '#0b0f19'
CARD_BG = '#111827'
BORDER_COLOR = '#1f2937'
def draw_isometric_block(ax, x, y, w, h, depth=0.8, color='#2563eb', top_color=None, side_color=None, alpha=0.9, edge_color='#ffffff'):
"""
Draws a clean 3D isometric cuboid block using 2D polygons to avoid 3D camera overlap.
x, y: bottom-left of front face
w: width, h: height
depth: isometric extrusion amount
"""
if top_color is None:
top_color = color
if side_color is None:
side_color = color
# Isometric offsets: dx > 0, dy > 0 (angles up-right)
iso_dx = depth * 0.7
iso_dy = depth * 0.45
# 1. Front face
front_pts = np.array([
[x, y],
[x + w, y],
[x + w, y + h],
[x, y + h]
])
front = Polygon(front_pts, closed=True, facecolor=color, edgecolor=edge_color, linewidth=1.5, alpha=alpha, zorder=3)
ax.add_patch(front)
# 2. Top face
top_pts = np.array([
[x, y + h],
[x + w, y + h],
[x + w + iso_dx, y + h + iso_dy],
[x + iso_dx, y + h + iso_dy]
])
top = Polygon(top_pts, closed=True, facecolor=top_color, edgecolor=edge_color, linewidth=1.5, alpha=min(alpha + 0.1, 1.0), zorder=4)
ax.add_patch(top)
# 3. Right side face
side_pts = np.array([
[x + w, y],
[x + w + iso_dx, y + iso_dy],
[x + w + iso_dx, y + h + iso_dy],
[x + w, y + h]
])
side = Polygon(side_pts, closed=True, facecolor=side_color, edgecolor=edge_color, linewidth=1.5, alpha=max(alpha - 0.2, 0.4), zorder=3)
ax.add_patch(side)
return (x + w/2, y + h/2) # center of front face
def add_header(ax, tag, title, subtitle):
"""Draws standardized high-contrast portfolio header."""
ax.text(0.5, 0.96, tag.upper(), transform=ax.transAxes, color='#38bdf8', fontsize=10,
fontweight='bold', ha='center', va='top')
ax.text(0.5, 0.91, title, transform=ax.transAxes, color='#ffffff', fontsize=16,
fontweight='bold', ha='center', va='top')
ax.text(0.5, 0.86, subtitle, transform=ax.transAxes, color='#94a3b8', fontsize=11,
ha='center', va='top')
# -----------------------------------------------------------------------------
# FIGURE 1: 1_data_pipeline_3d.png
# -----------------------------------------------------------------------------
def make_fig1_data_pipeline():
fig, ax = plt.subplots(figsize=(15, 8.5), dpi=300)
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
add_header(ax, "Data Ingestion & Telemetry Processing Flow",
"Industrial 3-Phase Telemetry Pipeline & Continuous Segment Extraction",
"Feeder 480 - Substation 224 Tan Hung (Tay Ninh Power Company) | 2020 - 2025")
# 4 distinct pipeline stages placed horizontally with wide gaps
stages = [
{
"num": "01",
"title": "Raw Telemetry\nIngestion",
"x": 0.8, "y": 3.2, "w": 2.2, "h": 2.2,
"col": '#0284c7', "top": '#38bdf8', "side": '#0369a1',
"specs": ["3-Phase U, I, cosφ, P, Q", "30-min sampling interval", "Multi-file quarterly CSVs"]
},
{
"num": "02",
"title": "Timestamp Grid\nRegularization",
"x": 4.1, "y": 3.2, "w": 2.2, "h": 2.2,
"col": '#0d9488', "top": '#2dd4bf', "side": '#0f766e',
"specs": ["Strict 30-min time index", "Detect missing step intervals", "Wide/Long tensor alignment"]
},
{
"num": "03",
"title": "Outlier Filtering &\nGap Partitioning",
"x": 7.4, "y": 3.2, "w": 2.2, "h": 2.2,
"col": '#d97706', "top": '#fbbf24', "side": '#b45309',
"specs": ["Physical bounds sanity check", "Telemetry blackout isolator", "Segment break boundaries"]
},
{
"num": "04",
"title": "Continuous Segments\nCache Construction",
"x": 10.7, "y": 3.2, "w": 2.2, "h": 2.2,
"col": '#16a34a', "top": '#4ade80', "side": '#15803d',
"specs": ["16 continuous test segments", "14,385 valid test records", "No cross-gap lag lookback"]
}
]
for s in stages:
# 3D block
cx, cy = draw_isometric_block(ax, s["x"], s["y"], s["w"], s["h"], depth=0.7,
color=s["col"], top_color=s["top"], side_color=s["side"],
alpha=0.85, edge_color='#ffffff')
# Text inside front face (Title)
ax.text(cx, cy + 0.35, s["num"], color='#ffffff', fontsize=16, fontweight='bold', ha='center', va='center', alpha=0.9)
ax.text(cx, cy - 0.35, s["title"], color='#ffffff', fontsize=10.5, fontweight='bold', ha='center', va='center')
# Annotation box below block (Specs)
box_y = 1.3
bbox_props = dict(boxstyle="round,pad=0.5", fc='#1e293b', ec=s["col"], lw=1.5)
spec_text = "\n".join([f"• {item}" for item in s["specs"]])
ax.text(s["x"] + s["w"]/2, box_y, spec_text, color='#e2e8f0', fontsize=9.5, ha='center', va='top', bbox=bbox_props)
# Clean connecting arrows between blocks
for x_start in [3.4, 6.7, 10.0]:
ax.annotate("", xy=(x_start + 0.6, 4.3), xytext=(x_start, 4.3),
arrowprops=dict(arrowstyle="->", color='#38bdf8', lw=2.5, mutation_scale=18))
ax.set_xlim(0, 13.8)
ax.set_ylim(0, 7.5)
ax.axis('off')
plt.tight_layout()
plt.savefig(OUTPUT_DIR / "1_data_pipeline_3d.png", dpi=300, facecolor=BG_COLOR, bbox_inches='tight')
plt.close()
print("Saved 1_data_pipeline_3d.png (Cleaned)")
# -----------------------------------------------------------------------------
# FIGURE 2: 2_preprocessing_contract_3d.png
# -----------------------------------------------------------------------------
def make_fig2_preprocessing_contract():
fig, ax = plt.subplots(figsize=(15, 8.5), dpi=300)
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
add_header(ax, "Zero Data Leakage & Temporal Segregation Protocol",
"Chronological Train / Validation / Test Splitting Contract",
"Parameters fitted strictly on historical train records | Zero lookahead bias")
splits = [
{
"tag": "TRAINING SPLIT",
"period": "03/2020 — 01/2024 (46 Months)",
"x": 0.8, "y": 3.2, "w": 3.2, "h": 2.2,
"col": '#1d4ed8', "top": '#60a5fa', "side": '#1e40af',
"contract": ["Fit SimpleImputer (median)", "Fit StandardScaler (μ, σ)", "Model parameters learning"]
},
{
"tag": "VALIDATION SPLIT",
"period": "01/2024 — 11/2024 (10 Months)",
"x": 5.2, "y": 3.2, "w": 2.8, "h": 2.2,
"col": '#b45309', "top": '#fde047', "side": '#92400e',
"contract": ["Transform only (stored μ, σ)", "Early stopping threshold", "Architecture hyperparameter tuning"]
},
{
"tag": "OUT-OF-SAMPLE TEST",
"period": "11/2024 — 08/2025 (10 Months)",
"x": 9.2, "y": 3.2, "w": 3.4, "h": 2.2,
"col": '#047857', "top": '#34d399', "side": '#065f46',
"contract": ["14,385 valid evaluation records", "Transform only (zero leakage)", "16 continuous segments for bootstrap"]
}
]
for sp in splits:
cx, cy = draw_isometric_block(ax, sp["x"], sp["y"], sp["w"], sp["h"], depth=0.7,
color=sp["col"], top_color=sp["top"], side_color=sp["side"],
alpha=0.85, edge_color='#ffffff')
# Inside block text
ax.text(cx, cy + 0.35, sp["tag"], color='#ffffff', fontsize=12, fontweight='bold', ha='center', va='center')
ax.text(cx, cy - 0.35, sp["period"], color='#e0f2fe', fontsize=9.5, ha='center', va='center')
# Contract spec below
box_y = 1.3
bbox_props = dict(boxstyle="round,pad=0.5", fc='#1e293b', ec=sp["col"], lw=1.5)
spec_text = "\n".join([f"✓ {c}" for c in sp["contract"]])
ax.text(sp["x"] + sp["w"]/2, box_y, spec_text, color='#e2e8f0', fontsize=9.5, ha='center', va='top', bbox=bbox_props)
# Isolation barriers between splits
for x_bar, label in [(4.4, "BARRIER 1\nNo Future Leakage"), (8.4, "BARRIER 2\nZero Test Optimization")]:
# Draw red dashed wall
ax.plot([x_bar, x_bar], [2.2, 5.8], color='#ef4444', lw=2.2, ls='--', zorder=5)
ax.text(x_bar, 6.0, label, color='#fca5a5', fontsize=8.5, fontweight='bold', ha='center', va='bottom',
bbox=dict(boxstyle="square,pad=0.25", fc='#450a0a', ec='#ef4444', lw=1.2))
ax.set_xlim(0, 13.5)
ax.set_ylim(0, 7.5)
ax.axis('off')
plt.tight_layout()
plt.savefig(OUTPUT_DIR / "2_preprocessing_contract_3d.png", dpi=300, facecolor=BG_COLOR, bbox_inches='tight')
plt.close()
print("Saved 2_preprocessing_contract_3d.png (Cleaned)")
# -----------------------------------------------------------------------------
# FIGURE 3: 3_model_benchmark_3d.png
# -----------------------------------------------------------------------------
def make_fig3_model_benchmark():
fig, ax = plt.subplots(figsize=(15, 8.5), dpi=300)
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
add_header(ax, "Comparative Out-of-Sample Performance",
"Error Metric Comparison Across Benchmarked Families (30-min Horizon)",
"Evaluated on 14,385 out-of-sample test samples | Feeder 480 Tan Hung")
models = [
{"name": "Persistence\n(Naive)", "mae": 598.36, "rmse": 1160.44, "col": '#64748b', "top": '#94a3b8', "side": '#475569', "delta": "Baseline (0.00%)"},
{"name": "Moving\nAverage (1.5h)", "mae": 1016.16, "rmse": 1586.70, "col": '#475569', "top": '#64748b', "side": '#334155', "delta": "-69.82%"},
{"name": "Ridge\nRegression", "mae": 730.96, "rmse": 1186.89, "col": '#0284c7', "top": '#38bdf8', "side": '#0369a1', "delta": "-22.16%"},
{"name": "XGBoost\nRegressor", "mae": 645.24, "rmse": 1155.21, "col": '#2563eb', "top": '#60a5fa', "side": '#1d4ed8', "delta": "-7.83%"},
{"name": "Residual KAN\n(5-Seed Ensemble)", "mae": 519.78, "rmse": 1019.95, "col": '#059669', "top": '#34d399', "side": '#047857', "delta": "+13.13% (Best)"}
]
x_start = 0.8
step = 2.5
for i, m in enumerate(models):
curr_x = x_start + i * step
# Height scaled proportionally
h_mae = (m["mae"] / 600.0) * 1.5
h_rmse = (m["rmse"] / 1200.0) * 2.2
# 1. RMSE block (taller, behind)
draw_isometric_block(ax, curr_x + 0.35, 2.5, 0.9, h_rmse, depth=0.5,
color=m["col"], top_color=m["top"], side_color=m["side"], alpha=0.4, edge_color='#94a3b8')
# 2. MAE block (front)
draw_isometric_block(ax, curr_x, 2.5, 0.9, h_mae, depth=0.5,
color=m["col"], top_color=m["top"], side_color=m["side"], alpha=0.9, edge_color='#ffffff')
# Model label above blocks
ax.text(curr_x + 0.6, 5.7, m["name"], color='#ffffff', fontsize=10.5, fontweight='bold', ha='center', va='bottom')
# Score box below blocks
bbox_props = dict(boxstyle="round,pad=0.4", fc='#1e293b', ec=m["col"], lw=1.5)
score_text = f"MAE: {m['mae']:.1f} kW\nRMSE: {m['rmse']:.1f} kW\nΔMAE: {m['delta']}"
ax.text(curr_x + 0.6, 1.8, score_text, color='#e2e8f0', fontsize=9, ha='center', va='top', bbox=bbox_props)
# Legend at bottom center
legend_text = "■ Solid Front: MAE (kW) | ▨ Shaded Back: RMSE (kW) | Evaluation Set: 14,385 Out-of-sample Records"
ax.text(0.5, 0.05, legend_text, transform=ax.transAxes, color='#38bdf8', fontsize=10, ha='center',
bbox=dict(boxstyle="round,pad=0.3", fc='#111827', ec='#1e293b', lw=1.2))
ax.set_xlim(0, 13.8)
ax.set_ylim(0, 7.5)
ax.axis('off')
plt.tight_layout()
plt.savefig(OUTPUT_DIR / "3_model_benchmark_3d.png", dpi=300, facecolor=BG_COLOR, bbox_inches='tight')
plt.close()
print("Saved 3_model_benchmark_3d.png (Cleaned)")
# -----------------------------------------------------------------------------
# FIGURE 4: 4_kan_ensemble_3d.png
# -----------------------------------------------------------------------------
def make_fig4_kan_ensemble():
fig, ax = plt.subplots(figsize=(15, 8.5), dpi=300)
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
add_header(ax, "Ensemble Architecture & Multi-Seed Stability",
"Uniform 5-Seed Residual KAN Ensembling Pipeline",
"Seeds: [42, 101, 2024, 7, 999] | Unweighted Average (Zero Test Overfitting)")
seeds = [42, 101, 2024, 7, 999]
y_coords = [5.6, 4.6, 3.6, 2.6, 1.6]
# Draw 5 individual seed model blocks (Left column)
for s, y_pos in zip(seeds, y_coords):
draw_isometric_block(ax, x=1.0, y=y_pos, w=2.6, h=0.7, depth=0.4,
color='#4f46e5', top_color='#818cf8', side_color='#3730a3',
alpha=0.85, edge_color='#ffffff')
ax.text(2.3, y_pos + 0.35, f"Residual KAN (Seed {s})", color='#ffffff', fontsize=9.5, fontweight='bold', ha='center', va='center')
# Arrow from seed to aggregator
ax.annotate("", xy=(5.2, 3.8), xytext=(4.0, y_pos + 0.35),
arrowprops=dict(arrowstyle="->", color='#818cf8', lw=1.8, mutation_scale=14))
# Central Uniform Aggregator Block
cx, cy = draw_isometric_block(ax, x=5.3, y=2.5, w=3.4, h=2.5, depth=0.8,
color='#059669', top_color='#34d399', side_color='#047857',
alpha=0.9, edge_color='#ffffff')
ax.text(cx, cy + 0.45, "UNIFORM ENSEMBLE", color='#ffffff', fontsize=12, fontweight='bold', ha='center', va='center')
ax.text(cx, cy - 0.2, "ŷ_ens = (1/5) * Σ ŷ_i\nFixed w_i = 0.2", color='#d1fae5', fontsize=10, ha='center', va='center')
# Arrow from aggregator to output
ax.annotate("", xy=(10.0, 3.8), xytext=(9.3, 3.8),
arrowprops=dict(arrowstyle="->", color='#34d399', lw=2.5, mutation_scale=18))
# Output Block (Right column)
ox, oy = draw_isometric_block(ax, x=10.1, y=2.8, w=2.8, h=2.0, depth=0.6,
color='#0284c7', top_color='#38bdf8', side_color='#0369a1',
alpha=0.85, edge_color='#ffffff')
ax.text(ox, oy + 0.3, "FINAL PREDICTION", color='#ffffff', fontsize=11, fontweight='bold', ha='center', va='center')
ax.text(ox, oy - 0.3, "P_pred (t+1)\n[kW]", color='#e0f2fe', fontsize=10, ha='center', va='center')
# Metric annotation box below
summary_text = (
"• Single KAN Stability: MAE = 528.99 ± 12.23 kW | RMSE = 1033.35 ± 13.78 kW (5 Seeds)\n"
"• Ensemble Performance: MAE = 519.78 kW (-13.13%) | RMSE = 1019.95 kW (-12.11%) | R² = 0.9050\n"
"• Zero-Leakage Guarantee: No weights tuned on the out-of-sample test split"
)
ax.text(0.5, 0.06, summary_text, transform=ax.transAxes, color='#f8fafc', fontsize=9.5, ha='center',
bbox=dict(boxstyle="round,pad=0.5", fc='#1e293b', ec='#059669', lw=1.5))
ax.set_xlim(0, 13.5)
ax.set_ylim(0, 7.5)
ax.axis('off')
plt.tight_layout()
plt.savefig(OUTPUT_DIR / "4_kan_ensemble_3d.png", dpi=300, facecolor=BG_COLOR, bbox_inches='tight')
plt.close()
print("Saved 4_kan_ensemble_3d.png (Cleaned)")
# -----------------------------------------------------------------------------
# FIGURE 5: 5_evaluation_3d.png
# -----------------------------------------------------------------------------
def make_fig5_evaluation():
fig, ax = plt.subplots(figsize=(15, 8.5), dpi=300)
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
add_header(ax, "Statistical Verification & Multi-Metric Trade-Offs",
"Evaluation Metrics and Baseline Trade-offs Analysis",
"Contiguous Moving-Block Bootstrap (B=1,000) & Metric Asymmetry Verification")
# Block 1: Bootstrap Rigor
b1_x = 1.2
draw_isometric_block(ax, x=b1_x, y=2.8, w=4.5, h=2.5, depth=0.7,
color='#0284c7', top_color='#38bdf8', side_color='#0369a1',
alpha=0.85, edge_color='#ffffff')
ax.text(b1_x + 2.25, 4.3, "CONTIGUOUS BOOTSTRAP", color='#ffffff', fontsize=12, fontweight='bold', ha='center')
ax.text(b1_x + 2.25, 3.6, "Moving-Block (B = 1,000)\nBlock 48 (24h) & Block 336 (7d)",
color='#e0f2fe', fontsize=10, ha='center')
b1_desc = (
"• Resampling strictly within 16 continuous test segments\n"
"• Zero cross-gap leakage over missing telemetry blackouts\n"
"• Validates statistical significance of error reductions"
)
ax.text(b1_x + 2.25, 1.4, b1_desc, color='#e2e8f0', fontsize=9.5, ha='center', va='top',
bbox=dict(boxstyle="round,pad=0.5", fc='#1e293b', ec='#0284c7', lw=1.5))
# Arrow between blocks
ax.annotate("", xy=(7.2, 4.0), xytext=(6.3, 4.0),
arrowprops=dict(arrowstyle="<->", color='#38bdf8', lw=2.5, mutation_scale=18))
# Block 2: Multi-Metric Trade-Off
b2_x = 7.5
draw_isometric_block(ax, x=b2_x, y=2.8, w=4.5, h=2.5, depth=0.7,
color='#059669', top_color='#34d399', side_color='#047857',
alpha=0.85, edge_color='#ffffff')
ax.text(b2_x + 2.25, 4.3, "METRIC TRADE-OFF ANALYSIS", color='#ffffff', fontsize=12, fontweight='bold', ha='center')
ax.text(b2_x + 2.25, 3.6, "Objective Loss Asymmetry\nResidual KAN vs Persistence Naive",
color='#d1fae5', fontsize=10, ha='center')
b2_desc = (
"• MAE: KAN Ensemble wins (519.78 vs 598.36 kW | -13.13%)\n"
"• RMSE: KAN Ensemble wins (1019.95 vs 1160.44 kW | -12.11%)\n"
"• sMAPE: Persistence Naive wins (13.35% vs 18.96%)\n"
"• Conclusion: Models must be chosen based on dispatch penalty"
)
ax.text(b2_x + 2.25, 1.4, b2_desc, color='#e2e8f0', fontsize=9.5, ha='center', va='top',
bbox=dict(boxstyle="round,pad=0.5", fc='#1e293b', ec='#059669', lw=1.5))
ax.set_xlim(0, 13.5)
ax.set_ylim(0, 7.5)
ax.axis('off')
plt.tight_layout()
plt.savefig(OUTPUT_DIR / "5_evaluation_3d.png", dpi=300, facecolor=BG_COLOR, bbox_inches='tight')
plt.close()
print("Saved 5_evaluation_3d.png (Cleaned)")
if __name__ == "__main__":
print("Regenerating clean 3D isometric architecture figures (Zero Overlap)...")
make_fig1_data_pipeline()
make_fig2_preprocessing_contract()
make_fig3_model_benchmark()
make_fig4_kan_ensemble()
make_fig5_evaluation()
print("All 5 clean figures regenerated successfully in docs/figures/!")