Add EPW scenario generation: code/generate_epw.py
Browse files- code/generate_epw.py +611 -0
code/generate_epw.py
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
EPW Weather File Generation from VAE Embeddings
|
| 3 |
+
================================================
|
| 4 |
+
Generates EnergyPlus Weather (EPW) files for building energy simulation:
|
| 5 |
+
|
| 6 |
+
1. BASELINE: Campus-mean weather from imputed observations (2025)
|
| 7 |
+
2. HEATWAVE: +2Β°C sustained warming via latent space manipulation
|
| 8 |
+
3. UHI INTENSIFICATION: +2Β°C warming with reduced wind (urban canyon effect)
|
| 9 |
+
|
| 10 |
+
The VAE's continuous latent space enables generation of physically coherent
|
| 11 |
+
extreme scenarios where all six weather variables shift together consistently β
|
| 12 |
+
something classical interpolation methods (IDW, kriging) cannot do.
|
| 13 |
+
|
| 14 |
+
Run: python generate_epw.py
|
| 15 |
+
Outputs: epw/NUS_baseline_2025.epw, epw/NUS_heatwave.epw, epw/NUS_uhi.epw
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import sys, os, json, warnings
|
| 19 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 20 |
+
warnings.filterwarnings('ignore')
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
import pandas as pd
|
| 24 |
+
import pvlib
|
| 25 |
+
import torch
|
| 26 |
+
from sklearn.linear_model import LinearRegression
|
| 27 |
+
from ladybug.epw import EPW
|
| 28 |
+
from ladybug.location import Location
|
| 29 |
+
|
| 30 |
+
from model import WeatherVAE
|
| 31 |
+
from train import load_nus40, VAR_NAMES, VAR_UNITS
|
| 32 |
+
|
| 33 |
+
# ββ Paths ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
+
BASE = '/app/campus_weather'
|
| 35 |
+
DATA_DIR = f'{BASE}/imputed'
|
| 36 |
+
RESULTS = f'{BASE}/results'
|
| 37 |
+
EPW_DIR = f'{BASE}/epw'
|
| 38 |
+
FIG_DIR = f'{BASE}/figures'
|
| 39 |
+
|
| 40 |
+
# ββ Campus location ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 41 |
+
CAMPUS_LAT = 1.2992
|
| 42 |
+
CAMPUS_LON = 103.7764
|
| 43 |
+
CAMPUS_ALT = 16 # metres ASL
|
| 44 |
+
TZ_OFFSET = 8 # UTC+8
|
| 45 |
+
TZ_STR = 'Asia/Singapore'
|
| 46 |
+
YEAR = 2025
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 50 |
+
# DERIVED FIELD COMPUTATIONS
|
| 51 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 52 |
+
|
| 53 |
+
def dewpoint_magnus(T_C, RH_pct):
|
| 54 |
+
"""Dew point temperature via Magnus formula. T in Β°C, RH in %."""
|
| 55 |
+
RH_safe = np.clip(RH_pct, 1, 100)
|
| 56 |
+
a, b = 17.27, 237.7
|
| 57 |
+
gamma = a * T_C / (b + T_C) + np.log(RH_safe / 100.0)
|
| 58 |
+
return b * gamma / (a - gamma)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def horizontal_infrared_radiation(T_C, RH_pct):
|
| 62 |
+
"""
|
| 63 |
+
Horizontal infrared radiation intensity (W/mΒ²).
|
| 64 |
+
Martin-Berdahl model using dew point temperature.
|
| 65 |
+
"""
|
| 66 |
+
sigma = 5.67e-8
|
| 67 |
+
T_K = T_C + 273.15
|
| 68 |
+
Tdp_K = dewpoint_magnus(T_C, RH_pct) + 273.15
|
| 69 |
+
eps_sky = np.clip(0.787 + 0.764 * np.log(Tdp_K / 273.16), 0.6, 1.0)
|
| 70 |
+
return eps_sky * sigma * T_K**4
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def compute_solar_derived(ghr, year=YEAR):
|
| 74 |
+
"""
|
| 75 |
+
From Global Horizontal Radiation (W/mΒ²), compute:
|
| 76 |
+
- DNI (Direct Normal Irradiance)
|
| 77 |
+
- DHI (Diffuse Horizontal Irradiance)
|
| 78 |
+
- Extraterrestrial radiation
|
| 79 |
+
- Sky cover (tenths)
|
| 80 |
+
|
| 81 |
+
All returned in Wh/mΒ² (numerically equal to W/mΒ² for hourly averages).
|
| 82 |
+
"""
|
| 83 |
+
times = pd.date_range(f'{year}-01-01', periods=8760, freq='1h', tz=TZ_STR)
|
| 84 |
+
solpos = pvlib.solarposition.get_solarposition(times, CAMPUS_LAT, CAMPUS_LON, CAMPUS_ALT)
|
| 85 |
+
zenith = solpos['apparent_zenith'].values
|
| 86 |
+
|
| 87 |
+
ghr_clean = np.clip(ghr, 0, 1400)
|
| 88 |
+
# Zero out nighttime
|
| 89 |
+
ghr_clean[zenith > 90] = 0
|
| 90 |
+
|
| 91 |
+
decomp = pvlib.irradiance.erbs(ghr_clean, zenith, times)
|
| 92 |
+
dni = np.nan_to_num(decomp['dni'].values, nan=0.0).clip(0, 1200)
|
| 93 |
+
dhi = np.nan_to_num(decomp['dhi'].values, nan=0.0).clip(0, 800)
|
| 94 |
+
|
| 95 |
+
# Extraterrestrial
|
| 96 |
+
etrn = pvlib.irradiance.get_extra_radiation(times).values # Direct normal
|
| 97 |
+
etr = (etrn * np.cos(np.radians(zenith))).clip(0) # Horizontal
|
| 98 |
+
|
| 99 |
+
# Sky cover from clearness index
|
| 100 |
+
kt = np.where(etr > 10, ghr_clean / etr, 0).clip(0, 1)
|
| 101 |
+
sky_cover = ((1 - kt) * 10).clip(0, 10).astype(int)
|
| 102 |
+
|
| 103 |
+
return {
|
| 104 |
+
'dni': dni, 'dhi': dhi,
|
| 105 |
+
'etrn': etrn, 'etr': etr,
|
| 106 |
+
'sky_cover': sky_cover, 'zenith': zenith,
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def physical_bounds_clip(weather):
|
| 111 |
+
"""Clip decoded weather to physical bounds."""
|
| 112 |
+
weather[:, 0] = np.clip(weather[:, 0], 0, 20) # WindSpeed: 0-20 m/s
|
| 113 |
+
weather[:, 1] = np.clip(weather[:, 1], 0, 360) # WindDir: 0-360Β°
|
| 114 |
+
weather[:, 2] = np.clip(weather[:, 2], 15, 50) # AirTemp: 15-50Β°C
|
| 115 |
+
weather[:, 3] = np.clip(weather[:, 3], 10, 100) # RelHum: 10-100%
|
| 116 |
+
weather[:, 4] = np.clip(weather[:, 4], 990, 1020) # AtmPress: 990-1020 hPa
|
| 117 |
+
weather[:, 5] = np.clip(weather[:, 5], 0, 1400) # GlobalRad: 0-1400 W/mΒ²
|
| 118 |
+
return weather
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 122 |
+
# EPW FILE GENERATION
|
| 123 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 124 |
+
|
| 125 |
+
def weather_to_epw(weather, scenario_name, description, output_path, station_id='NUS_VAE'):
|
| 126 |
+
"""
|
| 127 |
+
Convert a (8760, 6) weather array to a valid EPW file.
|
| 128 |
+
|
| 129 |
+
weather columns: [WindSpeed, WindDir, AirTemp, RelHum, AtmPress, GlobalRad]
|
| 130 |
+
"""
|
| 131 |
+
ws = weather[:, 0] # m/s
|
| 132 |
+
wd = weather[:, 1] # degrees
|
| 133 |
+
ta = weather[:, 2] # Β°C
|
| 134 |
+
rh = weather[:, 3] # %
|
| 135 |
+
pa = weather[:, 4] * 100 # hPa β Pa (EPW uses Pa)
|
| 136 |
+
ghr = weather[:, 5] # W/mΒ² = Wh/mΒ² for hourly
|
| 137 |
+
|
| 138 |
+
# Derived fields
|
| 139 |
+
dp = dewpoint_magnus(ta, rh)
|
| 140 |
+
hiri = horizontal_infrared_radiation(ta, rh)
|
| 141 |
+
solar = compute_solar_derived(ghr)
|
| 142 |
+
|
| 143 |
+
# Build EPW
|
| 144 |
+
epw = EPW.from_missing_values()
|
| 145 |
+
epw.location = Location(
|
| 146 |
+
city='NUS_Campus', state='', country='SGP',
|
| 147 |
+
source=f'VAE_{scenario_name}', station_id=station_id,
|
| 148 |
+
latitude=CAMPUS_LAT, longitude=CAMPUS_LON,
|
| 149 |
+
time_zone=TZ_OFFSET, elevation=CAMPUS_ALT
|
| 150 |
+
)
|
| 151 |
+
epw.comments_1 = f'VAE-generated EPW: {scenario_name}'
|
| 152 |
+
epw.comments_2 = description
|
| 153 |
+
|
| 154 |
+
# Assign fields (all 8760-length lists)
|
| 155 |
+
epw.years.values = [YEAR] * 8760
|
| 156 |
+
epw.dry_bulb_temperature.values = ta.tolist()
|
| 157 |
+
epw.dew_point_temperature.values = dp.tolist()
|
| 158 |
+
epw.relative_humidity.values = rh.tolist()
|
| 159 |
+
epw.atmospheric_station_pressure.values = pa.tolist()
|
| 160 |
+
epw.wind_speed.values = ws.tolist()
|
| 161 |
+
epw.wind_direction.values = wd.tolist()
|
| 162 |
+
epw.global_horizontal_radiation.values = ghr.tolist()
|
| 163 |
+
epw.direct_normal_radiation.values = solar['dni'].tolist()
|
| 164 |
+
epw.diffuse_horizontal_radiation.values = solar['dhi'].tolist()
|
| 165 |
+
epw.horizontal_infrared_radiation_intensity.values = hiri.tolist()
|
| 166 |
+
epw.extraterrestrial_horizontal_radiation.values = solar['etr'].tolist()
|
| 167 |
+
epw.extraterrestrial_direct_normal_radiation.values = solar['etrn'].tolist()
|
| 168 |
+
epw.total_sky_cover.values = solar['sky_cover'].tolist()
|
| 169 |
+
epw.opaque_sky_cover.values = solar['sky_cover'].tolist()
|
| 170 |
+
|
| 171 |
+
# Singapore-specific fixed fields
|
| 172 |
+
epw.snow_depth.values = [0] * 8760
|
| 173 |
+
epw.days_since_last_snowfall.values = [99] * 8760
|
| 174 |
+
epw.albedo.values = [0.15] * 8760 # typical urban
|
| 175 |
+
|
| 176 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 177 |
+
epw.save(output_path)
|
| 178 |
+
return epw
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 182 |
+
# SCENARIO GENERATION
|
| 183 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 184 |
+
|
| 185 |
+
def generate_scenarios():
|
| 186 |
+
"""Generate all three EPW scenarios."""
|
| 187 |
+
os.makedirs(EPW_DIR, exist_ok=True)
|
| 188 |
+
|
| 189 |
+
# Load data and model
|
| 190 |
+
print("Loading data and model...")
|
| 191 |
+
data, coords, datetimes = load_nus40(DATA_DIR)
|
| 192 |
+
T, N, V = data.shape
|
| 193 |
+
|
| 194 |
+
ckpt = torch.load(f'{RESULTS}/checkpoints/best.pt', map_location='cpu', weights_only=False)
|
| 195 |
+
model = WeatherVAE(**ckpt['config'])
|
| 196 |
+
model.load_state_dict(ckpt['model'])
|
| 197 |
+
model.set_normalisation(ckpt['mean'], ckpt['std'])
|
| 198 |
+
model.eval()
|
| 199 |
+
|
| 200 |
+
emb = np.load(f'{RESULTS}/checkpoints/embeddings.npz')['embeddings']
|
| 201 |
+
campus_emb = emb.mean(axis=1) # (8760, 6) β campus-mean embedding per hour
|
| 202 |
+
campus_data = data.mean(axis=1) # (8760, 6) β campus-mean weather per hour
|
| 203 |
+
|
| 204 |
+
# ββ Find latent directions via linear regression βββββββββββββββββ
|
| 205 |
+
print("Computing latent directions...")
|
| 206 |
+
directions = {}
|
| 207 |
+
for v, name in enumerate(VAR_NAMES):
|
| 208 |
+
reg = LinearRegression()
|
| 209 |
+
reg.fit(campus_emb, campus_data[:, v])
|
| 210 |
+
w = reg.coef_
|
| 211 |
+
directions[name] = w / np.linalg.norm(w)
|
| 212 |
+
|
| 213 |
+
# ββ Decode baseline to get the VAE's reconstruction ββββββββββββββ
|
| 214 |
+
with torch.no_grad():
|
| 215 |
+
baseline_decoded = model.decode(
|
| 216 |
+
torch.from_numpy(campus_emb.astype(np.float32))
|
| 217 |
+
).numpy()
|
| 218 |
+
baseline_decoded = physical_bounds_clip(baseline_decoded)
|
| 219 |
+
|
| 220 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
+
# SCENARIO 1: BASELINE
|
| 222 |
+
# Campus-mean imputed observations β the actual 2025 weather
|
| 223 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 224 |
+
print("\n" + "=" * 60)
|
| 225 |
+
print("SCENARIO 1: BASELINE (imputed observations)")
|
| 226 |
+
print("=" * 60)
|
| 227 |
+
|
| 228 |
+
baseline_weather = campus_data.copy()
|
| 229 |
+
epw_baseline = weather_to_epw(
|
| 230 |
+
baseline_weather,
|
| 231 |
+
scenario_name='Baseline_2025',
|
| 232 |
+
description='Campus-mean of 40 NUS stations, imputed observations, Jan-Dec 2025',
|
| 233 |
+
output_path=f'{EPW_DIR}/NUS_baseline_2025.epw',
|
| 234 |
+
station_id='NUS_BAS_2025',
|
| 235 |
+
)
|
| 236 |
+
print_scenario_stats('Baseline', baseline_weather)
|
| 237 |
+
|
| 238 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 239 |
+
# SCENARIO 2: HEATWAVE (+2Β°C)
|
| 240 |
+
# Shift the entire year's embeddings along the temperature
|
| 241 |
+
# direction in latent space, then decode. This produces a
|
| 242 |
+
# physically coherent heatwave where humidity drops, radiation
|
| 243 |
+
# increases, and pressure decreases β all simultaneously.
|
| 244 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 245 |
+
print("\n" + "=" * 60)
|
| 246 |
+
print("SCENARIO 2: HEATWAVE (+2Β°C via latent shift)")
|
| 247 |
+
print("=" * 60)
|
| 248 |
+
|
| 249 |
+
# Calibrate: scale=0.75 in AirTemp direction β ~+2Β°C
|
| 250 |
+
# Verified empirically above
|
| 251 |
+
heat_scale = 0.75
|
| 252 |
+
z_heat = campus_emb + heat_scale * directions['AirTemp']
|
| 253 |
+
|
| 254 |
+
with torch.no_grad():
|
| 255 |
+
heat_decoded = model.decode(
|
| 256 |
+
torch.from_numpy(z_heat.astype(np.float32))
|
| 257 |
+
).numpy()
|
| 258 |
+
heat_decoded = physical_bounds_clip(heat_decoded)
|
| 259 |
+
|
| 260 |
+
# Verify the shift magnitude
|
| 261 |
+
dt_mean = heat_decoded[:, 2].mean() - baseline_decoded[:, 2].mean()
|
| 262 |
+
print(f" Achieved mean ΞT: {dt_mean:+.2f}Β°C (target: +2Β°C)")
|
| 263 |
+
|
| 264 |
+
# If not close enough, adjust scale
|
| 265 |
+
if abs(dt_mean - 2.0) > 0.3:
|
| 266 |
+
# Binary search for correct scale
|
| 267 |
+
lo, hi = 0.1, 3.0
|
| 268 |
+
for _ in range(20):
|
| 269 |
+
mid = (lo + hi) / 2
|
| 270 |
+
z_test = campus_emb + mid * directions['AirTemp']
|
| 271 |
+
with torch.no_grad():
|
| 272 |
+
dec_test = model.decode(torch.from_numpy(z_test.astype(np.float32))).numpy()
|
| 273 |
+
dt_test = dec_test[:, 2].mean() - baseline_decoded[:, 2].mean()
|
| 274 |
+
if dt_test < 2.0:
|
| 275 |
+
lo = mid
|
| 276 |
+
else:
|
| 277 |
+
hi = mid
|
| 278 |
+
heat_scale = (lo + hi) / 2
|
| 279 |
+
z_heat = campus_emb + heat_scale * directions['AirTemp']
|
| 280 |
+
with torch.no_grad():
|
| 281 |
+
heat_decoded = model.decode(torch.from_numpy(z_heat.astype(np.float32))).numpy()
|
| 282 |
+
heat_decoded = physical_bounds_clip(heat_decoded)
|
| 283 |
+
dt_mean = heat_decoded[:, 2].mean() - baseline_decoded[:, 2].mean()
|
| 284 |
+
print(f" Recalibrated: scale={heat_scale:.3f}, ΞT={dt_mean:+.2f}Β°C")
|
| 285 |
+
|
| 286 |
+
epw_heat = weather_to_epw(
|
| 287 |
+
heat_decoded,
|
| 288 |
+
scenario_name='Heatwave_plus2C',
|
| 289 |
+
description=f'Heatwave scenario: +{dt_mean:.1f}C sustained warming via VAE latent shift (scale={heat_scale:.3f})',
|
| 290 |
+
output_path=f'{EPW_DIR}/NUS_heatwave.epw',
|
| 291 |
+
station_id='NUS_HW_2025',
|
| 292 |
+
)
|
| 293 |
+
print_scenario_stats('Heatwave', heat_decoded)
|
| 294 |
+
|
| 295 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 296 |
+
# SCENARIO 3: URBAN HEAT ISLAND INTENSIFICATION
|
| 297 |
+
# Combined shift: temperature up AND wind speed down.
|
| 298 |
+
# This represents the effect of increased building density
|
| 299 |
+
# reducing ventilation corridors while trapping more heat.
|
| 300 |
+
# Physically distinct from a heatwave: UHI warming is strongest
|
| 301 |
+
# at night (reduced nocturnal cooling), whereas heatwaves
|
| 302 |
+
# affect daytime peaks.
|
| 303 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 304 |
+
print("\n" + "=" * 60)
|
| 305 |
+
print("SCENARIO 3: UHI INTENSIFICATION (+2Β°C, -30% wind)")
|
| 306 |
+
print("=" * 60)
|
| 307 |
+
|
| 308 |
+
# Combine: push temperature up + push wind down
|
| 309 |
+
uhi_dir = directions['AirTemp'] - 0.5 * directions['WindSpeed']
|
| 310 |
+
uhi_dir = uhi_dir / np.linalg.norm(uhi_dir)
|
| 311 |
+
|
| 312 |
+
# Calibrate for +2Β°C
|
| 313 |
+
lo, hi = 0.1, 5.0
|
| 314 |
+
for _ in range(20):
|
| 315 |
+
mid = (lo + hi) / 2
|
| 316 |
+
z_test = campus_emb + mid * uhi_dir
|
| 317 |
+
with torch.no_grad():
|
| 318 |
+
dec_test = model.decode(torch.from_numpy(z_test.astype(np.float32))).numpy()
|
| 319 |
+
dt_test = dec_test[:, 2].mean() - baseline_decoded[:, 2].mean()
|
| 320 |
+
if dt_test < 2.0:
|
| 321 |
+
lo = mid
|
| 322 |
+
else:
|
| 323 |
+
hi = mid
|
| 324 |
+
uhi_scale = (lo + hi) / 2
|
| 325 |
+
|
| 326 |
+
z_uhi = campus_emb + uhi_scale * uhi_dir
|
| 327 |
+
with torch.no_grad():
|
| 328 |
+
uhi_decoded = model.decode(torch.from_numpy(z_uhi.astype(np.float32))).numpy()
|
| 329 |
+
uhi_decoded = physical_bounds_clip(uhi_decoded)
|
| 330 |
+
|
| 331 |
+
dt_uhi = uhi_decoded[:, 2].mean() - baseline_decoded[:, 2].mean()
|
| 332 |
+
dws_uhi = uhi_decoded[:, 0].mean() - baseline_decoded[:, 0].mean()
|
| 333 |
+
ws_pct = dws_uhi / baseline_decoded[:, 0].mean() * 100
|
| 334 |
+
print(f" Achieved: ΞT={dt_uhi:+.2f}Β°C, ΞWS={dws_uhi:+.3f} m/s ({ws_pct:+.0f}%)")
|
| 335 |
+
|
| 336 |
+
epw_uhi = weather_to_epw(
|
| 337 |
+
uhi_decoded,
|
| 338 |
+
scenario_name='UHI_Intensification',
|
| 339 |
+
description=f'Urban heat island intensification: +{dt_uhi:.1f}C warming, {ws_pct:.0f}% wind reduction via VAE latent shift (scale={uhi_scale:.3f})',
|
| 340 |
+
output_path=f'{EPW_DIR}/NUS_uhi.epw',
|
| 341 |
+
station_id='NUS_UHI_2025',
|
| 342 |
+
)
|
| 343 |
+
print_scenario_stats('UHI', uhi_decoded)
|
| 344 |
+
|
| 345 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 346 |
+
# COMPARISON STATISTICS
|
| 347 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 348 |
+
print("\n" + "=" * 60)
|
| 349 |
+
print("SCENARIO COMPARISON")
|
| 350 |
+
print("=" * 60)
|
| 351 |
+
|
| 352 |
+
scenarios = {
|
| 353 |
+
'Baseline (imputed)': baseline_weather,
|
| 354 |
+
'Baseline (VAE decoded)': baseline_decoded,
|
| 355 |
+
'Heatwave (+2Β°C)': heat_decoded,
|
| 356 |
+
'UHI Intensification': uhi_decoded,
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
print(f"\n{'Scenario':<25s} | {'AirTemp':>8s} {'RelHum':>8s} {'WindSpd':>8s} {'GloRad':>8s} {'Press':>8s}")
|
| 360 |
+
print("-" * 75)
|
| 361 |
+
for name, w in scenarios.items():
|
| 362 |
+
print(f"{name:<25s} | {w[:, 2].mean():>7.1f}Β° {w[:, 3].mean():>7.1f}% "
|
| 363 |
+
f"{w[:, 0].mean():>7.2f}m {w[:, 5].mean():>7.1f}W {w[:, 4].mean():>7.1f}h")
|
| 364 |
+
|
| 365 |
+
# Deltas from baseline
|
| 366 |
+
print(f"\n{'Scenario':<25s} | {'ΞTemp':>8s} {'ΞRH':>8s} {'ΞWind':>8s} {'ΞRad':>8s} {'ΞPress':>8s}")
|
| 367 |
+
print("-" * 75)
|
| 368 |
+
for name, w in [('Heatwave', heat_decoded), ('UHI', uhi_decoded)]:
|
| 369 |
+
dt = w[:, 2].mean() - baseline_decoded[:, 2].mean()
|
| 370 |
+
dr = w[:, 3].mean() - baseline_decoded[:, 3].mean()
|
| 371 |
+
dw = w[:, 0].mean() - baseline_decoded[:, 0].mean()
|
| 372 |
+
dg = w[:, 5].mean() - baseline_decoded[:, 5].mean()
|
| 373 |
+
dp = w[:, 4].mean() - baseline_decoded[:, 4].mean()
|
| 374 |
+
print(f"{name:<25s} | {dt:>+7.2f}Β° {dr:>+7.1f}% {dw:>+7.3f}m {dg:>+7.1f}W {dp:>+7.2f}h")
|
| 375 |
+
|
| 376 |
+
# Diurnal profiles
|
| 377 |
+
hours = np.arange(8760) % 24
|
| 378 |
+
print(f"\nDiurnal Temperature Profile:")
|
| 379 |
+
print(f"{'Hour':>6s} | {'Baseline':>10s} {'Heatwave':>10s} {'UHI':>10s} | {'ΞHW':>6s} {'ΞUHI':>6s}")
|
| 380 |
+
print("-" * 60)
|
| 381 |
+
for h in range(0, 24, 3):
|
| 382 |
+
mask = hours == h
|
| 383 |
+
b = baseline_decoded[mask, 2].mean()
|
| 384 |
+
hw = heat_decoded[mask, 2].mean()
|
| 385 |
+
uhi = uhi_decoded[mask, 2].mean()
|
| 386 |
+
print(f"{h:>6d} | {b:>10.2f} {hw:>10.2f} {uhi:>10.2f} | {hw-b:>+5.2f} {uhi-b:>+5.2f}")
|
| 387 |
+
|
| 388 |
+
# Nighttime vs daytime warming (UHI signature check)
|
| 389 |
+
day_mask = (hours >= 8) & (hours <= 18)
|
| 390 |
+
night_mask = ~day_mask
|
| 391 |
+
|
| 392 |
+
print(f"\nDay vs Night warming (UHI should warm more at night):")
|
| 393 |
+
for name, w in [('Heatwave', heat_decoded), ('UHI', uhi_decoded)]:
|
| 394 |
+
day_dt = w[day_mask, 2].mean() - baseline_decoded[day_mask, 2].mean()
|
| 395 |
+
night_dt = w[night_mask, 2].mean() - baseline_decoded[night_mask, 2].mean()
|
| 396 |
+
print(f" {name}: Day ΞT={day_dt:+.2f}Β°C, Night ΞT={night_dt:+.2f}Β°C, "
|
| 397 |
+
f"Night-Day={night_dt - day_dt:+.2f}Β°C")
|
| 398 |
+
|
| 399 |
+
# Save scenario metadata
|
| 400 |
+
results = {
|
| 401 |
+
'scenarios': {},
|
| 402 |
+
'latent_directions': {name: d.tolist() for name, d in directions.items()},
|
| 403 |
+
}
|
| 404 |
+
for sname, w, scale in [
|
| 405 |
+
('baseline_imputed', baseline_weather, None),
|
| 406 |
+
('baseline_vae', baseline_decoded, None),
|
| 407 |
+
('heatwave', heat_decoded, heat_scale),
|
| 408 |
+
('uhi', uhi_decoded, uhi_scale),
|
| 409 |
+
]:
|
| 410 |
+
stats = {}
|
| 411 |
+
for v, vname in enumerate(VAR_NAMES):
|
| 412 |
+
stats[vname] = {
|
| 413 |
+
'mean': round(float(w[:, v].mean()), 3),
|
| 414 |
+
'std': round(float(w[:, v].std()), 3),
|
| 415 |
+
'min': round(float(w[:, v].min()), 3),
|
| 416 |
+
'max': round(float(w[:, v].max()), 3),
|
| 417 |
+
}
|
| 418 |
+
if scale is not None:
|
| 419 |
+
stats['latent_scale'] = round(float(scale), 4)
|
| 420 |
+
results['scenarios'][sname] = stats
|
| 421 |
+
|
| 422 |
+
with open(f'{EPW_DIR}/scenario_results.json', 'w') as f:
|
| 423 |
+
json.dump(results, f, indent=2)
|
| 424 |
+
print(f"\nResults saved to {EPW_DIR}/scenario_results.json")
|
| 425 |
+
|
| 426 |
+
# Validate EPW files
|
| 427 |
+
print("\n" + "=" * 60)
|
| 428 |
+
print("EPW VALIDATION")
|
| 429 |
+
print("=" * 60)
|
| 430 |
+
validate_epw_files()
|
| 431 |
+
|
| 432 |
+
return results
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def print_scenario_stats(name, weather):
|
| 436 |
+
"""Print summary statistics for a scenario."""
|
| 437 |
+
print(f"\n {name} annual statistics:")
|
| 438 |
+
for v, (vname, unit) in enumerate(zip(VAR_NAMES, VAR_UNITS)):
|
| 439 |
+
col = weather[:, v]
|
| 440 |
+
print(f" {vname:>12s}: mean={col.mean():.2f} std={col.std():.2f} "
|
| 441 |
+
f"min={col.min():.2f} max={col.max():.2f} {unit}")
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def validate_epw_files():
|
| 445 |
+
"""Validate generated EPW files by reading them back."""
|
| 446 |
+
for fname in ['NUS_baseline_2025.epw', 'NUS_heatwave.epw', 'NUS_uhi.epw']:
|
| 447 |
+
path = f'{EPW_DIR}/{fname}'
|
| 448 |
+
if not os.path.exists(path):
|
| 449 |
+
print(f" β {fname}: NOT FOUND")
|
| 450 |
+
continue
|
| 451 |
+
|
| 452 |
+
try:
|
| 453 |
+
epw = EPW(path)
|
| 454 |
+
ta = np.array(epw.dry_bulb_temperature.values)
|
| 455 |
+
rh = np.array(epw.relative_humidity.values)
|
| 456 |
+
ws = np.array(epw.wind_speed.values)
|
| 457 |
+
ghr = np.array(epw.global_horizontal_radiation.values)
|
| 458 |
+
dni = np.array(epw.direct_normal_radiation.values)
|
| 459 |
+
dhi = np.array(epw.diffuse_horizontal_radiation.values)
|
| 460 |
+
|
| 461 |
+
n_hours = len(ta)
|
| 462 |
+
ta_range = f"{ta.min():.1f}β{ta.max():.1f}Β°C"
|
| 463 |
+
rh_range = f"{rh.min():.0f}β{rh.max():.0f}%"
|
| 464 |
+
|
| 465 |
+
# Physical checks
|
| 466 |
+
issues = []
|
| 467 |
+
if (ghr < -1).any(): issues.append("GHR < 0")
|
| 468 |
+
if (dni < -1).any(): issues.append("DNI < 0")
|
| 469 |
+
if (ta < -50).any() or (ta > 60).any(): issues.append("T out of range")
|
| 470 |
+
if (rh < 0).any() or (rh > 101).any(): issues.append("RH out of range")
|
| 471 |
+
if (ws < 0).any(): issues.append("WS < 0")
|
| 472 |
+
|
| 473 |
+
# Check GHR = 0 at night (approximately)
|
| 474 |
+
night_ghr = ghr[np.arange(8760) % 24 < 6] # midnight to 6am
|
| 475 |
+
if night_ghr.max() > 10:
|
| 476 |
+
issues.append(f"GHR at night: max={night_ghr.max():.1f}")
|
| 477 |
+
|
| 478 |
+
# Check DNI + DHI consistency (DNI*cos(z) + DHI β GHR)
|
| 479 |
+
# Only check during daytime
|
| 480 |
+
day_mask = ghr > 50
|
| 481 |
+
if day_mask.sum() > 100:
|
| 482 |
+
ghr_check = ghr[day_mask]
|
| 483 |
+
dni_check = dni[day_mask]
|
| 484 |
+
dhi_check = dhi[day_mask]
|
| 485 |
+
ratio = (dni_check + dhi_check) / np.maximum(ghr_check, 1)
|
| 486 |
+
# DNI here is direct normal, not horizontal. Skip strict check.
|
| 487 |
+
|
| 488 |
+
status = 'β' if not issues else f"β {', '.join(issues)}"
|
| 489 |
+
print(f" {status} {fname}: {n_hours} hours, T={ta_range}, RH={rh_range}, "
|
| 490 |
+
f"mean GHR={ghr.mean():.0f} W/mΒ²")
|
| 491 |
+
|
| 492 |
+
except Exception as e:
|
| 493 |
+
print(f" β {fname}: FAILED TO READ β {e}")
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
def make_scenario_figures():
|
| 497 |
+
"""Generate comparison figures for the three scenarios."""
|
| 498 |
+
import matplotlib
|
| 499 |
+
matplotlib.use('Agg')
|
| 500 |
+
import matplotlib.pyplot as plt
|
| 501 |
+
import seaborn as sns
|
| 502 |
+
|
| 503 |
+
os.makedirs(FIG_DIR, exist_ok=True)
|
| 504 |
+
C = ['#264653', '#2a9d8f', '#e9c46a', '#e76f51']
|
| 505 |
+
sns.set_theme(style='whitegrid', font_scale=1.05)
|
| 506 |
+
|
| 507 |
+
# Load the three EPW files
|
| 508 |
+
epw_files = {
|
| 509 |
+
'Baseline': f'{EPW_DIR}/NUS_baseline_2025.epw',
|
| 510 |
+
'Heatwave (+2Β°C)': f'{EPW_DIR}/NUS_heatwave.epw',
|
| 511 |
+
'UHI Intensification': f'{EPW_DIR}/NUS_uhi.epw',
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
all_data = {}
|
| 515 |
+
for name, path in epw_files.items():
|
| 516 |
+
epw = EPW(path)
|
| 517 |
+
all_data[name] = {
|
| 518 |
+
'AirTemp': np.array(epw.dry_bulb_temperature.values),
|
| 519 |
+
'RelHum': np.array(epw.relative_humidity.values),
|
| 520 |
+
'WindSpeed': np.array(epw.wind_speed.values),
|
| 521 |
+
'GlobalRad': np.array(epw.global_horizontal_radiation.values),
|
| 522 |
+
'DewPoint': np.array(epw.dew_point_temperature.values),
|
| 523 |
+
'Pressure': np.array(epw.atmospheric_station_pressure.values) / 100, # Pa β hPa
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
hours = np.arange(8760) % 24
|
| 527 |
+
months = np.array([(pd.Timestamp(f'{YEAR}-01-01') + pd.Timedelta(hours=h)).month for h in range(8760)])
|
| 528 |
+
|
| 529 |
+
# ββ Fig 14: Diurnal profiles β 4 panels ββββββββββββββββββββββββββ
|
| 530 |
+
fig, axes = plt.subplots(2, 2, figsize=(12, 9))
|
| 531 |
+
colors = [C[0], C[3], C[1]]
|
| 532 |
+
|
| 533 |
+
for ax, var, ylabel in zip(axes.flat,
|
| 534 |
+
['AirTemp', 'RelHum', 'WindSpeed', 'GlobalRad'],
|
| 535 |
+
['Temperature (Β°C)', 'Relative Humidity (%)', 'Wind Speed (m/s)', 'Global Radiation (W/mΒ²)']):
|
| 536 |
+
for (name, d), color in zip(all_data.items(), colors):
|
| 537 |
+
diurnal = [d[var][hours == h].mean() for h in range(24)]
|
| 538 |
+
ax.plot(range(24), diurnal, color=color, lw=2, label=name)
|
| 539 |
+
ax.set_xlabel('Hour of day')
|
| 540 |
+
ax.set_ylabel(ylabel)
|
| 541 |
+
ax.set_xlim(0, 23)
|
| 542 |
+
ax.set_xticks([0, 3, 6, 9, 12, 15, 18, 21])
|
| 543 |
+
ax.legend(fontsize=8)
|
| 544 |
+
|
| 545 |
+
axes[0, 0].set_title('(a) Air temperature')
|
| 546 |
+
axes[0, 1].set_title('(b) Relative humidity')
|
| 547 |
+
axes[1, 0].set_title('(c) Wind speed')
|
| 548 |
+
axes[1, 1].set_title('(d) Global solar radiation')
|
| 549 |
+
|
| 550 |
+
plt.suptitle('Diurnal Profiles: Baseline vs Extreme Scenarios', fontsize=13, y=1.01)
|
| 551 |
+
plt.tight_layout()
|
| 552 |
+
for ext in ['png', 'pdf']:
|
| 553 |
+
fig.savefig(f'{FIG_DIR}/fig14_epw_diurnal.{ext}', dpi=300, bbox_inches='tight')
|
| 554 |
+
plt.close()
|
| 555 |
+
print(" Saved fig14_epw_diurnal")
|
| 556 |
+
|
| 557 |
+
# ββ Fig 15: Monthly temperature box plots ββββββββββββββββββββββββ
|
| 558 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4.5), sharey=True)
|
| 559 |
+
|
| 560 |
+
for ax, (name, d), color in zip(axes, all_data.items(), colors):
|
| 561 |
+
month_data = [d['AirTemp'][months == m] for m in range(1, 13)]
|
| 562 |
+
bp = ax.boxplot(month_data, patch_artist=True, widths=0.6,
|
| 563 |
+
medianprops=dict(color='black', lw=1.5))
|
| 564 |
+
for patch in bp['boxes']:
|
| 565 |
+
patch.set_facecolor(color)
|
| 566 |
+
patch.set_alpha(0.6)
|
| 567 |
+
ax.set_xlabel('Month')
|
| 568 |
+
ax.set_title(name)
|
| 569 |
+
ax.set_xticklabels(['J','F','M','A','M','J','J','A','S','O','N','D'])
|
| 570 |
+
|
| 571 |
+
axes[0].set_ylabel('Air Temperature (Β°C)')
|
| 572 |
+
plt.suptitle('Monthly Temperature Distributions', fontsize=13, y=1.02)
|
| 573 |
+
plt.tight_layout()
|
| 574 |
+
for ext in ['png', 'pdf']:
|
| 575 |
+
fig.savefig(f'{FIG_DIR}/fig15_epw_monthly.{ext}', dpi=300, bbox_inches='tight')
|
| 576 |
+
plt.close()
|
| 577 |
+
print(" Saved fig15_epw_monthly")
|
| 578 |
+
|
| 579 |
+
# ββ Fig 16: Psychrometric-style scatter (T vs RH) ββββββββββββββββ
|
| 580 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 581 |
+
|
| 582 |
+
rng = np.random.RandomState(42)
|
| 583 |
+
n_sample = 2000
|
| 584 |
+
for (name, d), color, marker in zip(all_data.items(), colors, ['o', '^', 's']):
|
| 585 |
+
idx = rng.choice(8760, n_sample, replace=False)
|
| 586 |
+
ax.scatter(d['AirTemp'][idx], d['RelHum'][idx],
|
| 587 |
+
c=color, alpha=0.15, s=10, marker=marker, label=name, rasterized=True)
|
| 588 |
+
|
| 589 |
+
ax.set_xlabel('Air Temperature (Β°C)')
|
| 590 |
+
ax.set_ylabel('Relative Humidity (%)')
|
| 591 |
+
ax.set_title('TemperatureβHumidity State Space')
|
| 592 |
+
ax.legend(markerscale=3, fontsize=10)
|
| 593 |
+
ax.set_xlim(20, 42)
|
| 594 |
+
ax.set_ylim(25, 100)
|
| 595 |
+
|
| 596 |
+
plt.tight_layout()
|
| 597 |
+
for ext in ['png', 'pdf']:
|
| 598 |
+
fig.savefig(f'{FIG_DIR}/fig16_epw_psychrometric.{ext}', dpi=300, bbox_inches='tight')
|
| 599 |
+
plt.close()
|
| 600 |
+
print(" Saved fig16_epw_psychrometric")
|
| 601 |
+
|
| 602 |
+
|
| 603 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 604 |
+
|
| 605 |
+
if __name__ == '__main__':
|
| 606 |
+
results = generate_scenarios()
|
| 607 |
+
print("\n" + "=" * 60)
|
| 608 |
+
print("GENERATING FIGURES...")
|
| 609 |
+
print("=" * 60)
|
| 610 |
+
make_scenario_figures()
|
| 611 |
+
print("\nDone.")
|