Download model/fourcastnet_v2.py from OneScience-Group/FourCastNet_v2: direct link, hf CLI and curl.
- Browser
- Download file 7.59 kB
-
https://huggingface.co/OneScience-Group/FourCastNet_v2/resolve/main/model/fourcastnet_v2.py
- Command line
-
hf download hf://OneScience-Group/FourCastNet_v2/model/fourcastnet_v2.py
-
curl -L -o fourcastnet_v2.py https://huggingface.co/OneScience-Group/FourCastNet_v2/resolve/main/model/fourcastnet_v2.py
7.59 kB
| """OneScience adapter for NVIDIA's official legacy FourCastNet v2 network.""" | |
| from __future__ import annotations | |
| import sys | |
| from pathlib import Path | |
| from types import SimpleNamespace | |
| from typing import Any, Mapping | |
| import torch | |
| from torch import nn | |
| def _load_model_class(): | |
| package_dir = Path(__file__).resolve().parent / "fcnv2" | |
| if not (package_dir / "fcnv2_sfnonet.py").is_file(): | |
| raise FileNotFoundError( | |
| f"Bundled FCNv2 source was not found at {package_dir}" | |
| ) | |
| package_path = str(package_dir) | |
| if package_path not in sys.path: | |
| sys.path.insert(0, package_path) | |
| try: | |
| from fcnv2_sfnonet import FourierNeuralOperatorNet | |
| except ModuleNotFoundError as error: | |
| if error.name == "torch_harmonics": | |
| raise ModuleNotFoundError( | |
| "FourCastNet v2 requires NVIDIA torch-harmonics. Install the " | |
| "version pinned by this project before constructing the model." | |
| ) from error | |
| raise | |
| return FourierNeuralOperatorNet | |
| def _official_params(model_config: Mapping[str, Any]) -> SimpleNamespace: | |
| required = { | |
| "img_size", | |
| "in_channels", | |
| "out_channels", | |
| "spectral_transform", | |
| "filter_type", | |
| "scale_factor", | |
| "embed_dim", | |
| "num_layers", | |
| "num_blocks", | |
| "normalization_layer", | |
| "mlp_mode", | |
| "spectral_layers", | |
| "complex_activation", | |
| "hard_thresholding_fraction", | |
| "big_skip", | |
| } | |
| missing = sorted(required.difference(model_config)) | |
| if missing: | |
| raise ValueError(f"Missing FourCastNet v2 model settings: {missing}") | |
| height, width = model_config["img_size"] | |
| hidden_height = height // model_config["scale_factor"] | |
| hidden_width = width // model_config["scale_factor"] | |
| if hidden_height < 2 or hidden_width < 2: | |
| raise ValueError("The internal SFNO grid must have at least 2 x 2 points") | |
| return SimpleNamespace( | |
| img_crop_shape_x=int(height), | |
| img_crop_shape_y=int(width), | |
| N_in_channels=int(model_config["in_channels"]), | |
| N_out_channels=int(model_config["out_channels"]), | |
| spectral_transform=model_config["spectral_transform"], | |
| filter_type=model_config["filter_type"], | |
| scale_factor=int(model_config["scale_factor"]), | |
| embed_dim=int(model_config["embed_dim"]), | |
| num_layers=int(model_config["num_layers"]), | |
| num_blocks=int(model_config["num_blocks"]), | |
| normalization_layer=model_config["normalization_layer"], | |
| mlp_mode=model_config["mlp_mode"], | |
| spectral_layers=int(model_config["spectral_layers"]), | |
| complex_activation=model_config["complex_activation"], | |
| hard_thresholding_fraction=float( | |
| model_config["hard_thresholding_fraction"] | |
| ), | |
| big_skip=bool(model_config["big_skip"]), | |
| ) | |
| class FourCastNetV2(nn.Module): | |
| """Build the exact official FCNv2 network behind a stable project API.""" | |
| def __init__( | |
| self, | |
| model_config: Mapping[str, Any], | |
| ) -> None: | |
| super().__init__() | |
| self.model_config = dict(model_config) | |
| self.expected_shape = ( | |
| int(model_config["in_channels"]), | |
| int(model_config["img_size"][0]), | |
| int(model_config["img_size"][1]), | |
| ) | |
| model_class = _load_model_class() | |
| self.model = model_class(_official_params(model_config)) | |
| def forward(self, inputs: torch.Tensor) -> torch.Tensor: | |
| if inputs.ndim != 4: | |
| raise ValueError(f"Expected [B,C,H,W], got {tuple(inputs.shape)}") | |
| if tuple(inputs.shape[1:]) != self.expected_shape: | |
| raise ValueError( | |
| f"Expected trailing shape {self.expected_shape}, " | |
| f"got {tuple(inputs.shape[1:])}" | |
| ) | |
| return self.model(inputs) | |
| def no_weight_decay(self) -> set[str]: | |
| return {f"model.{name}" for name in self.model.no_weight_decay()} | |
| def _unwrap_state_dict(checkpoint: Any) -> Mapping[str, torch.Tensor]: | |
| if not isinstance(checkpoint, Mapping): | |
| raise TypeError("Checkpoint must contain a mapping") | |
| for key in ("model_state", "model_state_dict", "state_dict"): | |
| candidate = checkpoint.get(key) | |
| if isinstance(candidate, Mapping): | |
| return candidate | |
| if checkpoint and all(isinstance(value, torch.Tensor) for value in checkpoint.values()): | |
| return checkpoint | |
| raise KeyError("Checkpoint has no recognized model state mapping") | |
| def _normalize_state_keys( | |
| state_dict: Mapping[str, torch.Tensor], model: nn.Module | |
| ) -> dict[str, torch.Tensor]: | |
| target_keys = set(model.state_dict()) | |
| normalized: dict[str, torch.Tensor] = {} | |
| for key, value in state_dict.items(): | |
| clean_key = key | |
| while clean_key.startswith("module."): | |
| clean_key = clean_key[len("module.") :] | |
| if clean_key not in target_keys and f"model.{clean_key}" in target_keys: | |
| clean_key = f"model.{clean_key}" | |
| normalized[clean_key] = value | |
| return normalized | |
| def load_checkpoint( | |
| model: nn.Module, | |
| checkpoint_path: str | Path, | |
| *, | |
| expected_profile: str, | |
| expected_variables: list[str], | |
| allowed_stages: set[str], | |
| allowed_initializations: set[str], | |
| strict: bool = True, | |
| map_location: str | torch.device = "cpu", | |
| ) -> dict[str, Any]: | |
| """Load a project checkpoint without changing its parameter tensors.""" | |
| checkpoint = torch.load( | |
| Path(checkpoint_path).expanduser(), | |
| map_location=map_location, | |
| weights_only=False, | |
| ) | |
| validate_project_checkpoint( | |
| checkpoint, | |
| expected_profile=expected_profile, | |
| expected_variables=expected_variables, | |
| allowed_stages=allowed_stages, | |
| allowed_initializations=allowed_initializations, | |
| ) | |
| state_dict = _normalize_state_keys(_unwrap_state_dict(checkpoint), model) | |
| incompatible = model.load_state_dict(state_dict, strict=strict) | |
| return { | |
| "checkpoint": checkpoint, | |
| "missing_keys": list(incompatible.missing_keys), | |
| "unexpected_keys": list(incompatible.unexpected_keys), | |
| } | |
| def validate_project_checkpoint( | |
| checkpoint: Any, | |
| *, | |
| expected_profile: str, | |
| expected_variables: list[str], | |
| allowed_stages: set[str], | |
| allowed_initializations: set[str], | |
| ) -> None: | |
| if not isinstance(checkpoint, Mapping): | |
| raise TypeError("Project checkpoint must contain metadata") | |
| expected = { | |
| "checkpoint_format": "fourcastnet_v2_project", | |
| "scratch_lineage": True, | |
| "model_profile": expected_profile, | |
| "variables": expected_variables, | |
| } | |
| for key, value in expected.items(): | |
| if checkpoint.get(key) != value: | |
| raise ValueError( | |
| f"Checkpoint metadata {key!r} does not match the project config" | |
| ) | |
| if checkpoint.get("stage") not in allowed_stages: | |
| raise ValueError( | |
| f"Checkpoint stage must be one of {sorted(allowed_stages)}" | |
| ) | |
| if checkpoint.get("initialization") not in allowed_initializations: | |
| raise ValueError( | |
| "Checkpoint does not have an approved random-initialization lineage" | |
| ) | |
| expected_initialization = { | |
| "one_step": "random", | |
| "finetune": "one_step_checkpoint", | |
| }.get(checkpoint.get("stage")) | |
| if checkpoint.get("initialization") != expected_initialization: | |
| raise ValueError( | |
| "Checkpoint stage and initialization metadata are inconsistent" | |
| ) | |