Source code for neural_lam.config

"""Configuration dataclasses and helpers for Neural-LAM experiments."""

# Standard library
import dataclasses
from pathlib import Path
from typing import cast

# Third-party
import dataclass_wizard
import yaml

# Local
from .datastore import (
    DATASTORES,
    MDPDatastore,
    NpyFilesDatastoreMEPS,
    init_datastore,
)


[docs] @dataclasses.dataclass class DatastoreSelection: """ Configuration for selecting a datastore to use with neural-lam. Attributes ---------- kind : str The kind of datastore to use, currently `mdp` or `npyfilesmeps` are implemented. config_path : str The path to the configuration file for the selected datastore, this is assumed to be relative to the configuration file for neural-lam. n_boundary_points : int, optional Overrides :class:`MDPDatastore`'s default boundary-mask width (30), only valid for `kind: mdp`. Set it to 0 when the boundary forcing comes from a separate datastore. """ kind: str config_path: str n_boundary_points: int | None = None def __post_init__(self) -> None: """ Validate that the selected datastore kind is implemented. Raises ------ ValueError If the provided ``kind`` is not part of :data:`DATASTORES`. """ if self.kind not in DATASTORES: raise ValueError(f"Datastore kind {self.kind} is not implemented")
[docs] @dataclasses.dataclass class ManualStateFeatureWeighting: """ Configuration for weighting the state features in the loss function where the weights are manually specified. Attributes ---------- weights : dict[str, float] Manual weights for the state features. """ weights: dict[str, float]
[docs] @dataclasses.dataclass class UniformFeatureWeighting: """ Configuration for weighting the state features in the loss function where all state features are weighted equally. """ pass
[docs] @dataclasses.dataclass class OutputClamping: """ Configuration for clamping the output of the model. Attributes ---------- lower : dict[str, float] The minimum value to clamp each output feature to. upper : dict[str, float] The maximum value to clamp each output feature to. """ lower: dict[str, float] = dataclasses.field(default_factory=dict) upper: dict[str, float] = dataclasses.field(default_factory=dict)
[docs] @dataclasses.dataclass class TrainingConfig: """ Configuration related to training neural-lam Attributes ---------- state_feature_weighting : ManualStateFeatureWeighting | UniformFeatureWeighting The method to use for weighting the state features in the loss function. Defaults to uniform weighting (`UniformFeatureWeighting`, i.e. all features are weighted equally). output_clamping : OutputClamping Per-feature lower / upper clamping bounds applied to the model output. Defaults to an empty ``OutputClamping`` (no clamping). """ state_feature_weighting: ( ManualStateFeatureWeighting | UniformFeatureWeighting ) = dataclasses.field(default_factory=UniformFeatureWeighting) output_clamping: OutputClamping = dataclasses.field( default_factory=OutputClamping )
[docs] @dataclasses.dataclass class NeuralLAMConfig(dataclass_wizard.JSONWizard, dataclass_wizard.YAMLWizard): """ Configuration for the Neural-LAM model and training pipeline. Loads and stores all settings needed to run Neural-LAM, including datastore selection and training hyperparameters. Serialisation and deserialisation from YAML/JSON is handled via ``dataclass_wizard``. Attributes ---------- datastores : dict[str, DatastoreSelection] Mapping from a user-chosen datastore name to its selection config. A datastore's role follows from the categories it provides: one with `state` data is the interior (input and output), one without is input-only (boundary forcing). :func:`load_config_and_datastore` requires exactly one interior and at most one boundary. training : TrainingConfig Configuration for training the model, including loss function and feature-weighting strategy. Defaults to ``TrainingConfig()``. """ datastores: dict[str, DatastoreSelection] training: TrainingConfig = dataclasses.field(default_factory=TrainingConfig) class _(dataclass_wizard.JSONWizard.Meta): """ Define the configuration class as a JSON wizard class. Together `tag_key` and `auto_assign_tags` enable that when a `Union` of types are used for an attribute, the specific type to deserialize to can be specified in the serialised data using the `tag_key` value. In our case we call the tag key `__config_class__` to indicate to the user that they should pick a dataclass describing configuration in neural-lam. This Union-based selection allows us to support different configuration attributes for different choices of methods for example and is used when picking between different feature weighting methods in the `TrainingConfig` class. `auto_assign_tags` is set to True to automatically set that tag key (i.e. `__config_class__` in the config file) should just be the class name of the dataclass to deserialize to. """ tag_key = "__config_class__" auto_assign_tags = True
# ensure that all parts of the loaded configuration match the # dataclasses used # TODO: this should be enabled once # https://github.com/rnag/dataclass-wizard/issues/137 is fixed, but # currently cannot be used together with `auto_assign_tags` due to a # bug it seems # raise_on_unknown_json_key = True
[docs] class InvalidConfigError(Exception): """Raised when the Neural-LAM configuration file is invalid or malformed.""" pass
[docs] def load_config_and_datastore( config_path: str, ) -> tuple[ NeuralLAMConfig, MDPDatastore | NpyFilesDatastoreMEPS, MDPDatastore | NpyFilesDatastoreMEPS | None, ]: """ Load the neural-lam configuration and the datastores specified in the configuration. Parameters ---------- config_path : str Path to the Neural-LAM configuration file. Returns ------- tuple[NeuralLAMConfig, datastore, datastore_boundary] The configuration, the interior datastore (the one with `state` data), and the boundary datastore (the one without, or None). Raises ------ InvalidConfigError If not exactly one datastore provides `state` data, or if more than one omits it. """ with open(config_path, encoding="utf-8") as f: raw_config = yaml.safe_load(f) if isinstance(raw_config, dict) and ( "datastore" in raw_config and "datastores" not in raw_config ): raise InvalidConfigError( "The `datastore:` config key has been replaced by a named " "`datastores:` mapping, move your datastore under a named entry " "(`datastores: {<name>: {kind: ..., config_path: ...}}`)." ) try: config = NeuralLAMConfig.from_yaml_file(config_path) except dataclass_wizard.errors.UnknownJSONKey as ex: raise InvalidConfigError( "There was an error loading the configuration file at " f"{config_path}. " ) from ex # Datastore config paths are relative to the neural-lam config file. config_dir = Path(config_path).parent interior_datastores = {} boundary_datastores = {} for name, selection in config.datastores.items(): datastore = init_datastore( datastore_kind=selection.kind, config_path=config_dir / selection.config_path, n_boundary_points=selection.n_boundary_points, ) if datastore.get_num_data_vars(category="state") > 0: interior_datastores[name] = datastore else: boundary_datastores[name] = datastore if len(interior_datastores) != 1: raise InvalidConfigError( "Exactly one datastore must provide `state` data (the interior " f"domain), but {len(interior_datastores)} were found in " f"{config_path}: {sorted(interior_datastores)}." ) if len(boundary_datastores) > 1: raise InvalidConfigError( "At most one boundary datastore (a datastore without `state` " f"data) is currently supported, but {len(boundary_datastores)} " f"were found in {config_path}: {sorted(boundary_datastores)}." ) (datastore,) = interior_datastores.values() datastore_boundary = next(iter(boundary_datastores.values()), None) return ( config, cast("MDPDatastore | NpyFilesDatastoreMEPS", datastore), cast("MDPDatastore | NpyFilesDatastoreMEPS | None", datastore_boundary), )
[docs] @dataclasses.dataclass class ModelConfig: """ Configuration for neural-lam model architecture. Attributes ---------- model : str, default "graph_lam" Model architecture name. graph : str, default "multiscale" Name of the graph to load from the datastore. hidden_dim : int, default 64 Dimensionality of hidden representations. hidden_layers : int, default 1 Number of hidden layers in MLPs. processor_layers : int, default 4 Number of GNN layers in processor GNN. mesh_aggr : str, default "sum" Aggregation method for m2m GNN layers ("sum" or "mean"). output_std : bool, default False Whether models should output standard deviation per feature. g2m_gnn_type : str, default "InteractionNet" GNN layer type for grid-to-mesh encoding. m2g_gnn_type : str, default "InteractionNet" GNN layer type for mesh-to-grid decoding. mesh_up_gnn_type : str, default "InteractionNet" GNN layer type for upward mesh message passing. mesh_down_gnn_type : str, default "InteractionNet" GNN layer type for downward mesh message passing. """ model: str = "graph_lam" graph: str = "multiscale" hidden_dim: int = 64 hidden_layers: int = 1 processor_layers: int = 4 mesh_aggr: str = "sum" output_std: bool = False g2m_gnn_type: str = "InteractionNet" m2g_gnn_type: str = "InteractionNet" mesh_up_gnn_type: str = "InteractionNet" mesh_down_gnn_type: str = "InteractionNet"
[docs] @dataclasses.dataclass class TrainRunConfig: """ Hyperparameters and settings for a training or evaluation run. Attributes ---------- epochs : int, default 200 Number of training epochs. batch_size : int, default 4 Batch size. ar_steps_train : int, default 1 Autoregressive rollout steps during training. ar_steps_eval : int, default 10 Autoregressive rollout steps during evaluation. loss : str, default "wmse" Loss function to use ("mse" or "wmse"). lr : float, default 1e-3 Learning rate. val_interval : int, default 1 Validation epoch frequency. num_sanity_val_steps : int, default 2 Number of validation batches to run prior to training. val_steps_to_log : list of int, default [1, 2, 3, 5, 10] Rollout steps to log during validation. train_steps_to_log : list of int, default [] Rollout steps to log during training. metrics_watch : list of str, default [] List of watched metrics. var_leads_metrics_watch : dict of {int: list of int}, default {} Mapping from variable index to list of watched rollout steps. load : str or None, default None Checkpoint path to load. restore_opt : bool, default False Whether to restore optimizer state from checkpoint. eval : str or None, default None Evaluation split name ("val", "test") or None if training. n_example_pred : int, default 1 Number of example predictions to plot during testing. create_gif : bool, default False Whether to create animation GIFs during evaluation. """ epochs: int = 200 batch_size: int = 4 ar_steps_train: int = 1 ar_steps_eval: int = 10 loss: str = "wmse" lr: float = 1e-3 val_interval: int = 1 num_sanity_val_steps: int = 2 val_steps_to_log: list[int] = dataclasses.field( default_factory=lambda: [1, 2, 3, 5, 10] ) train_steps_to_log: list[int] = dataclasses.field(default_factory=list) metrics_watch: list[str] = dataclasses.field(default_factory=list) var_leads_metrics_watch: dict[int, list[int]] = dataclasses.field( default_factory=dict ) load: str | None = None restore_opt: bool = False eval: str | None = None n_example_pred: int = 1 create_gif: bool = False
[docs] @dataclasses.dataclass class DataConfig: """ Data loading and forcing parameters. Attributes ---------- num_past_forcing_steps : int, default 1 Number of past forcing time steps as input. num_future_forcing_steps : int, default 1 Number of future forcing time steps as input. num_past_boundary_steps : int, default 1 Number of past boundary forcing time steps as input. num_future_boundary_steps : int, default 1 Number of future boundary forcing time steps as input. num_workers : int, default 4 Number of data loader workers. load_single_member : bool, default False Whether to load only ensemble member 0. """ num_past_forcing_steps: int = 1 num_future_forcing_steps: int = 1 num_past_boundary_steps: int = 1 num_future_boundary_steps: int = 1 num_workers: int = 4 load_single_member: bool = False
[docs] @dataclasses.dataclass class ComputeConfig: """ Compute and accelerator configuration. Attributes ---------- seed : int, default 42 Random seed. num_nodes : int, default 1 Number of cluster nodes. devices : int or str or list of int or list of str, default ["auto"] Hardware devices to use. precision : str or int, default 32 Floating point precision. """ seed: int = 42 num_nodes: int = 1 devices: int | str | list[int] | list[str] = dataclasses.field( default_factory=lambda: ["auto"] ) precision: str | int = 32
[docs] @dataclasses.dataclass class LoggingConfig: """ Experiment tracking and artifact logging configuration. Attributes ---------- logger : str, default "wandb" Logger backend name ("wandb" or "mlflow"). logger_project : str, default "neural_lam" Project name for the logger. logger_run_name : str or None, default None Custom run name for the logger. runs_root : str, default "runs" Root directory for run artifacts and checkpoints. wandb_id : str or None, default None W&B run ID for resuming experiments. """ logger: str = "wandb" logger_project: str = "neural_lam" logger_run_name: str | None = None runs_root: str = "runs" wandb_id: str | None = None