| """Generate structured station-day PCA predictors with realistic surge drivers.""" |
|
|
| import argparse |
| from pathlib import Path |
|
|
| import numpy as np |
| import yaml |
|
|
|
|
| ROOT = Path(__file__).resolve().parents[1] |
|
|
|
|
| def correlated_features(rng, latent, count, phase): |
| weights = rng.normal(0, 0.35, (latent.shape[1], count)) |
| weights[0, :12] += np.linspace(1.5, 0.4, 12) |
| weights[1, 12:24] += np.linspace(1.2, 0.3, 12) |
| features = latent @ weights + rng.normal(0, 0.12, (len(latent), count)) |
| features += 0.08 * np.sin(np.arange(count)[None, :] * 0.13 + phase) |
| return features.astype(np.float32) |
|
|
|
|
| def make_split(path, count, config, seed, day_offset): |
| rng = np.random.default_rng(seed) |
| day = np.arange(count, dtype=np.float32) + day_offset |
| latitude = rng.uniform(-62, 62, count).astype(np.float32) |
| longitude = rng.uniform(-180, 180, count).astype(np.float32) |
| annual = np.sin(2 * np.pi * day / 365.25) |
| synoptic = np.zeros(count, np.float32) |
| for index in range(1, count): |
| synoptic[index] = 0.72 * synoptic[index - 1] + rng.normal(0, 0.7) |
| cyclone = np.maximum(synoptic - 0.55, 0) ** 2 |
| tropical = (np.abs(latitude) < 30).astype(np.float32) |
| latent = np.column_stack([synoptic, cyclone, annual, np.cos(np.deg2rad(latitude)), tropical]).astype(np.float32) |
| data = config["data"] |
| rs_daily = correlated_features(rng, latent, int(data["rs_daily_features"]), 0.1) |
| rs_lagged = correlated_features(rng, latent, int(data["rs_lagged_features"]), 0.4) |
| ar_daily = correlated_features(rng, latent, int(data["ar_daily_features"]), 0.7) |
| ar_lagged = correlated_features(rng, latent, int(data["ar_lagged_features"]), 1.0) |
| surge = (0.035 * synoptic + 0.055 * cyclone + 0.012 * annual + |
| 0.015 * rs_lagged[:, 0] - 0.010 * rs_lagged[:, 13]) |
| surge += rng.normal(0, 0.012 + 0.010 * tropical, count) |
| gtsr = 0.72 * surge + rng.normal(0, 0.025, count) |
| timestamps = (np.datetime64("2000-01-01") + day.astype("timedelta64[D]")).astype("datetime64[s]").astype(np.int64) |
| np.savez_compressed( |
| path, format_version=np.asarray(data["format_version"]), data_source=np.asarray("structured_synthetic"), |
| rs_daily=rs_daily, rs_lagged=rs_lagged, ar_daily=ar_daily, ar_lagged=ar_lagged, |
| targets_m=surge.astype(np.float32)[:, None], gtsr_m=gtsr.astype(np.float32)[:, None], |
| latitude_degrees=latitude, longitude_degrees=longitude, timestamps_unix_s=timestamps, |
| feature_normalization=np.asarray("PCA scores centered and scaled per training feature"), |
| ) |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--force", action="store_true") |
| args = parser.parse_args() |
| config = yaml.safe_load((ROOT / "conf/config.yaml").read_text()) |
| output = ROOT / config["data"]["root"] |
| output.mkdir(parents=True, exist_ok=True) |
| for offset, (name, count, day_offset) in enumerate((("train.npz", config["data"]["train_samples"], 0), |
| ("test.npz", config["data"]["test_samples"], 1000))): |
| path = output / name |
| if args.force or not path.exists(): |
| make_split(path, int(count), config, int(config["seed"]) + offset, day_offset) |
| print(f"generated={path.relative_to(ROOT)} samples={count} daily=50 lagged=300 target=1") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|