Skip to content

Configuration

Overview

Configuration is defined by config.py and values are stored in YAML files within the src/samudra/configs/ directory. Configuration files can include other configuration files using the !include directive.

Each configuration file is associated with a Pydantic model — you can generate JSON schemas for them with uv run src/samudra/config_schema.py (which is run automatically in pre-commit). To associate a configuration file with a Pydantic model, generate the JSON schema (if it doesn't already exist) and then add this line to the top of the config file:

# yaml-language-server: $schema=path/to/schema.json

This is what the config_schema.py script uses to determine which model to validate against, and also enables autocomplete/type checking in VS Code via the YAML extension.

Command Line Configuration

The train and eval modules accept the configuration file as a positional argument. You can override arbitrary keys on the command line — see --help for details. When overriding an object (as opposed to a single scalar value) via the command line, you can either supply JSON like --data '{"key": "value"}' or a YAML file with a leading @ symbol: --data @src/samudra/configs/data/file.yaml.

Training runs create a YAML file in the checkpoint directory with the final configuration used which you can use to reproduce the run by passing to train e.g. uv run -m samudra.train path/to/config.yaml.

Data locations

Each data source names three stores — data_location, data_means_location, and data_stds_location. Every one of them is a location, which can be written two ways:

  • A relative string (e.g. OM4.zarr). It is resolved against experiment.data_root, so the same data config works against a local copy or a bucket just by changing the root: --experiment.data_root /scratch/om4 or a structured root in the config.
  • A structured, absolute location with an explicit type. This ignores data_root and points at exactly one place. Two types are supported:
# Read a Zarr store directly from S3 (or an S3-compatible endpoint like OSN).
data_location:
  type: s3
  endpoint_url: "https://nyu1.osn.mghpcc.org"  # omit for AWS S3
  anon: true                                   # read a public bucket without credentials
  bucket: m2lines-pubs
  path: Samudra/v2026-07/om4_twodeg/OM4.zarr

# Read a Zarr/NetCDF store from an absolute local path.
data_location:
  type: local
  path: /scratch/om4/OM4.zarr

For a signed request (anon: false, the default) credentials come from the environment (the usual AWS_* variables); the config never carries secrets. Set anon: true to read a public bucket — such as the open datasets on OSN — with no credentials at all, which also avoids a stale AWS_ACCESS_KEY_ID being rejected by a non-AWS endpoint.

src/samudra/configs/data/om4_demo.yaml is a ready-made source that streams the public 2° OM4 dataset from OSN over S3 with no local download and no credentials. Because its locations are absolute, experiment.data_root is unused and can be omitted entirely. The same --data flag works across train, eval, and viz — for viz it names the ground-truth source (groundtruth_location defaults to the source's data_location):

samudra train samudra_om4/train.yaml --data @data/om4_demo.yaml
samudra eval  samudra_om4/eval.yaml  --data @data/om4_demo.yaml
samudra viz   samudra_om4/viz.yaml   --data @data/om4_demo.yaml

API Reference

samudra.config

JulianDate(value)

Represents a Julian date as a cftime.datetime at noon on the relevant day.

This is the format the OM4 data uses, so we match that here. TODO(jder): probably worth asserting the date format when opening the data.

Source code in src/samudra/config.py
def __init__(self, value: str):
    parsed = cftime.datetime.strptime(value, "%Y-%m-%d", calendar="julian")
    self.datetime = parsed.replace(hour=12)

PerceiverConfig

Bases: BaseConfig

A standard config interface to various perceiver implementations.

Builds either a regular Perceiver (for the encoder, via build) or a PerceiverIO (for the decoder, via build_io). Both respect the shared implementation setting from SamudraMultiConfig.perceiver_implementation.

build(in_channels, out_channels, max_patch_size, implementation)

Build a regular Perceiver (used by the encoder).

Source code in src/samudra/config.py
def build(
    self,
    in_channels: int,
    out_channels: int,
    max_patch_size: tuple[int, int],
    implementation: PerceiverImpl,
) -> nn.Module:
    """Build a regular Perceiver (used by the encoder)."""
    # This is not really a "frequency" but a maximum of the width appears to be reasonable from looking at the code.
    max_freq = max(*max_patch_size)

    num_freq_bands = 4
    fourier_dim = fourier_features_2d_dim(num_freq_bands)
    # Use the same explicit 2D Fourier features in both implementations so
    # intra-patch positions are encoded equivalently.
    if _use_flash(implementation):
        try:
            from flash_perceiver import Perceiver as FlashPerceiver  # type: ignore
        except ImportError as e:
            raise _flash_import_error() from e
        from einops.layers.torch import Rearrange

        perceiver: nn.Module = nn.Sequential(
            FourierFeatures2D(num_freq_bands=num_freq_bands, max_freq=max_freq),
            Rearrange("b ph pw v -> b (ph pw) v"),
            FlashPerceiver(
                depth=self.depth,
                input_dim=in_channels + fourier_dim,
                output_dim=out_channels,
                output_mode="average",
                latent_dim=self.latent_dim,
                num_latents=self.num_latents,
                use_flash_attn=True,
                weight_tie_layers=True,
                self_per_cross_attn=2,
            ),
        )
    elif _use_naive(implementation):
        perceiver = nn.Sequential(
            FourierFeatures2D(num_freq_bands=num_freq_bands, max_freq=max_freq),
            NaivePerceiver(
                # Required by perceiver-pytorch even when its internal
                # Fourier encoding is disabled below.
                num_freq_bands=num_freq_bands,
                max_freq=max_freq,
                depth=self.depth,
                input_axis=2,
                input_channels=in_channels + fourier_dim,
                num_classes=out_channels,
                latent_dim=self.latent_dim,
                num_latents=self.num_latents,
                weight_tie_layers=True,
                fourier_encode_data=False,
                self_per_cross_attn=2,
            ),
        )
    else:
        raise ValueError(f"Unknown perceiver implementation: {implementation}.")

    return perceiver

build_io(in_channels, queries_dim, out_channels, implementation)

Build a PerceiverIO (used by the decoder).

Source code in src/samudra/config.py
def build_io(
    self,
    in_channels: int,
    queries_dim: int,
    out_channels: int,
    implementation: PerceiverImpl,
) -> nn.Module:
    """Build a PerceiverIO (used by the decoder)."""
    if _use_flash(implementation):
        try:
            from flash_perceiver.perceiver import (  # type: ignore
                PerceiverIO as FlashPerceiverIO,  # type: ignore
            )
        except ImportError as e:
            raise _flash_import_error() from e
        perceiver_io: nn.Module = FlashPerceiverIO(
            depth=self.depth,
            input_dim=in_channels,
            query_dim=queries_dim,
            proj_dim=out_channels,
            num_latents=self.num_latents,
            latent_dim=self.latent_dim,
            use_flash_attn=True,
            weight_tie_layers=True,
        )
    elif _use_naive(implementation):
        from perceiver_pytorch.perceiver_io import PerceiverIO as NaivePerceiverIO

        perceiver_io = NaivePerceiverIO(
            depth=self.depth,
            dim=in_channels,
            queries_dim=queries_dim,
            logits_dim=out_channels,
            num_latents=self.num_latents,
            latent_dim=self.latent_dim,
            weight_tie_layers=True,
            decoder_ff=True,
        )
    else:
        raise ValueError(f"Unknown perceiver implementation: {implementation}.")

    return perceiver_io

DecoderConfig

Bases: BaseConfig

A PerceiverIO-based decoder configuration.

Uses PerceiverIO (with an explicit query mechanism) rather than a regular Perceiver. Output pixel positions are encoded as queries, so the output size is determined by the query count — not by num_latents.

When window_patches is set, the decoder tiles the output grid into spatial blocks of that many patches per side. Each block's PerceiverIO call receives only the overlapping latent tokens plus context_patches extra rings of neighbors, keeping cost bounded even when the latent grid is large (i.e. fine patch_extent).

samudra.config_base

BaseConfig

Bases: BaseModel

Base class for all configs.

TopLevelConfig(*args, **kwargs)

Bases: BaseSettings

Base class for top-level configs (ie tasks like train or eval).

Source code in src/samudra/config_base.py
def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)

from_yaml_and_cli(args_to_parse=None) classmethod

Load config from YAML & CLI with validation.

Source code in src/samudra/config_base.py
@classmethod
def from_yaml_and_cli(
    cls,
    args_to_parse: list[str] | None = None,
) -> Self:
    """Load config from YAML & CLI with validation."""
    parser = argparse.ArgumentParser(
        description=cls.__doc__,
        epilog=textwrap.dedent(
            """
            CONFIG may be a path to a YAML file or, when samudra is installed,
            the name of a bundled preset such as `samudra_om4/train.yaml`.
            YAML files can include other YAML files using the !include tag,
            as in `data: !include ../data/om4.yaml` (resolved relative to the
            including file). You can also replace any JSON argument listed above
            with a YAML file by specifying it with an @ symbol,
            eg `--some_param=@../data/om4.yaml`.
            """
        ),
    )
    parser.add_argument(
        "config",
        type=str,
        help="Path to a config YAML file, or a bundled preset name",
    )

    cli_source = IncludeYamlCliSettingsSource(
        cls,
        root_parser=parser,
        # If args_to_parse is None, we parse argv, which is what `True` does
        cli_parse_args=args_to_parse if args_to_parse is not None else True,
    )

    # We do this after creating CliSettingsSource (which populates the parser)
    # so the help is complete on error.
    args = parser.parse_args(args_to_parse)

    # Then we read the YAML file specified in the CLI. A bare path resolves
    # against the filesystem; if it's missing we try a preset bundled in the
    # installed package before giving up.
    # Note that by default, YamlConfigSettingsSource will ignore missing files
    config_path = resolve_config_path(args.config)
    if not config_path.exists():
        raise FileNotFoundError(
            f"Config file `{args.config}` (full path: {config_path}) not found"
        )
    yaml_values = YamlConfigSettingsSource(cls, yaml_file=config_path)()

    return cls(
        _cli_settings_source=cli_source,
        **yaml_values,
    )

save_yaml(save_path)

Save config to YAML file.

Source code in src/samudra/config_base.py
def save_yaml(self, save_path: Path) -> None:
    """Save config to YAML file."""
    with open(save_path, "w") as f:
        yaml.dump(self.model_dump(), f)

IncludeYamlCliSettingsSource(*args, **kwargs)

Bases: CliSettingsSource

CliSettingsSource which permits @filename.yaml for JSON arguments.

Source code in src/samudra/config_base.py
def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)

register_include_constructor()

Set up yaml.safe_load to include other yaml files via !include.

Source code in src/samudra/config_base.py
def register_include_constructor():
    """Set up yaml.safe_load to include other yaml files via !include."""

    def include_constructor(loader: yaml.Loader, node: yaml.Node) -> Any:
        if hasattr(loader.stream, "name"):
            name = loader.stream.name  # type: ignore
        else:
            raise ValueError(
                "To support includes, you must load a file object, not a string"
            )
        filename = os.path.normpath(os.path.join(os.path.dirname(name), node.value))
        with open(filename) as f:
            return yaml.safe_load(f)

    # This is arguably unsafe, but we don't parse untrusted YAML
    yaml.loader.SafeLoader.add_constructor("!include", include_constructor)

resolve_config_path(config)

Resolve a config argument to a file on disk.

A real filesystem path wins, so a checkout behaves exactly as before. If it doesn't exist, fall back to a preset bundled in the installed package (e.g. samudra_om4/train.yaml), so samudra train samudra_om4/train.yaml works with no checkout. The bundled configs install as real co-located files, so their relative !includes resolve unchanged.

Source code in src/samudra/config_base.py
def resolve_config_path(config: str) -> Path:
    """Resolve a config argument to a file on disk.

    A real filesystem path wins, so a checkout behaves exactly as before. If it
    doesn't exist, fall back to a preset bundled in the installed package (e.g.
    ``samudra_om4/train.yaml``), so ``samudra train samudra_om4/train.yaml``
    works with no checkout. The bundled configs install as real co-located
    files, so their relative ``!include``s resolve unchanged.
    """
    path = Path(config).expanduser()
    if not path.exists():
        bundled = importlib.resources.files("samudra") / "configs" / config
        if bundled.is_file():
            path = Path(str(bundled))
    return path.resolve()