#!/usr/bin/env python3 """Reproduce the design-conditions table (Sections 6, 16.1, 17.3; workbook sheet 'Design conditions') from the public TMYx 2011-2025 files at climate.onebuilding.org. Usage: count_hours.py station_manifest.csv [download_dir] For each station in the manifest the script downloads https://climate.onebuilding.org///[/]_TMYx.2011-2025.zip (the path is resolved from the directory listings by the WMO number), opens the .ddy member and reads the "Ann Clg .4% Condns DB=>MWB" design day (maximum dry-bulb and coincident wet-bulb) and the "Data Source" line, opens the .epw member and counts the hourly dry-bulb readings (EPW column 7) above 27, 30, 32, 34, 36, 38 and 40 C, and writes design_conditions_reproduced.csv. The .ddy design conditions are ASHRAE's (the header line reads 'Data Source="2025 ASHRAE Handbook -- Fundamentals - Chapter 14 Climatic Design Information"' for all 41 stations as read on October 24, 2026). The hour counts are from the typical meteorological year assembled by the file publisher from 2011-2025 observations; a typical year understates extreme years. Requires Python 3.8+ and network access; no third-party packages. """ import csv, io, re, sys, zipfile, urllib.request, posixpath BASE = 'https://climate.onebuilding.org/' THRESHOLDS = (27, 30, 32, 34, 36, 38, 40) def fetch(url): with urllib.request.urlopen(url, timeout=120) as r: return r.read() def list_dir(url): html = fetch(url).decode('utf-8', 'replace') hrefs = re.findall(r'href="([^"]+)"', html) dirs = [h for h in hrefs if h.endswith('/') and not h.startswith(('/', '?', 'http', '..'))] files = [h for h in hrefs if h.endswith('.zip')] return dirs, files def find_zip(dir_url, wmo, depth=2): dirs, files = list_dir(dir_url) for f in files: if f'{wmo}_TMYx.2011-2025.zip' in f: return dir_url + f if depth <= 0: return None for d in dirs: z = find_zip(dir_url + d, wmo, depth - 1) if z: return z return None def parse_ddy(text): lines = text.splitlines() src = next((l.split('Data Source=')[1].strip().strip('"') for l in lines if 'Data Source=' in l), '') out = {'source': src} for label, key in (('Ann Clg .4% Condns DB=>MWB', '04'), ('Ann Clg 1% Condns DB=>MWB', '1'), ('Ann Clg 2% Condns DB=>MWB', '2')): i = next((k for k, l in enumerate(lines) if label in l), None) if i is None: continue block = lines[i:i + 12] db = float(block[4].split(',')[0]) out['db' + key] = db if key == '04': out['mcwb04'] = float(block[9].split(',')[0]) return out def count_epw(text): counts = {t: 0 for t in THRESHOLDS} n = 0 tmax = -99.0 for line in text.splitlines()[8:]: # 8 header lines parts = line.split(',') if len(parts) < 7: continue try: db = float(parts[6]) # EPW column 7: dry-bulb temperature, C except ValueError: continue n += 1 tmax = max(tmax, db) for t in THRESHOLDS: if db > t: counts[t] += 1 return n, tmax, counts def main(): manifest = sys.argv[1] rows = list(csv.DictReader(open(manifest, newline='', encoding='utf-8'))) out_rows = [] for r in rows: wmo = r['wmo'].zfill(6) region_dir = r.get('directory') or '' url = None if region_dir: url = find_zip(BASE + region_dir, wmo) if not url: print(f'{wmo}: zip not found under {region_dir!r}; set the directory column', file=sys.stderr) continue z = zipfile.ZipFile(io.BytesIO(fetch(url))) ddy = next(n for n in z.namelist() if n.lower().endswith('.ddy')) epw = next(n for n in z.namelist() if n.lower().endswith('.epw')) d = parse_ddy(z.read(ddy).decode('latin1')) n, tmax, counts = count_epw(z.read(epw).decode('latin1')) rec = {'key': r['key'], 'station': r['station'], 'wmo': wmo, 'file': posixpath.basename(url), 'source': d['source'], 'db04': d.get('db04'), 'db1': d.get('db1'), 'db2': d.get('db2'), 'mcwb04': d.get('mcwb04'), 'hours_in_file': n, 'tmyx_max': tmax} rec.update({f'h{t}': counts[t] for t in THRESHOLDS}) out_rows.append(rec) print(f"{wmo} {r['key']}: 0.4% DB {rec['db04']} C, MCWB {rec['mcwb04']} C, hours >27 C {counts[27]}, >34 C {counts[34]}; {d['source'][:40]}") with open('design_conditions_reproduced.csv', 'w', newline='', encoding='utf-8') as f: w = csv.DictWriter(f, fieldnames=list(out_rows[0].keys())) w.writeheader() w.writerows(out_rows) if __name__ == '__main__': main()