""" Nature-standard figures for Campus Weather VAE paper. """ import os, sys, json sys.path.insert(0, os.path.dirname(__file__)) import numpy as np import matplotlib; matplotlib.use('Agg') import matplotlib.pyplot as plt from matplotlib.patches import FancyBboxPatch from sklearn.decomposition import PCA # Okabe-Ito colour-blind safe palette C = {'blue':'#0072B2','orange':'#E69F00','green':'#009E73','red':'#D55E00', 'purple':'#CC79A7','cyan':'#56B4E9','grey':'#999999','black':'#000000'} plt.rcParams.update({ 'font.family': 'sans-serif', 'font.size': 7, 'axes.labelsize': 8, 'axes.titlesize': 8, 'figure.dpi': 300, 'savefig.dpi': 300, 'savefig.bbox': 'tight', 'axes.linewidth': 0.5, 'axes.spines.top': False, 'axes.spines.right': False, 'axes.grid': False, }) FIG = '/app/campus_weather/figures' os.makedirs(FIG, exist_ok=True) def panel_label(ax, label, x=-0.15, y=1.08): ax.text(x, y, label, transform=ax.transAxes, fontsize=10, fontweight='bold', va='bottom') def fig1_campus_map(coords, cluster_labels): """Fig 1: Station map + discovered clusters.""" fig, axes = plt.subplots(1, 2, figsize=(7.08, 3.2)) # a: Station locations ax = axes[0] ax.scatter(coords[:, 1], coords[:, 0], c=C['blue'], s=25, edgecolors='white', linewidth=0.4, zorder=5) for i in range(len(coords)): ax.annotate(f'{i+1}', (coords[i,1], coords[i,0]), fontsize=3.5, ha='center', va='bottom', xytext=(0,3), textcoords='offset points', color='#333') ax.set_xlabel('Longitude (°E)'); ax.set_ylabel('Latitude (°N)') ax.set_title('Station network (N=40)') panel_label(ax, 'a') # b: Clusters ax = axes[1] colours = [C['blue'], C['orange'], C['green'], C['red']] for c in range(max(cluster_labels)+1): mask = np.array(cluster_labels) == c ax.scatter(coords[mask, 1], coords[mask, 0], c=colours[c], s=35, edgecolors='black', linewidth=0.3, zorder=5, label=f'Zone {c+1}') ax.legend(fontsize=6, frameon=False, loc='lower right') ax.set_xlabel('Longitude (°E)'); ax.set_ylabel('Latitude (°N)') ax.set_title('Discovered microclimate zones (K=4)') panel_label(ax, 'b') plt.tight_layout(w_pad=1.0) plt.savefig(f'{FIG}/fig1_campus.pdf'); plt.savefig(f'{FIG}/fig1_campus.png'); plt.close() print('✓ Fig 1') def fig2_reconstruction(results): """Fig 2: Reconstruction quality bar chart.""" from train import VAR_NAMES, VAR_UNITS fig, ax = plt.subplots(1, 1, figsize=(3.54, 2.5)) names = VAR_NAMES r2s = [results['reconstruction'][n]['R2'] for n in names] x = np.arange(len(names)) bars = ax.bar(x, r2s, color=C['blue'], width=0.6, edgecolor='white', linewidth=0.3) ax.set_ylim(0.93, 1.001) ax.set_xticks(x); ax.set_xticklabels(names, rotation=45, ha='right', fontsize=6) ax.set_ylabel('R²') ax.set_title('Reconstruction quality (test set)') ax.axhline(0.99, color=C['grey'], linestyle='--', linewidth=0.5) for bar, val in zip(bars, r2s): ax.text(bar.get_x() + bar.get_width()/2, val + 0.001, f'{val:.4f}', ha='center', va='bottom', fontsize=5) plt.tight_layout() plt.savefig(f'{FIG}/fig2_reconstruction.pdf'); plt.savefig(f'{FIG}/fig2_reconstruction.png'); plt.close() print('✓ Fig 2') def fig3_spatial_interpolation(results): """Fig 3: Spatial interpolation — held-out station performance.""" from train import VAR_NAMES fig, axes = plt.subplots(1, 2, figsize=(7.08, 2.8)) si = results['spatial_interpolation'] stations = [k for k in si if k.startswith('WS')] # a: AirTemp per station ax = axes[0] mae_vals = [si[s]['AirTemp']['MAE'] for s in stations] r2_vals = [si[s]['AirTemp']['R2'] for s in stations] x = np.arange(len(stations)) ax.bar(x, mae_vals, color=C['orange'], width=0.6) ax.set_xticks(x); ax.set_xticklabels(stations, fontsize=6) ax.set_ylabel('MAE (°C)'); ax.set_title('Air temperature at held-out stations') panel_label(ax, 'a') # b: All variables average ax = axes[1] avg = si['average'] vars_show = ['AirTemp', 'RelHum', 'AtmPress', 'WindSpeed'] mae_avg = [avg[v]['MAE'] for v in vars_show] r2_avg = [avg[v]['R2'] for v in vars_show] x = np.arange(len(vars_show)) bars = ax.bar(x - 0.15, mae_avg, 0.3, label='MAE', color=C['blue']) ax2 = ax.twinx() ax2.bar(x + 0.15, r2_avg, 0.3, label='R²', color=C['green'], alpha=0.7) ax.set_xticks(x); ax.set_xticklabels(vars_show, fontsize=6) ax.set_ylabel('MAE'); ax2.set_ylabel('R²') ax.set_title('Average across held-out stations') ax.legend(fontsize=5, loc='upper left', frameon=False) ax2.legend(fontsize=5, loc='upper right', frameon=False) panel_label(ax, 'b') plt.tight_layout(w_pad=1.0) plt.savefig(f'{FIG}/fig3_spatial.pdf'); plt.savefig(f'{FIG}/fig3_spatial.png'); plt.close() print('✓ Fig 3') def fig4_forecasting(results): """Fig 4: Forecasting — embedding vs persistence vs climatology.""" fc = results['temporal_forecasting'] fig, axes = plt.subplots(1, 3, figsize=(7.08, 2.5)) vars_show = ['AirTemp', 'RelHum', 'GlobalRad'] units = ['°C', '%', 'W/m²'] horizons = ['T+1', 'T+6', 'T+24'] for ax, var, unit, pl in zip(axes, vars_show, units, ['a','b','c']): emb = [fc[h][var]['MAE_embedding'] for h in horizons] per = [fc[h][var]['MAE_persistence'] for h in horizons] clm = [fc[h][var]['MAE_climatology'] for h in horizons] x = np.arange(len(horizons)) w = 0.25 ax.bar(x - w, emb, w, label='Embedding', color=C['blue']) ax.bar(x, per, w, label='Persistence', color=C['orange']) ax.bar(x + w, clm, w, label='Climatology', color=C['grey']) ax.set_xticks(x); ax.set_xticklabels(horizons, fontsize=6) ax.set_ylabel(f'MAE ({unit})') ax.set_title(var) if pl == 'a': ax.legend(fontsize=5, frameon=False) panel_label(ax, pl) plt.tight_layout(w_pad=0.8) plt.savefig(f'{FIG}/fig4_forecasting.pdf'); plt.savefig(f'{FIG}/fig4_forecasting.png'); plt.close() print('✓ Fig 4') def fig5_anomaly(results, data): """Fig 5: Anomaly detection — error timeseries + hour distribution.""" ad = results['anomaly_detection'] fig, axes = plt.subplots(1, 2, figsize=(7.08, 2.5)) # a: Error timeseries (use saved npy) ax = axes[0] errors = np.load('/app/campus_weather/results/anomaly_errors.npy') # Downsample for plotting (daily mean) daily = errors.reshape(-1, 24).mean(axis=1) ax.plot(np.arange(len(daily)), daily, linewidth=0.5, color=C['blue']) threshold_daily = np.percentile(daily, 95) ax.axhline(threshold_daily, color=C['red'], linestyle='--', linewidth=0.8, label='95th percentile') ax.set_xlabel('Day of year'); ax.set_ylabel('Reconstruction error') ax.set_title('Campus-wide anomaly score') ax.legend(fontsize=5, frameon=False) panel_label(ax, 'a') # b: Hour distribution of anomalies ax = axes[1] hour_dist = np.array(ad['anomaly_hour_distribution']) ax.bar(np.arange(24), hour_dist, color=C['orange'], width=0.8) ax.set_xlabel('Hour of day'); ax.set_ylabel('Count') ax.set_title('Temporal distribution of anomalies') ax.set_xticks([0, 6, 12, 18]) panel_label(ax, 'b') plt.tight_layout(w_pad=1.0) plt.savefig(f'{FIG}/fig5_anomaly.pdf'); plt.savefig(f'{FIG}/fig5_anomaly.png'); plt.close() print('✓ Fig 5') def fig6_future(results): """Fig 6: 24h rolling forecast skill curve.""" fp = results['future_prediction']['per_hour'] fig, axes = plt.subplots(1, 3, figsize=(7.08, 2.5)) vars_show = ['AirTemp', 'RelHum', 'GlobalRad'] units = ['°C', '%', 'W/m²'] hours = list(range(1, 25)) for ax, var, unit, pl in zip(axes, vars_show, units, ['a','b','c']): mae_emb = [fp[f'h+{h}'][var]['MAE_embedding'] for h in hours] mae_per = [fp[f'h+{h}'][var]['MAE_persistence'] for h in hours] mae_clm = [fp[f'h+{h}'][var]['MAE_climatology'] for h in hours] ax.plot(hours, mae_emb, color=C['blue'], linewidth=1.2, label='Embedding') ax.plot(hours, mae_per, color=C['orange'], linewidth=1.2, linestyle='--', label='Persistence') ax.plot(hours, mae_clm, color=C['grey'], linewidth=1.2, linestyle=':', label='Climatology') ax.set_xlabel('Forecast horizon (h)') ax.set_ylabel(f'MAE ({unit})') ax.set_title(var) ax.set_xticks([1, 6, 12, 18, 24]) if pl == 'a': ax.legend(fontsize=5, frameon=False) panel_label(ax, pl) plt.tight_layout(w_pad=0.8) plt.savefig(f'{FIG}/fig6_future.pdf'); plt.savefig(f'{FIG}/fig6_future.png'); plt.close() print('✓ Fig 6') if __name__ == '__main__': results = json.load(open('/app/campus_weather/results/all_results.json')) npz = np.load('/app/campus_weather/results/checkpoints/embeddings.npz', allow_pickle=True) data, coords = npz['data'], npz['coords'] cluster_labels = results['clustering']['K=4']['labels'] fig1_campus_map(coords, cluster_labels) fig2_reconstruction(results) fig3_spatial_interpolation(results) fig4_forecasting(results) fig5_anomaly(results, data) fig6_future(results) print(f'\nAll figures saved to {FIG}/')