File size: 20,185 Bytes
04cc18f | 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 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 | # -*- 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/!")
|