Stormer / scripts /fake_data.py
yzt15806542928's picture
Upload folder using huggingface_hub
0d5371b verified
Raw
History Blame Contribute Delete
11.7 kB
"""
Generate synthetic ERA5 data and normalization files for Stormer pipeline.
Creates:
1. HDF5 data files (lightweight, chunked with zero fill)
2. Static/invariant files (geopotential, land_sea_mask, lat, lon, etc.)
3. Normalization constants matching official Stormer format:
- normalize_mean.npz, normalize_std.npz (input normalization, 109+ vars)
- normalize_diff_mean_{t}.npz, normalize_diff_std_{t}.npz (diff normalization)
Expected output structure:
data/
├── data/
│ ├── {year}.h5
│ └── ...
├── static/
│ ├── geopotential.nc, land_sea_mask.nc
│ ├── land_mask.npy, soil_type.npy, topography.npy
│ ├── lat.npy, lon.npy
└── normalize/
├── normalize_mean.npz, normalize_std.npz
└── normalize_diff_mean_{6,12,24}.npz, normalize_diff_std_{6,12,24}.npz
"""
import os
import sys
from pathlib import Path
root_path = Path(__file__).parent.parent
sys.path.append(str(root_path))
import h5py
import numpy as np
import xarray as xr
from onescience.utils.YParams import YParams
# Stormer data dimensions:
# T=20 gives 120hrs (5 days) per year — enough for 72h lead time validation
DATASET_DIMS = {"T": 20, "H": 128, "W": 256, "time_step": 6}
# Full variable list from WeatherBench2 (109 vars) — used for normalization files
WB2_ALL_VARS = [
# Surface
"2m_temperature", "10m_u_component_of_wind", "10m_v_component_of_wind",
"mean_sea_level_pressure",
# Geopotential at pressure levels
"geopotential_50", "geopotential_100", "geopotential_150", "geopotential_200",
"geopotential_250", "geopotential_300", "geopotential_400", "geopotential_500",
"geopotential_600", "geopotential_700", "geopotential_850", "geopotential_925",
"geopotential_1000",
# U wind
"u_component_of_wind_50", "u_component_of_wind_100", "u_component_of_wind_150",
"u_component_of_wind_200", "u_component_of_wind_250", "u_component_of_wind_300",
"u_component_of_wind_400", "u_component_of_wind_500", "u_component_of_wind_600",
"u_component_of_wind_700", "u_component_of_wind_850", "u_component_of_wind_925",
"u_component_of_wind_1000",
# V wind
"v_component_of_wind_50", "v_component_of_wind_100", "v_component_of_wind_150",
"v_component_of_wind_200", "v_component_of_wind_250", "v_component_of_wind_300",
"v_component_of_wind_400", "v_component_of_wind_500", "v_component_of_wind_600",
"v_component_of_wind_700", "v_component_of_wind_850", "v_component_of_wind_925",
"v_component_of_wind_1000",
# Temperature
"temperature_50", "temperature_100", "temperature_150", "temperature_200",
"temperature_250", "temperature_300", "temperature_400", "temperature_500",
"temperature_600", "temperature_700", "temperature_850", "temperature_925",
"temperature_1000",
# Specific humidity
"specific_humidity_50", "specific_humidity_100", "specific_humidity_150",
"specific_humidity_200", "specific_humidity_250", "specific_humidity_300",
"specific_humidity_400", "specific_humidity_500", "specific_humidity_600",
"specific_humidity_700", "specific_humidity_850", "specific_humidity_925",
"specific_humidity_1000",
# Additional WB2 variables (for normalization compatibility)
"angle_of_sub_gridscale_orography", "anisotropy_of_sub_gridscale_orography",
"geopotential_at_surface", "high_vegetation_cover", "lake_cover", "lake_depth",
"land_sea_mask", "low_vegetation_cover", "orography",
"slope_of_sub_gridscale_orography", "soil_type",
"standard_deviation_of_filtered_subgrid_orography",
"standard_deviation_of_orography", "type_of_high_vegetation",
"type_of_low_vegetation",
"mean_surface_latent_heat_flux", "mean_surface_net_long_wave_radiation_flux",
"mean_surface_net_short_wave_radiation_flux", "mean_surface_sensible_heat_flux",
"mean_top_downward_short_wave_radiation_flux",
"mean_top_net_long_wave_radiation_flux", "mean_top_net_short_wave_radiation_flux",
"skin_temperature", "snow_depth", "10m_wind_speed", "surface_pressure",
"toa_incident_solar_radiation", "total_precipitation_6hr",
"total_column_water_vapour", "total_cloud_cover", "sea_ice_cover",
"sea_surface_temperature", "vertical_velocity_50", "vertical_velocity_100",
"vertical_velocity_150", "vertical_velocity_200", "vertical_velocity_250",
"vertical_velocity_300", "vertical_velocity_400", "vertical_velocity_500",
"vertical_velocity_600", "vertical_velocity_700", "vertical_velocity_850",
"vertical_velocity_925", "vertical_velocity_1000",
]
def generate_fake_h5(data_dir, var_names, years, dims):
"""Generate HDF5 files with random values for pipeline validation.
Writes actual random data (N(0, 1) per variable) to ensure non-zero
training loss for verifying gradient flow.
"""
os.makedirs(os.path.join(data_dir, "data"), exist_ok=True)
T, C = dims["T"], len(var_names)
H, W = dims["H"], dims["W"]
for year in years:
path = os.path.join(data_dir, "data", f"{year}.h5")
# Generate random data with per-variable mean=0, std=1
rng = np.random.default_rng(42 + year)
fields = rng.normal(0, 1, (T, C, H, W)).astype(np.float32)
# Add temporal correlation so consecutive frames are similar
# (simple AR(1): smooth the time dimension)
for t in range(1, T):
fields[t] = 0.7 * fields[t-1] + 0.3 * fields[t]
# Per-variable means/stds
means = fields.mean(axis=(0, 2, 3), keepdims=True).reshape(1, C, 1, 1)
stds = fields.std(axis=(0, 2, 3), keepdims=True).reshape(1, C, 1, 1)
stds = np.maximum(stds, 0.01) # avoid division by zero
with h5py.File(path, "w") as f:
ds = f.create_dataset(
"fields",
data=fields,
dtype="float32",
chunks=(1, C, H, W),
)
ds.attrs["variables"] = var_names
ds.attrs["time_step"] = dims["time_step"]
f.create_dataset("global_means", data=means)
f.create_dataset("global_stds", data=stds)
size_mb = os.path.getsize(path) / 1024 / 1024
print(f" {year}.h5 shape=({T},{C},{H},{W}) "
f"size={size_mb:.1f}MB logical={T*C*H*W*4/1024**3:.1f}GB")
def get_static(data_dir):
"""Generate synthetic static/invariant data files."""
os.makedirs(data_dir, exist_ok=True)
H, W = DATASET_DIMS["H"], DATASET_DIMS["W"]
# Geopotential at surface
ds = xr.Dataset(
data_vars={
"z": (("valid_time", "latitude", "longitude"),
np.random.rand(1, H, W).astype(np.float32))
},
coords={
"valid_time": ["2015-12-31"],
"latitude": np.linspace(90, -90, H, dtype=np.float64),
"longitude": np.linspace(0, 359.75, W, dtype=np.float64),
"number": 0, "expver": "",
},
attrs={
"GRIB_centre": "ecmf",
"Conventions": "CF-1.7",
"institution": "European Centre for Medium-Range Weather Forecasts",
"history": "Generated for Stormer onescience",
}
)
ds.to_netcdf(f"{data_dir}/geopotential.nc")
# Land-sea mask
ds_lsm = xr.Dataset(
data_vars={
"lsm": (("valid_time", "latitude", "longitude"),
np.random.rand(1, H, W).astype(np.float32))
},
coords={
"valid_time": ["2015-12-31"],
"latitude": np.linspace(90, -90, H, dtype=np.float64),
"longitude": np.linspace(0, 359.75, W, dtype=np.float64),
"number": 0, "expver": "",
},
)
ds_lsm.to_netcdf(f"{data_dir}/land_sea_mask.nc")
# Static numpy arrays
arr = np.random.randn(H, W).astype(np.float32)
np.save(f'{data_dir}/land_mask.npy', arr)
np.save(f'{data_dir}/soil_type.npy', arr)
np.save(f'{data_dir}/topography.npy', arr)
# Latitude/longitude arrays
lat = np.linspace(90, -90, H, dtype=np.float32)
np.save(f'{data_dir}/lat.npy', lat)
lon = np.linspace(0, 359.75, W, dtype=np.float32)
np.save(f'{data_dir}/lon.npy', lon)
print(f"✅ Static data generated in {data_dir}")
def generate_normalization_files(normalize_dir, all_vars, stormer_vars):
"""Generate normalization .npz files matching official Stormer format.
Creates files for ALL 109+ WB2 variables (for compatibility with official
normalization loading code), plus diff normalization for intervals [6, 12, 24].
For fake data: input mean=0, std=1; diff mean=0, diff_std scales with interval.
"""
os.makedirs(normalize_dir, exist_ok=True)
# ---- Input normalization ----
# mean = 0, std = 1 for all variables (fake zero-mean unit-variance data)
inp_mean = {v: np.array([0.0], dtype=np.float32) for v in all_vars}
inp_std = {v: np.array([1.0], dtype=np.float32) for v in all_vars}
np.savez(os.path.join(normalize_dir, "normalize_mean.npz"), **inp_mean)
np.savez(os.path.join(normalize_dir, "normalize_std.npz"), **inp_std)
print(f" normalize_mean.npz: {len(inp_mean)} vars")
print(f" normalize_std.npz: {len(inp_std)} vars")
# ---- Diff normalization for each interval ----
# diff_std scales with sqrt(interval) (diffusion-like behavior in atmosphere)
for interval in [6, 12, 24]:
scale = np.sqrt(interval / 6.0) # 6h→1.0, 12h→1.414, 24h→2.0
diff_mean = {v: np.array([0.0], dtype=np.float32) for v in all_vars}
diff_std = {v: np.array([scale], dtype=np.float32) for v in all_vars}
np.savez(os.path.join(normalize_dir, f"normalize_diff_mean_{interval}.npz"),
**diff_mean)
np.savez(os.path.join(normalize_dir, f"normalize_diff_std_{interval}.npz"),
**diff_std)
print(f" normalize_diff_*_{interval}.npz: {len(diff_std)} vars (scale={scale:.3f})")
print(f"✅ Normalization files generated in {normalize_dir}")
if __name__ == "__main__":
cfg_datapipe = YParams(os.path.join(str(root_path), "conf/config.yaml"), "datapipe")
cfg_model = YParams(os.path.join(str(root_path), "conf/config.yaml"), "model")
data_dir = cfg_datapipe.dataset.data_dir
# Safety: refuse to overwrite real data paths
if data_dir.startswith("/public/") or data_dir.startswith("/work2/") or data_dir.startswith("/work/"):
print("❌ 请检查 config,确保 data_dir 指向本地测试路径而非生产路径。")
sys.exit(1)
years = (cfg_datapipe.dataset.train_time +
cfg_datapipe.dataset.val_time +
cfg_datapipe.dataset.test_time)
stormer_vars = cfg_datapipe.dataset.channels
# 1. Generate fake HDF5 data
print(f"\n📂 Generating fake HDF5: {len(years)} years, {len(stormer_vars)} variables...")
generate_fake_h5(data_dir, stormer_vars, years, DATASET_DIMS)
# 2. Generate static files
static_dir = os.path.join(data_dir, "static")
print(f"\n📂 Generating static files...")
get_static(static_dir)
# 3. Generate normalization files
normalize_dir = cfg_model.normalize_dir
print(f"\n📂 Generating normalization files...")
generate_normalization_files(normalize_dir, WB2_ALL_VARS, stormer_vars)
print(f"\n✅ All fake datasets generated successfully.")
print(f" Data dir: {data_dir}")
print(f" Norm dir: {normalize_dir}")
print(f" Variables: {len(stormer_vars)} (Stormer) / {len(WB2_ALL_VARS)} (WB2 full)")
print(f" Years: {years}")
print(f" Resolution: {DATASET_DIMS['H']}×{DATASET_DIMS['W']} (1.40625°)")