File size: 6,869 Bytes
73ddb67 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | #!/usr/bin/env python3
"""Generate structured synthetic ERA5-MODIS monthly pairs for an executable demo."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
import numpy as np
import yaml
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "model"))
from ml_modis import PRESSURE_LEVELS, PRESSURE_VARIABLES, SINGLE_FEATURES, feature_names, validate_multimodal_keys
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--config", default=str(ROOT / "conf/config.yaml"))
parser.add_argument("--samples", type=int, default=None)
parser.add_argument("--output", default=None)
return parser.parse_args()
def ocean_mask(lat: np.ndarray, lon: np.ndarray) -> np.ndarray:
"""Analytic North Atlantic mask excluding coarse Greenland/Europe land shapes."""
greenland = (lat > 59) & (lon > -53) & (lon < -20 + 0.55 * (lat - 59))
europe = (lat > 50) & (lon > -10 + 0.35 * (lat - 50))
iceland = (lat > 63) & (lat < 67) & (lon > -25) & (lon < -13)
north_america = (lon < -52 + 0.3 * (lat - 45))
return ~(greenland | europe | iceland | north_america)
def main() -> None:
args = parse_args()
config = yaml.safe_load(Path(args.config).read_text())
n = int(args.samples or config["data"]["samples"])
rng = np.random.default_rng(config["runtime"]["seed"])
years = np.asarray(config["data"]["years"], dtype=np.int16)
months = np.asarray(config["data"]["months"], dtype=np.int8)
platforms = np.asarray(config["data"]["platforms"], dtype="U5")
records = []
used = set()
while len(records) < n:
year = int(rng.choice(years))
month = int(rng.choice(months))
platform = str(rng.choice(platforms))
lat = int(rng.integers(45, 76))
lon = int(rng.integers(-60, 31))
key = (year, month, platform, lat, lon)
if key in used or not ocean_mask(np.array([lat]), np.array([lon]))[0]:
continue
used.add(key)
records.append(key)
year = np.asarray([r[0] for r in records], dtype=np.int16)
month = np.asarray([r[1] for r in records], dtype=np.int8)
platform = np.asarray([r[2] for r in records], dtype="U5")
lat = np.asarray([r[3] for r in records], dtype=np.float32)
lon = np.asarray([r[4] for r in records], dtype=np.float32)
hour = np.where(platform == "Terra", 11.0, 13.0).astype(np.float32)
phase = np.deg2rad(lon + 25) + (month - 9) * 0.35
maritime = np.cos(np.deg2rad(lat - 58)) * np.cos(np.deg2rad(lon + 25))
synoptic = np.sin(phase * 1.7 + (year - 2001) * 0.43) + 0.45 * np.cos(np.deg2rad(lat * 3))
sst = 286.0 - 0.42 * (lat - 45) + 1.1 * np.cos(phase) - 0.35 * (month - 9) + 0.025 * (year - 2001)
surface_pressure = 101300 + 900 * synoptic - 8 * (lat - 55) + rng.normal(0, 160, n)
humidity_base = np.clip(0.82 - 0.008 * (lat - 45) + 0.08 * maritime + 0.04 * synoptic, 0.35, 0.98)
stability = 0.7 * (lat - 55) - 1.8 * synoptic + rng.normal(0, 0.7, n)
x = np.empty((n, 114), dtype=np.float32)
column = 0
for variable in PRESSURE_VARIABLES:
for level in PRESSURE_LEVELS:
z = (1000 - level) / 50.0
if variable == "temperature": value = sst - 1.7 - 3.15 * z + 0.15 * stability
elif variable == "specific_humidity": value = 0.010 * humidity_base * np.exp(-0.23 * z)
elif variable == "relative_humidity": value = np.clip(humidity_base - 0.025 * z + 0.04 * np.sin(phase + z), 0.05, 1.0)
elif variable == "u_wind": value = 5 + 0.8 * z + 2.2 * np.sin(phase) + 0.12 * (lat - 55)
elif variable == "v_wind": value = 1.5 + 1.6 * np.cos(phase * 1.3) - 0.25 * z
elif variable == "omega": value = -0.025 * synoptic * np.exp(-0.08 * z)
elif variable == "geopotential": value = z * 50 * 9.81 + 4 * synoptic
elif variable == "cloud_liquid": value = np.maximum(0, 2.2e-4 * (humidity_base - 0.55) * np.exp(-0.18 * z))
else: value = np.clip((humidity_base - 0.55) * 1.8 * np.exp(-0.12 * z), 0, 1)
x[:, column] = value + rng.normal(0, max(float(np.std(value)) * 0.035, 1e-6), n)
column += 1
cos_sza = np.clip(np.cos(np.deg2rad(lat - 20)) * (0.97 - 0.01 * (hour - 11)), 0, 1)
singles = np.column_stack([
sst, surface_pressure, surface_pressure + 35, sst - 0.4, sst - 1.1,
sst - (1 - humidity_base) * 12, x[:, 30], x[:, 40], 190 * cos_sza,
315 - 2.5 * (sst - 278), 65 + 18 * synoptic, 18 + 8 * stability,
650 + 120 * humidity_base + 20 * synoptic, 16 + 30 * humidity_base,
0.08 + 0.18 * np.maximum(synoptic, 0), 80 * np.maximum(synoptic, 0),
-25 * np.maximum(-synoptic, 0), np.clip(0.25 + 0.45 * humidity_base + 0.05 * synoptic, 0, 1),
np.clip((lat - 68) / 8, 0, 1), np.maximum(0, 1.8 + 1.5 * synoptic),
cos_sza, lat, lon, hour,
]).astype(np.float32)
x[:, 90:] = singles
platform_term = np.where(platform == "Aqua", 1.0, -1.0)
low_cloud = np.clip(0.22 + 0.55 * humidity_base + 0.035 * stability + 0.025 * synoptic, 0.05, 0.9)
nd = 62 + 48 * humidity_base + 5 * synoptic + 0.32 * (lat - 55) + 1.8 * platform_term
reff = 18.5 - 0.035 * nd + 0.055 * (sst - 278) - 0.10 * stability
lwp = 58 + 115 * low_cloud + 10 * synoptic - 2.0 * stability
cf = np.clip(low_cloud + 0.018 * platform_term, 0.03, 0.95)
plume = np.exp(-((lat - 60) / 10) ** 2 - ((lon + 20) / 25) ** 2)
eruption = (year == 2014).astype(np.float32) * (0.72 + 0.28 * (month == 10)) * plume
nd *= 1 + 0.28 * eruption
reff *= 1 - 0.08 * eruption
lwp *= 1 + 0.008 * eruption
cf = np.clip(cf * (1 + 0.11 * eruption), 0.01, 0.99)
y = np.column_stack([
nd + rng.normal(0, 3.0, n), reff + rng.normal(0, 0.28, n),
lwp + rng.normal(0, 5.0, n), cf + rng.normal(0, 0.018, n),
]).astype(np.float32)
y[:, 0:3] = np.maximum(y[:, 0:3], 1e-3)
y[:, 3] = np.clip(y[:, 3], 0.001, 0.999)
payload = {"X": x, "Y": y, "year": year, "month": month, "platform": platform,
"platform_hour": hour, "latitude": lat, "longitude": lon,
"feature_names": np.asarray(feature_names()), "target_names": np.asarray(config["data"]["variables"]["targets"]["names"]),
"format_version": np.array(config["format_version"]),
"is_ocean": np.ones(n, dtype=bool), "eruption_strength": eruption.astype(np.float32)}
validate_multimodal_keys(payload)
output = ROOT / (args.output or config["data"]["path"])
output.parent.mkdir(parents=True, exist_ok=True)
np.savez_compressed(output, **payload)
print(f"output={output.relative_to(ROOT)} samples={n} shape={list(x.shape)} "
f"eruption_samples={int((year == 2014).sum())}")
if __name__ == "__main__":
main()
|