Download code/visualize_3d_architecture.py from neitpm/power-load-forecasting: direct link, hf CLI and curl.
- Browser
- Download file 20.2 kB
-
https://huggingface.co/neitpm/power-load-forecasting/resolve/main/code/visualize_3d_architecture.py
- Command line
-
hf download hf://neitpm/power-load-forecasting/code/visualize_3d_architecture.py
-
curl -L -o visualize_3d_architecture.py https://huggingface.co/neitpm/power-load-forecasting/resolve/main/code/visualize_3d_architecture.py
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/!") | |