| """Generate deterministic SkySense-format data for connectivity tests.""" |
|
|
| import argparse |
| from pathlib import Path |
|
|
| import numpy as np |
| import yaml |
|
|
|
|
| ROOT = Path(__file__).resolve().parents[1] |
|
|
|
|
| def load_config(): |
| with (ROOT / "conf" / "config.yaml").open(encoding="utf-8") as handle: |
| return yaml.safe_load(handle) |
|
|
|
|
| def make_split(path, count, config, seed): |
| rng = np.random.default_rng(seed) |
| data = config["data"] |
| hr_size = data["hr_size"] |
| low_size = data["s2_size"] |
| if low_size != data["s1_size"]: |
| raise ValueError("The fake generator expects equal S1/S2 grid sizes") |
| y, x = np.mgrid[0:hr_size, 0:hr_size].astype(np.float32) / (hr_size - 1) |
| low_y, low_x = np.mgrid[0:low_size, 0:low_size].astype(np.float32) / (low_size - 1) |
| land = np.stack([x, y, np.sin(4 * np.pi * x) * np.cos(3 * np.pi * y)], axis=0) |
| hr = np.empty((count, data["hr_timesteps"], data["hr_channels"], hr_size, hr_size), np.float32) |
| s2 = np.empty((count, data["s2_timesteps"], data["s2_channels"], low_size, low_size), np.float32) |
| s1 = np.empty((count, data["s1_timesteps"], data["s1_channels"], low_size, low_size), np.float32) |
| for sample in range(count): |
| phase = rng.uniform(0, 2 * np.pi) |
| hr[sample, 0] = land + rng.normal(0, 0.04, land.shape) |
| for time in range(data["s2_timesteps"]): |
| seasonal = np.sin(2 * np.pi * time / data["s2_timesteps"] + phase) |
| s2[sample, time] = np.stack([ |
| 0.5 + 0.2 * np.sin((band + 1) * low_x + phase) + 0.1 * seasonal * low_y |
| for band in range(data["s2_channels"]) |
| ]) + rng.normal(0, 0.025, (data["s2_channels"], low_size, low_size)) |
| for time in range(data["s1_timesteps"]): |
| seasonal = np.cos(2 * np.pi * time / data["s1_timesteps"] + phase) |
| s1[sample, time, 0] = -0.6 + 0.3 * np.sin(4 * np.pi * low_x) * np.cos(3 * np.pi * low_y) + 0.1 * seasonal |
| s1[sample, time, 1] = -0.8 + 0.2 * low_y - 0.1 * seasonal |
| hr[sample] = np.clip(hr[sample], -1, 1) |
| s2[sample] = np.clip(s2[sample], 0, 1) |
| dates_hr = rng.integers(0, 365, size=(count, data["hr_timesteps"]), dtype=np.int64) |
| dates_s2 = rng.integers(0, 365, size=(count, data["s2_timesteps"]), dtype=np.int64) |
| dates_s1 = rng.integers(0, 365, size=(count, data["s1_timesteps"]), dtype=np.int64) |
| region = rng.integers(0, config["model"]["num_regions"], size=count, dtype=np.int64) |
| indices = np.linspace(0, low_size - 1, hr_size).round().astype(int) |
| s2_hr = s2[:, :, 3].mean(axis=1)[:, indices][:, :, indices] |
| s1_hr = s1[:, :, 0].mean(axis=1)[:, indices][:, :, indices] |
| signal = hr[:, 0, 0] + 0.35 * s2_hr - 0.2 * s1_hr |
| bins = np.quantile(signal, np.linspace(0, 1, data["num_classes"] + 1)[1:-1]) |
| labels = np.digitize(signal, bins).astype(np.int64) |
| np.savez_compressed( |
| path, |
| hr=hr, |
| s2=s2, |
| s1=s1, |
| dates_hr=dates_hr, |
| dates_s2=dates_s2, |
| dates_s1=dates_s1, |
| region=region, |
| labels=labels, |
| data_source=np.asarray("synthetic"), |
| protocol=np.asarray(config["data"]["protocol"]), |
| band_order_hr=np.asarray(["R", "G", "B"]), |
| band_order_s2=np.asarray(["B2", "B3", "B4", "B5", "B6", "B7", "B8", "B8A", "B11", "B12"]), |
| band_order_s1=np.asarray(["VV", "VH"]), |
| ) |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--force", action="store_true") |
| args = parser.parse_args() |
| config = load_config() |
| data_dir = ROOT / config["data"]["root"] |
| data_dir.mkdir(parents=True, exist_ok=True) |
| outputs = [("train.npz", config["data"]["train_samples"]), ("test.npz", config["data"]["test_samples"])] |
| for offset, (name, count) in enumerate(outputs): |
| target = data_dir / name |
| if args.force or not target.exists(): |
| make_split(target, count, config, config["seed"] + offset) |
| print( |
| f"generated={target.relative_to(ROOT)} samples={count} " |
| f"data_source=synthetic protocol={config['data']['protocol']}" |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|