"""Generate 21 structured, physically interpretable full-grid response pairs.""" import sys from pathlib import Path import numpy as np import yaml ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from model.climemu_s2l import FORMAT_VERSION, GRID_SHAPE SCENARIOS = [ ("PDRMIP_2xCO2", 2.5, "global", 0, 0), ("PDRMIP_3xCH4", 1.5, "global", 0, 0), ("PDRMIP_10xCFC12", 1.1, "global", 0, 0), ("PDRMIP_solar", 1.0, "global", 0, 0), ("PDRMIP_5xSO4", -1.6, "global", 0, 0), ("PDRMIP_10xBC", 1.2, "global", 0, 0), ("PDRMIP_10xSO4_Europe", -1.0, "regional", 52, 15), ("PDRMIP_10xSO4_Asia", -1.1, "regional", 35, 105), ("PDRMIP_preindustrial_SO4", 0.8, "global", 0, 0), ("ECLIPSE_CH4_minus20", -0.7, "global", 0, 0), ("ECLIPSE_2xCO2", 2.3, "global", 0, 0), ("ECLIPSE_BC_minus100", -0.6, "global", 0, 0), ("ECLIPSE_SO2_minus100", 1.0, "global", 0, 0), ("ECLIPSE_CO_minus100", -0.45, "global", 0, 0), ("KASOAR_SO2_NHML_minus100", 0.75, "regional", 42, 30), ("KASOAR_BC_NHML_minus100", -0.55, "regional", 42, 30), ("KASOAR_SO2_China_minus100", 0.70, "regional", 34, 105), ("KASOAR_SO2_EastAsia_minus100", 0.65, "regional", 40, 125), ("KASOAR_SO2_Europe_minus100", 0.62, "regional", 52, 15), ("KASOAR_SO2_US_minus100", 0.58, "regional", 40, 260), ("PDRMIP_SO4_Asia_alt", -0.72, "regional", 25, 80), ] def wrapped_distance(longitude, centre): return (longitude - centre + 180.0) % 360.0 - 180.0 def main(): config = yaml.safe_load((ROOT / "conf/config.yaml").read_text()) ny, nx = map(int, config["data"]["grid"]) if (ny, nx) != GRID_SHAPE or len(SCENARIOS) != 21: raise ValueError("protocol requires exactly 21 scenarios on a 145x192 grid") latitude = np.linspace(-90.0, 90.0, ny, dtype=np.float64) longitude = np.linspace(0.0, 360.0, nx, endpoint=False, dtype=np.float64) lat, lon = np.meshgrid(latitude, longitude, indexing="ij") polar = np.sin(np.deg2rad(lat)) ** 4 land_wave = np.cos(np.deg2rad(2.0 * lon - 0.6 * lat)) * np.cos(np.deg2rad(lat)) ** 2 short_fields, long_fields = [], [] for index, (scenario_id, amplitude, forcing_type, centre_lat, centre_lon) in enumerate(SCENARIOS): local = np.exp(-0.5 * ((lat - centre_lat) / 16.0) ** 2 -0.5 * (wrapped_distance(lon, centre_lon) / 28.0) ** 2) if forcing_type == "global": local = 0.25 * np.cos(np.deg2rad(lat)) ** 2 phase = 2.0 * np.pi * index / len(SCENARIOS) forcing_region = amplitude * local global_warming = amplitude * (0.52 + 0.34 * polar) circulation = 0.13 * amplitude * np.cos(np.deg2rad(lat * 2.0) + phase) * np.sin(np.deg2rad(lon) - phase) short = global_warming + 0.72 * forcing_region + 0.10 * amplitude * land_wave + circulation zonal = short.mean(axis=1, keepdims=True) remote_east = np.roll(short, nx // 5, axis=1) cross_equatorial = np.flip(zonal, axis=0) planetary_wave = np.cos(np.deg2rad(lon * 2.0 + centre_lon)) * np.cos(np.deg2rad(lat)) long = (1.42 * short + 0.28 * remote_east + 0.23 * cross_equatorial + short.mean() * (0.48 + 0.62 * polar) + 0.09 * amplitude * planetary_wave) short_fields.append(short.astype(np.float32)) long_fields.append(long.astype(np.float32)) short_response = np.stack(short_fields) long_response = np.stack(long_fields) if short_response.shape != (21, 145, 192) or not np.isfinite(long_response).all(): raise ValueError("invalid synthetic response fields") output = ROOT / config["data"]["path"] output.parent.mkdir(parents=True, exist_ok=True) np.savez_compressed(output, format_version=np.array(FORMAT_VERSION), scenario_ids=np.asarray([item[0] for item in SCENARIOS]), forcing_amplitude=np.asarray([item[1] for item in SCENARIOS], dtype=np.float32), latitude_deg=latitude.astype(np.float32), longitude_deg=longitude.astype(np.float32), short_response=short_response, long_response=long_response) print(f"data={output.relative_to(ROOT)} shape={short_response.shape} features={ny * nx}") if __name__ == "__main__": main()