CoolFace
Modelpublic

OneScience-Group/MP_PDE

sourceHugging Facemitupdated 1mo agoView on Hugging Face
0likes13downloads
dataset.py573 linesDownload Raw Back to models
1"""E3 trajectory generation and HDF5 loading for MP-PDE.2 3This is an independent implementation from the equations and numerical-method4description in arXiv:2202.03376.  No official repository source is used.5"""6 7from __future__ import annotations8 9import argparse10from concurrent.futures import FIRST_COMPLETED, ProcessPoolExecutor, wait11import json12import multiprocessing13import os14from pathlib import Path15from typing import Any, Dict, Iterable, Mapping, Optional, Tuple16 17import h5py18import numpy as np19import torch20import yaml21from torch.utils.data import Dataset22 23 24SPLITS = ("train", "valid", "test")25PARAMETER_ORDER = ("alpha", "beta", "gamma")26PARALLEL_SCHEMA_VERSION = "mp_pde_e3_parallel_resume_v1"27_WORKER_X: Optional[np.ndarray] = None28_WORKER_TIMES: Optional[np.ndarray] = None29_WORKER_CONFIG: Optional[Dict[str, Any]] = None30_WORKER_STRIDE: Optional[int] = None31 32 33def _shift(values: np.ndarray, offset: int) -> np.ndarray:34    """Return values[i + offset] under periodic indexing."""35    return np.roll(values, -offset, axis=-1)36 37 38def _weno5_left(values: np.ndarray, epsilon: float) -> np.ndarray:39    """Fifth-order WENO left state at every i+1/2 interface."""40    um2, um1, u0 = _shift(values, -2), _shift(values, -1), values41    up1, up2 = _shift(values, 1), _shift(values, 2)42    p0 = (2.0 * um2 - 7.0 * um1 + 11.0 * u0) / 6.043    p1 = (-um1 + 5.0 * u0 + 2.0 * up1) / 6.044    p2 = (2.0 * u0 + 5.0 * up1 - up2) / 6.045    b0 = (13.0 / 12.0) * (um2 - 2.0 * um1 + u0) ** 2 + 0.25 * (um2 - 4.0 * um1 + 3.0 * u0) ** 246    b1 = (13.0 / 12.0) * (um1 - 2.0 * u0 + up1) ** 2 + 0.25 * (um1 - up1) ** 247    b2 = (13.0 / 12.0) * (u0 - 2.0 * up1 + up2) ** 2 + 0.25 * (3.0 * u0 - 4.0 * up1 + up2) ** 248    alpha = np.stack((0.1 / (epsilon + b0) ** 2, 0.6 / (epsilon + b1) ** 2, 0.3 / (epsilon + b2) ** 2))49    weights = alpha / np.sum(alpha, axis=0, keepdims=True)50    return weights[0] * p0 + weights[1] * p1 + weights[2] * p251 52 53def _weno5_right(values: np.ndarray, epsilon: float) -> np.ndarray:54    """Fifth-order WENO right state at every i+1/2 interface."""55    um1, u0 = _shift(values, -1), values56    up1, up2, up3 = _shift(values, 1), _shift(values, 2), _shift(values, 3)57    p0 = (2.0 * up3 - 7.0 * up2 + 11.0 * up1) / 6.058    p1 = (-up2 + 5.0 * up1 + 2.0 * u0) / 6.059    p2 = (2.0 * up1 + 5.0 * u0 - um1) / 6.060    b0 = (13.0 / 12.0) * (up1 - 2.0 * up2 + up3) ** 2 + 0.25 * (3.0 * up1 - 4.0 * up2 + up3) ** 261    b1 = (13.0 / 12.0) * (u0 - 2.0 * up1 + up2) ** 2 + 0.25 * (u0 - up2) ** 262    b2 = (13.0 / 12.0) * (um1 - 2.0 * u0 + up1) ** 2 + 0.25 * (um1 - 4.0 * u0 + 3.0 * up1) ** 263    alpha = np.stack((0.1 / (epsilon + b0) ** 2, 0.6 / (epsilon + b1) ** 2, 0.3 / (epsilon + b2) ** 2))64    weights = alpha / np.sum(alpha, axis=0, keepdims=True)65    return weights[0] * p0 + weights[1] * p1 + weights[2] * p266 67 68def godunov_quadratic_flux(left: np.ndarray, right: np.ndarray) -> np.ndarray:69    """Godunov flux for the convex scalar flux f(u)=u**2."""70    left_flux, right_flux = left * left, right * right71    rarefaction = left <= right72    rare_flux = np.where((left <= 0.0) & (right >= 0.0), 0.0, np.minimum(left_flux, right_flux))73    shock_flux = np.maximum(left_flux, right_flux)74    return np.where(rarefaction, rare_flux, shock_flux)75 76 77def weno5_flux_derivative(values: np.ndarray, dx: float, epsilon: float = 1.0e-6) -> np.ndarray:78    """Conservative derivative d_x(u**2) on a periodic uniform grid."""79    interface_flux = godunov_quadratic_flux(_weno5_left(values, epsilon), _weno5_right(values, epsilon))80    return (interface_flux - np.roll(interface_flux, 1, axis=-1)) / dx81 82 83def fourth_order_second_derivative(values: np.ndarray, dx: float) -> np.ndarray:84    return (-_shift(values, 2) + 16.0 * _shift(values, 1) - 30.0 * values + 16.0 * _shift(values, -1) - _shift(values, -2)) / (12.0 * dx**2)85 86 87def fourth_order_third_derivative(values: np.ndarray, dx: float) -> np.ndarray:88    return (_shift(values, -3) - 8.0 * _shift(values, -2) + 13.0 * _shift(values, -1) - 13.0 * _shift(values, 1) + 8.0 * _shift(values, 2) - _shift(values, 3)) / (8.0 * dx**3)89 90 91def sample_e3_parameters(rng: np.random.Generator, cfg: Mapping[str, Any]) -> Tuple[np.ndarray, Dict[str, np.ndarray]]:92    equation, forcing = cfg["equation"], cfg["forcing"]93    params = np.array(94        [rng.uniform(*equation["alpha_range"]), rng.uniform(*equation["beta_range"]), rng.uniform(*equation["gamma_range"])],95        dtype=np.float64,96    )97    policy = cfg["ambiguity_policy"]98    if policy == "paper_strict":99        omega_range = forcing["paper_omega_range"]100    elif policy == "official_consistency":101        omega_range = forcing["official_consistency_omega_range"]102    else:103        raise ValueError(f"Unknown ambiguity_policy={policy!r}")104    terms = int(forcing["terms"])105    provenance = {106        "amplitude": rng.uniform(*forcing["amplitude_range"], size=terms),107        "omega": rng.uniform(*omega_range, size=terms),108        "mode": rng.choice(np.asarray(forcing["modes"], dtype=np.int64), size=terms),109        "phase": rng.uniform(*forcing["phase_range"], size=terms),110    }111    return params, provenance112 113 114def evaluate_forcing(time: float, x: np.ndarray, forcing: Mapping[str, np.ndarray], domain_length: float) -> np.ndarray:115    phase = (116        forcing["omega"][:, None] * time117        + 2.0 * np.pi * forcing["mode"][:, None] * x[None, :] / domain_length118        + forcing["phase"][:, None]119    )120    return np.sum(forcing["amplitude"][:, None] * np.sin(phase), axis=0)121 122 123def e3_rhs(124    time: float,125    state: np.ndarray,126    x: np.ndarray,127    params: np.ndarray,128    forcing: Mapping[str, np.ndarray],129    domain_length: float,130    weno_epsilon: float,131) -> np.ndarray:132    alpha, beta, gamma = params133    dx = domain_length / state.shape[-1]134    return (135        evaluate_forcing(time, x, forcing, domain_length)136        - alpha * weno5_flux_derivative(state, dx, weno_epsilon)137        + beta * fourth_order_second_derivative(state, dx)138        - gamma * fourth_order_third_derivative(state, dx)139    )140 141 142def _stable_step(state: np.ndarray, params: np.ndarray, dx: float, cfl: float) -> float:143    alpha, beta, gamma = np.abs(params)144    limits = []145    wave_speed = 2.0 * alpha * float(np.max(np.abs(state)))146    if wave_speed > 1.0e-14:147        limits.append(dx / wave_speed)148    if beta > 1.0e-14:149        limits.append(dx**2 / (2.0 * beta))150    if gamma > 1.0e-14:151        limits.append(dx**3 / (6.0 * gamma))152    return cfl * min(limits) if limits else np.inf153 154 155def generate_trajectory(156    x: np.ndarray,157    save_times: np.ndarray,158    params: np.ndarray,159    forcing: Mapping[str, np.ndarray],160    cfg: Mapping[str, Any],161) -> np.ndarray:162    """Integrate one trajectory using RK4 and stability-limited substeps."""163    domain_length = float(cfg["domain_length"])164    generation = cfg["generation"]165    dx = domain_length / x.size166    state = evaluate_forcing(float(save_times[0]), x, forcing, domain_length).astype(np.float64)167    trajectory = np.empty((save_times.size, x.size), dtype=np.float32)168    trajectory[0] = state169    current_time = float(save_times[0])170    for output_index, target_time in enumerate(save_times[1:], start=1):171        substeps = 0172        while current_time < float(target_time) - 1.0e-14:173            stable = _stable_step(state, params, dx, float(generation["cfl"]))174            remaining = float(target_time) - current_time175            step = min(stable, remaining)176            if step < float(generation["min_dt"]) and remaining > float(generation["min_dt"]):177                raise RuntimeError(f"Stable RK4 step {step:.3e} fell below min_dt at t={current_time:.6g}")178            step = remaining if remaining <= float(generation["min_dt"]) else step179            rhs_args = (x, params, forcing, domain_length, float(generation["weno_epsilon"]))180            k1 = e3_rhs(current_time, state, *rhs_args)181            k2 = e3_rhs(current_time + 0.5 * step, state + 0.5 * step * k1, *rhs_args)182            k3 = e3_rhs(current_time + 0.5 * step, state + 0.5 * step * k2, *rhs_args)183            k4 = e3_rhs(current_time + step, state + step * k3, *rhs_args)184            state = state + (step / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)185            current_time += step186            substeps += 1187            if substeps > int(generation["max_substeps"]):188                raise RuntimeError(f"max_substeps exceeded while advancing to t={target_time:.6g}")189            if not np.all(np.isfinite(state)):190                raise FloatingPointError(f"Non-finite E3 state at t={current_time:.6g}")191        current_time = float(target_time)192        trajectory[output_index] = state193    return trajectory194 195 196def _generation_config(config: Mapping[str, Any]) -> Dict[str, Any]:197    data = config["data"]198    return {199        "ambiguity_policy": config["experiment"]["ambiguity_policy"],200        "domain_length": data["domain_length"],201        "equation": data["equation"],202        "forcing": data["forcing"],203        "generation": data["generation"],204    }205 206 207def _initialize_generation_worker(208    x_high: np.ndarray, times: np.ndarray, generation_cfg: Mapping[str, Any], stride: int209) -> None:210    """Initialize immutable state used by one trajectory worker process."""211    global _WORKER_X, _WORKER_TIMES, _WORKER_CONFIG, _WORKER_STRIDE212    _WORKER_X = x_high213    _WORKER_TIMES = times214    _WORKER_CONFIG = dict(generation_cfg)215    _WORKER_STRIDE = int(stride)216 217 218def _generate_sample_worker(payload: Tuple[int, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]) -> Tuple[int, np.ndarray]:219    """Generate one independent trajectory; HDF5 remains owned by the parent."""220    if _WORKER_X is None or _WORKER_TIMES is None or _WORKER_CONFIG is None or _WORKER_STRIDE is None:221        raise RuntimeError("Parallel E3 worker was not initialized")222    sample_index, params, amplitude, omega, mode, phase = payload223    forcing = {"amplitude": amplitude, "omega": omega, "mode": mode, "phase": phase}224    trajectory = generate_trajectory(_WORKER_X, _WORKER_TIMES, params, forcing, _WORKER_CONFIG)225    return sample_index, trajectory[:, ::_WORKER_STRIDE]226 227 228def _sample_payloads(group: h5py.Group, indices: Iterable[int]) -> Iterable[Tuple[int, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]]:229    for sample_index in indices:230        yield (231            sample_index,232            np.asarray(group["params"][sample_index], dtype=np.float64),233            np.asarray(group["forcing_amplitude"][sample_index], dtype=np.float64),234            np.asarray(group["forcing_omega"][sample_index], dtype=np.float64),235            np.asarray(group["forcing_mode"][sample_index], dtype=np.int64),236            np.asarray(group["forcing_phase"][sample_index], dtype=np.float64),237        )238 239 240def _parallel_results(241    payloads: Iterable[Tuple[int, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]],242    workers: int,243    max_in_flight: int,244    x_high: np.ndarray,245    times: np.ndarray,246    generation_cfg: Mapping[str, Any],247    stride: int,248) -> Iterable[Tuple[int, np.ndarray]]:249    """Yield completed trajectories while keeping the process queue bounded."""250    if workers == 1:251        _initialize_generation_worker(x_high, times, generation_cfg, stride)252        for payload in payloads:253            yield _generate_sample_worker(payload)254        return255 256    thread_variables = ("OMP_NUM_THREADS", "MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS", "NUMEXPR_NUM_THREADS")257    previous_environment = {name: os.environ.get(name) for name in thread_variables}258    for name in thread_variables:259        os.environ[name] = "1"260    executor = ProcessPoolExecutor(261        max_workers=workers,262        mp_context=multiprocessing.get_context("spawn"),263        initializer=_initialize_generation_worker,264        initargs=(x_high, times, dict(generation_cfg), stride),265    )266    iterator = iter(payloads)267    futures = set()268    try:269        for _ in range(max_in_flight):270            try:271                futures.add(executor.submit(_generate_sample_worker, next(iterator)))272            except StopIteration:273                break274        while futures:275            completed, futures = wait(futures, return_when=FIRST_COMPLETED)276            for future in completed:277                yield future.result()278                try:279                    futures.add(executor.submit(_generate_sample_worker, next(iterator)))280                except StopIteration:281                    pass282    finally:283        executor.shutdown(wait=True, cancel_futures=True)284        for name, value in previous_environment.items():285            if value is None:286                os.environ.pop(name, None)287            else:288                os.environ[name] = value289 290 291def _generation_signature(292    config: Mapping[str, Any], counts: Mapping[str, int], nt: int, high_nx: int, target_nx: int293) -> str:294    payload = {295        "schema_version": PARALLEL_SCHEMA_VERSION,296        "paper": str(config["experiment"]["paper"]),297        "ambiguity_policy": str(config["experiment"]["ambiguity_policy"]),298        "generation_config": _generation_config(config),299        "counts": {name: int(counts[name]) for name in SPLITS},300        "nt": int(nt),301        "high_resolution_nx": int(high_nx),302        "resolution": int(target_nx),303        "seed": int(config["data"]["seed"]),304    }305    return json.dumps(payload, sort_keys=True, separators=(",", ":"))306 307 308def _initialize_partial_file(309    partial_path: Path,310    config: Mapping[str, Any],311    counts: Mapping[str, int],312    nt: int,313    high_nx: int,314    target_nx: int,315    x: np.ndarray,316    times: np.ndarray,317    generation_cfg: Mapping[str, Any],318    compression: Optional[str],319    compression_opts: Optional[int],320    signature: str,321) -> None:322    data = config["data"]323    base_seed = int(data["seed"])324    with h5py.File(partial_path, "w") as handle:325        handle.attrs["schema_version"] = PARALLEL_SCHEMA_VERSION326        handle.attrs["generation_signature"] = signature327        handle.attrs["experiment"] = "MP-PDE E3"328        handle.attrs["paper"] = str(config["experiment"]["paper"])329        handle.attrs["ambiguity_policy"] = str(config["experiment"]["ambiguity_policy"])330        handle.attrs["parameter_order"] = json.dumps(PARAMETER_ORDER)331        handle.attrs["generation_config"] = json.dumps(generation_cfg, sort_keys=True)332        handle.attrs["seed"] = base_seed333        handle.attrs["high_resolution_nx"] = high_nx334        handle.attrs["resolution"] = target_nx335        for split_index, split in enumerate(SPLITS):336            count = int(counts[split])337            rng = np.random.default_rng(np.random.SeedSequence([base_seed, split_index]))338            group = handle.create_group(split)339            group.attrs["seed_derivation"] = json.dumps([base_seed, split_index])340            group.create_dataset("x", data=x)341            group.create_dataset("t", data=times.astype(np.float32))342            group.create_dataset(343                "u", shape=(count, nt, target_nx), dtype="f4", chunks=(1, min(nt, 32), target_nx),344                compression=compression, compression_opts=compression_opts,345            )346            params_ds = group.create_dataset("params", shape=(count, 3), dtype="f4")347            terms = int(data["forcing"]["terms"])348            amplitude_ds = group.create_dataset("forcing_amplitude", shape=(count, terms), dtype="f4")349            omega_ds = group.create_dataset("forcing_omega", shape=(count, terms), dtype="f4")350            mode_ds = group.create_dataset("forcing_mode", shape=(count, terms), dtype="i8")351            phase_ds = group.create_dataset("forcing_phase", shape=(count, terms), dtype="f4")352            group.create_dataset("completed", shape=(count,), dtype="bool", data=np.zeros(count, dtype=bool))353            for sample_index in range(count):354                params, forcing = sample_e3_parameters(rng, generation_cfg)355                params_ds[sample_index] = params356                amplitude_ds[sample_index] = forcing["amplitude"]357                omega_ds[sample_index] = forcing["omega"]358                mode_ds[sample_index] = forcing["mode"]359                phase_ds[sample_index] = forcing["phase"]360        handle.flush()361 362 363def generate_e3_hdf5(364    config: Mapping[str, Any],365    output_path: Path | str,366    *,367    sample_counts: Optional[Mapping[str, int]] = None,368    nt: Optional[int] = None,369    high_resolution_nx: Optional[int] = None,370    resolution: Optional[int] = None,371    workers: Optional[int] = None,372    max_in_flight: Optional[int] = None,373    flush_every: Optional[int] = None,374    resume_partial: Optional[bool] = None,375    overwrite: bool = False,376) -> Path:377    """Generate E3 splits with process workers and a single resumable HDF5 writer."""378    output_path = Path(output_path)379    if output_path.exists() and not overwrite:380        raise FileExistsError(f"Refusing to overwrite existing dataset: {output_path}")381    output_path.parent.mkdir(parents=True, exist_ok=True)382    partial_path = output_path.with_suffix(output_path.suffix + ".partial")383 384    data = config["data"]385    parallel = data.get("parallel_generation", {})386    workers = int(workers if workers is not None else parallel.get("workers", 1))387    max_in_flight = int(max_in_flight if max_in_flight is not None else parallel.get("max_in_flight", 2 * workers))388    flush_every = int(flush_every if flush_every is not None else parallel.get("flush_every", workers))389    resume_partial = bool(resume_partial if resume_partial is not None else parallel.get("resume_partial", True))390    if workers < 1 or max_in_flight < workers or flush_every < 1:391        raise ValueError("workers>=1, max_in_flight>=workers, and flush_every>=1 are required")392    nt = int(nt or data["num_time_points"])393    high_nx = int(high_resolution_nx or data["high_resolution_nx"])394    target_nx = int(resolution or data["resolution"])395    if high_nx % target_nx != 0:396        raise ValueError(f"high_resolution_nx={high_nx} must be divisible by resolution={target_nx}")397    if high_nx < 7 or nt < 2:398        raise ValueError("Generation requires high_resolution_nx>=7 and nt>=2")399    counts = dict(sample_counts or {name: int(data[f"{name}_samples"]) for name in SPLITS})400    if set(counts) != set(SPLITS) or any(int(counts[name]) <= 0 for name in SPLITS):401        raise ValueError(f"sample_counts must provide positive counts for {SPLITS}")402 403    domain_length, final_time = float(data["domain_length"]), float(data["final_time"])404    x_high = np.linspace(0.0, domain_length, high_nx, endpoint=False, dtype=np.float64)405    stride = high_nx // target_nx406    x = x_high[::stride].astype(np.float32)407    times = np.linspace(0.0, final_time, nt, dtype=np.float64)408    generation_cfg = _generation_config(config)409    compression = data["generation"].get("compression")410    compression_opts = int(data["generation"].get("compression_level", 4)) if compression == "gzip" else None411    signature = _generation_signature(config, counts, nt, high_nx, target_nx)412    if partial_path.exists() and overwrite:413        partial_path.unlink()414    if partial_path.exists() and not resume_partial:415        raise FileExistsError(f"Partial dataset exists; enable resume_partial or use --overwrite: {partial_path}")416    if not partial_path.exists():417        _initialize_partial_file(418            partial_path, config, counts, nt, high_nx, target_nx, x, times, generation_cfg,419            compression, compression_opts, signature,420        )421 422    try:423        with h5py.File(partial_path, "r+") as handle:424            stored_signature = str(handle.attrs.get("generation_signature", ""))425            if stored_signature != signature:426                raise ValueError("Partial dataset configuration does not match this run; archive it or use --overwrite")427            print(428                f"[generate] workers={workers} max_in_flight={max_in_flight} flush_every={flush_every} "429                f"resume_partial={resume_partial}", flush=True,430            )431            for split in SPLITS:432                count = int(counts[split])433                group = handle[split]434                completed_ds = group["completed"]435                pending_indices = np.flatnonzero(~np.asarray(completed_ds[:], dtype=bool)).tolist()436                completed_count = count - len(pending_indices)437                if pending_indices:438                    print(f"[generate] split={split} resume_completed={completed_count}/{count}", flush=True)439                payloads = _sample_payloads(group, pending_indices)440                for sample_index, trajectory in _parallel_results(441                    payloads, workers, max_in_flight, x_high, times, generation_cfg, stride442                ):443                    group["u"][sample_index] = trajectory444                    completed_ds[sample_index] = True445                    completed_count += 1446                    if completed_count % flush_every == 0 or completed_count == count:447                        handle.flush()448                    print(449                        f"[generate] split={split} completed={completed_count}/{count} "450                        f"sample_index={sample_index} workers={workers}", flush=True,451                    )452                if not np.all(np.asarray(completed_ds[:], dtype=bool)):453                    raise RuntimeError(f"Split {split} is incomplete after generation")454                handle.flush()455        os.replace(partial_path, output_path)456    except Exception:457        print(f"Generation failed; partial file retained at {partial_path}", flush=True)458        raise459    return output_path460 461 462class E3Dataset(Dataset):463    """Lazy, process-safe reader for one E3 HDF5 split."""464 465    def __init__(self, path: Path | str, split: str, expected_nt: Optional[int] = None, expected_nx: Optional[int] = None):466        self.path = Path(path)467        self.split = split468        self._handle: Optional[h5py.File] = None469        if split not in SPLITS:470            raise ValueError(f"split must be one of {SPLITS}, got {split!r}")471        if not self.path.is_file():472            raise FileNotFoundError(f"E3 dataset not found: {self.path}")473        with h5py.File(self.path, "r") as handle:474            if split not in handle:475                raise KeyError(f"Missing HDF5 group {split!r}")476            group = handle[split]477            required = {"u", "x", "t", "params", "forcing_amplitude", "forcing_omega", "forcing_mode", "forcing_phase"}478            missing = required.difference(group.keys())479            if missing:480                raise KeyError(f"Missing HDF5 fields in {split}: {sorted(missing)}")481            shape = group["u"].shape482            if len(shape) != 3 or group["params"].shape != (shape[0], 3) or group["x"].shape != (shape[2],) or group["t"].shape != (shape[1],):483                raise ValueError(f"Inconsistent E3 schema in split={split}: u={shape}")484            if expected_nt is not None and shape[1] != expected_nt:485                raise ValueError(f"Expected nt={expected_nt}, found {shape[1]}")486            if expected_nx is not None and shape[2] != expected_nx:487                raise ValueError(f"Expected nx={expected_nx}, found {shape[2]}")488            self.length, self.nt, self.nx = shape489            x, t = group["x"][:], group["t"][:]490            if not np.all(np.isfinite(x)) or not np.all(np.isfinite(t)) or np.any(np.diff(t) <= 0.0):491                raise ValueError("Grid/time metadata are non-finite or non-monotone")492            if x.size > 1 and not np.allclose(np.diff(x), np.diff(x)[0], rtol=1e-5, atol=1e-7):493                raise ValueError("E3 x grid must be uniform")494            if t.size > 1 and not np.allclose(np.diff(t), np.diff(t)[0], rtol=1e-5, atol=1e-7):495                raise ValueError("E3 saved times must be uniform")496 497    def _group(self) -> h5py.Group:498        if self._handle is None:499            self._handle = h5py.File(self.path, "r")500        return self._handle[self.split]501 502    def __len__(self) -> int:503        return self.length504 505    def __getitem__(self, index: int) -> Dict[str, torch.Tensor]:506        group = self._group()507        trajectory = np.asarray(group["u"][index], dtype=np.float32)508        params = np.asarray(group["params"][index], dtype=np.float32)509        if not np.all(np.isfinite(trajectory)) or not np.all(np.isfinite(params)):510            raise FloatingPointError(f"Non-finite data at split={self.split}, sample={index}")511        return {512            "u": torch.from_numpy(trajectory),513            "x": torch.from_numpy(np.asarray(group["x"][:], dtype=np.float32)),514            "t": torch.from_numpy(np.asarray(group["t"][:], dtype=np.float32)),515            "params": torch.from_numpy(params),516            "index": torch.tensor(index, dtype=torch.long),517        }518 519    def close(self) -> None:520        if self._handle is not None:521            self._handle.close()522            self._handle = None523 524    def __del__(self) -> None:525        self.close()526 527 528def _load_yaml(path: Path) -> Dict[str, Any]:529    with path.open("r", encoding="utf-8") as stream:530        config = yaml.safe_load(stream)531    if not isinstance(config, dict):532        raise ValueError(f"Config must contain a mapping: {path}")533    return config534 535 536def main() -> None:537    project_root = Path(__file__).resolve().parents[1]538    parser = argparse.ArgumentParser(description="Generate the MP-PDE E3 HDF5 dataset")539    parser.add_argument("--config", type=Path, default=project_root / "config/config.yaml")540    parser.add_argument("--output", type=Path, default=None)541    parser.add_argument("--overwrite", action="store_true")542    parser.add_argument("--train-samples", type=int)543    parser.add_argument("--valid-samples", type=int)544    parser.add_argument("--test-samples", type=int)545    parser.add_argument("--nt", type=int)546    parser.add_argument("--high-resolution-nx", type=int)547    parser.add_argument("--resolution", type=int)548    parser.add_argument("--workers", type=int)549    parser.add_argument("--max-in-flight", type=int)550    parser.add_argument("--flush-every", type=int)551    parser.add_argument("--no-resume-partial", action="store_true")552    args = parser.parse_args()553    config = _load_yaml(args.config.resolve())554    configured = Path(config["paths"]["data"])555    output = args.output or (configured if configured.is_absolute() else project_root / configured)556    default_counts = {name: int(config["data"][f"{name}_samples"]) for name in SPLITS}557    counts = {558        "train": args.train_samples or default_counts["train"],559        "valid": args.valid_samples or default_counts["valid"],560        "test": args.test_samples or default_counts["test"],561    }562    generated = generate_e3_hdf5(563        config, output, sample_counts=counts, nt=args.nt, high_resolution_nx=args.high_resolution_nx,564        resolution=args.resolution, workers=args.workers, max_in_flight=args.max_in_flight,565        flush_every=args.flush_every, resume_partial=False if args.no_resume_partial else None,566        overwrite=args.overwrite,567    )568    print(f"Generated E3 dataset: {generated}", flush=True)569 570 571if __name__ == "__main__":572    main()573