from __future__ import annotations import random from collections.abc import Mapping from pathlib import Path from typing import Any import numpy as np import torch import yaml class AttrDict(dict): """Dictionary with recursive attribute access for OneScience datapipes.""" def __getattr__(self, key: str) -> Any: try: return self[key] except KeyError as error: raise AttributeError(key) from error def __setattr__(self, key: str, value: Any) -> None: self[key] = value def to_attr_dict(value: Any) -> Any: if isinstance(value, Mapping): return AttrDict({key: to_attr_dict(item) for key, item in value.items()}) if isinstance(value, list): return [to_attr_dict(item) for item in value] return value def to_plain_dict(value: Any) -> Any: if isinstance(value, Mapping): return {key: to_plain_dict(item) for key, item in value.items()} if isinstance(value, list): return [to_plain_dict(item) for item in value] return value def load_config(path: Path) -> dict[str, Any]: with path.open("r", encoding="utf-8") as stream: config = yaml.safe_load(stream) if not isinstance(config, dict) or not isinstance(config.get("root"), dict): raise ValueError(f"Config must contain a root mapping: {path}") return config["root"] def project_path(value: str | Path, project_root: Path) -> Path: path = Path(value).expanduser() return path if path.is_absolute() else project_root / path def resolve_device(requested: str) -> torch.device: if requested == "auto": return torch.device("cuda:0" if torch.cuda.is_available() else "cpu") device = torch.device(requested) if device.type == "cuda" and not torch.cuda.is_available(): raise RuntimeError("CUDA/DCU was requested but torch.cuda.is_available() is false") return device def seed_everything(seed: int) -> None: random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) def rollout( model: torch.nn.Module, pos: torch.Tensor, state: torch.Tensor, t_out: int, out_dim: int, max_latent_steps: int, ) -> torch.Tensor: predictions = [] remaining = int(t_out) while remaining > 0: step_count = min(int(max_latent_steps), remaining) prediction = model(pos, state, latent_steps=step_count) predictions.append(prediction) consumed = step_count * int(out_dim) state = torch.cat((state[..., consumed:], prediction), dim=-1) remaining -= step_count return torch.cat(predictions, dim=-1) def relative_l2(prediction: torch.Tensor, target: torch.Tensor) -> torch.Tensor: prediction = prediction.reshape(prediction.shape[0], -1) target = target.reshape(target.shape[0], -1) numerator = torch.linalg.vector_norm(prediction - target, dim=1) denominator = torch.linalg.vector_norm(target, dim=1).clamp_min(1.0e-8) return numerator / denominator