"""Generate sparse synthetic observations following the paper-confirmed data contract.""" import argparse from pathlib import Path import numpy as np import yaml ROOT = Path(__file__).resolve().parents[1] DAY_MS = 86_400_000 TARGET_ONLY_SLOTS = 3 def load_config(): return yaml.safe_load((ROOT / "conf/config.yaml").read_text()) def standardized_signal(rng, count, frames, channels, size, source_phase): y, x = np.mgrid[-1:1:complex(size), -1:1:complex(size)].astype(np.float32) base = np.sin(3 * np.pi * x) * np.cos(2 * np.pi * y) + 0.4 * x + 0.2 * y values = np.empty((count, frames, channels, size, size), np.float32) for sample in range(count): phase = rng.uniform(0, 2 * np.pi) + source_phase for step in range(frames): seasonal = np.sin(2 * np.pi * step / max(frames, 1) + phase) for channel in range(channels): values[sample, step, channel] = base + 0.08 * channel + 0.25 * seasonal values[sample] += rng.normal(0, 0.04, values[sample].shape) values -= values.mean(axis=(0, 1, 3, 4), keepdims=True) values /= values.std(axis=(0, 1, 3, 4), keepdims=True).clip(1e-6) return np.clip(values, -6, 6).astype(np.float32) def quality_mask(rng, count, frames, channels, size, sparse=False): probability = 0.08 if not sparse else 0.92 mask = rng.random((count, frames, 1, size, size)) > probability if not sparse: mask[:, :, :, :2] = False return np.repeat(mask, channels, axis=2).astype(np.float32) def make_split(path, count, config, seed): rng = np.random.default_rng(seed) data, size = config["data"], config["data"]["image_size"] start = np.datetime64("2020-01-01", "ms").astype(np.int64) payload = {"format_version": np.asarray(data["format_version"]), "data_source": np.asarray("synthetic")} for source_index, (name, spec) in enumerate(data["input_sources"].items()): frames, channels = spec["timesteps"], spec["channels"] payload[name] = standardized_signal(rng, count, frames, channels, size, source_index) days = np.linspace(0, 364, frames, dtype=np.int64) payload[f"timestamps_{name}"] = np.tile(start + days * DAY_MS + source_index, (count, 1)) available = np.ones((count, frames), np.bool_) available[:, -max(1, frames // 10):] = False payload[f"frame_available_{name}"] = available channel_available = np.ones((count, frames, channels), np.bool_) if name == "sentinel1": channel_available[..., :4] = False for sample in range(count): for step in range(frames): pair = (0, 1) if (sample + step) % 2 == 0 else (2, 3) channel_available[sample, step, list(pair)] = True payload[f"channel_available_{name}"] = channel_available payload[f"pixel_valid_{name}"] = quality_mask(rng, count, frames, channels, size) geometry = np.zeros((count, frames, config["model"]["max_geometry_dim"]), np.float32) if name == "sentinel1": geometry[..., 0] = rng.uniform(-np.pi, np.pi, (count, frames)) geometry[..., 1] = rng.uniform(1.6, 1.8, (count, frames)) payload[f"geometry_{name}"] = geometry for target_index, (name, spec) in enumerate(data["target_sources"].items()): if name in data["input_sources"]: continue channels = 1 if spec["type"] == "categorical" else spec["channels"] values = standardized_signal(rng, count, TARGET_ONLY_SLOTS, channels, size, target_index + 3) if spec["type"] == "categorical": bins = np.linspace(values.min(), values.max(), spec["channels"] + 1)[1:-1] values = np.digitize(values[:, :, 0], bins).astype(np.int64) payload[f"target_sequence_{name}"] = values payload[f"target_timestamps_{name}"] = np.tile( start + np.asarray([30, 182, 330], np.int64) * DAY_MS + target_index, (count, 1) ) mask_channels = 1 if spec["type"] == "categorical" else spec["channels"] payload[f"target_pixel_valid_{name}"] = quality_mask( rng, count, TARGET_ONLY_SLOTS, mask_channels, size, sparse=name == "gedi" ) geometry = np.zeros((count, TARGET_ONLY_SLOTS, config["model"]["max_geometry_dim"]), np.float32) if name == "palsar2": geometry[..., 0] = rng.integers(0, 2, (count, TARGET_ONLY_SLOTS)) geometry[..., 1] = rng.integers(0, 2, (count, TARGET_ONLY_SLOTS)) payload[f"target_geometry_{name}"] = geometry payload[f"target_frame_available_{name}"] = np.ones((count, TARGET_ONLY_SLOTS), np.bool_) payload["support_period"] = np.tile(np.array([start, start + 365 * DAY_MS], np.int64), (count, 1)) payload["valid_period"] = np.tile(np.array([start + 60 * DAY_MS, start + 300 * DAY_MS], np.int64), (count, 1)) text = rng.normal(size=(count, config["model"]["embedding_dim"])).astype(np.float32) payload["text_target"] = text / np.linalg.norm(text, axis=1, keepdims=True) np.savez_compressed(path, **payload) 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) for offset, (name, count) in enumerate((("train.npz", config["data"]["train_samples"]), ("test.npz", config["data"]["test_samples"]))): 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} format={config['data']['format_version']}") if __name__ == "__main__": main()