| """ |
| EPW Weather File Generation from VAE Embeddings |
| ================================================ |
| Generates EnergyPlus Weather (EPW) files for building energy simulation: |
| |
| 1. BASELINE: Campus-mean weather from imputed observations (2025) |
| 2. HEATWAVE: +2Β°C sustained warming via latent space manipulation |
| 3. UHI INTENSIFICATION: +2Β°C warming with reduced wind (urban canyon effect) |
| |
| The VAE's continuous latent space enables generation of physically coherent |
| extreme scenarios where all six weather variables shift together consistently β |
| something classical interpolation methods (IDW, kriging) cannot do. |
| |
| Run: python generate_epw.py |
| Outputs: epw/NUS_baseline_2025.epw, epw/NUS_heatwave.epw, epw/NUS_uhi.epw |
| """ |
|
|
| import sys, os, json, warnings |
| sys.path.insert(0, os.path.dirname(__file__)) |
| warnings.filterwarnings('ignore') |
|
|
| import numpy as np |
| import pandas as pd |
| import pvlib |
| import torch |
| from sklearn.linear_model import LinearRegression |
| from ladybug.epw import EPW |
| from ladybug.location import Location |
|
|
| from model import WeatherVAE |
| from train import load_nus40, VAR_NAMES, VAR_UNITS |
|
|
| |
| BASE = '/app/campus_weather' |
| DATA_DIR = f'{BASE}/imputed' |
| RESULTS = f'{BASE}/results' |
| EPW_DIR = f'{BASE}/epw' |
| FIG_DIR = f'{BASE}/figures' |
|
|
| |
| CAMPUS_LAT = 1.2992 |
| CAMPUS_LON = 103.7764 |
| CAMPUS_ALT = 16 |
| TZ_OFFSET = 8 |
| TZ_STR = 'Asia/Singapore' |
| YEAR = 2025 |
|
|
|
|
| |
| |
| |
|
|
| def dewpoint_magnus(T_C, RH_pct): |
| """Dew point temperature via Magnus formula. T in Β°C, RH in %.""" |
| RH_safe = np.clip(RH_pct, 1, 100) |
| a, b = 17.27, 237.7 |
| gamma = a * T_C / (b + T_C) + np.log(RH_safe / 100.0) |
| return b * gamma / (a - gamma) |
|
|
|
|
| def horizontal_infrared_radiation(T_C, RH_pct): |
| """ |
| Horizontal infrared radiation intensity (W/mΒ²). |
| Martin-Berdahl model using dew point temperature. |
| """ |
| sigma = 5.67e-8 |
| T_K = T_C + 273.15 |
| Tdp_K = dewpoint_magnus(T_C, RH_pct) + 273.15 |
| eps_sky = np.clip(0.787 + 0.764 * np.log(Tdp_K / 273.16), 0.6, 1.0) |
| return eps_sky * sigma * T_K**4 |
|
|
|
|
| def compute_solar_derived(ghr, year=YEAR): |
| """ |
| From Global Horizontal Radiation (W/mΒ²), compute: |
| - DNI (Direct Normal Irradiance) |
| - DHI (Diffuse Horizontal Irradiance) |
| - Extraterrestrial radiation |
| - Sky cover (tenths) |
| |
| All returned in Wh/mΒ² (numerically equal to W/mΒ² for hourly averages). |
| """ |
| times = pd.date_range(f'{year}-01-01', periods=8760, freq='1h', tz=TZ_STR) |
| solpos = pvlib.solarposition.get_solarposition(times, CAMPUS_LAT, CAMPUS_LON, CAMPUS_ALT) |
| zenith = solpos['apparent_zenith'].values |
|
|
| ghr_clean = np.clip(ghr, 0, 1400) |
| |
| ghr_clean[zenith > 90] = 0 |
|
|
| decomp = pvlib.irradiance.erbs(ghr_clean, zenith, times) |
| dni = np.nan_to_num(decomp['dni'].values, nan=0.0).clip(0, 1200) |
| dhi = np.nan_to_num(decomp['dhi'].values, nan=0.0).clip(0, 800) |
|
|
| |
| etrn = pvlib.irradiance.get_extra_radiation(times).values |
| etr = (etrn * np.cos(np.radians(zenith))).clip(0) |
|
|
| |
| kt = np.where(etr > 10, ghr_clean / etr, 0).clip(0, 1) |
| sky_cover = ((1 - kt) * 10).clip(0, 10).astype(int) |
|
|
| return { |
| 'dni': dni, 'dhi': dhi, |
| 'etrn': etrn, 'etr': etr, |
| 'sky_cover': sky_cover, 'zenith': zenith, |
| } |
|
|
|
|
| def physical_bounds_clip(weather): |
| """Clip decoded weather to physical bounds.""" |
| weather[:, 0] = np.clip(weather[:, 0], 0, 20) |
| weather[:, 1] = np.clip(weather[:, 1], 0, 360) |
| weather[:, 2] = np.clip(weather[:, 2], 15, 50) |
| weather[:, 3] = np.clip(weather[:, 3], 10, 100) |
| weather[:, 4] = np.clip(weather[:, 4], 990, 1020) |
| weather[:, 5] = np.clip(weather[:, 5], 0, 1400) |
| return weather |
|
|
|
|
| |
| |
| |
|
|
| def weather_to_epw(weather, scenario_name, description, output_path, station_id='NUS_VAE'): |
| """ |
| Convert a (8760, 6) weather array to a valid EPW file. |
| |
| weather columns: [WindSpeed, WindDir, AirTemp, RelHum, AtmPress, GlobalRad] |
| """ |
| ws = weather[:, 0] |
| wd = weather[:, 1] |
| ta = weather[:, 2] |
| rh = weather[:, 3] |
| pa = weather[:, 4] * 100 |
| ghr = weather[:, 5] |
|
|
| |
| dp = dewpoint_magnus(ta, rh) |
| hiri = horizontal_infrared_radiation(ta, rh) |
| solar = compute_solar_derived(ghr) |
|
|
| |
| epw = EPW.from_missing_values() |
| epw.location = Location( |
| city='NUS_Campus', state='', country='SGP', |
| source=f'VAE_{scenario_name}', station_id=station_id, |
| latitude=CAMPUS_LAT, longitude=CAMPUS_LON, |
| time_zone=TZ_OFFSET, elevation=CAMPUS_ALT |
| ) |
| epw.comments_1 = f'VAE-generated EPW: {scenario_name}' |
| epw.comments_2 = description |
|
|
| |
| epw.years.values = [YEAR] * 8760 |
| epw.dry_bulb_temperature.values = ta.tolist() |
| epw.dew_point_temperature.values = dp.tolist() |
| epw.relative_humidity.values = rh.tolist() |
| epw.atmospheric_station_pressure.values = pa.tolist() |
| epw.wind_speed.values = ws.tolist() |
| epw.wind_direction.values = wd.tolist() |
| epw.global_horizontal_radiation.values = ghr.tolist() |
| epw.direct_normal_radiation.values = solar['dni'].tolist() |
| epw.diffuse_horizontal_radiation.values = solar['dhi'].tolist() |
| epw.horizontal_infrared_radiation_intensity.values = hiri.tolist() |
| epw.extraterrestrial_horizontal_radiation.values = solar['etr'].tolist() |
| epw.extraterrestrial_direct_normal_radiation.values = solar['etrn'].tolist() |
| epw.total_sky_cover.values = solar['sky_cover'].tolist() |
| epw.opaque_sky_cover.values = solar['sky_cover'].tolist() |
|
|
| |
| epw.snow_depth.values = [0] * 8760 |
| epw.days_since_last_snowfall.values = [99] * 8760 |
| epw.albedo.values = [0.15] * 8760 |
|
|
| os.makedirs(os.path.dirname(output_path), exist_ok=True) |
| epw.save(output_path) |
| return epw |
|
|
|
|
| |
| |
| |
|
|
| def generate_scenarios(): |
| """Generate all three EPW scenarios.""" |
| os.makedirs(EPW_DIR, exist_ok=True) |
|
|
| |
| print("Loading data and model...") |
| data, coords, datetimes = load_nus40(DATA_DIR) |
| T, N, V = data.shape |
|
|
| ckpt = torch.load(f'{RESULTS}/checkpoints/best.pt', map_location='cpu', weights_only=False) |
| model = WeatherVAE(**ckpt['config']) |
| model.load_state_dict(ckpt['model']) |
| model.set_normalisation(ckpt['mean'], ckpt['std']) |
| model.eval() |
|
|
| emb = np.load(f'{RESULTS}/checkpoints/embeddings.npz')['embeddings'] |
| campus_emb = emb.mean(axis=1) |
| campus_data = data.mean(axis=1) |
|
|
| |
| print("Computing latent directions...") |
| directions = {} |
| for v, name in enumerate(VAR_NAMES): |
| reg = LinearRegression() |
| reg.fit(campus_emb, campus_data[:, v]) |
| w = reg.coef_ |
| directions[name] = w / np.linalg.norm(w) |
|
|
| |
| with torch.no_grad(): |
| baseline_decoded = model.decode( |
| torch.from_numpy(campus_emb.astype(np.float32)) |
| ).numpy() |
| baseline_decoded = physical_bounds_clip(baseline_decoded) |
|
|
| |
| |
| |
| |
| print("\n" + "=" * 60) |
| print("SCENARIO 1: BASELINE (imputed observations)") |
| print("=" * 60) |
|
|
| baseline_weather = campus_data.copy() |
| epw_baseline = weather_to_epw( |
| baseline_weather, |
| scenario_name='Baseline_2025', |
| description='Campus-mean of 40 NUS stations, imputed observations, Jan-Dec 2025', |
| output_path=f'{EPW_DIR}/NUS_baseline_2025.epw', |
| station_id='NUS_BAS_2025', |
| ) |
| print_scenario_stats('Baseline', baseline_weather) |
|
|
| |
| |
| |
| |
| |
| |
| |
| print("\n" + "=" * 60) |
| print("SCENARIO 2: HEATWAVE (+2Β°C via latent shift)") |
| print("=" * 60) |
|
|
| |
| |
| heat_scale = 0.75 |
| z_heat = campus_emb + heat_scale * directions['AirTemp'] |
|
|
| with torch.no_grad(): |
| heat_decoded = model.decode( |
| torch.from_numpy(z_heat.astype(np.float32)) |
| ).numpy() |
| heat_decoded = physical_bounds_clip(heat_decoded) |
|
|
| |
| dt_mean = heat_decoded[:, 2].mean() - baseline_decoded[:, 2].mean() |
| print(f" Achieved mean ΞT: {dt_mean:+.2f}Β°C (target: +2Β°C)") |
|
|
| |
| if abs(dt_mean - 2.0) > 0.3: |
| |
| lo, hi = 0.1, 3.0 |
| for _ in range(20): |
| mid = (lo + hi) / 2 |
| z_test = campus_emb + mid * directions['AirTemp'] |
| with torch.no_grad(): |
| dec_test = model.decode(torch.from_numpy(z_test.astype(np.float32))).numpy() |
| dt_test = dec_test[:, 2].mean() - baseline_decoded[:, 2].mean() |
| if dt_test < 2.0: |
| lo = mid |
| else: |
| hi = mid |
| heat_scale = (lo + hi) / 2 |
| z_heat = campus_emb + heat_scale * directions['AirTemp'] |
| with torch.no_grad(): |
| heat_decoded = model.decode(torch.from_numpy(z_heat.astype(np.float32))).numpy() |
| heat_decoded = physical_bounds_clip(heat_decoded) |
| dt_mean = heat_decoded[:, 2].mean() - baseline_decoded[:, 2].mean() |
| print(f" Recalibrated: scale={heat_scale:.3f}, ΞT={dt_mean:+.2f}Β°C") |
|
|
| epw_heat = weather_to_epw( |
| heat_decoded, |
| scenario_name='Heatwave_plus2C', |
| description=f'Heatwave scenario: +{dt_mean:.1f}C sustained warming via VAE latent shift (scale={heat_scale:.3f})', |
| output_path=f'{EPW_DIR}/NUS_heatwave.epw', |
| station_id='NUS_HW_2025', |
| ) |
| print_scenario_stats('Heatwave', heat_decoded) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| print("\n" + "=" * 60) |
| print("SCENARIO 3: UHI INTENSIFICATION (+2Β°C, -30% wind)") |
| print("=" * 60) |
|
|
| |
| uhi_dir = directions['AirTemp'] - 0.5 * directions['WindSpeed'] |
| uhi_dir = uhi_dir / np.linalg.norm(uhi_dir) |
|
|
| |
| lo, hi = 0.1, 5.0 |
| for _ in range(20): |
| mid = (lo + hi) / 2 |
| z_test = campus_emb + mid * uhi_dir |
| with torch.no_grad(): |
| dec_test = model.decode(torch.from_numpy(z_test.astype(np.float32))).numpy() |
| dt_test = dec_test[:, 2].mean() - baseline_decoded[:, 2].mean() |
| if dt_test < 2.0: |
| lo = mid |
| else: |
| hi = mid |
| uhi_scale = (lo + hi) / 2 |
|
|
| z_uhi = campus_emb + uhi_scale * uhi_dir |
| with torch.no_grad(): |
| uhi_decoded = model.decode(torch.from_numpy(z_uhi.astype(np.float32))).numpy() |
| uhi_decoded = physical_bounds_clip(uhi_decoded) |
|
|
| dt_uhi = uhi_decoded[:, 2].mean() - baseline_decoded[:, 2].mean() |
| dws_uhi = uhi_decoded[:, 0].mean() - baseline_decoded[:, 0].mean() |
| ws_pct = dws_uhi / baseline_decoded[:, 0].mean() * 100 |
| print(f" Achieved: ΞT={dt_uhi:+.2f}Β°C, ΞWS={dws_uhi:+.3f} m/s ({ws_pct:+.0f}%)") |
|
|
| epw_uhi = weather_to_epw( |
| uhi_decoded, |
| scenario_name='UHI_Intensification', |
| description=f'Urban heat island intensification: +{dt_uhi:.1f}C warming, {ws_pct:.0f}% wind reduction via VAE latent shift (scale={uhi_scale:.3f})', |
| output_path=f'{EPW_DIR}/NUS_uhi.epw', |
| station_id='NUS_UHI_2025', |
| ) |
| print_scenario_stats('UHI', uhi_decoded) |
|
|
| |
| |
| |
| print("\n" + "=" * 60) |
| print("SCENARIO COMPARISON") |
| print("=" * 60) |
|
|
| scenarios = { |
| 'Baseline (imputed)': baseline_weather, |
| 'Baseline (VAE decoded)': baseline_decoded, |
| 'Heatwave (+2Β°C)': heat_decoded, |
| 'UHI Intensification': uhi_decoded, |
| } |
|
|
| print(f"\n{'Scenario':<25s} | {'AirTemp':>8s} {'RelHum':>8s} {'WindSpd':>8s} {'GloRad':>8s} {'Press':>8s}") |
| print("-" * 75) |
| for name, w in scenarios.items(): |
| print(f"{name:<25s} | {w[:, 2].mean():>7.1f}Β° {w[:, 3].mean():>7.1f}% " |
| f"{w[:, 0].mean():>7.2f}m {w[:, 5].mean():>7.1f}W {w[:, 4].mean():>7.1f}h") |
|
|
| |
| print(f"\n{'Scenario':<25s} | {'ΞTemp':>8s} {'ΞRH':>8s} {'ΞWind':>8s} {'ΞRad':>8s} {'ΞPress':>8s}") |
| print("-" * 75) |
| for name, w in [('Heatwave', heat_decoded), ('UHI', uhi_decoded)]: |
| dt = w[:, 2].mean() - baseline_decoded[:, 2].mean() |
| dr = w[:, 3].mean() - baseline_decoded[:, 3].mean() |
| dw = w[:, 0].mean() - baseline_decoded[:, 0].mean() |
| dg = w[:, 5].mean() - baseline_decoded[:, 5].mean() |
| dp = w[:, 4].mean() - baseline_decoded[:, 4].mean() |
| print(f"{name:<25s} | {dt:>+7.2f}Β° {dr:>+7.1f}% {dw:>+7.3f}m {dg:>+7.1f}W {dp:>+7.2f}h") |
|
|
| |
| hours = np.arange(8760) % 24 |
| print(f"\nDiurnal Temperature Profile:") |
| print(f"{'Hour':>6s} | {'Baseline':>10s} {'Heatwave':>10s} {'UHI':>10s} | {'ΞHW':>6s} {'ΞUHI':>6s}") |
| print("-" * 60) |
| for h in range(0, 24, 3): |
| mask = hours == h |
| b = baseline_decoded[mask, 2].mean() |
| hw = heat_decoded[mask, 2].mean() |
| uhi = uhi_decoded[mask, 2].mean() |
| print(f"{h:>6d} | {b:>10.2f} {hw:>10.2f} {uhi:>10.2f} | {hw-b:>+5.2f} {uhi-b:>+5.2f}") |
|
|
| |
| day_mask = (hours >= 8) & (hours <= 18) |
| night_mask = ~day_mask |
|
|
| print(f"\nDay vs Night warming (UHI should warm more at night):") |
| for name, w in [('Heatwave', heat_decoded), ('UHI', uhi_decoded)]: |
| day_dt = w[day_mask, 2].mean() - baseline_decoded[day_mask, 2].mean() |
| night_dt = w[night_mask, 2].mean() - baseline_decoded[night_mask, 2].mean() |
| print(f" {name}: Day ΞT={day_dt:+.2f}Β°C, Night ΞT={night_dt:+.2f}Β°C, " |
| f"Night-Day={night_dt - day_dt:+.2f}Β°C") |
|
|
| |
| results = { |
| 'scenarios': {}, |
| 'latent_directions': {name: d.tolist() for name, d in directions.items()}, |
| } |
| for sname, w, scale in [ |
| ('baseline_imputed', baseline_weather, None), |
| ('baseline_vae', baseline_decoded, None), |
| ('heatwave', heat_decoded, heat_scale), |
| ('uhi', uhi_decoded, uhi_scale), |
| ]: |
| stats = {} |
| for v, vname in enumerate(VAR_NAMES): |
| stats[vname] = { |
| 'mean': round(float(w[:, v].mean()), 3), |
| 'std': round(float(w[:, v].std()), 3), |
| 'min': round(float(w[:, v].min()), 3), |
| 'max': round(float(w[:, v].max()), 3), |
| } |
| if scale is not None: |
| stats['latent_scale'] = round(float(scale), 4) |
| results['scenarios'][sname] = stats |
|
|
| with open(f'{EPW_DIR}/scenario_results.json', 'w') as f: |
| json.dump(results, f, indent=2) |
| print(f"\nResults saved to {EPW_DIR}/scenario_results.json") |
|
|
| |
| print("\n" + "=" * 60) |
| print("EPW VALIDATION") |
| print("=" * 60) |
| validate_epw_files() |
|
|
| return results |
|
|
|
|
| def print_scenario_stats(name, weather): |
| """Print summary statistics for a scenario.""" |
| print(f"\n {name} annual statistics:") |
| for v, (vname, unit) in enumerate(zip(VAR_NAMES, VAR_UNITS)): |
| col = weather[:, v] |
| print(f" {vname:>12s}: mean={col.mean():.2f} std={col.std():.2f} " |
| f"min={col.min():.2f} max={col.max():.2f} {unit}") |
|
|
|
|
| def validate_epw_files(): |
| """Validate generated EPW files by reading them back.""" |
| for fname in ['NUS_baseline_2025.epw', 'NUS_heatwave.epw', 'NUS_uhi.epw']: |
| path = f'{EPW_DIR}/{fname}' |
| if not os.path.exists(path): |
| print(f" β {fname}: NOT FOUND") |
| continue |
|
|
| try: |
| epw = EPW(path) |
| ta = np.array(epw.dry_bulb_temperature.values) |
| rh = np.array(epw.relative_humidity.values) |
| ws = np.array(epw.wind_speed.values) |
| ghr = np.array(epw.global_horizontal_radiation.values) |
| dni = np.array(epw.direct_normal_radiation.values) |
| dhi = np.array(epw.diffuse_horizontal_radiation.values) |
|
|
| n_hours = len(ta) |
| ta_range = f"{ta.min():.1f}β{ta.max():.1f}Β°C" |
| rh_range = f"{rh.min():.0f}β{rh.max():.0f}%" |
|
|
| |
| issues = [] |
| if (ghr < -1).any(): issues.append("GHR < 0") |
| if (dni < -1).any(): issues.append("DNI < 0") |
| if (ta < -50).any() or (ta > 60).any(): issues.append("T out of range") |
| if (rh < 0).any() or (rh > 101).any(): issues.append("RH out of range") |
| if (ws < 0).any(): issues.append("WS < 0") |
|
|
| |
| night_ghr = ghr[np.arange(8760) % 24 < 6] |
| if night_ghr.max() > 10: |
| issues.append(f"GHR at night: max={night_ghr.max():.1f}") |
|
|
| |
| |
| day_mask = ghr > 50 |
| if day_mask.sum() > 100: |
| ghr_check = ghr[day_mask] |
| dni_check = dni[day_mask] |
| dhi_check = dhi[day_mask] |
| ratio = (dni_check + dhi_check) / np.maximum(ghr_check, 1) |
| |
|
|
| status = 'β' if not issues else f"β {', '.join(issues)}" |
| print(f" {status} {fname}: {n_hours} hours, T={ta_range}, RH={rh_range}, " |
| f"mean GHR={ghr.mean():.0f} W/mΒ²") |
|
|
| except Exception as e: |
| print(f" β {fname}: FAILED TO READ β {e}") |
|
|
|
|
| def make_scenario_figures(): |
| """Generate comparison figures for the three scenarios.""" |
| import matplotlib |
| matplotlib.use('Agg') |
| import matplotlib.pyplot as plt |
| import seaborn as sns |
|
|
| os.makedirs(FIG_DIR, exist_ok=True) |
| C = ['#264653', '#2a9d8f', '#e9c46a', '#e76f51'] |
| sns.set_theme(style='whitegrid', font_scale=1.05) |
|
|
| |
| epw_files = { |
| 'Baseline': f'{EPW_DIR}/NUS_baseline_2025.epw', |
| 'Heatwave (+2Β°C)': f'{EPW_DIR}/NUS_heatwave.epw', |
| 'UHI Intensification': f'{EPW_DIR}/NUS_uhi.epw', |
| } |
|
|
| all_data = {} |
| for name, path in epw_files.items(): |
| epw = EPW(path) |
| all_data[name] = { |
| 'AirTemp': np.array(epw.dry_bulb_temperature.values), |
| 'RelHum': np.array(epw.relative_humidity.values), |
| 'WindSpeed': np.array(epw.wind_speed.values), |
| 'GlobalRad': np.array(epw.global_horizontal_radiation.values), |
| 'DewPoint': np.array(epw.dew_point_temperature.values), |
| 'Pressure': np.array(epw.atmospheric_station_pressure.values) / 100, |
| } |
|
|
| hours = np.arange(8760) % 24 |
| months = np.array([(pd.Timestamp(f'{YEAR}-01-01') + pd.Timedelta(hours=h)).month for h in range(8760)]) |
|
|
| |
| fig, axes = plt.subplots(2, 2, figsize=(12, 9)) |
| colors = [C[0], C[3], C[1]] |
|
|
| for ax, var, ylabel in zip(axes.flat, |
| ['AirTemp', 'RelHum', 'WindSpeed', 'GlobalRad'], |
| ['Temperature (Β°C)', 'Relative Humidity (%)', 'Wind Speed (m/s)', 'Global Radiation (W/mΒ²)']): |
| for (name, d), color in zip(all_data.items(), colors): |
| diurnal = [d[var][hours == h].mean() for h in range(24)] |
| ax.plot(range(24), diurnal, color=color, lw=2, label=name) |
| ax.set_xlabel('Hour of day') |
| ax.set_ylabel(ylabel) |
| ax.set_xlim(0, 23) |
| ax.set_xticks([0, 3, 6, 9, 12, 15, 18, 21]) |
| ax.legend(fontsize=8) |
|
|
| axes[0, 0].set_title('(a) Air temperature') |
| axes[0, 1].set_title('(b) Relative humidity') |
| axes[1, 0].set_title('(c) Wind speed') |
| axes[1, 1].set_title('(d) Global solar radiation') |
|
|
| plt.suptitle('Diurnal Profiles: Baseline vs Extreme Scenarios', fontsize=13, y=1.01) |
| plt.tight_layout() |
| for ext in ['png', 'pdf']: |
| fig.savefig(f'{FIG_DIR}/fig14_epw_diurnal.{ext}', dpi=300, bbox_inches='tight') |
| plt.close() |
| print(" Saved fig14_epw_diurnal") |
|
|
| |
| fig, axes = plt.subplots(1, 3, figsize=(15, 4.5), sharey=True) |
|
|
| for ax, (name, d), color in zip(axes, all_data.items(), colors): |
| month_data = [d['AirTemp'][months == m] for m in range(1, 13)] |
| bp = ax.boxplot(month_data, patch_artist=True, widths=0.6, |
| medianprops=dict(color='black', lw=1.5)) |
| for patch in bp['boxes']: |
| patch.set_facecolor(color) |
| patch.set_alpha(0.6) |
| ax.set_xlabel('Month') |
| ax.set_title(name) |
| ax.set_xticklabels(['J','F','M','A','M','J','J','A','S','O','N','D']) |
|
|
| axes[0].set_ylabel('Air Temperature (Β°C)') |
| plt.suptitle('Monthly Temperature Distributions', fontsize=13, y=1.02) |
| plt.tight_layout() |
| for ext in ['png', 'pdf']: |
| fig.savefig(f'{FIG_DIR}/fig15_epw_monthly.{ext}', dpi=300, bbox_inches='tight') |
| plt.close() |
| print(" Saved fig15_epw_monthly") |
|
|
| |
| fig, ax = plt.subplots(figsize=(8, 6)) |
|
|
| rng = np.random.RandomState(42) |
| n_sample = 2000 |
| for (name, d), color, marker in zip(all_data.items(), colors, ['o', '^', 's']): |
| idx = rng.choice(8760, n_sample, replace=False) |
| ax.scatter(d['AirTemp'][idx], d['RelHum'][idx], |
| c=color, alpha=0.15, s=10, marker=marker, label=name, rasterized=True) |
|
|
| ax.set_xlabel('Air Temperature (Β°C)') |
| ax.set_ylabel('Relative Humidity (%)') |
| ax.set_title('TemperatureβHumidity State Space') |
| ax.legend(markerscale=3, fontsize=10) |
| ax.set_xlim(20, 42) |
| ax.set_ylim(25, 100) |
|
|
| plt.tight_layout() |
| for ext in ['png', 'pdf']: |
| fig.savefig(f'{FIG_DIR}/fig16_epw_psychrometric.{ext}', dpi=300, bbox_inches='tight') |
| plt.close() |
| print(" Saved fig16_epw_psychrometric") |
|
|
|
|
| |
|
|
| if __name__ == '__main__': |
| results = generate_scenarios() |
| print("\n" + "=" * 60) |
| print("GENERATING FIGURES...") |
| print("=" * 60) |
| make_scenario_figures() |
| print("\nDone.") |
|
|